End-to-end passwordless admin auth. /admin/login accepts an email, POSTs
mint a 256-bit magic-link token stored only as SHA-256 in
magic_link_tokens (15-min TTL, single-use via atomic rowcount UPDATE).
Resend delivers the link; in dev with no API key, EmailService logs a
structured magic_link_dev_fallback event with the URL so the flow works
offline. /admin/auth/consume/{token} verifies, upserts a users row
(display_name from email local-part), creates a sessions row, and drops
an itsdangerous-signed cb_session cookie (HttpOnly, SameSite=Lax, Secure
in prod). /admin renders a placeholder "Welcome, <name>" page pending
Phase 4 CMS. /admin/logout flips revoked_at rather than deleting the row
to preserve the audit trail.
Rate limits use SlowAPI's in-memory limiter (5/15min/IP on login,
20/15min/IP on consume) plus a DB per-email count to catch
IP-rotating abuse. ADMIN_EMAILS enforces allowlist; non-allowlisted
submissions return the same "check your inbox" page with no token
inserted and no email sent (anti-enumeration). Every event lands in
auth_events via AuditService: link_requested, link_consumed,
consume_failed, session_created, session_revoked, rate_limited.
Add a production config validator refusing empty RESEND_API_KEY,
RESEND_FROM, or ADMIN_EMAILS; add PUBLIC_BASE_URL for email link
construction. CSRF deferred to Phase 6 per roadmap scoping; logout
handler marked # TODO(phase-6-csrf).
Mark Phase 3 complete in docs/ROADMAP.md.
77 lines
2.7 KiB
HTML
77 lines
2.7 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 the index route; omitted on pre-auth pages)
|
|
#}<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{% block title %}Admin — Chicken Babies R Us{% endblock %}</title>
|
|
<meta name="robots" content="noindex, nofollow">
|
|
<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">
|
|
<span class="site-nav__link" aria-current="page">Admin</span>
|
|
</li>
|
|
{% if user is defined and user %}
|
|
<li class="site-nav__item">
|
|
{#
|
|
Plain POST form — no CSRF token yet. Phase 6 adds a
|
|
double-submit token; SameSite=Lax is sufficient in the
|
|
meantime.
|
|
#}
|
|
<form action="/admin/logout" method="post" class="site-nav__logout-form">
|
|
<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 · Admin
|
|
</p>
|
|
</div>
|
|
</footer>
|
|
</body>
|
|
</html>
|