84 lines
2.4 KiB
TypeScript
84 lines
2.4 KiB
TypeScript
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<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,
|
|
},
|
|
}
|