feat: add new invite request functionality and update invite page with new dialog
This commit is contained in:
parent
14eb5bf113
commit
51ebe0be1f
2 changed files with 126 additions and 4 deletions
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue