Initial commit: backend scaffold, auth, frontend login
- 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>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Хранилище токенов вне 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)
|
||||
}
|
||||
Reference in New Issue
Block a user