Refine and Standardize Username Constraints (#4828)

* Implement Unix username standard validations on username creation and updating.

* Remove leading underscore permissibility | Replace hardcoded username rules with a centralized USERNAME_REQUIREMENTS_TEXT for better maintainability.

* Add username requirements translations for invite and admin user creation | Replace hardcoded username requirements with localized strings in user modals

* Refactor username requirements localization

* Remove unneeded comment | Move Regex comment to validator fn

* Remove username validation utility function to keep validation responsibilities on the server | Allow onboarding flow multi-user mode username creation step to send pre-validated credentials to server.

* Enhance error handling in system endpoints by returning a JSON response with error details instead of a plain status for internal server errors.

* Update username requirement localization in AccountModal and UserSetup components to use centralized translation key.

* test enforcements
allow users to keep existing usernames without collision

* Normalize Translations (#4861)

* normalize translations

* add translations

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
This commit is contained in:
Marcello Fitton
2026-01-26 16:18:11 -08:00
committed by GitHub
parent 2dc625193e
commit afa3073893
33 changed files with 207 additions and 121 deletions

View File

@@ -11,6 +11,11 @@ import { useTranslation } from "react-i18next";
import { useState, useEffect } from "react";
import { Tooltip } from "react-tooltip";
import { safeJsonParse } from "@/utils/request";
import {
USERNAME_MIN_LENGTH,
USERNAME_MAX_LENGTH,
USERNAME_PATTERN,
} from "@/utils/username";
export default function AccountModal({ user, hideModal }) {
const { pfp, setPfp } = usePfp();
@@ -143,13 +148,15 @@ export default function AccountModal({ user, hideModal }) {
type="text"
className="border-none bg-theme-settings-input-bg placeholder:text-theme-settings-input-placeholder border-gray-500 text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="User's username"
minLength={2}
minLength={USERNAME_MIN_LENGTH}
maxLength={USERNAME_MAX_LENGTH}
pattern={USERNAME_PATTERN}
defaultValue={user.username}
required
autoComplete="off"
/>
<p className="mt-2 text-xs text-white/60">
{t("profile_settings.username_description")}
{t("common.username_requirements")}
</p>
</div>
<div>

View File

@@ -21,8 +21,6 @@ const TRANSLATIONS = {
passwordReq: "يجب أن تحتوي كلمة المرور على ثمانية حروف على الأقل",
passwordWarn: "من المهم حفظ كلمة المرور هذه لأنه لا يمكن استردادها.",
adminUsername: "اسم مستعمل حساب المشرف",
adminUsernameReq:
"يجب أن يكون اسم المستعمل بطول 6 أحرف على الأقل وأن يحتوي فقط على أحرف صغيرة وأرقام وشرطات سفلية وواصلات بدون مسافات.",
adminPassword: "كلمة مرور حساب المشرف",
adminPasswordReq: "يجب أن تكون كلمات المرور 8 أحرف على الأقل.",
teamHint:
@@ -69,6 +67,8 @@ const TRANSLATIONS = {
yes: "نعم",
no: "لا",
search: null,
username_requirements:
"يجب أن يتكون اسم المستخدم من 2-32 حرفًا، وأن يبدأ بحرف صغير، وأن يحتوي فقط على أحرف صغيرة وأرقام وشرطات سفلية وشرطات ونقاط.",
},
settings: {
title: "إعدادات المثيل",
@@ -684,7 +684,6 @@ const TRANSLATIONS = {
profile_picture: null,
remove_profile_picture: null,
username: null,
username_description: null,
new_password: null,
password_description: null,
cancel: null,

View File

@@ -22,8 +22,6 @@ const TRANSLATIONS = {
passwordWarn:
"Je důležité toto heslo uložit, protože neexistuje způsob obnovení.",
adminUsername: "Uživatelské jméno správce",
adminUsernameReq:
"Uživatelské jméno musí mít alespoň 6 znaků a obsahovat pouze malá písmena, číslice, podtržítka a pomlčky bez mezer.",
adminPassword: "Heslo správce",
adminPasswordReq: "Hesla musí mít alespoň 8 znaků.",
teamHint:
@@ -71,6 +69,8 @@ const TRANSLATIONS = {
yes: "Ano",
no: "Ne",
search: "Hledat",
username_requirements:
"Uživatelské jméno musí mít 232 znaků, začínat malým písmenem a obsahovat pouze malá písmena, číslice, podtržítka, pomlčky a tečky.",
},
home: {
welcome: "Vítejte",
@@ -949,8 +949,6 @@ const TRANSLATIONS = {
profile_picture: "Profilový obrázek",
remove_profile_picture: "Odebrat profilový obrázek",
username: "Uživatelské jméno",
username_description:
"Uživatelské jméno musí obsahovat pouze malá písmena, číslice, podtržítka a pomlčky bez mezer",
new_password: "Nové heslo",
password_description: "Heslo musí mít délku alespoň 8 znaků",
cancel: "Zrušit",

View File

@@ -22,8 +22,6 @@ const TRANSLATIONS = {
passwordWarn:
"Det er vigtigt at gemme denne adgangskode, da der ikke findes nogen metode til genoprettelse.",
adminUsername: "Brugernavn til admin-konto",
adminUsernameReq:
"Brugernavnet skal være mindst 6 tegn langt og må kun indeholde små bogstaver, tal, understregninger og bindestreger uden mellemrum.",
adminPassword: "Adgangskode til admin-konto",
adminPasswordReq: "Adgangskoder skal være på mindst 8 tegn.",
teamHint:
@@ -71,6 +69,8 @@ const TRANSLATIONS = {
yes: "Ja",
no: "Nej",
search: null,
username_requirements:
"Brugernavnet skal være på 2-32 tegn, starte med et lille bogstav og kun indeholde små bogstaver, tal, understregninger, bindestreger og punktummer.",
},
settings: {
title: "Instansindstillinger",
@@ -722,8 +722,6 @@ const TRANSLATIONS = {
profile_picture: "Profilbillede",
remove_profile_picture: "Fjern profilbillede",
username: "Brugernavn",
username_description:
"Brugernavnet må kun indeholde små bogstaver, tal, understregninger og bindestreger uden mellemrum",
new_password: "Ny adgangskode",
password_description: "Adgangskoden skal være mindst 8 tegn lang",
cancel: "Annuller",

View File

@@ -22,8 +22,6 @@ const TRANSLATIONS = {
passwordWarn:
"Dieses Passwort sollte sicher aufbewahrt werden, da Wiederherstellung nicht möglich ist.",
adminUsername: "Benutzername des Admin-Accounts",
adminUsernameReq:
"Der Benutzername muss aus mindestens 6 Zeichen bestehen und darf ausschließlich Kleinbuchstaben, Ziffern, Unter- und Bindestriche enthalten keine Leerzeichen",
adminPassword: "Passwort des Admin-Accounts",
adminPasswordReq: "Das Passwort muss mindestens 8 Zeichen enthalten.",
teamHint:
@@ -71,6 +69,8 @@ const TRANSLATIONS = {
yes: "Ja",
no: "Nein",
search: null,
username_requirements:
"Der Benutzername muss 2-32 Zeichen lang sein, mit einem Kleinbuchstaben beginnen und darf nur Kleinbuchstaben, Zahlen, Unterstriche, Bindestriche und Punkte enthalten.",
},
settings: {
title: "Instanzeinstellungen",
@@ -922,8 +922,6 @@ const TRANSLATIONS = {
profile_picture: "Profilbild",
remove_profile_picture: "Profilbild entfernen",
username: "Nutzername",
username_description:
"Der Nutzername darf nur kleine Buchstaben, Zahlen, Unterstriche und Bindestriche ohne Leerzeichen enthalten.",
new_password: "Neues Passwort",
password_description: "Das Passwort muss mindestens 8 Zeichen haben.",
cancel: "Abbrechen",

View File

@@ -22,8 +22,6 @@ const TRANSLATIONS = {
"It's important to save this password because there is no recovery method.",
adminUsername: "Admin account username",
adminUsernameReq:
"Username must be at least 6 characters long and only contain lowercase letters, numbers, underscores, and hyphens with no spaces.",
adminPassword: "Admin account password",
adminPasswordReq: "Passwords must be at least 8 characters.",
teamHint:
@@ -71,6 +69,8 @@ const TRANSLATIONS = {
yes: "Yes",
no: "No",
search: "Search",
username_requirements:
"Username must be 2-32 characters, start with a lowercase letter, and only contain lowercase letters, numbers, underscores, hyphens, and periods.",
},
home: {
welcome: "Welcome",
@@ -989,8 +989,6 @@ const TRANSLATIONS = {
profile_picture: "Profile Picture",
remove_profile_picture: "Remove Profile Picture",
username: "Username",
username_description:
"Username must only contain lowercase letters, numbers, underscores, and hyphens with no spaces",
new_password: "New Password",
password_description: "Password must be at least 8 characters long",
cancel: "Cancel",

View File

@@ -22,8 +22,6 @@ const TRANSLATIONS = {
passwordWarn:
"Es importante guardar esta contraseña porque no hay método de recuperación.",
adminUsername: "Nombre de usuario del administrador",
adminUsernameReq:
"El nombre de usuario debe tener al menos 6 caracteres y solo puede contener letras minúsculas, números, guiones bajos y guiones sin espacios.",
adminPassword: "Contraseña de la cuenta de administrador",
adminPasswordReq: "Las contraseñas deben tener al menos 8 caracteres.",
teamHint:
@@ -71,6 +69,8 @@ const TRANSLATIONS = {
yes: "Sí",
no: "No",
search: "Buscar",
username_requirements:
"El nombre de usuario debe tener entre 2 y 32 caracteres, comenzar con una letra minúscula y solo contener letras minúsculas, números, guiones bajos, guiones y puntos.",
},
settings: {
title: "Ajustes de la instancia",
@@ -941,8 +941,6 @@ const TRANSLATIONS = {
profile_picture: "Foto de perfil",
remove_profile_picture: "Eliminar foto de perfil",
username: "Nombre de usuario",
username_description:
"El nombre de usuario solo debe contener letras minúsculas, números, guiones bajos y guiones sin espacios",
new_password: "Nueva contraseña",
password_description: "La contraseña debe tener al menos 8 caracteres",
cancel: "Cancelar",

View File

@@ -22,8 +22,6 @@ const TRANSLATIONS = {
passwordWarn:
"Salvesta see parool hoolikalt, sest taastamisvõimalust ei ole.",
adminUsername: "Admini kasutajanimi",
adminUsernameReq:
"Kasutajanimi peab olema vähemalt 6 märki ning võib sisaldada ainult väiketähti, numbreid, alakriipse ja sidekriipse.",
adminPassword: "Admini parool",
adminPasswordReq: "Parool peab olema vähemalt 8 märki.",
teamHint:
@@ -69,6 +67,8 @@ const TRANSLATIONS = {
yes: "Jah",
no: "Ei",
search: null,
username_requirements:
"Kasutajanimi peab olema 232 tähemärki, algama väiketähega ning sisaldama ainult väiketähti, numbreid, alakriipse, sidekriipse ja punkte.",
},
settings: {
title: "Instantsi seaded",
@@ -881,8 +881,6 @@ const TRANSLATIONS = {
profile_picture: "Profiilipilt",
remove_profile_picture: "Eemalda profiilipilt",
username: "Kasutajanimi",
username_description:
"Kasutajanimi võib sisaldada ainult väiketähti, numbreid, alakriipse ja sidekriipse, ilma tühikuteta",
new_password: "Uus parool",
password_description: "Parool peab olema vähemalt 8 märki",
cancel: "Tühista",

View File

@@ -33,7 +33,6 @@ const TRANSLATIONS = {
passwordReq: null,
passwordWarn: null,
adminUsername: null,
adminUsernameReq: null,
adminPassword: null,
adminPasswordReq: null,
teamHint: null,
@@ -62,6 +61,8 @@ const TRANSLATIONS = {
yes: null,
no: null,
search: null,
username_requirements:
"نام کاربری باید 2 تا 32 کاراکتر باشد، با حرف کوچک شروع شود و فقط شامل حروف کوچک، اعداد، زیرخط، خط تیره و نقطه باشد.",
},
settings: {
title: "تنظیمات سامانه",
@@ -676,7 +677,6 @@ const TRANSLATIONS = {
profile_picture: null,
remove_profile_picture: null,
username: null,
username_description: null,
new_password: null,
password_description: null,
cancel: null,

View File

@@ -36,8 +36,6 @@ const TRANSLATIONS = {
passwordWarn:
"Conservez ce mot de passe, il n'y a pas de récupération possible.",
adminUsername: "Nom d'utilisateur administrateur",
adminUsernameReq:
"Le nom d'utilisateur doit contenir au moins 6 caractères.",
adminPassword: "Mot de passe administrateur",
adminPasswordReq: "Le mot de passe doit contenir au moins 8 caractères.",
teamHint:
@@ -70,6 +68,8 @@ const TRANSLATIONS = {
yes: "Oui",
no: "Non",
search: "Rechercher",
username_requirements:
"Le nom d'utilisateur doit comporter entre 2 et 32 caractères, commencer par une lettre minuscule et ne contenir que des lettres minuscules, des chiffres, des tirets bas, des tirets et des points.",
},
settings: {
title: "Paramètres de l'instance",
@@ -739,8 +739,6 @@ const TRANSLATIONS = {
profile_picture: "Photo de profil",
remove_profile_picture: "Supprimer la photo de profil",
username: "Nom d'utilisateur",
username_description:
"Le nom d'utilisateur doit contenir uniquement des lettres minuscules, des chiffres, des tirets bas et des tirets, sans espaces.",
new_password: "Nouveau mot de passe",
password_description:
"Le mot de passe doit contenir au moins 8 caractères.",

View File

@@ -21,8 +21,6 @@ const TRANSLATIONS = {
passwordReq: "סיסמאות חייבות להכיל לפחות 8 תווים.",
passwordWarn: "חשוב לשמור סיסמה זו מכיוון שאין שיטת שחזור.",
adminUsername: "שם משתמש של חשבון מנהל",
adminUsernameReq:
"שם המשתמש חייב להכיל לפחות 6 תווים ויכול לכלול רק אותיות קטנות, מספרים, קווים תחתונים ומקפים, ללא רווחים.",
adminPassword: "סיסמת חשבון מנהל",
adminPasswordReq: "סיסמאות חייבות להכיל לפחות 8 תווים.",
teamHint:
@@ -68,6 +66,8 @@ const TRANSLATIONS = {
yes: "כן",
no: "לא",
search: "חיפוש",
username_requirements:
"שם המשתמש חייב להיות באורך 2-32 תווים, להתחיל באות קטנה ולהכיל רק אותיות קטנות, מספרים, קווים תחתונים, מקפים ונקודות.",
},
settings: {
title: "הגדרות מופע",
@@ -889,8 +889,6 @@ const TRANSLATIONS = {
profile_picture: "תמונת פרופיל",
remove_profile_picture: "הסר תמונת פרופיל",
username: "שם משתמש",
username_description:
"שם המשתמש חייב להכיל רק אותיות קטנות, מספרים, קווים תחתונים ומקפים, ללא רווחים",
new_password: "סיסמה חדשה",
password_description: "הסיסמה חייבת להכיל לפחות 8 תווים",
cancel: "בטל",

View File

@@ -33,7 +33,6 @@ const TRANSLATIONS = {
passwordReq: null,
passwordWarn: null,
adminUsername: null,
adminUsernameReq: null,
adminPassword: null,
adminPasswordReq: null,
teamHint: null,
@@ -62,6 +61,8 @@ const TRANSLATIONS = {
yes: null,
no: null,
search: null,
username_requirements:
"Il nome utente deve essere compreso tra 2 e 32 caratteri, iniziare con una lettera minuscola e contenere solo lettere minuscole, numeri, trattini bassi, trattini e punti.",
},
settings: {
title: "Impostazioni istanza",
@@ -682,7 +683,6 @@ const TRANSLATIONS = {
profile_picture: null,
remove_profile_picture: null,
username: null,
username_description: null,
new_password: null,
password_description: null,
cancel: null,

View File

@@ -22,8 +22,6 @@ const TRANSLATIONS = {
passwordWarn:
"このパスワードを保存することが重要です。回復方法はありません。",
adminUsername: "管理者アカウントのユーザー名",
adminUsernameReq:
"ユーザー名は6文字以上で、小文字の英字、数字、アンダースコア、ハイフンのみを含む必要があります。スペースは使用できません。",
adminPassword: "管理者アカウントのパスワード",
adminPasswordReq: "パスワードは8文字以上である必要があります。",
teamHint:
@@ -70,6 +68,8 @@ const TRANSLATIONS = {
yes: "はい",
no: "いいえ",
search: null,
username_requirements:
"ユーザー名は2〜32文字で、小文字で始まり、小文字、数字、アンダースコア、ハイフン、ピリオドのみを含む必要があります。",
},
settings: {
title: "インスタンス設定",
@@ -714,8 +714,6 @@ const TRANSLATIONS = {
profile_picture: "プロフィール画像",
remove_profile_picture: "プロフィール画像を削除",
username: "ユーザー名",
username_description:
"ユーザー名は小文字の英字、数字、アンダースコア、ハイフンのみ使用でき、スペースは使えません",
new_password: "新しいパスワード",
password_description: "パスワードは8文字以上である必要があります",
cancel: "キャンセル",

View File

@@ -21,8 +21,6 @@ const TRANSLATIONS = {
passwordReq: "비밀번호는 최소 8자 이상이어야 합니다.",
passwordWarn: "이 비밀번호는 복구 방법이 없으니 꼭 안전하게 보관하세요.",
adminUsername: "관리자 계정 사용자명",
adminUsernameReq:
"사용자명은 6자 이상이어야 하며, 소문자, 숫자, 밑줄(_), 하이픈(-)만 사용할 수 있습니다. 공백은 허용되지 않습니다.",
adminPassword: "관리자 계정 비밀번호",
adminPasswordReq: "비밀번호는 최소 8자 이상이어야 합니다.",
teamHint:
@@ -69,6 +67,8 @@ const TRANSLATIONS = {
yes: "예",
no: "아니오",
search: null,
username_requirements:
"사용자 이름은 2-32자여야 하고, 소문자로 시작해야 하며, 소문자, 숫자, 밑줄, 하이픈, 마침표만 포함할 수 있습니다.",
},
settings: {
title: "인스턴스 설정",
@@ -899,8 +899,6 @@ const TRANSLATIONS = {
profile_picture: "프로필 사진",
remove_profile_picture: "프로필 사진 삭제",
username: "사용자명",
username_description:
"사용자명은 소문자, 숫자, 밑줄(_), 하이픈(-)만 사용할 수 있으며, 공백은 허용되지 않습니다.",
new_password: "새 비밀번호",
password_description: "비밀번호는 최소 8자 이상이어야 합니다.",
cancel: "취소",

View File

@@ -21,8 +21,6 @@ const TRANSLATIONS = {
passwordReq: "Parolēm jābūt vismaz 8 rakstzīmes garām.",
passwordWarn: "Svarīgi saglabāt šo paroli, jo nav atjaunošanas metodes.",
adminUsername: "Administratora konta lietotājvārds",
adminUsernameReq:
"Lietotājvārdam jābūt vismaz 6 rakstzīmes garam un jāsatur tikai mazie burti, cipari, pasvītrojumi un domuzīmes bez atstarpēm.",
adminPassword: "Administratora konta parole",
adminPasswordReq: "Parolēm jābūt vismaz 8 rakstzīmes garām.",
teamHint:
@@ -70,6 +68,8 @@ const TRANSLATIONS = {
yes: "Jā",
no: "Nē",
search: null,
username_requirements:
"Lietotājvārdam jābūt 232 rakstzīmju garam, jāsākas ar mazo burtu un jāsatur tikai mazie burti, cipari, apakšsvītras, domuzīmes un punkti.",
},
settings: {
title: "Instances iestatījumi",
@@ -913,8 +913,6 @@ const TRANSLATIONS = {
profile_picture: "Profila attēls",
remove_profile_picture: "Noņemt profila attēlu",
username: "Lietotājvārds",
username_description:
"Lietotājvārdam jāsatur tikai mazie burti, cipari, pasvītrojumi un defises bez atstarpēm",
new_password: "Jauna parole",
password_description: "Parolei jābūt vismaz 8 rakstzīmes garai",
cancel: "Atcelt",

View File

@@ -37,8 +37,6 @@ const TRANSLATIONS = {
passwordWarn:
"Het is belangrijk om dit wachtwoord te bewaren, omdat er geen herstelmethode is.",
adminUsername: "Gebruikersnaam van het beheerdersaccount",
adminUsernameReq:
"De gebruikersnaam moet minimaal 6 tekens lang zijn en mag alleen kleine letters, cijfers, underscores en koppeltekens bevatten, zonder spaties.",
adminPassword: "Wachtwoord van het beheerdersaccount",
adminPasswordReq: "Wachtwoorden moeten minimaal 8 tekens lang zijn.",
teamHint:
@@ -71,6 +69,8 @@ const TRANSLATIONS = {
yes: "Ja",
no: "Nee",
search: "Zoeken",
username_requirements:
"De gebruikersnaam moet 2-32 tekens bevatten, beginnen met een kleine letter en mag alleen kleine letters, cijfers, underscores, koppeltekens en punten bevatten.",
},
settings: {
title: "Instelling Instanties",
@@ -736,8 +736,6 @@ const TRANSLATIONS = {
profile_picture: "Profielafbeelding",
remove_profile_picture: "Profielafbeelding verwijderen",
username: "Gebruikersnaam",
username_description:
"Gebruikersnaam mag alleen kleine letters, cijfers, underscores en koppeltekens bevatten, zonder spaties",
new_password: "Nieuw wachtwoord",
password_description: "Wachtwoord moet minimaal 8 tekens lang zijn",
cancel: "Annuleren",

View File

@@ -22,8 +22,6 @@ const TRANSLATIONS = {
passwordWarn:
"Ważne jest, aby zapisać to hasło, ponieważ nie ma metody jego odzyskania.",
adminUsername: "Nazwa użytkownika konta administratora",
adminUsernameReq:
"Nazwa użytkownika musi składać się z co najmniej 6 znaków i zawierać wyłącznie małe litery, cyfry, podkreślenia i myślniki bez spacji.",
adminPassword: "Hasło konta administratora",
adminPasswordReq: "Hasła muszą składać się z co najmniej 8 znaków.",
teamHint:
@@ -71,6 +69,8 @@ const TRANSLATIONS = {
yes: "Tak",
no: "Nie",
search: null,
username_requirements:
"Nazwa użytkownika musi mieć od 2 do 32 znaków, zaczynać się małą literą i zawierać tylko małe litery, cyfry, podkreślenia, myślniki i kropki.",
},
settings: {
title: "Ustawienia instancji",
@@ -919,8 +919,6 @@ const TRANSLATIONS = {
profile_picture: "Zdjęcie profilowe",
remove_profile_picture: "Usuń zdjęcie profilowe",
username: "Nazwa użytkownika",
username_description:
"Nazwa użytkownika musi zawierać tylko małe litery, cyfry, podkreślenia i myślniki bez spacji.",
new_password: "Nowe hasło",
password_description: null,
cancel: "Anuluj",

View File

@@ -22,8 +22,6 @@ const TRANSLATIONS = {
passwordWarn:
"É importante salvar esta senha pois não há método de recuperação.",
adminUsername: "Nome de usuário admin",
adminUsernameReq:
"O nome deve ter pelo menos 6 caracteres e conter apenas letras minúsculas, números, sublinhados e hífens, sem espaços.",
adminPassword: "Senha de admin",
adminPasswordReq: "Senhas devem ter pelo menos 8 caracteres.",
teamHint:
@@ -69,6 +67,8 @@ const TRANSLATIONS = {
yes: "Sim",
no: "Não",
search: "Pesquisar",
username_requirements:
"O nome de usuário deve ter de 2 a 32 caracteres, começar com uma letra minúscula e conter apenas letras minúsculas, números, sublinhados, hífens e pontos.",
},
settings: {
title: "Configurações da Instância",
@@ -897,8 +897,6 @@ const TRANSLATIONS = {
profile_picture: "Foto de perfil",
remove_profile_picture: "Remover foto de perfil",
username: "Nome de usuário",
username_description:
"Somente letras minúsculas, números, sublinhados e hífens. Sem espaços.",
new_password: "Nova senha",
password_description: "A senha deve ter no mínimo 8 caracteres",
cancel: "Cancelar",

View File

@@ -22,8 +22,6 @@ const TRANSLATIONS = {
passwordWarn:
"Este important să salvezi această parolă deoarece nu există metodă de recuperare.",
adminUsername: "Numele contului de administrator",
adminUsernameReq:
"Numele de utilizator trebuie să aibă cel puțin 6 caractere și să conțină numai litere mici, cifre, underscore și liniuțe fără spații.",
adminPassword: "Parola contului de administrator",
adminPasswordReq: "Parolele trebuie să aibă cel puțin 8 caractere.",
teamHint:
@@ -71,6 +69,8 @@ const TRANSLATIONS = {
yes: "Da",
no: "Nu",
search: "Caută",
username_requirements:
"Numele de utilizator trebuie să aibă între 2 și 32 de caractere, să înceapă cu o literă mică și să conțină doar litere mici, cifre, liniuțe de subliniere, cratime și puncte.",
},
settings: {
title: "Setările instanței",
@@ -656,8 +656,6 @@ const TRANSLATIONS = {
profile_picture: "Poză profil",
remove_profile_picture: "Șterge poza profil",
username: "Nume utilizator",
username_description:
"Numele de utilizator trebuie să conțină doar litere mici, cifre, underscore și liniuțe fără spații",
new_password: "Parolă nouă",
password_description: "Parola trebuie să aibă cel puțin 8 caractere",
cancel: "Anulează",

View File

@@ -22,8 +22,6 @@ const TRANSLATIONS = {
passwordWarn:
"Важно сохранить этот пароль, так как способа его восстановления не существует.",
adminUsername: "Имя пользователя для учётной записи администратора",
adminUsernameReq:
"Имя пользователя должно состоять не менее чем из 6 символов и содержать только строчные буквы, цифры, символы подчеркивания и дефисы без пробелов.",
adminPassword: "Пароль для учётной записи администратора",
adminPasswordReq: "Пароль должен содержать не менее 8 символов.",
teamHint:
@@ -70,6 +68,8 @@ const TRANSLATIONS = {
yes: "Да",
no: "Нет",
search: null,
username_requirements:
"Имя пользователя должно содержать от 2 до 32 символов, начинаться со строчной буквы и содержать только строчные буквы, цифры, символы подчёркивания, дефисы и точки.",
},
settings: {
title: "Настройки экземпляра",
@@ -723,8 +723,6 @@ const TRANSLATIONS = {
profile_picture: "Изображение профиля",
remove_profile_picture: "Удалить изображение профиля",
username: "Имя пользователя",
username_description:
"Имя пользователя должно состоять только из строчных букв, цифр, символов подчеркивания и дефисов без пробелов",
new_password: "Новый пароль",
password_description: "Пароль должен содержать не менее 8 символов",
cancel: "Отмена",

View File

@@ -33,7 +33,6 @@ const TRANSLATIONS = {
passwordReq: null,
passwordWarn: null,
adminUsername: null,
adminUsernameReq: null,
adminPassword: null,
adminPasswordReq: null,
teamHint: null,
@@ -62,6 +61,8 @@ const TRANSLATIONS = {
yes: null,
no: null,
search: null,
username_requirements:
"Kullanıcı adı 2-32 karakter uzunluğunda olmalı, küçük harfle başlamalı ve yalnızca küçük harfler, rakamlar, alt çizgiler, tireler ve noktalar içermelidir.",
},
settings: {
title: "Instance Ayarları",
@@ -679,7 +680,6 @@ const TRANSLATIONS = {
profile_picture: null,
remove_profile_picture: null,
username: null,
username_description: null,
new_password: null,
password_description: null,
cancel: null,

View File

@@ -33,7 +33,6 @@ const TRANSLATIONS = {
passwordReq: null,
passwordWarn: null,
adminUsername: null,
adminUsernameReq: null,
adminPassword: null,
adminPasswordReq: null,
teamHint: null,
@@ -62,6 +61,8 @@ const TRANSLATIONS = {
yes: null,
no: null,
search: null,
username_requirements:
"Tên người dùng phải có 2-32 ký tự, bắt đầu bằng chữ cái thường và chỉ chứa chữ cái thường, số, dấu gạch dưới, dấu gạch ngang và dấu chấm.",
},
settings: {
title: "Cài đặt hệ thống",
@@ -678,7 +679,6 @@ const TRANSLATIONS = {
profile_picture: null,
remove_profile_picture: null,
username: null,
username_description: null,
new_password: null,
password_description: null,
cancel: null,

View File

@@ -21,8 +21,6 @@ const TRANSLATIONS = {
passwordReq: "密码必须至少包含 8 个字符。",
passwordWarn: "保存此密码很重要,因为没有恢复方法。",
adminUsername: "管理员账户用户名",
adminUsernameReq:
"用户名必须至少为 6 个字符,并且只能包含小写字母、数字、下划线和连字符,不含空格。",
adminPassword: "管理员账户密码",
adminPasswordReq: "密码必须至少包含 8 个字符。",
teamHint:
@@ -66,6 +64,8 @@ const TRANSLATIONS = {
yes: "是",
no: "否",
search: "搜索",
username_requirements:
"用户名必须为 2-32 个字符,以小写字母开头,只能包含小写字母、数字、下划线、连字符和句点。",
},
settings: {
title: "设置",
@@ -857,8 +857,6 @@ const TRANSLATIONS = {
profile_picture: "头像",
remove_profile_picture: "移除头像",
username: "用户名",
username_description:
"用户名必须仅包含小写字母、数字、下划线和连字符,且不能包含空格",
new_password: "新密码",
password_description: "密码长度必须至少为 8 个字符",
cancel: "取消",

View File

@@ -21,8 +21,6 @@ const TRANSLATIONS = {
passwordReq: "密碼必須至少包含 8 個字元。",
passwordWarn: "保存此密碼很重要,因為沒有恢復方法。",
adminUsername: "管理員帳號使用者名稱",
adminUsernameReq:
"使用者名稱必須至少為 6 個字元,並且只能包含小寫字母、數字、底線和連字號,不含空格。",
adminPassword: "管理員帳號密碼",
adminPasswordReq: "密碼必須至少包含 8 個字元。",
teamHint:
@@ -66,6 +64,8 @@ const TRANSLATIONS = {
yes: "是",
no: "否",
search: "搜尋",
username_requirements:
"使用者名稱必須為 2-32 個字元,以小寫字母開頭,且只能包含小寫字母、數字、底線、連字號和句點。",
},
settings: {
title: "系統設定",
@@ -686,8 +686,6 @@ const TRANSLATIONS = {
profile_picture: "個人資料圖片",
remove_profile_picture: "移除個人資料圖片",
username: "使用者名稱",
username_description:
"使用者名稱必須只包含小寫字母、數字、底線和連字號,且沒有空格",
new_password: "新密碼",
password_description: "密碼長度必須至少為 8 個字元",
cancel: "取消",

View File

@@ -3,6 +3,12 @@ import { X } from "@phosphor-icons/react";
import Admin from "@/models/admin";
import { userFromStorage } from "@/utils/request";
import { MessageLimitInput, RoleHintDisplay } from "..";
import { useTranslation } from "react-i18next";
import {
USERNAME_MIN_LENGTH,
USERNAME_MAX_LENGTH,
USERNAME_PATTERN,
} from "@/utils/username";
export default function NewUserModal({ closeModal }) {
const [error, setError] = useState(null);
@@ -11,6 +17,7 @@ export default function NewUserModal({ closeModal }) {
enabled: false,
limit: 10,
});
const { t } = useTranslation();
const handleCreate = async (e) => {
setError(null);
@@ -59,13 +66,14 @@ export default function NewUserModal({ closeModal }) {
type="text"
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="User's username"
minLength={2}
minLength={USERNAME_MIN_LENGTH}
maxLength={USERNAME_MAX_LENGTH}
pattern={USERNAME_PATTERN}
required={true}
autoComplete="off"
/>
<p className="mt-2 text-xs text-white/60">
Username must only contain lowercase letters, periods,
numbers, underscores, and hyphens with no spaces
{t("common.username_requirements")}
</p>
</div>
<div>

View File

@@ -3,6 +3,12 @@ import { X } from "@phosphor-icons/react";
import Admin from "@/models/admin";
import { MessageLimitInput, RoleHintDisplay } from "../..";
import { AUTH_USER } from "@/utils/constants";
import { useTranslation } from "react-i18next";
import {
USERNAME_MIN_LENGTH,
USERNAME_MAX_LENGTH,
USERNAME_PATTERN,
} from "@/utils/username";
export default function EditUserModal({ currentUser, user, closeModal }) {
const [role, setRole] = useState(user.role);
@@ -11,6 +17,7 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
enabled: user.dailyMessageLimit !== null,
limit: user.dailyMessageLimit || 10,
});
const { t } = useTranslation();
const handleUpdate = async (e) => {
setError(null);
@@ -75,13 +82,14 @@ export default function EditUserModal({ currentUser, user, closeModal }) {
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="User's username"
defaultValue={user.username}
minLength={2}
minLength={USERNAME_MIN_LENGTH}
maxLength={USERNAME_MAX_LENGTH}
pattern={USERNAME_PATTERN}
required={true}
autoComplete="off"
/>
<p className="mt-2 text-xs text-white/60">
Username must only contain lowercase letters, periods,
numbers, underscores, and hyphens with no spaces
{t("common.username_requirements")}
</p>
</div>
<div>

View File

@@ -8,6 +8,11 @@ import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "@/utils/constants";
import PreLoader from "@/components/Preloader";
import CTAButton from "@/components/lib/CTAButton";
import { useTranslation } from "react-i18next";
import {
USERNAME_MIN_LENGTH,
USERNAME_MAX_LENGTH,
USERNAME_PATTERN,
} from "@/utils/username";
export default function GeneralSecurity() {
const { t } = useTranslation();
@@ -154,12 +159,17 @@ function MultiUserMode() {
type="text"
className="border-none bg-theme-settings-input-bg text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 placeholder:text-theme-settings-input-placeholder focus:ring-blue-500"
placeholder="Your admin username"
minLength={2}
minLength={USERNAME_MIN_LENGTH}
maxLength={USERNAME_MAX_LENGTH}
pattern={USERNAME_PATTERN}
required={true}
autoComplete="off"
disabled={multiUserModeEnabled}
defaultValue={multiUserModeEnabled ? "********" : ""}
/>
<p className="text-white text-opacity-60 text-xs mt-2">
{t("common.username_requirements")}
</p>
</div>
<div className="mt-4 w-80">
<label
@@ -326,7 +336,9 @@ function PasswordProtection() {
type="text"
className="border-none bg-theme-settings-input-bg text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 placeholder:text-theme-settings-input-placeholder"
placeholder="Your Instance Password"
minLength={8}
minLength={PASSWORD_MIN_LENGTH}
maxLength={PASSWORD_MAX_LENGTH}
pattern={PASSWORD_PATTERN}
required={true}
autoComplete="off"
defaultValue={usePassword ? "********" : ""}

View File

@@ -4,10 +4,17 @@ import paths from "@/utils/paths";
import { useParams } from "react-router-dom";
import { AUTH_TOKEN, AUTH_USER } from "@/utils/constants";
import System from "@/models/system";
import { useTranslation } from "react-i18next";
import {
USERNAME_MIN_LENGTH,
USERNAME_MAX_LENGTH,
USERNAME_PATTERN,
} from "@/utils/username";
export default function NewUserModal() {
const { code } = useParams();
const [error, setError] = useState(null);
const { t } = useTranslation();
const handleCreate = async (e) => {
setError(null);
@@ -53,10 +60,15 @@ export default function NewUserModal() {
type="text"
className="border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="My username"
minLength={2}
minLength={USERNAME_MIN_LENGTH}
maxLength={USERNAME_MAX_LENGTH}
pattern={USERNAME_PATTERN}
required={true}
autoComplete="off"
/>
<p className="mt-2 text-xs text-theme-text-secondary">
{t("common.username_requirements")}
</p>
</div>
<div>
<label

View File

@@ -6,6 +6,7 @@ import paths from "@/utils/paths";
import { useNavigate } from "react-router-dom";
import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "@/utils/constants";
import { useTranslation } from "react-i18next";
import { USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH } from "@/utils/username";
export default function UserSetup({ setHeader, setForwardBtn, setBackBtn }) {
const { t } = useTranslation();
@@ -267,7 +268,9 @@ const MyTeam = ({ setMultiUserLoginValid, myTeamSubmitRef, navigate }) => {
const handlePasswordChange = debounce(setNewPassword, 500);
useEffect(() => {
if (username.length >= 6 && password.length >= 8) {
// Enable button if there's any input, allowing users to attempt submission
// Validation errors will be shown via toast in handleSubmit
if (username.trim().length > 0 && password.length > 0) {
setMultiUserLoginValid(true);
} else {
setMultiUserLoginValid(false);
@@ -291,14 +294,15 @@ const MyTeam = ({ setMultiUserLoginValid, myTeamSubmitRef, navigate }) => {
type="text"
className="border-none bg-theme-settings-input-bg text-white text-sm rounded-lg block w-full p-2.5 focus:outline-primary-button active:outline-primary-button placeholder:text-theme-text-secondary outline-none"
placeholder="Your admin username"
minLength={6}
minLength={USERNAME_MIN_LENGTH}
maxLength={USERNAME_MAX_LENGTH}
required={true}
autoComplete="off"
onChange={handleUsernameChange}
/>
</div>
<p className=" text-white text-opacity-80 text-xs font-base">
{t("onboarding.userSetup.adminUsernameReq")}
{t("common.username_requirements")}
</p>
<div className="mt-4">
<label

View File

@@ -0,0 +1,17 @@
/**
* Unix-style username validation utilities
*
* Requirements:
* - 2-32 characters long
* - Must start with a lowercase letter
* - Can contain lowercase letters, digits, underscores, hyphens, @ signs, and periods
*/
export const USERNAME_REGEX = /^[a-z][a-z0-9._@-]*$/;
export const USERNAME_MIN_LENGTH = 2;
export const USERNAME_MAX_LENGTH = 32;
/**
* HTML5 pattern attribute for username inputs (without ^ and $)
*/
export const USERNAME_PATTERN = "[a-z][a-z0-9._@-]*";

View File

@@ -0,0 +1,46 @@
const { User } = require("../../models/user");
describe("username validation restrictions", () => {
beforeEach(() => {
jest.clearAllMocks();
});
const failureMessages = [
"Username cannot be longer than 32 characters",
"Username must be at least 2 characters",
"Username must start with a lowercase letter and only contain lowercase letters, numbers, underscores, hyphens, and periods",
];
it("should throw an error if the username is longer than 32 characters", () => {
expect(() => User.validations.username("a".repeat(33))).toThrow(failureMessages[0]);
});
it("should throw an error if the username is less than 2 characters", () => {
expect(() => User.validations.username("a")).toThrow(failureMessages[1]);
});
it("should throw an error if the username does not start with a lowercase letter", () => {
expect(() => User.validations.username("Aa1")).toThrow(failureMessages[2]);
});
it("should throw an error if the username contains invalid characters", () => {
expect(() => User.validations.username("ad-123_456.789*")).toThrow(failureMessages[2]);
expect(() => User.validations.username("ad-123_456#456")).toThrow(failureMessages[2]);
expect(() => User.validations.username("ad-123_456!456")).toThrow(failureMessages[2]);
});
it("should return the username if it is valid or an email address", () => {
expect(User.validations.username("a123_456.789@")).toBe("a123_456.789@");
expect(User.validations.username("a123_456.789@example.com")).toBe("a123_456.789@example.com");
});
it("should throw an error if the username is not a string", () => {
expect(() => User.validations.username(123)).toThrow(failureMessages[2]);
expect(() => User.validations.username(null)).not.toThrow();
expect(() => User.validations.username(undefined)).toThrow(failureMessages[1]);
expect(() => User.validations.username({})).toThrow(failureMessages[3]);
expect(() => User.validations.username([])).toThrow(failureMessages[3]);
expect(() => User.validations.username(true)).not.toThrow();
expect(() => User.validations.username(false)).not.toThrow();
});
});

View File

@@ -1208,7 +1208,9 @@ function systemEndpoints(app) {
}
const updates = {};
if (username)
// If the username is being changed, validate it.
// Otherwise, do not attempt to validate it to allow existing users to keep their username if not changing it.
if (username !== sessionUser.username)
updates.username = User.validations.username(String(username));
if (password) updates.password = String(password);
if (bio) updates.bio = String(bio);
@@ -1224,7 +1226,9 @@ function systemEndpoints(app) {
response.status(200).json({ success, error });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
response
.status(500)
.json({ success: false, error: e.message || "Internal server error" });
}
});

View File

@@ -13,7 +13,7 @@ const { EventLogs } = require("./eventLogs");
*/
const User = {
usernameRegex: new RegExp(/^[a-zA-Z0-9._%+-@]+$/),
usernameRegex: new RegExp(/^[a-z][a-z0-9._@-]*$/),
writable: [
// Used for generic updates so we can validate keys in request body
"username",
@@ -25,13 +25,24 @@ const User = {
"bio",
],
validations: {
/**
* Unix-style username regex:
* - Must start with a lowercase letter
* - Can contain lowercase letters, digits, underscores, hyphens, @ signs, and periods
* - 2-32 characters long
*/
username: (newValue = "") => {
try {
if (String(newValue).length > 100)
throw new Error("Username cannot be longer than 100 characters");
if (String(newValue).length < 2)
const username = String(newValue);
if (username.length > 32)
throw new Error("Username cannot be longer than 32 characters");
if (username.length < 2)
throw new Error("Username must be at least 2 characters");
return String(newValue);
if (!User.usernameRegex.test(username))
throw new Error(
"Username must start with a lowercase letter and only contain lowercase letters, numbers, underscores, hyphens, and periods"
);
return username;
} catch (e) {
throw new Error(e.message);
}
@@ -92,17 +103,14 @@ const User = {
}
try {
// Do not allow new users to bypass validation
if (!this.usernameRegex.test(username))
throw new Error(
"Username must only contain letters, numbers, periods, underscores, hyphens, and email characters (@, %, +, -) with no spaces"
);
// Validate username format (validation function handles all checks)
const validatedUsername = this.validations.username(username);
const bcrypt = require("bcryptjs");
const hashedPassword = bcrypt.hashSync(password, 10);
const user = await prisma.users.create({
data: {
username: this.validations.username(username),
username: validatedUsername,
password: hashedPassword,
role: this.validations.role(role),
bio: this.validations.bio(bio),
@@ -138,6 +146,14 @@ const User = {
where: { id: parseInt(userId) },
});
if (!currentUser) return { success: false, error: "User not found" };
// We previously had more lenient username validation, but now with more strict validation
// we dont want to break existing users by changing non-username fields.
// If they are not explictly changing the username, do not attempt to validate it.
if (updates.hasOwnProperty("username")) {
if (updates.username === currentUser.username) delete updates.username;
}
// Removes non-writable fields for generic updates
// and force-casts to the proper type;
Object.entries(updates).forEach(([key, value]) => {
@@ -167,17 +183,6 @@ const User = {
updates.password = bcrypt.hashSync(updates.password, 10);
}
if (
updates.hasOwnProperty("username") &&
currentUser.username !== updates.username &&
!this.usernameRegex.test(updates.username)
)
return {
success: false,
error:
"Username must only contain letters, numbers, periods, underscores, hyphens, and email characters (@, %, +, -) with no spaces",
};
const user = await prisma.users.update({
where: { id: parseInt(userId) },
data: updates,