- FastAPI + SQLAlchemy async + Alembic + Postgres backend - Auth: JWT access + rotating refresh tokens, argon2, roles, audit log - React 19 + Vite frontend: login page, protected route, auth context - Docker Compose: postgres, redis, migrate, api, worker (placeholder), frontend/nginx Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
84 lines
2.6 KiB
TypeScript
84 lines
2.6 KiB
TypeScript
/**
|
|
* Хранилище токенов вне React-дерева.
|
|
*
|
|
* `client.ts` должен уметь читать access-токен и молча обновлять его по 401
|
|
* без импорта React-контекста (иначе получится цикл api -> auth-context ->
|
|
* api). Поэтому токены живут в модульном синглтоне, а `AuthProvider`
|
|
* подписывается на его изменения через `subscribe`.
|
|
*
|
|
* access-токен — только в памяти (переживает не дольше вкладки).
|
|
* refresh-токен — в localStorage, иначе при обновлении страницы
|
|
* пользователя выкидывало бы из системы каждый раз.
|
|
*/
|
|
|
|
const REFRESH_TOKEN_KEY = 'lux_fiscal.refresh_token'
|
|
|
|
interface TokenState {
|
|
accessToken: string | null
|
|
refreshToken: string | null
|
|
}
|
|
|
|
let state: TokenState = {
|
|
accessToken: null,
|
|
refreshToken: readRefreshTokenFromStorage(),
|
|
}
|
|
|
|
type Listener = (state: TokenState) => void
|
|
const listeners = new Set<Listener>()
|
|
|
|
function readRefreshTokenFromStorage(): string | null {
|
|
try {
|
|
return localStorage.getItem(REFRESH_TOKEN_KEY)
|
|
} catch {
|
|
// Приватный режим браузера может запрещать доступ к localStorage.
|
|
return null
|
|
}
|
|
}
|
|
|
|
function writeRefreshTokenToStorage(token: string | null): void {
|
|
try {
|
|
if (token) {
|
|
localStorage.setItem(REFRESH_TOKEN_KEY, token)
|
|
} else {
|
|
localStorage.removeItem(REFRESH_TOKEN_KEY)
|
|
}
|
|
} catch {
|
|
// Не критично: без localStorage просто не переживём перезагрузку страницы.
|
|
}
|
|
}
|
|
|
|
function notify(): void {
|
|
for (const listener of listeners) listener(state)
|
|
}
|
|
|
|
export function getAccessToken(): string | null {
|
|
return state.accessToken
|
|
}
|
|
|
|
export function getRefreshToken(): string | null {
|
|
return state.refreshToken
|
|
}
|
|
|
|
export function setTokens(accessToken: string, refreshToken: string): void {
|
|
state = { accessToken, refreshToken }
|
|
writeRefreshTokenToStorage(refreshToken)
|
|
notify()
|
|
}
|
|
|
|
export function setAccessToken(accessToken: string): void {
|
|
state = { ...state, accessToken }
|
|
notify()
|
|
}
|
|
|
|
export function clearTokens(): void {
|
|
state = { accessToken: null, refreshToken: null }
|
|
writeRefreshTokenToStorage(null)
|
|
notify()
|
|
}
|
|
|
|
/** Вызывается при монтировании AuthProvider и при каждом изменении токенов. */
|
|
export function subscribeToTokens(listener: Listener): () => void {
|
|
listeners.add(listener)
|
|
return () => listeners.delete(listener)
|
|
}
|