feat: add new invite request functionality and update invite page with new dialog

This commit is contained in:
Marco Pedone 2026-04-13 18:47:38 +02:00
parent 14eb5bf113
commit 51ebe0be1f
2 changed files with 126 additions and 4 deletions

View file

@ -1,8 +1,10 @@
"use client"; "use client";
import { Eye, EyeOff, Frown } from "lucide-react"; import { ExternalLink, Eye, EyeOff, Frown, MailPlus } from "lucide-react";
import type { GetServerSideProps } from "next"; import type { GetServerSideProps } from "next";
import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { z } from "zod"; import { z } from "zod";
import { import {
@ -13,7 +15,19 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "~/components/custom_ui/form"; } from "~/components/custom_ui/form";
import LoadingButton from "~/components/custom_ui/loading-button";
import { PasswordSVG } from "~/components/svgs"; import { PasswordSVG } from "~/components/svgs";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "~/components/ui/alert-dialog";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import Input from "~/components/ui/input"; import Input from "~/components/ui/input";
import { useStrongPassword } from "~/hooks/useStrongPassword"; import { useStrongPassword } from "~/hooks/useStrongPassword";
@ -103,9 +117,17 @@ const AcceptInvitePage: NextPageWithLayout<InviteTokenProps> = ({
<div className="flex flex-1 flex-col items-center justify-center gap-2 text-center"> <div className="flex flex-1 flex-col items-center justify-center gap-2 text-center">
<Frown className="mx-auto mb-4 size-28 text-muted-foreground" /> <Frown className="mx-auto mb-4 size-28 text-muted-foreground" />
<h1 className="font-bold text-xl">Questo link non valido</h1> <h1 className="font-bold text-xl">Invito non valido</h1>
<p>Il link è scaduto o è già stato utilizzato.</p> <p>L'invito è scaduto o è già stato utilizzato.</p>
<p>Contattaci per ricevere una nuova email.</p> <div className="flex flex-col items-center gap-4 pt-4 md:flex-row">
<Link href="/login">
<Button variant="info">
<span>Vai al Login</span>
<ExternalLink />
</Button>
</Link>
<NewInviteDialog />
</div>
</div> </div>
); );
} }
@ -197,6 +219,68 @@ const AcceptInvitePage: NextPageWithLayout<InviteTokenProps> = ({
); );
}; };
const NewInviteDialog = () => {
const { t } = useTranslation();
const [email, setEmail] = useState("");
const [open, setOpen] = useState(false);
const { mutate, isPending } = api.invite.requestNewInvite.useMutation({
onSuccess: () => {
toast.success("Email inviata! Controlla la tua casella di posta.", {
duration: 5000,
});
setOpen(false);
setEmail("");
},
onError: (error) => {
toast.error(error.message);
setOpen(false);
setEmail("");
},
});
return (
<AlertDialog onOpenChange={setOpen} open={open}>
<AlertDialogTrigger asChild>
<Button>
<span>Richiedi un nuovo invito</span>
<MailPlus />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Inserisci il tuo indirizzo email</AlertDialogTitle>
<AlertDialogDescription>
Inserisci l'indirizzo email associato al tuo account per ricevere un
nuovo invito.
</AlertDialogDescription>
</AlertDialogHeader>
<Input
autoComplete="off"
id="email"
onChange={(e) => setEmail(e.target.value)}
placeholder="esempio@email.com"
type="email"
value={email}
/>
<AlertDialogFooter>
<AlertDialogCancel>{t.cancella}</AlertDialogCancel>
<AlertDialogAction asChild>
<LoadingButton
disabled={!email || email.trim() === "" || !email.includes("@")}
loading={isPending}
onClick={() => mutate({ email })}
variant="default"
>
{t.procedi}
</LoadingButton>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
export default AcceptInvitePage; export default AcceptInvitePage;
export const getServerSideProps = (async (context) => { export const getServerSideProps = (async (context) => {

View file

@ -55,9 +55,47 @@ export const inviteRouter = createTRPCRouter({
} }
}), }),
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) // Verify invite token (public - user clicking the link)
verifyInvite: publicProcedure verifyInvite: publicProcedure
.input(z.object({ token: z.string() })) .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 }) => { .query(async ({ input }) => {
const invite = await verifyInviteToken(input.token); const invite = await verifyInviteToken(input.token);