fix: prevent sign-out crash and surface real email-send failures
- NextAuth session/jwt callbacks no longer return null (the client useSession() hook chokes on a null session, crashing the app on sign-out for deactivated/refreshed sessions) — fixes #57 - UserMenu/Header sign-out handlers close the Manage Users overlay and popover before calling signOut(), and no longer let a rejected signOut() promise go unhandled - Added an app-level error boundary as a safety net for any remaining client render errors - Registration and forgot-password flows now tell the user honestly when the confirmation/reset email failed to send instead of always claiming success — related to #56 - lib/email.ts throws a clear, actionable error when neither SMTP nor Resend is configured, instead of a cryptic SDK error - Documented required SMTP/Resend env vars in DEPLOYMENT_CHECKLIST.md (were missing entirely, which is why no confirmation/reset emails were ever sent in deployed environments) — fixes #56
This commit is contained in:
@@ -99,7 +99,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
token.email = user.email;
|
||||
token.isAdmin = user.isAdmin;
|
||||
token.hasWriteAccess = user.hasWriteAccess;
|
||||
token.exp = Math.floor(Date.now() / 1000) + (24 * 60 * 60); // 24 hours from now
|
||||
token.isActive = true;
|
||||
}
|
||||
|
||||
// Refresh user data from database on token refresh to get latest hasWriteAccess and isActive
|
||||
@@ -117,12 +117,15 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
},
|
||||
});
|
||||
|
||||
if (dbUser) {
|
||||
// Check if user is still active (treat null/undefined as true)
|
||||
if (dbUser.isActive === false) {
|
||||
// User was deactivated, invalidate token
|
||||
return null as any;
|
||||
}
|
||||
if (!dbUser || dbUser.isActive === false) {
|
||||
// User was deactivated or deleted. Flag it on the token instead of
|
||||
// returning `null` here: NextAuth v5's client `useSession()` hook does
|
||||
// not expect a `null` JWT and throws when it encounters one, which is
|
||||
// what caused the sign-out crash (issue #57). The `session` callback
|
||||
// below turns this flag into a clean signed-out session instead.
|
||||
token.isActive = false;
|
||||
} else {
|
||||
token.isActive = true;
|
||||
token.id = dbUser.id.toString();
|
||||
token.isAdmin = dbUser.isAdmin;
|
||||
token.hasWriteAccess = dbUser.hasWriteAccess;
|
||||
@@ -136,9 +139,11 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
// If token is null or expired, return null session to force logout
|
||||
if (!token || (token.exp && token.exp < Math.floor(Date.now() / 1000))) {
|
||||
return null as any;
|
||||
// Never return `null` for the whole session object — NextAuth v5's client
|
||||
// hooks assume a `{ user, expires }` shape and crash on `null` (issue #57).
|
||||
// Instead, drop `user` so the client treats this as a normal signed-out state.
|
||||
if (!token || token.isActive === false) {
|
||||
return { ...session, user: undefined } as any;
|
||||
}
|
||||
|
||||
if (session.user) {
|
||||
|
||||
@@ -83,19 +83,25 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
|
||||
// Send confirmation email
|
||||
let emailSendFailed = false;
|
||||
try {
|
||||
await sendEmailConfirmation(email, name.trim(), confirmationToken);
|
||||
} catch (emailError) {
|
||||
console.error('Error sending confirmation email:', emailError);
|
||||
// Don't fail registration if email fails, but log it
|
||||
// User can request a resend later
|
||||
// Don't fail registration if email fails, but let the client know so it
|
||||
// doesn't tell the user to "check their email" when nothing was sent
|
||||
// (issue #56). The user can still request a resend later.
|
||||
emailSendFailed = true;
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'User created successfully. Please check your email to confirm your account.',
|
||||
message: emailSendFailed
|
||||
? 'Account created, but we could not send the confirmation email right now. Use "Resend confirmation email" on the login screen once you try to sign in.'
|
||||
: 'User created successfully. Please check your email to confirm your account.',
|
||||
user,
|
||||
requiresEmailConfirmation: true
|
||||
requiresEmailConfirmation: true,
|
||||
emailSendFailed,
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
/**
|
||||
* App-level error boundary. Without this, any uncaught client render error
|
||||
* (e.g. during a sign-out/session transition) produces a hard white-screen
|
||||
* crash instead of a recoverable UI (issue #57).
|
||||
*/
|
||||
export default function GlobalError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('[APP_ERROR]', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center gap-4 p-6 text-center">
|
||||
<h1 className="text-xl font-semibold text-secondary">Something went wrong</h1>
|
||||
<p className="max-w-md text-sm text-muted-foreground">
|
||||
An unexpected error occurred. You can try again, or head back to the homepage.
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={() => reset()}>
|
||||
Try again
|
||||
</Button>
|
||||
<Button onClick={() => { window.location.href = '/'; }}>
|
||||
Go home
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user