From 68f83139eef2e8ab6aee9ece5d7b5a14272f2aad Mon Sep 17 00:00:00 2001 From: Mikhail Yenuchenko Date: Thu, 6 Aug 2026 16:13:36 +0300 Subject: [PATCH] =?UTF-8?q?docs/chore:=20Phase=201=20doc/code=20hygiene=20?= =?UTF-8?q?(code-review.md=20=C2=A7=20Phase=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes ~30 stale "Phase N" / historical-staging comment references from code and shell scripts now that v1.0 is done; fixes a stale dashboard comment claiming applications/send-log were unimplemented; adds a CSRF ADR to security.md documenting the Origin-check-over-tokens decision; resolves docs/logo in roadmap.md (directory doesn't exist, criterion already met); adds a gofmt -l check to CI so unformatted Go fails the build. The known-limitations write-up for the log-tailer offset gap (the other Phase 1 item) was already present in architecture.md § Log tailer, so no change was needed there. gofmt/go vet/go test clean on both Go modules (main + test/e2e), verified on the dev server. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/test.yml | 9 +++++++++ CHANGELOG.md | 11 +++++++++++ build/Dockerfile | 5 ++--- build/entrypoint.sh | 8 ++++---- build/postfix-config.sh | 2 +- build/postfix-wrapper.sh | 2 +- cmd/panel/httpserver.go | 4 ++-- cmd/panel/main.go | 10 +++------- docs/progress.md | 3 ++- docs/roadmap.md | 11 +++++------ docs/security.md | 30 ++++++++++++++++++++++++++++++ internal/app/sasl.go | 2 +- internal/milter/milter.go | 11 +++++------ internal/postfix/postfix.go | 11 +++++------ internal/store/domains_test.go | 4 ++-- internal/store/sendlog_test.go | 4 ++-- internal/web/handlers_apps.go | 6 +++--- internal/web/handlers_auth.go | 4 ++-- internal/web/handlers_domains.go | 4 ++-- internal/web/handlers_status.go | 4 ++-- internal/web/security.go | 14 +++++++------- internal/web/security_test.go | 2 +- internal/web/static/panel.css | 2 +- internal/web/static/panel.js | 2 +- internal/web/templates/layout.html | 2 +- internal/web/templates_test.go | 8 ++++---- internal/web/web.go | 8 ++++---- 27 files changed, 113 insertions(+), 70 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 859ed79..619b007 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,6 +16,15 @@ jobs: go-version: "1.26" cache: true + - name: Format + run: | + fmt="$(gofmt -l .)" + if [ -n "$fmt" ]; then + echo "$fmt" + echo "gofmt needed on the files above; run 'gofmt -w .'" + exit 1 + fi + - name: Vet run: go vet ./... diff --git a/CHANGELOG.md b/CHANGELOG.md index 37c24ab..bfb833b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version ## [Unreleased] +### Changed + +- Phase 1 doc/code hygiene (`docs/code-review.md`): removed ~30 stale + "Phase N" / historical-staging references from code and shell-script + comments (`cmd/panel`, `internal/*`, `build/*`) now that v1.0 is done; + fixed a stale dashboard comment (`internal/web/handlers_domains.go`) + claiming applications/send-log were unimplemented; added a CSRF ADR to + `docs/security.md` (why Origin-check, not tokens); resolved `docs/logo` in + `docs/roadmap.md` (directory doesn't exist, criterion already met); added a + `gofmt -l` check to CI (`.github/workflows/test.yml`). + ### Security - Pre-release security review (plan § D, model Fable, 2026-08-06): full pass diff --git a/build/Dockerfile b/build/Dockerfile index 3205088..cb43760 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -13,8 +13,7 @@ WORKDIR /src # Version stamped into both binaries; MUST match the image tag (spec 7.5.A). ARG VERSION=dev -# Module metadata first for layer caching. go.sum arrived in Phase 2 with the -# SQLite driver and bcrypt. +# Module metadata first for layer caching. COPY go.mod go.sum ./ RUN go mod download @@ -36,7 +35,7 @@ FROM debian:bookworm-slim AS runtime ENV DEBIAN_FRONTEND=noninteractive # Preseed Postfix so its install is non-interactive and yields a working -# main.cf. The real relay configuration is generated by the panel in Phase 5. +# main.cf. The real relay configuration is generated by the panel. RUN echo "postfix postfix/mailname string localhost" | debconf-set-selections \ && echo "postfix postfix/main_mailer_type string Internet Site" | debconf-set-selections \ && apt-get update \ diff --git a/build/entrypoint.sh b/build/entrypoint.sh index bcfaa40..779f92e 100644 --- a/build/entrypoint.sh +++ b/build/entrypoint.sh @@ -36,7 +36,7 @@ find /data/opendkim/keys -type f -name '*.private' -exec chmod 0640 {} + # sasldb2 via saslpasswd2; Postfix (user `postfix`) reads it to authenticate SMTP # clients. Share it through the `selfpost` group the same way as the DKIM tree: # setgid directory so new files inherit the group, and the database itself -# group-readable (0640). Postfix wiring to actually consult it lands in Phase 5. +# group-readable (0640). mkdir -p /data/sasl chown -R panel:selfpost /data/sasl chmod 2750 /data/sasl @@ -51,9 +51,9 @@ chown -R panel:selfpost /data/postfix chmod 2750 /data/postfix chmod 0640 /data/postfix/sender_login_maps -# Milter socket directories (spec 5 p.3, 7.3). From Phase 5 Postfix (user -# `postfix`) must actually CONNECT to both milter sockets — OpenDKIM's and the -# panel's journal-milter — not just probe them at start-up. The sockets are +# Milter socket directories (spec 5 p.3, 7.3). Postfix (user `postfix`) must +# actually CONNECT to both milter sockets — OpenDKIM's and the panel's +# journal-milter — not just probe them at start-up. The sockets are # created by the opendkim and panel users respectively, so bridge them to # `postfix` through the shared `selfpost` group: group-owned + setgid dirs mean # each socket created inside inherits group `selfpost`, and group-traversable diff --git a/build/postfix-config.sh b/build/postfix-config.sh index 1ee6b83..61578f4 100644 --- a/build/postfix-config.sh +++ b/build/postfix-config.sh @@ -106,7 +106,7 @@ postconf -e \ "smtpd_sender_restrictions=reject_sender_login_mismatch, permit" # Level-1 rate limit by client IP (spec 5 p.5). Backstop that keeps working even -# if the journal-milter (level 2, Phase 8) is down. +# if the journal-milter (level 2) is down. postconf -e \ "smtpd_client_message_rate_limit=${RATE_MSGS}" \ "anvil_rate_time_unit=${RATE_WINDOW}s" diff --git a/build/postfix-wrapper.sh b/build/postfix-wrapper.sh index fb1d2b5..83e166b 100644 --- a/build/postfix-wrapper.sh +++ b/build/postfix-wrapper.sh @@ -8,7 +8,7 @@ # an explicit startup failure instead of a relay running blind. # # This handles cold start only. Runtime milter failures after a successful start -# are governed by fail-open (milter_default_action), configured in Phase 5. +# are governed by fail-open (milter_default_action). set -eu OPENDKIM_SOCK="${OPENDKIM_SOCKET:-/run/opendkim/opendkim.sock}" diff --git a/cmd/panel/httpserver.go b/cmd/panel/httpserver.go index 8a1e6b8..6faf1d3 100644 --- a/cmd/panel/httpserver.go +++ b/cmd/panel/httpserver.go @@ -16,8 +16,8 @@ import ( ) // serveHTTP runs the control-panel HTTP server until ctx is cancelled, using -// the database handle shared by all roles. From Phase 2 this serves the real -// setup, login and authenticated panel surface (spec 7.6). +// the database handle shared by all roles: setup, login and the authenticated +// panel surface (spec 7.6). func serveHTTP(ctx context.Context, cfg config, st *store.Store) error { // Applications own the SASL accounts and the Postfix sender map; the domain // service delegates to them when a domain (and its applications) is deleted. diff --git a/cmd/panel/main.go b/cmd/panel/main.go index 8c11635..61efdff 100644 --- a/cmd/panel/main.go +++ b/cmd/panel/main.go @@ -1,10 +1,6 @@ -// Command panel is the SelfPost control panel. In the finished product this -// single binary combines several roles (spec 7.1): the HTTP panel server, +// Command panel is the SelfPost control panel. This single binary combines +// several roles (spec 7.1) as a supervised process: the HTTP panel server, // the journal-milter, the mail.log tailer and the rate-limit checks. -// -// Phase 1 wires those roles up as a supervised process with a minimal HTTP -// stub, a journal-milter socket stub (so the Postfix start wrapper's readiness -// probe passes) and a log-tailer stub. Real behaviour lands in later phases. package main import ( @@ -133,7 +129,7 @@ func loadConfig() config { // saslRealm chooses the realm new SASL accounts live under. It mirrors the // hostname Postfix's SASL layer uses so a client authenticating with a bare -// login resolves to the right account (finalised in Phase 5). +// login resolves to the right account. func saslRealm() string { if r := os.Getenv("SASL_REALM"); r != "" { return r diff --git a/docs/progress.md b/docs/progress.md index de07080..a91280d 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -50,7 +50,8 @@ - **Документация:** план D1–D9 закрыт ([documentation-plan.md](documentation-plan.md) — только метод и правила поддержки). Хвост v1.x (Codeberg в Quick start, тег образа, `docs/logo`) — [roadmap.md](roadmap.md) § «v1.x — хвост документации и деплоя». - **Рецензирование кодовой базы** (2026-08-05): [code-review.md](code-review.md) — 10 разделов (архитектура, качество, docs, GUI, legacy, риски), приоритизированный план реализации и маршрутизация моделей. Критичных багов не найдено; блокер релиза — § D ниже. - **§ D выполнен (2026-08-06):** предрелизная ревизия безопасности моделью Fable — диф от аудита v1.0 (Фаза 11, `bd64e80`) до HEAD + полный проход по чек-листу [security.md](security.md) (бывшее ТЗ 7.6). Эксплуатируемых находок нет; одна правка defence-in-depth (`--` перед логином в argv `saslpasswd2`, `internal/app/sasl.go` + тест). Принятые риски не пополнились. Детали — [implementation-plan.md](implementation-plan.md) § D и CHANGELOG `[Unreleased]/Security`. Локально `go vet`/`go test ./internal/app/...` чистые; падения `internal/domain` (`TestWriteLoadPrivateKeyRoundtrip`, `TestRenderTables`) и `internal/logtail` (`TestFollowTailsAndRotates`) — Windows-специфика (права файлов/`\` в путях/rename открытого файла), на Linux CI зелено. -- **Дальше:** релизный гейт открыт (e2e C.4 + ревизия § D) — по явной команде пользователя: резка версии в CHANGELOG, тег, пуш образа (workflow release.yml). Остальные пункты из [code-review.md](code-review.md) — polish (фазы 1–3). +- **Фаза 1 выполнена (2026-08-06)** ([code-review.md](code-review.md) § Фаза 1 — doc/code hygiene, P1): cleanup ~30 stale «Phase N» комментариев в коде и shell-скриптах; исправлен stale-комментарий в `handlers_domains.go`; ADR CSRF (Origin vs токены) добавлен в [security.md](security.md); known-limitations по log-tailer уже был в [architecture.md](architecture.md) § Log tailer — отдельного действия не потребовалось; `docs/logo` в [roadmap.md](roadmap.md) закрыт (каталога нет, критерию соответствует); `gofmt -l` добавлен в CI (`.github/workflows/test.yml`). `gofmt`/`go vet`/`go test ./...` чистые в обоих модулях (dev-server). +- **Дальше:** релизный гейт (Фаза 0) закрыт по существу — e2e C.4 и ревизия § D пройдены; остаются только шаги, которые делаются в момент резки версии (бамп тега образа + Codeberg URL, git tag) по явной команде пользователя. Остальные пункты из [code-review.md](code-review.md) — polish (фазы 2–3, 1.5). - **Принятые риски** — [security.md](security.md). **Опционально v1.x / 2.x** — [roadmap.md](roadmap.md) (хвост документации, send-log gaps, Фаза O1+, роль администратора домена). - **Прод:** `selfpost.mixfed.ru`, реальный Let's Encrypt сертификат, живой e2e (DKIM/SPF pass). Контейнер там всё ещё на образе v1.0 — Фаза 14 в него не выкатывалась. При апгрейде: админа один раз разлогинит (сменилось имя cookie), а от reverse-proxy требуется передача исходного `Host` (Apache-фрагмент из `deploy/` это делает). diff --git a/docs/roadmap.md b/docs/roadmap.md index 2a217f9..ef3bf91 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -31,9 +31,6 @@ Codeberg (`codeberg.org/mix/selfpost/raw/branch/main/deploy/...`). `vX.Y.Z` — не раньше. Сейчас может отставать от целевой версии релиза; несовпадение мешает только до первого выката по тегу. -**Каталог `docs/logo`.** Пустой; либо наполнить (если нужен отдельный asset для -внешних ссылок), либо удалить каталог, чтобы не создавать ложное ожидание. - **Убрать `implementation-plan.md` — в релизном коммите.** Документ закрыт: уникального содержания в нём нет, § D (предрелизная ревизия безопасности) продублирован в [progress.md](progress.md), [security.md](security.md) и @@ -57,9 +54,11 @@ CHANGELOG `[Unreleased]/Security`, а разделы B.1–B.3 и C.4 вырез он выполнен. **Готово, когда:** Quick start указывает на Codeberg; тег образа в compose -совпадает с релизом; `docs/logo` либо содержит файлы, либо отсутствует; -`implementation-plan.md` в `docs/archive/`, ссылок на него в активных документах -и в коде/CI не осталось. +совпадает с релизом; `implementation-plan.md` в `docs/archive/`, ссылок на него +в активных документах и в коде/CI не осталось. + +(`docs/logo` уже отсутствует как каталог — критерию «либо содержит файлы, либо +отсутствует» удовлетворяет; отдельного действия не требуется.) **Send-log vs `mail.log` (опционально).** После рестарта панели или пересоздания контейнера строки send-log могут навсегда остаться `queued` — log-tailer не diff --git a/docs/security.md b/docs/security.md index 5cdc98f..6c4d653 100644 --- a/docs/security.md +++ b/docs/security.md @@ -98,6 +98,36 @@ Hardening сверх обязательного (security-заголовки, п `/data` и теряется при recreate. Rename-ротация это не лечит. См. [architecture.md](architecture.md) § Log tailer — known gaps. +## ADR: CSRF через проверку Origin, без токенов + +**Контекст.** Панель — формы (`POST`) с cookie-сессией; классическая CSRF- +поверхность. Нужен способ отличить запрос со страницы панели от запроса, +инициированного сторонним сайтом в браузере залогиненного админа. + +**Решение.** `originAllowed` в +[internal/web/security.go](../internal/web/security.go) сверяет `Sec-Fetch-Site` +(если браузер его шлёт) либо `Origin` (fallback) с хостом панели; запрос без +обоих заголовков **пропускается**, а не отклоняется. Токенов, привязанных к +сессии и встроенных в формы, нет. + +**Почему не токены.** Панель однопользовательская (один администратор на +инстанс) — модель угроз не включает межпользовательский CSRF внутри самой +панели, только внешний сайт, заставляющий браузер админа отправить запрос. +Origin-проверка закрывает это без изменения ни одного шаблона: токен потребовал +бы скрытого поля примерно в двух десятках форм и синхронизации при каждой +новой форме, а от XSS внутри панели токен всё равно не защищает — код, +исполняющийся в origin панели, читает токен и отправляет запрос сам. От XSS +защищают автоэкранирование `html/template` и CSP, поэтому это отдельная линия +обороны, не CSRF-токен. + +**Компромисс.** Клиент, не посылающий ни `Sec-Fetch-Site`, ни `Origin` +(по-настоящему старый браузер или webview с замороженным движком), остаётся +уязвим — см. «Принятые риски» выше. Это осознанный выбор в пользу не ломать +панель в таком клиенте ценой узкой остаточной поверхности. + +**Пересмотр, если:** появится требование защиты, не зависящей от поведения +браузера, или панель станет многопользовательской. + ## Как этот список пополняется Предрелизная проверка на уязвимости ([implementation-plan.md](implementation-plan.md) diff --git a/internal/app/sasl.go b/internal/app/sasl.go index 049bf7b..4b6f0ad 100644 --- a/internal/app/sasl.go +++ b/internal/app/sasl.go @@ -28,7 +28,7 @@ type SASLDB struct { // NewSASLDB builds a manager for the sasldb2 at path with the given realm. The // realm should match SELFPOST_HOSTNAME so the account identity lines up with -// Postfix's SASL configuration in Phase 5. +// Postfix's SASL configuration. func NewSASLDB(path, realm string) *SASLDB { return &SASLDB{path: path, realm: realm, run: runSaslpasswd2, dump: dumpSASLDB} } diff --git a/internal/milter/milter.go b/internal/milter/milter.go index cb58a48..09dbcb9 100644 --- a/internal/milter/milter.go +++ b/internal/milter/milter.go @@ -37,7 +37,7 @@ type Store interface { // session accumulates the fields of one message as the milter callbacks fire. // Milter macros arrive per-stage and do not accumulate, so each value is -// captured at the stage that carries it (spec 7.3 / Phase 0 spike): SASL login +// captured at the stage that carries it (spec 7.3): SASL login // and From at MAIL, each recipient at RCPT, Subject in the headers, and the // queue-id at end-of-message. go-milter creates one session per connection; a // connection may carry several messages, so per-message fields are reset at @@ -55,8 +55,8 @@ type session struct { } // Connect captures the client IP, which comes from the addr parameter rather -// than a macro (the {client_addr} macro was empty in the spike). It is the -// rate-limit key for Phase 8; here it is recorded for completeness. +// than a macro (the {client_addr} macro was empty in testing). It is the +// rate-limit key; here it is recorded for completeness. func (s *session) Connect(host, family string, port uint16, addr net.IP, m *milter.Modifier) (milter.Response, error) { if addr != nil { s.clientIP = addr.String() @@ -133,8 +133,7 @@ func (s *session) Body(m *milter.Modifier) (milter.Response, error) { // macro reads a milter macro, tolerating Postfix's convention of wrapping // multi-character macro names in curly braces (e.g. {auth_authen}) while // single-character names (e.g. i) arrive bare. go-milter stores whatever name -// Postfix sends verbatim, so a lookup must try both forms — this is exactly the -// distinction the SASL-less Phase 0 spike could not observe. +// Postfix sends verbatim, so a lookup must try both forms. func macro(m *milter.Modifier, name string) string { if v, ok := m.Macros[name]; ok { return v @@ -180,7 +179,7 @@ func cleanAddress(a string) string { } // domainOf returns the lower-cased domain of an email address, or "" if there -// is no domain part. Sender binding (Phase 4) guarantees the From domain equals +// is no domain part. Sender binding guarantees the From domain equals // the application's domain, so this is the sending domain (spec 7.3). func domainOf(addr string) string { if i := strings.LastIndexByte(addr, '@'); i >= 0 { diff --git a/internal/postfix/postfix.go b/internal/postfix/postfix.go index 2db9e30..8104859 100644 --- a/internal/postfix/postfix.go +++ b/internal/postfix/postfix.go @@ -1,8 +1,7 @@ // Package postfix owns the Postfix configuration files the panel edits at -// runtime and the privileged reload that applies them (spec 5.1, 7.6.3-4). In -// Phase 4 that is the smtpd_sender_login_maps table binding each application's -// SASL login to the sender addresses it may use; the full relay configuration -// lands in Phase 5. +// runtime and the privileged reload that applies them (spec 5.1, 7.6.3-4): the +// smtpd_sender_login_maps table binding each application's SASL login to the +// sender addresses it may use, plus the relay configuration in main.cf. package postfix import ( @@ -33,8 +32,8 @@ func New(dir string) *Postfix { } } -// SenderLoginMapsPath is the absolute path of the generated map, so the Postfix -// main.cf written in Phase 5 can point smtpd_sender_login_maps at it. +// SenderLoginMapsPath is the absolute path of the generated map, so main.cf +// can point smtpd_sender_login_maps at it. func (p *Postfix) SenderLoginMapsPath() string { return p.senderLoginMapsPath } diff --git a/internal/store/domains_test.go b/internal/store/domains_test.go index 6d8e8ae..8fdab04 100644 --- a/internal/store/domains_test.go +++ b/internal/store/domains_test.go @@ -62,8 +62,8 @@ func TestDeleteDomainCascadesApplications(t *testing.T) { t.Fatal(err) } - // Insert an application + address directly (the AddApplication API lands in - // Phase 4); this verifies the ON DELETE CASCADE wiring now. + // Insert an application + address directly (bypassing the AddApplication + // API) to verify the ON DELETE CASCADE wiring. now := time.Now().UTC().Format(time.RFC3339) res, err := st.db.Exec( "INSERT INTO applications (domain_id, login, address_mode, created_at) VALUES (?, ?, 'wildcard', ?)", diff --git a/internal/store/sendlog_test.go b/internal/store/sendlog_test.go index a042781..b6f00d8 100644 --- a/internal/store/sendlog_test.go +++ b/internal/store/sendlog_test.go @@ -5,8 +5,8 @@ import ( "time" ) -// readSendLog returns every send_log row ordered by id. Phase 6 has no read -// query yet (the monitoring UI is Phase 7), so tests read the table directly. +// readSendLog returns every send_log row ordered by id. The monitoring UI has +// no equivalent read query, so tests read the table directly. type sendLogRow struct { QueueID string Domain string diff --git a/internal/web/handlers_apps.go b/internal/web/handlers_apps.go index c469aca..d32e6e7 100644 --- a/internal/web/handlers_apps.go +++ b/internal/web/handlers_apps.go @@ -97,8 +97,8 @@ func (s *Server) renderDomainDetail(w http.ResponseWriter, r *http.Request, stat } // What DNS actually publishes for the domain today, checked against the key - // this server signs with (phase 13.B). Cached by the checker, so - // re-rendering the page after a form post costs nothing. + // this server signs with. Cached by the checker, so re-rendering the page + // after a form post costs nothing. dns, srv := s.domainDNS(d, record, false) s.render(w, status, "domain_detail", map[string]any{ @@ -139,7 +139,7 @@ func (s *Server) renderDomainDetail(w http.ResponseWriter, r *http.Request, stat } // domainDNS resolves what the world sees for a domain: its DKIM, SPF and DMARC -// records (phase 13.B). The server's own address comes from the (separately +// records. The server's own address comes from the (separately // cached) hostname check, so the SPF heuristic knows which IP it is looking for // and no extra environment variable is needed. That server result is returned // alongside, because the page's suggested SPF record is built from the same diff --git a/internal/web/handlers_auth.go b/internal/web/handlers_auth.go index 569c99a..fad3c41 100644 --- a/internal/web/handlers_auth.go +++ b/internal/web/handlers_auth.go @@ -13,7 +13,7 @@ import ( // TLS in front, so CookieSecure — it carries the __Host- prefix, which turns // what the cookie's attributes merely promise into something the browser // enforces: Secure, Path=/ and, the point of the exercise, no Domain -// attribute, so no other host may set a cookie by this name (phase 14.B). +// attribute, so no other host may set a cookie by this name. // The prefix is only valid on a Secure cookie, so a development instance on // plain HTTP has to keep the bare name: with the prefix the browser would // discard the Set-Cookie outright and logging in would silently never stick. @@ -42,7 +42,7 @@ func (s *Server) sessionCookie() string { // is denial of service, not compromise; refusing the request and saying so in // the log is what makes it diagnosable instead of an endless login loop. The // __Host- prefix prevents this outright, but only where it applies — this -// check also covers the plain-HTTP development shape (phase 14.B). +// check also covers the plain-HTTP development shape. func (s *Server) sessionToken(r *http.Request) (string, bool) { name := s.sessionCookie() var token string diff --git a/internal/web/handlers_domains.go b/internal/web/handlers_domains.go index bdb7e91..5dbd29a 100644 --- a/internal/web/handlers_domains.go +++ b/internal/web/handlers_domains.go @@ -11,7 +11,7 @@ import ( // handleDashboard is the authenticated landing page: the list of sending // domains with their DKIM/selector and application counts, plus the add-domain -// form (spec 7.2.2). Applications and the send log arrive in later phases. +// form (spec 7.2.2). func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { s.renderDashboard(w, r, http.StatusOK, "", "") } @@ -118,7 +118,7 @@ func (s *Server) handleDeleteDomain(w http.ResponseWriter, r *http.Request) { // handleReload re-applies both the OpenDKIM configuration and the Postfix // sender map on demand (spec 7.2.12). Each Resync regenerates its files from the // database and reloads its daemon, so the button doubles as a drift-recovery. -// The button lives on the status page (phase 13.D): it is a "put the daemons +// The button lives on the status page: it is a "put the daemons // back in the state the database describes" action, which belongs with the rest // of the server-health screen rather than in the domain list's top bar. func (s *Server) handleReload(w http.ResponseWriter, r *http.Request) { diff --git a/internal/web/handlers_status.go b/internal/web/handlers_status.go index fbe8c1d..875d678 100644 --- a/internal/web/handlers_status.go +++ b/internal/web/handlers_status.go @@ -8,8 +8,8 @@ import ( ) // handleStatus renders the server status page: the panel's landing page and the -// one screen that answers "is the service healthy and will mail be accepted" -// (phase 13.A). The cheap local checks live in the polled "status_body" +// one screen that answers "is the service healthy and will mail be accepted". +// The cheap local checks live in the polled "status_body" // fragment; the hostname/PTR lookup and the configuration reload sit outside it, // because neither belongs on a five-second timer. func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { diff --git a/internal/web/security.go b/internal/web/security.go index b2eb1d4..9767a3d 100644 --- a/internal/web/security.go +++ b/internal/web/security.go @@ -8,7 +8,7 @@ import ( // contentSecurityPolicy is the panel's CSP. Everything the pages load — // stylesheet, HTMX, the panel's own script, the favicon — is served from // /static by this same origin, and no template carries an inline diff --git a/internal/web/templates_test.go b/internal/web/templates_test.go index 0d1083e..c6e2468 100644 --- a/internal/web/templates_test.go +++ b/internal/web/templates_test.go @@ -142,9 +142,9 @@ func TestNavLeadsWithStatusAndPointsDomainsAtItsOwnPath(t *testing.T) { } } -// Since the panel root now redirects to the status page, a link left pointing at +// Since the panel root redirects to the status page, a link left pointing at // "/" silently lands on the wrong screen instead of failing — so no template may -// contain one (phase 13.C). +// contain one. func TestNoTemplateLinksToTheBareRoot(t *testing.T) { forEachTemplate(t, func(name, body string) { if strings.Contains(body, `href="/"`) { @@ -154,7 +154,7 @@ func TestNoTemplateLinksToTheBareRoot(t *testing.T) { } // The reload action is a server-health control and lives only on the status -// page (phase 13.D). +// page. func TestReloadFormLivesOnlyOnTheStatusPage(t *testing.T) { forEachTemplate(t, func(name, body string) { if strings.Contains(body, `action="/reload"`) && name != "status.html" { @@ -164,7 +164,7 @@ func TestReloadFormLivesOnlyOnTheStatusPage(t *testing.T) { } // The panel's Content-Security-Policy is a plain default-src 'self' with no -// inline exemption (phase 14.A), which makes inline script and inline style a +// inline exemption, which makes inline script and inline style a // failure mode rather than a style question: an onclick= handler or a // style="..." attribute added to a template does not error, it silently stops // working in the browser. Behaviour belongs in static/panel.js (triggered from diff --git a/internal/web/web.go b/internal/web/web.go index 18c2bdd..a21a678 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -110,7 +110,7 @@ func New(st *store.Store, domains *domain.Service, apps *app.Service, cfg Config sessions: newSessionStore(st, time.Duration(idleDays)*24*time.Hour), // Published-DNS checks for the status page and the domain pages. The // checker caches its own results, so page views do not each pay for a - // round of lookups (phase 13). + // round of lookups. dns: dnscheck.New(cfg.DNSResolvers), // Setup: a handful of attempts per minute per IP is plenty for a // legitimate admin and blunts automated probing (spec 7.6.1). @@ -152,7 +152,7 @@ func (s *Server) Handler() http.Handler { // above falls through to this sub-mux, wrapped once in the auth middleware. authed := http.NewServeMux() - // The landing page is the server status (phase 13.C): the first thing an + // The landing page is the server status: the first thing an // administrator should see after logging in is whether the service is // healthy, not the domain list. handleLogin still redirects to "/". authed.HandleFunc("GET /{$}", redirectToStatus) @@ -196,12 +196,12 @@ func (s *Server) Handler() http.Handler { mux.Handle("/", s.requireAuth(authed)) // Security headers and the origin check wrap everything, including the - // unauthenticated login and setup routes (phase 14.A). + // unauthenticated login and setup routes. return s.secure(mux) } // redirectToStatus points the panel root at the status page, so there is one -// canonical URL for that content instead of two (phase 13.C). +// canonical URL for that content instead of two. func redirectToStatus(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/status", http.StatusSeeOther) }