option on, if the user didn't verify his email address yet, a verification email is automatically sent. This means that the user could spam the sign-in button and/or route
How to limit the number of requests a user can make to the server in a given time period.
Solution
I ended up doing exactly that, creating a new column in my db. This is what is looks like:
const throwInvalidCredsError = (isEmail: boolean) => { throw new APIError("FORBIDDEN", { code: isEmail ? "INVALID_EMAIL_OR_PASSWORD" : "INVALID_USERNAME_OR_PASSWORD", message: isEmail ? "Invalid email or password" : "Invalid username or password", })}export const beforeHook = createAuthMiddleware(async (ctx) => { /** * Before signing-in with email or username, we need to check 2 things: * * 1. If the user is trying to sign in using an email or username from a non-credential provider * 2. If the user is trying to sign in to an account that is not verified and which has a pending verification email */ if (ctx.path === "/sign-in/email" || ctx.path === "/sign-in/username") { const user = await db.user.findFirst({ where: ctx.body.email ? { email: ctx.body.email } : { username: ctx.body.username }, }) if (!user) { return } const accounts = await db.account.findMany({ where: { userId: user.id } }) // Tried logging in with an email or username from a non-credential provider if (!accounts.some((a) => a.providerId === "credential")) { throwInvalidCredsError(ctx.body.email !== undefined) } const account = accounts.find((a) => a.providerId === "credential")! const isCorrectPassword = await ctx.context.password.verify({ password: ctx.body.password, hash: account.password!, }) if (!isCorrectPassword) { throwInvalidCredsError(ctx.body.email !== undefined) } // Prevent sending emails too often if (user.verificationEmailSentAt) { const lastVerificationSentAt = DateTime.fromJSDate( user.verificationEmailSentAt, ) const timeLimit = DateTime.now().minus({ hours: 1 }) if (lastVerificationSentAt > timeLimit) { throw new APIError("FORBIDDEN", { code: "VERIFICATION_EMAIL_ALREADY_SENT", message: "Verification email already sent", }) } } await db.user.update({ where: { id: user.id }, data: { verificationEmailSentAt: new Date() }, }) }})
const throwInvalidCredsError = (isEmail: boolean) => { throw new APIError("FORBIDDEN", { code: isEmail ? "INVALID_EMAIL_OR_PASSWORD" : "INVALID_USERNAME_OR_PASSWORD", message: isEmail ? "Invalid email or password" : "Invalid username or password", })}export const beforeHook = createAuthMiddleware(async (ctx) => { /** * Before signing-in with email or username, we need to check 2 things: * * 1. If the user is trying to sign in using an email or username from a non-credential provider * 2. If the user is trying to sign in to an account that is not verified and which has a pending verification email */ if (ctx.path === "/sign-in/email" || ctx.path === "/sign-in/username") { const user = await db.user.findFirst({ where: ctx.body.email ? { email: ctx.body.email } : { username: ctx.body.username }, }) if (!user) { return } const accounts = await db.account.findMany({ where: { userId: user.id } }) // Tried logging in with an email or username from a non-credential provider if (!accounts.some((a) => a.providerId === "credential")) { throwInvalidCredsError(ctx.body.email !== undefined) } const account = accounts.find((a) => a.providerId === "credential")! const isCorrectPassword = await ctx.context.password.verify({ password: ctx.body.password, hash: account.password!, }) if (!isCorrectPassword) { throwInvalidCredsError(ctx.body.email !== undefined) } // Prevent sending emails too often if (user.verificationEmailSentAt) { const lastVerificationSentAt = DateTime.fromJSDate( user.verificationEmailSentAt, ) const timeLimit = DateTime.now().minus({ hours: 1 }) if (lastVerificationSentAt > timeLimit) { throw new APIError("FORBIDDEN", { code: "VERIFICATION_EMAIL_ALREADY_SENT", message: "Verification email already sent", }) } } await db.user.update({ where: { id: user.id }, data: { verificationEmailSentAt: new Date() }, }) }})