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

94 lines
2.6 KiB
TypeScript

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<typeof BodySchema> }> = 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,
},
}