fix: global Unauthorized handling — redirect to /login, suppress log spam
Some checks failed
Build and Push Frontend Image / build (push) Has been cancelled
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:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
{},
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user