Add project:manage_members, CSP frame-src, task-run member view, CreateUser tx error handling, ERR_NO_PROJECT chat gate, and creator-scoped project name/client_key uniqueness to stop duplicate creates and auth gaps. Co-authored-by: Cursor <cursoragent@cursor.com>
124 lines
2.9 KiB
Go
124 lines
2.9 KiB
Go
package httpx
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
CodeAUTHZ = "AUTHZ"
|
|
CodeVALIDATION = "VALIDATION"
|
|
CodeVERSION_CONFLICT = "VERSION_CONFLICT"
|
|
CodeIDEMPOTENCY_CONFLICT = "IDEMPOTENCY_CONFLICT"
|
|
CodeINTERNAL = "INTERNAL"
|
|
HeaderRequestID = "X-Request-ID"
|
|
)
|
|
|
|
type Envelope struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
RequestID string `json:"request_id"`
|
|
Error string `json:"error"`
|
|
Details map[string]any `json:"details,omitempty"`
|
|
}
|
|
|
|
func CodeFromStatus(status int) string {
|
|
switch status {
|
|
case http.StatusUnauthorized, http.StatusForbidden:
|
|
return CodeAUTHZ
|
|
case http.StatusConflict:
|
|
return CodeVERSION_CONFLICT
|
|
case http.StatusBadRequest, http.StatusUnprocessableEntity:
|
|
return CodeVALIDATION
|
|
default:
|
|
if status >= 500 {
|
|
return CodeINTERNAL
|
|
}
|
|
if status == http.StatusNotFound {
|
|
return CodeAUTHZ
|
|
}
|
|
return CodeVALIDATION
|
|
}
|
|
}
|
|
|
|
func SafeMessage(code string) string {
|
|
switch code {
|
|
case CodeAUTHZ:
|
|
return "无权访问"
|
|
case CodeVALIDATION:
|
|
return "请求参数无效"
|
|
case CodeVERSION_CONFLICT:
|
|
return "资源版本冲突"
|
|
case CodeIDEMPOTENCY_CONFLICT:
|
|
return "重复提交"
|
|
default:
|
|
return "请求无法完成"
|
|
}
|
|
}
|
|
|
|
func LooksInternal(s string) bool {
|
|
low := strings.ToLower(s)
|
|
needles := []string{"sql", "pq:", "pgx", "postgres", "stack", "panic", "token", "password", "secret", "api_key", "bearer ", "../", "prompt", "commit", "rollback", "transaction", "constraint", "violates"}
|
|
for _, n := range needles {
|
|
if strings.Contains(low, n) {
|
|
return true
|
|
}
|
|
}
|
|
if strings.Contains(s, "/home/") || strings.Contains(s, "/var/") || strings.Contains(s, "\\") {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func RequestID(r *http.Request) string {
|
|
if r == nil {
|
|
return newRequestID()
|
|
}
|
|
if v := strings.TrimSpace(r.Header.Get(HeaderRequestID)); v != "" {
|
|
return v
|
|
}
|
|
return newRequestID()
|
|
}
|
|
|
|
func newRequestID() string {
|
|
var b [8]byte
|
|
_, _ = rand.Read(b[:])
|
|
return "req_" + hex.EncodeToString(b[:])
|
|
}
|
|
|
|
func PublicLegacy(code, legacy string) string {
|
|
legacy = strings.TrimSpace(legacy)
|
|
if legacy == "" || LooksInternal(legacy) {
|
|
return code
|
|
}
|
|
if strings.HasPrefix(legacy, "<") || strings.Contains(legacy, "<script") {
|
|
return code
|
|
}
|
|
return legacy
|
|
}
|
|
|
|
func Write(w http.ResponseWriter, status int, code, legacy, requestID string, details map[string]any) {
|
|
if code == "" {
|
|
code = CodeFromStatus(status)
|
|
}
|
|
if requestID == "" {
|
|
requestID = newRequestID()
|
|
}
|
|
env := Envelope{
|
|
Code: code,
|
|
Message: SafeMessage(code),
|
|
RequestID: requestID,
|
|
Error: PublicLegacy(code, legacy),
|
|
Details: details,
|
|
}
|
|
if w.Header().Get("Content-Type") == "" {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
}
|
|
w.Header().Set(HeaderRequestID, requestID)
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(env)
|
|
}
|