Commit Graph

3 Commits

Author SHA1 Message Date
mix 538a4b6603 feat: implement B.1 — persist login sessions in SQLite with sliding idle timeout
Sessions move from an in-memory map (absolute 12h TTL) to a `sessions`
table (migration 0002), storing only the SHA-256 of the token. Expiry is
now a sliding idle window (PANEL_SESSION_IDLE_DAYS, default 7, no
absolute cap), extended at most once an hour and never by the
monitoring screens' background polling (GET + HX-Request), so a
forgotten open tab doesn't keep a session alive indefinitely. A login
now survives a container restart or redeploy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 23:21:49 +03:00
mix fc53ae1314 panel: shared nav, account settings, backup page, connection settings
Phase 12 (UI/UX). The navigation bar now renders once from layout.html
instead of being copied into each content template, so it is present on
every authenticated page — including the domain page and its delete
confirmation, which had no links at all — and the current page is
highlighted via .Active rather than quietly dropping out of the list.

New /account page changes the administrator's username and/or password:
the current password is required and the attempt is throttled on the same
limiter as the login form, so this route cannot be used to brute-force
past that limit. A password change invalidates every other session while
keeping the one performing it; a rename carries that session over.

Backup and domain import move from a card in the middle of the domain
list to their own /backup page, one card each; the handlers themselves
are unchanged, only the page the import form renders its errors on.

The domain page gains a "Sending server settings" card (server, port,
encryption) so a client can be configured without reading the docs; 587
is listed only when SUBMISSION_ENABLE is true for this deployment, which
is a deploy-time flag the panel cannot verify at runtime.

Client-side (static/panel.js, no libraries): Copy buttons on the values
that get carried elsewhere (DKIM record, new application credentials,
server name), and the Addresses field is hidden while the address mode is
wildcard, where the server ignores it.

Verified in a container on the dev server: setup, login, every page's
nav and active item, domain and application creation, all account-form
paths including cross-session invalidation, import errors, full backup
download. gofmt/vet/test/docker build green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:34:59 +03:00
mix d72a383904 Phase 2: SQLite persistence, admin setup-link, login/sessions
Implements the secure single-admin panel entry (spec 7.6).

- internal/store: modernc.org/sqlite (pure Go, static build), WAL +
  foreign keys, embedded PRAGMA user_version migrations; schema 0001
  covers admin/settings/domains/applications/send_log/rate_limits (spec 9).
- Setup secret-link (spec 7.6.1): 128-bit crypto/rand token, printed to
  log + /data/setup-token (0600), 10-min TTL with regeneration, per-IP
  rate limit, subtle.ConstantTimeCompare, failures don't invalidate,
  one-time admin form, permanent invalidation once admin exists (/setup 404).
- bcrypt admin password; server-side username/password validation.
- Login + in-memory sessions, crypto-random token, cookie
  HttpOnly/Secure/SameSite (Secure toggleable for dev HTTP), login
  rate limit, auth middleware.
- html/template base layout + setup/login/dashboard, vendored htmx 2.0.4.
- build/entrypoint.sh: fix bind-mounted /data ownership as root before
  supervisord drops to the unprivileged panel user (found via container test).

Verified on selfpost.example.com: go vet/build/test/gofmt clean; e2e curl
of setup+login flows; docker build + run with -v ./data:/data creates the
DB and 0600 token owned by panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:24:09 +03:00