diff --git a/console/src/app/modules/password-complexity-view/password-complexity-view.component.html b/console/src/app/modules/password-complexity-view/password-complexity-view.component.html index 8ad4998ff0..72415228af 100644 --- a/console/src/app/modules/password-complexity-view/password-complexity-view.component.html +++ b/console/src/app/modules/password-complexity-view/password-complexity-view.component.html @@ -1,42 +1,42 @@
-
+
- + - -
- - -
-
- - - + +
+ + +
+
+ + + - {{ 'USER.PASSWORD.MINLENGTHERROR' | translate: {value: policy?.minLength} }} - ({{password?.value?.length}}/{{ policy.minLength}}) - -
-
- - - {{ 'USER.VALIDATION.SYMBOLERROR' | translate }} -
-
- - - {{ 'USER.VALIDATION.NUMBERERROR' | translate }} -
-
- - - {{ 'USER.VALIDATION.UPPERCASEMISSING' | translate }} -
-
- - - {{ 'USER.VALIDATION.LOWERCASEMISSING' | translate }} -
+ {{ 'USER.PASSWORD.MINLENGTHERROR' | translate: {value: policy?.minLength} }} + ({{password?.value?.length}}/{{ policy.minLength}}) + +
+
+ + + {{ 'USER.VALIDATION.SYMBOLERROR' | translate }} +
+
+ + + {{ 'USER.VALIDATION.NUMBERERROR' | translate }} +
+
+ + + {{ 'USER.VALIDATION.UPPERCASEMISSING' | translate }} +
+
+ + + {{ 'USER.VALIDATION.LOWERCASEMISSING' | translate }} +
\ No newline at end of file diff --git a/console/src/app/modules/password-complexity-view/password-complexity-view.component.scss b/console/src/app/modules/password-complexity-view/password-complexity-view.component.scss index 0531f30b51..3c39c7fe78 100644 --- a/console/src/app/modules/password-complexity-view/password-complexity-view.component.scss +++ b/console/src/app/modules/password-complexity-view/password-complexity-view.component.scss @@ -1,4 +1,3 @@ - .validation-col { display: flex; flex-wrap: wrap; @@ -6,7 +5,7 @@ width: 100%; &.between { - margin: 0 .5rem; + margin: 0 0.5rem; } .val { @@ -20,20 +19,20 @@ color: var(--grey); } - .sp-wrapper { + .complexity-sp-wrapper { height: 20px; width: 20px; margin-right: 1rem; i { - font-size: .9rem; + font-size: 0.9rem; position: absolute; top: 50%; left: 50%; transform: translateX(-50%) translateY(-50%); } - .spinner[color='valid'] { + .complexity-spinner[color='valid'] { color: #56a392; } } diff --git a/console/src/app/pages/users/user-create/user-create.component.html b/console/src/app/pages/users/user-create/user-create.component.html index 07a4b526f0..c4f433ebdc 100644 --- a/console/src/app/pages/users/user-create/user-create.component.html +++ b/console/src/app/pages/users/user-create/user-create.component.html @@ -53,14 +53,46 @@
- + {{'USER.LOGINMETHODS.EMAIL.ISVERIFIED' | translate}} + + {{'ORG.PAGES.USEPASSWORD' | translate}} + - {{'USER.LOGINMETHODS.EMAIL.ISVERIFIEDDESC' | translate}} + {{'USER.CREATE.INITMAILDESCRIPTION' | translate}}
+
+ + + +
+ + {{ 'USER.PASSWORD.NEWINITIAL' | translate }} + + + + {{ 'USER.VALIDATION.REQUIRED' | translate }} + + + + + {{ 'USER.PASSWORD.CONFIRMINITIAL' | translate }} + + + + {{ 'USER.VALIDATION.REQUIRED' | translate }} + + + {{ 'USER.PASSWORD.NOTEQUAL' | translate }} + + +
+
+

{{ 'USER.CREATE.GENDERLANGSECTION' | translate }}

@@ -97,7 +129,8 @@
-
diff --git a/console/src/app/pages/users/user-create/user-create.component.scss b/console/src/app/pages/users/user-create/user-create.component.scss index bf2b7ab98c..adaa3bc52b 100644 --- a/console/src/app/pages/users/user-create/user-create.component.scss +++ b/console/src/app/pages/users/user-create/user-create.component.scss @@ -33,9 +33,29 @@ margin: 0 0.5rem; } - .email-is-verified { + .email-is-verified, + .use-password-block { margin: 0 0.5rem; flex-basis: 100%; margin-top: 1.5rem; + + .block-checkbox { + display: block; + margin: 0.25rem 0; + } + } +} + +.pwd-section { + margin: 0 0.5rem; + + .section { + padding: 0.5rem 0; + } + + .user-create-pwd-form { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 1rem; } } diff --git a/console/src/app/pages/users/user-create/user-create.component.ts b/console/src/app/pages/users/user-create/user-create.component.ts index da83ca691e..196b1b1276 100644 --- a/console/src/app/pages/users/user-create/user-create.component.ts +++ b/console/src/app/pages/users/user-create/user-create.component.ts @@ -1,15 +1,38 @@ import { ChangeDetectorRef, Component, OnDestroy, ViewChild } from '@angular/core'; -import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { AbstractControl, FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import parsePhoneNumber from 'libphonenumber-js'; import { Subject } from 'rxjs'; import { debounceTime, takeUntil } from 'rxjs/operators'; import { AddHumanUserRequest } from 'src/app/proto/generated/zitadel/management_pb'; import { Domain } from 'src/app/proto/generated/zitadel/org_pb'; +import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; import { Gender } from 'src/app/proto/generated/zitadel/user_pb'; import { ManagementService } from 'src/app/services/mgmt.service'; import { ToastService } from 'src/app/services/toast.service'; +import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator } from '../../validators'; + +function passwordConfirmValidator(c: AbstractControl): any { + if (!c.parent || !c) { + return; + } + const pwd = c.parent.get('password'); + const cpwd = c.parent.get('confirmPassword'); + + if (!pwd || !cpwd) { + return; + } + if (pwd.value !== cpwd.value) { + return { + invalid: true, + notequal: { + valid: false, + }, + }; + } +} + @Component({ selector: 'cnsl-user-create', templateUrl: './user-create.component.html', @@ -20,6 +43,8 @@ export class UserCreateComponent implements OnDestroy { public genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED]; public languages: string[] = ['de', 'en']; public userForm!: FormGroup; + public pwdForm!: FormGroup; + public envSuffixLabel: string = ''; private destroyed$: Subject = new Subject(); @@ -28,6 +53,8 @@ export class UserCreateComponent implements OnDestroy { @ViewChild('suffix') public suffix!: any; private primaryDomain!: Domain.AsObject; + public usePassword: boolean = false; + public policy!: PasswordComplexityPolicy.AsObject; constructor( private router: Router, @@ -79,6 +106,37 @@ export class UserCreateComponent implements OnDestroy { isVerified: [false, []], }); + const validators: Validators[] = [Validators.required]; + + this.mgmtService.getPasswordComplexityPolicy().then((data) => { + if (data.policy) { + this.policy = data.policy; + + if (this.policy.minLength) { + validators.push(Validators.minLength(this.policy.minLength)); + } + if (this.policy.hasLowercase) { + validators.push(lowerCaseValidator); + } + if (this.policy.hasUppercase) { + validators.push(upperCaseValidator); + } + if (this.policy.hasNumber) { + validators.push(numberValidator); + } + if (this.policy.hasSymbol) { + validators.push(symbolValidator); + } + const pwdValidators = [...validators] as ValidatorFn[]; + const confirmPwdValidators = [...validators, passwordConfirmValidator] as ValidatorFn[]; + + this.pwdForm = this.fb.group({ + password: ['', pwdValidators], + confirmPassword: ['', confirmPwdValidators], + }); + } + }); + this.userForm.controls['phone'].valueChanges.pipe(takeUntil(this.destroyed$), debounceTime(300)).subscribe((value) => { const phoneNumber = parsePhoneNumber(value ?? '', 'CH'); if (phoneNumber) { @@ -112,6 +170,10 @@ export class UserCreateComponent implements OnDestroy { emailreq.setIsEmailVerified(this.isVerified?.value); humanReq.setEmail(emailreq); + if (this.usePassword && this.password?.value) { + humanReq.setInitialPassword(this.password.value); + } + if (this.phone && this.phone.value) { humanReq.setPhone(new AddHumanUserRequest.Phone().setPhone(this.phone.value)); } @@ -162,6 +224,12 @@ export class UserCreateComponent implements OnDestroy { return this.userForm.get('phone'); } + public get password(): AbstractControl | null { + return this.pwdForm.get('password'); + } + public get confirmPassword(): AbstractControl | null { + return this.pwdForm.get('confirmPassword'); + } private envSuffix(): string { if (this.userLoginMustBeDomain && this.primaryDomain?.domainName) { return `@${this.primaryDomain.domainName}`; diff --git a/console/src/app/pages/users/user-create/user-create.module.ts b/console/src/app/pages/users/user-create/user-create.module.ts index afcf51088f..4af4988cdf 100644 --- a/console/src/app/pages/users/user-create/user-create.module.ts +++ b/console/src/app/pages/users/user-create/user-create.module.ts @@ -13,6 +13,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module'; import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module'; import { InputModule } from 'src/app/modules/input/input.module'; +import { PasswordComplexityViewModule } from 'src/app/modules/password-complexity-view/password-complexity-view.module'; import { UserCreateRoutingModule } from './user-create-routing.module'; import { UserCreateComponent } from './user-create.component'; @@ -29,6 +30,7 @@ import { UserCreateComponent } from './user-create.component'; MatIconModule, MatProgressSpinnerModule, MatProgressBarModule, + PasswordComplexityViewModule, MatCheckboxModule, MatTooltipModule, TranslateModule, diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 52b228cb97..14a5282dad 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -325,7 +325,8 @@ "GENDERLANGSECTION": "Geschlecht und Sprache", "PHONESECTION": "Telefonnummer", "PASSWORDSECTION": "Setze ein initiales Passwort.", - "ADDRESSANDPHONESECTION": "Telefonnummer" + "ADDRESSANDPHONESECTION": "Telefonnummer", + "INITMAILDESCRIPTION": "Wenn beide Optionen ausgewählt sind, wird keine E-Mail zur Initialisierung gesendet. Wenn nur eine der Optionen ausgewählt ist, wird eine E-Mail zur Aufforderung der Bereitstellung oder Verifikation der Daten gesendet." }, "CODEDIALOG": { "TITLE": "Telefonnummer verifizieren", @@ -395,12 +396,14 @@ }, "PASSWORD": { "TITLE": "Passwort", - "DESCRIPTION": "Gebe das neue Password unter Einhaltung der Richtlinie für die Komplexität ein.", + "DESCRIPTION": "Gebe das neue PassworT unter Einhaltung der Richtlinie für die Komplexität ein.", "OLD": "Aktuelles Passwort", "NEW": "Neues Passwort", "CONFIRM": "Neues Passwort wiederholen", + "NEWINITIAL": "Passwort", + "CONFIRMINITIAL": "PassworT wiederholen", "RESET": "Passwort zurücksetzen", - "SET": "Password neu setzen", + "SET": "PassworT neu setzen", "RESENDNOTIFICATION": "Link für das Zurücksetzen des Passworts senden", "REQUIRED": "Bitte prüfe, dass alle notwendigen Felder ausgefüllt sind.", "MINLENGTHERROR": "Das Passwort muss mindestens {{value}} Zeichen lang sein.", @@ -420,7 +423,7 @@ "TITLE": "E-Mail", "VALID": "Validiert", "ISVERIFIED": "Email Verifiziert", - "ISVERIFIEDDESC": "Wenn die Email als verifiziert angegeben wird, wird keine Initialisierungsmail versendet.", + "ISVERIFIEDDESC": "Wenn die Email als verifiziert angegeben wird, wird keine Verifikationsmail an den Benutzer versendet.", "RESEND": "Verifikationsmail erneut senden", "EDITTITLE": "Email ändern", "EDITDESC": "Geben Sie die neue Email in dem darunterliegenden Feld ein!" @@ -665,7 +668,8 @@ "SELECTORGTOOLTIP": "Diese Organisation auswählen", "PRIMARYDOMAIN": "Primäre Domain", "STATE": "Status", - "USEPASSWORD": "Initiales Password setzen" + "USEPASSWORD": "Initiales Password setzen", + "USEPASSWORDDESC": "Der Nutzer muss das Password bei der Initalisierung nicht setzen." }, "DOMAINS": { "NEW": "Domain hinzufügen", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 7e35a0f6c0..a5707fac59 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -325,7 +325,8 @@ "GENDERLANGSECTION": "Gender and Language", "PHONESECTION": "Phonenumbers", "PASSWORDSECTION": "Initial Password", - "ADDRESSANDPHONESECTION": "Phonenumber" + "ADDRESSANDPHONESECTION": "Phonenumber", + "INITMAILDESCRIPTION": "If both options are selected, no email for initialization will be sent. If only one of the options is selected, a mail to provide / verify the data will be sent." }, "CODEDIALOG": { "TITLE": "Verify Phone Number", @@ -399,6 +400,8 @@ "OLD": "Current Password", "NEW": "New Password", "CONFIRM": "Confirm New Password", + "NEWINITIAL": "Password", + "CONFIRMINITIAL": "Confirm Password", "RESET": "Reset Current Password", "SET": "Set New Password", "RESENDNOTIFICATION": "Resend Password Reset Link", @@ -420,7 +423,7 @@ "TITLE": "E-mail", "VALID": "validated", "ISVERIFIED": "Email Verified", - "ISVERIFIEDDESC": "If the email is indicated as verified, no initialization email will be sent.", + "ISVERIFIEDDESC": "If the email is indicated as verified, no email verification request will be made.", "RESEND": "Resend Verification E-mail", "EDITTITLE": "Change Email", "EDITDESC": "Enter the new email in the field below." @@ -665,7 +668,8 @@ "SELECTORGTOOLTIP": "Select this organisation.", "PRIMARYDOMAIN": "Primary Domain", "STATE": "State", - "USEPASSWORD": "Set Initial Password" + "USEPASSWORD": "Set Initial Password", + "USEPASSWORDDESC": "The user does not have to set the password during initialization." }, "DOMAINS": { "NEW": "Add Domain", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 5ef36acaed..8babf3a037 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -325,7 +325,8 @@ "GENDERLANGSECTION": "Genere e linguaggio", "PHONESECTION": "Phonenumbers", "PASSWORDSECTION": "Password iniziale", - "ADDRESSANDPHONESECTION": "Numero di telefono" + "ADDRESSANDPHONESECTION": "Numero di telefono", + "INITMAILDESCRIPTION": "Se vengono selezionate entrambe le opzioni, non verrà inviata alcuna e-mail per l'inizializzazione. Se solo una delle opzioni viene selezionata, verrà inviata una mail per fornire/verificare i dati." }, "CODEDIALOG": { "TITLE": "Verificare il numero di telefono", @@ -399,6 +400,8 @@ "OLD": "Password attuale", "NEW": "Nuova password", "CONFIRM": "Conferma la nuova password", + "NEWINITIAL": "Password", + "CONFIRMINITIAL": "Conferma password", "RESET": "Ripristina la password attuale", "SET": "Imposta nuova password", "RESENDNOTIFICATION": "Reinvia il link per la reimpostazione della password", @@ -420,7 +423,7 @@ "TITLE": "E-mail", "VALID": "convalidato", "ISVERIFIED": "Email Verificato", - "ISVERIFIEDDESC": "Se l'email viene indicata come verificata, non verrà inviata alcuna email di inizializzazione.", + "ISVERIFIEDDESC": "Se l'email viene indicata come verificata, non verrà inviata alcuna email di verificazione.", "RESEND": "Invia di nuovo l'e-mail di verifica", "EDITTITLE": "Cambiare l'e-mail", "EDITDESC": "Inserisci la nuova email nel campo sottostante." @@ -665,7 +668,8 @@ "SELECTORGTOOLTIP": "Seleziona questa organizzazione.", "PRIMARYDOMAIN": "Dominio primario", "STATE": "Stato", - "USEPASSWORD": "Imposta la password iniziale" + "USEPASSWORD": "Imposta la password iniziale", + "USEPASSWORDDESC": "L'utente non deve impostare la password durante l'inizializzazione." }, "DOMAINS": { "NEW": "Aggiungi dominio",