fix: resolve invitation link expiration by adding verification landing page
All checks were successful
Staging Build / build (push) Successful in 3m35s
All checks were successful
Staging Build / build (push) Successful in 3m35s
This commit is contained in:
76
shop/app/auth/verify-link/page.tsx
Normal file
76
shop/app/auth/verify-link/page.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { use, useState } from "react";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ShieldCheck, Loader2 } from "lucide-react";
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
searchParams: Promise<{
|
||||||
|
token_hash?: string;
|
||||||
|
type?: string;
|
||||||
|
next?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function VerifyLinkPage({ searchParams }: PageProps) {
|
||||||
|
const params = use(searchParams);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const tokenHash = params.token_hash || "";
|
||||||
|
const type = params.type || "";
|
||||||
|
const next = params.next || "/";
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex min-h-svh w-full items-center justify-center p-6 md:p-10 overflow-hidden bg-slate-950">
|
||||||
|
{/* Background gradients for rich aesthetics */}
|
||||||
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,#0f172a_1px,transparent_1px),linear-gradient(to_bottom,#0f172a_1px,transparent_1px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)] opacity-60" />
|
||||||
|
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[500px] h-[500px] bg-blue-500/10 rounded-full blur-[120px] pointer-events-none" />
|
||||||
|
<div className="absolute bottom-0 right-1/4 w-[400px] h-[400px] bg-indigo-500/10 rounded-full blur-[100px] pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="relative w-full max-w-md z-10">
|
||||||
|
<Card className="border-slate-800/80 bg-slate-900/60 backdrop-blur-xl shadow-2xl transition-all duration-300 hover:border-slate-700/80">
|
||||||
|
<CardHeader className="flex flex-col items-center justify-center text-center space-y-4 pt-8">
|
||||||
|
<div className="p-3.5 bg-blue-500/10 rounded-full text-blue-400 border border-blue-500/20 shadow-[0_0_20px_rgba(59,130,246,0.15)] animate-pulse">
|
||||||
|
<ShieldCheck className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<CardTitle className="text-2xl font-bold tracking-tight text-slate-100">
|
||||||
|
Sicherheit prüfen
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-sm text-slate-400 max-w-xs mx-auto">
|
||||||
|
Bitte bestätigen Sie den Vorgang mit einem Klick auf den Button, um fortzufahren.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pb-8">
|
||||||
|
<form action="/auth/confirm" method="GET" onSubmit={handleSubmit} className="w-full">
|
||||||
|
<input type="hidden" name="token_hash" value={tokenHash} />
|
||||||
|
<input type="hidden" name="type" value={type} />
|
||||||
|
<input type="hidden" name="next" value={next} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting || !tokenHash}
|
||||||
|
className="w-full bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-medium shadow-[0_0_20px_rgba(59,130,246,0.25)] transition-all duration-200 h-11 active:scale-[0.98] disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Wird weitergeleitet...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Bestätigen & Fortfahren"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -84,7 +84,7 @@ export async function createUser(data: { email: string; role?: 'admin' | 'partne
|
|||||||
const tokenHash = (linkData as any)?.properties?.hashed_token;
|
const tokenHash = (linkData as any)?.properties?.hashed_token;
|
||||||
if (tokenHash) {
|
if (tokenHash) {
|
||||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de';
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de';
|
||||||
const setupLink = `${siteUrl}/auth/confirm?token_hash=${tokenHash}&type=recovery&next=/auth/update-password`;
|
const setupLink = `${siteUrl}/auth/verify-link?token_hash=${tokenHash}&type=recovery&next=/auth/update-password`;
|
||||||
await sendMail({
|
await sendMail({
|
||||||
to: data.email,
|
to: data.email,
|
||||||
subject: 'Willkommen bei CASPOS Shop – Ihr Benutzerkonto wurde erstellt',
|
subject: 'Willkommen bei CASPOS Shop – Ihr Benutzerkonto wurde erstellt',
|
||||||
@@ -183,7 +183,7 @@ export async function resetPassword(email: string) {
|
|||||||
if (!tokenHash) throw new Error('Recovery token not generated');
|
if (!tokenHash) throw new Error('Recovery token not generated');
|
||||||
// Build link pointing to our own /auth/confirm route
|
// Build link pointing to our own /auth/confirm route
|
||||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de';
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de';
|
||||||
const resetLink = `${siteUrl}/auth/confirm?token_hash=${tokenHash}&type=recovery&next=/auth/update-password`;
|
const resetLink = `${siteUrl}/auth/verify-link?token_hash=${tokenHash}&type=recovery&next=/auth/update-password`;
|
||||||
await sendMail({
|
await sendMail({
|
||||||
to: email,
|
to: email,
|
||||||
subject: 'Passwort zurücksetzen',
|
subject: 'Passwort zurücksetzen',
|
||||||
|
|||||||
Reference in New Issue
Block a user