Compare commits
5 Commits
2f84a6327f
...
cd3b2e0694
| Author | SHA1 | Date | |
|---|---|---|---|
| cd3b2e0694 | |||
| 22f357f3e8 | |||
| a376207243 | |||
| 5f3bd69e95 | |||
| bf4352231a |
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