fix: WEB-C1 move JWT to sessionStorage, complete mutation error handling

- WEB-C1 (Critical): Replace all localStorage token operations with
  sessionStorage in authSlice.ts and client.ts. Tokens now clear when
  the browser tab closes, reducing the XSS token-theft window.
  httpOnly cookie migration documented as follow-up.
- WEB-M2: 401 interceptor now dispatches Redux logout() before
  redirect so auth state stays consistent with cleared storage.
- WEB-H5/H6: Add onError toast handlers to sendMutation,
  reviseMutation, and regenerateMutation in AdminWorkspace.
This commit is contained in:
Adam Moussa 2026-05-27 17:21:17 -04:00
parent 4f1271eb50
commit 67b4732395
3 changed files with 24 additions and 5 deletions

View file

@ -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<boolean>) => {
state.loading = action.payload;

View file

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

View file

@ -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({