Files

212 lines
7.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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_keysSSH 立即失效)。
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"})
}