From 6c0629c78e50dbf5ff7fd87e9d5fbece8772418d Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 13 Nov 2025 22:30:11 +0000 Subject: [PATCH] enhanced logging and added verification error catching --- src/app/api/auth/[...nextauth]/authOptions.ts | 41 +++++++++++++-- src/app/api/auth/error/route.ts | 50 +++++++++++++++++++ src/app/auth/error/page.tsx | 48 ++++++++++++++++++ src/app/layout.tsx | 2 +- 4 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 src/app/api/auth/error/route.ts create mode 100644 src/app/auth/error/page.tsx diff --git a/src/app/api/auth/[...nextauth]/authOptions.ts b/src/app/api/auth/[...nextauth]/authOptions.ts index 2d08eba..e5a5d69 100644 --- a/src/app/api/auth/[...nextauth]/authOptions.ts +++ b/src/app/api/auth/[...nextauth]/authOptions.ts @@ -82,7 +82,16 @@ export const AuthOptions: NextAuthOptions = { }, from: EMAIL_FROM, // noreply email maxAge: 60 * 60, // magic link valid for 1 hour - sendVerificationRequest: MagicLinksEmail, + // Slightly extended magic link email sender, to log sends + sendVerificationRequest: async ({ identifier, url, provider }) => { + console.log("EMAIL MAGIC LINK SENT:", { + email: identifier, + url, + timestamp: new Date().toISOString(), + }); + + await MagicLinksEmail({ identifier, url, provider }); + }, }), ], @@ -92,7 +101,7 @@ export const AuthOptions: NextAuthOptions = { pages: { signIn: "/", // your landing/login page verifyRequest: "/auth/verify-request", - error: "/auth/error", + error: "/api/auth/error", }, // ------------------------------------------------------------------ @@ -107,6 +116,13 @@ export const AuthOptions: NextAuthOptions = { if (!user?.email) return false; const normalisedEmail = user.email.toLowerCase(); + console.log("SignIn attempt:", { + email: normalisedEmail, + provider: account?.provider, + type: account?.type, + timestamp: new Date().toISOString(), + }); + // Fetch the user (NextAuth will have created them already if new) const [dbUser] = await db .select() @@ -115,7 +131,10 @@ export const AuthOptions: NextAuthOptions = { // New user - next auth will handle if (!dbUser) { - console.log("New user sign up for email:", normalisedEmail); + console.log("New user created through sign-in:", { + email: normalisedEmail, + provider: account?.provider, + }); return true; } @@ -159,7 +178,10 @@ export const AuthOptions: NextAuthOptions = { // Link OAuth ID if missing (helps for older accounts) if (account && !dbUser.oauthId) { - console.log("Linking OAuth ID for user:", normalisedEmail); + console.log("Backfilling OAuth ID:", { + email: normalisedEmail, + provider: account.provider, + }); const provider = account.provider as OauthProvider; await db .update(users) @@ -189,6 +211,10 @@ export const AuthOptions: NextAuthOptions = { async jwt({ token, user: userFromLogin }) { // Initial sign-in: attach DB fields if (userFromLogin) { + console.log("JWT initial login:", { + email: userFromLogin.email, + timestamp: new Date().toISOString(), + }); const existing = await db.query.user.findFirst({ where: eq(users.email, userFromLogin.email!), }); @@ -224,8 +250,13 @@ export const AuthOptions: NextAuthOptions = { /** * Redirect users after login */ - async redirect({ baseUrl }) { + async redirect({ url, baseUrl }) { // If the user has not onboarded, send them to onboarding + console.log("Redirect triggered:", { + from: url, + to: `${baseUrl}/home`, + timestamp: new Date().toISOString(), + }); return `${baseUrl}/home`; }, }, diff --git a/src/app/api/auth/error/route.ts b/src/app/api/auth/error/route.ts new file mode 100644 index 0000000..1519717 --- /dev/null +++ b/src/app/api/auth/error/route.ts @@ -0,0 +1,50 @@ +import { NextResponse } from "next/server"; +import { db } from "@/app/db/db"; +import { verificationTokens } from "@/app/db/schema/users"; +import { eq } from "drizzle-orm"; + +export async function GET(request: Request) { + const url = new URL(request.url); + const error = url.searchParams.get("error"); + const token = url.searchParams.get("token"); + + // Capture where the user came from + const referer = request.headers.get("referer"); + const forwardedFor = request.headers.get("x-forwarded-for"); + const host = request.headers.get("host"); + + let tokenRecord = null; + + if (token) { + try { + tokenRecord = await db + .select() + .from(verificationTokens) + .where(eq(verificationTokens.token, token)) + .limit(1); + } catch (err) { + console.error("Error querying verificationTokens", err); + } + } + + console.error("Auth error callback triggered", { + error, + timestamp: new Date().toISOString(), + path: url.pathname, + query: Object.fromEntries(url.searchParams), + + userAgent: request.headers.get("user-agent"), + referer, + forwardedFor, + host, + + tokenPresentInQuery: Boolean(token), + tokenMatchedDB: Boolean(tokenRecord?.length), + tokenDBRecord: tokenRecord?.[0] ?? null, + }); + + const redirectUrl = new URL("/auth/error", request.url); + if (error) redirectUrl.searchParams.set("error", error); + + return NextResponse.redirect(redirectUrl); +} diff --git a/src/app/auth/error/page.tsx b/src/app/auth/error/page.tsx new file mode 100644 index 0000000..8b37bb3 --- /dev/null +++ b/src/app/auth/error/page.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; + +export default function AuthErrorPage() { + const params = useSearchParams(); + const errorType = params.get("error"); + + const message = + errorType === "Verification" + ? "Your login link is no longer valid." + : "We couldn’t complete your login."; + + const subMessage = + errorType === "Verification" + ? "This usually happens when the link was already used, expired, or opened automatically by your email app." + : "Please request a new login link below."; + + return ( +
+
+
+ +
+ +

{message}

+ +

{subMessage}

+ + + Request a new login link + +
+ +

+ Need help? Contact your Domna representative. +

+
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c5f03c0..6c1ff8a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -25,7 +25,7 @@ const getSession = cache(async () => { return session; }); -export function Footer() { +function Footer() { return (