fix: Create Human V1 (#9425)

# Which Problems Are Solved
- Correctly load Avatar on first load

# How the Problems Are Solved
- The Avatar issue was mostly due to how we resolved the current user, I
changed this behaviour

# Additional Changes
- Removed V2 create human code till seperate page is finished
- Remove Console Use V2 API feature flag from features page (till new
page is added)

# Additional Context
- Partially fixes #9382
- This will get implemented next week
https://github.com/zitadel/zitadel/issues/9382#issuecomment-2681347477
This commit is contained in:
Ramon
2025-02-27 09:31:48 +01:00
committed by GitHub
parent 3c471944c2
commit 83614562a2
9 changed files with 42 additions and 303 deletions
@@ -2,24 +2,12 @@ import { Location } from '@angular/common';
import { Component, DestroyRef, ElementRef, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormControl, ValidatorFn } from '@angular/forms';
import { Router } from '@angular/router';
import {
debounceTime,
defer,
of,
Observable,
shareReplay,
firstValueFrom,
forkJoin,
ObservedValueOf,
EMPTY,
ReplaySubject,
} from 'rxjs';
import { debounceTime, defer, of, Observable, shareReplay, forkJoin, ObservedValueOf, EMPTY, ReplaySubject } from 'rxjs';
import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
import { Gender } from 'src/app/proto/generated/zitadel/user_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { CountryCallingCodesService, CountryPhoneCode } from 'src/app/services/country-calling-codes.service';
import { formatPhone } from 'src/app/utils/formatPhone';
import {
@@ -34,15 +22,8 @@ import {
requiredValidator,
} from 'src/app/modules/form-field/validators/validators';
import { LanguagesService } from 'src/app/services/languages.service';
import { UserService } from 'src/app/services/user.service';
import { AddHumanUserRequest } from 'src/app/proto/generated/zitadel/management_pb';
import { AddHumanUserRequestSchema } from '@zitadel/proto/zitadel/user/v2/user_service_pb';
import { create } from '@bufbuild/protobuf';
import { SetHumanPhoneSchema } from '@zitadel/proto/zitadel/user/v2/phone_pb';
import { PasswordSchema } from '@zitadel/proto/zitadel/user/v2/password_pb';
import { SetHumanEmailSchema } from '@zitadel/proto/zitadel/user/v2/email_pb';
import { FeatureService } from 'src/app/services/feature.service';
import { catchError, filter, map, startWith, timeout } from 'rxjs/operators';
import { catchError, map, startWith } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
@@ -51,27 +32,26 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
styleUrls: ['./user-create.component.scss'],
})
export class UserCreateComponent implements OnInit {
public readonly genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED];
public selected: CountryPhoneCode | undefined = {
protected readonly genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED];
protected selected: CountryPhoneCode | undefined = {
countryCallingCode: '1',
countryCode: 'US',
countryName: 'United States of America',
};
public readonly countryPhoneCodes: CountryPhoneCode[];
protected readonly countryPhoneCodes: CountryPhoneCode[];
public loading = false;
protected loading = false;
private readonly suffix$ = new ReplaySubject<HTMLSpanElement>(1);
@ViewChild('suffix') public set suffix(suffix: ElementRef<HTMLSpanElement>) {
this.suffix$.next(suffix.nativeElement);
}
public usePassword: boolean = false;
protected readonly useV2Api$: Observable<boolean>;
protected usePassword: boolean = false;
protected readonly envSuffix$: Observable<string>;
protected readonly userForm: ReturnType<typeof this.buildUserForm>;
protected readonly pwdForm$: ReturnType<typeof this.buildPwdForm>;
protected readonly passwordComplexityPolicy$: Observable<PasswordComplexityPolicy.AsObject>;
protected readonly passwordComplexityPolicy$: Observable<PasswordComplexityPolicy.AsObject | undefined>;
protected readonly suffixPadding$: Observable<string>;
constructor(
@@ -79,15 +59,12 @@ export class UserCreateComponent implements OnInit {
private readonly toast: ToastService,
private readonly fb: FormBuilder,
private readonly mgmtService: ManagementService,
private readonly userService: UserService,
public readonly langSvc: LanguagesService,
private readonly featureService: FeatureService,
private readonly destroyRef: DestroyRef,
private readonly breadcrumbService: BreadcrumbService,
protected readonly location: Location,
protected readonly langSvc: LanguagesService,
countryCallingCodesService: CountryCallingCodesService,
) {
this.useV2Api$ = this.getUseV2Api().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
this.envSuffix$ = this.getEnvSuffix();
this.suffixPadding$ = this.getSuffixPadding();
this.passwordComplexityPolicy$ = this.getPasswordComplexityPolicy().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
@@ -99,8 +76,6 @@ export class UserCreateComponent implements OnInit {
}
ngOnInit(): void {
// already start loading if we should use v2 api
this.useV2Api$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe();
this.watchPhoneChanges();
this.breadcrumbService.setBreadcrumb([
@@ -111,14 +86,6 @@ export class UserCreateComponent implements OnInit {
]);
}
private getUseV2Api(): Observable<boolean> {
return defer(() => this.featureService.getInstanceFeatures(true)).pipe(
map((features) => !!features.getConsoleUseV2UserApi()?.getEnabled()),
timeout(1000),
catchError(() => of(false)),
);
}
private getEnvSuffix() {
const domainPolicy$ = defer(() => this.mgmtService.getDomainPolicy());
const orgDomains$ = defer(() => this.mgmtService.listOrgDomains());
@@ -147,7 +114,6 @@ export class UserCreateComponent implements OnInit {
private getPasswordComplexityPolicy() {
return defer(() => this.mgmtService.getPasswordComplexityPolicy()).pipe(
map(({ policy }) => policy),
filter(Boolean),
catchError((error) => {
this.toast.showError(error);
return EMPTY;
@@ -155,7 +121,7 @@ export class UserCreateComponent implements OnInit {
);
}
public buildUserForm() {
private buildUserForm() {
return this.fb.group({
email: new FormControl('', { nonNullable: true, validators: [requiredValidator, emailValidator] }),
userName: new FormControl('', { nonNullable: true, validators: [requiredValidator, minLengthValidator(2)] }),
@@ -166,29 +132,26 @@ export class UserCreateComponent implements OnInit {
preferredLanguage: new FormControl('', { nonNullable: true }),
phone: new FormControl('', { nonNullable: true, validators: [phoneValidator] }),
emailVerified: new FormControl(false, { nonNullable: true }),
sendEmail: new FormControl(true, { nonNullable: true }),
phoneVerified: new FormControl(false, { nonNullable: true }),
sendSms: new FormControl(true, { nonNullable: true }),
});
}
public buildPwdForm(passwordComplexityPolicy$: Observable<PasswordComplexityPolicy.AsObject>) {
private buildPwdForm(passwordComplexityPolicy$: Observable<PasswordComplexityPolicy.AsObject | undefined>) {
return passwordComplexityPolicy$.pipe(
map((policy) => {
const validators: [ValidatorFn] = [requiredValidator];
if (policy.minLength) {
const validators: ValidatorFn[] = [requiredValidator];
if (policy?.minLength) {
validators.push(minLengthValidator(policy.minLength));
}
if (policy.hasLowercase) {
if (policy?.hasLowercase) {
validators.push(containsLowerCaseValidator);
}
if (policy.hasUppercase) {
if (policy?.hasUppercase) {
validators.push(containsUpperCaseValidator);
}
if (policy.hasNumber) {
if (policy?.hasNumber) {
validators.push(containsNumberValidator);
}
if (policy.hasSymbol) {
if (policy?.hasSymbol) {
validators.push(containsSymbolValidator);
}
return this.fb.group({
@@ -214,15 +177,7 @@ export class UserCreateComponent implements OnInit {
});
}
public async createUser(pwdForm: ObservedValueOf<typeof this.pwdForm$>): Promise<void> {
if (await firstValueFrom(this.useV2Api$)) {
await this.createUserV2(pwdForm);
} else {
await this.createUserV1(pwdForm);
}
}
public async createUserV1(pwdForm: ObservedValueOf<typeof this.pwdForm$>): Promise<void> {
protected async createUser(pwdForm: ObservedValueOf<typeof this.pwdForm$>): Promise<void> {
this.loading = true;
const controls = this.userForm.controls;
@@ -258,107 +213,6 @@ export class UserCreateComponent implements OnInit {
try {
const data = await this.mgmtService.addHumanUser(humanReq);
this.toast.showInfo('USER.TOAST.CREATED', true);
this.router.navigate(['users', data.userId], { queryParams: { new: true } }).then();
} catch (error) {
this.toast.showError(error);
} finally {
this.loading = false;
}
}
public async createUserV2(pwdForm: ObservedValueOf<typeof this.pwdForm$>): Promise<void> {
this.loading = true;
const controls = this.userForm.controls;
const humanReq = create(AddHumanUserRequestSchema, {
username: controls.userName.value,
profile: {
givenName: controls.firstName.value,
familyName: controls.lastName.value,
nickName: controls.nickName.value,
preferredLanguage: controls.preferredLanguage.value,
// the enum numbers of v1 gender are the same as v2 gender
gender: controls.gender.value as unknown as any,
},
});
if (this.usePassword) {
const password = create(PasswordSchema, { password: pwdForm.controls.password.value });
humanReq.passwordType = { case: 'password', value: password };
}
if (controls.emailVerified.value) {
humanReq.email = create(SetHumanEmailSchema, {
email: controls.email.value,
verification: {
value: true,
case: 'isVerified',
},
});
} else {
if (controls.sendEmail.value) {
humanReq.email = create(SetHumanEmailSchema, {
email: controls.email.value,
verification: {
case: 'sendCode',
value: {},
},
});
} else {
humanReq.email = create(SetHumanEmailSchema, {
email: controls.email.value,
verification: {
value: false,
case: 'isVerified',
},
});
}
}
const phoneNumber = formatPhone(controls.phone.value);
if (phoneNumber) {
const country = phoneNumber.country;
this.selected = this.countryPhoneCodes.find((code) => code.countryCode === country);
if (controls.phoneVerified.value) {
humanReq.phone = create(SetHumanPhoneSchema, {
phone: phoneNumber.phone,
verification: {
case: 'isVerified',
value: true,
},
});
} else {
if (controls.sendSms.value) {
humanReq.phone = create(SetHumanPhoneSchema, {
phone: phoneNumber.phone,
verification: {
case: 'sendCode',
value: {},
},
});
} else {
humanReq.phone = create(SetHumanPhoneSchema, {
phone: phoneNumber.phone,
verification: {
case: 'isVerified',
value: false,
},
});
}
}
}
try {
const data = await this.userService.addHumanUser(humanReq);
if (this.sendEmailAfterCreation) {
await this.userService.passwordReset({
userId: data.userId,
medium: {
case: 'sendLink',
value: {},
},
});
}
this.toast.showInfo('USER.TOAST.CREATED', true);
await this.router.navigate(['users', data.userId], { queryParams: { new: true } });
} catch (error) {
this.toast.showError(error);
@@ -367,14 +221,7 @@ export class UserCreateComponent implements OnInit {
}
}
public get sendEmailAfterCreation() {
const controls = this.userForm.controls;
const sendEmailAfterCreationIsAOption = controls.emailVerified.value && !this.usePassword;
return sendEmailAfterCreationIsAOption && controls.sendEmail.value;
}
public setCountryCallingCode(): void {
protected setCountryCallingCode(): void {
let value = this.userForm.controls.phone.value;
this.countryPhoneCodes.forEach((code) => (value = value.replace(`+${code.countryCallingCode}`, '')));
value = value.trim();
@@ -382,7 +229,7 @@ export class UserCreateComponent implements OnInit {
this.userForm.controls.phone.setValue('+' + this.selected?.countryCallingCode + ' ' + value);
}
public compareCountries(i1: CountryPhoneCode, i2: CountryPhoneCode) {
protected compareCountries(i1: CountryPhoneCode, i2: CountryPhoneCode) {
return (
i1 &&
i2 &&