package api import ( "context" "errors" "net/http" "strconv" "time" "github.com/gin-gonic/gin" "ws_usernode/internal/config" "ws_usernode/internal/model" "ws_usernode/internal/service" ) // UserHandler 外部用户接口(列表/详情/创建/更新/禁用/启用/延期/删除,admin)。 type UserHandler struct { svc *service.UserService cfg *config.Config h *Handler // 访问审计 helper } // UserCreateRequest 管理员创建外部用户请求。 type UserCreateRequest struct { Username string `json:"username" binding:"required"` // 不含 ext_ 前缀 Email string `json:"email" binding:"required"` Supervisor string `json:"supervisor"` Purpose string `json:"purpose"` TTLDays int64 `json:"ttl_days"` // 0 表示用配置默认 } // Create 管理员创建外部用户(自动建系统账号)。 func (h *UserHandler) Create(c *gin.Context) { var req UserCreateRequest if err := c.ShouldBindJSON(&req); err != nil { fail(c, http.StatusBadRequest, "请求参数不合法: "+err.Error()) return } createdBy := uint(0) if sess := sessionFrom(c); sess != nil { createdBy = sess.RefID } u, err := h.svc.Create(c.Request.Context(), req.Username, req.Email, req.Supervisor, req.Purpose, time.Duration(req.TTLDays)*24*time.Hour, createdBy) if err != nil { h.h.audit(c, "user.create", "user", "", map[string]any{"username": req.Username, "err": err.Error()}, model.ResultFailed) switch { case errors.Is(err, service.ErrUserExists): fail(c, http.StatusConflict, err.Error()) default: fail(c, http.StatusBadRequest, err.Error()) } return } h.h.audit(c, "user.create", "user", strconv.FormatUint(uint64(u.ID), 10), map[string]any{"username": u.Username}, model.ResultSuccess) ok(c, gin.H{"id": u.ID, "username": u.Username, "status": u.Status, "expire_at": u.ExpireAt}) } // List 用户列表(分页/筛选:status、supervisor)。 func (h *UserHandler) List(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) users, total, err := h.svc.List(c.Request.Context(), service.UserFilter{ Status: c.Query("status"), Supervisor: c.Query("supervisor"), Page: page, PageSize: pageSize, }) if err != nil { fail(c, http.StatusInternalServerError, err.Error()) return } ok(c, gin.H{"total": total, "items": users}) } // Get 用户详情。 func (h *UserHandler) Get(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { fail(c, http.StatusBadRequest, "无效的用户 ID") return } u, err := h.svc.GetByID(c.Request.Context(), uint(id)) if err != nil { fail(c, http.StatusNotFound, err.Error()) return } ok(c, u) } // UserUpdateRequest 更新外部用户信息(仅更新提供的字段;邮箱仅管理员可改)。 type UserUpdateRequest struct { Email *string `json:"email"` Supervisor *string `json:"supervisor"` Purpose *string `json:"purpose"` } // Update PATCH /users/:id。 func (h *UserHandler) Update(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { fail(c, http.StatusBadRequest, "无效的用户 ID") return } var req UserUpdateRequest if err := c.ShouldBindJSON(&req); err != nil { fail(c, http.StatusBadRequest, "请求参数不合法: "+err.Error()) return } u, err := h.svc.Update(c.Request.Context(), uint(id), req.Email, req.Supervisor, req.Purpose) if err != nil { h.h.audit(c, "user.update", "user", c.Param("id"), map[string]any{"err": err.Error()}, model.ResultFailed) if errors.Is(err, service.ErrUserNotFound) { fail(c, http.StatusNotFound, err.Error()) return } fail(c, http.StatusBadRequest, err.Error()) return } h.h.audit(c, "user.update", "user", c.Param("id"), map[string]any{"email": req.Email, "supervisor": req.Supervisor, "purpose": req.Purpose}, model.ResultSuccess) ok(c, u) } // Disable POST /users/:id/disable —— 禁用(清空 authorized_keys,SSH 立即失效)。 func (h *UserHandler) Disable(c *gin.Context) { h.setStatus(c, "user.disable", model.UserStatusDisabled, h.svc.Disable) } // Enable POST /users/:id/enable —— 启用(按 DB 密钥状态恢复)。 func (h *UserHandler) Enable(c *gin.Context) { h.setStatus(c, "user.enable", model.UserStatusActive, h.svc.Enable) } // setStatus 复用禁用/启用的公共流程(解析 ID、调 service、审计)。 func (h *UserHandler) setStatus(c *gin.Context, action, wantStatus string, fn func(ctx context.Context, id uint) error) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { fail(c, http.StatusBadRequest, "无效的用户 ID") return } if err := fn(c, uint(id)); err != nil { h.h.audit(c, action, "user", c.Param("id"), map[string]any{"err": err.Error()}, model.ResultFailed) switch { case errors.Is(err, service.ErrUserNotFound): fail(c, http.StatusNotFound, err.Error()) case errors.Is(err, service.ErrUserExpired): fail(c, http.StatusConflict, err.Error()) case errors.Is(err, service.ErrSystemAccountMissing): fail(c, http.StatusConflict, err.Error()) default: fail(c, http.StatusInternalServerError, err.Error()) } return } h.h.audit(c, action, "user", c.Param("id"), map[string]any{"status": wantStatus}, model.ResultSuccess) ok(c, gin.H{"status": wantStatus}) } // ExtendRequest 延期请求。 type ExtendRequest struct { Days int `json:"days"` // 0 表示用配置默认(90 天) } // Extend POST /users/:id/extend —— 延长有效期;已过期用户在回收期内可恢复。 func (h *UserHandler) Extend(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { fail(c, http.StatusBadRequest, "无效的用户 ID") return } var req ExtendRequest if err := c.ShouldBindJSON(&req); err != nil { fail(c, http.StatusBadRequest, "请求参数不合法: "+err.Error()) return } u, err := h.svc.GetByID(c.Request.Context(), uint(id)) if err != nil { fail(c, http.StatusNotFound, err.Error()) return } newExpire, err := h.svc.Extend(c.Request.Context(), uint(id), req.Days) if err != nil { h.h.audit(c, "user.extend", "user", c.Param("id"), map[string]any{"days": req.Days, "err": err.Error()}, model.ResultFailed) fail(c, http.StatusInternalServerError, err.Error()) return } h.h.audit(c, "user.extend", "user", c.Param("id"), map[string]any{"days": req.Days, "old_status": u.Status}, model.ResultSuccess) ok(c, gin.H{"expire_at": newExpire.UTC()}) } // Delete DELETE /users/:id —— 删除并回收(系统账号 + 家目录 + 密钥,保留审计)。 func (h *UserHandler) Delete(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { fail(c, http.StatusBadRequest, "无效的用户 ID") return } if err := h.svc.Delete(c.Request.Context(), uint(id)); err != nil { h.h.audit(c, "user.delete", "user", c.Param("id"), map[string]any{"err": err.Error()}, model.ResultFailed) switch { case errors.Is(err, service.ErrUserNotFound): fail(c, http.StatusNotFound, err.Error()) default: fail(c, http.StatusInternalServerError, err.Error()) } return } h.h.audit(c, "user.delete", "user", c.Param("id"), nil, model.ResultSuccess) ok(c, gin.H{"status": "deleted"}) }