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 }> = 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('
Email Verified!
') } catch (err) { this.log.error(err) throw err } } export default { handler: verifyEmail, schema: { querystring: QuerystringSchema, response: ResponseSchema, }, }