Files
lux_fiscal/frontend/src/features/auth/AuthContext.tsx
T
lauadminandClaude Opus 5.5 b8f2fe6b6e Translate UI and user-facing messages to Ukrainian (#5)
- All frontend pages, labels, notices and errors; html lang=uk, uk-UA money format
- Brand "Assistant System" in the top bar and page title
- Backend error details returned to the UI (auth, orders, receipts,
  cash registers, Checkbox/CRM/NP errors) and CLI output
- Tests updated for the new messages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:27:39 +03:00

96 lines
3.4 KiB
TypeScript

import { createContext, useCallback, useEffect, useMemo, useState } from 'react'
import type { ReactNode } from 'react'
import * as authApi from '@/api/auth'
import { ApiError } from '@/api/client'
import { clearTokens, getRefreshToken, setTokens } from '@/api/tokenStore'
import type { User } from '@/api/types'
type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated'
interface AuthContextValue {
status: AuthStatus
user: User | null
/** null, пока запрос не завершится (успехом или ошибкой). */
error: string | null
login: (email: string, password: string) => Promise<void>
logout: () => Promise<void>
}
// oxlint-disable-next-line react/only-export-components -- контекст и провайдер экспортируются вместе намеренно
export const AuthContext = createContext<AuthContextValue | null>(null)
export function AuthProvider({ children }: { children: ReactNode }) {
// Если refresh-токена нет, статус известен сразу же, без похода в сеть —
// вычисляем его в инициализаторе, а не синхронным setState внутри эффекта.
const [status, setStatus] = useState<AuthStatus>(() =>
getRefreshToken() ? 'loading' : 'unauthenticated',
)
const [user, setUser] = useState<User | null>(null)
const [error, setError] = useState<string | null>(null)
// При открытии приложения пытаемся восстановить сессию по refresh-токену
// из localStorage — иначе каждая перезагрузка страницы требовала бы входа.
useEffect(() => {
const existingRefreshToken = getRefreshToken()
if (!existingRefreshToken) return
let cancelled = false
void (async () => {
try {
const pair = await authApi.refresh(existingRefreshToken)
setTokens(pair.access_token, pair.refresh_token)
const me = await authApi.fetchMe()
if (cancelled) return
setUser(me)
setStatus('authenticated')
} catch {
if (cancelled) return
clearTokens()
setStatus('unauthenticated')
}
})()
return () => {
cancelled = true
}
}, [])
const login = useCallback(async (email: string, password: string) => {
setError(null)
try {
const result = await authApi.login(email, password)
setTokens(result.access_token, result.refresh_token)
setUser(result.user)
setStatus('authenticated')
} catch (err) {
const message = err instanceof ApiError ? err.message : 'Не вдалося підключитися до сервера'
setError(message)
throw err
}
}, [])
const logout = useCallback(async () => {
const refreshToken = getRefreshToken()
if (refreshToken) {
// Best-effort: даже если сервер недоступен, локально выходим всё равно.
try {
await authApi.logout(refreshToken)
} catch {
// Токен и так будет забыт локально ниже.
}
}
clearTokens()
setUser(null)
setStatus('unauthenticated')
}, [])
const value = useMemo<AuthContextValue>(
() => ({ status, user, error, login, logout }),
[status, user, error, login, logout],
)
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}