feat: add support for request ID tracking across services

- Implemented request ID handling in various services and handlers to improve traceability of requests.
- Added new AuthOptions endpoint to expose WxWork and OIDC configuration options.
- Updated message and event logging to include request ID for better debugging.
- Enhanced login form to dynamically show available authentication options based on server configuration.
- Introduced utility functions for normalizing and ensuring valid request IDs.
- Updated tests to verify request ID functionality in message sending and event logging.
This commit is contained in:
mlogclub
2026-05-27 22:18:43 +08:00
parent 498932de61
commit c246b85a9e
30 changed files with 504 additions and 59 deletions
+1
View File
@@ -11,6 +11,7 @@ import (
func registerApiAuthRoutes(group *gin.RouterGroup) {
group.POST("/login", api.Login)
group.POST("/logout", api.Logout)
group.GET("/options", api.AuthOptions)
group.GET("/profile", api.Profile)
group.GET("/wxwork_callback", api.WxWorkCallback)
group.POST("/wxwork_exchange", api.WxWorkExchange)
+15
View File
@@ -13,6 +13,7 @@ import (
"cs-agent/internal/pkg/ginx"
"cs-agent/internal/pkg/httpx"
"cs-agent/internal/pkg/i18nx"
"cs-agent/internal/pkg/tracex"
"cs-agent/internal/services"
webspa "cs-agent/web"
@@ -29,6 +30,7 @@ func NewServer() (*gin.Engine, error) {
printBanner()
app := gin.New()
app.Use(requestIDMiddleware())
app.Use(corsMiddleware())
app.Use(gin.Recovery())
app.Use(requestLogMiddleware())
@@ -103,14 +105,27 @@ func corsMiddleware() gin.HandlerFunc {
}
}
func requestIDMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
requestID := tracex.EnsureRequestID(ctx.GetHeader(tracex.RequestIDHeader))
ctx.Set(tracex.GinRequestIDKey, requestID)
if requestID != "" {
ctx.Header(tracex.RequestIDHeader, requestID)
}
ctx.Next()
}
}
func requestLogMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
start := time.Now()
path := ctx.Request.URL.Path
method := ctx.Request.Method
requestID, _ := ctx.Get(tracex.GinRequestIDKey)
ctx.Next()
slog.Info("http request",
"requestId", requestID,
"method", method,
"path", path,
"status", ctx.Writer.Status(),
+102
View File
@@ -1,6 +1,7 @@
package bootstrap
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
@@ -49,6 +50,59 @@ func TestNewServerRegistersGinRoutes(t *testing.T) {
}
}
func TestNewServerExposesPublicAuthOptions(t *testing.T) {
config.SetCurrent(&config.Config{
Storage: config.StorageConfig{
Local: config.LocalStorageConfig{
Root: "storage",
BaseURL: "/storage",
},
},
WxWork: config.WxWorkConfig{
Enabled: true,
},
OIDC: config.OIDCConfig{
Enabled: false,
ClientSecret: "must-not-leak",
},
})
app, err := NewServer()
if err != nil {
t.Fatalf("NewServer() error = %v", err)
}
rec := httptest.NewRecorder()
app.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/auth/options", nil))
if rec.Code != http.StatusOK {
t.Fatalf("status=%d want %d", rec.Code, http.StatusOK)
}
var body struct {
Success bool `json:"success"`
Data struct {
WxWorkEnabled bool `json:"wxworkEnabled"`
OIDCEnabled bool `json:"oidcEnabled"`
} `json:"data"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("unmarshal response: %v", err)
}
if !body.Success {
t.Fatalf("success=false, body=%s", rec.Body.String())
}
if !body.Data.WxWorkEnabled {
t.Fatalf("wxworkEnabled=false want true")
}
if body.Data.OIDCEnabled {
t.Fatalf("oidcEnabled=true want false")
}
if strings.Contains(rec.Body.String(), "must-not-leak") {
t.Fatalf("response leaked sensitive OIDC config: %s", rec.Body.String())
}
}
func TestNewServerSeparatesAPIStaticAndSPA(t *testing.T) {
config.SetCurrent(&config.Config{
Storage: config.StorageConfig{
@@ -159,3 +213,51 @@ func TestNewServerRejectsUnconfiguredCORSOrigin(t *testing.T) {
t.Fatalf("Access-Control-Allow-Origin=%q want empty", got)
}
}
func TestNewServerEchoesRequestID(t *testing.T) {
config.SetCurrent(&config.Config{
Storage: config.StorageConfig{
Local: config.LocalStorageConfig{
Root: "storage",
BaseURL: "/storage",
},
},
})
app, err := NewServer()
if err != nil {
t.Fatalf("NewServer() error = %v", err)
}
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/not-exists", nil)
req.Header.Set("X-Request-Id", "trace-123")
app.ServeHTTP(rec, req)
if got := rec.Header().Get("X-Request-Id"); got != "trace-123" {
t.Fatalf("X-Request-Id=%q want %q", got, "trace-123")
}
}
func TestNewServerGeneratesRequestID(t *testing.T) {
config.SetCurrent(&config.Config{
Storage: config.StorageConfig{
Local: config.LocalStorageConfig{
Root: "storage",
BaseURL: "/storage",
},
},
})
app, err := NewServer()
if err != nil {
t.Fatalf("NewServer() error = %v", err)
}
rec := httptest.NewRecorder()
app.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/not-exists", nil))
if got := rec.Header().Get("X-Request-Id"); got == "" {
t.Fatalf("X-Request-Id should be generated")
}
}