package httpapi import ( "encoding/json" "net/http" "strconv" "strings" "webui4frpc/internal/store" ) // handleMe returns the authenticated principal. The frontend uses this to gate // UI (hide Users nav + disable write controls for viewer/audit users). func (h *Handler) handleMe(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { methodNotAllowed(w) return } id := identityFrom(r) writeJSON(w, http.StatusOK, map[string]any{ "type": id.Type, "name": id.Name, "level": id.Level, "userId": id.UserID, }) } // ---- Users ---- // handleUsers is the collection endpoint: GET lists users, POST creates one. // Admin only. password_hash is never serialized (User.PasswordHash has json:"-"). func (h *Handler) handleUsers(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: users, err := h.Store.ListUsers() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, http.StatusOK, map[string]any{"users": users}) case http.MethodPost: var req struct { Username string `json:"username"` Password string `json:"password"` Role string `json:"role"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "parse json: "+err.Error(), http.StatusBadRequest) return } u, err := h.Store.CreateUser(req.Username, req.Password, req.Role) if err != nil { writeStoreErr(w, err) return } writeJSON(w, http.StatusCreated, u) default: methodNotAllowed(w) } } // handleUserByName operates on /users/{name}: PUT updates role/enabled/password, // DELETE removes the user. System (flag-synced) users are read-only; the last // enabled admin cannot be deleted or disabled. func (h *Handler) handleUserByName(w http.ResponseWriter, r *http.Request) { name := strings.TrimPrefix(r.URL.Path, apiPrefix+"/users/") if name == "" { http.Error(w, "username required", http.StatusBadRequest) return } u, ok := h.Store.GetUser(name) if !ok { http.Error(w, "user not found", http.StatusNotFound) return } switch r.Method { case http.MethodPut: var req struct { Password string `json:"password"` Role string `json:"role"` Enabled *bool `json:"enabled"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "parse json: "+err.Error(), http.StatusBadRequest) return } // Role defaults to the current value when omitted. role := req.Role if role == "" { role = u.Role } enabled := u.Enabled if req.Enabled != nil { enabled = *req.Enabled } // Guard: never disable/demote the last admin or superadmin. if (u.Role == "admin" || u.Role == "superadmin") && (role != u.Role || !enabled) { n, _ := h.Store.CountAdmins() if n <= 1 { http.Error(w, "cannot demote or disable the last admin", http.StatusConflict) return } } if err := h.Store.UpdateUser(u.ID, role, enabled, req.Password); err != nil { writeStoreErr(w, err) return } updated, _ := h.Store.GetUserByID(u.ID) writeJSON(w, http.StatusOK, updated) case http.MethodDelete: if u.System { http.Error(w, "system user is managed by -user/-password flags", http.StatusConflict) return } if u.Role == "admin" || u.Role == "superadmin" { n, _ := h.Store.CountAdmins() if n <= 1 { http.Error(w, "cannot delete the last admin", http.StatusConflict) return } } if err := h.Store.DeleteUser(u.ID); err != nil { writeStoreErr(w, err) return } w.WriteHeader(http.StatusNoContent) default: methodNotAllowed(w) } } // ---- API keys ---- // handleApiKeys is the collection endpoint: GET lists keys (no hashes), POST // creates a key and returns the plaintext exactly once. func (h *Handler) handleApiKeys(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: keys, err := h.Store.ListApiKeys() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, http.StatusOK, map[string]any{"apiKeys": keys}) case http.MethodPost: var req struct { UserID int64 `json:"userId"` Label string `json:"label"` Scope string `json:"scope"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "parse json: "+err.Error(), http.StatusBadRequest) return } k, plaintext, err := h.Store.CreateApiKey(req.UserID, req.Label, req.Scope) if err != nil { writeStoreErr(w, err) return } writeJSON(w, http.StatusCreated, map[string]any{ "id": k.ID, "userId": k.UserID, "label": k.Label, "scope": k.Scope, "prefix": k.Prefix, "createdAt": k.CreatedAt, "key": plaintext, // shown exactly once }) default: methodNotAllowed(w) } } // handleApiKeyByID operates on /apikeys/{id}: DELETE revokes the key. func (h *Handler) handleApiKeyByID(w http.ResponseWriter, r *http.Request) { raw := strings.TrimPrefix(r.URL.Path, apiPrefix+"/apikeys/") id, err := strconv.ParseInt(raw, 10, 64) if err != nil || id <= 0 { http.Error(w, "invalid key id", http.StatusBadRequest) return } switch r.Method { case http.MethodDelete: if err := h.Store.DeleteApiKey(id); err != nil { writeStoreErr(w, err) return } w.WriteHeader(http.StatusNoContent) default: methodNotAllowed(w) } } // writeStoreErr maps store sentinel errors to HTTP statuses. func writeStoreErr(w http.ResponseWriter, err error) { switch err { case store.ErrNotFound: http.Error(w, "not found", http.StatusNotFound) case store.ErrAlreadyExists: http.Error(w, "already exists", http.StatusConflict) case store.ErrInvalid: http.Error(w, "invalid argument", http.StatusBadRequest) default: http.Error(w, err.Error(), http.StatusInternalServerError) } }