import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { env } from "~/env"; import { adminProcedure, createTRPCRouter, publicProcedure, } from "~/server/api/trpc"; import { swapAuth } from "~/server/controllers/auth.controller"; import { consumeInviteToken, createInviteToken, sendInvitoEmail, verifyInviteToken, } from "~/server/controllers/invites.controller"; import { editAccountHandler } from "~/server/controllers/user.controller"; import { db } from "~/server/db"; import { generateSalt, hashPassword } from "~/server/services/password.service"; import { findUser_byEmail, getUser } from "~/server/services/user.service"; import { zUserId } from "~/server/utils/zod_types"; export const inviteRouter = createTRPCRouter({ // Admin sends invite sendInvite: adminProcedure .input( z.object({ id: zUserId, }), ) .mutation(async ({ input }) => { try { const user = await getUser({ userId: input.id, db }); const token = await createInviteToken(user.email); const inviteUrl = `${env.BASE_URL}/auth/accetta-invito/${token}`; await sendInvitoEmail({ userId: input.id, email: user.email, nome: user.nome, //password: user.password, inviteUrl, }); return { success: true }; } catch (e) { console.error("Error in sendInvite:", e); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Errore durante l'invio dell'invito: " + (e instanceof Error ? e.message : "Unknown error"), }); } }), requestNewInvite: publicProcedure .input(z.object({ email: z.email() })) .mutation(async ({ input }) => { try { const user = await findUser_byEmail({ email: input.email }); if (!user) { // For security, we don't reveal whether the email exists or not return { success: true }; } const token = await createInviteToken(user.email); const inviteUrl = `${env.BASE_URL}/auth/accetta-invito/${token}`; await sendInvitoEmail({ userId: user.id, email: user.email, nome: user.nome, //password: user.password, inviteUrl, }); return { success: true }; } catch (e) { console.error("Error in requestNewInvite:", e); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Errore durante la richiesta di un nuovo invito: " + (e instanceof Error ? e.message : "Unknown error"), }); } }), // Verify invite token (public - user clicking the link) verifyInvite: publicProcedure .input(z.object({ token: z.string() })) .output( z.discriminatedUnion("valid", [ z.object({ valid: z.literal(true), email: z.email() }), z.object({ valid: z.literal(false), email: z.null() }), ]), ) .query(async ({ input }) => { const invite = await verifyInviteToken(input.token); if (!invite) { return { valid: false, email: null }; } return { valid: true, email: invite.email }; }), // Accept invite and set password acceptInvite: publicProcedure .input( z.object({ email: z.email(), token: z.string(), password: z.string().min(8), }), ) .mutation(async ({ input, ctx }) => { const user = await findUser_byEmail({ email: input.email }); if (!user) { throw new TRPCError({ code: "NOT_FOUND", message: `Accept Invite: Utente non trovato con email: ${input.email}`, }); } const invite = await verifyInviteToken(input.token); if (!invite) { throw new TRPCError({ code: "NOT_FOUND", message: `Accept Invite: Invito non valido o scaduto per email: ${input.email}`, }); } // Create user with password const salt = generateSalt(); const hashedPassword = await hashPassword(input.password, salt); await editAccountHandler({ id: user.id, data: { password: hashedPassword, salt, }, }); // Mark invite as used await consumeInviteToken(input.token); await swapAuth({ ctx, userId: user.id }); return { success: true }; }), });