fix(console): change username abort dialog and set email as verified #10803 #12146 (#12155)

<!--
Please inform yourself about the contribution guidelines on submitting a
PR here:
https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr.
Take note of how PR/commit titles should be written and replace the
template texts in the sections below. Don't remove any of the sections.
It is important that the commit history clearly shows what is changed
and why.
Important: By submitting a contribution you agree to the terms from our
Licensing Policy as described here:
https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions.
-->

# Which Problems Are Solved
Cancelling the username change dialog in the console leads to an error
toast #10803.
Setting a users email as verified only worked if the email was also
changed #12146.

# How the Problems Are Solved

Correctly destructure the abort response of the edit dialog.
Use the update user endpoint instead of the set email endpoint to change
the email of the user.

# Additional Changes
- Moved repetitive dialog code from rxjs to async await
- Removed useless tests
- Improved imports
- Simplified phone number parsing and replaced deprecated function call

# Additional Context
Both changes were done in the same pr because the changes are in the
same set of files.
- Closes #10803
- Closes #12146

---------

Co-authored-by: Max Peintner <[email protected]>
This commit is contained in:
Ramon
2026-07-31 09:34:06 +02:00
committed by GitHub
co-authored by Max Peintner
parent f34cc45eac
commit d725b9a0ed
7 changed files with 362 additions and 473 deletions
@@ -1,24 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { WarnDialogComponent } from './warn-dialog.component';
describe('WarnDialogComponent', () => {
let component: WarnDialogComponent;
let fixture: ComponentFixture<WarnDialogComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [WarnDialogComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(WarnDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -16,11 +16,11 @@ import { User } 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 { NewOrganizationService } from '../../../services/new-organization.service';
import { NewOrganizationService } from 'src/app/services/new-organization.service';
import { injectMutation } from '@tanstack/angular-query-experimental';
import { Organization, OrganizationState } from '@zitadel/proto/zitadel/org/v2/org_pb';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { GrpcAuthService } from '../../../services/grpc-auth.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { Org } from '@zitadel/proto/zitadel/org_pb';
@Component({
@@ -33,7 +33,7 @@ export class OrgDetailComponent implements OnInit {
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public OrganizationState = OrganizationState;
public ChangeType: any = ChangeType;
public ChangeType = ChangeType;
public metadata: Metadata.AsObject[] = [];
public loadingMetadata: boolean = true;
@@ -4,7 +4,7 @@ import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Buffer } from 'buffer';
import { defer, EMPTY, Observable, of, shareReplay, Subject, switchMap, take } from 'rxjs';
import { defer, EMPTY, lastValueFrom, Observable, of, shareReplay, Subject, switchMap } from 'rxjs';
import { ChangeType } from 'src/app/modules/changes/changes.component';
import { phoneValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators';
import { InfoDialogComponent, InfoDialogData, InfoDialogResult } from 'src/app/modules/info-dialog/info-dialog.component';
@@ -32,7 +32,6 @@ import { NewMgmtService } from 'src/app/services/new-mgmt.service';
import { Metadata } from '@zitadel/proto/zitadel/metadata_pb';
import { UserService } from 'src/app/services/user.service';
import { LoginPolicy } from '@zitadel/proto/zitadel/policy_pb';
import { query } from '@angular/animations';
import { QueryClient } from '@tanstack/angular-query-experimental';
type MetadataQuery =
@@ -183,42 +182,40 @@ export class AuthUserDetailComponent implements OnInit {
});
}
protected changeUsername(user: User): void {
protected async changeUsername(user: User) {
const data = {
confirmKey: 'ACTIONS.CHANGE' as const,
cancelKey: 'ACTIONS.CANCEL' as const,
labelKey: 'ACTIONS.NEWVALUE' as const,
titleKey: 'USER.PROFILE.CHANGEUSERNAME_TITLE' as const,
descriptionKey: 'USER.PROFILE.CHANGEUSERNAME_DESC' as const,
confirmKey: 'ACTIONS.CHANGE',
cancelKey: 'ACTIONS.CANCEL',
labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.PROFILE.CHANGEUSERNAME_TITLE',
descriptionKey: 'USER.PROFILE.CHANGEUSERNAME_DESC',
value: user.username,
};
const dialogRef = this.dialog.open<EditDialogComponent, typeof data, EditDialogResult>(EditDialogComponent, {
type: EditDialogType.GENERIC,
} as const satisfies EditDialogData;
const dialogRef = this.dialog.open<EditDialogComponent, EditDialogData, EditDialogResult>(EditDialogComponent, {
data,
width: '400px',
});
dialogRef
.afterClosed()
.pipe(
map((value) => value?.value),
filter(Boolean),
filter((value) => user.username != value),
switchMap((username) => this.userService.updateUser({ userId: user.userId, username })),
)
.subscribe({
next: () => {
this.toast.showInfo('USER.TOAST.USERNAMECHANGED', true);
this.invalidateUser().then();
},
error: (error) => {
this.toast.showError(error);
},
});
const { value: username } = (await lastValueFrom(dialogRef.afterClosed())) ?? {};
if (!username || user.username === username) {
// no changes made
return;
}
try {
await this.userService.updateUser({ userId: user.userId, username });
this.toast.showInfo('USER.TOAST.USERNAMECHANGED', true);
await this.invalidateUser();
} catch (error) {
this.toast.showError(error);
}
}
public saveProfile(user: User, profile: HumanProfile): void {
this.userService
.updateUser({
public async saveProfile(user: User, profile: HumanProfile) {
try {
await this.userService.updateUser({
userId: user.userId,
profile: {
givenName: profile.givenName,
@@ -228,30 +225,26 @@ export class AuthUserDetailComponent implements OnInit {
preferredLanguage: profile.preferredLanguage,
gender: profile.gender,
},
})
.then(() => {
this.toast.showInfo('USER.TOAST.SAVED', true);
this.invalidateUser().then();
})
.catch((error) => {
this.toast.showError(error);
});
this.toast.showInfo('USER.TOAST.SAVED', true);
await this.invalidateUser();
} catch (error) {
this.toast.showError(error);
}
}
public enteredPhoneCode(code: string): void {
this.newAuthService
.verifyMyPhone(code)
.then(() => {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
this.invalidateUser().then();
this.promptSetupforSMSOTP();
})
.catch((error) => {
this.toast.showError(error);
});
public async enteredPhoneCode(code: string) {
try {
await this.newAuthService.verifyMyPhone(code);
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
await this.invalidateUser();
await this.promptSetupforSMSOTP();
} catch (error) {
this.toast.showError(error);
}
}
public promptSetupforSMSOTP(): void {
public async promptSetupforSMSOTP() {
const dialogRef = this.dialog.open<InfoDialogComponent, InfoDialogData, InfoDialogResult>(InfoDialogComponent, {
data: {
confirmKey: 'ACTIONS.CONTINUE',
@@ -262,71 +255,63 @@ export class AuthUserDetailComponent implements OnInit {
width: '400px',
});
dialogRef
.afterClosed()
.pipe(
filter(Boolean),
switchMap(() => this.newAuthService.addMyAuthFactorOTPSMS()),
switchMap(() => this.translate.get('USER.MFA.OTPSMSSUCCESS').pipe(take(1))),
)
.subscribe({
next: (msg) => this.toast.showInfo(msg),
error: (err) => this.toast.showError(err),
});
const confirmed = await lastValueFrom(dialogRef.afterClosed());
if (!confirmed) {
return;
}
try {
await this.newAuthService.addMyAuthFactorOTPSMS();
this.toast.showInfo('USER.MFA.OTPSMSSUCCESS', true);
} catch (error) {
this.toast.showError(error);
}
}
public changedLanguage(language: string): void {
this.translate.use(language);
}
public resendEmailVerification(user: User): void {
this.newMgmtService
.resendHumanEmailVerification(user.userId)
.then(() => {
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
this.invalidateUser().then();
})
.catch((error) => {
this.toast.showError(error);
});
}
public resendPhoneVerification(user: User): void {
this.newMgmtService
.resendHumanPhoneVerification(user.userId)
.then(() => {
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
this.invalidateUser().then();
})
.catch((error) => {
this.toast.showError(error);
});
}
public deletePhone(user: User): void {
this.userService
.removePhone(user.userId)
.then(() => {
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
this.invalidateUser().then();
})
.catch((error) => {
this.toast.showError(error);
});
}
public openEditDialog(user: UserWithHumanType, type: EditDialogType): void {
switch (type) {
case EditDialogType.PHONE:
this.openEditPhoneDialog(user);
return;
case EditDialogType.EMAIL:
this.openEditEmailDialog(user);
return;
public async resendEmailVerification(user: User) {
try {
await this.newMgmtService.resendHumanEmailVerification(user.userId);
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
await this.invalidateUser();
} catch (error) {
this.toast.showError(error);
}
}
private openEditEmailDialog(user: UserWithHumanType) {
public async resendPhoneVerification(user: User) {
try {
await this.newMgmtService.resendHumanPhoneVerification(user.userId);
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
await this.invalidateUser();
} catch (error) {
this.toast.showError(error);
}
}
public async deletePhone(user: User) {
try {
await this.userService.removePhone(user.userId);
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
await this.invalidateUser();
} catch (error) {
this.toast.showError(error);
}
}
public async openEditDialog(user: UserWithHumanType, type: EditDialogType) {
switch (type) {
case EditDialogType.PHONE:
return this.openEditPhoneDialog(user);
case EditDialogType.EMAIL:
return this.openEditEmailDialog(user);
}
}
private async openEditEmailDialog(user: UserWithHumanType) {
const data: EditDialogData = {
confirmKey: 'ACTIONS.SAVE',
cancelKey: 'ACTIONS.CANCEL',
@@ -335,35 +320,32 @@ export class AuthUserDetailComponent implements OnInit {
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
value: user.type.value?.email?.email,
type: EditDialogType.EMAIL,
} as const;
} as const satisfies EditDialogData;
const dialogRefEmail = this.dialog.open<EditDialogComponent, EditDialogData, EditDialogResult>(EditDialogComponent, {
data,
width: '400px',
});
dialogRefEmail
.afterClosed()
.pipe(
filter((resp): resp is Required<EditDialogResult> => !!resp?.value),
switchMap(({ value, isVerified }) =>
this.userService.setEmail({
userId: user.userId,
email: value,
verification: isVerified ? { case: 'isVerified', value: isVerified } : { case: undefined },
}),
),
)
.subscribe({
next: () => {
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
this.invalidateUser().then();
},
error: (error) => this.toast.showError(error),
const { value, isVerified } = (await lastValueFrom(dialogRefEmail.afterClosed())) ?? {};
if (!value) {
return;
}
try {
await this.userService.setEmail({
userId: user.userId,
email: value,
verification: isVerified ? { case: 'isVerified', value: isVerified } : { case: undefined },
});
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
await this.invalidateUser();
} catch (error) {
this.toast.showError(error);
}
}
private openEditPhoneDialog(user: UserWithHumanType) {
private async openEditPhoneDialog(user: UserWithHumanType) {
const data = {
confirmKey: 'ACTIONS.SAVE',
cancelKey: 'ACTIONS.CANCEL',
@@ -372,32 +354,30 @@ export class AuthUserDetailComponent implements OnInit {
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
value: user.type.value.phone?.phone,
type: EditDialogType.PHONE,
validator: Validators.compose([phoneValidator, requiredValidator]),
};
const dialogRefPhone = this.dialog.open<EditDialogComponent, typeof data, { value: string; isVerified: boolean }>(
EditDialogComponent,
{ data, width: '400px' },
);
validator: Validators.compose([phoneValidator, requiredValidator]) ?? undefined,
} as const satisfies EditDialogData;
dialogRefPhone
.afterClosed()
.pipe(
map((resp) => formatPhone(resp?.value)),
filter(Boolean),
switchMap(({ phone }) => this.userService.setPhone({ userId: user.userId, phone })),
)
.subscribe({
next: () => {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
this.invalidateUser().then();
},
error: (error) => {
this.toast.showError(error);
},
});
const dialogRefPhone = this.dialog.open<EditDialogComponent, EditDialogData, EditDialogResult>(EditDialogComponent, {
data,
width: '400px',
});
const { value } = (await lastValueFrom(dialogRefPhone.afterClosed())) ?? {};
const formatted = formatPhone(value);
if (!formatted) {
return;
}
try {
await this.userService.setPhone({ userId: user.userId, phone: formatted.phone });
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
await this.invalidateUser();
} catch (error) {
this.toast.showError(error);
}
}
public deleteUser(user: User): void {
public async deleteUser(user: User) {
const data = {
confirmKey: 'USER.DIALOG.DELETE_BTN',
cancelKey: 'ACTIONS.CANCEL',
@@ -410,29 +390,28 @@ export class AuthUserDetailComponent implements OnInit {
width: '400px',
});
dialogRef
.afterClosed()
.pipe(
filter(Boolean),
switchMap(() => this.userService.deleteUser(user.userId)),
)
.subscribe({
next: () => {
this.toast.showInfo('USER.PAGES.DELETEACCOUNT_SUCCESS', true);
this.auth.signout();
},
error: (error) => this.toast.showError(error),
});
const confirmed = await lastValueFrom(dialogRef.afterClosed());
if (!confirmed) {
return;
}
try {
await this.userService.deleteUser(user.userId);
this.toast.showInfo('USER.PAGES.DELETEACCOUNT_SUCCESS', true);
this.auth.signout();
} catch (error) {
this.toast.showError(error);
}
}
public editMetadata(user: User, metadata: Metadata[]): void {
public async editMetadata(user: User, metadata: Metadata[]) {
const setFcn = (key: string, value: string) =>
this.newMgmtService.setUserMetadata({
key,
value: Buffer.from(value),
id: user.userId,
});
const removeFcn = (key: string): Promise<any> => this.newMgmtService.removeUserMetadata({ key, id: user.userId });
const removeFcn = (key: string) => this.newMgmtService.removeUserMetadata({ key, id: user.userId });
const dialogRef = this.dialog.open<MetadataDialogComponent, MetadataDialogData>(MetadataDialogComponent, {
data: {
@@ -442,16 +421,10 @@ export class AuthUserDetailComponent implements OnInit {
},
});
dialogRef
.afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.refreshMetadata$.next(true);
});
await lastValueFrom(dialogRef.afterClosed());
this.refreshMetadata$.next(true);
}
protected readonly query = query;
public humanUser(user: User | undefined): UserWithHumanType | undefined {
if (user?.type.case === 'human') {
return { ...user, type: user.type };
@@ -1,24 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { EditDialogComponent } from './edit-dialog.component';
describe('CodeDialogComponent', () => {
let component: EditDialogComponent;
let fixture: ComponentFixture<EditDialogComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [EditDialogComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EditDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -7,6 +7,7 @@ import { CountryCallingCodesService, CountryPhoneCode } from 'src/app/services/c
import { formatPhone } from 'src/app/utils/formatPhone';
export enum EditDialogType {
GENERIC = 0,
PHONE = 1,
EMAIL = 2,
}
@@ -15,8 +16,11 @@ export type EditDialogData = {
confirmKey: 'ACTIONS.SAVE' | 'ACTIONS.CHANGE';
cancelKey: 'ACTIONS.CANCEL';
labelKey: 'ACTIONS.NEWVALUE';
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE';
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC';
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE' | 'USER.PROFILE.CHANGEUSERNAME_TITLE' | 'USER.LOGINMETHODS.PHONE.EDITTITLE';
descriptionKey:
| 'USER.LOGINMETHODS.EMAIL.EDITDESC'
| 'USER.PROFILE.CHANGEUSERNAME_DESC'
| 'USER.LOGINMETHODS.PHONE.EDITDESC';
isVerifiedTextKey?: 'USER.LOGINMETHODS.EMAIL.ISVERIFIED';
isVerifiedTextDescKey?: 'USER.LOGINMETHODS.EMAIL.ISVERIFIEDDESC';
value: string | undefined;
@@ -39,9 +43,8 @@ export class EditDialogComponent implements OnInit {
public controlKey = 'editingField';
public isPhone: boolean = false;
public isVerified: boolean = false;
public phoneCountry: string = 'US';
public dialogForm!: UntypedFormGroup;
public EditDialogType: any = EditDialogType;
protected readonly EditDialogType = EditDialogType;
public selected: CountryPhoneCode | undefined = {
countryCallingCode: '1',
countryCode: 'US',
@@ -37,6 +37,7 @@ import {
combineLatestWith,
defer,
EMPTY,
lastValueFrom,
mergeWith,
Observable,
ObservedValueOf,
@@ -232,8 +233,8 @@ export class UserDetailComponent implements OnInit {
.subscribe((setting) => this.currentSetting$.set(setting));
}
public changeUsername(user: UserV2): void {
const dialogRef = this.dialog.open(EditDialogComponent, {
public async changeUsername(user: UserV2) {
const dialogRef = this.dialog.open<EditDialogComponent, EditDialogData, EditDialogResult>(EditDialogComponent, {
data: {
confirmKey: 'ACTIONS.CHANGE',
cancelKey: 'ACTIONS.CANCEL',
@@ -241,99 +242,83 @@ export class UserDetailComponent implements OnInit {
titleKey: 'USER.PROFILE.CHANGEUSERNAME_TITLE',
descriptionKey: 'USER.PROFILE.CHANGEUSERNAME_DESC',
value: user.username,
type: EditDialogType.GENERIC,
},
width: '400px',
});
dialogRef
.afterClosed()
.pipe(
map(({ value }: { value?: string }) => value),
filter(Boolean),
filter((value) => user.username != value),
switchMap((username) => this.userService.updateUser({ userId: user.userId, username })),
)
.subscribe({
next: () => {
this.toast.showInfo('USER.TOAST.USERNAMECHANGED', true);
this.refreshChanges$.emit();
},
error: (error) => {
this.toast.showError(error);
},
});
}
const { value: username } = (await lastValueFrom(dialogRef.afterClosed())) ?? {};
if (!username || user.username === username) {
// no changes made
return;
}
public unlockUser(user: UserV2): void {
this.userService
.unlockUser(user.userId)
.then(() => {
this.toast.showInfo('USER.TOAST.UNLOCKED', true);
this.refreshChanges$.emit();
})
.catch((error) => {
this.toast.showError(error);
});
}
public generateMachineSecret(user: UserV2): void {
this.newMgmtService
.generateMachineSecret(user.userId)
.then((resp) => {
this.toast.showInfo('USER.TOAST.SECRETGENERATED', true);
this.dialog.open(MachineSecretDialogComponent, {
data: {
clientId: resp.clientId,
clientSecret: resp.clientSecret,
},
width: '400px',
});
this.refreshChanges$.emit();
})
.catch((error) => {
this.toast.showError(error);
});
}
public removeMachineSecret(user: UserV2): void {
this.newMgmtService
.removeMachineSecret(user.userId)
.then(() => {
this.toast.showInfo('USER.TOAST.SECRETREMOVED', true);
this.refreshChanges$.emit();
})
.catch((error) => {
this.toast.showError(error);
});
}
public changeState(user: UserV2, newState: UserState): void {
if (newState === UserState.ACTIVE) {
this.userService
.reactivateUser(user.userId)
.then(() => {
this.toast.showInfo('USER.TOAST.REACTIVATED', true);
this.refreshChanges$.emit();
})
.catch((error) => {
this.toast.showError(error);
});
} else if (newState === UserState.INACTIVE) {
this.userService
.deactivateUser(user.userId)
.then(() => {
this.toast.showInfo('USER.TOAST.DEACTIVATED', true);
this.refreshChanges$.emit();
})
.catch((error) => {
this.toast.showError(error);
});
try {
await this.userService.updateUser({ userId: user.userId, username });
this.toast.showInfo('USER.TOAST.USERNAMECHANGED', true);
this.refreshChanges$.emit();
} catch (error) {
this.toast.showError(error);
}
}
public saveProfile(user: UserV2, profile: HumanProfile): void {
this.userService
.updateUser({
public async unlockUser(user: UserV2) {
try {
await this.userService.unlockUser(user.userId);
this.toast.showInfo('USER.TOAST.UNLOCKED', true);
this.refreshChanges$.emit();
} catch (error) {
this.toast.showError(error);
}
}
public async generateMachineSecret(user: UserV2) {
try {
const resp = await this.newMgmtService.generateMachineSecret(user.userId);
this.toast.showInfo('USER.TOAST.SECRETGENERATED', true);
this.dialog.open(MachineSecretDialogComponent, {
data: {
clientId: resp.clientId,
clientSecret: resp.clientSecret,
},
width: '400px',
});
this.refreshChanges$.emit();
} catch (error) {
this.toast.showError(error);
}
}
public async removeMachineSecret(user: UserV2) {
try {
await this.newMgmtService.removeMachineSecret(user.userId);
this.toast.showInfo('USER.TOAST.SECRETREMOVED', true);
this.refreshChanges$.emit();
} catch (error) {
this.toast.showError(error);
}
}
public async changeState(user: UserV2, newState: UserState) {
try {
if (newState === UserState.ACTIVE) {
await this.userService.reactivateUser(user.userId);
this.toast.showInfo('USER.TOAST.REACTIVATED', true);
} else if (newState === UserState.INACTIVE) {
await this.userService.deactivateUser(user.userId);
this.toast.showInfo('USER.TOAST.DEACTIVATED', true);
} else {
return;
}
this.refreshChanges$.emit();
} catch (error) {
this.toast.showError(error);
}
}
public async saveProfile(user: UserV2, profile: HumanProfile) {
try {
await this.userService.updateUser({
userId: user.userId,
profile: {
givenName: profile.givenName,
@@ -343,89 +328,77 @@ export class UserDetailComponent implements OnInit {
preferredLanguage: profile.preferredLanguage,
gender: profile.gender,
},
})
.then(() => {
this.toast.showInfo('USER.TOAST.SAVED', true);
this.refreshChanges$.emit();
})
.catch((error) => {
this.toast.showError(error);
});
this.toast.showInfo('USER.TOAST.SAVED', true);
this.refreshChanges$.emit();
} catch (error) {
this.toast.showError(error);
}
}
public saveMachine(user: UserV2, form: ObservedValueOf<DetailFormMachineComponent['submitData']>): void {
this.newMgmtService
.updateMachine({
public async saveMachine(user: UserV2, form: ObservedValueOf<DetailFormMachineComponent['submitData']>) {
try {
await this.newMgmtService.updateMachine({
userId: user.userId,
name: form.name,
description: form.description,
accessTokenType: form.accessTokenType,
})
.then(() => {
this.toast.showInfo('USER.TOAST.SAVED', true);
this.refreshChanges$.emit();
})
.catch((error) => {
this.toast.showError(error);
});
this.toast.showInfo('USER.TOAST.SAVED', true);
this.refreshChanges$.emit();
} catch (error) {
this.toast.showError(error);
}
}
public resendEmailVerification(user: UserV2): void {
this.newMgmtService
.resendHumanEmailVerification(user.userId)
.then(() => {
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
this.refreshChanges$.emit();
})
.catch((error) => {
this.toast.showError(error);
});
public async resendEmailVerification(user: UserV2) {
try {
await this.newMgmtService.resendHumanEmailVerification(user.userId);
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
this.refreshChanges$.emit();
} catch (error) {
this.toast.showError(error);
}
}
public resendPhoneVerification(user: UserV2): void {
this.newMgmtService
.resendHumanPhoneVerification(user.userId)
.then(() => {
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
this.refreshChanges$.emit();
})
.catch((error) => {
this.toast.showError(error);
});
public async resendPhoneVerification(user: UserV2) {
try {
await this.newMgmtService.resendHumanPhoneVerification(user.userId);
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
this.refreshChanges$.emit();
} catch (error) {
this.toast.showError(error);
}
}
public deletePhone(user: UserV2): void {
this.userService
.removePhone(user.userId)
.then(() => {
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
this.refreshChanges$.emit();
})
.catch((error) => {
this.toast.showError(error);
});
public async deletePhone(user: UserV2) {
try {
await this.userService.removePhone(user.userId);
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
this.refreshChanges$.emit();
} catch (error) {
this.toast.showError(error);
}
}
public navigateBack(): void {
this._location.back();
}
public sendSetPasswordNotification(user: UserV2): void {
this.newMgmtService
.sendHumanResetPasswordNotification({
public async sendSetPasswordNotification(user: UserV2) {
try {
await this.newMgmtService.sendHumanResetPasswordNotification({
userId: user.userId,
type: SendHumanResetPasswordNotificationRequest_Type.EMAIL,
})
.then(() => {
this.toast.showInfo('USER.TOAST.PASSWORDNOTIFICATIONSENT', true);
this.refreshChanges$.emit();
})
.catch((error) => {
this.toast.showError(error);
});
this.toast.showInfo('USER.TOAST.PASSWORDNOTIFICATIONSENT', true);
this.refreshChanges$.emit();
} catch (error) {
this.toast.showError(error);
}
}
public deleteUser(user: UserV2): void {
public async deleteUser(user: UserV2) {
const data = {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
@@ -438,26 +411,25 @@ export class UserDetailComponent implements OnInit {
width: '400px',
});
dialogRef
.afterClosed()
.pipe(
filter(Boolean),
switchMap(() => this.userService.deleteUser(user.userId)),
)
.subscribe({
next: () => {
const params: Params = {
deferredReload: true,
type: user.type.case === 'human' ? 'humans' : 'machines',
};
this.router.navigate(['/users'], { queryParams: params }).then();
this.toast.showInfo('USER.TOAST.DELETED', true);
},
error: (error) => this.toast.showError(error),
});
const confirmed = await lastValueFrom(dialogRef.afterClosed());
if (!confirmed) {
return;
}
try {
await this.userService.deleteUser(user.userId);
const params: Params = {
deferredReload: true,
type: user.type.case === 'human' ? 'humans' : 'machines',
};
this.router.navigate(['/users'], { queryParams: params }).then();
this.toast.showInfo('USER.TOAST.DELETED', true);
} catch (error) {
this.toast.showError(error);
}
}
public resendInitEmail(user: UserV2): void {
public async resendInitEmail(user: UserV2) {
const dialogRef = this.dialog.open<ResendEmailDialogComponent, ResendEmailDialogData, ResendEmailDialogResult>(
ResendEmailDialogComponent,
{
@@ -468,34 +440,33 @@ export class UserDetailComponent implements OnInit {
},
);
dialogRef
.afterClosed()
.pipe(
filter((resp): resp is { send: true; email: string } => !!resp?.send && !!user.userId),
switchMap(({ email }) => this.newMgmtService.resendHumanInitialization(user.userId, email)),
)
.subscribe({
next: () => {
this.toast.showInfo('USER.TOAST.INITEMAILSENT', true);
this.refreshChanges$.emit();
},
error: (error) => this.toast.showError(error),
});
const resp = (await lastValueFrom(dialogRef.afterClosed())) ?? { send: false as const };
if (!resp.send) {
return;
}
try {
await this.newMgmtService.resendHumanInitialization(user.userId, resp.email);
this.toast.showInfo('USER.TOAST.INITEMAILSENT', true);
this.refreshChanges$.emit();
} catch (error) {
this.toast.showError(error);
}
}
public openEditDialog(user: UserWithHumanType, type: EditDialogType): void {
public async openEditDialog(user: UserWithHumanType, type: EditDialogType) {
switch (type) {
case EditDialogType.PHONE:
this.openEditPhoneDialog(user);
await this.openEditPhoneDialog(user);
return;
case EditDialogType.EMAIL:
this.openEditEmailDialog(user);
await this.openEditEmailDialog(user);
return;
}
}
private openEditEmailDialog(user: UserWithHumanType) {
const data: EditDialogData = {
private async openEditEmailDialog(user: UserWithHumanType) {
const data = {
confirmKey: 'ACTIONS.SAVE',
cancelKey: 'ACTIONS.CANCEL',
labelKey: 'ACTIONS.NEWVALUE',
@@ -505,40 +476,38 @@ export class UserDetailComponent implements OnInit {
isVerifiedTextDescKey: 'USER.LOGINMETHODS.EMAIL.ISVERIFIEDDESC',
value: user.type.value?.email?.email,
type: EditDialogType.EMAIL,
} as const;
} as const satisfies EditDialogData;
const dialogRefEmail = this.dialog.open<EditDialogComponent, EditDialogData, EditDialogResult>(EditDialogComponent, {
data,
width: '400px',
});
dialogRefEmail
.afterClosed()
.pipe(
filter((resp): resp is Required<EditDialogResult> => !!resp?.value),
switchMap(({ value, isVerified }) =>
this.userService.setEmail({
userId: user.userId,
email: value,
verification: isVerified ? { case: 'isVerified', value: isVerified } : { case: undefined },
}),
),
switchMap(() => {
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
this.refreshChanges$.emit();
if (user.state !== UserState.INITIAL) {
return EMPTY;
}
return this.userService.resendInviteCode(user.userId);
}),
)
.subscribe({
next: () => this.toast.showInfo('USER.TOAST.INITEMAILSENT', true),
error: (error) => this.toast.showError(error),
const { value, isVerified } = (await lastValueFrom(dialogRefEmail.afterClosed())) ?? { isVerified: false };
if (!value) {
return;
}
try {
await this.userService.updateUser({
userId: user.userId,
email: {
email: value,
verification: { case: 'isVerified', value: isVerified },
},
});
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
this.refreshChanges$.emit();
if (user.state === UserState.INITIAL) {
await this.userService.resendInviteCode(user.userId);
this.toast.showInfo('USER.TOAST.INITEMAILSENT', true);
}
} catch (error) {
this.toast.showError(error);
}
}
private openEditPhoneDialog(user: UserWithHumanType) {
private async openEditPhoneDialog(user: UserWithHumanType) {
const data = {
confirmKey: 'ACTIONS.SAVE',
cancelKey: 'ACTIONS.CANCEL',
@@ -547,29 +516,27 @@ export class UserDetailComponent implements OnInit {
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
value: user.type.value.phone?.phone,
type: EditDialogType.PHONE,
validator: Validators.compose([phoneValidator, requiredValidator]),
};
const dialogRefPhone = this.dialog.open<EditDialogComponent, typeof data, { value: string; isVerified: boolean }>(
EditDialogComponent,
{ data, width: '400px' },
);
validator: Validators.compose([phoneValidator, requiredValidator]) ?? undefined,
} as const satisfies EditDialogData;
dialogRefPhone
.afterClosed()
.pipe(
map((resp) => formatPhone(resp?.value)),
filter(Boolean),
switchMap(({ phone }) => this.userService.setPhone({ userId: user.userId, phone })),
)
.subscribe({
next: () => {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
this.refreshChanges$.emit();
},
error: (error) => {
this.toast.showError(error);
},
});
const dialogRefPhone = this.dialog.open<EditDialogComponent, EditDialogData, EditDialogResult>(EditDialogComponent, {
data,
width: '400px',
});
const { value } = (await lastValueFrom(dialogRefPhone.afterClosed())) ?? {};
const formatted = formatPhone(value);
if (!formatted) {
return;
}
try {
await this.userService.setPhone({ userId: user.userId, phone: formatted.phone });
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
this.refreshChanges$.emit();
} catch (error) {
this.toast.showError(error);
}
}
private getMetadataById(userId: string): Observable<MetadataQuery> {
@@ -580,7 +547,7 @@ export class UserDetailComponent implements OnInit {
);
}
public editMetadata(user: UserV2, metadata: Metadata[]): void {
public async editMetadata(user: UserV2, metadata: Metadata[]) {
const setFcn = (key: string, value: string) =>
this.newMgmtService.setUserMetadata({
key,
@@ -597,12 +564,8 @@ export class UserDetailComponent implements OnInit {
},
});
dialogRef
.afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.refreshMetadata$.next(true);
});
await lastValueFrom(dialogRef.afterClosed());
this.refreshMetadata$.next(true);
}
public humanUser(user: UserV2): UserWithHumanType | undefined {
+10 -12
View File
@@ -1,19 +1,17 @@
import { CountryCode, parsePhoneNumber } from 'libphonenumber-js';
import { CountryCode, parsePhoneNumberWithError } from 'libphonenumber-js';
export function formatPhone(phone?: string): { phone: string; country: CountryCode } | null {
const defaultCountry = 'US';
if (phone) {
try {
const phoneNumber = parsePhoneNumber(phone, defaultCountry);
const country = phoneNumber.country ?? defaultCountry;
if (phoneNumber) {
return { phone: phoneNumber.formatInternational(), country };
}
} catch {
return null;
}
if (!phone) {
return null;
}
return null;
try {
const phoneNumber = parsePhoneNumberWithError(phone, defaultCountry);
const country = phoneNumber.country ?? defaultCountry;
return { phone: phoneNumber.formatInternational(), country };
} catch {
return null;
}
}