brf/server/plugins/auth/routes/verify_email.ts
2025-12-18 10:20:02 +01:00

71 lines
1.8 KiB
TypeScript

import type { RouteHandler } from 'fastify'
import * as z from 'zod'
import config from '../../../config.ts'
import StatusError from '../../../lib/status_error.ts'
import { StatusErrorSchema } from '../../../schemas/status_error.ts'
import { sql } from 'kysely'
const { errors, timeouts } = config.auth
const QuerystringSchema = z.object({
email: z.email(),
token: z.string(),
})
const ResponseSchema = {
200: {},
'4XX': StatusErrorSchema,
}
const verifyEmail: RouteHandler<{ Querystring: z.infer<typeof QuerystringSchema> }> = async function (request, reply) {
const { db } = request.server
try {
const { token, email } = request.query
const emailToken = await db
.selectFrom('emailToken')
.selectAll()
.where((eb) => eb.and({ email, token }))
.executeTakeFirst()
if (!emailToken) {
throw new StatusError(...errors.tokenNotFound)
} else if (emailToken.consumedAt) {
throw new StatusError(...errors.tokenConsumed)
} else if (Date.now() > emailToken.createdAt.getTime() + timeouts.verifyEmail) {
throw new StatusError(...errors.tokenExpired)
}
await db.transaction().execute((trx) =>
Promise.all([
trx
.updateTable('user')
.set({ emailVerifiedAt: sql`now()` })
.where('id', '=', emailToken.userId)
.execute(),
trx
.updateTable('emailToken')
.set({ consumedAt: sql`now()` })
.where('id', '=', emailToken.id)
.execute(),
]),
)
// TODO better template
return reply.status(200).type('text/html').send('<div>Email Verified!</div>')
} catch (err) {
this.log.error(err)
throw err
}
}
export default {
handler: verifyEmail,
schema: {
querystring: QuerystringSchema,
response: ResponseSchema,
},
}