From 1b3c832fc30f82df7fd42b6450e6128c5b343575 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Wed, 7 Jan 2026 18:17:58 -0500 Subject: [PATCH] change template loading --- src/routes/api/auth/callback/github.ts | 62 ++++++++++++++++- src/routes/api/auth/callback/google.ts | 62 ++++++++++++++++- src/server/api/routers/auth.ts | 94 +++++++++++++++++++++++--- src/server/email-templates/index.ts | 37 +++------- 4 files changed, 214 insertions(+), 41 deletions(-) diff --git a/src/routes/api/auth/callback/github.ts b/src/routes/api/auth/callback/github.ts index 0df492b..c3a5ced 100644 --- a/src/routes/api/auth/callback/github.ts +++ b/src/routes/api/auth/callback/github.ts @@ -1,13 +1,21 @@ import type { APIEvent } from "@solidjs/start/server"; import { appRouter } from "~/server/api/root"; import { createTRPCContext } from "~/server/api/utils"; +import { getResponseHeaders } from "vinxi/http"; export async function GET(event: APIEvent) { const url = new URL(event.request.url); const code = url.searchParams.get("code"); const error = url.searchParams.get("error"); + console.log("[GitHub OAuth Callback] Request received:", { + hasCode: !!code, + codeLength: code?.length, + error + }); + if (error) { + console.error("[GitHub OAuth Callback] OAuth error from provider:", error); return new Response(null, { status: 302, headers: { Location: `/login?error=${encodeURIComponent(error)}` } @@ -15,6 +23,7 @@ export async function GET(event: APIEvent) { } if (!code) { + console.error("[GitHub OAuth Callback] Missing authorization code"); return new Response(null, { status: 302, headers: { Location: "/login?error=missing_code" } @@ -22,28 +31,77 @@ export async function GET(event: APIEvent) { } try { + console.log("[GitHub OAuth Callback] Creating tRPC caller..."); const ctx = await createTRPCContext(event); const caller = appRouter.createCaller(ctx); + console.log("[GitHub OAuth Callback] Calling githubCallback procedure..."); const result = await caller.auth.githubCallback({ code }); + console.log("[GitHub OAuth Callback] Result:", result); + if (result.success) { + console.log( + "[GitHub OAuth Callback] Login successful, redirecting to:", + result.redirectTo + ); + + // Get the response headers that were set by the session (includes Set-Cookie) + const responseHeaders = getResponseHeaders(event.nativeEvent); + console.log( + "[GitHub OAuth Callback] Response headers from event:", + Object.keys(responseHeaders) + ); + + // Create redirect response with the session cookie + const redirectUrl = result.redirectTo || "/account"; + const headers = new Headers({ + Location: redirectUrl + }); + + // Copy Set-Cookie headers from the session response + if (responseHeaders["set-cookie"]) { + const cookies = Array.isArray(responseHeaders["set-cookie"]) + ? responseHeaders["set-cookie"] + : [responseHeaders["set-cookie"]]; + + console.log("[GitHub OAuth Callback] Found cookies:", cookies.length); + cookies.forEach((cookie) => { + headers.append("Set-Cookie", cookie); + console.log( + "[GitHub OAuth Callback] Adding cookie:", + cookie.substring(0, 50) + "..." + ); + }); + } else { + console.error("[GitHub OAuth Callback] NO SET-COOKIE HEADER FOUND!"); + console.error("[GitHub OAuth Callback] All headers:", responseHeaders); + } + return new Response(null, { status: 302, - headers: { Location: result.redirectTo || "/account" } + headers }); } else { + console.error( + "[GitHub OAuth Callback] Login failed (result.success=false)" + ); return new Response(null, { status: 302, headers: { Location: "/login?error=auth_failed" } }); } } catch (error) { - console.error("GitHub OAuth callback error:", error); + console.error("[GitHub OAuth Callback] Error caught:", error); if (error && typeof error === "object" && "code" in error) { const trpcError = error as { code: string; message?: string }; + console.error("[GitHub OAuth Callback] tRPC error:", { + code: trpcError.code, + message: trpcError.message + }); + if (trpcError.code === "CONFLICT") { return new Response(null, { status: 302, diff --git a/src/routes/api/auth/callback/google.ts b/src/routes/api/auth/callback/google.ts index 5c6606b..b0417d7 100644 --- a/src/routes/api/auth/callback/google.ts +++ b/src/routes/api/auth/callback/google.ts @@ -1,13 +1,21 @@ import type { APIEvent } from "@solidjs/start/server"; import { appRouter } from "~/server/api/root"; import { createTRPCContext } from "~/server/api/utils"; +import { getResponseHeaders } from "vinxi/http"; export async function GET(event: APIEvent) { const url = new URL(event.request.url); const code = url.searchParams.get("code"); const error = url.searchParams.get("error"); + console.log("[Google OAuth Callback] Request received:", { + hasCode: !!code, + codeLength: code?.length, + error + }); + if (error) { + console.error("[Google OAuth Callback] OAuth error from provider:", error); return new Response(null, { status: 302, headers: { Location: `/login?error=${encodeURIComponent(error)}` } @@ -15,6 +23,7 @@ export async function GET(event: APIEvent) { } if (!code) { + console.error("[Google OAuth Callback] Missing authorization code"); return new Response(null, { status: 302, headers: { Location: "/login?error=missing_code" } @@ -22,28 +31,77 @@ export async function GET(event: APIEvent) { } try { + console.log("[Google OAuth Callback] Creating tRPC caller..."); const ctx = await createTRPCContext(event); const caller = appRouter.createCaller(ctx); + console.log("[Google OAuth Callback] Calling googleCallback procedure..."); const result = await caller.auth.googleCallback({ code }); + console.log("[Google OAuth Callback] Result:", result); + if (result.success) { + console.log( + "[Google OAuth Callback] Login successful, redirecting to:", + result.redirectTo + ); + + // Get the response headers that were set by the session (includes Set-Cookie) + const responseHeaders = getResponseHeaders(event.nativeEvent); + console.log( + "[Google OAuth Callback] Response headers from event:", + Object.keys(responseHeaders) + ); + + // Create redirect response with the session cookie + const redirectUrl = result.redirectTo || "/account"; + const headers = new Headers({ + Location: redirectUrl + }); + + // Copy Set-Cookie headers from the session response + if (responseHeaders["set-cookie"]) { + const cookies = Array.isArray(responseHeaders["set-cookie"]) + ? responseHeaders["set-cookie"] + : [responseHeaders["set-cookie"]]; + + console.log("[Google OAuth Callback] Found cookies:", cookies.length); + cookies.forEach((cookie) => { + headers.append("Set-Cookie", cookie); + console.log( + "[Google OAuth Callback] Adding cookie:", + cookie.substring(0, 50) + "..." + ); + }); + } else { + console.error("[Google OAuth Callback] NO SET-COOKIE HEADER FOUND!"); + console.error("[Google OAuth Callback] All headers:", responseHeaders); + } + return new Response(null, { status: 302, - headers: { Location: result.redirectTo || "/account" } + headers }); } else { + console.error( + "[Google OAuth Callback] Login failed (result.success=false)" + ); return new Response(null, { status: 302, headers: { Location: "/login?error=auth_failed" } }); } } catch (error) { - console.error("Google OAuth callback error:", error); + console.error("[Google OAuth Callback] Error caught:", error); if (error && typeof error === "object" && "code" in error) { const trpcError = error as { code: string; message?: string }; + console.error("[Google OAuth Callback] tRPC error:", { + code: trpcError.code, + message: trpcError.message + }); + if (trpcError.code === "CONFLICT") { return new Response(null, { status: 302, diff --git a/src/server/api/routers/auth.ts b/src/server/api/routers/auth.ts index 7d8c2db..4c3210b 100644 --- a/src/server/api/routers/auth.ts +++ b/src/server/api/routers/auth.ts @@ -173,7 +173,13 @@ export const authRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const { code } = input; + console.log( + "[GitHub Callback] Starting OAuth flow with code:", + code.substring(0, 10) + "..." + ); + try { + console.log("[GitHub Callback] Exchanging code for access token..."); const tokenResponse = await fetchWithTimeout( "https://github.com/login/oauth/access_token", { @@ -195,12 +201,16 @@ export const authRouter = createTRPCRouter({ const { access_token } = await tokenResponse.json(); if (!access_token) { + console.error("[GitHub Callback] No access token received"); throw new TRPCError({ code: "UNAUTHORIZED", message: "Failed to get access token from GitHub" }); } + console.log( + "[GitHub Callback] Access token received, fetching user data..." + ); const userResponse = await fetchWithTimeout( "https://api.github.com/user", { @@ -216,6 +226,9 @@ export const authRouter = createTRPCRouter({ const login = user.login; const icon = user.avatar_url; + console.log("[GitHub Callback] User data received:", { login }); + + console.log("[GitHub Callback] Fetching user emails..."); const emailsResponse = await fetchWithTimeout( "https://api.github.com/user/emails", { @@ -236,8 +249,16 @@ export const authRouter = createTRPCRouter({ const email = primaryEmail?.email || null; const emailVerified = primaryEmail?.verified || false; + console.log( + "[GitHub Callback] Primary email:", + email, + "verified:", + emailVerified + ); + const conn = ConnectionFactory(); + console.log("[GitHub Callback] Checking if user exists..."); const query = `SELECT * FROM User WHERE provider = ? AND display_name = ?`; const params = ["github", login]; const res = await conn.execute({ sql: query, args: params }); @@ -246,17 +267,23 @@ export const authRouter = createTRPCRouter({ if (res.rows[0]) { userId = (res.rows[0] as unknown as User).id; + console.log("[GitHub Callback] Existing user found:", userId); try { await conn.execute({ sql: `UPDATE User SET email = ?, email_verified = ?, image = ? WHERE id = ?`, args: [email, emailVerified ? 1 : 0, icon, userId] }); + console.log("[GitHub Callback] User data updated"); } catch (updateError: any) { if ( updateError.code === "SQLITE_CONSTRAINT" && updateError.message?.includes("User.email") ) { + console.error( + "[GitHub Callback] Email conflict during update:", + email + ); throw new TRPCError({ code: "CONFLICT", message: @@ -267,6 +294,7 @@ export const authRouter = createTRPCRouter({ } } else { userId = uuidV4(); + console.log("[GitHub Callback] Creating new user:", userId); const insertQuery = `INSERT INTO User (id, email, email_verified, display_name, provider, image) VALUES (?, ?, ?, ?, ?, ?)`; const insertParams = [ @@ -280,11 +308,16 @@ export const authRouter = createTRPCRouter({ try { await conn.execute({ sql: insertQuery, args: insertParams }); + console.log("[GitHub Callback] New user created"); } catch (insertError: any) { if ( insertError.code === "SQLITE_CONSTRAINT" && insertError.message?.includes("User.email") ) { + console.error( + "[GitHub Callback] Email conflict during insert:", + email + ); throw new TRPCError({ code: "CONFLICT", message: @@ -297,6 +330,7 @@ export const authRouter = createTRPCRouter({ const isAdmin = userId === env.ADMIN_ID; + console.log("[GitHub Callback] Creating session for user:", userId); // Create session with Vinxi (OAuth defaults to remember me) const clientIP = getClientIP(getH3Event(ctx)); const userAgent = getUserAgent(getH3Event(ctx)); @@ -312,6 +346,8 @@ export const authRouter = createTRPCRouter({ // Set CSRF token for authenticated session setCSRFToken(getH3Event(ctx)); + console.log("[GitHub Callback] Session created successfully"); + // Log successful OAuth login await logAuditEvent({ userId, @@ -322,11 +358,14 @@ export const authRouter = createTRPCRouter({ success: true }); + console.log("[GitHub Callback] OAuth flow completed successfully"); return { success: true, redirectTo: "/account" }; } catch (error) { + console.error("[GitHub Callback] Error during OAuth flow:", error); + // Log failed OAuth login const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx)); await logAuditEvent({ @@ -345,26 +384,30 @@ export const authRouter = createTRPCRouter({ } if (error instanceof TimeoutError) { - console.error("GitHub API timeout:", error.message); + console.error("[GitHub Callback] Timeout:", error.message); throw new TRPCError({ code: "TIMEOUT", message: "GitHub authentication timed out. Please try again." }); } else if (error instanceof NetworkError) { - console.error("GitHub API network error:", error.message); + console.error("[GitHub Callback] Network error:", error.message); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Unable to connect to GitHub. Please try again later." }); } else if (error instanceof APIError) { - console.error("GitHub API error:", error.status, error.statusText); + console.error( + "[GitHub Callback] API error:", + error.status, + error.statusText + ); throw new TRPCError({ code: "BAD_REQUEST", message: "GitHub authentication failed. Please try again." }); } - console.error("GitHub authentication failed:", error); + console.error("[GitHub Callback] Unknown error:", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "GitHub authentication failed" @@ -377,7 +420,13 @@ export const authRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const { code } = input; + console.log( + "[Google Callback] Starting OAuth flow with code:", + code.substring(0, 10) + "..." + ); + try { + console.log("[Google Callback] Exchanging code for access token..."); const tokenResponse = await fetchWithTimeout( "https://oauth2.googleapis.com/token", { @@ -400,12 +449,16 @@ export const authRouter = createTRPCRouter({ const { access_token } = await tokenResponse.json(); if (!access_token) { + console.error("[Google Callback] No access token received"); throw new TRPCError({ code: "UNAUTHORIZED", message: "Failed to get access token from Google" }); } + console.log( + "[Google Callback] Access token received, fetching user data..." + ); const userResponse = await fetchWithTimeout( "https://www.googleapis.com/oauth2/v3/userinfo", { @@ -423,8 +476,15 @@ export const authRouter = createTRPCRouter({ const email = userData.email; const email_verified = userData.email_verified; + console.log("[Google Callback] User data received:", { + name, + email, + email_verified + }); + const conn = ConnectionFactory(); + console.log("[Google Callback] Checking if user exists..."); const query = `SELECT * FROM User WHERE provider = ? AND email = ?`; const params = ["google", email]; const res = await conn.execute({ sql: query, args: params }); @@ -433,13 +493,16 @@ export const authRouter = createTRPCRouter({ if (res.rows[0]) { userId = (res.rows[0] as unknown as User).id; + console.log("[Google Callback] Existing user found:", userId); await conn.execute({ sql: `UPDATE User SET email = ?, email_verified = ?, display_name = ?, image = ? WHERE id = ?`, args: [email, email_verified ? 1 : 0, name, image, userId] }); + console.log("[Google Callback] User data updated"); } else { userId = uuidV4(); + console.log("[Google Callback] Creating new user:", userId); const insertQuery = `INSERT INTO User (id, email, email_verified, display_name, provider, image) VALUES (?, ?, ?, ?, ?, ?)`; const insertParams = [ @@ -456,11 +519,16 @@ export const authRouter = createTRPCRouter({ sql: insertQuery, args: insertParams }); + console.log("[Google Callback] New user created"); } catch (insertError: any) { if ( insertError.code === "SQLITE_CONSTRAINT" && insertError.message?.includes("User.email") ) { + console.error( + "[Google Callback] Email conflict during insert:", + email + ); throw new TRPCError({ code: "CONFLICT", message: @@ -473,6 +541,7 @@ export const authRouter = createTRPCRouter({ const isAdmin = userId === env.ADMIN_ID; + console.log("[Google Callback] Creating session for user:", userId); // Create session with Vinxi (OAuth defaults to remember me) const clientIP = getClientIP(getH3Event(ctx)); const userAgent = getUserAgent(getH3Event(ctx)); @@ -488,6 +557,8 @@ export const authRouter = createTRPCRouter({ // Set CSRF token for authenticated session setCSRFToken(getH3Event(ctx)); + console.log("[Google Callback] Session created successfully"); + // Log successful OAuth login await logAuditEvent({ userId, @@ -498,11 +569,14 @@ export const authRouter = createTRPCRouter({ success: true }); + console.log("[Google Callback] OAuth flow completed successfully"); return { success: true, redirectTo: "/account" }; } catch (error) { + console.error("[Google Callback] Error during OAuth flow:", error); + // Log failed OAuth login const { ipAddress, userAgent } = getAuditContext(getH3Event(ctx)); await logAuditEvent({ @@ -521,26 +595,30 @@ export const authRouter = createTRPCRouter({ } if (error instanceof TimeoutError) { - console.error("Google API timeout:", error.message); + console.error("[Google Callback] Timeout:", error.message); throw new TRPCError({ code: "TIMEOUT", message: "Google authentication timed out. Please try again." }); } else if (error instanceof NetworkError) { - console.error("Google API network error:", error.message); + console.error("[Google Callback] Network error:", error.message); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Unable to connect to Google. Please try again later." }); } else if (error instanceof APIError) { - console.error("Google API error:", error.status, error.statusText); + console.error( + "[Google Callback] API error:", + error.status, + error.statusText + ); throw new TRPCError({ code: "BAD_REQUEST", message: "Google authentication failed. Please try again." }); } - console.error("Google authentication failed:", error); + console.error("[Google Callback] Unknown error:", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Google authentication failed" diff --git a/src/server/email-templates/index.ts b/src/server/email-templates/index.ts index 20f2f9f..646615a 100644 --- a/src/server/email-templates/index.ts +++ b/src/server/email-templates/index.ts @@ -1,7 +1,10 @@ -import { readFileSync } from "fs"; -import { join } from "path"; import { AUTH_CONFIG } from "~/config"; +// Import email templates as raw strings - Vite will bundle these at build time +import loginLinkTemplate from "./login-link.html?raw"; +import passwordResetTemplate from "./password-reset.html?raw"; +import emailVerificationTemplate from "./email-verification.html?raw"; + /** * Convert expiry string to human-readable format * @param expiry - Expiry string like "15m", "1h", "7d" @@ -19,27 +22,6 @@ export function expiryToHuman(expiry: string): string { return expiry; } -/** - * Load email template from file - * @param templateName - Name of the template file (without .html extension) - * @returns Template content as string - */ -function loadTemplate(templateName: string): string { - try { - const templatePath = join( - process.cwd(), - "src", - "server", - "email-templates", - `${templateName}.html` - ); - return readFileSync(templatePath, "utf-8"); - } catch (error) { - console.error(`Failed to load email template: ${templateName}`, error); - throw new Error(`Email template not found: ${templateName}`); - } -} - /** * Replace placeholders in template with actual values * @param template - Template string with {{PLACEHOLDER}} markers @@ -68,10 +50,9 @@ export interface LoginLinkEmailParams { * Generate login link email HTML */ export function generateLoginLinkEmail(params: LoginLinkEmailParams): string { - const template = loadTemplate("login-link"); const expiryTime = expiryToHuman(AUTH_CONFIG.EMAIL_LOGIN_LINK_EXPIRY); - return processTemplate(template, { + return processTemplate(loginLinkTemplate, { LOGIN_URL: params.loginUrl, LOGIN_CODE: params.loginCode, EXPIRY_TIME: expiryTime @@ -88,10 +69,9 @@ export interface PasswordResetEmailParams { export function generatePasswordResetEmail( params: PasswordResetEmailParams ): string { - const template = loadTemplate("password-reset"); const expiryTime = "1 hour"; // Password reset is hardcoded to 1 hour - return processTemplate(template, { + return processTemplate(passwordResetTemplate, { RESET_URL: params.resetUrl, EXPIRY_TIME: expiryTime }); @@ -107,10 +87,9 @@ export interface EmailVerificationParams { export function generateEmailVerificationEmail( params: EmailVerificationParams ): string { - const template = loadTemplate("email-verification"); const expiryTime = expiryToHuman(AUTH_CONFIG.EMAIL_VERIFICATION_LINK_EXPIRY); - return processTemplate(template, { + return processTemplate(emailVerificationTemplate, { VERIFICATION_URL: params.verificationUrl, EXPIRY_TIME: expiryTime });