security: fix all Critical and High findings from FLAWS_REPORT audit
- Auth: pessimistic locking on login tokens and refresh token rotation, backup code attempt counter, rate limiting verification - Schema: unique constraints on business numbers, FK relations, unsigned/signed alignment, attendance duplicate prevention - Invoices/PDFs: DOMPurify sanitization, bounded queries in stats and alerts, VAT rounding, Puppeteer error handling - Orders/Offers: transactional parent+child creation, Zod NaN refinement, status enums, uniqueness checks - Projects/Files: path traversal protection, streamed uploads, permission guards, query param validation - Attendance/HR: duplicate checks, ownership validation, GPS restrictions, trip distance validation - Frontend: modal lock reference counting, XSS escaping in print HTML, ref mutation fixes, accessibility attributes Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,39 +1,55 @@
|
||||
let showSessionExpiredAlert = false;
|
||||
let showLogoutAlert = false;
|
||||
let getTokenFn: (() => string | null) | null = null;
|
||||
let refreshFn: (() => Promise<boolean>) | null = null;
|
||||
let refreshPromise: Promise<boolean> | null = null;
|
||||
class ApiState {
|
||||
showSessionExpiredAlert = false;
|
||||
showLogoutAlert = false;
|
||||
getTokenFn: (() => string | null) | null = null;
|
||||
refreshFn: (() => Promise<boolean>) | null = null;
|
||||
refreshPromise: Promise<boolean> | null = null;
|
||||
|
||||
reset() {
|
||||
this.showSessionExpiredAlert = false;
|
||||
this.showLogoutAlert = false;
|
||||
this.getTokenFn = null;
|
||||
this.refreshFn = null;
|
||||
this.refreshPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
const state = new ApiState();
|
||||
|
||||
export const resetApiState = (): void => {
|
||||
state.reset();
|
||||
};
|
||||
|
||||
export const shouldShowSessionExpiredAlert = (): boolean => {
|
||||
if (showSessionExpiredAlert) {
|
||||
showSessionExpiredAlert = false;
|
||||
if (state.showSessionExpiredAlert) {
|
||||
state.showSessionExpiredAlert = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const setSessionExpired = (): void => {
|
||||
showSessionExpiredAlert = true;
|
||||
state.showSessionExpiredAlert = true;
|
||||
};
|
||||
|
||||
export const shouldShowLogoutAlert = (): boolean => {
|
||||
if (showLogoutAlert) {
|
||||
showLogoutAlert = false;
|
||||
if (state.showLogoutAlert) {
|
||||
state.showLogoutAlert = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const setLogoutAlert = (): void => {
|
||||
showLogoutAlert = true;
|
||||
state.showLogoutAlert = true;
|
||||
};
|
||||
|
||||
export const setTokenGetter = (fn: () => string | null): void => {
|
||||
getTokenFn = fn;
|
||||
state.getTokenFn = fn;
|
||||
};
|
||||
|
||||
export const setRefreshFn = (fn: () => Promise<boolean>): void => {
|
||||
refreshFn = fn;
|
||||
state.refreshFn = fn;
|
||||
};
|
||||
|
||||
export const apiFetch = async (
|
||||
@@ -42,7 +58,7 @@ export const apiFetch = async (
|
||||
): Promise<Response> => {
|
||||
let token: string | null = null;
|
||||
try {
|
||||
token = getTokenFn ? getTokenFn() : null;
|
||||
token = state.getTokenFn ? state.getTokenFn() : null;
|
||||
} catch {
|
||||
// token retrieval failed
|
||||
}
|
||||
@@ -69,21 +85,22 @@ export const apiFetch = async (
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (response.status === 401 && refreshFn) {
|
||||
if (response.status === 401 && state.refreshFn) {
|
||||
try {
|
||||
if (!refreshPromise) {
|
||||
refreshPromise = refreshFn().finally(() => {
|
||||
refreshPromise = null;
|
||||
if (!state.refreshPromise) {
|
||||
state.refreshPromise = state.refreshFn().finally(() => {
|
||||
state.refreshPromise = null;
|
||||
});
|
||||
}
|
||||
const refreshed = await refreshPromise;
|
||||
const refreshed = await state.refreshPromise;
|
||||
if (refreshed) {
|
||||
token = getTokenFn ? getTokenFn() : null;
|
||||
token = state.getTokenFn ? state.getTokenFn() : null;
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
const { signal, ...retryOptions } = options;
|
||||
response = await fetch(url, {
|
||||
...options,
|
||||
...retryOptions,
|
||||
headers,
|
||||
credentials: "include",
|
||||
});
|
||||
@@ -100,7 +117,7 @@ export const apiFetch = async (
|
||||
|
||||
export const getAccessToken = (): string | null => {
|
||||
try {
|
||||
return getTokenFn ? getTokenFn() : null;
|
||||
return state.getTokenFn ? state.getTokenFn() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user