Files
usernode/internal/router/router.go
T

111 lines
3.4 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"
"net/http"
"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)
}
// 申请审批(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)
}
// 后续里程碑
v1.GET("/audit", notImplemented("审计查询(M4"))
v1.GET("/audit/export", notImplemented("审计导出(M4"))
v1.GET("/settings", notImplemented("设置(M4"))
v1.PUT("/settings", notImplemented("设置(M4"))
}
// 前端静态资源(go:embeddev 阶段由 Vite dev server 代理,见 Makefile dev
r.NoRoute(gin.WrapH(webui.NewHandler()))
return r
}
// notImplemented 返回 501 占位 handler,标注里程碑。
func notImplemented(what string) gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "接口 " + what + " 尚未实现"})
}
}
// 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(),
)
}
}