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