diff --git a/dev/prod/public/config.json b/dev/prod/public/config.json index d2f1c4d3a0..fee334f040 100644 --- a/dev/prod/public/config.json +++ b/dev/prod/public/config.json @@ -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" -} \ No newline at end of file + "STATS_URL": "http://localhost:4900", + "PASSWORD_STRICTNESS": "none" +} diff --git a/dev/prod/src/platform.ts b/dev/prod/src/platform.ts index cfee6d5396..12482bbaf6 100644 --- a/dev/prod/src/platform.ts +++ b/dev/prod/src/platform.ts @@ -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 = { 'dev-worker-local': '/config-worker-local.json', } +const PASSWORD_REQUIREMENTS : Record> = { + 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) diff --git a/plugins/login-assets/lang/cs.json b/plugins/login-assets/lang/cs.json index d611b97dba..9babbe924d 100644 --- a/plugins/login-assets/lang/cs.json +++ b/plugins/login-assets/lang/cs.json @@ -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í..." } -} \ No newline at end of file +} diff --git a/plugins/login-assets/lang/de.json b/plugins/login-assets/lang/de.json index 93efdd2b0e..9d81a390b4 100644 --- a/plugins/login-assets/lang/de.json +++ b/plugins/login-assets/lang/de.json @@ -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..." } } diff --git a/plugins/login-assets/lang/en.json b/plugins/login-assets/lang/en.json index 64d0f6106b..5906d2ef6a 100644 --- a/plugins/login-assets/lang/en.json +++ b/plugins/login-assets/lang/en.json @@ -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..." } } diff --git a/plugins/login-assets/lang/es.json b/plugins/login-assets/lang/es.json index b723fc6b0b..b05bfece33 100644 --- a/plugins/login-assets/lang/es.json +++ b/plugins/login-assets/lang/es.json @@ -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..." } -} \ No newline at end of file +} diff --git a/plugins/login-assets/lang/fr.json b/plugins/login-assets/lang/fr.json index 7df88de47b..34e57a088c 100644 --- a/plugins/login-assets/lang/fr.json +++ b/plugins/login-assets/lang/fr.json @@ -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..." } -} \ No newline at end of file +} diff --git a/plugins/login-assets/lang/it.json b/plugins/login-assets/lang/it.json index d7c8c39e1a..34b72b087f 100644 --- a/plugins/login-assets/lang/it.json +++ b/plugins/login-assets/lang/it.json @@ -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..." } } diff --git a/plugins/login-assets/lang/pt.json b/plugins/login-assets/lang/pt.json index 82cbcec990..b8df9dab0e 100644 --- a/plugins/login-assets/lang/pt.json +++ b/plugins/login-assets/lang/pt.json @@ -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..." } -} \ No newline at end of file +} diff --git a/plugins/login-assets/lang/ru.json b/plugins/login-assets/lang/ru.json index 53a29a7703..0143a02b8e 100644 --- a/plugins/login-assets/lang/ru.json +++ b/plugins/login-assets/lang/ru.json @@ -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": "Рабочее пространство архивировано из-за неиспользования, пожалуйста, свяжитесь с нами для восстановления..." } } diff --git a/plugins/login-assets/lang/zh.json b/plugins/login-assets/lang/zh.json index 8670f49447..4d6f23bfd0 100644 --- a/plugins/login-assets/lang/zh.json +++ b/plugins/login-assets/lang/zh.json @@ -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": "工作区因未使用而归档,请与我们联系以恢复..." } } diff --git a/plugins/login-resources/src/components/Form.svelte b/plugins/login-resources/src/components/Form.svelte index d040397339..ba27a04d98 100644 --- a/plugins/login-resources/src/components/Form.svelte +++ b/plugins/login-resources/src/components/Form.svelte @@ -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 @@ -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 } } diff --git a/plugins/login-resources/src/components/PasswordRestore.svelte b/plugins/login-resources/src/components/PasswordRestore.svelte index bed8c838b1..fd47f6fdeb 100644 --- a/plugins/login-resources/src/components/PasswordRestore.svelte +++ b/plugins/login-resources/src/components/PasswordRestore.svelte @@ -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 = [ + { + id: 'new-password', + name: 'password', + i18n: login.string.Password, + password: true, + rules: getPasswordValidationRules() + }, { id: 'new-password', name: 'password2', i18n: login.string.PasswordRepeat, password: true } ] diff --git a/plugins/login-resources/src/components/SignupForm.svelte b/plugins/login-resources/src/components/SignupForm.svelte index 57268ccaf3..3c6bf122d2 100644 --- a/plugins/login-resources/src/components/SignupForm.svelte +++ b/plugins/login-resources/src/components/SignupForm.svelte @@ -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 = [ { 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 } ] diff --git a/plugins/login-resources/src/types.ts b/plugins/login-resources/src/types.ts new file mode 100644 index 0000000000..135d08da7e --- /dev/null +++ b/plugins/login-resources/src/types.ts @@ -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> + ruleDescrParams?: Record + }> +} diff --git a/plugins/login-resources/src/validations.ts b/plugins/login-resources/src/validations.ts new file mode 100644 index 0000000000..7a8d5a8182 --- /dev/null +++ b/plugins/login-resources/src/validations.ts @@ -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 +} diff --git a/plugins/login/src/index.ts b/plugins/login/src/index.ts index 65b9464394..eb2adf2772 100644 --- a/plugins/login/src/index.ts +++ b/plugins/login/src/index.ts @@ -72,7 +72,14 @@ export default plugin(loginId, { LoginEndpoint: '' as Metadata, LoginEmail: '' as Metadata, DisableSignUp: '' as Metadata, - TransactorOverride: '' as Metadata + TransactorOverride: '' as Metadata, + 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 },