Files
webui4frpc/internal/httpapi/handlers_users.go
JianFeeeee 4a41608d94 refactor: 审查修复 — netload 消除重复 /sys 读 + 全项目 gofmt
- netload_linux.go: SampleNetLoad 聚合循环不再对每接口重复
  readIfaceSpeed (snapshot 已汇总 cur.capMbps), 每次采样省 N 次 /sys 读
- gofmt -w: ring.go/ring_engine.go/auth.go/handlers.go/handlers_logs.go/
  handlers_users.go/store.go 结构体字段对齐与注释缩进
- README.md: markdownlint 自动修复 (MD028/MD040)

审查结论: 令牌环本身即互斥协议 — OnToken(收令牌)与 StartRing(发令牌)
在同一节点上由令牌串行化, 不存在需要加锁的竞争; WatchLeader 的读为
良性读, 无需 mutex
2026-08-24 22:24:35 +08:00

207 lines
5.6 KiB
Go

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.
if u.Role == "admin" && (role != "admin" || !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" {
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)
}
}