"""Canonical persistence-layer dataclasses. One dataclass per table in the authoritative SQLite schema documented in ``docs/ROADMAP.md`` ("SQLite Schema (authoritative)"). These map 1:1 to the columns of each table — field names, types, and nullability all match — so the mapper layer (:mod:`app.models.mappers`) can convert ``sqlalchemy.Row`` objects to dataclass instances with no guesswork. Design notes ------------ - Dataclasses are *not* frozen. Later phases mutate fields such as ``User.last_login_at`` or ``MagicLinkToken.used_at`` on successful auth events; freezing would force service code into hand-rolled copying. Immutability for view-layer projections is still enforced via ``PostSummary`` in :mod:`app.models.posts`. - Datetimes are always timezone-aware UTC at the Python boundary. The SQLite columns are ``TEXT`` holding ISO-8601 strings; conversion happens only in :mod:`app.models.mappers`, so application code never sees a naive datetime. - ``PostStatus`` is a string-valued ``Enum`` to keep JSON/template rendering trivial while still providing type-level safety. """ from __future__ import annotations from dataclasses import dataclass from datetime import datetime from enum import Enum from typing import Optional class PostStatus(str, Enum): """Publication lifecycle for a blog post. The string values match the ``CHECK`` constraint on ``posts.status`` in the SQLite schema; adding a new value here would require a migration, so this enum is deliberately small. """ DRAFT = "draft" PUBLISHED = "published" @dataclass class User: """Admin user row. Phase 2 seeds a single inactive system user (``id=1``) so the ``posts.author_user_id`` foreign key has something to reference; real admin users are provisioned in Phase 3's magic-link flow. """ id: int email: str display_name: str created_at: datetime last_login_at: Optional[datetime] active: bool @dataclass class MagicLinkToken: """Single-use email-login token. ``token_hash`` stores the SHA-256 of the raw token; the raw token is emailed to the user and never persisted. ``used_at`` is set when the token is consumed so we can refuse replay attempts without deleting the audit row. """ id: int email: str token_hash: str created_at: datetime expires_at: datetime used_at: Optional[datetime] request_ip: str @dataclass class Session: """Authenticated admin session. ``revoked_at`` records logouts without deleting the row, so the audit log remains complete. ``ip`` / ``user_agent`` are snapshots from session creation, not live values. """ 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: """Static-ish content page (e.g. About). ``body_html_cached`` is regenerated on write by the Phase 4 admin flow via the Markdown pipeline and stored here so render time costs only a SELECT, not a sanitize. See "Caching Strategy" in ``docs/ROADMAP.md``. """ id: int slug: str title: str body_md: str body_html_cached: str updated_at: datetime published: bool @dataclass class Post: """Blog post row. Mirrors the ``posts`` table exactly. ``body_html_cached`` follows the same regenerate-on-write convention as :class:`Page`. """ 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: """Uploaded image metadata. ``filename`` is the random storage name assigned on upload; the original client-supplied filename is preserved for display only and NEVER used to build a filesystem path. ``stored_path`` is relative to the project root. """ id: int filename: str original_filename: str content_type: str size_bytes: int stored_path: str alt_text: str uploaded_by: int uploaded_at: datetime @dataclass class ContactSubmission: """Submission from the public ``/contact`` form. ``handled`` flips true once Head Hen has actioned the submission; retained indefinitely as part of the contact audit log. No sensitive fields — by design we only capture what the form asks for. """ id: int name: str email: str message: str ip: str user_agent: str submitted_at: datetime handled: bool @dataclass class AuthEvent: """Append-only audit record for auth-related events. ``event_type`` values are one of ``link_requested``, ``link_consumed``, ``session_revoked``, ``rate_limited`` (see Phase 3). ``detail`` is a JSON string so we can attach event-specific context without schema churn. """ id: int event_type: str email: Optional[str] user_id: Optional[int] ip: str user_agent: str created_at: datetime detail: str