diff --git a/packages/frontend/src/hooks.server.ts b/packages/frontend/src/hooks.server.ts index a1cfe09..dca9f6e 100644 --- a/packages/frontend/src/hooks.server.ts +++ b/packages/frontend/src/hooks.server.ts @@ -1,4 +1,5 @@ -import { isAuthenticated } from "$lib/services"; +import { redirect } from "@sveltejs/kit"; +import { isAuthenticated, UnauthorizedError } from "$lib/services"; import { logger, generateRequestId } from "$lib/logger"; import type { Handle } from "@sveltejs/kit"; @@ -65,6 +66,10 @@ export const handle: Handle = async ({ event, resolve }) => { }, }); } catch (error) { + if (error instanceof UnauthorizedError) { + const loginUrl = `/login?redirect=${encodeURIComponent(url.pathname)}`; + throw redirect(303, loginUrl); + } const duration = Date.now() - startTime; logger.error("Request handler error", { requestId, diff --git a/packages/frontend/src/lib/services.ts b/packages/frontend/src/lib/services.ts index 5fbe506..5c87929 100644 --- a/packages/frontend/src/lib/services.ts +++ b/packages/frontend/src/lib/services.ts @@ -16,6 +16,22 @@ import type { } from "$lib/types"; import { logger } from "$lib/logger"; +export class UnauthorizedError extends Error { + constructor() { + super("Unauthorized"); + this.name = "UnauthorizedError"; + } +} + +function isUnauthorizedError(error: unknown): boolean { + if (error && typeof error === "object" && "response" in error) { + const resp = (error as { response?: { errors?: { message: string }[] } }).response; + if (resp?.errors?.some((e) => e.message === "Unauthorized")) return true; + } + const msg = error instanceof Error ? error.message : String(error); + return msg.startsWith("Unauthorized"); +} + // Helper to log API calls async function loggedApiCall( operationName: string, @@ -32,6 +48,10 @@ async function loggedApiCall( return result; } catch (error) { const duration = Date.now() - startTime; + if (isUnauthorizedError(error)) { + logger.debug(`🔒 API: ${operationName} unauthorized`, { duration, context }); + throw new UnauthorizedError(); + } logger.error(`❌ API: ${operationName} failed`, { duration, context, @@ -816,13 +836,12 @@ const RECORDINGS_QUERY = gql` } `; -export async function getRecordings(fetchFn?: typeof globalThis.fetch) { +export async function getRecordings(fetchFn?: typeof globalThis.fetch, token?: string) { return loggedApiCall( "getRecordings", async () => { - const data = await getGraphQLClient(fetchFn).request<{ recordings: Recording[] }>( - RECORDINGS_QUERY, - ); + const client = token ? getAuthClient(token, fetchFn) : getGraphQLClient(fetchFn); + const data = await client.request<{ recordings: Recording[] }>(RECORDINGS_QUERY); return data.recordings; }, {}, @@ -960,14 +979,12 @@ const RECORDING_QUERY = gql` } `; -export async function getRecording(id: string, fetchFn?: typeof globalThis.fetch) { +export async function getRecording(id: string, fetchFn?: typeof globalThis.fetch, token?: string) { return loggedApiCall( "getRecording", async () => { - const data = await getGraphQLClient(fetchFn).request<{ recording: Recording | null }>( - RECORDING_QUERY, - { id }, - ); + const client = token ? getAuthClient(token, fetchFn) : getGraphQLClient(fetchFn); + const data = await client.request<{ recording: Recording | null }>(RECORDING_QUERY, { id }); return data.recording; }, { id }, @@ -1799,13 +1816,12 @@ const ANALYTICS_QUERY = gql` } `; -export async function getAnalytics(fetchFn?: typeof globalThis.fetch) { +export async function getAnalytics(fetchFn?: typeof globalThis.fetch, token?: string) { return loggedApiCall( "getAnalytics", async () => { - const data = await getGraphQLClient(fetchFn).request<{ analytics: Analytics | null }>( - ANALYTICS_QUERY, - ); + const client = token ? getAuthClient(token, fetchFn) : getGraphQLClient(fetchFn); + const data = await client.request<{ analytics: Analytics | null }>(ANALYTICS_QUERY); return data.analytics; }, {}, diff --git a/packages/frontend/src/routes/me/analytics/+page.server.ts b/packages/frontend/src/routes/me/analytics/+page.server.ts index 4d6019f..61c59f1 100644 --- a/packages/frontend/src/routes/me/analytics/+page.server.ts +++ b/packages/frontend/src/routes/me/analytics/+page.server.ts @@ -2,11 +2,12 @@ import { redirect } from "@sveltejs/kit"; import { isModel } from "$lib/api"; import { getAnalytics } from "$lib/services"; -export async function load({ locals, fetch }) { +export async function load({ locals, fetch, cookies }) { if (!isModel(locals.authStatus.user!)) { throw redirect(302, "/me/profile"); } + const token = cookies.get("session_token") || ""; return { - analytics: await getAnalytics(fetch).catch(() => null), + analytics: await getAnalytics(fetch, token).catch(() => null), }; } diff --git a/packages/frontend/src/routes/play/buttplug/+page.server.ts b/packages/frontend/src/routes/play/buttplug/+page.server.ts index 1c2f5ec..9a4f7dc 100644 --- a/packages/frontend/src/routes/play/buttplug/+page.server.ts +++ b/packages/frontend/src/routes/play/buttplug/+page.server.ts @@ -1,19 +1,14 @@ import { getRecording } from "$lib/services"; import type { Recording } from "$lib/types"; -export async function load({ url, fetch }) { +export async function load({ url, fetch, cookies }) { const recordingId = url.searchParams.get("recording"); + const token = cookies.get("session_token") || ""; let recording: Recording | null = null; if (recordingId) { - try { - recording = await getRecording(recordingId, fetch); - } catch (error) { - console.error("Failed to load recording:", error); - } + recording = await getRecording(recordingId, fetch, token).catch(() => null); } - return { - recording, - }; + return { recording }; } diff --git a/packages/frontend/src/routes/play/recordings/+page.server.ts b/packages/frontend/src/routes/play/recordings/+page.server.ts index c87da3e..6bbb94f 100644 --- a/packages/frontend/src/routes/play/recordings/+page.server.ts +++ b/packages/frontend/src/routes/play/recordings/+page.server.ts @@ -1,7 +1,8 @@ import { getRecordings } from "$lib/services"; -export async function load({ fetch }) { +export async function load({ fetch, cookies }) { + const token = cookies.get("session_token") || ""; return { - recordings: await getRecordings(fetch).catch(() => []), + recordings: await getRecordings(fetch, token), }; }