feat: audit api, sdwan persist, relay fallback updates

This commit is contained in:
2026-03-06 14:47:03 +08:00
parent e96a2e5dd9
commit 57b4dadd42
26 changed files with 991 additions and 183 deletions

176
internal/server/user_api.go Normal file
View File

@@ -0,0 +1,176 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"unicode"
)
// Admin user management
// GET /api/v1/admin/users?tenant=1
// POST /api/v1/admin/users {tenant, role, email, password}
// POST /api/v1/admin/users/{id}?status=0|1
// POST /api/v1/admin/users/{id}/password {password}
func IsValidGlobalUsername(v string) bool {
if len(v) < 6 {
return false
}
for _, r := range v {
if r > unicode.MaxASCII || !unicode.IsLetter(r) {
return false
}
}
return true
}
func (s *Server) HandleAdminUsers(w http.ResponseWriter, r *http.Request) {
if s.store == nil {
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"store not ready"}`)
return
}
// list
if r.Method == http.MethodGet {
tenantID := int64(0)
_ = r.ParseForm()
fmt.Sscanf(r.Form.Get("tenant"), "%d", &tenantID)
if tenantID <= 0 {
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"tenant required"}`)
return
}
users, err := s.store.ListUsers(tenantID)
if err != nil {
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"list users failed"}`)
return
}
// strip password hash
out := make([]map[string]any, 0, len(users))
for _, u := range users {
out = append(out, map[string]any{
"id": u.ID,
"tenant_id": u.TenantID,
"role": u.Role,
"email": u.Email,
"status": u.Status,
"created_at": u.CreatedAt,
})
}
resp := struct {
Error int `json:"error"`
Message string `json:"message"`
Users interface{} `json:"users"`
}{0, "ok", out}
b, _ := json.Marshal(resp)
writeJSON(w, http.StatusOK, string(b))
return
}
// update status or password
if r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/admin/users/") {
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
var id int64
// /api/v1/admin/users/{id}/password
if strings.HasSuffix(r.URL.Path, "/password") && len(parts) >= 5 {
_, _ = fmt.Sscanf(parts[len(parts)-2], "%d", &id)
} else if strings.HasSuffix(r.URL.Path, "/delete") && len(parts) >= 5 {
_, _ = fmt.Sscanf(parts[len(parts)-2], "%d", &id)
} else {
_, _ = fmt.Sscanf(parts[len(parts)-1], "%d", &id)
}
if id <= 0 {
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"bad request"}`)
return
}
// /password
if strings.HasSuffix(r.URL.Path, "/password") {
var req struct {
Password string `json:"password"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
if req.Password == "" || len(req.Password) < 6 {
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"password too short"}`)
return
}
if err := s.store.UpdateUserPassword(id, req.Password); err != nil {
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"update password failed"}`)
return
}
if ac := GetAccessContext(r); ac != nil {
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "user_password", "user", fmt.Sprintf("%d", id), "", r.RemoteAddr)
}
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
return
}
// delete
if strings.HasSuffix(r.URL.Path, "/delete") {
if err := s.store.UpdateUserStatus(id, 0); err != nil {
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"delete failed"}`)
return
}
if ac := GetAccessContext(r); ac != nil {
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "user_delete", "user", fmt.Sprintf("%d", id), "", r.RemoteAddr)
}
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
return
}
// status
st := r.URL.Query().Get("status")
if st == "" {
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"status required"}`)
return
}
status := 0
if st == "1" {
status = 1
}
if err := s.store.UpdateUserStatus(id, status); err != nil {
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"update status failed"}`)
return
}
if ac := GetAccessContext(r); ac != nil {
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "user_status", "user", fmt.Sprintf("%d", id), fmt.Sprintf("status=%d", status), r.RemoteAddr)
}
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
return
}
// create
if r.Method != http.MethodPost {
writeJSON(w, http.StatusMethodNotAllowed, `{"error":1,"message":"method not allowed"}`)
return
}
var req struct {
TenantID int64 `json:"tenant"`
Role string `json:"role"`
Email string `json:"email"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.TenantID <= 0 || req.Role == "" || req.Email == "" || req.Password == "" {
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"bad request"}`)
return
}
if len(req.Password) < 6 {
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"password too short"}`)
return
}
if !IsValidGlobalUsername(req.Email) {
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"username must be letters only and >=6"}`)
return
}
if exists, err := s.store.UserEmailExistsGlobal(req.Email); err != nil {
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"check user failed"}`)
return
} else if exists {
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"username exists"}`)
return
}
if _, err := s.store.CreateUser(req.TenantID, req.Role, req.Email, req.Password, 1); err != nil {
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"create user failed"}`)
return
}
if ac := GetAccessContext(r); ac != nil {
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "user_create", "tenant", fmt.Sprintf("%d", req.TenantID), req.Email, r.RemoteAddr)
}
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
}