package httpapi import ( "net/http" "os" "strings" ) // NewCORS 返回一个 CORS 中间件:为白名单 Origin(精确匹配)补 CORS 响应头,并直接 // 应答 OPTIONS 预检。Web 用户中心(app.yanmeiai.com,浏览器)跨域调用 /v1/*,浏览器 // 会先发预检、并校验 Access-Control-Allow-Origin;原生移动/桌面客户端不是浏览器、 // 不受 CORS 约束,故此前无需 CORS。 // // Origin 白名单来自 CORS_ORIGINS(逗号分隔),缺省含 usercenter 的正式域与 pages.dev。 // 认证走 Authorization: Bearer(非 cookie),所以不需要 Allow-Credentials。 func NewCORS() func(http.Handler) http.Handler { raw := os.Getenv("CORS_ORIGINS") if raw == "" { // 用户中心已迁到主站子路径 pangolin.yanmeiai.com/user/,其 origin 即主站域名 // (旧 app.yanmeiai.com 已停用)。pages.dev 为 CF Pages 直连兜底。 raw = "https://pangolin.yanmeiai.com,https://pangolin-site.pages.dev" } allowed := map[string]bool{} for _, o := range strings.Split(raw, ",") { o = strings.TrimSpace(o) if o != "" { allowed[o] = true } } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") if origin != "" && allowed[origin] { h := w.Header() h.Set("Access-Control-Allow-Origin", origin) h.Add("Vary", "Origin") h.Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") // X-Refresh-Token:静默续期/登出把 refresh token 放在此自定义头里 // (header token 方案,见 web/usercenter/lib/api/http.ts)。不放行则浏览器 // 预检拦截 /v1/auth/refresh → 登录后一刷新即掉线(login 不带此头故不受影响)。 h.Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Refresh-Token") h.Set("Access-Control-Max-Age", "600") } // 预检请求直接 204(不落到业务路由,避免 /v1/... 的 OPTIONS 404)。 if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } }