diff --git a/DEPLOYMENT_CHECKLIST.md b/DEPLOYMENT_CHECKLIST.md index f69deed..8d57d52 100644 --- a/DEPLOYMENT_CHECKLIST.md +++ b/DEPLOYMENT_CHECKLIST.md @@ -31,8 +31,29 @@ DATABASE_URL_AUTH=postgresql://user:password@10.0.10.179:5432/punimtag_auth NEXTAUTH_URL=http://10.0.10.121:3001 NEXTAUTH_SECRET=your-secret-key-here AUTH_URL=http://10.0.10.121:3001 + +# Password reset / email verification emails (lib/email.ts). Without these, +# "Forgot password" and "Confirm your email" never actually send mail — see +# issue #56. Pick ONE provider path: + +# Option A: SMTP (primary; falls back to Resend automatically on failure) +EMAIL_PROVIDER=smtp +SMTP_HOST=mail.your-domain.com +SMTP_PORT=465 +SMTP_SECURE=true +SMTP_USER=mailer@your-domain.com +SMTP_PASS=your-mailbox-password +SMTP_FROM_EMAIL=noreply@your-domain.com +SMTP_FROM_NAME=PunimTag Viewer + +# Option B: Resend only (set EMAIL_PROVIDER=resend, or leave SMTP_* unset above) +RESEND_API_KEY=re_xxx +RESEND_FROM_EMAIL=onboarding@resend.dev +RESEND_FROM_NAME=PunimTag Viewer ``` +Verify email sending works with `cd viewer-frontend && npx tsx scripts/test-email-sending.ts` after setting these. + ## 2. PM2 Configuration Copy the template and customize for your server: diff --git a/viewer-frontend/app/api/auth/[...nextauth]/route.ts b/viewer-frontend/app/api/auth/[...nextauth]/route.ts index 244c9b4..f70bec7 100644 --- a/viewer-frontend/app/api/auth/[...nextauth]/route.ts +++ b/viewer-frontend/app/api/auth/[...nextauth]/route.ts @@ -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) { diff --git a/viewer-frontend/app/api/auth/register/route.ts b/viewer-frontend/app/api/auth/register/route.ts index f4f1567..c70bef0 100644 --- a/viewer-frontend/app/api/auth/register/route.ts +++ b/viewer-frontend/app/api/auth/register/route.ts @@ -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 } ); diff --git a/viewer-frontend/app/error.tsx b/viewer-frontend/app/error.tsx new file mode 100644 index 0000000..77aa86a --- /dev/null +++ b/viewer-frontend/app/error.tsx @@ -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 ( +
+ An unexpected error occurred. You can try again, or head back to the homepage. +
+