Repository: DefGuard/defguard (language: rust)

Issue:
Username Special Chars
**Is your feature request related to a problem? Please describe.**
Username chars are very limited and restricted. 

**Describe the solution you'd like**
Ability to use special chars in usernames where possible - for example @ or - 
Some users may want their email to be their username. Using valid email regex might be an option https://regexr.com/3e48o


Discussion / hints:
Because we support sync with LDAP, the username field is currently restricted to a simple set of characters plus numbers.
We will explore possible solutions to fix this issue in the future.
That's fair enough but LDAP's reserved chars are only the below to my knowledge, dashes and @ are supported.

``` " / \ [ ] : ; | = , + * ? < > ```
wow! this is relevant for me!
our logins are indicated as first.last, and it is impossible to use our logins for sso defg
The problem with special chars we have is that we also support synchronisation with LDAP which has some restrictions on the login chars.

But we'll take this on the workshop and see which chars are supported by LDAP protocol and implement them.
@moubctez any comments?

Fix patch:
diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts
index deed5ae3b..b53083ef1 100644
--- a/web/src/i18n/en/index.ts
+++ b/web/src/i18n/en/index.ts
@@ -711,7 +711,8 @@ const en: BaseTranslation = {
       username: 'Username',
     },
     error: {
-      usernameTaken: 'Username is already in use',
+      forbiddenCharacter: 'Field contain forbidden characters.',
+      usernameTaken: 'Username is already in use.',
       invalidKey: 'Key is invalid.',
       invalid: 'Field is invalid.',
       required: 'Field is required.',
@@ -728,8 +729,8 @@ const en: BaseTranslation = {
       validPort: 'Enter a valid port.',
       validCode: 'Code should have 6 digits.',
       allowedIps: 'Only valid IP or domain is allowed.',
-      startFromNumber: 'Cannot start from number',
-      repeat: `Fields don't match`,
+      startFromNumber: 'Cannot start from number.',
+      repeat: `Fields don't match.`,
     },
     floatingErrors: {
       title: 'Please correct the following:',
diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts
index 1d30e200b..8b6df2feb 100644
--- a/web/src/i18n/i18n-types.ts
+++ b/web/src/i18n/i18n-types.ts
@@ -1670,7 +1670,11 @@ type RootTranslation = {
 		}
 		error: {
 			/**
-			 * U​s​e​r​n​a​m​e​ ​i​s​ ​a​l​r​e​a​d​y​ ​i​n​ ​u​s​e
+			 * F​i​e​l​d​ ​c​o​n​t​a​i​n​ ​f​o​r​b​i​d​d​e​n​ ​c​h​a​r​a​c​t​e​r​s​.
+			 */
+			forbiddenCharacter: string
+			/**
+			 * U​s​e​r​n​a​m​e​ ​i​s​ ​a​l​r​e​a​d​y​ ​i​n​ ​u​s​e​.
 			 */
 			usernameTaken: string
 			/**
@@ -1738,11 +1742,11 @@ type RootTranslation = {
 			 */
 			allowedIps: string
 			/**
-			 * C​a​n​n​o​t​ ​s​t​a​r​t​ ​f​r​o​m​ ​n​u​m​b​e​r
+			 * C​a​n​n​o​t​ ​s​t​a​r​t​ ​f​r​o​m​ ​n​u​m​b​e​r​.
 			 */
 			startFromNumber: string
 			/**
-			 * F​i​e​l​d​s​ ​d​o​n​'​t​ ​m​a​t​c​h
+			 * F​i​e​l​d​s​ ​d​o​n​'​t​ ​m​a​t​c​h​.
 			 */
 			repeat: string
 		}
@@ -5206,7 +5210,11 @@ export type TranslationFunctions = {
 		}
 		error: {
 			/**
-			 * Username is already in use
+			 * Field contain forbidden characters.
+			 */
+			forbiddenCharacter: () => LocalizedString
+			/**
+			 * Username is already in use.
 			 */
 			usernameTaken: () => LocalizedString
 			/**
@@ -5274,11 +5282,11 @@ export type TranslationFunctions = {
 			 */
 			allowedIps: () => LocalizedString
 			/**
-			 * Cannot start from number
+			 * Cannot start from number.
 			 */
 			startFromNumber: () => LocalizedString
 			/**
-			 * Fields don't match
+			 * Fields don't match.
 			 */
 			repeat: () => LocalizedString
 		}
diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts
index d4327383c..fe41d274f 100644
--- a/web/src/i18n/pl/index.ts
+++ b/web/src/i18n/pl/index.ts
@@ -696,12 +696,13 @@ Uwaga, konfiguracje tutaj podane, nie posiadają twojego klucza prywatnego. Musi
       username: 'Nazwa użytkownika',
     },
     error: {
-      usernameTaken: 'Nazwa użytkownika jest już w użyciu',
+      forbiddenCharacter: 'Pole zawiera niedozwolone znaki.',
+      usernameTaken: 'Nazwa użytkownika jest już w użyciu.',
       invalidKey: 'Klucz jest nieprawidłowy.',
       invalid: 'Pole jest nieprawidłowe.',
       required: 'Pole jest wymagane.',
       maximumLength: 'Maksymalna długość przekroczona.',
-      minimumLength: 'Minimalna długość nie została osiągnięta',
+      minimumLength: 'Minimalna długość nie została osiągnięta.',
       noSpecialChars: 'Nie wolno używać znaków specjalnych.',
       oneDigit: 'Wymagana jedna cyfra.',
       oneSpecial: 'Wymagany jest znak specjalny.',
@@ -713,8 +714,8 @@ Uwaga, konfiguracje tutaj podane, nie posiadają twojego klucza prywatnego. Musi
       validPort: 'Wprowadź prawidłowy port.',
       validCode: 'Kod powinien mieć 6 cyfr.',
       allowedIps: 'Tylko poprawne adresy IP oraz domeny.',
-      startFromNumber: 'Nie może zaczynać się od liczby',
-      repeat: 'Wartości się nie pokrywają',
+      startFromNumber: 'Nie może zaczynać się od liczby.',
+      repeat: 'Wartości się nie pokrywają.',
     },
     floatingErrors: {
       title: 'Popraw następujące błędy:',
diff --git a/web/src/pages/network/style.scss b/web/src/pages/network/style.scss
index cc62fa838..185c669d1 100644
--- a/web/src/pages/network/style.scss
+++ b/web/src/pages/network/style.scss
@@ -119,7 +119,7 @@
           }
         }
 
