diff --git a/web/src/app/slices/authSlice.ts b/web/src/app/slices/authSlice.ts index 2de12bc..2fd6fdf 100644 --- a/web/src/app/slices/authSlice.ts +++ b/web/src/app/slices/authSlice.ts @@ -9,9 +9,12 @@ interface AuthState { error: string | null; } +// Fix: WEB-C1 — use sessionStorage instead of localStorage to limit JWT exposure. +// Tokens are cleared when the browser tab closes, reducing XSS token-theft window. +// Follow-up: move to httpOnly cookies once the API supports Set-Cookie auth flow. const getUserFromStorage = (): AuthUser | null => { try { - const result = window.localStorage.getItem(STORAGE_KEY_TOKEN); + const result = window.sessionStorage.getItem(STORAGE_KEY_TOKEN); return result ? JSON.parse(result) : null; } catch { return null; @@ -48,13 +51,13 @@ const authSlice = createSlice({ state.isAuthenticated = action.payload.token ? isTokenValid(action.payload.token) : false; state.loading = false; state.error = null; - window.localStorage.setItem(STORAGE_KEY_TOKEN, JSON.stringify(action.payload)); + window.sessionStorage.setItem(STORAGE_KEY_TOKEN, JSON.stringify(action.payload)); // WEB-C1 }, logout: (state) => { state.user = null; state.isAuthenticated = false; state.error = null; - window.localStorage.removeItem(STORAGE_KEY_TOKEN); + window.sessionStorage.removeItem(STORAGE_KEY_TOKEN); // WEB-C1 }, setLoading: (state, action: PayloadAction) => { state.loading = action.payload; diff --git a/web/src/lib/api/client.ts b/web/src/lib/api/client.ts index 02db718..c96e7ee 100644 --- a/web/src/lib/api/client.ts +++ b/web/src/lib/api/client.ts @@ -1,5 +1,7 @@ import axios from 'axios'; import { API_URL, STORAGE_KEY_TOKEN } from '../../constants'; +import { store } from '../../app/store'; +import { logout } from '../../app/slices/authSlice'; const apiClient = axios.create({ baseURL: API_URL, @@ -11,7 +13,7 @@ const apiClient = axios.create({ apiClient.interceptors.request.use( (config) => { - const tokenData = window.localStorage.getItem(STORAGE_KEY_TOKEN); + const tokenData = window.sessionStorage.getItem(STORAGE_KEY_TOKEN); // WEB-C1 if (tokenData) { try { const parsed = JSON.parse(tokenData); @@ -34,7 +36,9 @@ apiClient.interceptors.response.use( const { status, data } = error.response; if (status === 401) { - window.localStorage.removeItem(STORAGE_KEY_TOKEN); + // Fix: WEB-C1 — use sessionStorage; WEB-M2 — clear Redux auth state before redirect + window.sessionStorage.removeItem(STORAGE_KEY_TOKEN); + store.dispatch(logout()); window.location.href = '/login'; return Promise.reject(new Error('Session expired. Please log in again.')); } diff --git a/web/src/pages/admin/workspace/AdminWorkspace.tsx b/web/src/pages/admin/workspace/AdminWorkspace.tsx index d8eb7ff..7508df2 100644 --- a/web/src/pages/admin/workspace/AdminWorkspace.tsx +++ b/web/src/pages/admin/workspace/AdminWorkspace.tsx @@ -166,6 +166,10 @@ export default function AdminWorkspace() { setSendDialogOpen(false); toast.success('Proposal marked as sent'); }, + // Fix: WEB-H5 — add missing onError handler + onError: (error: Error) => { + toast.error(`Send failed: ${error.message}`); + }, }); const reviseMutation = useMutation({ @@ -175,6 +179,10 @@ export default function AdminWorkspace() { setReviseDialogOpen(false); toast.success('Revision created'); }, + // Fix: WEB-H6 — add missing onError handler + onError: (error: Error) => { + toast.error(`Revision failed: ${error.message}`); + }, }); const regenerateMutation = useMutation({ @@ -182,6 +190,10 @@ export default function AdminWorkspace() { onSuccess: () => { toast.info('AI suggestion generation started'); }, + // Fix: WEB-H5 — add missing onError handler + onError: (error: Error) => { + toast.error(`Regeneration failed: ${error.message}`); + }, }); const pdfMutation = useMutation({