feat: registration_enabled admin setting — hides signup surfaces, gates the API
Release / build-and-push (push) Successful in 3m9s

One boolean (Security > Public Registration) switches the whole public
signup lifecycle: front page + login links, the logged-out sidebar's
Create-an-account (via a package-level templates hook so shared page
templates keep their signatures), GET /register -> 302 /login, and an
early 403 on POST /api/auth/register. First-user exception preserved:
zero admins keeps every route and link reachable for bootstrapping —
same 'users exist' reasoning as the setup gate. Admin user creation is
unaffected by design.

Live E2E verified: flag off hides all surfaces and blocks the POST
(403), flag on restores them (201); the admin UI renders the row
automatically.
This commit is contained in:
John O'Keefe
2026-10-03 23:54:07 -04:00
parent 6aaefe210a
commit b9f797789f
43 changed files with 627 additions and 519 deletions
+15
View File
@@ -10,6 +10,7 @@ import (
"bookhoard/internal/router"
"bookhoard/internal/services"
"bookhoard/internal/sync"
"bookhoard/templates"
"context"
"log"
"time"
@@ -56,6 +57,20 @@ func main() {
if err := registry.Load(ctx); err != nil {
log.Printf("⚠️ Could not load system settings (using defaults): %v", err)
}
// Public signup surfaces follow the registration_enabled flag, with the
// first-user exception: an unconfigured server (zero admins) keeps its
// signup links and registration route reachable for bootstrapping.
templates.SetSignupVisibility(func() bool {
if registry.RegistrationEnabled() {
return true
}
count, err := queries.CountAdmins(context.Background())
if err != nil {
return true // fail open, matching setupstatus
}
return count == 0
})
// Wire the registry into the package-level password validator so live
// rule changes apply to the echo struct-tag validator and ValidatePassword.
middleware.SetDefaultPasswordSettings(registry)