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:
285
plans/phase-1-foundation.md
Normal file
285
plans/phase-1-foundation.md
Normal 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 1–6)
|
||||
|
||||
- [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 7–13)
|
||||
|
||||
- [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 14–18)
|
||||
|
||||
- [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 19–27)
|
||||
|
||||
- [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 28–33)
|
||||
|
||||
- [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 34–36)
|
||||
|
||||
- [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.
|
||||
Reference in New Issue
Block a user