Files
chicken_babies_site/app/templates/admin/page_form.html
Phillip Tarrant 9a8506970c feat: phase 4 admin CMS — dashboard, editor, media, CSRF
Head Hen CMS end-to-end: dashboard lists all posts (drafts + published),
Markdown editor with live preview + drag-drop image upload, Pillow media
pipeline re-encoding every upload to JPEG, post CRUD + publish toggle +
hard delete, About page edit, and double-submit CSRF cookie enforced on
every admin mutating endpoint (Phase 3's TODO markers resolved).

Slug auto-generated on create and server-locked once a post has been
published. Unpublish preserves `published_at` so re-publish keeps
original date ordering. Every admin write invalidates the read-side
Post/Page TTL caches and records an `auth_events` audit row.

CSRF middleware is narrow by design — issues/refreshes the `cb_csrf`
cookie only on `GET /admin*`, and mutating endpoints opt in via
`require_csrf_form` or `require_csrf_header` Depends. Public routes,
healthz, and pre-auth login stay untouched.

64 new tests cover slugs, CSRF, media, admin posts/pages services, and
end-to-end CMS routes. Tests never mock the DB — real temp SQLite files
per the CLAUDE.md mandate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 20:42:01 -05:00

76 lines
2.3 KiB
HTML

{#
About page edit form.
Context:
- user : app.models.entities.User
- page : app.models.entities.Page
- form : dict {title, body_md}
- errors : dict {field_name: message}
- csrf_token : str
#}
{% extends "admin/base.html" %}
{% block title %}Edit About &mdash; Admin{% endblock %}
{% block content %}
<section class="page-article">
<header class="page-article__header">
<h1 class="page-article__title">Edit About page</h1>
<p>Slug: <code>{{ page.slug }}</code> &middot; slug is fixed.</p>
</header>
{% if errors %}
<p class="admin-flash admin-flash--error" role="alert">
{% for field, msg in errors.items() %}
<span>{{ msg }}</span>
{% endfor %}
</p>
{% endif %}
<form class="editor" method="post" action="/admin/pages/about">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="editor__field">
<label for="page-title">Title</label>
<input type="text"
id="page-title"
name="title"
value="{{ form.title|e }}"
required>
</div>
<div class="editor__split">
<div class="editor__pane">
<label for="page-body">Body (Markdown)</label>
<textarea id="page-body"
name="body_md"
data-editor
data-preview-target="#page-preview"
rows="20">{{ form.body_md|e }}</textarea>
<div class="drop-zone" data-drop-zone
aria-label="Drag an image here to upload and insert it">
Drop an image here to upload &amp; insert. Accepted: JPG, PNG, WebP up to 8&nbsp;MB.
</div>
</div>
<div class="editor__pane">
<span class="editor__label">Preview</span>
<div id="page-preview" class="editor__preview" aria-live="polite">
{{ page.body_html_cached|safe }}
</div>
</div>
</div>
<div class="editor__actions">
<button type="submit" class="btn btn--primary">Save changes</button>
<a class="btn btn--secondary" href="/admin">Cancel</a>
</div>
</form>
</section>
{% endblock %}
{% block scripts %}
<script defer src="{{ url_for('static', path='js/admin_editor.js') }}"></script>
{% endblock %}