Files
goclaw/internal/http/auth.go
T
viettranx 344e2ac7d1 feat(i18n): add full i18n support for backend and web UI
- 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
2026-03-09 22:22:42 +07:00

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
}