import type { RouteHandler } from 'fastify' import * as z from 'zod' import config from '../../../config.ts' import knex from '../../../lib/knex.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, ) { try { if (!request.body.email || !request.body.password || !request.body.token) { throw new StatusError(...errors.missingParameters) } const [token, latestToken] = await Promise.all([ knex('passwordToken as pt') .first('pt.id', 'u.id as userId', 'u.email', 'pt.token', 'pt.createdAt', 'pt.cancelledAt', 'pt.consumedAt') .innerJoin('user as u', 'pt.userId', 'u.id') .where({ 'u.email': request.body.email, 'pt.token': request.body.token, }), knex('passwordToken as pt') .first('pt.id') .innerJoin('user as u', 'pt.userId', 'u.id') .where({ 'u.email': request.body.email, }) .orderBy('pt.createdAt', 'desc') .limit(1), ]) 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 knex.transaction((trx) => Promise.all([ trx('user').update({ password: hash }).where('id', token.userId), trx('password_token').update({ consumedAt: knex.fn.now() }).where('id', token.id), ]), ) 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, }, }