# destroying_sap A fullstack SaaS web application built with FastAPI, React, and PostgreSQL. ## Stack | Layer | Tech | |---|---| | Backend | FastAPI (async), SQLAlchemy 2, Alembic, PostgreSQL 16 | | Auth | JWT bearer tokens, bcrypt password hashing | | Frontend | React 18, TypeScript, Vite, React Router v6, TanStack Query | ## Current State - User registration and login (JWT auth) - Protected dashboard with nav bar (Dashboard | Profile | Logout) - `/api/users/me` — authenticated user info - `/api/profile/me` — GET/PUT personal profile (position, phone, date of birth, address) - Profile data stored in a dedicated `profiles` table; auto-created on first access - Admin role flag (`is_superuser`) stored in `users` table; exposed as `is_admin` in API (false for regular users, true for admins) - Admin-only user management at `/admin`: list all users, add users, delete users, toggle active status - All input sanitized before reaching the DB (null-byte rejection, length caps, format validation) - 3 separate Docker containers: `db` (PostgreSQL), `backend` (FastAPI), `frontend` (nginx) - All containers run as non-root users (UID 1001 for backend and frontend, UID 70 for db) - Network-isolated: only the frontend exposes a host port (80/5173); db and backend are unreachable from outside Docker - Dev environment seeds a test user automatically on startup (`test@example.com` / `Test123!`) - Password policy: min 8 chars, upper + lowercase, digit, special character, no common words - Pre-commit security hook (`scripts/security_check.py`) runs inside Docker on every commit ## Containers | Container | Image | Host port | Network | User (UID:GID) | Description | |---|---|---|---|---|---| | `db` | postgres:16-alpine | none | backend-net | 70:70 | PostgreSQL database | | `backend` | custom (python:3.12-slim) | none | backend-net | 1001:1001 | FastAPI management API | | `frontend` | custom (nginxinc/nginx-unprivileged:alpine) | 80 (prod) / 5173 (dev) | backend-net + frontend-net | 1001:1001 | React UI + nginx reverse proxy | **Networks:** - `backend-net` (`internal: true`) — db, backend, and frontend reverse proxy communicate here; no host routing - `frontend-net` — frontend only; this is where the single host port (80/5173) is bound The frontend nginx proxies `/api/*` to `backend:8000` via `backend-net`. No backend or database port is ever exposed to the host. ## Installation ### Prerequisites - Docker + Docker Compose ### Production ```bash git clone cd destroying_sap cp .env.example backend/.env python scripts/generate_jwt_keys.py # paste output into backend/.env docker compose up --build -d ``` - Frontend: http://localhost - API docs: not directly accessible from host (backend port not exposed); access via `docker compose exec backend` or add a dev-only port mapping ### Development (hot reload) ```bash docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build ``` - Frontend (Vite): http://localhost:5173 - Backend: reachable by frontend via Docker network only (not exposed to host) ### Local (no Docker) **1. Start PostgreSQL** ```bash docker compose up db -d ``` **2. Backend** ```bash cd backend python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -e ".[dev]" cp ../.env.example .env alembic upgrade head uvicorn app.main:app --reload ``` **3. Frontend** ```bash cd frontend && npm install && npm run dev ``` ## Environment Variables Copy `.env.example` to `backend/.env` and adjust: | Variable | Default | Description | |---|---|---| | `DATABASE_URL` | `postgresql+asyncpg://postgres:password@localhost:5432/destroying_sap` | Async PostgreSQL URL | | `JWT_PRIVATE_KEY` | — | RS256 private key PEM (generate with `scripts/generate_jwt_keys.py`) | | `JWT_PUBLIC_KEY` | — | RS256 public key PEM (generate with `scripts/generate_jwt_keys.py`) | | `CORS_ORIGINS` | `["http://localhost:5173"]` | Allowed frontend origins | ## Development ```bash # Backend lint + format cd backend && ruff check . && ruff format . # Backend tests cd backend && pytest # Frontend type check + lint cd frontend && npm run typecheck && npm run lint # New DB migration (after changing models) cd backend && alembic revision --autogenerate -m "describe change" cd backend && alembic upgrade head ```