mirror of
https://github.com/zitadel/zitadel.git
synced 2025-02-28 21:47:23 +00:00
wip
This commit is contained in:
parent
83614562a2
commit
73f5b52ab8
@ -15,7 +15,7 @@
|
||||
diameter="20"
|
||||
[color]="currentError ? 'warn' : 'valid'"
|
||||
mode="determinate"
|
||||
[value]="(password?.value?.length / policy.minLength) * 100"
|
||||
[value]="(password?.value?.length / minLength) * 100"
|
||||
>
|
||||
</mat-progress-spinner>
|
||||
</div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { AbstractControl } from '@angular/forms';
|
||||
import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
||||
import { PasswordComplexityPolicy as BufPasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-password-complexity-view',
|
||||
@ -9,5 +10,9 @@ import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy
|
||||
})
|
||||
export class PasswordComplexityViewComponent {
|
||||
@Input() public password: AbstractControl | null = null;
|
||||
@Input() public policy!: PasswordComplexityPolicy.AsObject;
|
||||
@Input() public policy!: PasswordComplexityPolicy.AsObject | BufPasswordComplexityPolicy;
|
||||
|
||||
protected get minLength() {
|
||||
return Number(this.policy.minLength);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,92 @@
|
||||
<cnsl-create-layout
|
||||
title="{{ 'USER.CREATE.TITLE' | translate }}"
|
||||
[createSteps]="1"
|
||||
[currentCreateStep]="1"
|
||||
(closed)="location.back()"
|
||||
>
|
||||
<mat-progress-bar *ngIf="loading()" color="primary" mode="indeterminate"></mat-progress-bar>
|
||||
<form *ngIf="authenticationFactor$ | async as authenticationFactor" class="form-grid" [formGroup]="userForm" (ngSubmit)="createUserV2(authenticationFactor)">
|
||||
<cnsl-form-field class="email">
|
||||
<cnsl-label>{{ 'USER.PROFILE.EMAIL' | translate }}</cnsl-label>
|
||||
<input cnslInput matRipple formControlName="email" required />
|
||||
</cnsl-form-field>
|
||||
<div class="emailVerified">
|
||||
<mat-checkbox *ngIf="authenticationFactor.factor !== 'invitation'" formControlName="emailVerified">
|
||||
Mark Email as Verified
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<cnsl-form-field class="givenName">
|
||||
<cnsl-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="givenName" required />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="familyName">
|
||||
<cnsl-label>{{ 'USER.PROFILE.LASTNAME' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="familyName" required />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="nickName">
|
||||
<cnsl-label>{{ 'USER.PROFILE.NICKNAME' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="nickName" />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="username">
|
||||
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}</cnsl-label>
|
||||
<input
|
||||
cnslInput
|
||||
formControlName="username"
|
||||
required
|
||||
/>
|
||||
</cnsl-form-field>
|
||||
|
||||
|
||||
<div class="authenticationFactor">
|
||||
<div [class.authenticationFactorIntroductionDone]="authenticationFactor.factor !== 'none'">
|
||||
Would you like to set up authentication methods now?
|
||||
<span>You don't have to do this now, you can do it later</span>
|
||||
<button mat-raised-button color="primary" (click)="$event.preventDefault(); userForm.controls.authenticationFactor.setValue('invitation')">
|
||||
Setup
|
||||
</button>
|
||||
</div>
|
||||
<mat-radio-group *ngIf="authenticationFactor.factor !== 'none'" aria-label="Select an option" formControlName="authenticationFactor">
|
||||
<mat-radio-button [value]="'invitation'">
|
||||
Invitation
|
||||
</mat-radio-button>
|
||||
<mat-radio-button [value]="'initialPassword'">
|
||||
Initial password
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<form *ngIf="authenticationFactor.factor === 'initialPassword'" [formGroup]="authenticationFactor.form">
|
||||
<cnsl-password-complexity-view
|
||||
class="complexity-view"
|
||||
[policy]="authenticationFactor.policy"
|
||||
[password]="authenticationFactor.form.controls.password"
|
||||
>
|
||||
</cnsl-password-complexity-view>
|
||||
<cnsl-form-field>
|
||||
<cnsl-label>{{ 'USER.PASSWORD.NEWINITIAL' | translate }}</cnsl-label>
|
||||
<input cnslInput autocomplete="off" name="firstpassword" formControlName="password" type="password" />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field>
|
||||
<cnsl-label>{{ 'USER.PASSWORD.CONFIRMINITIAL' | translate }}</cnsl-label>
|
||||
<input
|
||||
cnslInput
|
||||
autocomplete="off"
|
||||
name="confirmPassword"
|
||||
formControlName="confirmPassword"
|
||||
type="password"
|
||||
/>
|
||||
</cnsl-form-field>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button
|
||||
data-e2e="create-button"
|
||||
color="primary"
|
||||
[disabled]="userForm.invalid || (authenticationFactor.factor === 'initialPassword' && authenticationFactor.form.invalid)"
|
||||
type="submit"
|
||||
class="create-button"
|
||||
mat-raised-button
|
||||
>
|
||||
{{ 'ACTIONS.CREATE' | translate }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</cnsl-create-layout>
|
@ -0,0 +1,44 @@
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas:
|
||||
"email email"
|
||||
"emailVerified emailVerified"
|
||||
"givenName familyName"
|
||||
"nickName username"
|
||||
"authenticationFactor authenticationFactor";
|
||||
;
|
||||
}
|
||||
|
||||
.email {
|
||||
grid-area: email;
|
||||
}
|
||||
|
||||
.emailVerified {
|
||||
grid-area: emailVerified;
|
||||
}
|
||||
|
||||
.givenName {
|
||||
grid-area: givenName;
|
||||
}
|
||||
|
||||
.familyName {
|
||||
grid-area: familyName;
|
||||
}
|
||||
|
||||
.nickName {
|
||||
grid-area: nickName;
|
||||
}
|
||||
|
||||
.username {
|
||||
grid-area: username;
|
||||
}
|
||||
|
||||
.authenticationFactor {
|
||||
grid-area: authenticationFactor;
|
||||
}
|
||||
|
||||
.authenticationFactorIntroductionDone {
|
||||
background: red;
|
||||
}
|
@ -0,0 +1,190 @@
|
||||
import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, signal } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { FormBuilder, FormControl, ValidatorFn } from '@angular/forms';
|
||||
import { UserService } from 'src/app/services/user.service';
|
||||
import { LanguagesService } from 'src/app/services/languages.service';
|
||||
import { FeatureService } from 'src/app/services/feature.service';
|
||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||
import { Location } from '@angular/common';
|
||||
import {
|
||||
containsLowerCaseValidator,
|
||||
containsNumberValidator,
|
||||
containsSymbolValidator,
|
||||
containsUpperCaseValidator,
|
||||
emailValidator,
|
||||
minLengthValidator,
|
||||
passwordConfirmValidator,
|
||||
requiredValidator,
|
||||
} from 'src/app/modules/form-field/validators/validators';
|
||||
import { NewMgmtService } from 'src/app/services/new-mgmt.service';
|
||||
import { defaultIfEmpty, defer, EMPTY, Observable, shareReplay } from 'rxjs';
|
||||
import { catchError, filter, map, startWith } from 'rxjs/operators';
|
||||
import { PasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { AddHumanUserRequestSchema } from '@zitadel/proto/zitadel/user/v2/user_service_pb';
|
||||
import { withLatestFromSynchronousFix } from '../../../../utils/withLatestFromSynchronousFix';
|
||||
|
||||
type PwdForm = ReturnType<UserCreateV2Component['buildPwdForm']>;
|
||||
type AuthenticationFactor =
|
||||
| { factor: 'none' }
|
||||
| { factor: 'initialPassword'; form: PwdForm; policy: PasswordComplexityPolicy }
|
||||
| { factor: 'invitation' };
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-user-create-v2',
|
||||
templateUrl: './user-create-v2.component.html',
|
||||
styleUrls: ['./user-create-v2.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UserCreateV2Component implements OnInit {
|
||||
protected readonly userForm: ReturnType<typeof this.buildUserForm>;
|
||||
private readonly passwordComplexityPolicy$: Observable<PasswordComplexityPolicy>;
|
||||
protected readonly authenticationFactor$: Observable<AuthenticationFactor>;
|
||||
|
||||
protected loading = signal(false);
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private readonly toast: ToastService,
|
||||
private readonly fb: FormBuilder,
|
||||
private readonly userService: UserService,
|
||||
public readonly langSvc: LanguagesService,
|
||||
private readonly featureService: FeatureService,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly breadcrumbService: BreadcrumbService,
|
||||
private readonly newMgmtService: NewMgmtService,
|
||||
protected readonly location: Location,
|
||||
) {
|
||||
this.userForm = this.buildUserForm();
|
||||
|
||||
this.passwordComplexityPolicy$ = this.getPasswordComplexityPolicy().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.authenticationFactor$ = this.getAuthenticationFactor(this.userForm, this.passwordComplexityPolicy$);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.breadcrumbService.setBreadcrumb([
|
||||
new Breadcrumb({
|
||||
type: BreadcrumbType.ORG,
|
||||
routerLink: ['/org'],
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
private getAuthenticationFactor(
|
||||
userForm: typeof this.userForm,
|
||||
passwordComplexityPolicy$: Observable<PasswordComplexityPolicy>,
|
||||
): Observable<AuthenticationFactor> {
|
||||
const pwdForm$ = passwordComplexityPolicy$.pipe(
|
||||
defaultIfEmpty(undefined),
|
||||
map((policy) => this.buildPwdForm(policy)),
|
||||
);
|
||||
|
||||
return userForm.controls.authenticationFactor.valueChanges.pipe(
|
||||
startWith(userForm.controls.authenticationFactor.value),
|
||||
withLatestFromSynchronousFix(pwdForm$, passwordComplexityPolicy$),
|
||||
map(([factor, form, policy]) => {
|
||||
if (factor === 'initialPassword') {
|
||||
return { factor, form, policy };
|
||||
}
|
||||
return { factor };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public buildUserForm() {
|
||||
return this.fb.group({
|
||||
email: new FormControl('', { nonNullable: true, validators: [requiredValidator, emailValidator] }),
|
||||
username: new FormControl('', { nonNullable: true, validators: [requiredValidator, minLengthValidator(2)] }),
|
||||
givenName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
familyName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
nickName: new FormControl('', { nonNullable: true }),
|
||||
emailVerified: new FormControl(false, { nonNullable: true }),
|
||||
authenticationFactor: new FormControl<AuthenticationFactor['factor']>('none', { nonNullable: true }),
|
||||
});
|
||||
}
|
||||
|
||||
private buildPwdForm(policy: PasswordComplexityPolicy | undefined) {
|
||||
const validators: ValidatorFn[] = [requiredValidator];
|
||||
if (policy?.minLength) {
|
||||
validators.push(minLengthValidator(Number(policy.minLength)));
|
||||
}
|
||||
if (policy?.hasLowercase) {
|
||||
validators.push(containsLowerCaseValidator);
|
||||
}
|
||||
if (policy?.hasUppercase) {
|
||||
validators.push(containsUpperCaseValidator);
|
||||
}
|
||||
if (policy?.hasNumber) {
|
||||
validators.push(containsNumberValidator);
|
||||
}
|
||||
if (policy?.hasSymbol) {
|
||||
validators.push(containsSymbolValidator);
|
||||
}
|
||||
return this.fb.group({
|
||||
password: new FormControl('', { nonNullable: true, validators }),
|
||||
confirmPassword: new FormControl('', {
|
||||
nonNullable: true,
|
||||
validators: [requiredValidator, passwordConfirmValidator()],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
private getPasswordComplexityPolicy() {
|
||||
return defer(() => this.newMgmtService.getPasswordComplexityPolicy()).pipe(
|
||||
map(({ policy }) => policy),
|
||||
filter(Boolean),
|
||||
catchError((error) => {
|
||||
this.toast.showError(error);
|
||||
return EMPTY;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
protected async createUserV2(authenticationFactor: AuthenticationFactor) {
|
||||
this.loading.set(true);
|
||||
const userValues = this.userForm.getRawValue();
|
||||
const humanReq: MessageInitShape<typeof AddHumanUserRequestSchema> = {
|
||||
username: userValues.username,
|
||||
profile: {
|
||||
givenName: userValues.givenName,
|
||||
familyName: userValues.familyName,
|
||||
nickName: userValues.nickName,
|
||||
},
|
||||
email: {
|
||||
email: userValues.email,
|
||||
verification: {
|
||||
case: 'isVerified',
|
||||
value: userValues.emailVerified,
|
||||
},
|
||||
},
|
||||
};
|
||||
if (authenticationFactor.factor === 'initialPassword') {
|
||||
const { password } = authenticationFactor.form.getRawValue();
|
||||
humanReq.passwordType = {
|
||||
case: 'password',
|
||||
value: {
|
||||
password,
|
||||
},
|
||||
};
|
||||
}
|
||||
try {
|
||||
const resp = await this.userService.addHumanUser(humanReq);
|
||||
if (authenticationFactor.factor === 'invitation') {
|
||||
await this.userService.createInviteCode({
|
||||
userId: resp.userId,
|
||||
verification: {
|
||||
case: 'sendCode',
|
||||
value: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
this.toast.showInfo('USER.TOAST.CREATED', true);
|
||||
await this.router.navigate(['users', resp.userId], { queryParams: { new: true } });
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
} finally {
|
||||
this.loading.set(false);
|
||||
}
|
||||
}
|
||||
}
|
@ -19,9 +19,11 @@ import { PasswordComplexityViewModule } from 'src/app/modules/password-complexit
|
||||
import { CountryCallingCodesService } from 'src/app/services/country-calling-codes.service';
|
||||
import { UserCreateRoutingModule } from './user-create-routing.module';
|
||||
import { UserCreateComponent } from './user-create.component';
|
||||
import { UserCreateV2Component } from './user-create-v2/user-create-v2.component';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
|
||||
@NgModule({
|
||||
declarations: [UserCreateComponent],
|
||||
declarations: [UserCreateComponent, UserCreateV2Component],
|
||||
providers: [CountryCallingCodesService],
|
||||
imports: [
|
||||
UserCreateRoutingModule,
|
||||
@ -42,6 +44,7 @@ import { UserCreateComponent } from './user-create.component';
|
||||
DetailLayoutModule,
|
||||
InputModule,
|
||||
MatRippleModule,
|
||||
MatRadioModule,
|
||||
],
|
||||
})
|
||||
export default class UserCreateModule {}
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
GenerateMachineSecretResponse,
|
||||
GetLoginPolicyRequestSchema,
|
||||
GetLoginPolicyResponse,
|
||||
GetPasswordComplexityPolicyResponse,
|
||||
ListUserMetadataRequestSchema,
|
||||
ListUserMetadataResponse,
|
||||
RemoveMachineSecretRequestSchema,
|
||||
@ -89,4 +90,8 @@ export class NewMgmtService {
|
||||
): Promise<RemoveUserMetadataResponse> {
|
||||
return this.grpcService.mgmtNew.removeUserMetadata(create(RemoveUserMetadataRequestSchema, req));
|
||||
}
|
||||
|
||||
public getPasswordComplexityPolicy(): Promise<GetPasswordComplexityPolicyResponse> {
|
||||
return this.grpcService.mgmtNew.getPasswordComplexityPolicy({});
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user