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

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