chore: build
This commit is contained in:
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Omnex — Plateforme de gestion de commandes & livraison</title>
|
||||
<meta name="description" content="Déployez en un clic une démo complète de la plateforme de gestion de commandes et de livraison." />
|
||||
<script type="module" crossorigin src="/assets/index-yR0d-eKM.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DkbJvUQ3.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
+14
-27
@@ -1,19 +1,8 @@
|
||||
// Client HTTP vers l'API Omnex.
|
||||
const BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:8080'
|
||||
|
||||
const TOKEN_KEY = 'omnex.token'
|
||||
|
||||
|
||||
export function getToken(): string | null {
|
||||
return localStorage.getItem(TOKEN_KEY)
|
||||
}
|
||||
export function setToken(t: string) {
|
||||
localStorage.setItem(TOKEN_KEY, t)
|
||||
}
|
||||
export function clearToken() {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
}
|
||||
|
||||
// Client HTTP vers l'API Omnex. "" (chemin relatif) par défaut : same-origin
|
||||
// en prod (nginx proxy /api/ vers l'API, voir docker/waf/nginx.conf) et en
|
||||
// dev (proxy Vite, voir vite.config.ts) — nécessaire pour que le cookie de
|
||||
// session SameSite=Lax soit envoyé (jamais cross-origin, voir lib/auth.tsx).
|
||||
const BASE = import.meta.env.VITE_API_URL ?? ''
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number
|
||||
@@ -23,14 +12,19 @@ export class ApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
// Authentification exclusivement via le cookie de session HttpOnly (posé par
|
||||
// le backend sur /auth/login et /auth/register) — jamais de JWT lu/stocké en
|
||||
// JS. Le dupliquer en localStorage (comme avant) annulait la protection
|
||||
// HttpOnly contre le vol de session par XSS (pentest F-003) : le SPA et
|
||||
// l'API étant same-origin, le cookie authentifie déjà chaque fetch() sans
|
||||
// qu'aucun en-tête Authorization manuel ne soit nécessaire.
|
||||
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
const token = getToken()
|
||||
if (token) headers.Authorization = `Bearer ${token}`
|
||||
|
||||
const res = await fetch(`${BASE}/api/v1${path}`, {
|
||||
method,
|
||||
headers,
|
||||
credentials: 'include',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
})
|
||||
if (!res.ok) {
|
||||
@@ -177,16 +171,9 @@ export interface AppDownloadsResponse {
|
||||
|
||||
export const api = {
|
||||
login: (username: string, password: string, role: Role) =>
|
||||
request<{ token: string; token_type: string; role: Role }>('POST', '/auth/login', {
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
}),
|
||||
request<{ role: Role }>('POST', '/auth/login', { username, password, role }),
|
||||
register: (username: string, password: string) =>
|
||||
request<{ token: string; token_type: string; role: Role }>('POST', '/auth/register', {
|
||||
username,
|
||||
password,
|
||||
}),
|
||||
request<{ role: Role }>('POST', '/auth/register', { username, password }),
|
||||
me: () =>
|
||||
request<{
|
||||
user_id: string
|
||||
|
||||
+13
-26
@@ -7,7 +7,7 @@ import {
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
import { api, clearToken, getToken, setToken, type Role } from './api'
|
||||
import { api, type Role } from './api'
|
||||
|
||||
interface AuthState {
|
||||
isAuthenticated: boolean
|
||||
@@ -26,16 +26,15 @@ interface AuthState {
|
||||
const AuthContext = createContext<AuthState | null>(null)
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [token, setTok] = useState<string | null>(getToken())
|
||||
const [role, setRole] = useState<Role | null>(null)
|
||||
const [typeAbo, setTypeAbo] = useState<string | null>(null)
|
||||
const [initializing, setInitializing] = useState<boolean>(!!getToken())
|
||||
const [initializing, setInitializing] = useState(true)
|
||||
|
||||
// Le cookie de session est HttpOnly : impossible à lire en JS pour savoir
|
||||
// à l'avance si l'utilisateur est connecté. On tente donc systématiquement
|
||||
// /auth/me au montage — le cookie (s'il existe et est valide) l'authentifie
|
||||
// automatiquement ; un 401 signifie simplement "pas de session".
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
setInitializing(false)
|
||||
return
|
||||
}
|
||||
let active = true
|
||||
api
|
||||
.me()
|
||||
@@ -46,12 +45,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (active) {
|
||||
clearToken()
|
||||
setTok(null)
|
||||
setRole(null)
|
||||
setTypeAbo(null)
|
||||
}
|
||||
/* pas de session valide — état par défaut (déconnecté) */
|
||||
})
|
||||
.finally(() => {
|
||||
if (active) setInitializing(false)
|
||||
@@ -59,24 +53,19 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const login = useCallback(async (username: string, password: string, role: Role) => {
|
||||
const res = await api.login(username, password, role)
|
||||
setToken(res.token)
|
||||
setTok(res.token)
|
||||
setRole(res.role)
|
||||
await api.login(username, password, role)
|
||||
const me = await api.me()
|
||||
setRole(me.role)
|
||||
setTypeAbo(me.type_abonnement)
|
||||
}, [])
|
||||
|
||||
const register = useCallback(async (username: string, password: string) => {
|
||||
const res = await api.register(username, password)
|
||||
setToken(res.token)
|
||||
setTok(res.token)
|
||||
setRole(res.role)
|
||||
await api.register(username, password)
|
||||
const me = await api.me()
|
||||
setRole(me.role)
|
||||
setTypeAbo(me.type_abonnement)
|
||||
}, [])
|
||||
|
||||
@@ -84,8 +73,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
try {
|
||||
await api.logout()
|
||||
} finally {
|
||||
clearToken()
|
||||
setTok(null)
|
||||
setRole(null)
|
||||
setTypeAbo(null)
|
||||
}
|
||||
@@ -99,7 +86,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const value = useMemo<AuthState>(
|
||||
() => ({
|
||||
isAuthenticated: !!token,
|
||||
isAuthenticated: !!role,
|
||||
isAdmin: role === 'admin',
|
||||
isClient: role === 'client',
|
||||
isPremium: typeAbo === 'premium',
|
||||
@@ -111,7 +98,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
logout,
|
||||
refreshAbo,
|
||||
}),
|
||||
[token, role, typeAbo, initializing, login, register, logout, refreshAbo],
|
||||
[role, typeAbo, initializing, login, register, logout, refreshAbo],
|
||||
)
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
||||
|
||||
@@ -4,6 +4,17 @@ import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
// Fait passer l'API par la même origine que le SPA en dev (localhost:5173)
|
||||
// au lieu d'un appel cross-origin direct vers localhost:8080 — nécessaire
|
||||
// depuis le passage à l'authentification 100% cookie HttpOnly (plus de
|
||||
// fallback Authorization: Bearer, voir lib/api.ts) : un cookie
|
||||
// SameSite=Lax n'est pas envoyé sur un fetch() cross-site.
|
||||
proxy: {
|
||||
'/api': { target: 'http://localhost:8080', changeOrigin: true },
|
||||
'/healthz': { target: 'http://localhost:8080', changeOrigin: true },
|
||||
},
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
|
||||
Reference in New Issue
Block a user