diff --git a/src/components/blog/AuthenticatedLike.tsx b/src/components/blog/AuthenticatedLike.tsx index 07ef846..89f175e 100644 --- a/src/components/blog/AuthenticatedLike.tsx +++ b/src/components/blog/AuthenticatedLike.tsx @@ -5,7 +5,7 @@ import LikeIcon from "~/components/icons/LikeIcon"; export interface PostLike { id: number; user_id: string; - post_id: string; + post_id: number; } export interface AuthenticatedLikeProps { @@ -36,15 +36,15 @@ export default function AuthenticatedLike(props: AuthenticatedLikeProps) { if (initialHasLiked) { const result = await api.database.removePostLike.mutate({ user_id: props.currentUserID, - post_id: props.projectID.toString() + post_id: props.projectID }); - setLikes(result.newLikes as PostLike[]); + setLikes(result.newLikes as unknown as PostLike[]); } else { const result = await api.database.addPostLike.mutate({ user_id: props.currentUserID, - post_id: props.projectID.toString() + post_id: props.projectID }); - setLikes(result.newLikes as PostLike[]); + setLikes(result.newLikes as unknown as PostLike[]); } setInstantOffset(0); } catch (error) { diff --git a/src/components/blog/CommentSection.tsx b/src/components/blog/CommentSection.tsx index a1ffc07..abef269 100644 --- a/src/components/blog/CommentSection.tsx +++ b/src/components/blog/CommentSection.tsx @@ -6,7 +6,8 @@ import type { UserPublicData, ReactionType, ModificationType, - SortingMode + SortingMode, + CommentSectionProps } from "~/types/comment"; import CommentInputBlock from "./CommentInputBlock"; import CommentSortingSelect from "./CommentSortingSelect"; diff --git a/src/components/blog/DeletePostButton.tsx b/src/components/blog/DeletePostButton.tsx index 4880322..0823923 100644 --- a/src/components/blog/DeletePostButton.tsx +++ b/src/components/blog/DeletePostButton.tsx @@ -20,6 +20,7 @@ export default function DeletePostButton(props: DeletePostButtonProps) { await api.database.deletePost.mutate({ id: props.postID }); window.location.reload(); } catch (error) { + console.error("Failed to delete post:", error); alert("Failed to delete post"); setLoading(false); } diff --git a/src/components/blog/PostBodyClient.tsx b/src/components/blog/PostBodyClient.tsx index 5ce8449..fe9cbf4 100644 --- a/src/components/blog/PostBodyClient.tsx +++ b/src/components/blog/PostBodyClient.tsx @@ -271,11 +271,12 @@ export default function PostBodyClient(props: PostBodyClientProps) { const headings = contentRef.querySelectorAll("h2"); let referencesSection: HTMLElement | null = null; - headings.forEach((heading) => { + for (const heading of headings) { if (heading.textContent?.trim() === referencesHeadingText) { referencesSection = heading; + break; } - }); + } if (referencesSection) { referencesSection.className = "text-2xl font-bold mb-4 text-text"; @@ -285,7 +286,7 @@ export default function PostBodyClient(props: PostBodyClientProps) { parentDiv.classList.add("references-heading"); } - let currentElement = referencesSection.nextElementSibling; + let currentElement: Element | null = referencesSection.nextElementSibling; while (currentElement) { if (currentElement.tagName === "P") { diff --git a/src/components/blog/PostForm.tsx b/src/components/blog/PostForm.tsx index 7ae11d1..5c11fb8 100644 --- a/src/components/blog/PostForm.tsx +++ b/src/components/blog/PostForm.tsx @@ -22,7 +22,7 @@ interface PostFormProps { published: boolean; tags: string[]; }; - userID: number; + userID: string; } export default function PostForm(props: PostFormProps) { @@ -89,7 +89,7 @@ export default function PostForm(props: PostFormProps) { tags: null, author_id: props.userID }); - const newId = result.data as number; + const newId = Number(result.data); setCreatedPostId(newId); setHasSaved(true); return newId; diff --git a/src/lib/auth-callback-utils.ts b/src/lib/auth-callback-utils.ts new file mode 100644 index 0000000..2921530 --- /dev/null +++ b/src/lib/auth-callback-utils.ts @@ -0,0 +1,79 @@ +import type { APIEvent } from "@solidjs/start/server"; +import { createServerCaller } from "~/server/api/root"; + +/** + * Result from an auth callback tRPC procedure + */ +interface AuthCallbackResult { + success: boolean; + redirectTo?: string; +} + +/** + * Handle a successful auth callback result by redirecting + */ +export function redirectSuccess(result: AuthCallbackResult) { + return new Response(null, { + status: 302, + headers: { Location: result.redirectTo || "/account" } + }); +} + +/** + * Redirect to login with an error parameter + */ +export function redirectError(error: string) { + return new Response(null, { + status: 302, + headers: { Location: `/login?error=${encodeURIComponent(error)}` } + }); +} + +/** + * Handle tRPC CONFLICT error (email already in use) + */ +export function isConflictError(error: unknown): boolean { + return ( + error != null && + typeof error === "object" && + "code" in error && + (error as { code: string }).code === "CONFLICT" + ); +} + +/** + * Create an auth callback handler that calls a tRPC procedure and redirects + */ +export function createAuthCallbackHandler( + procedureName: string, + callProcedure: ( + caller: ReturnType extends Promise + ? T + : never, + params: Params + ) => Promise, + handleError?: (error: unknown) => Response +) { + return async (event: APIEvent, params: Params) => { + try { + const caller = await createServerCaller(event); + const result = await callProcedure(caller, params); + + if (result.success) { + return redirectSuccess(result); + } + + return redirectError("auth_failed"); + } catch (error) { + if (handleError) { + return handleError(error); + } + + if (isConflictError(error)) { + return redirectError("email_in_use"); + } + + return redirectError("server_error"); + } + }; +} diff --git a/src/routes/api/Gaze/appcast.xml.ts b/src/routes/api/Gaze/appcast.xml.ts index 12967f5..2801db1 100644 --- a/src/routes/api/Gaze/appcast.xml.ts +++ b/src/routes/api/Gaze/appcast.xml.ts @@ -8,7 +8,7 @@ import { env } from "~/env/server"; * * URL: https://freno.me/api/Gaze/appcast.xml */ -export async function GET(event: APIEvent) { +export async function GET(_event: APIEvent) { const bucket = env.VITE_DOWNLOAD_BUCKET_STRING; const key = "api/Gaze/appcast.xml"; @@ -47,7 +47,7 @@ export async function GET(event: APIEvent) { headers: { "Content-Type": "application/xml; charset=utf-8", "Cache-Control": "public, max-age=300", // Cache for 5 minutes - "Access-Control-Allow-Origin": "*" // Allow CORS for appcast + "Access-Control-Allow-Origin": "*" // Allow CORS for Sparkle appcast } }); } catch (error) { diff --git a/src/routes/api/InputHalo/appcast.xml.ts b/src/routes/api/InputHalo/appcast.xml.ts index a8d9e1c..1ed049f 100644 --- a/src/routes/api/InputHalo/appcast.xml.ts +++ b/src/routes/api/InputHalo/appcast.xml.ts @@ -8,7 +8,7 @@ import { env } from "~/env/server"; * * URL: https://freno.me/api/InputHalo/appcast.xml */ -export async function GET(event: APIEvent) { +export async function GET(_event: APIEvent) { const bucket = env.VITE_DOWNLOAD_BUCKET_STRING; const key = "api/InputHalo/appcast.xml"; @@ -47,7 +47,7 @@ export async function GET(event: APIEvent) { headers: { "Content-Type": "application/xml; charset=utf-8", "Cache-Control": "public, max-age=300", // Cache for 5 minutes - "Access-Control-Allow-Origin": "*" // Allow CORS for appcast + "Access-Control-Allow-Origin": "*" // Allow CORS for Sparkle appcast } }); } catch (error) { diff --git a/src/routes/api/auth/callback/github.ts b/src/routes/api/auth/callback/github.ts index 1ce182e..2408da2 100644 --- a/src/routes/api/auth/callback/github.ts +++ b/src/routes/api/auth/callback/github.ts @@ -1,86 +1,26 @@ import type { APIEvent } from "@solidjs/start/server"; -import { createServerCaller } from "~/server/api/root"; +import { + createAuthCallbackHandler, + redirectError +} from "~/lib/auth-callback-utils"; 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)}` } - }); + return redirectError(error); } if (!code) { - console.error("[GitHub OAuth Callback] Missing authorization code"); - return new Response(null, { - status: 302, - headers: { Location: "/login?error=missing_code" } - }); + return redirectError("missing_code"); } - try { - console.log("[GitHub OAuth Callback] Creating tRPC caller..."); - const caller = await createServerCaller(event); + const handler = createAuthCallbackHandler<{ code: string }>( + "githubCallback", + (caller, params) => caller.auth.githubCallback(params) + ); - 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 - ); - - // Auth handler already set cookie headers - // Just redirect - the cookies are already in the response - const redirectUrl = result.redirectTo || "/account"; - return new Response(null, { - status: 302, - headers: { Location: redirectUrl } - }); - } 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 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, - headers: { Location: "/login?error=email_in_use" } - }); - } - } - - return new Response(null, { - status: 302, - headers: { Location: "/login?error=server_error" } - }); - } + return handler(event, { code }); } diff --git a/src/routes/api/auth/callback/google.ts b/src/routes/api/auth/callback/google.ts index 19ccdb2..f3fce5b 100644 --- a/src/routes/api/auth/callback/google.ts +++ b/src/routes/api/auth/callback/google.ts @@ -1,86 +1,26 @@ import type { APIEvent } from "@solidjs/start/server"; -import { createServerCaller } from "~/server/api/root"; +import { + createAuthCallbackHandler, + redirectError +} from "~/lib/auth-callback-utils"; 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)}` } - }); + return redirectError(error); } if (!code) { - console.error("[Google OAuth Callback] Missing authorization code"); - return new Response(null, { - status: 302, - headers: { Location: "/login?error=missing_code" } - }); + return redirectError("missing_code"); } - try { - console.log("[Google OAuth Callback] Creating tRPC caller..."); - const caller = await createServerCaller(event); + const handler = createAuthCallbackHandler<{ code: string }>( + "googleCallback", + (caller, params) => caller.auth.googleCallback(params) + ); - 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 - ); - - // Auth handler already set cookie headers - // Just redirect - the cookies are already in the response - const redirectUrl = result.redirectTo || "/account"; - return new Response(null, { - status: 302, - headers: { Location: redirectUrl } - }); - } 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 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, - headers: { Location: "/login?error=email_in_use" } - }); - } - } - - return new Response(null, { - status: 302, - headers: { Location: "/login?error=server_error" } - }); - } + return handler(event, { code }); } diff --git a/src/routes/api/auth/email-login-callback.ts b/src/routes/api/auth/email-login-callback.ts index 56f388d..faadc63 100644 --- a/src/routes/api/auth/email-login-callback.ts +++ b/src/routes/api/auth/email-login-callback.ts @@ -1,86 +1,32 @@ import type { APIEvent } from "@solidjs/start/server"; -import { createServerCaller } from "~/server/api/root"; +import { + createAuthCallbackHandler, + redirectError +} from "~/lib/auth-callback-utils"; export async function GET(event: APIEvent) { const url = new URL(event.request.url); const email = url.searchParams.get("email"); const token = url.searchParams.get("token"); - console.log("[Email Login Callback] Request received:", { - email, - hasToken: !!token, - tokenLength: token?.length - }); - if (!email || !token) { - console.error("[Email Login Callback] Missing required parameters:", { - hasEmail: !!email, - hasToken: !!token - }); - return new Response(null, { - status: 302, - headers: { Location: "/login?error=missing_params" } - }); + return redirectError("missing_params"); } - try { - console.log("[Email Login Callback] Creating tRPC caller..."); - // Create tRPC caller to invoke the emailLogin procedure - const caller = await createServerCaller(event); - - console.log("[Email Login Callback] Calling emailLogin procedure..."); - // Call the email login handler - rememberMe will be read from JWT payload - const result = await caller.auth.emailLogin({ - email, - token - }); - - console.log("[Email Login Callback] Login result:", result); - - if (result.success) { - console.log( - "[Email Login Callback] Login successful, redirecting to:", - result.redirectTo - ); - - // Auth handler already set cookie headers - // Just redirect - the cookies are already in the response - const redirectUrl = result.redirectTo || "/account"; - return new Response(null, { - status: 302, - headers: { Location: redirectUrl } - }); - } else { - console.error( - "[Email Login Callback] Login failed (result.success=false)" - ); - return new Response(null, { - status: 302, - headers: { Location: "/login?error=auth_failed" } - }); + const handler = createAuthCallbackHandler<{ + email: string; + token: string; + }>( + "emailLogin", + (caller, params) => caller.auth.emailLogin(params), + (error) => { + // Check for token expiration + const message = error instanceof Error ? error.message : ""; + const isTokenError = + message.includes("expired") || message.includes("invalid"); + return redirectError(isTokenError ? "link_expired" : "server_error"); } - } catch (error) { - console.error("[Email Login Callback] Error caught:", error); + ); - // Check if it's a token expiration error - const errorMessage = - error instanceof Error ? error.message : "server_error"; - const isTokenError = - errorMessage.includes("expired") || errorMessage.includes("invalid"); - - console.error("[Email Login Callback] Error details:", { - errorMessage, - isTokenError, - errorType: error instanceof Error ? error.constructor.name : typeof error - }); - - return new Response(null, { - status: 302, - headers: { - Location: isTokenError - ? "/login?error=link_expired" - : "/login?error=server_error" - } - }); - } + return handler(event, { email, token }); } diff --git a/src/server/api/schemas/database.ts b/src/server/api/schemas/database.ts index 7d2f5b3..ad8d8aa 100644 --- a/src/server/api/schemas/database.ts +++ b/src/server/api/schemas/database.ts @@ -155,7 +155,9 @@ export const reactionTypeSchema = z.enum([ "moneyEye", "sick", "upsideDown", - "worried" + "worried", + "upVote", + "downVote" ]); /** diff --git a/src/server/auth.ts b/src/server/auth.ts index 4c108ba..7d41c6a 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -1,10 +1,10 @@ import type { H3Event } from "vinxi/http"; -import { getCookie, setCookie } from "vinxi/http"; +import { getCookie, setCookie, getHeader } from "vinxi/http"; import { OAuth2Client } from "google-auth-library"; import type { Row } from "@libsql/client/web"; import { SignJWT, jwtVerify } from "jose"; import { env } from "~/env/server"; -import { ConnectionFactory } from "./database"; +import { ConnectionFactory } from "./db-connections"; import { AUTH_CONFIG, expiryToSeconds, getAccessTokenExpiry } from "~/config"; export const authCookieName = "auth_token"; @@ -30,7 +30,7 @@ function getAuthCookieOptions(rememberMe: boolean) { } function getAuthHeaderToken(event: H3Event): string | null { - const requestHeader = event.request?.headers?.get?.("authorization") || null; + const requestHeader = getHeader(event, "authorization") || null; const eventHeader = event.headers ? typeof (event.headers as any).get === "function" ? (event.headers as any).get("authorization") @@ -199,6 +199,7 @@ export async function validateLineageRequest({ return false; } } catch (err) { + console.error("Failed to verify email auth token:", err); return false; } } else if (provider == "apple") { diff --git a/src/server/database.ts b/src/server/database.ts index 2c5e204..464afc1 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -11,43 +11,13 @@ import { TimeoutError, APIError } from "~/server/fetch-utils"; - -let mainDBConnection: ReturnType | null = null; -let lineageDBConnection: ReturnType | null = null; -let nessaDBConnection: ReturnType | null = null; - -export function ConnectionFactory() { - if (!mainDBConnection) { - const config = { - url: env.TURSO_DB_URL, - authToken: env.TURSO_DB_TOKEN - }; - mainDBConnection = createClient(config); - } - return mainDBConnection; -} - -export function LineageConnectionFactory() { - if (!lineageDBConnection) { - const config = { - url: env.TURSO_LINEAGE_URL, - authToken: env.TURSO_LINEAGE_TOKEN - }; - lineageDBConnection = createClient(config); - } - return lineageDBConnection; -} - -export function NessaConnectionFactory() { - if (!nessaDBConnection) { - const config = { - url: env.NESSA_DB_URL, - authToken: env.NESSA_DB_TOKEN - }; - nessaDBConnection = createClient(config); - } - return nessaDBConnection; -} +import { + ConnectionFactory, + LineageConnectionFactory, + NessaConnectionFactory +} from "~/server/db-connections"; +// Re-export connection factories to avoid circular import with auth.ts +export { ConnectionFactory, LineageConnectionFactory, NessaConnectionFactory }; export async function LineageDBInit() { const turso = createAPIClient({ @@ -209,7 +179,7 @@ export async function getUserBasicInfo(event: H3Event): Promise<{ return { email: null, isAuthenticated: false }; } - const user = res.rows[0] as { email: string | null }; + const user = res.rows[0] as unknown as { email: string | null }; return { email: user.email, isAuthenticated: true diff --git a/src/server/middleare/security-headers.ts b/src/server/middleare/security-headers.ts index fb9b5eb..b08f61f 100644 --- a/src/server/middleare/security-headers.ts +++ b/src/server/middleare/security-headers.ts @@ -1,19 +1,15 @@ -import { defineMiddleware } from "vinxi/http"; +import { defineMiddleware, setHeaders } from "vinxi/http"; // Security headers middleware — sets CSP and hardening headers on all responses -export default defineMiddleware((_event, next) => { - return next().then((response) => { - response.headers.set( - "Content-Security-Policy", - "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" - ); - response.headers.set("X-Content-Type-Options", "nosniff"); - response.headers.set("X-Frame-Options", "DENY"); - response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); - response.headers.set( - "Permissions-Policy", - "camera=(), microphone=(), geolocation=()" - ); - return response; - }); +export default defineMiddleware({ + onRequest: (event) => { + setHeaders(event, { + "Content-Security-Policy": + "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "Referrer-Policy": "strict-origin-when-cross-origin", + "Permissions-Policy": "camera=(), microphone=(), geolocation=()" + }); + } }); diff --git a/src/server/security.ts b/src/server/security.ts index f7c7638..e7b0ba2 100644 --- a/src/server/security.ts +++ b/src/server/security.ts @@ -51,7 +51,12 @@ function getCookieValue(event: H3Event, name: string): string | undefined { try { const value = getCookie(event, name); if (value) return value; - } catch (e) {} + } catch (e) { + console.warn( + "[security] getCookie failed, falling back to header parse:", + e + ); + } try { const cookieHeader = diff --git a/src/types/comment.ts b/src/types/comment.ts index 2e1818c..43d1bfc 100644 --- a/src/types/comment.ts +++ b/src/types/comment.ts @@ -26,7 +26,9 @@ export type ReactionType = | "moneyEye" | "sick" | "upsideDown" - | "worried"; + | "worried" + | "upVote" + | "downVote"; export interface UserPublicData { email?: string;