style: run prettier on entire codebase

This commit is contained in:
BOHA
2026-03-24 19:59:14 +01:00
parent 872be42107
commit 3c167cf5c4
148 changed files with 26740 additions and 13990 deletions

View File

@@ -1,53 +1,70 @@
import { createContext, useContext, useState, useCallback, useMemo, useRef, type ReactNode } from 'react'
import {
createContext,
useContext,
useState,
useCallback,
useMemo,
useRef,
type ReactNode,
} from "react";
interface Alert {
id: string
message: string
type: 'success' | 'error' | 'warning' | 'info'
id: string;
message: string;
type: "success" | "error" | "warning" | "info";
}
interface AlertMethods {
addAlert: (message: string, type?: string, duration?: number) => string
removeAlert: (id: string) => void
success: (message: string, duration?: number) => string
error: (message: string, duration?: number) => string
warning: (message: string, duration?: number) => string
info: (message: string, duration?: number) => string
addAlert: (message: string, type?: string, duration?: number) => string;
removeAlert: (id: string) => void;
success: (message: string, duration?: number) => string;
error: (message: string, duration?: number) => string;
warning: (message: string, duration?: number) => string;
info: (message: string, duration?: number) => string;
}
interface AlertStateValue {
alerts: Alert[]
removeAlert: (id: string) => void
alerts: Alert[];
removeAlert: (id: string) => void;
}
const AlertContext = createContext<AlertMethods | null>(null)
const AlertStateContext = createContext<AlertStateValue | null>(null)
const AlertContext = createContext<AlertMethods | null>(null);
const AlertStateContext = createContext<AlertStateValue | null>(null);
export function AlertProvider({ children }: { children: ReactNode }) {
const [alerts, setAlerts] = useState<Alert[]>([])
const [alerts, setAlerts] = useState<Alert[]>([]);
const removeAlert = useCallback((id: string) => {
setAlerts(prev => prev.filter(alert => alert.id !== id))
}, [])
setAlerts((prev) => prev.filter((alert) => alert.id !== id));
}, []);
const counterRef = useRef(0)
const addAlert = useCallback((message: string, type = 'success', duration = 4000) => {
const id = `${Date.now()}-${counterRef.current++}`
setAlerts(prev => [...prev, { id, message, type: type as Alert['type'] }])
if (duration > 0) {
setTimeout(() => removeAlert(id), duration)
}
return id
}, [removeAlert])
const counterRef = useRef(0);
const addAlert = useCallback(
(message: string, type = "success", duration = 4000) => {
const id = `${Date.now()}-${counterRef.current++}`;
setAlerts((prev) => [
...prev,
{ id, message, type: type as Alert["type"] },
]);
if (duration > 0) {
setTimeout(() => removeAlert(id), duration);
}
return id;
},
[removeAlert],
);
const methods = useMemo<AlertMethods>(() => ({
addAlert,
removeAlert,
success: (message, duration) => addAlert(message, 'success', duration),
error: (message, duration) => addAlert(message, 'error', duration),
warning: (message, duration) => addAlert(message, 'warning', duration),
info: (message, duration) => addAlert(message, 'info', duration),
}), [addAlert, removeAlert])
const methods = useMemo<AlertMethods>(
() => ({
addAlert,
removeAlert,
success: (message, duration) => addAlert(message, "success", duration),
error: (message, duration) => addAlert(message, "error", duration),
warning: (message, duration) => addAlert(message, "warning", duration),
info: (message, duration) => addAlert(message, "info", duration),
}),
[addAlert, removeAlert],
);
return (
<AlertContext.Provider value={methods}>
@@ -55,17 +72,19 @@ export function AlertProvider({ children }: { children: ReactNode }) {
{children}
</AlertStateContext.Provider>
</AlertContext.Provider>
)
);
}
export function useAlert(): AlertMethods {
const context = useContext(AlertContext)
if (!context) throw new Error('useAlert must be used within an AlertProvider')
return context
const context = useContext(AlertContext);
if (!context)
throw new Error("useAlert must be used within an AlertProvider");
return context;
}
export function useAlertState(): AlertStateValue {
const context = useContext(AlertStateContext)
if (!context) throw new Error('useAlertState must be used within an AlertProvider')
return context
const context = useContext(AlertStateContext);
if (!context)
throw new Error("useAlertState must be used within an AlertProvider");
return context;
}

