diff --git a/apps/infoalloggi/src/pages/auth/accetta-invito/[token].tsx b/apps/infoalloggi/src/pages/auth/accetta-invito/[token].tsx index 2e32716..6de35cb 100644 --- a/apps/infoalloggi/src/pages/auth/accetta-invito/[token].tsx +++ b/apps/infoalloggi/src/pages/auth/accetta-invito/[token].tsx @@ -1,6 +1,13 @@ "use client"; -import { ExternalLink, Eye, EyeOff, Frown, MailPlus } from "lucide-react"; +import { + ExternalLink, + Eye, + EyeOff, + Frown, + MailPlus, + UserSearch, +} from "lucide-react"; import type { GetServerSideProps } from "next"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -38,25 +45,47 @@ import { useTranslation } from "~/providers/I18nProvider"; import { generateSSGHelper } from "~/server/utils/ssgHelper"; import { api } from "~/utils/api"; -type Valid = { - valid: true; +type InviteTokenProps = { token: string; - email: string; + verification: + | { + status: "valid"; + email: string; + } + | { + status: "invalid"; + reason: "not_found" | "expired"; + } + | { + status: "already_user"; + }; }; -type Invalid = { - valid: false; - token: null; - email: null; -}; -type InviteTokenProps = Valid | Invalid; + /** * Pagina di accettazione invito: /auth/accetta-invito/[token] */ const AcceptInvitePage: NextPageWithLayout = ({ - valid, + verification, + token, +}: InviteTokenProps) => { + switch (verification.status) { + case "valid": + return ; + case "invalid": + return ; + case "already_user": + return ; + default: + return ; + } +}; +const ValidInvitePage = ({ token, email, -}: InviteTokenProps) => { +}: { + token: string; + email: string; +}) => { const { isVisible, getStrengthColor, @@ -91,7 +120,6 @@ const AcceptInvitePage: NextPageWithLayout = ({ newPsw: "", }, }); - function onSubmit(fields: FormValues) { if (!email || !token) return; acceptInvite({ @@ -112,27 +140,6 @@ const AcceptInvitePage: NextPageWithLayout = ({ toast.error(error.message); }, }); - - if (!valid) { - return ( -
- - -

Invito non valido

-

L'invito è scaduto o è già stato utilizzato.

-
- - - - -
-
- ); - } - return (
@@ -221,6 +228,42 @@ const AcceptInvitePage: NextPageWithLayout = ({
); }; +const AlreadyUserPage = () => { + return ( +
+ +

Utente già registrato

+

L'email associata a questo invito è già registrata.

+
+ + + +
+
+ ); +}; + +const InvalidInvitePage = ({ reason }: { reason: "not_found" | "expired" }) => { + const message = + reason === "expired" + ? "L'invito è scaduto, è passato troppo tempo dall'invio." + : "Non è stato trovato alcun invito valido per questo collegamento."; + + return ( +
+ + +

Invito non valido

+

{message}

+
+ +
+
+ ); +}; const NewInviteDialog = () => { const { t } = useTranslation(); @@ -244,7 +287,7 @@ const NewInviteDialog = () => { return ( - @@ -297,24 +340,24 @@ export const getServerSideProps = (async (context) => { }, }; } + const e = context.query.e; + let email: string | null = null; + if (typeof e === "string") { + try { + email = atob(e); + } catch { + email = null; + } + } + const helper = generateSSGHelper(); - const valid = await helper.invite.verifyInvite.fetch({ token }); + const verification = await helper.invite.verifyInvite.fetch({ token, email }); - if (valid.valid) { - return { - props: { - valid: true, - token, - email: valid.email as string, - }, - }; - } return { props: { - valid: false, - token: null, - email: null, + verification, + token, }, }; }) satisfies GetServerSideProps; diff --git a/apps/infoalloggi/src/server/api/routers/invite.ts b/apps/infoalloggi/src/server/api/routers/invite.ts index 240a780..f30831a 100644 --- a/apps/infoalloggi/src/server/api/routers/invite.ts +++ b/apps/infoalloggi/src/server/api/routers/invite.ts @@ -33,7 +33,7 @@ export const inviteRouter = createTRPCRouter({ const token = await createInviteToken(user.email); - const inviteUrl = `${env.BASE_URL}/auth/accetta-invito/${token}`; + const inviteUrl = `${env.BASE_URL}/auth/accetta-invito/${token}?e=${btoa(user.email)}`; await sendInvitoEmail({ userId: input.id, @@ -66,7 +66,7 @@ export const inviteRouter = createTRPCRouter({ } const token = await createInviteToken(user.email); - const inviteUrl = `${env.BASE_URL}/auth/accetta-invito/${token}`; + const inviteUrl = `${env.BASE_URL}/auth/accetta-invito/${token}?e=${btoa(user.email)}`; await sendInvitoEmail({ userId: user.id, @@ -89,21 +89,32 @@ export const inviteRouter = createTRPCRouter({ // Verify invite token (public - user clicking the link) verifyInvite: publicProcedure - .input(z.object({ token: z.string() })) + .input(z.object({ token: z.string(), email: z.string().nullable() })) .output( - z.discriminatedUnion("valid", [ - z.object({ valid: z.literal(true), email: z.email() }), - z.object({ valid: z.literal(false), email: z.null() }), + 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 }) => { - const invite = await verifyInviteToken(input.token); - - if (!invite) { - return { valid: false, email: null }; + if (input.email) { + const existingUser = await findUser_byEmail({ email: input.email }); + if (existingUser) { + return { status: "already_user" }; + } } - return { valid: true, email: invite.email }; + 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 @@ -123,9 +134,9 @@ export const inviteRouter = createTRPCRouter({ message: `Accept Invite: Utente non trovato con email: ${input.email}`, }); } - const invite = await verifyInviteToken(input.token); + const ver = await verifyInviteToken(input.token); - if (!invite) { + if (!ver.status) { throw new TRPCError({ code: "NOT_FOUND", message: `Accept Invite: Invito non valido o scaduto per email: ${input.email}`, diff --git a/apps/infoalloggi/src/server/controllers/invites.controller.ts b/apps/infoalloggi/src/server/controllers/invites.controller.ts index c25054e..afcef70 100644 --- a/apps/infoalloggi/src/server/controllers/invites.controller.ts +++ b/apps/infoalloggi/src/server/controllers/invites.controller.ts @@ -1,5 +1,6 @@ import crypto from "node:crypto"; import { TRPCError } from "@trpc/server"; +import type { UserInvites } from "~/schemas/public/UserInvites"; import type { UsersId } from "~/schemas/public/Users"; import { db } from "~/server/db"; import { NewMail } from "../services/mailer"; @@ -31,18 +32,33 @@ export async function createInviteToken(email: string) { return token; } -export async function verifyInviteToken(token: string) { +type VerifyInviteResult = + | { + status: true; + invite: UserInvites; + } + | { + status: false; + reason: "not_found" | "expired"; + }; +export async function verifyInviteToken( + token: string, +): Promise { try { const invite = await db .selectFrom("user_invites") - .selectAll() + .selectAll("user_invites") .where("token", "=", token) - .where("expires_at", ">", new Date()) + //.where("expires_at", ">", new Date()) .executeTakeFirst(); if (!invite) { - return null; + return { status: false, reason: "not_found" }; } - return invite; + if (invite.expires_at < new Date()) { + return { status: false, reason: "expired" }; + } + + return { status: true, invite }; } catch (e) { console.error("Error verifying invite token:", e); throw new TRPCError({