Files
personal-ai-assistant/plans/phase-1-foundation.md
dolgolyov.alexei 7c752cae6b 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>
2026-03-19 12:25:02 +03:00

286 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.