View File

@@ -1,279 +1,397 @@
import { createContext, useContext, useState, useEffect, useCallback, useMemo, useRef, type ReactNode } from 'react'
import { setSessionExpired, setTokenGetter, setRefreshFn } from '../utils/api'
import {
createContext,
useContext,
useState,
useEffect,
useCallback,
useMemo,
useRef,
type ReactNode,
} from "react";
import { setSessionExpired, setTokenGetter, setRefreshFn } from "../utils/api";
const API_BASE = '/api/admin'
const API_BASE = "/api/admin";
interface User {
id: number
username: string
email: string
fullName: string
roleDisplay: string
isAdmin: boolean
totpEnabled: boolean
require2FA: boolean
permissions: string[]
[key: string]: unknown
id: number;
username: string;
email: string;
fullName: string;
roleDisplay: string;
isAdmin: boolean;
totpEnabled: boolean;
require2FA: boolean;
permissions: string[];
[key: string]: unknown;
}
interface AuthState {
user: User | null
loading: boolean
error: string | null
isAuthenticated: boolean
isAdmin: boolean
permissions: string[]
hasPermission: (permission: string) => boolean
user: User | null;
loading: boolean;
error: string | null;
isAuthenticated: boolean;
isAdmin: boolean;
permissions: string[];
hasPermission: (permission: string) => boolean;
}
interface AuthActions {
login: (username: string, password: string, remember?: boolean) => Promise<{ success: boolean; requires2FA?: boolean; loginToken?: string; error?: string; remember?: boolean }>
verify2FA: (loginToken: string, code: string, remember?: boolean, isBackup?: boolean) => Promise<{ success: boolean; error?: string }>
logout: () => Promise<void>
checkSession: () => Promise<boolean>
getAccessToken: () => string | null
apiRequest: (endpoint: string, options?: RequestInit) => Promise<Response>
silentRefresh: () => Promise<boolean>
updateUser: (updates: Partial<User>) => void
login: (
username: string,
password: string,
remember?: boolean,
) => Promise<{
success: boolean;
requires2FA?: boolean;
loginToken?: string;
error?: string;
remember?: boolean;
}>;
verify2FA: (
loginToken: string,
code: string,
remember?: boolean,
isBackup?: boolean,
) => Promise<{ success: boolean; error?: string }>;
logout: () => Promise<void>;
checkSession: () => Promise<boolean>;
getAccessToken: () => string | null;
apiRequest: (endpoint: string, options?: RequestInit) => Promise<Response>;
silentRefresh: () => Promise<boolean>;
updateUser: (updates: Partial<User>) => void;
}
const AuthStateContext = createContext<AuthState | null>(null)
const AuthActionsContext = createContext<AuthActions | null>(null)
const AuthStateContext = createContext<AuthState | null>(null);
const AuthActionsContext = createContext<AuthActions | null>(null);
function mapUser(u: Record<string, unknown> | null): User | null {
if (!u) return null
const id = (u.userId ?? u.id) as number
const firstName = (u.firstName ?? u.first_name ?? '') as string
const lastName = (u.lastName ?? u.last_name ?? '') as string
const roleName = (u.roleName ?? u.role_name ?? '') as string
if (!u) return null;
const id = (u.userId ?? u.id) as number;
const firstName = (u.firstName ?? u.first_name ?? "") as string;
const lastName = (u.lastName ?? u.last_name ?? "") as string;
const roleName = (u.roleName ?? u.role_name ?? "") as string;
return {
...u,
id,
fullName: (u.fullName ?? u.full_name ?? `${firstName} ${lastName}`.trim()) as string,
fullName: (u.fullName ??
u.full_name ??
`${firstName} ${lastName}`.trim()) as string,
roleDisplay: (u.roleDisplay ?? u.role_display ?? roleName) as string,
isAdmin: (u.isAdmin ?? u.is_admin ?? roleName === 'admin') as boolean,
isAdmin: (u.isAdmin ?? u.is_admin ?? roleName === "admin") as boolean,
totpEnabled: (u.totpEnabled ?? u.totp_enabled ?? false) as boolean,
require2FA: (u.require2FA ?? u.require_2fa ?? false) as boolean,
permissions: (u.permissions ?? []) as string[],
} as User
} as User;
}
let accessToken: string | null = null
let tokenExpiresAt: number | null = null
let cachedUser: User | null = null
let sessionFetched = false
let silentRefreshInFlight: Promise<boolean> | null = null
let accessToken: string | null = null;
let tokenExpiresAt: number | null = null;
let cachedUser: User | null = null;
let sessionFetched = false;
let silentRefreshInFlight: Promise<boolean> | null = null;
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(cachedUser)
const [loading, setLoading] = useState(!sessionFetched)
const [error, setError] = useState<string | null>(null)
const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const [user, setUser] = useState<User | null>(cachedUser);
const [loading, setLoading] = useState(!sessionFetched);
const [error, setError] = useState<string | null>(null);
const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => { cachedUser = user }, [user])
useEffect(() => {
cachedUser = user;
}, [user]);
const getAccessTokenFn = useCallback((): string | null => {
if (!tokenExpiresAt || Date.now() > tokenExpiresAt - 30000) return null
return accessToken
}, [])
if (!tokenExpiresAt || Date.now() > tokenExpiresAt - 30000) return null;
return accessToken;
}, []);
const setAccessTokenFn = useCallback((token: string | null, expiresIn?: number) => {
const ttl = expiresIn ?? 900 // default 15 min matching backend config
accessToken = token
tokenExpiresAt = token ? Date.now() + ttl * 1000 : null
if (refreshTimeoutRef.current) {
clearTimeout(refreshTimeoutRef.current)
refreshTimeoutRef.current = null
}
if (token && ttl > 60) {
refreshTimeoutRef.current = setTimeout(() => silentRefresh(), (ttl - 60) * 1000)
}
}, []) // eslint-disable-line react-hooks/exhaustive-deps
const setAccessTokenFn = useCallback(
(token: string | null, expiresIn?: number) => {
const ttl = expiresIn ?? 900; // default 15 min matching backend config
accessToken = token;
tokenExpiresAt = token ? Date.now() + ttl * 1000 : null;
if (refreshTimeoutRef.current) {
clearTimeout(refreshTimeoutRef.current);
refreshTimeoutRef.current = null;
}
if (token && ttl > 60) {
refreshTimeoutRef.current = setTimeout(
() => silentRefresh(),
(ttl - 60) * 1000,
);
}
},
[],
); // eslint-disable-line react-hooks/exhaustive-deps
const silentRefresh = useCallback(async (): Promise<boolean> => {
// Deduplicate concurrent refresh calls — token rotation means only one call can succeed
if (silentRefreshInFlight) return silentRefreshInFlight
if (silentRefreshInFlight) return silentRefreshInFlight;
const promise = (async (): Promise<boolean> => {
try {
const response = await fetch(`${API_BASE}/refresh`, { method: 'POST', credentials: 'include' })
const data = await response.json()
const response = await fetch(`${API_BASE}/refresh`, {
method: "POST",
credentials: "include",
});
const data = await response.json();
if (data.success && data.data?.access_token) {
setAccessTokenFn(data.data.access_token, data.data.expires_in)
setUser(mapUser(data.data.user))
return true
setAccessTokenFn(data.data.access_token, data.data.expires_in);
setUser(mapUser(data.data.user));
return true;
}
accessToken = null
tokenExpiresAt = null
setUser(null)
cachedUser = null
setSessionExpired()
return false
accessToken = null;
tokenExpiresAt = null;
setUser(null);
cachedUser = null;
setSessionExpired();
return false;
} catch {
// Network error — don't kick the user out, just return false
return false
return false;
} finally {
silentRefreshInFlight = null
silentRefreshInFlight = null;
}
})()
})();
silentRefreshInFlight = promise
return promise
}, [setAccessTokenFn])
silentRefreshInFlight = promise;
return promise;
}, [setAccessTokenFn]);
const checkSession = useCallback(async (): Promise<boolean> => {
try {
const token = getAccessTokenFn()
const token = getAccessTokenFn();
if (token) {
const headers: Record<string, string> = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }
const response = await fetch(`${API_BASE}/session`, { method: 'GET', credentials: 'include', headers })
if (response.status === 429 || response.status >= 500) return !!cachedUser
const data = await response.json()
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
const response = await fetch(`${API_BASE}/session`, {
method: "GET",
credentials: "include",
headers,
});
if (response.status === 429 || response.status >= 500)
return !!cachedUser;
const data = await response.json();
if (data.success && data.data?.user) {
if (data.data.access_token) setAccessTokenFn(data.data.access_token)
setUser(mapUser(data.data.user))
cachedUser = mapUser(data.data.user)
return true
if (data.data.access_token) setAccessTokenFn(data.data.access_token);
setUser(mapUser(data.data.user));
cachedUser = mapUser(data.data.user);
return true;
}
}
// No token or session invalid — try silent refresh via cookie
const refreshed = await silentRefresh()
if (refreshed) return true
setUser(null)
cachedUser = null
accessToken = null
tokenExpiresAt = null
return false
const refreshed = await silentRefresh();
if (refreshed) return true;
setUser(null);
cachedUser = null;
accessToken = null;
tokenExpiresAt = null;
return false;
} catch {
return !!cachedUser
return !!cachedUser;
} finally {
setLoading(false)
sessionFetched = true
setLoading(false);
sessionFetched = true;
}
}, [getAccessTokenFn, setAccessTokenFn, silentRefresh])
}, [getAccessTokenFn, setAccessTokenFn, silentRefresh]);
useEffect(() => {
setTokenGetter(getAccessTokenFn)
setRefreshFn(silentRefresh)
}, [getAccessTokenFn, silentRefresh])
setTokenGetter(getAccessTokenFn);
setRefreshFn(silentRefresh);
}, [getAccessTokenFn, silentRefresh]);
useEffect(() => {
checkSession()
return () => { if (refreshTimeoutRef.current) clearTimeout(refreshTimeoutRef.current) }
}, [checkSession])
checkSession();
return () => {
if (refreshTimeoutRef.current) clearTimeout(refreshTimeoutRef.current);
};
}, [checkSession]);
const login = useCallback(async (username: string, password: string, remember = false) => {
setError(null)
try {
const response = await fetch(`${API_BASE}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ username, password, remember_me: remember }),
})
const data = await response.json()
if (data.success) {
if (data.data?.totp_required) {
return { success: false, requires2FA: true, loginToken: data.data.login_token, remember }
const login = useCallback(
async (username: string, password: string, remember = false) => {
setError(null);
try {
const response = await fetch(`${API_BASE}/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ username, password, remember_me: remember }),
});
const data = await response.json();
if (data.success) {
if (data.data?.totp_required) {
return {
success: false,
requires2FA: true,
loginToken: data.data.login_token,
remember,
};
}
setAccessTokenFn(data.data.access_token, data.data.expires_in);
setUser(mapUser(data.data.user));
cachedUser = mapUser(data.data.user);
sessionFetched = true;
return { success: true };
}
setAccessTokenFn(data.data.access_token, data.data.expires_in)
setUser(mapUser(data.data.user))
cachedUser = mapUser(data.data.user)
sessionFetched = true
return { success: true }
setError(data.error);
return { success: false, error: data.error };
} catch {
const errorMsg =
"Chyba pripojeni. Zkontrolujte prosim pripojeni k internetu a zkuste to znovu.";
setError(errorMsg);
return { success: false, error: errorMsg };
}
setError(data.error)
return { success: false, error: data.error }
} catch {
const errorMsg = 'Chyba pripojeni. Zkontrolujte prosim pripojeni k internetu a zkuste to znovu.'
setError(errorMsg)
return { success: false, error: errorMsg }
}
}, [setAccessTokenFn])
},
[setAccessTokenFn],
);
const verify2FA = useCallback(async (loginToken: string, code: string, remember = false, isBackup = false) => {
setError(null)
try {
const response = await fetch(`${API_BASE}/login/totp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ login_token: loginToken, totp_code: code, remember_me: remember }),
})
const data = await response.json()
if (data.success) {
setAccessTokenFn(data.data.access_token, data.data.expires_in)
setUser(mapUser(data.data.user))
cachedUser = mapUser(data.data.user)
sessionFetched = true
return { success: true }
const verify2FA = useCallback(
async (
loginToken: string,
code: string,
remember = false,
isBackup = false,
) => {
setError(null);
try {
const response = await fetch(`${API_BASE}/login/totp`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
login_token: loginToken,
totp_code: code,
remember_me: remember,
}),
});
const data = await response.json();
if (data.success) {
setAccessTokenFn(data.data.access_token, data.data.expires_in);
setUser(mapUser(data.data.user));
cachedUser = mapUser(data.data.user);
sessionFetched = true;
return { success: true };
}
setError(data.error);
return { success: false, error: data.error };
} catch {
const errorMsg = "Chyba pripojeni.";
setError(errorMsg);
return { success: false, error: errorMsg };
}
setError(data.error)
return { success: false, error: data.error }
} catch {
const errorMsg = 'Chyba pripojeni.'
setError(errorMsg)
return { success: false, error: errorMsg }
}
}, [setAccessTokenFn])
},
[setAccessTokenFn],
);
const logout = useCallback(async () => {
try {
const token = getAccessTokenFn()
const token = getAccessTokenFn();
await fetch(`${API_BASE}/logout`, {
method: 'POST',
method: "POST",
headers: { ...(token && { Authorization: `Bearer ${token}` }) },
credentials: 'include',
})
} catch { /* ignore */ } finally {
accessToken = null
tokenExpiresAt = null
setUser(null)
cachedUser = null
sessionFetched = false
if (refreshTimeoutRef.current) { clearTimeout(refreshTimeoutRef.current); refreshTimeoutRef.current = null }
}
}, [getAccessTokenFn])
const apiRequest = useCallback(async (endpoint: string, options: RequestInit = {}) => {
let token = getAccessTokenFn()
if (!token && user) {
const refreshed = await silentRefresh()
if (refreshed) token = getAccessTokenFn()
}
const headers: Record<string, string> = { 'Content-Type': 'application/json', ...(options.headers as Record<string, string>) }
if (token) headers['Authorization'] = `Bearer ${token}`
const response = await fetch(`${API_BASE}${endpoint}`, { ...options, headers, credentials: 'include' })
if (response.status === 401 && user) {
const refreshed = await silentRefresh()
if (refreshed) {
token = getAccessTokenFn()
if (token) headers['Authorization'] = `Bearer ${token}`
return fetch(`${API_BASE}${endpoint}`, { ...options, headers, credentials: 'include' })
credentials: "include",
});
} catch {
/* ignore */
} finally {
accessToken = null;
tokenExpiresAt = null;
setUser(null);
cachedUser = null;
sessionFetched = false;
if (refreshTimeoutRef.current) {
clearTimeout(refreshTimeoutRef.current);
refreshTimeoutRef.current = null;
}
}
return response
}, [getAccessTokenFn, silentRefresh, user])
}, [getAccessTokenFn]);
const apiRequest = useCallback(
async (endpoint: string, options: RequestInit = {}) => {
let token = getAccessTokenFn();
if (!token && user) {
const refreshed = await silentRefresh();
if (refreshed) token = getAccessTokenFn();
}
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(options.headers as Record<string, string>),
};
if (token) headers["Authorization"] = `Bearer ${token}`;
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers,
credentials: "include",
});
if (response.status === 401 && user) {
const refreshed = await silentRefresh();
if (refreshed) {
token = getAccessTokenFn();
if (token) headers["Authorization"] = `Bearer ${token}`;
return fetch(`${API_BASE}${endpoint}`, {
...options,
headers,
credentials: "include",
});
}
}
return response;
},
[getAccessTokenFn, silentRefresh, user],
);
const updateUser = useCallback((updates: Partial<User>) => {
setUser(prev => prev ? { ...prev, ...updates } : null)
}, [])
setUser((prev) => (prev ? { ...prev, ...updates } : null));
}, []);
const hasPermission = useCallback((permission: string): boolean => {
if (!user) return false
if (user.isAdmin) return true
return (user.permissions || []).includes(permission)
}, [user])
const hasPermission = useCallback(
(permission: string): boolean => {
if (!user) return false;
if (user.isAdmin) return true;
return (user.permissions || []).includes(permission);
},
[user],
);
const permissions = useMemo(() => user?.permissions || [], [user])
const permissions = useMemo(() => user?.permissions || [], [user]);
const stateValue = useMemo<AuthState>(() => ({
user, loading, error, isAuthenticated: !!user, isAdmin: user?.isAdmin || false, permissions, hasPermission,
}), [user, loading, error, permissions, hasPermission])
const stateValue = useMemo<AuthState>(
() => ({
user,
loading,
error,
isAuthenticated: !!user,
isAdmin: user?.isAdmin || false,
permissions,
hasPermission,
}),
[user, loading, error, permissions, hasPermission],
);
const actionsValue = useMemo<AuthActions>(() => ({
login, verify2FA, logout, checkSession, getAccessToken: getAccessTokenFn, apiRequest, silentRefresh, updateUser,
}), [login, verify2FA, logout, checkSession, getAccessTokenFn, apiRequest, silentRefresh, updateUser])
const actionsValue = useMemo<AuthActions>(
() => ({
login,
verify2FA,
logout,
checkSession,
getAccessToken: getAccessTokenFn,
apiRequest,
silentRefresh,
updateUser,
}),
[
login,
verify2FA,
logout,
checkSession,
getAccessTokenFn,
apiRequest,
silentRefresh,
updateUser,
],
);
return (
<AuthActionsContext.Provider value={actionsValue}>
@@ -281,26 +399,29 @@ export function AuthProvider({ children }: { children: ReactNode }) {
{children}
</AuthStateContext.Provider>
</AuthActionsContext.Provider>
)
);
}
export function useAuth(): AuthState & AuthActions {
const state = useContext(AuthStateContext)
const actions = useContext(AuthActionsContext)
if (!state || !actions) throw new Error('useAuth must be used within an AuthProvider')
return { ...state, ...actions }
const state = useContext(AuthStateContext);
const actions = useContext(AuthActionsContext);
if (!state || !actions)
throw new Error("useAuth must be used within an AuthProvider");
return { ...state, ...actions };
}
export function useAuthState(): AuthState {
const context = useContext(AuthStateContext)
if (!context) throw new Error('useAuthState must be used within an AuthProvider')
return context
const context = useContext(AuthStateContext);
if (!context)
throw new Error("useAuthState must be used within an AuthProvider");
return context;
}
export function useAuthActions(): AuthActions {
const context = useContext(AuthActionsContext)
if (!context) throw new Error('useAuthActions must be used within an AuthProvider')
return context
const context = useContext(AuthActionsContext);
if (!context)
throw new Error("useAuthActions must be used within an AuthProvider");
return context;
}
export default AuthStateContext
export default AuthStateContext;