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:
2026-09-22 15:07:15 +03:00
co-authored by Claude Sonnet 5
commit 2664cb8213
71 changed files with 4999 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
import { useId, useState } from 'react'
import type { FormEvent } from 'react'
import { Navigate, useLocation } from 'react-router-dom'
import { ApiError } from '@/api/client'
import '@/features/auth/LoginPage.css'
import { useAuth } from '@/features/auth/useAuth'
interface LocationState {
from?: { pathname: string }
}
export function LoginPage() {
const { status, login } = useAuth()
const location = useLocation()
const emailId = useId()
const passwordId = useId()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [submitting, setSubmitting] = useState(false)
const [formError, setFormError] = useState<string | null>(null)
// Уже вошли — на странице входа делать нечего.
if (status === 'authenticated') {
const state = location.state as LocationState | null
const redirectTo = state?.from?.pathname ?? '/'
return <Navigate to={redirectTo} replace />
}
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
setFormError(null)
setSubmitting(true)
try {
await login(email, password)
} catch (err) {
const message = err instanceof ApiError ? err.message : 'Не удалось подключиться к серверу'
setFormError(message)
} finally {
setSubmitting(false)
}
}
return (
<div className="login-page">
<div className="login-card">
<div className="login-header">
<h1>lux_fiscal</h1>
<p>Вход в систему фискализации заказов</p>
</div>
<form onSubmit={handleSubmit} noValidate>
{formError && (
<div className="login-error" role="alert">
{formError}
</div>
)}
<div className="login-field">
<label htmlFor={emailId}>Email</label>
<input
id={emailId}
type="email"
autoComplete="username"
required
value={email}
disabled={submitting}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
/>
</div>
<div className="login-field">
<label htmlFor={passwordId}>Пароль</label>
<input
id={passwordId}
type="password"
autoComplete="current-password"
required
value={password}
disabled={submitting}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
/>
</div>
<button type="submit" className="login-submit" disabled={submitting}>
{submitting && <span className="spinner" aria-hidden="true" />}
{submitting ? 'Входим…' : 'Войти'}
</button>
</form>
</div>
</div>
)
}