-         & > .form-checkbox {
+        & > .form-checkbox {
           margin-bottom: 25px;
         }
       }
diff --git a/web/src/pages/users/UsersOverview/modals/AddUserModal/components/AddUserForm/AddUserForm.tsx b/web/src/pages/users/UsersOverview/modals/AddUserModal/components/AddUserForm/AddUserForm.tsx
index e1c3fa66b..224e49e2a 100644
--- a/web/src/pages/users/UsersOverview/modals/AddUserModal/components/AddUserForm/AddUserForm.tsx
+++ b/web/src/pages/users/UsersOverview/modals/AddUserModal/components/AddUserForm/AddUserForm.tsx
@@ -19,8 +19,7 @@ import {
 import useApi from '../../../../../../../shared/hooks/useApi';
 import { useToaster } from '../../../../../../../shared/hooks/useToaster';
 import {
-  patternDigitOrLowercase,
-  patternNoSpecialChars,
+  patternSafeUsernameCharacters,
   patternStartsWithDigit,
   patternValidEmail,
   patternValidPhoneNumber,
@@ -57,8 +56,7 @@ export const AddUserForm = () => {
           username: yup
             .string()
             .required(LL.form.error.required())
-            .matches(patternNoSpecialChars, LL.form.error.noSpecialChars())
-            .matches(patternDigitOrLowercase, LL.form.error.invalid())
+            .matches(patternSafeUsernameCharacters, LL.form.error.forbiddenCharacter())
             .min(3, LL.form.error.minimumLength())
             .max(64, LL.form.error.maximumLength())
             .test('starts-with-number', LL.form.error.startFromNumber(), (value) => {
diff --git a/web/src/pages/wizard/components/WizardNetworkConfiguration/style.scss b/web/src/pages/wizard/components/WizardNetworkConfiguration/style.scss
index 5d705890a..269f55821 100644
--- a/web/src/pages/wizard/components/WizardNetworkConfiguration/style.scss
+++ b/web/src/pages/wizard/components/WizardNetworkConfiguration/style.scss
@@ -21,7 +21,7 @@
       margin-bottom: 25px;
     }
 
-     & > .form-checkbox {
+    & > .form-checkbox {
       margin-bottom: 25px;
     }
   }
diff --git a/web/src/shared/patterns.ts b/web/src/shared/patterns.ts
index 307b3ec84..9c497c7bb 100644
--- a/web/src/shared/patterns.ts
+++ b/web/src/shared/patterns.ts
@@ -72,3 +72,8 @@ export const patternValidDomain =
 
 export const patternValidIp =
   /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
+
+export const patternSafeUsernameCharacters =
+  /^[a-zA-Z0-9.!@#$%^&*()_+\-=\[\]{}|,<>\/?~]+$/;
+
+export const patternSafePasswordCharacters = patternSafeUsernameCharacters;
diff --git a/web/src/shared/validators/password.ts b/web/src/shared/validators/password.ts
index 7b1ce2b11..319ae23d3 100644
--- a/web/src/shared/validators/password.ts
+++ b/web/src/shared/validators/password.ts
@@ -6,6 +6,7 @@ import {
   patternAtLeastOneLowerCaseChar,
   patternAtLeastOneSpecialChar,
   patternAtLeastOneUpperCaseChar,
+  patternSafePasswordCharacters,
 } from '../patterns';
 
 export const passwordValidator = (LL: TranslationFunctions) =>
@@ -17,4 +18,5 @@ export const passwordValidator = (LL: TranslationFunctions) =>
     .matches(patternAtLeastOneSpecialChar, LL.form.error.oneSpecial())
     .matches(patternAtLeastOneUpperCaseChar, LL.form.error.oneUppercase())
     .matches(patternAtLeastOneLowerCaseChar, LL.form.error.oneLowercase())
+    .matches(patternSafePasswordCharacters, LL.form.error.forbiddenCharacter())
     .required(LL.form.error.required());


Test patch: