brf/server/plugins/auth/routes/change_password.ts
2025-12-18 07:31:37 +01:00

84 lines
2.5 KiB
TypeScript

import type { RouteHandler } from 'fastify'
import { Type, type Static } from '@fastify/type-provider-typebox'
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 = Type.Object({
email: Type.String({ format: 'email' }),
password: Type.String(),
token: Type.String(),
})
const ResponseSchema = {
204: {},
'4XX': StatusErrorSchema,
}
const changePassword: RouteHandler<{ Body: Static<typeof BodySchema> }> = 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,
},
}