brf/server/routes/api/invites.ts

135 lines
3.4 KiB
TypeScript

import * as z from 'zod'
import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod'
import { jsonArrayFrom } from 'kysely/helpers/postgres'
import config from '../../config.ts'
import sendMail from '../../lib/send_mail.ts'
import { generateToken } from '../../plugins/auth/helpers.ts'
import inviteEmailTemplate from '../../templates/emails/invite.ts'
import { InviteSchema, RoleSchema } from '../../schemas/db.ts'
const invitesPlugin: FastifyPluginCallbackZod = (fastify, _option, done) => {
const { db } = fastify
// fastify.addHook('onRequest', fastify.auth)
fastify.route({
url: '/',
method: 'GET',
schema: {
response: {
200: z.array(
InviteSchema.extend({
roles: z.array(RoleSchema.pick({ id: true, name: true })),
}),
),
},
},
handler() {
return db
.selectFrom('invite as i')
.selectAll()
.select((eb) => [
jsonArrayFrom(
eb
.selectFrom('role as r')
.innerJoin('invites_roles as ir', 'ir.roleId', 'r.id')
.select(['r.id', 'r.name'])
.whereRef('r.id', '=', 'ir.roleId'),
).as('roles'),
])
.execute()
},
})
fastify.route({
url: '/',
method: 'POST',
schema: {
body: z.object({
email: z.email(),
roles: z.array(z.coerce.number()),
}),
response: {
201: InviteSchema.extend({
roles: z.array(RoleSchema.pick({ id: true, name: true })),
}),
},
},
async handler(request, reply) {
const trx = await db.startTransaction().execute()
const token = generateToken()
const invite = await trx
.insertInto('invite')
.values({ email: request.body.email, token, createdById: request.session.userId })
.returningAll()
.executeTakeFirstOrThrow()
await trx
.insertInto('invites_roles')
.values(
request.body.roles.map((roleId) => ({
inviteId: invite.id,
roleId,
})),
)
.execute()
try {
const roles = await trx
.selectFrom('role as r')
.select(['id', 'name'])
.innerJoin('invites_roles as ir', 'ir.roleId', 'r.id')
.where('ir.inviteId', '=', invite.id)
.execute()
const link = `${new URL(config.auth.paths.register, config.site.url)}?email=${invite.email}&token=${invite.token}`
await sendMail({
to: invite.email,
subject: `Invite to ${config.site.title} Admin`,
html: await inviteEmailTemplate({ link }).text(),
})
await trx.commit().execute()
return reply
.header('Location', `${request.url}/${invite.id}`)
.status(201)
.send({
...invite,
roles,
})
} catch (err) {
await trx.rollback().execute()
throw err
}
},
})
fastify.route({
url: '/:id',
method: 'DELETE',
schema: {
params: z.object({
id: z.number(),
}),
response: {
204: {},
404: {},
},
},
async handler(request, reply) {
const result = await db.deleteFrom('invite').where('id', '=', request.params.id).executeTakeFirstOrThrow()
return reply.status(result.numDeletedRows > 0 ? 204 : 404).send()
},
})
done()
}
export default invitesPlugin