feat: audit api, sdwan persist, relay fallback updates
This commit is contained in:
56
internal/server/admin_settings.go
Normal file
56
internal/server/admin_settings.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// GET /api/v1/admin/settings
|
||||
// POST /api/v1/admin/settings {key,value}
|
||||
func (s *Server) HandleAdminSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if s.store == nil {
|
||||
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"store not ready"}`)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodGet {
|
||||
settings, err := s.store.ListSettings()
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"list settings failed"}`)
|
||||
return
|
||||
}
|
||||
b, _ := json.Marshal(map[string]any{"error": 0, "settings": settings})
|
||||
writeJSON(w, http.StatusOK, string(b))
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, `{"error":1,"message":"method not allowed"}`)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Key == "" {
|
||||
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"bad request"}`)
|
||||
return
|
||||
}
|
||||
// allowlist
|
||||
switch req.Key {
|
||||
case "advanced_impersonate", "advanced_force_network", "advanced_cross_tenant":
|
||||
default:
|
||||
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"invalid key"}`)
|
||||
return
|
||||
}
|
||||
if req.Value == "" {
|
||||
req.Value = "0"
|
||||
}
|
||||
if err := s.store.SetSetting(req.Key, req.Value); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"set failed"}`)
|
||||
return
|
||||
}
|
||||
if ac := GetAccessContext(r); ac != nil {
|
||||
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "setting_change", "setting", req.Key, req.Value, r.RemoteAddr)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
|
||||
}
|
||||
40
internal/server/audit_api.go
Normal file
40
internal/server/audit_api.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// GET /api/v1/admin/audit?tenant=3&limit=50&offset=0
|
||||
func (s *Server) HandleAdminAudit(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
limit := 50
|
||||
offset := 0
|
||||
if v := r.URL.Query().Get("limit"); v != "" {
|
||||
if i, err := strconv.Atoi(v); err == nil && i > 0 && i <= 500 {
|
||||
limit = i
|
||||
}
|
||||
}
|
||||
if v := r.URL.Query().Get("offset"); v != "" {
|
||||
if i, err := strconv.Atoi(v); err == nil && i >= 0 {
|
||||
offset = i
|
||||
}
|
||||
}
|
||||
tenantID := int64(0)
|
||||
if v := r.URL.Query().Get("tenant"); v != "" {
|
||||
if i, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||||
tenantID = i
|
||||
}
|
||||
}
|
||||
logs, err := s.store.ListAuditLogs(tenantID, limit, offset)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"error": 0, "logs": logs})
|
||||
}
|
||||
@@ -26,6 +26,17 @@ func (s *Server) ResolveAccess(r *http.Request, masterToken uint64) (*AccessCont
|
||||
return s.ResolveTenantAccessToken(tok)
|
||||
}
|
||||
|
||||
func GetAccessContext(r *http.Request) *AccessContext {
|
||||
v := r.Context().Value(ServerCtxKeyAccess{})
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
if ac, ok := v.(*AccessContext); ok {
|
||||
return ac
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) ResolveTenantAccessToken(tok string) (*AccessContext, bool) {
|
||||
if tok == "" || s.store == nil {
|
||||
return nil, false
|
||||
|
||||
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/openp2p-cn/inp2p/pkg/auth"
|
||||
@@ -68,6 +69,19 @@ func (s *Server) HandleConnectReq(from *NodeInfo, req protocol.ConnectReq) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// Debug: force relay path if explicit env set
|
||||
if os.Getenv("INP2P_FORCE_RELAY") == "1" {
|
||||
log.Printf("[coord] %s → %s: force relay requested", from.Name, to.Name)
|
||||
from.Conn.Write(protocol.MsgPush, protocol.SubPushConnectRsp, protocol.ConnectRsp{
|
||||
Error: 0,
|
||||
From: to.Name,
|
||||
To: from.Name,
|
||||
Peer: toParams,
|
||||
Detail: "punch-failed",
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Push PunchStart to BOTH sides simultaneously
|
||||
punchID := fmt.Sprintf("%s-%s-%d", from.Name, to.Name, time.Now().UnixMilli())
|
||||
|
||||
|
||||
7
internal/server/ctx.go
Normal file
7
internal/server/ctx.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package server
|
||||
|
||||
// ctx key alias for main
|
||||
// NOTE: main sets this type to avoid import cycles
|
||||
// use GetAccessContext to retrieve
|
||||
|
||||
type ServerCtxKeyAccess struct{}
|
||||
Binary file not shown.
Binary file not shown.
@@ -121,5 +121,21 @@ func normalizeSDWAN(c protocol.SDWANConfig) protocol.SDWANConfig {
|
||||
c.Nodes = append(c.Nodes, protocol.SDWANNode{Node: node, IP: ip})
|
||||
}
|
||||
sort.Slice(c.Nodes, func(i, j int) bool { return c.Nodes[i].Node < c.Nodes[j].Node })
|
||||
|
||||
// de-dup subnet proxies by node+cidr
|
||||
if len(c.SubnetProxies) > 0 {
|
||||
m2 := make(map[string]protocol.SubnetProxy)
|
||||
for _, sp := range c.SubnetProxies {
|
||||
if sp.Node == "" || sp.VirtualCIDR == "" || sp.LocalCIDR == "" {
|
||||
continue
|
||||
}
|
||||
key := sp.Node + "|" + sp.VirtualCIDR + "|" + sp.LocalCIDR
|
||||
m2[key] = sp
|
||||
}
|
||||
c.SubnetProxies = c.SubnetProxies[:0]
|
||||
for _, sp := range m2 {
|
||||
c.SubnetProxies = append(c.SubnetProxies, sp)
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/netip"
|
||||
|
||||
@@ -24,7 +25,7 @@ func (s *Server) SetSDWAN(cfg protocol.SDWANConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) SetSDWANTenant(tenantID int64, cfg protocol.SDWANConfig) error {
|
||||
func (s *Server) SetSDWANTenant(tenantID int64, cfg protocol.SDWANConfig, actorType, actorID, ip string) error {
|
||||
if cfg.Mode == "hub" {
|
||||
if cfg.HubNode == "" {
|
||||
return errors.New("hub mode requires hubNode")
|
||||
@@ -37,6 +38,10 @@ func (s *Server) SetSDWANTenant(tenantID int64, cfg protocol.SDWANConfig) error
|
||||
if err := s.sdwan.saveTenant(tenantID, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if actorType != "" && s.store != nil {
|
||||
detail := fmt.Sprintf("mode=%s hub=%s nodes=%d subnetProxies=%d", cfg.Mode, cfg.HubNode, len(cfg.Nodes), len(cfg.SubnetProxies))
|
||||
_ = s.store.AddAuditLog(actorType, actorID, "sdwan_update", "tenant", fmt.Sprintf("%d", tenantID), detail, ip)
|
||||
}
|
||||
s.broadcastSDWANTenant(tenantID, s.sdwan.getTenant(tenantID))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ type NodeInfo struct {
|
||||
ShareBandwidth int `json:"shareBandwidth"`
|
||||
RelayEnabled bool `json:"relayEnabled"`
|
||||
SuperRelay bool `json:"superRelay"`
|
||||
RelayOfficial bool `json:"relayOfficial"`
|
||||
HasIPv4 int `json:"hasIPv4"`
|
||||
IPv6 string `json:"ipv6"`
|
||||
LoginTime time.Time `json:"loginTime"`
|
||||
@@ -78,12 +79,12 @@ func New(cfg config.ServerConfig) *Server {
|
||||
if err != nil {
|
||||
log.Printf("[server] open store failed: %v", err)
|
||||
} else {
|
||||
// bootstrap default admin/admin in tenant 1
|
||||
// bootstrap default tenant if missing
|
||||
if _, gErr := st.GetTenantByID(1); gErr != nil {
|
||||
if _, _, _, cErr := st.CreateTenantWithUsers("default", "admin", "admin"); cErr != nil {
|
||||
log.Printf("[server] bootstrap default tenant failed: %v", cErr)
|
||||
} else {
|
||||
log.Printf("[server] bootstrap default tenant created (tenant=1, admin/admin)")
|
||||
log.Printf("[server] bootstrap default tenant created (tenant=1)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,7 +161,7 @@ func (s *Server) GetOnlineNodesByTenant(tenantID int64) []*NodeInfo {
|
||||
}
|
||||
|
||||
// GetRelayNodes returns nodes that can serve as relay.
|
||||
// Priority: same-user private relay → super relay
|
||||
// Priority: same-user private relay → super relay (exclude official relays)
|
||||
func (s *Server) GetRelayNodes(forUser string, excludeNodes ...string) []*NodeInfo {
|
||||
excludeSet := make(map[string]bool)
|
||||
for _, n := range excludeNodes {
|
||||
@@ -172,7 +173,7 @@ func (s *Server) GetRelayNodes(forUser string, excludeNodes ...string) []*NodeIn
|
||||
|
||||
var privateRelays, superRelays []*NodeInfo
|
||||
for _, n := range s.nodes {
|
||||
if !n.IsOnline() || excludeSet[n.Name] || !n.RelayEnabled {
|
||||
if !n.IsOnline() || excludeSet[n.Name] || !n.RelayEnabled || n.RelayOfficial {
|
||||
continue
|
||||
}
|
||||
if n.User == forUser {
|
||||
@@ -200,13 +201,33 @@ func (s *Server) GetRelayNodesByTenant(tenantID int64, excludeNodes ...string) [
|
||||
if !n.IsOnline() || excludeSet[n.Name] {
|
||||
continue
|
||||
}
|
||||
if n.TenantID == tenantID && (n.RelayEnabled || n.SuperRelay) {
|
||||
if n.TenantID == tenantID && (n.RelayEnabled || n.SuperRelay) && !n.RelayOfficial {
|
||||
relays = append(relays, n)
|
||||
}
|
||||
}
|
||||
return relays
|
||||
}
|
||||
|
||||
// GetOfficialRelays returns official relay nodes (global pool)
|
||||
func (s *Server) GetOfficialRelays(excludeNodes ...string) []*NodeInfo {
|
||||
excludeSet := make(map[string]bool)
|
||||
for _, n := range excludeNodes {
|
||||
excludeSet[n] = true
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var relays []*NodeInfo
|
||||
for _, n := range s.nodes {
|
||||
if !n.IsOnline() || excludeSet[n.Name] || !n.RelayEnabled || !n.RelayOfficial {
|
||||
continue
|
||||
}
|
||||
relays = append(relays, n)
|
||||
}
|
||||
return relays
|
||||
}
|
||||
|
||||
// HandleWS is the WebSocket handler for client connections.
|
||||
func (s *Server) HandleWS(w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := s.upgrader.Upgrade(w, r, nil)
|
||||
@@ -287,6 +308,7 @@ func (s *Server) HandleWS(w http.ResponseWriter, r *http.Request) {
|
||||
ShareBandwidth: loginReq.ShareBandwidth,
|
||||
RelayEnabled: loginReq.RelayEnabled,
|
||||
SuperRelay: loginReq.SuperRelay,
|
||||
RelayOfficial: loginReq.RelayOfficial,
|
||||
PublicIP: loginReq.PublicIP,
|
||||
PublicPort: loginReq.PublicPort,
|
||||
LoginTime: time.Now(),
|
||||
@@ -464,23 +486,68 @@ func (s *Server) registerHandlers(conn *signal.Conn, node *NodeInfo) {
|
||||
|
||||
// handleRelayNodeReq finds and returns the best relay node.
|
||||
func (s *Server) handleRelayNodeReq(conn *signal.Conn, requester *NodeInfo, req protocol.RelayNodeReq) error {
|
||||
relays := s.GetRelayNodes(requester.User, requester.Name, req.PeerNode)
|
||||
|
||||
if len(relays) == 0 {
|
||||
mode := "tenant"
|
||||
if req.Mode == "official" {
|
||||
mode = "official"
|
||||
official := s.GetOfficialRelays(requester.Name, req.PeerNode)
|
||||
if len(official) == 0 {
|
||||
return conn.Write(protocol.MsgRelay, protocol.SubRelayNodeRsp, protocol.RelayNodeRsp{Error: 1})
|
||||
}
|
||||
relay := official[0]
|
||||
totp := auth.GenTOTP(relay.Token, time.Now().Unix())
|
||||
log.Printf("[server] relay selected: %s (%s) for %s → %s", relay.Name, mode, requester.Name, req.PeerNode)
|
||||
return conn.Write(protocol.MsgRelay, protocol.SubRelayNodeRsp, protocol.RelayNodeRsp{
|
||||
Error: 1,
|
||||
RelayName: relay.Name,
|
||||
RelayIP: relay.PublicIP,
|
||||
RelayPort: config.DefaultRelayPort,
|
||||
RelayToken: totp,
|
||||
Mode: mode,
|
||||
Error: 0,
|
||||
})
|
||||
}
|
||||
// prefer hub relay if sdwan mode=hub
|
||||
if requester.TenantID > 0 && s.sdwan != nil {
|
||||
cfg := s.sdwan.getTenant(requester.TenantID)
|
||||
if cfg.Mode == "hub" && cfg.HubNode != "" && cfg.HubNode != requester.Name && cfg.HubNode != req.PeerNode {
|
||||
hub := s.GetNode(cfg.HubNode)
|
||||
if hub != nil && hub.IsOnline() && hub.TenantID == requester.TenantID && hub.RelayEnabled {
|
||||
log.Printf("[server] relay selected: %s (hub) for %s → %s", hub.Name, requester.Name, req.PeerNode)
|
||||
totp := auth.GenTOTP(hub.Token, time.Now().Unix())
|
||||
return conn.Write(protocol.MsgRelay, protocol.SubRelayNodeRsp, protocol.RelayNodeRsp{
|
||||
RelayName: hub.Name,
|
||||
RelayIP: hub.PublicIP,
|
||||
RelayPort: config.DefaultRelayPort,
|
||||
RelayToken: totp,
|
||||
Mode: "private",
|
||||
Error: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// prefer same-tenant relays, exclude requester and peer
|
||||
relays := s.GetRelayNodesByTenant(requester.TenantID, requester.Name, req.PeerNode)
|
||||
if len(relays) == 0 {
|
||||
// fallback to same-user (private) then super
|
||||
relays = s.GetRelayNodes(requester.User, requester.Name, req.PeerNode)
|
||||
if len(relays) == 0 {
|
||||
// final fallback: official relays
|
||||
official := s.GetOfficialRelays(requester.Name, req.PeerNode)
|
||||
if len(official) == 0 {
|
||||
return conn.Write(protocol.MsgRelay, protocol.SubRelayNodeRsp, protocol.RelayNodeRsp{Error: 1})
|
||||
}
|
||||
relays = official
|
||||
mode = "official"
|
||||
} else if relays[0].User != requester.User {
|
||||
mode = "super"
|
||||
} else {
|
||||
mode = "private"
|
||||
}
|
||||
}
|
||||
|
||||
// Pick the first (best) relay
|
||||
relay := relays[0]
|
||||
totp := auth.GenTOTP(relay.Token, time.Now().Unix())
|
||||
|
||||
mode := "private"
|
||||
if relay.User != requester.User {
|
||||
mode = "super"
|
||||
}
|
||||
|
||||
log.Printf("[server] relay selected: %s (%s) for %s → %s", relay.Name, mode, requester.Name, req.PeerNode)
|
||||
|
||||
return conn.Write(protocol.MsgRelay, protocol.SubRelayNodeRsp, protocol.RelayNodeRsp{
|
||||
@@ -510,6 +577,7 @@ func (s *Server) PushConnect(fromNode *NodeInfo, toNodeName string, app protocol
|
||||
FromIP: fromNode.PublicIP,
|
||||
Peer: protocol.PunchParams{
|
||||
IP: fromNode.PublicIP,
|
||||
Port: fromNode.PublicPort,
|
||||
NATType: fromNode.NATType,
|
||||
HasIPv4: fromNode.HasIPv4,
|
||||
Token: auth.GenTOTP(fromNode.Token, time.Now().Unix()),
|
||||
@@ -598,6 +666,9 @@ func (s *Server) StartCleanup() {
|
||||
cfg.Mode = "mesh"
|
||||
cfg.HubNode = ""
|
||||
_ = s.sdwan.saveTenant(tid, cfg)
|
||||
if s.store != nil {
|
||||
_ = s.store.AddAuditLog("system", "0", "sdwan_update", "tenant", fmt.Sprintf("%d", tid), "hub->mesh (hub offline)", "")
|
||||
}
|
||||
s.broadcastSDWANTenant(tid, cfg)
|
||||
log.Printf("[sdwan] hub offline, auto fallback to mesh (tenant=%d)", tid)
|
||||
}
|
||||
|
||||
@@ -62,6 +62,9 @@ func (s *Server) HandleAdminCreateTenant(w http.ResponseWriter, r *http.Request)
|
||||
status = 1
|
||||
}
|
||||
_ = s.store.UpdateTenantStatus(id, status)
|
||||
if ac := GetAccessContext(r); ac != nil {
|
||||
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "tenant_status", "tenant", fmt.Sprintf("%d", id), fmt.Sprintf("status=%d", status), r.RemoteAddr)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
|
||||
return
|
||||
}
|
||||
@@ -165,6 +168,9 @@ func (s *Server) HandleAdminCreateAPIKey(w http.ResponseWriter, r *http.Request)
|
||||
status = 1
|
||||
}
|
||||
_ = s.store.UpdateAPIKeyStatus(keyID, status)
|
||||
if ac := GetAccessContext(r); ac != nil {
|
||||
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "apikey_status", "apikey", fmt.Sprintf("%d", keyID), fmt.Sprintf("status=%d", status), r.RemoteAddr)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
|
||||
return
|
||||
}
|
||||
@@ -191,6 +197,9 @@ func (s *Server) HandleAdminCreateAPIKey(w http.ResponseWriter, r *http.Request)
|
||||
}{0, "ok", key, tenantID}
|
||||
b, _ := json.Marshal(resp)
|
||||
writeJSON(w, http.StatusOK, string(b))
|
||||
if ac := GetAccessContext(r); ac != nil {
|
||||
_ = s.store.AddAuditLog(ac.Kind, fmt.Sprintf("%d", ac.UserID), "apikey_create", "tenant", fmt.Sprintf("%d", tenantID), req.Scope, r.RemoteAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) HandleTenantEnroll(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
176
internal/server/user_api.go
Normal file
176
internal/server/user_api.go
Normal 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"}`)
|
||||
}
|
||||
Reference in New Issue
Block a user