# 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 (RS256), bcrypt password hashing | | Frontend | React 18, TypeScript, Vite, React Router v6, TanStack Query | ## Current State - User registration and login (JWT RS256 auth, 8-hour expiry) - Protected dashboard with nav bar - `/api/users/me` — authenticated user info - `/api/profile/me` — GET/PUT personal profile (position, phone, date of birth, address) - Admin-only user management at `/admin`: list, add, delete, toggle active - All input sanitized before reaching the DB (null-byte rejection, length caps, format validation) - **PDF Documents app** (`/apps/documents`): upload PDFs, async text extraction (pdfplumber), AI classification via Anthropic / Ollama / LM Studio, per-user categories, file download - Admin settings per app at `/apps/documents/settings/admin`: AI provider (cloud or local), upload limits; config stored in `/config/doc_service_config.json` on a shared Docker volume - `/apps` launcher hub — one card per installed app with Open + Settings links - 4 separate Docker containers: `db`, `backend`, `doc-service`, `frontend` - All containers run as non-root users (UID 1001 for app containers, UID 70 for db) - Network-isolated: only the frontend exposes a host port (80/5173); all backend services 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 + proxy to doc-service | | `doc-service` | custom (python:3.12-slim) | none | backend-net | 1001:1001 | PDF extraction microservice | | `frontend` | custom (nginxinc/nginx-unprivileged:alpine) | 80 (prod) / 5173 (dev) | backend-net + frontend-net | 1001:1001 | React UI + nginx reverse proxy | **Networks:** - `backend-net` — db, backend, doc-service, and frontend reverse proxy; no host ports bound; outbound internet access allowed (needed for cloud AI API calls) - `frontend-net` — frontend only; this is where the single host port (80/5173) is bound **Volumes:** - `postgres_data` — PostgreSQL data files - `doc_data` — uploaded PDF files (mounted into doc-service at `/data/documents`) - `app_config` — per-service runtime config JSON files (mounted into backend and doc-service at `/config`) The frontend nginx proxies `/api/*` to `backend:8000` via `backend-net`. The backend proxies `/api/documents/*` and `/api/documents/categories/*` to `doc-service:8001`. No backend, doc-service, 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) After first start, configure the AI provider at `/apps/documents/settings/admin` (admin login required). ### 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 pip install -e ".[dev]" cp ../.env.example .env alembic upgrade head uvicorn app.main:app --reload ``` **3. doc-service** ```bash cd features/doc-service python -m venv .venv && source .venv/bin/activate pip install -e . alembic upgrade head uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload ``` **4. 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 | | `APP_CONFIG_DIR` | `/config` | Directory for per-service runtime config JSON files | | `DOC_SERVICE_URL` | `http://doc-service:8001` | Internal URL of the doc-service (set by docker-compose) | `doc-service` reads `DATABASE_URL`, `DATA_DIR`, and `CONFIG_PATH` from its own environment (set in `docker-compose.yml`). ## 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 — main backend cd backend && alembic revision --autogenerate -m "describe change" cd backend && alembic upgrade head # New DB migration — doc-service cd features/doc-service && alembic revision --autogenerate -m "describe change" cd features/doc-service && alembic upgrade head ```