Merge branch 'dev' into master: project bootstrap
This commit is contained in:
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
|
||||
# Secrets / local config
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# SQLite runtime
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
*.db
|
||||
*.db-journal
|
||||
*.db-wal
|
||||
*.db-shm
|
||||
|
||||
# Runtime data (DB + media uploads live here in prod)
|
||||
data/
|
||||
!data/.gitkeep
|
||||
|
||||
# Editors / IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
|
||||
# Build / Docker
|
||||
build/
|
||||
dist/
|
||||
*.log
|
||||
116
CLAUDE.md
Normal file
116
CLAUDE.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# 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`
|
||||
BIN
Logo/chicken babies r us-01.jpg
Normal file
BIN
Logo/chicken babies r us-01.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 340 KiB |
2166
Logo/chicken babies r us.ai
Normal file
2166
Logo/chicken babies r us.ai
Normal file
File diff suppressed because one or more lines are too long
BIN
Logo/chicken babies r us.png
Normal file
BIN
Logo/chicken babies r us.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
27
docs/README.md
Normal file
27
docs/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Documentation Folder
|
||||
|
||||
This folder contains business planning, architecture decisions, and documentation.
|
||||
|
||||
**No application code belongs here.**
|
||||
|
||||
## Contents
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| `ROADMAP.md` | Phased build plan, data model (dataclasses), SQL schema, visual design, env-var contract |
|
||||
| `code_guidelines.md` | Generic Python coding standards (FastAPI overrides its Flask default for this project) |
|
||||
| `security.md` | Python security baseline (OWASP-aligned) |
|
||||
| `MANUAL_TESTING.md` *(added in Phase 1)* | Manual test checklist for the public site + admin |
|
||||
|
||||
## Related Docs (in repo root)
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| `../CLAUDE.md` | Project instructions — stack, topology, security must-haves, git flow |
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Keep documents focused and concise.
|
||||
- Update docs when architecture decisions change.
|
||||
- Use markdown tables for structured information.
|
||||
- Link to external documentation where relevant.
|
||||
326
docs/ROADMAP.md
Normal file
326
docs/ROADMAP.md
Normal file
@@ -0,0 +1,326 @@
|
||||
# Chicken Babies R Us — Roadmap
|
||||
|
||||
High-level phased plan. Each phase ends in a mergeable `dev` state and a passing manual test. Claude implements phase-by-phase. This document intentionally avoids application code; the only code here is **data model (dataclasses)** and **SQL schema**, which are authoritative.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Foundation
|
||||
|
||||
- Scaffold `app/` package, `requirements.txt`, `.env.example`, `Dockerfile`, `docker-compose.yml`.
|
||||
- Pinned deps (upper bounds):
|
||||
`fastapi`, `uvicorn[standard]`, `jinja2`, `pydantic`, `pydantic-settings`, `sqlalchemy` (Core only), `markdown-it-py`, `bleach`, `Pillow`, `python-magic`, `resend`, `slowapi`, `structlog`, `itsdangerous`, `python-multipart`, `pytest`, `httpx`.
|
||||
- `structlog` init at app startup.
|
||||
- Health endpoint `/healthz` (returns app version + commit SHA from env).
|
||||
- Typed config loader reading env via `pydantic-settings`.
|
||||
|
||||
## Phase 1 — Public Site Skeleton
|
||||
|
||||
- Base Jinja layout: header with logo, nav (Home · About · Contact · Shop (disabled)), footer.
|
||||
- Mobile-first responsive CSS, no JS framework. CSS custom properties from the palette below.
|
||||
- Routes: `/`, `/about`, `/contact`, `/shop` (shop shows "Coming soon" card, no form).
|
||||
- `/` renders the blog index from DB (empty list is acceptable this phase).
|
||||
- Manual test checklist → `docs/MANUAL_TESTING.md`.
|
||||
|
||||
## Phase 2 — Content Model + Cache
|
||||
|
||||
- SQLite schema (below) with `PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;`.
|
||||
- Dataclasses (below) as the in-app model; SQL → dataclass mapper lives in `app/models/`.
|
||||
- Row-level rendered-HTML cache (`body_html_cached`) regenerated on write.
|
||||
- In-process TTL cache (60 s) over *hot query results* (published posts list, page-by-slug); invalidated on admin writes.
|
||||
- Initial migration seeds one welcome blog post + an About page so the site is not blank before admin exists.
|
||||
|
||||
## Phase 3 — Admin Auth (Magic Link)
|
||||
|
||||
- `/admin/login` — email-only form.
|
||||
- POST creates a magic-link token (256-bit, hashed at rest, 15-min TTL, single-use), sends via Resend.
|
||||
- Click link → create signed session cookie (30-day) → redirect `/admin`.
|
||||
- Logout revokes the session row (does not delete — audit trail).
|
||||
- Rate limits (SlowAPI): 5 / 15 min / IP *and* / email.
|
||||
- Allowlist from `ADMIN_EMAILS` env var. Non-allowlisted addresses: return same success message, send no email (no user enumeration).
|
||||
- Audit log row for every auth event.
|
||||
|
||||
## Phase 4 — Admin CMS
|
||||
|
||||
- `/admin` dashboard: lists pages + posts, links to edit.
|
||||
- Markdown editor: textarea + live preview + drag-and-drop image upload. Prefer minimal hand-rolled (a single `textarea` + `fetch`-based upload) over a heavy library; EasyMDE acceptable only if the minimal path proves clunky in manual testing.
|
||||
- Media upload endpoint: magic-byte validation, 8 MB cap, Pillow re-encode (JPEG/WebP/PNG), random storage name under `data/media/<yyyy>/<mm>/<random>.<ext>`.
|
||||
- CRUD: pages (About), posts (blog) with publish toggle and slug auto-gen.
|
||||
- Save path: markdown → `markdown-it-py` → `bleach` allowlist → stored in `body_html_cached`.
|
||||
|
||||
## Phase 5 — Contact Form
|
||||
|
||||
- `/contact` POST flow: field validation → hCaptcha verify → honeypot check → rate limit → `Resend` send → persist submission row → success page.
|
||||
- `FROM` on verified domain; `Reply-To` = submitter's email.
|
||||
- No internal errors leak to the user; they see a generic "something went wrong, please try again".
|
||||
|
||||
## Phase 6 — Hardening + Deploy
|
||||
|
||||
- Security headers middleware (strict nonce-based CSP, HSTS, etc.).
|
||||
- CSRF middleware on admin routes (double-submit cookie).
|
||||
- Structured access log + error log (structlog JSON in prod, pretty in dev).
|
||||
- Backup script: `sqlite3 data/app.db ".backup data/backups/app-<ts>.db"` plus a tar of `data/media/`; cron nightly on the VM.
|
||||
- Dockerfile hardening: non-root user, slim base, `HEALTHCHECK`.
|
||||
- Gitea Action: on tag push, build image and publish to the internal registry.
|
||||
|
||||
## Phase 7 (Future) — Shop
|
||||
|
||||
- Stripe Checkout integration (test-mode first).
|
||||
- Product catalog: eggs (fertile/hatchable, eating), live birds (geese, ducks, chickens).
|
||||
- Inventory counts, order history, email confirmations.
|
||||
- Admin views for orders.
|
||||
|
||||
---
|
||||
|
||||
## Data Model (dataclasses)
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class PostStatus(str, Enum):
|
||||
DRAFT = "draft"
|
||||
PUBLISHED = "published"
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
id: int
|
||||
email: str
|
||||
display_name: str
|
||||
created_at: datetime
|
||||
last_login_at: Optional[datetime]
|
||||
active: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class MagicLinkToken:
|
||||
id: int
|
||||
email: str
|
||||
token_hash: str # sha256 of raw token; raw token never stored
|
||||
created_at: datetime
|
||||
expires_at: datetime
|
||||
used_at: Optional[datetime]
|
||||
request_ip: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Session:
|
||||
id: int
|
||||
user_id: int
|
||||
token_hash: str
|
||||
created_at: datetime
|
||||
expires_at: datetime
|
||||
ip: str
|
||||
user_agent: str
|
||||
revoked_at: Optional[datetime]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Page:
|
||||
id: int
|
||||
slug: str # e.g. "about"
|
||||
title: str
|
||||
body_md: str
|
||||
body_html_cached: str # sanitized, ready-to-render HTML
|
||||
updated_at: datetime
|
||||
published: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class Post:
|
||||
id: int
|
||||
slug: str
|
||||
title: str
|
||||
body_md: str
|
||||
body_html_cached: str
|
||||
status: PostStatus
|
||||
published_at: Optional[datetime]
|
||||
updated_at: datetime
|
||||
author_user_id: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Media:
|
||||
id: int
|
||||
filename: str # random storage name
|
||||
original_filename: str
|
||||
content_type: str
|
||||
size_bytes: int
|
||||
stored_path: str # e.g. data/media/2026/04/abc123.jpg
|
||||
alt_text: str
|
||||
uploaded_by: int
|
||||
uploaded_at: datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContactSubmission:
|
||||
id: int
|
||||
name: str
|
||||
email: str
|
||||
message: str
|
||||
ip: str
|
||||
user_agent: str
|
||||
submitted_at: datetime
|
||||
handled: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuthEvent:
|
||||
id: int
|
||||
event_type: str # link_requested | link_consumed | session_revoked | rate_limited
|
||||
email: Optional[str]
|
||||
user_id: Optional[int]
|
||||
ip: str
|
||||
user_agent: str
|
||||
created_at: datetime
|
||||
detail: str # JSON string
|
||||
```
|
||||
|
||||
## SQLite Schema (authoritative)
|
||||
|
||||
```sql
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
last_login_at TEXT,
|
||||
active INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE magic_link_tokens (
|
||||
id INTEGER PRIMARY KEY,
|
||||
email TEXT NOT NULL,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
used_at TEXT,
|
||||
request_ip TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX idx_magic_email_created ON magic_link_tokens(email, created_at);
|
||||
|
||||
CREATE TABLE sessions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
ip TEXT NOT NULL,
|
||||
user_agent TEXT NOT NULL,
|
||||
revoked_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE pages (
|
||||
id INTEGER PRIMARY KEY,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
body_md TEXT NOT NULL,
|
||||
body_html_cached TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
published INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE posts (
|
||||
id INTEGER PRIMARY KEY,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
body_md TEXT NOT NULL,
|
||||
body_html_cached TEXT NOT NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('draft','published')),
|
||||
published_at TEXT,
|
||||
updated_at TEXT NOT NULL,
|
||||
author_user_id INTEGER NOT NULL REFERENCES users(id)
|
||||
);
|
||||
CREATE INDEX idx_posts_status_pub ON posts(status, published_at DESC);
|
||||
|
||||
CREATE TABLE media (
|
||||
id INTEGER PRIMARY KEY,
|
||||
filename TEXT NOT NULL UNIQUE,
|
||||
original_filename TEXT NOT NULL,
|
||||
content_type TEXT NOT NULL,
|
||||
size_bytes INTEGER NOT NULL,
|
||||
stored_path TEXT NOT NULL,
|
||||
alt_text TEXT NOT NULL DEFAULT '',
|
||||
uploaded_by INTEGER NOT NULL REFERENCES users(id),
|
||||
uploaded_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE contact_submissions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
ip TEXT NOT NULL,
|
||||
user_agent TEXT NOT NULL,
|
||||
submitted_at TEXT NOT NULL,
|
||||
handled INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE auth_events (
|
||||
id INTEGER PRIMARY KEY,
|
||||
event_type TEXT NOT NULL,
|
||||
email TEXT,
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
ip TEXT NOT NULL,
|
||||
user_agent TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
detail TEXT NOT NULL DEFAULT '{}'
|
||||
);
|
||||
CREATE INDEX idx_auth_events_created ON auth_events(created_at DESC);
|
||||
```
|
||||
|
||||
## Caching Strategy
|
||||
|
||||
- **Row-level cache**: `pages.body_html_cached` and `posts.body_html_cached` store the fully rendered, sanitized HTML. Regenerated on write, never on read. Request path = one indexed SELECT + one template render.
|
||||
- **Query cache**: a small in-process TTL cache (60 s) wraps hot queries (published-posts list, page-by-slug). Keyed by route + slug. Explicitly invalidated by any admin write.
|
||||
- **No Redis / memcached**: traffic scale (single-digit requests/second at most) doesn't justify it.
|
||||
|
||||
## Visual Design
|
||||
|
||||
Palette anchored on light blues (Head Hen's preference), softened with farm neutrals. CSS custom-property tokens:
|
||||
|
||||
| Token | Value | Use |
|
||||
|---|---|---|
|
||||
| `--c-sky` | `#A9CCE3` | Primary surfaces, header accent |
|
||||
| `--c-sky-deep` | `#5D8AA8` | Links, active states |
|
||||
| `--c-cream` | `#FAF3E7` | Page background |
|
||||
| `--c-wheat` | `#E4D4A8` | Card surfaces, subtle emphasis |
|
||||
| `--c-ink` | `#2B3A42` | Body text |
|
||||
| `--c-leaf` | `#7FA66B` | Success / small accent |
|
||||
|
||||
Typography:
|
||||
- One serif display face for headers (system serif fallback).
|
||||
- One humanist sans for body (system sans fallback).
|
||||
- Self-host any webfont. Do **not** hit Google Fonts on the hot path.
|
||||
|
||||
Logo:
|
||||
- Source assets in `Logo/`.
|
||||
- Ship PNG (and ideally a WebP conversion) in `app/static/img/`.
|
||||
- Header uses logo at ~48 px tall; include `alt="Chicken Babies R Us"`.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables (contract)
|
||||
|
||||
`.env.example` will expose these. None have defaults baked in for secrets.
|
||||
|
||||
| Var | Purpose |
|
||||
|---|---|
|
||||
| `APP_ENV` | `development` \| `production` |
|
||||
| `SECRET_KEY` | itsdangerous signer for cookies / CSRF |
|
||||
| `DATABASE_URL` | `sqlite:///data/app.db` |
|
||||
| `RESEND_API_KEY` | Resend server key |
|
||||
| `RESEND_FROM` | e.g. `no-reply@chickenbabies.example` |
|
||||
| `ADMIN_EMAILS` | Comma-separated allowlist |
|
||||
| `ADMIN_CONTACT_EMAIL` | Target inbox for contact form |
|
||||
| `HCAPTCHA_SITE_KEY` | Public key (rendered in template) |
|
||||
| `HCAPTCHA_SECRET` | Server verification secret |
|
||||
| `FORWARDED_ALLOW_IPS` | Caddy LAN IP (for Uvicorn) |
|
||||
| `SESSION_MAX_DAYS` | Default `30` |
|
||||
| `MAGIC_LINK_TTL_MIN` | Default `15` |
|
||||
122
docs/code_guidelines.md
Normal file
122
docs/code_guidelines.md
Normal file
@@ -0,0 +1,122 @@
|
||||
### Coding Standards
|
||||
|
||||
**Style & Structure**
|
||||
- Prefer longer, explicit code over compact one-liners
|
||||
- Always include docstrings for functions/classes + inline comments
|
||||
- Strongly prefer OOP-style code (classes over functional/nested functions)
|
||||
- Strong typing throughout (dataclasses, TypedDict, Enums, type hints)
|
||||
- Value future-proofing and expanded usage insights
|
||||
|
||||
**Data Design**
|
||||
- Use dataclasses for internal data modeling
|
||||
- Typed JSON structures
|
||||
- Functions return fully typed objects (no loose dicts)
|
||||
- Snapshot files in JSON or YAML
|
||||
- Human-readable fields (e.g., `scan_duration`)
|
||||
|
||||
**Templates & UI**
|
||||
- Don't mix large HTML/CSS blocks in Python code
|
||||
- Prefer Jinja templates for HTML rendering
|
||||
- Clean CSS, minimal inline clutter, readable template logic
|
||||
|
||||
**Writing & Documentation**
|
||||
- Markdown documentation
|
||||
- Clear section headers
|
||||
- Roadmap/Phase/Feature-Session style documents
|
||||
- Boilerplate templates first, then refinements
|
||||
|
||||
**Logging**
|
||||
- Use structlog (pip package)
|
||||
- Setup logging at app start: `logger = logging.get_logger(__file__)`
|
||||
|
||||
**Preferred Pip Packages**
|
||||
- API/Web Server: Flask
|
||||
- HTTP: Requests
|
||||
- Logging: Structlog
|
||||
- Scheduling: APScheduler
|
||||
|
||||
> **Per-project override:** the `chicken_babies_site` project uses **FastAPI** (not Flask). See `../CLAUDE.md` for the full authoritative stack for that project.
|
||||
|
||||
### Error Handling
|
||||
- Custom exception classes for domain-specific errors
|
||||
- Consistent error response formats (JSON structure)
|
||||
- Logging severity levels (ERROR vs WARNING)
|
||||
|
||||
### Configuration
|
||||
- Each component has environment-specific configs in its own `/config/*.yaml`
|
||||
- API: `/api/config/development.yaml`, `/api/config/production.yaml`
|
||||
- Web: `/public_web/config/development.yaml`, `/public_web/config/production.yaml`
|
||||
- `.env` for secrets (never committed)
|
||||
- Maintain `.env.example` in each component for documentation
|
||||
- Typed config loaders using dataclasses
|
||||
- Validation on startup
|
||||
|
||||
### Containerization & Deployment
|
||||
- Explicit Dockerfiles
|
||||
- Production-friendly hardening (distroless/slim when meaningful)
|
||||
- Clear build/push scripts that:
|
||||
- Use git branch as tag
|
||||
- Ask whether to tag `:latest`
|
||||
- Ask whether to push
|
||||
- Support private registries
|
||||
|
||||
### API Design
|
||||
- RESTful conventions
|
||||
- Versioning strategy (`/api/v1/...`)
|
||||
- Standardized response format:
|
||||
|
||||
```json
|
||||
{
|
||||
"app": "<APP NAME>",
|
||||
"version": "<APP VERSION>",
|
||||
"status": <HTTP STATUS CODE>,
|
||||
"timestamp": "<UTC ISO8601>",
|
||||
"request_id": "<optional request id>",
|
||||
"result": <data OR null>,
|
||||
"error": {
|
||||
"code": "<optional machine code>",
|
||||
"message": "<human message>",
|
||||
"details": {}
|
||||
},
|
||||
"meta": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Dependency Management
|
||||
- Use `requirements.txt` and virtual environments (`python3 -m venv venv`)
|
||||
- Use path `venv` for all virtual environments
|
||||
- Pin versions to version ranges
|
||||
- Activate venv before running code (unless in Docker)
|
||||
|
||||
### Testing Standards
|
||||
- Manual testing preferred for applications
|
||||
- **API Backend:** Maintain `api/docs/API_TESTING.md` with endpoint examples, curl/httpie commands, expected responses
|
||||
- **Unit tests:** Use pytest for API backend (`api/tests/`)
|
||||
- **Web Frontend:** If using a web frontend, Manual testing checklist are created in `public_web/docs`
|
||||
|
||||
### Git Standards
|
||||
|
||||
**Branch Strategy:**
|
||||
- `master` - Production-ready code only
|
||||
- `dev` - Main development branch, integration point
|
||||
- `beta` - (Optional) Public pre-release testing
|
||||
|
||||
**Workflow:**
|
||||
- Feature work branches off `dev` (e.g., `feature/add-scheduler`)
|
||||
- Merge features back to `dev` for testing
|
||||
- Promote `dev` → `beta` for public testing (when applicable)
|
||||
- Promote `beta` (or `dev`) → `master` for production
|
||||
|
||||
**Commit Messages:**
|
||||
- Use conventional commit format: `feat:`, `fix:`, `docs:`, `refactor:`, etc.
|
||||
- Keep commits atomic and focused
|
||||
- Write clear, descriptive messages
|
||||
|
||||
**Tagging:**
|
||||
- Tag releases on `master` with semantic versioning (e.g., `v1.2.3`)
|
||||
- Optionally tag beta releases (e.g., `v1.2.3-beta.1`)
|
||||
|
||||
---
|
||||
|
||||
## Workflow Preference
|
||||
I follow a pattern: **brainstorm → design → code → revise**
|
||||
46
docs/security.md
Normal file
46
docs/security.md
Normal file
@@ -0,0 +1,46 @@
|
||||
## Foundational Security Instructions
|
||||
|
||||
- Act as a security-aware software engineer generating secure Python code.
|
||||
- Produce implementations that are **secure-by-design and secure-by-default**, not merely cosmetically "secured."
|
||||
- Focus on **preventing vulnerabilities**, not renaming functions or adding superficial security wrappers.
|
||||
- Explicitly identify **trust boundaries** (user input, external systems, internal components) and apply stricter controls at all boundary crossings.
|
||||
- Treat **all external input as untrusted by default**, regardless of source, and validate or sanitize it before use.
|
||||
- Explicitly consider **data sensitivity** (e.g., public, internal, confidential, regulated) and enforce controls appropriate to the highest sensitivity level involved.
|
||||
- Clearly distinguish between **authentication**, **authorization**, and **session management**, and never conflate their responsibilities.
|
||||
- Ensure implementations **fail securely**: errors, exceptions, and edge cases MUST NOT expose sensitive data or weaken security guarantees.
|
||||
- Use inline comments (when generating code) to clearly highlight critical security controls, assumptions, and security-relevant design decisions.
|
||||
- Adhere strictly to OWASP best practices, with particular consideration for the OWASP ASVS.
|
||||
- **Avoid slopsquatting and dependency confusion**: never guess package names or APIs; only reference well-known, reputable, and maintained libraries. Explicitly note any uncommon or low-reputation dependencies.
|
||||
- Do not hardcode secrets, credentials, tokens, or cryptographic material. Always require secure external configuration or secret management mechanisms.
|
||||
|
||||
---
|
||||
|
||||
## Common Weaknesses for Python
|
||||
|
||||
### CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
|
||||
**Summary:** Failure to properly sanitize or encode user input can lead to injection of malicious scripts into web pages, enabling XSS attacks.
|
||||
**Mitigation Rule:** All user input rendered in web pages MUST be sanitized and contextually encoded using a secure library such as `bleach` or `html.escape`.
|
||||
|
||||
### CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
|
||||
**Summary:** Unsanitized user input in SQL queries can allow attackers to execute arbitrary SQL commands, compromising data integrity and confidentiality.
|
||||
**Mitigation Rule:** SQL queries MUST use parameterized statements or prepared statements provided by libraries such as `sqlite3` or `SQLAlchemy`. Direct concatenation of user input into queries MUST NOT be used.
|
||||
|
||||
### CWE-327: Use of a Broken or Risky Cryptographic Algorithm
|
||||
**Summary:** Using outdated or insecure cryptographic algorithms can compromise data confidentiality and integrity.
|
||||
**Mitigation Rule:** Cryptographic operations MUST use secure algorithms provided by the `cryptography` library. Deprecated algorithms such as MD5 or SHA-1 MUST NOT be used.
|
||||
|
||||
### CWE-798: Use of Hard-coded Credentials
|
||||
**Summary:** Hardcoding credentials in source code can lead to unauthorized access if the code is exposed or leaked.
|
||||
**Mitigation Rule:** Secrets, credentials, and tokens MUST be stored securely using environment variables, secret management tools, or configuration files outside the source code repository.
|
||||
|
||||
### CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
|
||||
**Summary:** Improper error handling or logging can expose sensitive data to unauthorized users.
|
||||
**Mitigation Rule:** Error messages and logs MUST NOT include sensitive information such as stack traces, database connection strings, or user credentials. Use logging libraries such as `logging` with appropriate log levels and sanitization.
|
||||
|
||||
### CWE-502: Deserialization of Untrusted Data
|
||||
**Summary:** Deserializing untrusted data can lead to arbitrary code execution or data tampering.
|
||||
**Mitigation Rule:** Deserialization MUST only be performed on trusted data sources. Unsafe libraries such as `pickle` MUST NOT be used for deserialization of untrusted input.
|
||||
|
||||
### CWE-829: Inclusion of Functionality from Untrusted Control Sphere
|
||||
**Summary:** Using dependencies or code from untrusted sources can introduce malicious functionality or vulnerabilities.
|
||||
**Mitigation Rule:** Dependencies MUST be sourced from reputable package repositories such as PyPI. Verify the integrity and reputation of packages before use, and pin dependency versions to avoid supply chain attacks.
|
||||
Reference in New Issue
Block a user