Files
chicken_babies_site/app/templates/admin/base.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

89 lines
3.3 KiB
HTML

{#
Minimal admin layout shell.
Reuses the public site's CSS (palette + typography) so Head Hen sees a
consistent visual language without us maintaining a second stylesheet.
Intentionally simpler than the public base: no marketing hero,
no multi-link primary nav — just the brand mark, the admin context
label, and a logout control when the viewer is authenticated.
Context the child template may override:
- title : <title> content
- content : main body
- user : app.models.entities.User | None
(passed by authed routes; omitted on pre-auth pages)
- csrf_token : str
(empty string on pre-auth pages; otherwise the signed
CSRF token issued by CSRFCookieMiddleware)
#}<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Admin &mdash; Chicken Babies R Us{% endblock %}</title>
<meta name="robots" content="noindex, nofollow">
{#
CSRF meta tag: admin JS (live preview, drag-drop upload) reads
this to send the X-CSRF-Token header. Empty string on pre-auth
pages is harmless — those endpoints don't require CSRF.
#}
<meta name="csrf-token" content="{{ csrf_token|default('', true) }}">
<link rel="icon" href="{{ url_for('static', path='img/favicon.ico') }}" sizes="any">
<link rel="stylesheet" href="{{ url_for('static', path='css/site.css') }}">
</head>
<body class="admin-body">
<a class="skip-link" href="#main-content">Skip to main content</a>
<header class="site-header">
<div class="wrap site-header__wrap">
<a class="site-header__brand" href="/" aria-label="Chicken Babies R Us home">
<picture>
<source srcset="{{ url_for('static', path='img/logo.webp') }}" type="image/webp">
<img src="{{ url_for('static', path='img/logo.png') }}"
alt="Chicken Babies R Us"
height="48"
class="site-header__logo">
</picture>
</a>
<nav class="site-nav" aria-label="Admin">
<ul class="site-nav__list">
<li class="site-nav__item">
<a class="site-nav__link" href="/admin">Dashboard</a>
</li>
{% if user is defined and user %}
<li class="site-nav__item">
{#
Plain POST form with the double-submit CSRF token.
The token is stamped into the form by the route and
verified against the `cb_csrf` cookie server-side.
#}
<form action="/admin/logout" method="post" class="site-nav__logout-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token|default('', true) }}">
<button type="submit" class="btn btn--link">Log out</button>
</form>
</li>
{% endif %}
</ul>
</nav>
</div>
</header>
<main id="main-content" class="site-main" tabindex="-1">
<div class="wrap">
{% block content %}{% endblock %}
</div>
</main>
<footer class="site-footer">
<div class="wrap site-footer__wrap">
<p class="site-footer__tag">
Chicken Babies R Us &middot; Admin
</p>
</div>
</footer>
{% block scripts %}{% endblock %}
</body>
</html>