diff --git a/backend/go.mod b/backend/go.mod index 4723903..4604a69 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,10 +3,12 @@ module github.com/denisovdennis/autohero go 1.23 require ( + github.com/TwiN/go-away v1.6.12 github.com/go-chi/chi/v5 v5.1.0 github.com/gorilla/websocket v1.5.3 github.com/jackc/pgx/v5 v5.7.1 github.com/redis/go-redis/v9 v9.7.0 + golang.org/x/text v0.18.0 ) require ( @@ -17,5 +19,4 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect golang.org/x/crypto v0.27.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/text v0.18.0 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 3ed0e15..889ae58 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,3 +1,5 @@ +github.com/TwiN/go-away v1.6.12 h1:80AjDyeTjfQaSFYbALzRcDKMAmxKW0a5PoxwXKZlW2A= +github.com/TwiN/go-away v1.6.12/go.mod h1:MpvIC9Li3minq+CGgbgUDvQ9tDaeW35k5IXZrF9MVas= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= diff --git a/backend/internal/handler/game.go b/backend/internal/handler/game.go index 7a172ee..6144142 100644 --- a/backend/internal/handler/game.go +++ b/backend/internal/handler/game.go @@ -11,12 +11,15 @@ import ( "strings" "sync" "time" + "unicode/utf8" "github.com/go-chi/chi/v5" + "golang.org/x/text/unicode/norm" "github.com/denisovdennis/autohero/internal/changelog" "github.com/denisovdennis/autohero/internal/game" "github.com/denisovdennis/autohero/internal/model" + "github.com/denisovdennis/autohero/internal/profanity" "github.com/denisovdennis/autohero/internal/storage" "github.com/denisovdennis/autohero/internal/tuning" "github.com/denisovdennis/autohero/internal/version" @@ -1035,13 +1038,14 @@ type heroNameRequest struct { Name string `json:"name"` } -// isValidHeroName checks that a name is 2-16 chars, only latin/cyrillic letters and digits, -// no leading/trailing spaces. +// isValidHeroName checks that a name is 2-16 Unicode characters, only latin/cyrillic letters and digits, +// no leading/trailing spaces. Expects trimmed, NFC-normalized input (see SetHeroName). func isValidHeroName(name string) bool { - if len(name) < 2 || len(name) > 16 { + n := utf8.RuneCountInString(name) + if n < 2 || n > 16 { return false } - if name[0] == ' ' || name[len(name)-1] == ' ' { + if strings.TrimSpace(name) != name { return false } for _, r := range name { @@ -1079,12 +1083,20 @@ func (h *GameHandler) SetHeroName(w http.ResponseWriter, r *http.Request) { return } + req.Name = norm.NFC.String(strings.TrimSpace(req.Name)) + if !isValidHeroName(req.Name) { writeJSON(w, http.StatusBadRequest, map[string]string{ "error": "invalid name: must be 2-16 characters, letters (latin/cyrillic) and digits only", }) return } + if profanity.HeroNameIsProfane(req.Name) { + writeJSON(w, http.StatusBadRequest, map[string]string{ + "error": "invalid name: inappropriate language", + }) + return + } hero, err := h.store.GetByTelegramID(r.Context(), telegramID) if err != nil { diff --git a/frontend/src/ui/NameEntryScreen.tsx b/frontend/src/ui/NameEntryScreen.tsx index b7ad766..3bff96d 100644 --- a/frontend/src/ui/NameEntryScreen.tsx +++ b/frontend/src/ui/NameEntryScreen.tsx @@ -125,7 +125,12 @@ export function NameEntryScreen({ onNameSet }: NameEntryScreenProps) { } else if (err.status === 400) { try { const j = JSON.parse(err.body) as { error?: string }; - setError(j.error ?? tr.invalidName); + const errMsg = j.error ?? ''; + if (errMsg === 'invalid name: inappropriate language') { + setError(tr.invalidName); + } else { + setError(errMsg || tr.invalidName); + } } catch { setError(tr.invalidName); }