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
@@ -174,6 +174,21 @@ func (h *AuthHandler) Register(c *echo.Context) error {
req.Role = strings.ToLower(req.Role)
}
// Public registration can be disabled by admins. The giveaway exception
// is an unconfigured server: with zero admins the first-user-becomes-
// admin bootstrap path must stay reachable (mirrors setupRedirect-
// Middleware's "users exist" reasoning). Admin account creation via the
// admin API never rides this route, so it is unaffected regardless.
if h.settings != nil && !h.settings.RegistrationEnabled() {
count, countErr := h.db.CountAdmins(c.Request().Context())
if countErr != nil || count > 0 {
if c.Request().Header.Get("HX-Request") == "true" {
return c.HTML(http.StatusForbidden, `<div class="text-red-500">Registration is disabled on this server</div>`)
}
return c.JSON(http.StatusForbidden, map[string]string{"error": "registration is disabled on this server"})
}
}
if _, err := h.db.GetUserByEmail(c.Request().Context(), req.Email); err == nil {
if c.Request().Header.Get("HX-Request") == "true" {
return c.HTML(http.StatusConflict, `<div class="text-red-500">Email already exists</div>`)