Next 14 + Lucia v3 + SQLite + Drizzle + Conform + Zod + Email Verification using OTP with Next.js Server Actions & Resend
- stars
- 59
- watchers
- 1
- forks
- 5
- open issues
- 2
- Last updated
- 03/11/2024
Next 14 + Lucia v3 + SQLite + Drizzle + Conform + Zod + Email Verification using OTP with Next.js Server Actions & Resend
.env.example file to .env filecp .env.example .env # duplicate .env.example & name it .envDATABASE_PATH to the filename of your choiceDATABASE_PATH=sqlite.db
pnpm install
pnpm start
pnpm db:push
pnpm db:generate
https://redis.io/docs/install/install-redis/
Note: You can disable it by removing Redis Specific Code &
middleware.ts
Since it is a simple example, I will go with IP-based blocking for 1 day using browser finderprinting library.
Go to https://hoppscotch.io/ (or https://www.postman.com/)
Select POST & enter http://localhost:3000/signup in URL.
Go to Headers & add Next-Action to 948fdf27b221db98253b47aa8f8d1c589c93e063 as well as Origin to localhost:3000 & click on Send 10 times to see the error.
The scripts are in rate-limit folder. Both for some reason send 2 requests & it always gives Email is not unique error since 1st request adds it to the database. Its a nasty bug in either Puppeteer/Playwright or my scripts. But definitely not in my email signup process. It works well manually.
Tried it with Next-Action as a header but that doesn't work either.
Most of the issues are with Next.js 14 Server Actions that I stumbled upon only while automating rate limits.
I don't think its ready for prime time yet so I'm gonna use API routes on my main project.
If you want to use this repo, check out 2 commits behind this as that works without any errors.
import { NextRequest, NextResponse } from 'next/server' import { RateLimiterMemory } from 'rate-limiter-flexible' const opts = { points: 10, duration: 5, // Per second } const rateLimiter = new RateLimiterMemory(opts) export default async function middleware(request: NextRequest) { if (request.method === 'POST') { let res: any try { res = await rateLimiter.consume(2) console.log({ res }) } catch (error) { res = error } if (res._remainingPoints > 0) { return NextResponse.next() } else { return NextResponse.json( { error: 'Rate limit exceeded. Please try again later.', }, { status: 429, }, ) } } } export const config = { matcher: ['/api/login'], }
const LOCALHOST_URL = 'http://localhost:3000' async function main() { for (let i = 0; i < 20; i++) { const url = `${LOCALHOST_URL}/api/login` const email = `test${i}@example.com` const body = JSON.stringify({ email }) const response = await fetch(url, { method: 'POST', body }) const data = await response.json() console.log({ data }) } } main()
Run Next.js dev server using pnpm dev in one terminal & brute-force the login api in another using pnpm ratelimit:login & it'll show this output in ratelimit terminal:
{ data: { success: false } }
{ data: { success: false } }
{ data: { success: false } }
{ data: { success: false } }
{ data: { success: false } }
{ data: { success: false } }
{ data: { success: false } }
{ data: { success: false } }
{ data: { success: false } }
{ data: { error: 'Rate limit exceeded. Please try again later.' } }
{ data: { error: 'Rate limit exceeded. Please try again later.' } }
{ data: { error: 'Rate limit exceeded. Please try again later.' } }
{ data: { error: 'Rate limit exceeded. Please try again later.' } }
{ data: { error: 'Rate limit exceeded. Please try again later.' } }
{ data: { error: 'Rate limit exceeded. Please try again later.' } }
{ data: { error: 'Rate limit exceeded. Please try again later.' } }
{ data: { error: 'Rate limit exceeded. Please try again later.' } }
{ data: { error: 'Rate limit exceeded. Please try again later.' } }
{ data: { error: 'Rate limit exceeded. Please try again later.' } }
{ data: { error: 'Rate limit exceeded. Please try again later.' } }And this output in dev server terminal:
{
res: RateLimiterRes {
_remainingPoints: 9,
_msBeforeNext: 5000,
_consumedPoints: 1,
_isFirstInDuration: true
}
}
[19:49:26.652] INFO (9732): 🏁 POST /api/login/route
{
res: RateLimiterRes {
_remainingPoints: 8,
_msBeforeNext: 4940,
_consumedPoints: 2,
_isFirstInDuration: false
}
}
[19:49:26.706] INFO (9732): 🏁 POST /api/login/route
{
res: RateLimiterRes {
_remainingPoints: 7,
_msBeforeNext: 4898,
_consumedPoints: 3,
_isFirstInDuration: false
}
}
[19:49:26.750] INFO (9732): 🏁 POST /api/login/route
{
res: RateLimiterRes {
_remainingPoints: 6,
_msBeforeNext: 4855,
_consumedPoints: 4,
_isFirstInDuration: false
}
}
[19:49:26.793] INFO (9732): 🏁 POST /api/login/route
{
res: RateLimiterRes {
_remainingPoints: 5,
_msBeforeNext: 4810,
_consumedPoints: 5,
_isFirstInDuration: false
}
}
[19:49:26.839] INFO (9732): 🏁 POST /api/login/route
{
res: RateLimiterRes {
_remainingPoints: 4,
_msBeforeNext: 4765,
_consumedPoints: 6,
_isFirstInDuration: false
}
}
[19:49:26.882] INFO (9732): 🏁 POST /api/login/route
{
res: RateLimiterRes {
_remainingPoints: 3,
_msBeforeNext: 4721,
_consumedPoints: 7,
_isFirstInDuration: false
}
}
[19:49:26.931] INFO (9732): 🏁 POST /api/login/route
{
res: RateLimiterRes {
_remainingPoints: 2,
_msBeforeNext: 4672,
_consumedPoints: 8,
_isFirstInDuration: false
}
}
[19:49:26.977] INFO (9732): 🏁 POST /api/login/route
{
res: RateLimiterRes {
_remainingPoints: 1,
_msBeforeNext: 4625,
_consumedPoints: 9,
_isFirstInDuration: false
}
}
[19:49:27.022] INFO (9732): 🏁 POST /api/login/route
{
res: RateLimiterRes {
_remainingPoints: 0,
_msBeforeNext: 4583,
_consumedPoints: 10,
_isFirstInDuration: false
}
}It only works with API routes. Idk how to make it work for Server Actions. I tried Puppeteer/Playwright but for some reason, it calls login api twice & halts the process because of not unique email.