2025-12-14 18:57:56 +01:00
|
|
|
import { TRPCError } from "@trpc/server";
|
|
|
|
|
import { z } from "zod";
|
2026-03-27 19:18:54 +01:00
|
|
|
import { env } from "~/env";
|
2025-12-14 18:57:56 +01:00
|
|
|
import {
|
|
|
|
|
adminProcedure,
|
|
|
|
|
createTRPCRouter,
|
|
|
|
|
publicProcedure,
|
|
|
|
|
} from "~/server/api/trpc";
|
2026-03-27 19:18:54 +01:00
|
|
|
import { swapAuth } from "~/server/controllers/auth.controller";
|
2025-12-14 18:57:56 +01:00
|
|
|
import {
|
|
|
|
|
consumeInviteToken,
|
2026-03-27 19:18:54 +01:00
|
|
|
createInviteToken,
|
2025-12-14 18:57:56 +01:00
|
|
|
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 }) => {
|
2026-01-16 11:52:59 +01:00
|
|
|
try {
|
|
|
|
|
const user = await getUser({ userId: input.id, db });
|
2025-12-14 18:57:56 +01:00
|
|
|
|
2026-03-27 19:18:54 +01:00
|
|
|
const token = await createInviteToken(user.email);
|
|
|
|
|
|
2026-04-14 17:23:13 +02:00
|
|
|
const inviteUrl = `${env.BASE_URL}/auth/accetta-invito/${token}?e=${btoa(user.email)}`;
|
2025-12-14 18:57:56 +01:00
|
|
|
|
2026-01-16 11:52:59 +01:00
|
|
|
await sendInvitoEmail({
|
|
|
|
|
userId: input.id,
|
|
|
|
|
email: user.email,
|
|
|
|
|
nome: user.nome,
|
2026-03-27 19:18:54 +01:00
|
|
|
//password: user.password,
|
|
|
|
|
inviteUrl,
|
2026-01-16 11:52:59 +01:00
|
|
|
});
|
2025-12-14 18:57:56 +01:00
|
|
|
|
2026-01-16 11:52:59 +01:00
|
|
|
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"),
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-12-14 18:57:56 +01:00
|
|
|
}),
|
|
|
|
|
|
2026-04-13 18:47:38 +02:00
|
|
|
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);
|
2026-04-14 17:23:13 +02:00
|
|
|
const inviteUrl = `${env.BASE_URL}/auth/accetta-invito/${token}?e=${btoa(user.email)}`;
|
2026-04-13 18:47:38 +02:00
|
|
|
|
|
|
|
|
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"),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
2025-12-14 18:57:56 +01:00
|
|
|
// Verify invite token (public - user clicking the link)
|
|
|
|
|
verifyInvite: publicProcedure
|
2026-04-14 17:23:13 +02:00
|
|
|
.input(z.object({ token: z.string(), email: z.string().nullable() }))
|
2026-04-13 18:47:38 +02:00
|
|
|
.output(
|
2026-04-14 17:23:13 +02:00
|
|
|
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") }),
|
2026-04-13 18:47:38 +02:00
|
|
|
]),
|
|
|
|
|
)
|
2025-12-14 18:57:56 +01:00
|
|
|
.query(async ({ input }) => {
|
2026-04-14 17:23:13 +02:00
|
|
|
if (input.email) {
|
|
|
|
|
const existingUser = await findUser_byEmail({ email: input.email });
|
2026-04-15 15:08:57 +02:00
|
|
|
if (existingUser?.usedInvite) {
|
2026-04-14 17:23:13 +02:00
|
|
|
return { status: "already_user" };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ver = await verifyInviteToken(input.token);
|
2025-12-14 18:57:56 +01:00
|
|
|
|
2026-04-14 17:23:13 +02:00
|
|
|
if (!ver.status) {
|
|
|
|
|
return { status: "invalid", reason: ver.reason };
|
2025-12-14 18:57:56 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 17:23:13 +02:00
|
|
|
return { status: "valid", email: ver.invite.email };
|
2025-12-14 18:57:56 +01:00
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
// Accept invite and set password
|
|
|
|
|
acceptInvite: publicProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
email: z.email(),
|
|
|
|
|
token: z.string(),
|
|
|
|
|
password: z.string().min(8),
|
|
|
|
|
}),
|
|
|
|
|
)
|
2026-03-27 19:18:54 +01:00
|
|
|
.mutation(async ({ input, ctx }) => {
|
2025-12-14 18:57:56 +01:00
|
|
|
const user = await findUser_byEmail({ email: input.email });
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: "NOT_FOUND",
|
2026-04-13 12:45:42 +02:00
|
|
|
message: `Accept Invite: Utente non trovato con email: ${input.email}`,
|
2025-12-14 18:57:56 +01:00
|
|
|
});
|
|
|
|
|
}
|
2026-04-14 17:23:13 +02:00
|
|
|
const ver = await verifyInviteToken(input.token);
|
2025-12-14 18:57:56 +01:00
|
|
|
|
2026-04-14 17:23:13 +02:00
|
|
|
if (!ver.status) {
|
2025-12-14 18:57:56 +01:00
|
|
|
throw new TRPCError({
|
|
|
|
|
code: "NOT_FOUND",
|
2026-04-13 12:45:42 +02:00
|
|
|
message: `Accept Invite: Invito non valido o scaduto per email: ${input.email}`,
|
2025-12-14 18:57:56 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create user with password
|
|
|
|
|
const salt = generateSalt();
|
|
|
|
|
const hashedPassword = await hashPassword(input.password, salt);
|
|
|
|
|
|
|
|
|
|
await editAccountHandler({
|
|
|
|
|
id: user.id,
|
|
|
|
|
data: {
|
|
|
|
|
password: hashedPassword,
|
|
|
|
|
salt,
|
2026-04-15 15:08:57 +02:00
|
|
|
usedInvite: true,
|
2025-12-14 18:57:56 +01:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Mark invite as used
|
|
|
|
|
await consumeInviteToken(input.token);
|
2026-03-27 19:18:54 +01:00
|
|
|
await swapAuth({ ctx, userId: user.id });
|
2025-12-14 18:57:56 +01:00
|
|
|
|
|
|
|
|
return { success: true };
|
|
|
|
|
}),
|
|
|
|
|
});
|