From 3a35766bacc015b3bfb9d51bd285fc06560bb442 Mon Sep 17 00:00:00 2001 From: KyleTryon Date: Sun, 11 Jan 2026 14:10:08 -0500 Subject: [PATCH 1/5] feat: add comprehensive auth debugging logs Add detailed logging for cross-domain authentication debugging: Frontend: - Log auth client configuration (baseURL, environment) - Log login flow steps (type detection, result, navigation) - Log session fetch in root beforeLoad - Log navigation steps after auth (session state, cookies, router) Backend: - Log Better Auth configuration (cookie domain, CORS origins) - Log CORS middleware configuration - Log incoming auth requests (method, path, origin) - Log Set-Cookie headers in auth responses This will help diagnose staging login redirect issues where frontend and API are on different subdomains. Co-Authored-By: Claude Sonnet 4.5 --- packages/api/src/auth/better-auth.ts | 10 ++++++++ packages/api/src/hono/app.ts | 22 ++++++++++++++++-- packages/app/src/lib/auth-client.ts | 15 ++++++------ packages/app/src/lib/hooks/useAuth.ts | 33 +++++++++++++++++++++++---- packages/app/src/routes/__root.tsx | 9 +++++++- 5 files changed, 74 insertions(+), 15 deletions(-) diff --git a/packages/api/src/auth/better-auth.ts b/packages/api/src/auth/better-auth.ts index 2849a1bb..ff190282 100644 --- a/packages/api/src/auth/better-auth.ts +++ b/packages/api/src/auth/better-auth.ts @@ -86,6 +86,16 @@ export function createAuth(env: Env, db?: ReturnType) { } : {}; + // Log Better Auth configuration (for debugging cross-domain issues) + console.log("🔧 Better Auth Configuration:", { + apiUrl, + frontendUrl, + trustedOrigins, + hasCookieDomain: !!env.COOKIE_DOMAIN, + cookieDomain: env.COOKIE_DOMAIN || "(not set)", + crossSubDomainEnabled: !!env.COOKIE_DOMAIN, + }); + return betterAuth({ database: drizzleAdapter(database, { provider: "sqlite", diff --git a/packages/api/src/hono/app.ts b/packages/api/src/hono/app.ts index dcbec418..0228ccd4 100644 --- a/packages/api/src/hono/app.ts +++ b/packages/api/src/hono/app.ts @@ -39,10 +39,15 @@ export function createHonoApp(config: HonoAppConfig) { }); // CORS middleware (must be before routes) + const corsOrigins = getCorsOrigins(config.env); + console.log("🔧 CORS Configuration:", { + origins: corsOrigins, + credentials: true, + }); app.use( "*", cors({ - origin: getCorsOrigins(config.env), + origin: corsOrigins, credentials: true, allowMethods: ["GET", "POST", "OPTIONS"], allowHeaders: [ @@ -155,9 +160,22 @@ export function createHonoApp(config: HonoAppConfig) { // BetterAuth routes app.on(["POST", "GET"], "/api/auth/*", async (c) => { + const path = c.req.path; + const origin = c.req.header("origin"); + console.log(`[Auth] 📥 ${c.req.method} ${path} from origin: ${origin}`); + const { createAuth } = await import("../auth/better-auth"); const auth = createAuth(c.get("env")); - return auth.handler(c.req.raw); + const response = await auth.handler(c.req.raw); + + // Log response headers (especially Set-Cookie) + const setCookieHeader = response.headers.get("set-cookie"); + if (setCookieHeader) { + console.log(`[Auth] 🍪 Setting cookies:`, setCookieHeader); + } + + console.log(`[Auth] 📤 Response status: ${response.status}`); + return response; }); // tRPC routes using @hono/trpc-server middleware diff --git a/packages/app/src/lib/auth-client.ts b/packages/app/src/lib/auth-client.ts index b702ef2e..13561d78 100644 --- a/packages/app/src/lib/auth-client.ts +++ b/packages/app/src/lib/auth-client.ts @@ -16,14 +16,13 @@ const baseURL = viteApiUrl ? new URL(viteApiUrl).origin : "http://localhost:3001"; // Default to API server in development -// Debug logging in development -if (import.meta.env.DEV) { - console.log("🔧 Better Auth Client Configuration:", { - VITE_API_URL: viteApiUrl, - baseURL, - mode: import.meta.env.MODE, - }); -} +// Debug logging (always enabled for debugging cross-domain issues) +console.log("🔧 Better Auth Client Configuration:", { + VITE_API_URL: viteApiUrl, + baseURL, + mode: import.meta.env.MODE, + environment: import.meta.env.VITE_SENTRY_ENVIRONMENT || "development", +}); export const authClient = createAuthClient({ baseURL, diff --git a/packages/app/src/lib/hooks/useAuth.ts b/packages/app/src/lib/hooks/useAuth.ts index 554b5eb8..226d84f1 100644 --- a/packages/app/src/lib/hooks/useAuth.ts +++ b/packages/app/src/lib/hooks/useAuth.ts @@ -26,20 +26,36 @@ const navigateAfterAuth = async ( router: ReturnType ): Promise => { try { + console.log("[Auth] 🔄 Starting post-login navigation..."); + // Get fresh session from Better Auth - await authClient.getSession(); + console.log("[Auth] 📡 Fetching fresh session from API..."); + const sessionResult = await authClient.getSession(); + console.log("[Auth] ✅ Session fetch result:", { + hasData: !!sessionResult?.data, + hasUser: !!sessionResult?.data?.user, + userId: sessionResult?.data?.user?.id, + hasSession: !!sessionResult?.data?.session, + }); + + // Log cookies for debugging + console.log("[Auth] 🍪 Current cookies:", document.cookie); // Invalidate router to force root beforeLoad to re-run with fresh session + console.log("[Auth] 🔄 Invalidating router..."); await router.invalidate({ sync: true }); // Navigate to articles page // Email verification can be checked on protected routes + console.log("[Auth] 🚀 Navigating to /app/articles..."); await router.navigate({ to: "/app/articles", search: { category_id: undefined, subscription_id: undefined }, }); + console.log("[Auth] ✅ Navigation completed successfully"); } catch (error) { - console.error("Navigation failed:", error); + console.error("[Auth] ❌ Navigation failed:", error); + console.log("[Auth] 🔄 Falling back to hard navigation..."); // Fallback to hard navigation window.location.href = "/app/articles"; } @@ -55,8 +71,10 @@ export const useLogin = () => { const mutate = async (values: { username: string; password: string }) => { setIsPending(true); try { + console.log("[Auth] 🔐 Starting login..."); // Detect if input is email or username const isEmail = values.username.includes("@"); + console.log("[Auth] 📧 Login type:", isEmail ? "email" : "username"); let result; if (isEmail) { @@ -71,17 +89,24 @@ export const useLogin = () => { }); } + console.log("[Auth] 📥 Login result:", { + hasError: !!result.error, + hasData: !!result.data, + dataKeys: result.data ? Object.keys(result.data) : [], + }); + if (result.error) { - console.error("Login error:", result.error); + console.error("[Auth] ❌ Login error:", result.error); toast.error(result.error.message || "Invalid credentials"); return; } + console.log("[Auth] ✅ Login successful!"); toast.success("Welcome back!"); await queryClient.invalidateQueries(); await navigateAfterAuth(router); } catch (error) { - console.error("Login error:", error); + console.error("[Auth] ❌ Login exception:", error); toast.error((error as Error).message || "Invalid credentials"); } finally { setIsPending(false); diff --git a/packages/app/src/routes/__root.tsx b/packages/app/src/routes/__root.tsx index 881d0a7d..8924d899 100644 --- a/packages/app/src/routes/__root.tsx +++ b/packages/app/src/routes/__root.tsx @@ -99,8 +99,15 @@ function RootErrorComponent({ error }: { error: Error }) { export const Route = createRootRouteWithContext()({ beforeLoad: async () => { // Fetch session once at root level + console.log("[Root] 🔄 beforeLoad: Fetching session..."); try { const sessionResult = await authClient.getSession(); + console.log("[Root] ✅ Session result:", { + hasData: !!sessionResult?.data, + hasUser: !!sessionResult?.data?.user, + userId: sessionResult?.data?.user?.id, + hasSession: !!sessionResult?.data?.session, + }); // Set Sentry user context for Session Replay identification (non-PII) if (sessionResult?.data?.user?.id) { @@ -132,7 +139,7 @@ export const Route = createRootRouteWithContext()({ // Fail open - allow navigation without session // The individual route guards will handle redirects - console.warn("Failed to fetch session at root level:", error); + console.warn("[Root] ❌ Failed to fetch session at root level:", error); return { auth: { session: null, From d9137b5ac1ab268058f9d7095e40504e48ddb802 Mon Sep 17 00:00:00 2001 From: KyleTryon Date: Sun, 11 Jan 2026 17:56:41 -0500 Subject: [PATCH 2/5] fix: enable cross-origin cookies for staging auth Fixes authentication on staging where frontend (staging.tuvix.app) and API (tuvix-api-staging.cf-93e.workers.dev) are on different domains. Changes: - Client: Add credentials: 'include' to Better Auth client for cross-origin cookies - Server: Set sameSite=none, secure=true, partitioned=true for cross-domain auth - Secrets: Remove COOKIE_DOMAIN from staging (only for subdomains, not cross-domain) Co-Authored-By: Claude Sonnet 4.5 --- packages/api/src/auth/better-auth.ts | 14 +++++++++++++- packages/app/src/lib/auth-client.ts | 5 +++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/api/src/auth/better-auth.ts b/packages/api/src/auth/better-auth.ts index ff190282..15779c72 100644 --- a/packages/api/src/auth/better-auth.ts +++ b/packages/api/src/auth/better-auth.ts @@ -139,8 +139,20 @@ export function createAuth(env: Env, db?: ReturnType) { ipAddress: { ipAddressHeaders: ["cf-connecting-ip", "x-real-ip", "x-forwarded-for"], }, - // Cross-subdomain cookies configuration + // Cross-subdomain cookies configuration (for subdomains like auth.example.com and app.example.com) ...crossSubDomainConfig, + // Cross-domain cookie attributes (for completely different domains) + // Required when frontend and API are on different domains (e.g., staging.tuvix.app vs tuvix-api-staging.cf-93e.workers.dev) + // Only set these if we're NOT using cross-subdomain cookies (different use case) + ...(env.COOKIE_DOMAIN + ? {} + : { + defaultCookieAttributes: { + sameSite: "none" as const, + secure: true, + partitioned: true, // New browser standards mandate this for foreign cookies + }, + }), }, emailAndPassword: { enabled: true, diff --git a/packages/app/src/lib/auth-client.ts b/packages/app/src/lib/auth-client.ts index 13561d78..41bb8179 100644 --- a/packages/app/src/lib/auth-client.ts +++ b/packages/app/src/lib/auth-client.ts @@ -26,6 +26,11 @@ console.log("🔧 Better Auth Client Configuration:", { export const authClient = createAuthClient({ baseURL, + // CRITICAL: Include credentials (cookies) in cross-origin requests + // Required when API and frontend are on different domains (e.g., staging.tuvix.app vs tuvix-api-staging.cf-93e.workers.dev) + fetchOptions: { + credentials: "include", + }, plugins: [ // Custom session plugin for type inference // This ensures TypeScript knows about the banned field we added via customSession From b25e70ce7a579a438d8627c2a3355264bb487740 Mon Sep 17 00:00:00 2001 From: KyleTryon Date: Sun, 11 Jan 2026 18:08:27 -0500 Subject: [PATCH 3/5] fix: revert to cross-subdomain cookies for staging Staging uses subdomains (staging.tuvix.app and api-staging.tuvix.app), not completely different domains. Requires cross-subdomain cookies. Changes: - Reverted cross-origin cookie attributes (sameSite=none, etc.) - Set COOKIE_DOMAIN=.tuvix.app for subdomain cookie sharing - Keep credentials: 'include' on client (needed for CORS with credentials) Co-Authored-By: Claude Sonnet 4.5 --- packages/api/src/auth/better-auth.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/api/src/auth/better-auth.ts b/packages/api/src/auth/better-auth.ts index 15779c72..ff190282 100644 --- a/packages/api/src/auth/better-auth.ts +++ b/packages/api/src/auth/better-auth.ts @@ -139,20 +139,8 @@ export function createAuth(env: Env, db?: ReturnType) { ipAddress: { ipAddressHeaders: ["cf-connecting-ip", "x-real-ip", "x-forwarded-for"], }, - // Cross-subdomain cookies configuration (for subdomains like auth.example.com and app.example.com) + // Cross-subdomain cookies configuration ...crossSubDomainConfig, - // Cross-domain cookie attributes (for completely different domains) - // Required when frontend and API are on different domains (e.g., staging.tuvix.app vs tuvix-api-staging.cf-93e.workers.dev) - // Only set these if we're NOT using cross-subdomain cookies (different use case) - ...(env.COOKIE_DOMAIN - ? {} - : { - defaultCookieAttributes: { - sameSite: "none" as const, - secure: true, - partitioned: true, // New browser standards mandate this for foreign cookies - }, - }), }, emailAndPassword: { enabled: true, From e6a7676fe0ea634fa4d82bfe9c3ca95a2a370100 Mon Sep 17 00:00:00 2001 From: KyleTryon Date: Sun, 11 Jan 2026 20:12:20 -0500 Subject: [PATCH 4/5] chore: format --- packages/api/src/hono/app.ts | 5 +++-- packages/api/src/routers/subscriptions.ts | 15 +++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/api/src/hono/app.ts b/packages/api/src/hono/app.ts index 0228ccd4..ca7bf7f6 100644 --- a/packages/api/src/hono/app.ts +++ b/packages/api/src/hono/app.ts @@ -201,8 +201,9 @@ export function createHonoApp(config: HonoAppConfig) { const { username, slug } = c.req.param(); const env = c.get("env"); const { getUserLimits } = await import("../services/limits"); - const { checkPublicFeedRateLimit } = - await import("../services/rate-limiter"); + const { checkPublicFeedRateLimit } = await import( + "../services/rate-limiter" + ); const schema = await import("../db/schema"); const { sql, eq, and } = await import("drizzle-orm"); diff --git a/packages/api/src/routers/subscriptions.ts b/packages/api/src/routers/subscriptions.ts index 29079023..bd271a92 100644 --- a/packages/api/src/routers/subscriptions.ts +++ b/packages/api/src/routers/subscriptions.ts @@ -974,8 +974,9 @@ export const subscriptionsRouter = router({ FeedValidationError, FeedDiscoveryError, } = await import("@tuvixrss/tricorder"); - const { sentryTelemetryAdapter } = - await import("@/adapters/sentry-telemetry"); + const { sentryTelemetryAdapter } = await import( + "@/adapters/sentry-telemetry" + ); // Use the extensible discovery system with Sentry telemetry let discoveredFeeds; @@ -1401,8 +1402,9 @@ export const subscriptionsRouter = router({ { name: "ai.getSuggestions", op: "ai.categorize" }, async () => { const { checkAiFeatureAccess } = await import("@/services/limits"); - const { suggestCategories } = - await import("@/services/ai-category-suggester"); + const { suggestCategories } = await import( + "@/services/ai-category-suggester" + ); const env = ctx.env as { OPENAI_API_KEY?: string }; const access = await checkAiFeatureAccess(ctx.db, userId, env); @@ -2230,8 +2232,9 @@ export const subscriptionsRouter = router({ // Immediately fetch articles for the new subscription // This matches single subscription behavior and provides instant feedback try { - const { fetchSingleFeed } = - await import("@/services/rss-fetcher"); + const { fetchSingleFeed } = await import( + "@/services/rss-fetcher" + ); await fetchSingleFeed(sourceId, normalizedFeedUrl, ctx.db); Sentry.addBreadcrumb({ From bb006b0ae1a92d5a2cf3e9d82ce9b61a759fa612 Mon Sep 17 00:00:00 2001 From: KyleTryon Date: Sun, 11 Jan 2026 20:25:08 -0500 Subject: [PATCH 5/5] chore: format --- packages/api/src/hono/app.ts | 5 ++--- packages/api/src/routers/subscriptions.ts | 15 ++++++--------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/api/src/hono/app.ts b/packages/api/src/hono/app.ts index ca7bf7f6..0228ccd4 100644 --- a/packages/api/src/hono/app.ts +++ b/packages/api/src/hono/app.ts @@ -201,9 +201,8 @@ export function createHonoApp(config: HonoAppConfig) { const { username, slug } = c.req.param(); const env = c.get("env"); const { getUserLimits } = await import("../services/limits"); - const { checkPublicFeedRateLimit } = await import( - "../services/rate-limiter" - ); + const { checkPublicFeedRateLimit } = + await import("../services/rate-limiter"); const schema = await import("../db/schema"); const { sql, eq, and } = await import("drizzle-orm"); diff --git a/packages/api/src/routers/subscriptions.ts b/packages/api/src/routers/subscriptions.ts index bd271a92..29079023 100644 --- a/packages/api/src/routers/subscriptions.ts +++ b/packages/api/src/routers/subscriptions.ts @@ -974,9 +974,8 @@ export const subscriptionsRouter = router({ FeedValidationError, FeedDiscoveryError, } = await import("@tuvixrss/tricorder"); - const { sentryTelemetryAdapter } = await import( - "@/adapters/sentry-telemetry" - ); + const { sentryTelemetryAdapter } = + await import("@/adapters/sentry-telemetry"); // Use the extensible discovery system with Sentry telemetry let discoveredFeeds; @@ -1402,9 +1401,8 @@ export const subscriptionsRouter = router({ { name: "ai.getSuggestions", op: "ai.categorize" }, async () => { const { checkAiFeatureAccess } = await import("@/services/limits"); - const { suggestCategories } = await import( - "@/services/ai-category-suggester" - ); + const { suggestCategories } = + await import("@/services/ai-category-suggester"); const env = ctx.env as { OPENAI_API_KEY?: string }; const access = await checkAiFeatureAccess(ctx.db, userId, env); @@ -2232,9 +2230,8 @@ export const subscriptionsRouter = router({ // Immediately fetch articles for the new subscription // This matches single subscription behavior and provides instant feedback try { - const { fetchSingleFeed } = await import( - "@/services/rss-fetcher" - ); + const { fetchSingleFeed } = + await import("@/services/rss-fetcher"); await fetchSingleFeed(sourceId, normalizedFeedUrl, ctx.db); Sentry.addBreadcrumb({