When the database has no users, the UI redirects to /setup and prompts for creation of the first admin account. The setup endpoints are self-disabling — once any user exists, POST /setup/admin returns 403 and the frontend redirects /setup back to /login. The backend uses an atomic transaction to prevent race conditions on concurrent requests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
85 lines
2.4 KiB
Go
85 lines
2.4 KiB
Go
package setup
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"git.unsupervised.ca/walkies/internal/auth"
|
|
"git.unsupervised.ca/walkies/internal/respond"
|
|
)
|
|
|
|
// TokenIssuer is the subset of auth.Service the setup handler needs.
|
|
type TokenIssuer interface {
|
|
IssueToken(volunteerID int64, role string) (string, error)
|
|
}
|
|
|
|
type Handler struct {
|
|
store Storer
|
|
authSvc TokenIssuer
|
|
}
|
|
|
|
func NewHandler(store *Store, authSvc *auth.Service) *Handler {
|
|
return &Handler{store: store, authSvc: authSvc}
|
|
}
|
|
|
|
// NewHandlerFromInterfaces constructs a Handler from interface values, intended for testing.
|
|
func NewHandlerFromInterfaces(store Storer, authSvc TokenIssuer) *Handler {
|
|
return &Handler{store: store, authSvc: authSvc}
|
|
}
|
|
|
|
// Status handles GET /api/v1/setup/status.
|
|
func (h *Handler) Status(w http.ResponseWriter, r *http.Request) {
|
|
needs, err := h.store.NeedsSetup(r.Context())
|
|
if err != nil {
|
|
respond.Error(w, http.StatusInternalServerError, "could not check setup status")
|
|
return
|
|
}
|
|
respond.JSON(w, http.StatusOK, map[string]bool{"needs_setup": needs})
|
|
}
|
|
|
|
// CreateAdmin handles POST /api/v1/setup/admin.
|
|
func (h *Handler) CreateAdmin(w http.ResponseWriter, r *http.Request) {
|
|
var body struct {
|
|
Name string `json:"name"`
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
respond.Error(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
if body.Name == "" || body.Email == "" || body.Password == "" {
|
|
respond.Error(w, http.StatusBadRequest, "name, email, and password are required")
|
|
return
|
|
}
|
|
if len(body.Password) < 8 {
|
|
respond.Error(w, http.StatusBadRequest, "password must be at least 8 characters")
|
|
return
|
|
}
|
|
|
|
hashed, err := auth.HashPassword(body.Password)
|
|
if err != nil {
|
|
respond.Error(w, http.StatusInternalServerError, "could not hash password")
|
|
return
|
|
}
|
|
|
|
id, err := h.store.CreateAdmin(r.Context(), body.Name, body.Email, hashed)
|
|
if errors.Is(err, ErrSetupAlreadyDone) {
|
|
respond.Error(w, http.StatusForbidden, "setup already completed")
|
|
return
|
|
}
|
|
if err != nil {
|
|
respond.Error(w, http.StatusInternalServerError, "could not create admin account")
|
|
return
|
|
}
|
|
|
|
token, err := h.authSvc.IssueToken(id, "admin")
|
|
if err != nil {
|
|
respond.Error(w, http.StatusInternalServerError, "could not issue token")
|
|
return
|
|
}
|
|
|
|
respond.JSON(w, http.StatusCreated, map[string]string{"token": token})
|
|
}
|