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

152 lines
4.6 KiB
TypeScript

import _ from 'lodash'
import { Type, type Static } from '@fastify/type-provider-typebox'
import type { RouteHandler } from 'fastify'
import config from '../../../config.ts'
import emitter from '../../../lib/emitter.ts'
import knex from '../../../lib/knex.ts'
import AdmissionQueries from '../../../services/admissions/queries.ts'
import InviteQueries from '../../../services/invites/queries.ts'
import UserQueries from '../../../services/users/queries.ts'
import verifyEmailTemplate from '../../../templates/emails/verify_email.ts'
import sendMail from '../../../lib/send_mail.ts'
import StatusError from '../../../lib/status_error.ts'
import { generateToken, hashPassword } from '../helpers.ts'
import type { Invite } from '../../../services/invites/types.ts'
import type { Role } from '../../../services/roles/types.ts'
const admissionQueries = AdmissionQueries({ knex, emitter })
const inviteQueries = InviteQueries({ knex, emitter })
const userQueries = UserQueries({ knex, emitter })
const { errors, timeouts } = config.auth
const BodySchema = Type.Object({
email: Type.String({ format: 'email' }),
password: Type.String(),
inviteEmail: Type.Optional(Type.String({ format: 'email' })),
inviteToken: Type.Optional(Type.String()),
// required: config.auth.requireInvite ? ['email', 'password', 'inviteEmail', 'inviteToken'] : ['email', 'password'],
})
const ResponseSchema = {
201: {
type: 'object',
properties: {
id: { type: 'integer' },
email: { type: 'string' },
password: { type: 'string' },
emailVerifiedAt: { type: 'string' },
bannedAt: { type: 'string' },
blockedAt: { type: 'string' },
loginAttempts: { type: 'integer' },
},
},
'4XX': { $ref: 'status-error' },
}
const register: RouteHandler<{ Body: Static<typeof BodySchema> }> = async function (request, reply) {
try {
// TODO validate and ensure same as confirm
const email = request.body.email.trim().toLowerCase()
let invite: Invite | null = null
if (request.body.inviteEmail && request.body.inviteToken) {
invite = await inviteQueries.findOne({ email: request.body.inviteEmail, token: request.body.inviteToken })
const latestInvite = await inviteQueries.findOne({
email: request.body.inviteEmail,
sort: [{ column: 'createdAt', order: 'desc' }],
})
if (!invite) {
throw new StatusError(...errors.tokenNotFound)
} else if (invite.id !== latestInvite.id) {
throw new StatusError(...errors.tokenNotLatest)
} else if (Date.now() > new Date(invite.createdAt).getTime() + timeouts.invite) {
throw new StatusError(...errors.tokenExpired)
} else if (invite.consumedAt) {
throw new StatusError(...errors.tokenConsumed)
}
}
const admissions = await admissionQueries.findMatches(email)
const roles = Array.from(
new Set(
[
invite?.roles ? invite.roles.map((role) => role.id) : [],
...admissions.map((admission) => (admission.roles ? admission.roles.map((role: Role) => role.id) : [])),
].flat(),
),
)
if (!roles.length) {
throw new StatusError(...errors.notAuthorized)
}
const user = await knex.transaction(async (trx) => {
const user = await userQueries.create(
{
email,
password: await hashPassword(request.body.password),
roles,
emailVerifiedAt: invite && invite.email === email ? (knex.fn.now() as unknown as string) : null,
},
trx,
)
if (invite) {
await inviteQueries.consume(invite.id, user.id, trx)
}
if (!user.emailVerifiedAt) {
const token = generateToken()
await trx('email_token').insert({
user_id: user.id,
email: user.email,
token,
})
const link = `${new URL('/auth/verify-email', config.site.url)}?email=${user.email}&token=${token}`
await sendMail({
to: user.email,
subject: `Verify ${config.site.title} account`,
html: await verifyEmailTemplate({ link }).text(),
})
}
return userQueries.findById(user.id)
})
reply.type('application/json')
return reply.status(201).send(_.omit(user, 'password'))
} catch (err) {
this.log.error(err)
// @ts-ignore
if (err.code == 23505) {
err = new StatusError(...errors.duplicateEmail)
}
if (err instanceof StatusError) {
return reply
.status(err.status || 500)
.type('application/json')
.send(err.toJSON())
} else {
throw err
}
}
}
export default {
handler: register,
schema: {
body: BodySchema,
resopnse: ResponseSchema,
},
}