Phase 1: Foundation — backend auth, frontend shell, Docker setup

Backend (FastAPI):
- App factory with async SQLAlchemy 2.0 + PostgreSQL
- Alembic migration for users and sessions tables
- JWT auth (access + refresh tokens, bcrypt passwords)
- Auth endpoints: register, login, refresh, logout, me
- Admin seed script, role-based access deps

Frontend (React + TypeScript):
- Vite + Tailwind CSS + shadcn/ui theme (health-oriented palette)
- i18n with English and Russian translations
- Zustand auth/UI stores with localStorage persistence
- Axios client with automatic token refresh on 401
- Login/register pages, protected routing
- App layout: collapsible sidebar, header with theme/language toggles
- Dashboard with placeholder stats

Infrastructure:
- Docker Compose (postgres, backend, frontend, nginx)
- Nginx reverse proxy with WebSocket support
- Dev override with hot reload

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 12:25:02 +03:00
parent 5bdc296172
commit 7c752cae6b
75 changed files with 7706 additions and 2 deletions

285
plans/phase-1-foundation.md Normal file
View File

@@ -0,0 +1,285 @@
# Phase 1: Foundation — Subplan
## Goal
Stand up the monorepo skeleton with Docker Compose orchestration, a working FastAPI backend with database migrations and JWT authentication, a React frontend shell with auth pages and protected routing, and an admin seed script — so that a user can register, log in, see a dashboard shell, and an admin account exists.
## Prerequisites
- Git repository initialized (done)
- Docker and Docker Compose installed on the development machine
- Node.js 20+ and Python 3.12+ available locally for development outside containers
- `GeneralPlan.md`, `README.md`, `.gitignore` already in repo (done)
---
## Database Schema (Phase 1)
### `users` table
| Column | Type | Constraints |
|---|---|---|
| id | UUID | PK, default gen_random_uuid() |
| email | VARCHAR(255) | UNIQUE, NOT NULL |
| username | VARCHAR(100) | UNIQUE, NOT NULL |
| hashed_password | VARCHAR(255) | NOT NULL |
| full_name | VARCHAR(255) | NULL |
| role | VARCHAR(20) | NOT NULL, default 'user', CHECK IN ('user','admin') |
| is_active | BOOLEAN | NOT NULL, default true |
| max_chats | INTEGER | NOT NULL, default 10 |
| oauth_provider | VARCHAR(50) | NULL |
| oauth_provider_id | VARCHAR(255) | NULL |
| telegram_chat_id | BIGINT | NULL |
| avatar_url | VARCHAR(500) | NULL |
| created_at | TIMESTAMPTZ | NOT NULL, default now() |
| updated_at | TIMESTAMPTZ | NOT NULL, default now(), on-update trigger |
Indexes: `ix_users_email` (unique), `ix_users_username` (unique).
### `sessions` table
| Column | Type | Constraints |
|---|---|---|
| id | UUID | PK, default gen_random_uuid() |
| user_id | UUID | FK -> users.id ON DELETE CASCADE, NOT NULL |
| refresh_token_hash | VARCHAR(255) | NOT NULL |
| device_info | VARCHAR(500) | NULL |
| ip_address | VARCHAR(45) | NULL |
| expires_at | TIMESTAMPTZ | NOT NULL |
| created_at | TIMESTAMPTZ | NOT NULL, default now() |
Indexes: `ix_sessions_user_id`, `ix_sessions_refresh_token_hash`.
---
## API Endpoint Signatures (Phase 1 Auth)
All under `/api/v1/auth/`.
| Method | Path | Request Body | Response | Notes |
|---|---|---|---|---|
| POST | `/register` | `{ email, username, password, full_name? }` | `{ user, access_token, refresh_token }` | Creates user + session. 201 |
| POST | `/login` | `{ email, password, remember_me?: bool }` | `{ user, access_token, refresh_token }` | `remember_me` controls refresh expiry (30d vs 24h). 200 |
| POST | `/refresh` | `{ refresh_token }` | `{ access_token, refresh_token }` | Rotates refresh token (old invalidated). 200 |
| POST | `/logout` | `{ refresh_token }` | `204 No Content` | Deletes session row |
| GET | `/me` | — | `UserResponse` | Requires valid access token. 200 |
**Token details:**
- Access token: JWT, HS256, 15-minute expiry, payload: `{ sub: user_id, role, exp, iat, jti }`
- Refresh token: opaque 64-byte hex string; only its SHA-256 hash is stored in `sessions.refresh_token_hash`
- Password hashing: bcrypt via `passlib[bcrypt]`
---
## Frontend Route Structure (Phase 1)
| Path | Component | Auth Required | Notes |
|---|---|---|---|
| `/login` | LoginPage | No | Redirects to `/` if already authenticated |
| `/register` | RegisterPage | No | Redirects to `/` if already authenticated |
| `/` | DashboardPage | Yes | Wrapped in AppLayout (sidebar + header) |
| `*` | NotFoundPage | No | 404 fallback |
---
## Tasks
### A. Project Infrastructure & Docker (Tasks 16)
- [x] **A1.** Create root-level config files: `.env.example` (all env vars: `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `SECRET_KEY`, `BACKEND_CORS_ORIGINS`, `FIRST_ADMIN_EMAIL`, `FIRST_ADMIN_PASSWORD`, `FIRST_ADMIN_USERNAME`), `docker-compose.yml`, `docker-compose.dev.yml` (volume mounts + hot reload).
- [x] **A2.** Create `nginx/nginx.conf` with reverse proxy rules: `/api/*``backend:8000`, `/``frontend:80`. Include WebSocket upgrade headers for future `/ws/*`. Add gzip, security headers, `client_max_body_size 50M`.
- [x] **A.** Create `nginx/Dockerfile` (FROM nginx:1.25-alpine, copy nginx.conf).
- [x] **A.** Create `backend/Dockerfile` (Python 3.12-slim, install deps from pyproject.toml, run uvicorn). Create `backend/pyproject.toml` with dependencies: fastapi, uvicorn[standard], sqlalchemy[asyncio], asyncpg, alembic, pydantic-settings, passlib[bcrypt], python-jose[cryptography], python-multipart, httpx. Dev deps: pytest, pytest-asyncio, httpx.
- [x] **A.** Create `frontend/Dockerfile` (multi-stage: Node 20-alpine build stage + nginx:alpine serve stage). Create `frontend/package.json` with dependencies: react, react-dom, react-router-dom, axios, zustand, @tanstack/react-query, i18next, react-i18next, i18next-http-backend, i18next-browser-languagedetector, lucide-react, clsx, tailwind-merge, class-variance-authority. Dev deps: vite, @vitejs/plugin-react, typescript, tailwindcss, postcss, autoprefixer, @types/react, @types/react-dom.
- [x] **A.** Verify `docker compose build` succeeds for all services and `docker compose up -d` starts postgres, backend, frontend, nginx. Backend health endpoint responds at `/api/v1/health`. Frontend serves through nginx.
### B. Backend Core Infrastructure (Tasks 713)
- [x] **B.** Create `backend/app/__init__.py`, `backend/app/config.py` using pydantic-settings `BaseSettings`. Fields: `DATABASE_URL`, `SECRET_KEY`, `ACCESS_TOKEN_EXPIRE_MINUTES` (15), `REFRESH_TOKEN_EXPIRE_DAYS` (30), `REFRESH_TOKEN_EXPIRE_HOURS` (24), `CORS_ORIGINS`, `ENVIRONMENT`.
- [x] **B.** Create `backend/app/database.py`: async engine, `async_sessionmaker`, `get_db` async generator, `Base = declarative_base()` with UUID pk mixin.
- [x] **B.** Create `backend/app/models/__init__.py`, `backend/app/models/user.py`, `backend/app/models/session.py` matching the schema above. SQLAlchemy 2.0 `mapped_column` style. `updated_at` with `onupdate=func.now()`.
- [x] **B0.** Set up Alembic: `backend/alembic.ini`, `backend/alembic/env.py` (async-compatible), `backend/alembic/script.py.mako`. Generate initial migration for users + sessions.
- [x] **B1.** Create `backend/app/main.py`: `create_app()` factory. Lifespan runs Alembic `upgrade head` on startup. CORS middleware. Include API routers. `/api/v1/health` endpoint.
- [x] **B2.** Create `backend/app/core/__init__.py`, `backend/app/core/security.py`: `hash_password`, `verify_password` (bcrypt), `create_access_token`, `decode_access_token`, `generate_refresh_token`, `hash_refresh_token` (SHA-256).
- [x] **B3.** Create `backend/app/schemas/__init__.py`, `backend/app/schemas/auth.py`: `RegisterRequest`, `LoginRequest` (with `remember_me`), `RefreshRequest`, `TokenResponse`, `UserResponse`. Create `backend/app/schemas/common.py`: `MessageResponse`.
### C. Backend Auth Logic (Tasks 1418)
- [x] **C4.** Create `backend/app/services/__init__.py`, `backend/app/services/auth_service.py`: `register_user`, `login_user`, `refresh_tokens`, `logout_user`. Each manages session rows. Raises HTTPException on errors.
- [x] **C5.** Create `backend/app/api/__init__.py`, `backend/app/api/deps.py`: `get_current_user` dependency (JWT → User), `require_admin` dependency.
- [x] **C6.** Create `backend/app/api/v1/__init__.py`, `backend/app/api/v1/auth.py` router with 5 auth endpoints.
- [x] **C7.** Create `backend/app/api/v1/router.py` to aggregate v1 sub-routers under `/api/v1`. Include in `main.py`.
- [x] **C8.** Create `backend/scripts/seed_admin.py`: standalone async script, reads env, creates admin user if not exists. Idempotent.
### D. Frontend Infrastructure (Tasks 1927)
- [x] **D9.** Create `frontend/vite.config.ts` (React plugin, proxy `/api` to backend in dev), `frontend/tsconfig.json`, `frontend/tsconfig.node.json` with `@/` path alias.
- [x] **D0.** Set up Tailwind CSS: `frontend/tailwind.config.ts`, `frontend/postcss.config.js`, `frontend/src/index.css` (Tailwind directives + shadcn CSS variables for dark mode).
- [x] **D1.** Initialize shadcn/ui: `frontend/components.json`. Install components: Button, Input, Label, Card, DropdownMenu, Avatar, Sheet, Separator, Sonner. Place in `frontend/src/components/ui/`.
- [x] **D2.** Set up i18next: `frontend/src/i18n.ts`, `frontend/public/locales/en/translation.json`, `frontend/public/locales/ru/translation.json` with keys for auth, layout, and common strings.
- [x] **D3.** Create `frontend/src/api/client.ts`: Axios instance, request interceptor (attach token), response interceptor (401 → refresh → retry). Create `frontend/src/api/auth.ts` with typed API functions.
- [x] **D4.** Create `frontend/src/stores/auth-store.ts` (Zustand, persisted): user, tokens, isAuthenticated, actions. Create `frontend/src/stores/ui-store.ts`: sidebarOpen, theme, persisted.
- [x] **D5.** Create `frontend/src/components/shared/theme-provider.tsx`, `language-toggle.tsx`, `protected-route.tsx`.
- [x] **D6.** Create `frontend/src/App.tsx`, `frontend/src/routes.tsx` (React Router v6), `frontend/src/main.tsx` entry point.
- [x] **D7.** Set up TanStack Query: `frontend/src/lib/query-client.ts`. Wire `QueryClientProvider` in App.
### E. Frontend Auth Pages & Layout (Tasks 2833)
- [x] **E8.** Create `frontend/src/components/auth/login-form.tsx`: email + password + remember me + submit. Calls API, stores tokens, navigates to `/`.
- [x] **E9.** Create `frontend/src/components/auth/register-form.tsx`: email, username, password, confirm password, full_name. Validation: email format, password min 8, passwords match, username 3-50 chars.
- [x] **E0.** Create `frontend/src/pages/login.tsx` and `frontend/src/pages/register.tsx`: centered card layout, redirect if authenticated.
- [x] **E1.** Create `frontend/src/components/layout/app-layout.tsx` (sidebar + main content), `frontend/src/components/layout/sidebar.tsx` (collapsible nav, placeholder items, mobile Sheet drawer).
- [x] **E2.** Create `frontend/src/components/layout/header.tsx`: sidebar toggle, page title, language toggle, theme toggle, user dropdown with logout.
- [x] **E3.** Create `frontend/src/pages/dashboard.tsx` (welcome card, placeholder stats), `frontend/src/pages/not-found.tsx` (404).
### F. Integration & Verification (Tasks 3436)
- [x] **F3.** Create `frontend/nginx.conf` for SPA fallback. Ensure multi-stage Dockerfile copies build output and nginx config.
- [ ] **F35.** End-to-end smoke test: `docker compose up --build`. Verify: postgres + migrations, health endpoint, seed_admin, frontend loads, register, login, dashboard, logout, language toggle, theme toggle.
- [x] **F3.** Write backend tests: `backend/tests/conftest.py` (async test client), `backend/tests/test_auth.py` (register, login, refresh, logout, invalid credentials, duplicate email, expired token). All pass with `pytest`.
---
## Files to Create
### Root
- `.env.example`
- `docker-compose.yml`
- `docker-compose.dev.yml`
### nginx/
- `nginx/Dockerfile`
- `nginx/nginx.conf`
### backend/
- `backend/Dockerfile`
- `backend/pyproject.toml`
- `backend/alembic.ini`
- `backend/alembic/env.py`
- `backend/alembic/script.py.mako`
- `backend/alembic/versions/<initial_migration>.py`
- `backend/app/__init__.py`
- `backend/app/main.py`
- `backend/app/config.py`
- `backend/app/database.py`
- `backend/app/models/__init__.py`
- `backend/app/models/user.py`
- `backend/app/models/session.py`
- `backend/app/schemas/__init__.py`
- `backend/app/schemas/auth.py`
- `backend/app/schemas/common.py`
- `backend/app/core/__init__.py`
- `backend/app/core/security.py`
- `backend/app/services/__init__.py`
- `backend/app/services/auth_service.py`
- `backend/app/api/__init__.py`
- `backend/app/api/deps.py`
- `backend/app/api/v1/__init__.py`
- `backend/app/api/v1/router.py`
- `backend/app/api/v1/auth.py`
- `backend/scripts/__init__.py`
- `backend/scripts/seed_admin.py`
- `backend/tests/__init__.py`
- `backend/tests/conftest.py`
- `backend/tests/test_auth.py`
### frontend/
- `frontend/Dockerfile`
- `frontend/nginx.conf`
- `frontend/package.json`
- `frontend/vite.config.ts`
- `frontend/tsconfig.json`
- `frontend/tsconfig.node.json`
- `frontend/tailwind.config.ts`
- `frontend/postcss.config.js`
- `frontend/components.json`
- `frontend/index.html`
- `frontend/src/main.tsx`
- `frontend/src/App.tsx`
- `frontend/src/routes.tsx`
- `frontend/src/index.css`
- `frontend/src/i18n.ts`
- `frontend/src/vite-env.d.ts`
- `frontend/src/lib/query-client.ts`
- `frontend/src/lib/utils.ts`
- `frontend/src/api/client.ts`
- `frontend/src/api/auth.ts`
- `frontend/src/stores/auth-store.ts`
- `frontend/src/stores/ui-store.ts`
- `frontend/src/components/ui/` (shadcn components)
- `frontend/src/components/shared/theme-provider.tsx`
- `frontend/src/components/shared/language-toggle.tsx`
- `frontend/src/components/shared/protected-route.tsx`
- `frontend/src/components/auth/login-form.tsx`
- `frontend/src/components/auth/register-form.tsx`
- `frontend/src/components/layout/app-layout.tsx`
- `frontend/src/components/layout/sidebar.tsx`
- `frontend/src/components/layout/header.tsx`
- `frontend/src/pages/login.tsx`
- `frontend/src/pages/register.tsx`
- `frontend/src/pages/dashboard.tsx`
- `frontend/src/pages/not-found.tsx`
- `frontend/public/locales/en/translation.json`
- `frontend/public/locales/ru/translation.json`
---
## Acceptance Criteria
1. `docker compose up --build` starts all 4 services (postgres, backend, frontend, nginx) without errors
2. `GET /api/v1/health` returns `{"status": "ok"}`
3. Alembic migration creates `users` and `sessions` tables with correct schema
4. `seed_admin.py` creates an admin user; re-running is idempotent
5. `POST /api/v1/auth/register` creates a new user and returns tokens
6. `POST /api/v1/auth/login` with valid credentials returns tokens; `remember_me=true` → 30-day refresh, `false` → 24-hour
7. `POST /api/v1/auth/refresh` rotates refresh token and returns new pair
8. `POST /api/v1/auth/logout` invalidates the session
9. `GET /api/v1/auth/me` with valid token returns user data; expired/invalid → 401
10. Frontend loads at `http://localhost`, unauthenticated users see login page
11. User can register, log in, and see dashboard with their name
12. Sidebar renders with placeholder navigation; header has theme toggle, language toggle, user menu with logout
13. Dark/light theme toggle works (persisted across reload)
14. Language toggle switches between English and Russian (persisted across reload)
15. Axios interceptor auto-refreshes access token on 401 and retries the request
16. All backend auth tests pass (`pytest`)
---
## Status
**IN PROGRESS** — All code written. F35 (Docker smoke test) blocked: Docker not installed on this machine. Install Docker Desktop to complete end-to-end verification.