fix: global Unauthorized handling — redirect to /login, suppress log spam
Some checks failed
Build and Push Frontend Image / build (push) Has been cancelled

- Add UnauthorizedError class exported from services.ts
- loggedApiCall now detects Unauthorized GraphQL errors, logs at DEBUG
  instead of ERROR, and throws UnauthorizedError (no more stack dumps)
- hooks.server.ts catches UnauthorizedError from any load function and
  redirects to /login?redirect=<original-path>
- getRecordings, getRecording, getAnalytics now accept an optional token
  and use getAuthClient server-side so cross-origin cookie forwarding works
- Update play/recordings, play/buttplug, me/analytics page.server.ts to
  pass the session token — prevents Unauthorized on auth-protected pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 09:01:47 +01:00
parent 3fd876180a
commit ad4f5b3700
5 changed files with 45 additions and 27 deletions

View File

@@ -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,

View File

@@ -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<T>(
operationName: string,
@@ -32,6 +48,10 @@ async function loggedApiCall<T>(
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;
},
{},

View File

@@ -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),
};
}

View File

@@ -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 };
}

View File

@@ -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),
};
}