fix: code compliance audit and fix across all services
NestJS (6 services): implement @RequirePermission decorator with SetMetadata+Reflector, register APP_GUARD globally, fix as assertions to type guards, add explicit return types, fix import type for express, fix /metrics implicit any, replace native Error with ApplicationError, remove typeorm remnants, register LifecycleService. teacher-bff: add logger, ApplicationError, GlobalErrorFilter, forward real userId to downstream, log downstream failures, migrate health controller to shared/health. Go (2 services): interface to any, doc comments, CORS dev whitelist, JWT secret fail-fast, push-gateway internal API auth, metrics and readyz endpoints, remove dead code. Python (2 services): lifespan return type, dev_mode to bool, data-ana APIRouter, ai POST body model, ClickHouse async wrapping.
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Config 持有 api-gateway 运行时配置
|
||||
type Config struct {
|
||||
Port string
|
||||
JWTSecret string
|
||||
@@ -23,10 +25,26 @@ type Config struct {
|
||||
DevMode bool
|
||||
}
|
||||
|
||||
// devJWTSecret 是 DevMode 下的默认 JWT 密钥(仅用于本地联调,生产必须配置 JWT_SECRET)
|
||||
const devJWTSecret = "p1-dev-secret-change-in-production"
|
||||
|
||||
// Load 从环境变量加载配置。
|
||||
// 非 DevMode 下若 JWT_SECRET 未配置则 fatal 退出;DevMode 下使用默认密钥并打印 warning。
|
||||
func Load() *Config {
|
||||
devMode := getEnvBool("DEV_MODE", false)
|
||||
jwtSecret := getEnv("JWT_SECRET", "")
|
||||
if jwtSecret == "" {
|
||||
if devMode {
|
||||
log.Println("warning: JWT_SECRET not set, using dev default (DEV_MODE=true)")
|
||||
jwtSecret = devJWTSecret
|
||||
} else {
|
||||
log.Fatal("JWT_SECRET must be set in non-dev mode")
|
||||
}
|
||||
}
|
||||
|
||||
return &Config{
|
||||
Port: getEnv("API_GATEWAY_PORT", "8080"),
|
||||
JWTSecret: getEnv("JWT_SECRET", "p1-dev-secret-change-in-production"),
|
||||
JWTSecret: jwtSecret,
|
||||
JWTIssuer: getEnv("JWT_ISSUER", "next-edu-cloud"),
|
||||
JWTAudience: getEnv("JWT_AUDIENCE", "next-edu-cloud"),
|
||||
ClassesServiceURL: getEnv("CLASSES_SERVICE_URL", "http://localhost:3001"),
|
||||
@@ -39,10 +57,11 @@ func Load() *Config {
|
||||
AiServiceURL: getEnv("AI_SERVICE_URL", "http://localhost:3008"),
|
||||
OTLPEndpoint: getEnv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318"),
|
||||
LogLevel: getEnv("LOG_LEVEL", "info"),
|
||||
DevMode: getEnvBool("DEV_MODE", false),
|
||||
DevMode: devMode,
|
||||
}
|
||||
}
|
||||
|
||||
// getEnv 读取环境变量,缺失时返回 fallback
|
||||
func getEnv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
@@ -50,15 +69,7 @@ func getEnv(key, fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
func getEnvInt(key string, fallback int) int {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// getEnvBool 读取环境变量并解析为 bool,缺失或解析失败时返回 fallback
|
||||
func getEnvBool(key string, fallback bool) bool {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if b, err := strconv.ParseBool(v); err == nil {
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/edu-cloud/api-gateway/internal/config"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// publicPaths 是无需鉴权的公开路径(精确匹配,基于去掉 /api/v1 前缀后的路径)
|
||||
@@ -73,7 +72,7 @@ func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
|
||||
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (any, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
}
|
||||
@@ -107,7 +106,7 @@ func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
|
||||
if sub, ok := claims["sub"].(string); ok {
|
||||
c.Request.Header.Set("x-user-id", sub)
|
||||
}
|
||||
if roles, ok := claims["roles"].([]interface{}); ok {
|
||||
if roles, ok := claims["roles"].([]any); ok {
|
||||
roleStrs := make([]string, 0, len(roles))
|
||||
for _, r := range roles {
|
||||
if s, ok := r.(string); ok {
|
||||
@@ -120,22 +119,3 @@ func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequestIDMiddleware 注入请求 ID 用于全链路追踪
|
||||
func RequestIDMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = generateUUID()
|
||||
}
|
||||
c.Set("request_id", requestID)
|
||||
c.Writer.Header().Set("X-Request-ID", requestID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// generateUUID 生成带 req- 前缀的唯一请求 ID
|
||||
// 使用 uuid.New() 基于 RFC 4122 v4 随机 UUID,避免 time.Now() 产生的冲突与可预测性
|
||||
func generateUUID() string {
|
||||
return "req-" + uuid.New().String()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -12,22 +13,26 @@ import (
|
||||
// corsMaxAge 预检缓存时长:12 小时
|
||||
const corsMaxAge = 12 * 60 * 60
|
||||
|
||||
// devCORSOrigins 是未配置 CORS_ORIGINS 时的开发环境默认白名单
|
||||
const devCORSOrigins = "http://localhost:3000,http://localhost:3001"
|
||||
|
||||
// CORS 返回跨域资源共享中间件。
|
||||
// 允许来源从环境变量 CORS_ORIGINS 读取(逗号分隔,默认 *)。
|
||||
// 允许来源从环境变量 CORS_ORIGINS 读取(逗号分隔);
|
||||
// 未配置时使用开发环境白名单(localhost:3000/3001)并打印 warning。
|
||||
// 允许方法:GET POST PUT DELETE OPTIONS PATCH
|
||||
// 允许头:Authorization Content-Type X-Request-Id X-Trace-Id
|
||||
// 暴露头:X-Request-Id X-Trace-Id
|
||||
func CORS() gin.HandlerFunc {
|
||||
allowed := parseCORSOrigins(os.Getenv("CORS_ORIGINS"))
|
||||
if len(allowed) == 0 {
|
||||
log.Println("warning: CORS_ORIGINS not set, using dev default whitelist")
|
||||
allowed = parseCORSOrigins(devCORSOrigins)
|
||||
}
|
||||
|
||||
return func(c *gin.Context) {
|
||||
origin := c.GetHeader("Origin")
|
||||
allowOrigin := ""
|
||||
|
||||
if len(allowed) == 0 {
|
||||
// 未配置则默认允许所有来源
|
||||
allowOrigin = "*"
|
||||
} else if allowed[origin] {
|
||||
if allowed[origin] {
|
||||
allowOrigin = origin
|
||||
}
|
||||
|
||||
@@ -39,9 +44,7 @@ func CORS() gin.HandlerFunc {
|
||||
h.Set("Access-Control-Expose-Headers", "X-Request-Id, X-Trace-Id")
|
||||
h.Set("Access-Control-Max-Age", strconv.Itoa(corsMaxAge))
|
||||
// 非通配来源需标注 Vary,便于缓存正确区分
|
||||
if allowOrigin != "*" {
|
||||
h.Add("Vary", "Origin")
|
||||
}
|
||||
h.Add("Vary", "Origin")
|
||||
}
|
||||
|
||||
// 预检请求直接返回 204
|
||||
@@ -53,7 +56,7 @@ func CORS() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// parseCORSOrigins 解析逗号分隔的来源列表为集合,空字符串返回空 map(表示通配 *)
|
||||
// parseCORSOrigins 解析逗号分隔的来源列表为集合,空字符串返回空 map
|
||||
func parseCORSOrigins(raw string) map[string]bool {
|
||||
allowed := map[string]bool{}
|
||||
if raw == "" {
|
||||
|
||||
Reference in New Issue
Block a user