package admin import ( "encoding/json" "log/slog" "net/http" "strings" ) // Handler bundles admin HTTP handlers and their dependencies. type Handler struct { store *UserStore syncWriter *SyncWriter logger *slog.Logger } // NewHandler creates a new admin Handler. func NewHandler(store *UserStore, syncWriter *SyncWriter, logger *slog.Logger) *Handler { return &Handler{ store: store, syncWriter: syncWriter, logger: logger, } } // RegisterRoutes mounts admin routes on the given mux. func (h *Handler) RegisterRoutes(mux *http.ServeMux, authMiddleware func(http.Handler) http.Handler) { // Admin API (protected by bearer token) mux.Handle("GET /admin/api/users", authMiddleware(http.HandlerFunc(h.listUsers))) mux.Handle("POST /admin/api/users", authMiddleware(http.HandlerFunc(h.createUsers))) mux.Handle("DELETE /admin/api/users/{username}", authMiddleware(http.HandlerFunc(h.deleteUser))) mux.Handle("GET /admin/api/health", authMiddleware(http.HandlerFunc(h.adminHealth))) } // --- API Handlers --- func (h *Handler) listUsers(w http.ResponseWriter, r *http.Request) { users, err := h.store.List() if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if users == nil { users = []User{} } writeJSON(w, http.StatusOK, users) } type createUsersResponse struct { Results []CreateUserResult `json:"results"` } func (h *Handler) createUsers(w http.ResponseWriter, r *http.Request) { var req CreateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON body"}) return } if len(req.Users) == 0 { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "no users provided"}) return } results := h.store.Create(req) // Sync to Authelia YAML if err := h.syncWriter.Sync(); err != nil { h.logger.Error("sync failed after create", "error", err) } writeJSON(w, http.StatusCreated, createUsersResponse{Results: results}) } func (h *Handler) deleteUser(w http.ResponseWriter, r *http.Request) { username := r.PathValue("username") if username == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username is required"}) return } if err := h.store.Delete(username); err != nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()}) return } // Sync to Authelia YAML if err := h.syncWriter.Sync(); err != nil { h.logger.Error("sync failed after delete", "error", err) } writeJSON(w, http.StatusOK, map[string]string{"status": "deleted", "username": username}) } func (h *Handler) adminHealth(w http.ResponseWriter, r *http.Request) { count, err := h.store.Count() status := "ok" if err != nil { status = "degraded" } writeJSON(w, http.StatusOK, map[string]interface{}{ "status": status, "user_count": count, "authelia_db": h.syncWriter.usersDBPath, }) } // --- Helpers --- func writeJSON(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) } // ErrorResponse is a generic error response. type ErrorResponse struct { Error string `json:"error"` } // IsHTMLRequest checks if the client expects HTML (for HTMX routing). func IsHTMLRequest(r *http.Request) bool { accept := r.Header.Get("Accept") return strings.Contains(accept, "text/html") || r.Header.Get("HX-Request") != "" }