Self-heal stale email verification banner
The banner was wired off session.user.isEmailVerified, which lives on the JWT cookie. If a user verified their email in another tab or was auto-verified by the invite flow after an existing session was issued, the token kept saying unverified until sign-out, so the banner hung around indefinitely. - Add GET /api/auth/verify-email/status, a tiny authenticated endpoint that returns the authoritative emailVerified state straight from the DB. - Banner now fetches that on mount and only renders if the DB agrees the user is unverified. If the DB says verified but the cookie is stale, we call useSession().update() to refresh the JWT so the rest of the app sees the correct state on the next render. Made-with: Cursor
This commit is contained in:
parent
f2c7d13a3e
commit
11773a2ad2
2 changed files with 69 additions and 3 deletions
21
src/app/api/auth/verify-email/status/route.ts
Normal file
21
src/app/api/auth/verify-email/status/route.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/auth";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
select: { emailVerified: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ verified: !!user.emailVerified });
|
||||
}
|
||||
|
|
@ -1,17 +1,62 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { Mail, X, Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type Status = "loading" | "verified" | "unverified";
|
||||
|
||||
export function EmailVerificationBanner() {
|
||||
const { data: session } = useSession();
|
||||
const { data: session, status: sessionStatus, update } = useSession();
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [sent, setSent] = useState(false);
|
||||
const [status, setStatus] = useState<Status>("loading");
|
||||
|
||||
if (!session?.user || session.user.isEmailVerified || dismissed) {
|
||||
// Authoritative check against the DB. The JWT cookie can be stale (e.g.
|
||||
// user verified their email in another tab, or was auto-verified by the
|
||||
// invite flow on an existing session), so we confirm with the server
|
||||
// before ever showing the banner. If the DB disagrees with the session,
|
||||
// we also trigger a session update so the banner hides immediately on
|
||||
// subsequent renders without requiring a full sign-out.
|
||||
useEffect(() => {
|
||||
if (sessionStatus !== "authenticated") return;
|
||||
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/auth/verify-email/status", {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) {
|
||||
if (!cancelled) setStatus("unverified");
|
||||
return;
|
||||
}
|
||||
const data = (await res.json()) as { verified: boolean };
|
||||
if (cancelled) return;
|
||||
setStatus(data.verified ? "verified" : "unverified");
|
||||
if (data.verified && session?.user && !session.user.isEmailVerified) {
|
||||
void update();
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) setStatus("unverified");
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [sessionStatus, session?.user, update]);
|
||||
|
||||
if (
|
||||
sessionStatus !== "authenticated" ||
|
||||
!session?.user ||
|
||||
session.user.isEmailVerified ||
|
||||
status === "loading" ||
|
||||
status === "verified" ||
|
||||
dismissed
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue