mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-31 00:24:21 +00:00
- Add i18next + react-i18next with namespace-split locale files (27 namespaces x 3 languages) - Add language switcher in topbar (EN/VI/ZH) with localStorage persistence - Replace hardcoded strings in 160+ React components with t() translations - Add Go message catalog (internal/i18n) with T(locale, key, args...) function - Replace 81 hardcoded error strings in gateway methods and HTTP handlers - Add locale context propagation: WS connect param + HTTP Accept-Language header - Keep technical terms in English: Agent, Session, Channel, Provider, Skill, Team, MCP, Cron - Update CLAUDE.md and review-pr skill with i18n compliance checks
88 lines
2.5 KiB
Go
88 lines
2.5 KiB
Go
package http
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// extractBearerToken extracts a bearer token from the Authorization header.
|
|
func extractBearerToken(r *http.Request) string {
|
|
auth := r.Header.Get("Authorization")
|
|
if auth == "" {
|
|
return ""
|
|
}
|
|
if !strings.HasPrefix(auth, "Bearer ") {
|
|
return ""
|
|
}
|
|
return strings.TrimPrefix(auth, "Bearer ")
|
|
}
|
|
|
|
// tokenMatch performs a constant-time comparison of a provided token against the expected token.
|
|
// Returns true if expected is empty (no auth configured) or if tokens match.
|
|
func tokenMatch(provided, expected string) bool {
|
|
if expected == "" {
|
|
return true
|
|
}
|
|
return subtle.ConstantTimeCompare([]byte(provided), []byte(expected)) == 1
|
|
}
|
|
|
|
// extractUserID extracts the external user ID from the request header.
|
|
// Returns "" if no user ID is provided (anonymous).
|
|
// Rejects IDs exceeding MaxUserIDLength (VARCHAR(255) DB constraint).
|
|
func extractUserID(r *http.Request) string {
|
|
id := r.Header.Get("X-GoClaw-User-Id")
|
|
if id == "" {
|
|
return ""
|
|
}
|
|
if err := store.ValidateUserID(id); err != nil {
|
|
slog.Warn("security.user_id_too_long", "length", len(id), "max", store.MaxUserIDLength)
|
|
return ""
|
|
}
|
|
return id
|
|
}
|
|
|
|
// extractAgentID determines the target agent from the request.
|
|
// Checks model field, headers, and falls back to "default".
|
|
func extractAgentID(r *http.Request, model string) string {
|
|
// From model field: "goclaw:<agentId>" or "agent:<agentId>"
|
|
if strings.HasPrefix(model, "goclaw:") {
|
|
return strings.TrimPrefix(model, "goclaw:")
|
|
}
|
|
if strings.HasPrefix(model, "agent:") {
|
|
return strings.TrimPrefix(model, "agent:")
|
|
}
|
|
|
|
// From headers
|
|
if id := r.Header.Get("X-GoClaw-Agent-Id"); id != "" {
|
|
return id
|
|
}
|
|
if id := r.Header.Get("X-GoClaw-Agent"); id != "" {
|
|
return id
|
|
}
|
|
|
|
return "default"
|
|
}
|
|
|
|
// extractLocale parses the Accept-Language header and returns a supported locale.
|
|
// Falls back to "en" if no supported language is found.
|
|
func extractLocale(r *http.Request) string {
|
|
accept := r.Header.Get("Accept-Language")
|
|
if accept == "" {
|
|
return i18n.DefaultLocale
|
|
}
|
|
// Simple parser: take the first language tag before comma or semicolon
|
|
for _, part := range strings.Split(accept, ",") {
|
|
tag := strings.TrimSpace(strings.SplitN(part, ";", 2)[0])
|
|
locale := i18n.Normalize(tag)
|
|
if locale != i18n.DefaultLocale || strings.HasPrefix(tag, "en") {
|
|
return locale
|
|
}
|
|
}
|
|
return i18n.DefaultLocale
|
|
}
|