mirror of
https://github.com/Mintplex-Labs/anything-llm
synced 2026-04-25 17:15:37 +02:00
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:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 2–32 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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 2–32 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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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: "בטל",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: "キャンセル",
|
||||
|
||||
@@ -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: "취소",
|
||||
|
||||
@@ -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 2–32 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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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ă",
|
||||
|
||||
@@ -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: "Отмена",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: "取消",
|
||||
|
||||
@@ -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: "取消",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? "********" : ""}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
17
frontend/src/utils/username.js
Normal file
17
frontend/src/utils/username.js
Normal 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._@-]*";
|
||||
46
server/__tests__/models/user.test.js
Normal file
46
server/__tests__/models/user.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user