Keeps repo root lean: CLAUDE.md is the only doc at root. All reference/architecture material lives under docs/. Also updates all cross-references in CLAUDE.md, docs/README.md, and the FastAPI override note in code_guidelines.md so links stay valid after the move. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
117 lines
5.6 KiB
Markdown
117 lines
5.6 KiB
Markdown
# Chicken Babies R Us — Project Instructions
|
|
|
|
Small farm website for **Chicken Babies R Us** (Morrison TN — address is intentionally **not** displayed publicly). Public brochure site with a blog-style home, About, Contact, and a disabled "Shop (coming soon)" placeholder. The owner's wife, **Head Hen**, edits all content through an admin area protected by email magic-link authentication.
|
|
|
|
> This file is authoritative for *this project*. Where it conflicts with `docs/code_guidelines.md`, this file wins.
|
|
|
|
---
|
|
|
|
## Stack (authoritative for this project)
|
|
|
|
| Layer | Choice |
|
|
|---|---|
|
|
| Language | Python 3.12 |
|
|
| Web framework | **FastAPI** (overrides the Flask default in `docs/code_guidelines.md`) |
|
|
| Templates | Jinja2 |
|
|
| ASGI server | Uvicorn, behind Caddy reverse proxy |
|
|
| Database | SQLite (WAL mode) — single-writer is fine at this scale |
|
|
| Cache | In-process TTL cache + row-level rendered-HTML cache (no Redis) |
|
|
| Email | Resend (contact form + magic-link auth) |
|
|
| Anti-spam | hCaptcha + honeypot + SlowAPI rate limits |
|
|
| Logging | `structlog` |
|
|
| Container | Docker (multi-stage), target = Debian 12 VM on home server |
|
|
| Repo host | Gitea (Actions for image build/publish wired later) |
|
|
|
|
## Deployment Topology
|
|
|
|
```
|
|
Internet
|
|
→ Cloudflare (proxied DNS)
|
|
→ OPNsense firewall (inbound 443 allowed only from Cloudflare IP ranges)
|
|
→ Virtual IP
|
|
→ Debian 12 VM
|
|
→ Caddy (TLS termination)
|
|
→ Uvicorn + FastAPI container
|
|
```
|
|
|
|
The app MUST trust `X-Forwarded-For` / `X-Forwarded-Proto` **only from Caddy's IP**. Run Uvicorn with `--proxy-headers --forwarded-allow-ips=<caddy-ip>`. Never read `X-Forwarded-*` in application code directly — let Starlette's `ProxyHeadersMiddleware` do it.
|
|
|
|
## Target Repository Layout (after Phase 0)
|
|
|
|
```
|
|
/ repo root
|
|
├── app/ FastAPI application package
|
|
│ ├── main.py app factory + startup
|
|
│ ├── config.py typed config loader (pydantic-settings)
|
|
│ ├── models/ dataclasses + SQL schema + migrations
|
|
│ ├── routes/ public_router.py, admin_router.py, auth_router.py
|
|
│ ├── services/ auth, email, cache, markdown, media, hcaptcha
|
|
│ ├── templates/ Jinja2 (public/ + admin/ + emails/)
|
|
│ └── static/ CSS, JS, site images, logo
|
|
├── data/ runtime (SQLite DB + uploads) — mounted volume in prod
|
|
├── docs/ business + architecture docs (NO application code)
|
|
│ ├── README.md
|
|
│ ├── ROADMAP.md
|
|
│ ├── code_guidelines.md
|
|
│ ├── security.md
|
|
│ └── MANUAL_TESTING.md (added in Phase 1)
|
|
├── tests/ pytest
|
|
├── Logo/ brand assets (source)
|
|
├── Dockerfile
|
|
├── docker-compose.yml
|
|
├── requirements.txt
|
|
├── .env.example
|
|
├── .gitignore
|
|
└── CLAUDE.md
|
|
```
|
|
|
|
## Security Must-Haves (in addition to `docs/security.md`)
|
|
|
|
- **SQL**: parameterized statements only (sqlite3 `?` placeholders or SQLAlchemy Core bind params). Never f-string a query.
|
|
- **Markdown**: hardened pipeline `markdown-it-py` → `bleach` allowlist. No raw HTML pass-through.
|
|
- **Image uploads**: validate magic bytes with `python-magic`, cap at 8 MB, re-encode through Pillow, store under a random filename, discard the client-supplied extension.
|
|
- **CSRF**: double-submit cookie on every admin `POST` / `PUT` / `DELETE`.
|
|
- **Cookies**: `Secure`, `HttpOnly`, `SameSite=Lax`; session IDs signed with `itsdangerous`.
|
|
- **Security headers** (middleware): strict nonce-based CSP, HSTS, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy`.
|
|
- **Magic-link tokens**: 256-bit random (`secrets.token_urlsafe(32)`), stored hashed (SHA-256), single-use, 15-minute expiry. IP is logged but not enforced (mobile roaming).
|
|
- **Rate limits** on auth endpoints: 5 requests / 15 min / IP *and* / email.
|
|
- **Admin allowlist**: only addresses in `ADMIN_EMAILS` env var may request a magic link. Any other address silently succeeds (no user enumeration) but sends no email.
|
|
- **Secrets**: env / `.env` only, never committed. `.env.example` is the public contract.
|
|
- **Audit logging**: every auth event (link requested, link consumed, session created/revoked, rate-limit hit) at INFO. Never log raw tokens or email bodies.
|
|
|
|
## How to Run (dev)
|
|
|
|
```bash
|
|
python3.12 -m venv venv
|
|
source venv/bin/activate
|
|
pip install -r requirements.txt
|
|
cp .env.example .env # fill RESEND_API_KEY, HCAPTCHA_*, ADMIN_EMAILS, SECRET_KEY
|
|
uvicorn app.main:app --reload
|
|
```
|
|
|
|
Docker (parity with prod):
|
|
|
|
```bash
|
|
docker compose up --build
|
|
```
|
|
|
|
## Testing
|
|
|
|
- `pytest` for auth, magic-link lifecycle, markdown sanitization, rate limits, contact form.
|
|
- **Never mock the DB** in auth/magic-link tests — use a temp SQLite file so behavior matches prod.
|
|
- Manual test checklist lives in `docs/MANUAL_TESTING.md` (added in roadmap Phase 1).
|
|
|
|
## Git Strategy (this project)
|
|
|
|
- Branches: `master` (prod) · `dev` (integration) · `feat/*`, `chore/*`, `docs/*`, `fix/*` (work branches).
|
|
- **Work branches off `dev`.** Local `--no-ff` merge into `dev`, then push `dev`. Promote `dev` → `master` with `--no-ff` for releases. Tag `master` with `vX.Y.Z`.
|
|
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:`.
|
|
- Do **not** push directly to `master`. Do **not** force-push shared branches.
|
|
|
|
## Pointers
|
|
|
|
- Generic Python standards: `docs/code_guidelines.md` (FastAPI overrides its Flask default)
|
|
- Security baseline: `docs/security.md`
|
|
- Phased roadmap, dataclasses, SQL schema, visual design: `docs/ROADMAP.md`
|
|
- Docs folder guide: `docs/README.md`
|