diff --git a/console/src/app/modules/warn-dialog/warn-dialog.component.spec.ts b/console/src/app/modules/warn-dialog/warn-dialog.component.spec.ts deleted file mode 100644 index b86357e96..000000000 --- a/console/src/app/modules/warn-dialog/warn-dialog.component.spec.ts +++ /dev/null @@ -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; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [WarnDialogComponent], - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(WarnDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/console/src/app/pages/orgs/org-detail/org-detail.component.ts b/console/src/app/pages/orgs/org-detail/org-detail.component.ts index bc076c9aa..e53b8b9d0 100644 --- a/console/src/app/pages/orgs/org-detail/org-detail.component.ts +++ b/console/src/app/pages/orgs/org-detail/org-detail.component.ts @@ -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; diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts index 4d32f20e6..c0a985156 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts @@ -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, { + type: EditDialogType.GENERIC, + } as const satisfies EditDialogData; + + const dialogRef = this.dialog.open(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, { 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, { data, width: '400px', }); - dialogRefEmail - .afterClosed() - .pipe( - filter((resp): resp is Required => !!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, - { 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, { + 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 => this.newMgmtService.removeUserMetadata({ key, id: user.userId }); + const removeFcn = (key: string) => this.newMgmtService.removeUserMetadata({ key, id: user.userId }); const dialogRef = this.dialog.open(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 }; diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/edit-dialog/edit-dialog.component.spec.ts b/console/src/app/pages/users/user-detail/auth-user-detail/edit-dialog/edit-dialog.component.spec.ts deleted file mode 100644 index 42623269a..000000000 --- a/console/src/app/pages/users/user-detail/auth-user-detail/edit-dialog/edit-dialog.component.spec.ts +++ /dev/null @@ -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; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [EditDialogComponent], - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(EditDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/edit-dialog/edit-dialog.component.ts b/console/src/app/pages/users/user-detail/auth-user-detail/edit-dialog/edit-dialog.component.ts index 47862c509..916350954 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/edit-dialog/edit-dialog.component.ts +++ b/console/src/app/pages/users/user-detail/auth-user-detail/edit-dialog/edit-dialog.component.ts @@ -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', diff --git a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts index 0ec8bc6ec..8b04eab07 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts +++ b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts @@ -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, { 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): void { - this.newMgmtService - .updateMachine({ + public async saveMachine(user: UserV2, form: ObservedValueOf) { + 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, { @@ -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, { data, width: '400px', }); - dialogRefEmail - .afterClosed() - .pipe( - filter((resp): resp is Required => !!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, - { 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, { + 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 { @@ -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 { diff --git a/console/src/app/utils/formatPhone.ts b/console/src/app/utils/formatPhone.ts index ddf79ddc3..1812d2dec 100644 --- a/console/src/app/utils/formatPhone.ts +++ b/console/src/app/utils/formatPhone.ts @@ -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; + } }