Files
usernode/internal/router/router.go
T

118 lines
3.7 KiB
Go
Raw 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 router 负责路由注册与中间件装配。
package router
import (
"log/slog"
"time"
"github.com/gin-gonic/gin"
"ws_usernode/internal/api"
"ws_usernode/internal/auth"
"ws_usernode/internal/config"
"ws_usernode/internal/webui"
)
// New 构建根 routerAPI v1 + 前端静态资源(go:embed)。
// production 模式启用 gin.ReleaseMode;否则启用调试模式与开发日志。
func New(cfg *config.Config, h *api.Handler, sessions auth.SessionStore, log *slog.Logger) *gin.Engine {
if cfg.App.Env == "production" {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
r.Use(gin.Recovery(), requestLogger(log))
// 健康检查(不进 /api/v1 前缀,便于负载均衡/探针)
r.GET("/healthz", h.Health.Healthz)
// RESTful API v1
v1 := r.Group("/api/v1")
{
authGrp := v1.Group("/auth")
{
authGrp.GET("/captcha", h.Auth.Captcha)
authGrp.POST("/otp/send", h.Auth.OTPSend)
authGrp.POST("/otp/login", h.Auth.OTPLogin)
authGrp.POST("/admin/login", h.Auth.AdminLogin)
authGrp.POST("/admin/forgot", h.Auth.AdminForgot)
authGrp.POST("/admin/reset", h.Auth.AdminReset)
// 需要会话(管理员或外部用户)
authed := authGrp.Group("", sessionMiddleware(sessions))
authed.POST("/logout", h.Auth.Logout)
authed.GET("/me", h.Auth.Me)
}
// 用户管理(admin
users := v1.Group("/users", sessionMiddleware(sessions), requireUserType(auth.SessionUserAdmin))
{
users.GET("", h.User.List)
users.POST("", h.User.Create)
users.GET("/:id", h.User.Get)
users.PATCH("/:id", h.User.Update)
users.POST("/:id/disable", h.User.Disable)
users.POST("/:id/enable", h.User.Enable)
users.POST("/:id/extend", h.User.Extend)
users.DELETE("/:id", h.User.Delete)
users.GET("/:id/keys", h.Key.ListForUser)
}
// 我的密钥(外部用户,自助管理;仅用户上传,管理员不代签)
me := v1.Group("/me", sessionMiddleware(sessions), requireUserType(auth.SessionUserUser))
{
me.GET("/keys", h.Key.ListMine)
me.POST("/keys", h.Key.Create)
me.PATCH("/keys/:id", h.Key.Rename)
me.DELETE("/keys/:id", h.Key.Revoke)
me.GET("/audit", h.Audit.MyAudit) // 我的审计痕迹(个人中心)
}
// 申请审批(M3):提交公开;列表与审批需管理员会话
v1.POST("/approvals", h.Approval.Submit)
approvals := v1.Group("/approvals", sessionMiddleware(sessions), requireUserType(auth.SessionUserAdmin))
{
approvals.GET("", h.Approval.List)
approvals.POST("/:id/review", h.Approval.Review)
}
// 审计(M4):查询 + 手动 CSV 导出(admin
auditGrp := v1.Group("/audit", sessionMiddleware(sessions), requireUserType(auth.SessionUserAdmin))
{
auditGrp.GET("", h.Audit.List)
auditGrp.GET("/export", h.Audit.Export)
}
// 系统设置(M4):读写(admin)
settings := v1.Group("/settings", sessionMiddleware(sessions), requireUserType(auth.SessionUserAdmin))
{
settings.GET("", h.Settings.List)
settings.PUT("", h.Settings.Update)
}
// 仪表盘统计(admin):用户状态分布 / 近期待过期 / 待审批数
adminGrp := v1.Group("/admin", sessionMiddleware(sessions), requireUserType(auth.SessionUserAdmin))
{
adminGrp.GET("/stats", h.Stats.Stats)
}
}
// 前端静态资源(go:embeddev 阶段由 Vite dev server 代理,见 Makefile dev
r.NoRoute(gin.WrapH(webui.NewHandler()))
return r
}
// requestLogger 以 slog 输出结构化请求日志。
func requestLogger(log *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
log.Info("http",
"method", c.Request.Method,
"path", c.Request.URL.Path,
"status", c.Writer.Status(),
"ip", c.ClientIP(),
"latency_ms", time.Since(start).Milliseconds(),
)
}
}