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}?e=${btoa(user.email)}`; 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}?e=${btoa(user.email)}`; 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(), email: z.string().nullable() })) .output( z.discriminatedUnion("status", [ z.object({ status: z.literal("valid"), email: z.email() }), z.object({ status: z.literal("invalid"), reason: z.enum(["not_found", "expired"]), }), z.object({ status: z.literal("already_user") }), ]), ) .query(async ({ input }) => { if (input.email) { const existingUser = await findUser_byEmail({ email: input.email }); if (existingUser) { return { status: "already_user" }; } } const ver = await verifyInviteToken(input.token); if (!ver.status) { return { status: "invalid", reason: ver.reason }; } return { status: "valid", email: ver.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 ver = await verifyInviteToken(input.token); if (!ver.status) { 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 }; }), });