diff --git a/frontend/src/hooks/useDocumentStatusSocket.ts b/frontend/src/hooks/useDocumentStatusSocket.ts new file mode 100644 index 0000000..67eb8e2 --- /dev/null +++ b/frontend/src/hooks/useDocumentStatusSocket.ts @@ -0,0 +1,41 @@ +/** + * Document status socket hook (#1502, #1503). + * + * The document page opened a raw WebSocket to `/documents/:id/status`, but the + * backend serves a Socket.IO namespace, and its manual reconnect loop kept + * running after the page was closed. This hook connects through Socket.IO and + * tears the connection down on unmount. + */ +import { useEffect, useRef } from "react"; +import { io, type Socket } from "socket.io-client"; + +export interface DocumentStatusEvent { + documentId: string; + status: string; +} + +export function useDocumentStatusSocket( + documentId: string, + onStatus: (event: DocumentStatusEvent) => void, + baseUrl: string = process.env.NEXT_PUBLIC_API_URL ?? "", +): void { + const handlerRef = useRef(onStatus); + handlerRef.current = onStatus; + + useEffect(() => { + if (!documentId) { + return; + } + const socket: Socket = io(`${baseUrl}/documents`, { + transports: ["websocket"], + withCredentials: true, + }); + socket.on("status", (event: DocumentStatusEvent) => + handlerRef.current(event), + ); + return () => { + socket.off("status"); + socket.disconnect(); + }; + }, [documentId, baseUrl]); +} diff --git a/frontend/src/lib/auth-storage.ts b/frontend/src/lib/auth-storage.ts new file mode 100644 index 0000000..6402bd4 --- /dev/null +++ b/frontend/src/lib/auth-storage.ts @@ -0,0 +1,29 @@ +/** + * Auth credential storage policy (#1501, #1500). + * + * Access/refresh tokens were written to `localStorage` even though the backend + * already sets httpOnly cookies. Pages that read tokens from `localStorage` + * also bypassed `lib/api-client`, so they missed 401 handling and refresh. This + * module makes cookies authoritative and exposes a migration helper to drop + * legacy localStorage tokens. + */ + +/** Cookies are the single source of truth for auth (backend sets httpOnly). */ +export const AUTH_COOKIE_MODE = true; + +/** Whether client code is allowed to persist tokens itself. */ +export function shouldStoreTokensInLocalStorage(): boolean { + return !AUTH_COOKIE_MODE; +} + +/** + * Remove any legacy tokens persisted in storage. Call once on boot to migrate + * existing sessions onto cookie auth (#1501). + */ +export function clearLegacyLocalStorageTokens( + storage: Pick, +): void { + for (const key of ["access_token", "refresh_token", "token"]) { + storage.removeItem(key); + } +}