refactor(auth): replace dashboard sessions with stateless tokens and session control (#6329)
* refactor(auth): replace dashboard sessions with stateless tokens * feat(auth): harden session issuance and distributed enforcement * fix(proxy): preserve trusted proxy compatibility defaults * refactor: address dashboard auth review feedback * refactor: remove classic frontend and flatten web app
This commit is contained in:
+29
-28
@@ -43,15 +43,16 @@ func SetApiRouter(router *gin.Engine) {
|
||||
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
||||
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, controller.ResetPassword)
|
||||
// OAuth routes - specific routes must come before :provider wildcard
|
||||
apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode)
|
||||
apiRouter.POST("/oauth/email/bind", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, controller.EmailBind)
|
||||
apiRouter.POST("/oauth/state", middleware.CriticalRateLimit(), middleware.DisableCache(), middleware.TryUserAuth(), anonymousRequestBodyLimit, controller.GenerateOAuthCode)
|
||||
apiRouter.POST("/oauth/email/bind", middleware.UserAuth(), middleware.CriticalRateLimit(), controller.EmailBind)
|
||||
// Non-standard OAuth (WeChat, Telegram) - keep original routes
|
||||
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
||||
apiRouter.POST("/oauth/wechat/bind", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, controller.WeChatBind)
|
||||
apiRouter.GET("/oauth/telegram/login", middleware.CriticalRateLimit(), controller.TelegramLogin)
|
||||
apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), controller.TelegramBind)
|
||||
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), middleware.DisableCache(), controller.WeChatAuth)
|
||||
apiRouter.POST("/oauth/wechat/bind", middleware.UserAuth(), middleware.CriticalRateLimit(), controller.WeChatBind)
|
||||
apiRouter.GET("/oauth/telegram/login", middleware.CriticalRateLimit(), middleware.DisableCache(), controller.TelegramLogin)
|
||||
apiRouter.POST("/oauth/telegram/bind/start", middleware.UserAuth(), middleware.CriticalRateLimit(), middleware.DisableCache(), controller.TelegramBindStart)
|
||||
apiRouter.GET("/oauth/telegram/bind/:flow_token", middleware.CriticalRateLimit(), middleware.DisableCache(), controller.TelegramBind)
|
||||
// Standard OAuth providers (GitHub, Discord, OIDC, LinuxDO) - unified route
|
||||
apiRouter.GET("/oauth/:provider", middleware.CriticalRateLimit(), controller.HandleOAuth)
|
||||
apiRouter.GET("/oauth/:provider", middleware.CriticalRateLimit(), middleware.DisableCache(), middleware.TryUserAuth(), controller.HandleOAuth)
|
||||
apiRouter.GET("/ratio_config", middleware.CriticalRateLimit(), controller.GetRatioConfig)
|
||||
|
||||
apiRouter.POST("/stripe/webhook", anonymousRequestBodyLimit, controller.StripeWebhook)
|
||||
@@ -62,17 +63,18 @@ func SetApiRouter(router *gin.Engine) {
|
||||
apiRouter.POST("/waffo-pancake/webhook/:env", anonymousRequestBodyLimit, controller.WaffoPancakeWebhook)
|
||||
|
||||
// Universal secure verification routes
|
||||
apiRouter.POST("/verify", middleware.UserAuth(), middleware.CriticalRateLimit(), controller.UniversalVerify)
|
||||
apiRouter.POST("/verify", middleware.UserAuth(), middleware.CriticalRateLimit(), middleware.DisableCache(), controller.UniversalVerify)
|
||||
|
||||
userRoute := apiRouter.Group("/user")
|
||||
{
|
||||
userRoute.POST("/auth/refresh", middleware.SessionCookieOriginGuard(), middleware.CriticalRateLimit(), middleware.DisableCache(), controller.RefreshAuth)
|
||||
userRoute.POST("/auth/logout", middleware.SessionCookieOriginGuard(), middleware.CriticalRateLimit(), middleware.DisableCache(), controller.AuthLogout)
|
||||
userRoute.POST("/register", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, middleware.TurnstileCheck(), controller.Register)
|
||||
userRoute.POST("/login", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, middleware.TurnstileCheck(), controller.Login)
|
||||
userRoute.POST("/login/2fa", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, controller.Verify2FALogin)
|
||||
userRoute.POST("/passkey/login/begin", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, controller.PasskeyLoginBegin)
|
||||
userRoute.POST("/passkey/login/finish", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, controller.PasskeyLoginFinish)
|
||||
userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.DisableCache(), anonymousRequestBodyLimit, middleware.TurnstileCheck(), controller.Login)
|
||||
userRoute.POST("/login/2fa", middleware.CriticalRateLimit(), middleware.DisableCache(), anonymousRequestBodyLimit, controller.Verify2FALogin)
|
||||
userRoute.POST("/passkey/login/begin", middleware.CriticalRateLimit(), middleware.DisableCache(), anonymousRequestBodyLimit, controller.PasskeyLoginBegin)
|
||||
userRoute.POST("/passkey/login/finish", middleware.CriticalRateLimit(), middleware.DisableCache(), anonymousRequestBodyLimit, controller.PasskeyLoginFinish)
|
||||
//userRoute.POST("/tokenlog", middleware.CriticalRateLimit(), controller.TokenLog)
|
||||
userRoute.GET("/logout", controller.Logout)
|
||||
userRoute.POST("/epay/notify", anonymousRequestBodyLimit, controller.EpayNotify)
|
||||
userRoute.GET("/epay/notify", controller.EpayNotify)
|
||||
userRoute.GET("/groups", controller.GetUserGroups)
|
||||
@@ -80,18 +82,21 @@ func SetApiRouter(router *gin.Engine) {
|
||||
selfRoute := userRoute.Group("/")
|
||||
selfRoute.Use(middleware.UserAuth())
|
||||
{
|
||||
selfRoute.GET("/sessions", middleware.DisableCache(), controller.GetLoginSessions)
|
||||
selfRoute.DELETE("/sessions/:sid", middleware.DisableCache(), controller.DeleteLoginSession)
|
||||
selfRoute.POST("/sessions/revoke-others", middleware.DisableCache(), controller.RevokeOtherLoginSessions)
|
||||
selfRoute.GET("/self/groups", controller.GetUserGroups)
|
||||
selfRoute.GET("/self", controller.GetSelf)
|
||||
selfRoute.GET("/models", controller.GetUserModels)
|
||||
selfRoute.PUT("/self", middleware.CriticalRateLimit(), controller.UpdateSelf)
|
||||
selfRoute.PUT("/self", middleware.CriticalRateLimit(), middleware.DisableCache(), controller.UpdateSelf)
|
||||
selfRoute.DELETE("/self", controller.DeleteSelf)
|
||||
selfRoute.GET("/token", controller.GenerateAccessToken)
|
||||
selfRoute.GET("/token", middleware.DisableCache(), controller.GenerateAccessToken)
|
||||
selfRoute.GET("/passkey", controller.PasskeyStatus)
|
||||
selfRoute.POST("/passkey/register/begin", controller.PasskeyRegisterBegin)
|
||||
selfRoute.POST("/passkey/register/finish", controller.PasskeyRegisterFinish)
|
||||
selfRoute.POST("/passkey/verify/begin", controller.PasskeyVerifyBegin)
|
||||
selfRoute.POST("/passkey/verify/finish", controller.PasskeyVerifyFinish)
|
||||
selfRoute.DELETE("/passkey", controller.PasskeyDelete)
|
||||
selfRoute.POST("/passkey/register/begin", middleware.DisableCache(), controller.PasskeyRegisterBegin)
|
||||
selfRoute.POST("/passkey/register/finish", middleware.DisableCache(), controller.PasskeyRegisterFinish)
|
||||
selfRoute.POST("/passkey/verify/begin", middleware.DisableCache(), controller.PasskeyVerifyBegin)
|
||||
selfRoute.POST("/passkey/verify/finish", middleware.DisableCache(), controller.PasskeyVerifyFinish)
|
||||
selfRoute.DELETE("/passkey", middleware.DisableCache(), controller.PasskeyDelete)
|
||||
selfRoute.GET("/aff", controller.GetAffCode)
|
||||
selfRoute.GET("/topup/info", controller.GetTopUpInfo)
|
||||
selfRoute.GET("/topup/self", controller.GetUserTopUps)
|
||||
@@ -110,10 +115,10 @@ func SetApiRouter(router *gin.Engine) {
|
||||
|
||||
// 2FA routes
|
||||
selfRoute.GET("/2fa/status", controller.Get2FAStatus)
|
||||
selfRoute.POST("/2fa/setup", controller.Setup2FA)
|
||||
selfRoute.POST("/2fa/enable", controller.Enable2FA)
|
||||
selfRoute.POST("/2fa/disable", controller.Disable2FA)
|
||||
selfRoute.POST("/2fa/backup_codes", controller.RegenerateBackupCodes)
|
||||
selfRoute.POST("/2fa/setup", middleware.DisableCache(), controller.Setup2FA)
|
||||
selfRoute.POST("/2fa/enable", middleware.DisableCache(), controller.Enable2FA)
|
||||
selfRoute.POST("/2fa/disable", middleware.DisableCache(), controller.Disable2FA)
|
||||
selfRoute.POST("/2fa/backup_codes", middleware.DisableCache(), controller.RegenerateBackupCodes)
|
||||
|
||||
// Check-in routes
|
||||
selfRoute.GET("/checkin", controller.GetCheckinStatus)
|
||||
@@ -192,7 +197,6 @@ func SetApiRouter(router *gin.Engine) {
|
||||
optionRoute.GET("/channel_affinity_cache", controller.GetChannelAffinityCacheStats)
|
||||
optionRoute.DELETE("/channel_affinity_cache", controller.ClearChannelAffinityCache)
|
||||
optionRoute.POST("/rest_model_ratio", controller.ResetModelRatio)
|
||||
optionRoute.POST("/migrate_console_setting", controller.MigrateConsoleSetting) // 用于迁移检测的旧键,下个版本会删除
|
||||
optionRoute.GET("/waffo-pancake/catalog", controller.ListWaffoPancakeCatalog)
|
||||
optionRoute.POST("/waffo-pancake/pair", controller.CreateWaffoPancakePair)
|
||||
optionRoute.POST("/waffo-pancake/save", controller.SaveWaffoPancake)
|
||||
@@ -266,9 +270,6 @@ func SetApiRouter(router *gin.Engine) {
|
||||
}
|
||||
logRoute := apiRouter.Group("/log")
|
||||
logRoute.GET("/", middleware.AdminAuth(), controller.GetAllLogs)
|
||||
// Legacy synchronous direct-delete route used only by the classic frontend.
|
||||
// TODO: remove once the classic frontend is removed; the default frontend uses /system-task/log-cleanup.
|
||||
logRoute.DELETE("/", middleware.RootAuth(), controller.DeleteHistoryLogs)
|
||||
logRoute.GET("/stat", middleware.AdminAuth(), controller.GetLogsStat)
|
||||
logRoute.GET("/self/stat", middleware.UserAuth(), controller.GetLogsSelfStat)
|
||||
logRoute.GET("/channel_affinity_usage_cache", middleware.AdminAuth(), controller.GetChannelAffinityUsageCacheStats)
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetRouter(router *gin.Engine, assets ThemeAssets) {
|
||||
func SetRouter(router *gin.Engine, assets WebAssets) {
|
||||
SetApiRouter(router)
|
||||
SetDashboardRouter(router)
|
||||
SetRelayRouter(router)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRetiredFrontendAPIRoutes(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
SetApiRouter(engine)
|
||||
|
||||
routes := make(map[string]struct{}, len(engine.Routes()))
|
||||
for _, route := range engine.Routes() {
|
||||
routes[route.Method+" "+route.Path] = struct{}{}
|
||||
}
|
||||
_, hasAsyncCleanup := routes[http.MethodPost+" /api/system-task/log-cleanup"]
|
||||
_, hasDirectDelete := routes[http.MethodDelete+" /api/log/"]
|
||||
_, hasConsoleMigration := routes[http.MethodPost+" /api/option/migrate_console_setting"]
|
||||
assert.True(t, hasAsyncCleanup)
|
||||
assert.False(t, hasDirectDelete)
|
||||
assert.False(t, hasConsoleMigration)
|
||||
}
|
||||
+8
-16
@@ -13,23 +13,19 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ThemeAssets holds the embedded frontend assets for both themes.
|
||||
type ThemeAssets struct {
|
||||
DefaultBuildFS embed.FS
|
||||
DefaultIndexPage []byte
|
||||
ClassicBuildFS embed.FS
|
||||
ClassicIndexPage []byte
|
||||
// WebAssets holds the embedded dashboard frontend assets.
|
||||
type WebAssets struct {
|
||||
BuildFS embed.FS
|
||||
IndexPage []byte
|
||||
}
|
||||
|
||||
func SetWebRouter(router *gin.Engine, assets ThemeAssets) {
|
||||
defaultFS := common.EmbedFolder(assets.DefaultBuildFS, "web/default/dist")
|
||||
classicFS := common.EmbedFolder(assets.ClassicBuildFS, "web/classic/dist")
|
||||
themeFS := common.NewThemeAwareFS(defaultFS, classicFS)
|
||||
func SetWebRouter(router *gin.Engine, assets WebAssets) {
|
||||
frontendFS := common.EmbedFolder(assets.BuildFS, "web/dist")
|
||||
|
||||
router.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||
router.Use(middleware.GlobalWebRateLimit())
|
||||
router.Use(middleware.Cache())
|
||||
router.Use(static.Serve("/", themeFS))
|
||||
router.Use(static.Serve("/", frontendFS))
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
c.Set(middleware.RouteTagKey, "web")
|
||||
if strings.HasPrefix(c.Request.RequestURI, "/v1") || strings.HasPrefix(c.Request.RequestURI, "/api") || strings.HasPrefix(c.Request.RequestURI, "/assets") {
|
||||
@@ -37,10 +33,6 @@ func SetWebRouter(router *gin.Engine, assets ThemeAssets) {
|
||||
return
|
||||
}
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
if common.GetTheme() == "classic" {
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", assets.ClassicIndexPage)
|
||||
} else {
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", assets.DefaultIndexPage)
|
||||
}
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", assets.IndexPage)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user