import type { RouteHandler } from 'fastify' import * as z from 'zod' import { sql } from 'kysely' import config from '../../../config.ts' import StatusError from '../../../lib/status_error.ts' import { hashPassword } from '../helpers.ts' import { StatusErrorSchema } from '../../../schemas/status_error.ts' const { errors, timeouts } = config.auth const BodySchema = z.object({ email: z.email(), password: z.string(), token: z.string(), }) const ResponseSchema = { 204: {}, '4XX': StatusErrorSchema, } const changePassword: RouteHandler<{ Body: z.infer }> = async function changePassword( request, reply, ) { const { db } = request.server try { if (!request.body.email || !request.body.password || !request.body.token) { throw new StatusError(...errors.missingParameters) } const [token, latestToken] = await Promise.all([ db .selectFrom('passwordToken as pt') .innerJoin('user as u', 'pt.userId', 'u.id') .select(['pt.id', 'u.id as userId', 'u.email', 'pt.token', 'pt.createdAt', 'pt.cancelledAt', 'pt.consumedAt']) .where((eb) => eb.and({ 'u.email': request.body.email, 'pt.token': request.body.token, }), ) .executeTakeFirst(), db .selectFrom('passwordToken as pt') .innerJoin('user as u', 'pt.userId', 'u.id') .select('pt.id') .where('u.email', '=', request.body.email) .orderBy('pt.createdAt', 'desc') .limit(1) .executeTakeFirst(), ]) if (!token) { throw new StatusError(...errors.tokenNotFound) } else if (token.id !== latestToken!.id) { throw new StatusError(...errors.tokenNotLatest) } else if (token.consumedAt) { throw new StatusError(...errors.tokenConsumed) } else if (token.cancelledAt || Date.now() > token.createdAt.getTime() + timeouts.changePassword) { throw new StatusError(...errors.tokenExpired) } const hash = await hashPassword(request.body.password) await db.transaction().execute((trx) => Promise.all([ trx.updateTable('user').set({ password: hash }).where('id', '=', token.userId).execute(), trx .updateTable('passwordToken') .set({ consumedAt: sql`now()` }) .where('id', '=', token.id) .execute(), ]), ) return reply.status(204).send() } catch (err) { request.log.error(err) if (err instanceof StatusError) return reply.status(err.status).send(err.toJSON()) else throw err } } export default { handler: changePassword, schema: { body: BodySchema, response: ResponseSchema, }, }