This commit is contained in:
ryan
2026-05-13 10:53:14 +08:00
parent 856e3f46d2
commit e85df49962
31 changed files with 2504 additions and 645 deletions
+12
View File
@@ -21,6 +21,9 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
apiRouter.GET("/oauth/:source/authorize", middleware.CriticalRateLimit(), controller.OAuthAuthorize)
apiRouter.GET("/oauth/:source/callback", middleware.CriticalRateLimit(), controller.OAuthCallback)
apiRouter.POST("/oauth/link-existing", middleware.CriticalRateLimit(), controller.LinkExistingOAuthAccount)
userRoute := apiRouter.Group("/user")
{
@@ -58,6 +61,15 @@ func SetApiRouter(router *gin.Engine) {
optionRoute.POST("/geoip/lookup", controller.LookupGeoIP)
optionRoute.POST("/database/cleanup", controller.CleanupDatabaseObservability)
}
authSourceRoute := apiRouter.Group("/auth-sources")
authSourceRoute.Use(middleware.RootAuth(), middleware.NoTokenAuth())
{
authSourceRoute.GET("/", controller.ListAuthSources)
authSourceRoute.POST("/", controller.CreateAuthSource)
authSourceRoute.POST("/:id/update", controller.UpdateAuthSource)
authSourceRoute.POST("/:id/delete", controller.DeleteAuthSource)
authSourceRoute.POST("/:id/toggle", controller.ToggleAuthSource)
}
updateRoute := apiRouter.Group("/update")
updateRoute.Use(middleware.RootAuth(), middleware.NoTokenAuth())
{
@@ -201,6 +201,74 @@ func TestPhase2BatchOptionUpdateValidatesMergedState(t *testing.T) {
}
}
func TestAuthSourceUpdateAcceptsClientSecret(t *testing.T) {
gin.SetMode(gin.TestMode)
common.RedisEnabled = false
setupTestDB(t)
engine := gin.New()
engine.Use(sessions.Sessions("session", cookie.NewStore([]byte("test-secret"))))
router.SetApiRouter(engine)
loginCookie := loginAsRoot(t, engine)
createResp := performSessionJSONRequest(t, engine, loginCookie, http.MethodPost, "/api/auth-sources/", map[string]any{
"name": "GitHub",
"type": "github",
"display_name": "GitHub",
"is_active": false,
"client_id": "github-client-id",
"client_secret": "initial-secret",
"scopes": "user:email",
})
var created model.AuthSource
decodeResponseData(t, createResp, &created)
if created.ClientSecret != "" {
t.Fatal("expected create response to avoid exposing client_secret")
}
if !created.ClientSecretConfigured {
t.Fatal("expected create response to mark client_secret as configured")
}
updateResp := performSessionJSONRequest(t, engine, loginCookie, http.MethodPost, "/api/auth-sources/1/update", map[string]any{
"name": "GitHub",
"type": "github",
"display_name": "GitHub",
"is_active": true,
"client_id": "github-client-id",
"client_secret": "updated-secret",
"scopes": "user:email",
})
var updated model.AuthSource
decodeResponseData(t, updateResp, &updated)
if updated.ClientSecret != "" {
t.Fatal("expected update response to avoid exposing client_secret")
}
if !updated.ClientSecretConfigured {
t.Fatal("expected update response to mark client_secret as configured")
}
if !updated.IsActive {
t.Fatal("expected auth source to be active after update")
}
stored, err := model.GetAuthSourceByID(1)
if err != nil {
t.Fatalf("expected auth source to exist: %v", err)
}
if stored.ClientSecret != "updated-secret" {
t.Fatalf("expected stored client secret to be updated, got %q", stored.ClientSecret)
}
performSessionJSONRequest(t, engine, loginCookie, http.MethodPost, "/api/auth-sources/1/toggle", map[string]any{
"is_active": false,
})
performSessionJSONRequest(t, engine, loginCookie, http.MethodPost, "/api/auth-sources/1/toggle", map[string]any{
"is_active": true,
})
}
func loginAsRoot(t *testing.T, engine http.Handler) *http.Cookie {
t.Helper()
payload, err := json.Marshal(map[string]any{
+16
View File
@@ -41,6 +41,9 @@ func setWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
func serveExportedPage(c *gin.Context, buildFS fs.FS) bool {
requestPath := strings.Trim(c.Request.URL.Path, "/")
if isOAuthCallbackPath(requestPath) {
requestPath = "oauth/callback"
}
candidates := []string{"index.html"}
if requestPath != "" {
@@ -61,6 +64,19 @@ func serveExportedPage(c *gin.Context, buildFS fs.FS) bool {
return false
}
func isOAuthCallbackPath(requestPath string) bool {
if !strings.HasPrefix(requestPath, "oauth/") || strings.Count(requestPath, "/") != 1 {
return false
}
source := strings.TrimPrefix(requestPath, "oauth/")
switch source {
case "", "callback", "link":
return false
default:
return true
}
}
func normalizeStaticExportDataNavigation() gin.HandlerFunc {
return func(c *gin.Context) {
requestPath := c.Request.URL.Path
@@ -93,3 +93,20 @@ func TestCacheHeadersKeepImmutableStaticAssets(t *testing.T) {
t.Fatalf("unexpected cache-control for static asset: %q", got)
}
}
func TestOAuthCallbackPathMatchesSourceNames(t *testing.T) {
cases := map[string]bool{
"oauth/github": true,
"oauth/oidc-main": true,
"oauth/1": true,
"oauth/callback": false,
"oauth/link": false,
"oauth": false,
}
for requestPath, expected := range cases {
if got := isOAuthCallbackPath(requestPath); got != expected {
t.Fatalf("expected %s match=%v, got %v", requestPath, expected, got)
}
}
}