feat: added configurable password validation (#7640)

Signed-off-by: Dakshesh Jain <dakshesh.jain14@gmail.com>
Signed-off-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com>
This commit is contained in:
Dakshesh Jain
2025-01-27 07:35:24 +07:00
committed by GitHub
parent 7987ac9f81
commit e6b1ea15c7
17 changed files with 224 additions and 31 deletions
+6 -5
View File
@@ -1,14 +1,14 @@
{
"ACCOUNTS_URL":"/account",
"ACCOUNTS_URL": "/account",
"COLLABORATOR_URL": "ws://localhost:3078",
"UPLOAD_URL":"/files",
"UPLOAD_URL": "/files",
"TELEGRAM_URL": "http://localhost:8086",
"GMAIL_URL": "http://localhost:8088",
"CALENDAR_URL": "http://localhost:8095",
"REKONI_URL": "/rekoni",
"GITHUB_APP": "uberflow-dev",
"GITHUB_CLIENTID": "Iv1.43f9cac43bd68617",
"GITHUB_URL":"http://localhost:3500",
"GITHUB_URL": "http://localhost:3500",
"LAST_NAME_FIRST": "true",
"PRINT_URL": "http://localhost:4005",
"SIGN_URL": "http://localhost:4006",
@@ -17,5 +17,6 @@
"BRANDING_URL": "/branding.json",
"VERSION": null,
"MODEL_VERSION": null,
"STATS_URL": "http://localhost:4900"
}
"STATS_URL": "http://localhost:4900",
"PASSWORD_STRICTNESS": "none"
}
+34
View File
@@ -158,6 +158,7 @@ export interface Config {
AI_URL?:string
DISABLE_SIGNUP?: string
LINK_PREVIEW_URL?: string
PASSWORD_STRICTNESS?: "very_strict" | "strict" | "normal" | "none"
// Could be defined for dev environment
FRONT_URL?: string
PREVIEW_CONFIG?: string
@@ -198,6 +199,37 @@ const configs: Record<string, string> = {
'dev-worker-local': '/config-worker-local.json',
}
const PASSWORD_REQUIREMENTS : Record<Config['PASSWORD_STRICTNESS'], Record<string, number>> = {
very_strict: {
MinDigits: 4,
MinLength: 32,
MinLowerChars: 4,
MinSpecialChars: 4,
MinUpperChars: 4
},
strict: {
MinDigits: 2,
MinLength: 16,
MinLowerChars: 2,
MinSpecialChars: 2,
MinUpperChars: 2
},
normal: {
MinDigits: 1,
MinLength: 8,
MinLowerChars: 1,
MinSpecialChars: 1,
MinUpperChars: 1
},
none: {
MinDigits: 0,
MinLength: 0,
MinLowerChars: 0,
MinSpecialChars: 0,
MinUpperChars: 0
}
}
function configureI18n(): void {
//Add localization
addStringsLoader(platformId, async (lang: string) => await import(`@hcengineering/platform/lang/${lang}.json`))
@@ -306,6 +338,8 @@ export async function configurePlatform() {
setMetadata(login.metadata.AccountsUrl, config.ACCOUNTS_URL)
setMetadata(login.metadata.DisableSignUp, config.DISABLE_SIGNUP === 'true')
setMetadata(login.metadata.PasswordValidations, PASSWORD_REQUIREMENTS[config.PASSWORD_STRICTNESS ?? 'none'])
setMetadata(presentation.metadata.FilesURL, config.FILES_URL)
setMetadata(presentation.metadata.UploadURL, config.UPLOAD_URL)
+6 -1
View File
@@ -58,6 +58,11 @@
"Skip": "Přeskočit",
"SignUpCompleted": "Registrace dokončena",
"StartUsingHuly": "Začněte používat Huly",
"PasswordMinLength": "{count, plural, =1 {Heslo musí být alespoň # znak dlouhé} other {Heslo musí být alespoň # znaků dlouhé}}",
"PasswordMinSpecialChars": "{count, plural, =1 {Heslo musí obsahovat alespoň # speciální znak} other {Heslo musí obsahovat alespoň # speciálních znaků}}",
"PasswordMinDigits": "{count, plural, =1 {Heslo musí obsahovat alespoň # číslo} other {Heslo musí obsahovat alespoň # čísel}}",
"PasswordMinUpperChars": "{count, plural, =1 {Heslo musí obsahovat alespoň # velké písmeno} other {Heslo musí obsahovat alespoň # velkých písmen}}",
"PasswordMinLowerChars": "{count, plural, =1 {Heslo musí obsahovat alespoň # malé písmeno} other {Heslo musí obsahovat alespoň # malých písmen}}",
"WorkspaceIsArchived": "Pracovní prostor je archivován kvůli nečinnosti. Kontaktujte nás prosím pro obnovení..."
}
}
}
+5
View File
@@ -58,6 +58,11 @@
"Skip": "Überspringen",
"SignUpCompleted": "Registrierung abgeschlossen",
"StartUsingHuly": "Starten Sie mit Huly",
"PasswordMinLength": "{count, plural, =1 {Das Passwort muss mindestens # Zeichen lang sein} other {Das Passwort muss mindestens # Zeichen lang sein}}",
"PasswordMinSpecialChars": "{count, plural, =1 {Das Passwort muss mindestens # Sonderzeichen enthalten} other {Das Passwort muss mindestens # Sonderzeichen enthalten}}",
"PasswordMinDigits": "{count, plural, =1 {Das Passwort muss mindestens # Zahl enthalten} other {Das Passwort muss mindestens # Zahlen enthalten}}",
"PasswordMinUpperChars": "{count, plural, =1 {Das Passwort muss mindestens # Großbuchstaben enthalten} other {Das Passwort muss mindestens # Großbuchstaben enthalten}}",
"PasswordMinLowerChars": "{count, plural, =1 {Das Passwort muss mindestens # Kleinbuchstaben enthalten} other {Das Passwort muss mindestens # Kleinbuchstaben enthalten}}",
"WorkspaceArchivedDesc": "Workspace wurde wegen Inaktivität archiviert. Bitte kontaktieren Sie uns zur Wiederherstellung..."
}
}
+5
View File
@@ -58,6 +58,11 @@
"Skip": "Skip",
"SignUpCompleted": "Sign up completed",
"StartUsingHuly": "Start using Huly",
"PasswordMinLength": "{count, plural, =1 {Password must be at least # character long} other {Password must be at least # characters long}}",
"PasswordMinSpecialChars": "{count, plural, =1 {Password must contain at least # special character} other {Password must contain at least # special characters}}",
"PasswordMinDigits": "{count, plural, =1 {Password must contain at least # number} other {Password must contain at least # numbers}}",
"PasswordMinUpperChars": "{count, plural, =1 {Password must contain at least # uppercase letter} other {Password must contain at least # uppercase letters}}",
"PasswordMinLowerChars": "{count, plural, =1 {Password must contain at least # lowercase letter} other {Password must contain at least # lowercase letters}}",
"WorkspaceArchivedDesc": "Workspace is archived because of being unused, Please contact us to restore..."
}
}
+6 -1
View File
@@ -58,6 +58,11 @@
"Skip": "Saltar",
"SignUpCompleted": "Registro completado",
"StartUsingHuly": "Comienza a usar Huly",
"PasswordMinLength": "{count, plural, =1 {La contraseña debe tener al menos # carácter} other {La contraseña debe tener al menos # caracteres}}",
"PasswordMinSpecialChars": "{count, plural, =1 {La contraseña debe contener al menos # carácter especial} other {La contraseña debe contener al menos # caracteres especiales}}",
"PasswordMinDigits": "{count, plural, =1 {La contraseña debe contener al menos # número} other {La contraseña debe contener al menos # números}}",
"PasswordMinUpperChars": "{count, plural, =1 {La contraseña debe contener al menos # letra mayúscula} other {La contraseña debe contener al menos # letras mayúsculas}}",
"PasswordMinLowerChars": "{count, plural, =1 {La contraseña debe contener al menos # letra minúscula} other {La contraseña debe contener al menos # letras minúsculas}}",
"WorkspaceArchivedDesc": "El espacio de trabajo está archivado por no estar en uso, por favor contáctenos para restaurarlo..."
}
}
}
+7 -2
View File
@@ -58,6 +58,11 @@
"Skip": "Passer",
"SignUpCompleted": "Inscription terminée",
"StartUsingHuly": "Commencez à utiliser Huly",
"WorkspaceArchivedDesc": "L'espace de travail est archivé en raison de son inactivité, veuillez nous contacter pour le restaurer..."
"PasswordMinLength": "{count, plural, =1 {Le mot de passe doit contenir au moins # caractère} other {Le mot de passe doit contenir au moins # caractères}}",
"PasswordMinSpecialChars": "{count, plural, =1 {Le mot de passe doit contenir au moins # caractère spécial} other {Le mot de passe doit contenir au moins # caractères spéciaux}}",
"PasswordMinDigits": "{count, plural, =1 {Le mot de passe doit contenir au moins # chiffre} other {Le mot de passe doit contenir au moins # chiffres}}",
"PasswordMinUpperChars": "{count, plural, =1 {Le mot de passe doit contenir au moins # lettre majuscule} other {Le mot de passe doit contenir au moins # lettres majuscules}}",
"PasswordMinLowerChars": "{count, plural, =1 {Le mot de passe doit contenir au moins # lettre minuscule} other {Le mot de passe doit contenir au moins # lettres minuscules}}",
"WorkspaceArchivedDesc": "L'espace de travail est archivé en raison de son inactivité, veuillez nous contacter pour le restaurer..."
}
}
}
+5
View File
@@ -58,6 +58,11 @@
"Skip": "Salta",
"SignUpCompleted": "Registrazione completata",
"StartUsingHuly": "Inizia a usare Huly",
"PasswordMinLength": "{count, plural, =1 {La password deve contenere almeno # carattere} other {La password deve contenere almeno # caratteri}}",
"PasswordMinSpecialChars": "{count, plural, =1 {La password deve contenere almeno # carattere speciale} other {La password deve contenere almeno # caratteri speciali}}",
"PasswordMinDigits": "{count, plural, =1 {La password deve contenere almeno # numero} other {La password deve contenere almeno # numeri}}",
"PasswordMinUpperChars": "{count, plural, =1 {La password deve contenere almeno # lettera maiuscola} other {La password deve contenere almeno # lettere maiuscole}}",
"PasswordMinLowerChars": "{count, plural, =1 {La password deve contenere almeno # lettera minuscola} other {La password deve contenere almeno # lettere minuscole}}",
"WorkspaceArchivedDesc": "Il workspace è stato archiviato perché inutilizzato. Si prega di contattarci per ripristinarlo..."
}
}
+6 -1
View File
@@ -58,6 +58,11 @@
"Skip": "Saltar",
"SignUpCompleted": "Registo concluído",
"StartUsingHuly": "Começar a usar Huly",
"PasswordMinLength": "{count, plural, =1 {A senha deve ter pelo menos # caractere} other {A senha deve ter pelo menos # caracteres}}",
"PasswordMinSpecialChars": "{count, plural, =1 {A senha deve conter pelo menos # caractere especial} other {A senha deve conter pelo menos # caracteres especiais}}",
"PasswordMinDigits": "{count, plural, =1 {A senha deve conter pelo menos # número} other {A senha deve conter pelo menos # números}}",
"PasswordMinUpperChars": "{count, plural, =1 {A senha deve conter pelo menos # letra maiúscula} other {A senha deve conter pelo menos # letras maiúsculas}}",
"PasswordMinLowerChars": "{count, plural, =1 {A senha deve conter pelo menos # letra minúscula} other {A senha deve conter pelo menos # letras minúsculas}}",
"WorkspaceArchivedDesc": "O espaço de trabalho está arquivado por estar inativo, por favor, entre em contato conosco para restaurá-lo..."
}
}
}
+5
View File
@@ -58,6 +58,11 @@
"Skip": "Пропустить",
"SignUpCompleted": "Регистрация завершена",
"StartUsingHuly": "Начать использовать Huly",
"PasswordMinLength": "{count, plural, =1 {Пароль должен содержать минимум # символ} other {Пароль должен содержать минимум # символов}}",
"PasswordMinSpecialChars": "{count, plural, =1 {Пароль должен содержать минимум # специальный символ} other {Пароль должен содержать минимум # специальных символов}}",
"PasswordMinDigits": "{count, plural, =1 {Пароль должен содержать минимум # цифру} other {Пароль должен содержать минимум # цифр}}",
"PasswordMinUpperChars": "{count, plural, =1 {Пароль должен содержать минимум # заглавную букву} other {Пароль должен содержать минимум # заглавных букв}}",
"PasswordMinLowerChars": "{count, plural, =1 {Пароль должен содержать минимум # строчную букву} other {Пароль должен содержать минимум # строчных букв}}",
"WorkspaceArchivedDesc": "Рабочее пространство архивировано из-за неиспользования, пожалуйста, свяжитесь с нами для восстановления..."
}
}
+5
View File
@@ -58,6 +58,11 @@
"Skip": "跳过",
"SignUpCompleted": "注册完成",
"StartUsingHuly": "开始使用 Huly",
"PasswordMinLength": "{count, plural, =1 {密码至少需要 # 个字符} other {密码至少需要 # 个字符}}",
"PasswordMinSpecialChars": "{count, plural, =1 {密码至少需要 # 个特殊字符} other {密码至少需要 # 个特殊字符}}",
"PasswordMinDigits": "{count, plural, =1 {密码至少需要 # 个数字} other {密码至少需要 # 个数字}}",
"PasswordMinUpperChars": "{count, plural, =1 {密码至少需要 # 个大写字母} other {密码至少需要 # 个大写字母}}",
"PasswordMinLowerChars": "{count, plural, =1 {密码至少需要 # 个小写字母} other {密码至少需要 # 个小写字母}}",
"WorkspaceArchivedDesc": "工作区因未使用而归档,请与我们联系以恢复..."
}
}
@@ -22,25 +22,12 @@
import { onMount } from 'svelte'
import { BottomAction } from '..'
import { makeSequential } from '../mutex'
import type { Field } from '../types'
import login from '../plugin'
import BottomActionComponent from './BottomAction.svelte'
import Providers from './Providers.svelte'
import Tabs from './Tabs.svelte'
interface Field {
id?: string
name: string
i18n: IntlString
password?: boolean
optional?: boolean
short?: boolean
rules?: {
rule: RegExp
notMatch: boolean
ruleDescr: IntlString
}[]
}
interface Action {
i18n: IntlString
func: () => Promise<void>
@@ -85,8 +72,11 @@
}
if (f.rules !== undefined) {
for (const rule of f.rules) {
if (rule.rule.test(v) === rule.notMatch) {
status = new Status(Severity.INFO, rule.ruleDescr, {})
const isValid =
typeof rule.rule === 'function' ? rule.rule(v) !== rule.notMatch : rule.rule.test(v) !== rule.notMatch
if (!isValid) {
status = new Status(Severity.INFO, rule.ruleDescr, rule.ruleDescrParams ?? {})
return false
}
}
@@ -18,11 +18,19 @@
import presentation from '@hcengineering/presentation'
import { getCurrentLocation, setMetadataLocalStorage } from '@hcengineering/ui'
import login from '../plugin'
import type { Field } from '../types'
import { goTo, restorePassword } from '../utils'
import Form from './Form.svelte'
import { getPasswordValidationRules } from '../validations'
const fields = [
{ id: 'new-password', name: 'password', i18n: login.string.Password, password: true },
const fields: Array<Field> = [
{
id: 'new-password',
name: 'password',
i18n: login.string.Password,
password: true,
rules: getPasswordValidationRules()
},
{ id: 'new-password', name: 'password2', i18n: login.string.PasswordRepeat, password: true }
]
@@ -18,16 +18,24 @@
import presentation from '@hcengineering/presentation'
import { setMetadataLocalStorage } from '@hcengineering/ui'
import login from '../plugin'
import { getPasswordValidationRules } from '../validations'
import { goTo, signUp } from '../utils'
import Form from './Form.svelte'
import type { Field } from '../types'
export let signUpDisabled = false
const fields = [
const fields: Array<Field> = [
{ id: 'given-name', name: 'first', i18n: login.string.FirstName, short: true },
{ id: 'family-name', name: 'last', i18n: login.string.LastName, short: true },
{ id: 'email', name: 'username', i18n: login.string.Email },
{ id: 'new-password', name: 'password', i18n: login.string.Password, password: true },
{
id: 'new-password',
name: 'password',
i18n: login.string.Password,
password: true,
rules: getPasswordValidationRules()
},
{ id: 'new-password', name: 'password2', i18n: login.string.PasswordRepeat, password: true }
]
+31
View File
@@ -0,0 +1,31 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { type IntlString } from '@hcengineering/platform'
export interface Field {
id?: string
name: string
i18n: IntlString
password?: boolean
optional?: boolean
short?: boolean
rules?: Array<{
rule: RegExp | ((value: string) => boolean)
notMatch: boolean
ruleDescr: IntlString | IntlString<Record<string, any>>
ruleDescrParams?: Record<string, any>
}>
}
@@ -0,0 +1,64 @@
//
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { getMetadata } from '@hcengineering/platform'
import login from './plugin'
import type { Field } from './types'
/**
* Generates password validation rules based on metadata.
*
* @returns {Field['rules']} An array of password validation rules.
*/
export function getPasswordValidationRules (): Field['rules'] {
const passwordValidations = getMetadata(login.metadata.PasswordValidations)
const passwordValidationRules: Field['rules'] = [
{
rule: (value: string) => value.length >= (passwordValidations?.MinLength ?? 0),
notMatch: false,
ruleDescr: login.string.PasswordMinLength,
ruleDescrParams: { count: passwordValidations?.MinLength }
},
{
rule: (value: string) =>
(value.match(/[^a-zA-Z0-9]/g)?.length ?? 0) >= (passwordValidations?.MinSpecialChars ?? 0),
notMatch: false,
ruleDescr: login.string.PasswordMinSpecialChars,
ruleDescrParams: { count: passwordValidations?.MinSpecialChars }
},
{
rule: (value: string) => (value.match(/[0-9]/g)?.length ?? 0) >= (passwordValidations?.MinDigits ?? 0),
notMatch: false,
ruleDescr: login.string.PasswordMinDigits,
ruleDescrParams: { count: passwordValidations?.MinDigits }
},
{
rule: (value: string) => (value.match(/[A-Z]/g)?.length ?? 0) >= (passwordValidations?.MinUpperChars ?? 0),
notMatch: false,
ruleDescr: login.string.PasswordMinUpperChars,
ruleDescrParams: { count: passwordValidations?.MinUpperChars }
},
{
rule: (value: string) => (value.match(/[a-z]/g)?.length ?? 0) >= (passwordValidations?.MinLowerChars ?? 0),
notMatch: false,
ruleDescr: login.string.PasswordMinLowerChars,
ruleDescrParams: { count: passwordValidations?.MinLowerChars }
}
]
return passwordValidationRules
}
+13 -1
View File
@@ -72,7 +72,14 @@ export default plugin(loginId, {
LoginEndpoint: '' as Metadata<string>,
LoginEmail: '' as Metadata<string>,
DisableSignUp: '' as Metadata<boolean>,
TransactorOverride: '' as Metadata<string>
TransactorOverride: '' as Metadata<string>,
PasswordValidations: '' as Metadata<{
MinLength: number
MinSpecialChars: number
MinDigits: number
MinUpperChars: number
MinLowerChars: number
}>
},
component: {
LoginApp: '' as AnyComponent,
@@ -86,6 +93,11 @@ export default plugin(loginId, {
EmailMask: '' as IntlString,
NoLimit: '' as IntlString,
InviteLimit: '' as IntlString,
PasswordMinLength: '' as IntlString<{ count: number }>,
PasswordMinSpecialChars: '' as IntlString<{ count: number }>,
PasswordMinDigits: '' as IntlString<{ count: number }>,
PasswordMinUpperChars: '' as IntlString<{ count: number }>,
PasswordMinLowerChars: '' as IntlString<{ count: number }>,
WorkspaceArchived: '' as IntlString,
WorkspaceArchivedDesc: '' as IntlString
},