mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 16:47:32 +00:00
feat: Use V2 API's in Console (#9312)
# Which Problems Are Solved Solves #8976 # Additional Changes I have done some intensive refactorings and we are using the new @zitadel/client package for GRPC access. # Additional Context - Closes #8976 --------- Co-authored-by: Max Peintner <peintnerm@gmail.com>
This commit is contained in:
@@ -63,7 +63,7 @@
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "8mb",
|
||||
"maximumError": "9mb"
|
||||
"maximumError": "10mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
|
@@ -24,6 +24,8 @@
|
||||
"@angular/platform-browser-dynamic": "^16.2.5",
|
||||
"@angular/router": "^16.2.5",
|
||||
"@angular/service-worker": "^16.2.5",
|
||||
"@connectrpc/connect": "^2.0.0",
|
||||
"@connectrpc/connect-web": "^2.0.0",
|
||||
"@ctrl/ngx-codemirror": "^6.1.0",
|
||||
"@fortawesome/angular-fontawesome": "^0.13.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||
@@ -31,6 +33,8 @@
|
||||
"@grpc/grpc-js": "^1.11.2",
|
||||
"@netlify/framework-info": "^9.8.13",
|
||||
"@ngx-translate/core": "^15.0.0",
|
||||
"@zitadel/client": "^1.0.6",
|
||||
"@zitadel/proto": "^1.0.3",
|
||||
"angular-oauth2-oidc": "^15.0.1",
|
||||
"angularx-qrcode": "^16.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div class="main-container">
|
||||
<ng-container *ngIf="(authService.userSubject | async) || {} as user">
|
||||
<ng-container *ngIf="(authService.user | async) || {} as user">
|
||||
<cnsl-header
|
||||
*ngIf="user && user !== {}"
|
||||
[org]="org"
|
||||
|
@@ -8,7 +8,7 @@ import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
|
||||
import { LangChangeEvent, TranslateService } from '@ngx-translate/core';
|
||||
import { Observable, of, Subject } from 'rxjs';
|
||||
import { filter, map, takeUntil } from 'rxjs/operators';
|
||||
import { filter, map, startWith, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { accountCard, adminLineAnimation, navAnimations, routeAnimations, toolbarAnimation } from './animations';
|
||||
import { Org } from './proto/generated/zitadel/org_pb';
|
||||
@@ -275,7 +275,7 @@ export class AppComponent implements OnDestroy {
|
||||
const currentUrl = this.router.url;
|
||||
this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => {
|
||||
// We use navigateByUrl as our urls may have queryParams
|
||||
this.router.navigateByUrl(currentUrl);
|
||||
this.router.navigateByUrl(currentUrl).then();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -283,8 +283,7 @@ export class AppComponent implements OnDestroy {
|
||||
this.translate.addLangs(supportedLanguages);
|
||||
this.translate.setDefaultLang(fallbackLanguage);
|
||||
|
||||
this.authService.userSubject.pipe(takeUntil(this.destroy$)).subscribe((userprofile) => {
|
||||
if (userprofile) {
|
||||
this.authService.user.pipe(filter(Boolean), takeUntil(this.destroy$)).subscribe((userprofile) => {
|
||||
const cropped = navigator.language.split('-')[0] ?? fallbackLanguage;
|
||||
const fallbackLang = cropped.match(supportedLanguagesRegexp) ? cropped : fallbackLanguage;
|
||||
|
||||
@@ -294,7 +293,6 @@ export class AppComponent implements OnDestroy {
|
||||
this.translate.use(lang);
|
||||
this.language = lang;
|
||||
this.document.documentElement.lang = lang;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -308,7 +306,7 @@ export class AppComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
private setFavicon(theme: string): void {
|
||||
this.authService.labelpolicy.pipe(takeUntil(this.destroy$)).subscribe((lP) => {
|
||||
this.authService.labelpolicy$.pipe(startWith(undefined), takeUntil(this.destroy$)).subscribe((lP) => {
|
||||
if (theme === 'dark-theme' && lP?.iconUrlDark) {
|
||||
// Check if asset url is stable, maybe it was deleted but still wasn't applied
|
||||
fetch(lP.iconUrlDark).then((response) => {
|
||||
|
@@ -403,6 +403,34 @@
|
||||
'SETTING.FEATURES.OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION' | translate
|
||||
}}</cnsl-info-section>
|
||||
</div>
|
||||
|
||||
<div class="feature-row" *ngIf="toggleStates.consoleUseV2UserApi">
|
||||
<span>{{ 'SETTING.FEATURES.CONSOLEUSEV2USERAPI' | translate }}</span>
|
||||
<div class="row">
|
||||
<mat-button-toggle-group
|
||||
class="theme-toggle"
|
||||
class="buttongroup"
|
||||
[(ngModel)]="toggleStates.consoleUseV2UserApi.state"
|
||||
(change)="validateAndSave()"
|
||||
name="displayview"
|
||||
aria-label="Display View"
|
||||
>
|
||||
<mat-button-toggle [value]="ToggleState.DISABLED">
|
||||
<div class="toggle-row">
|
||||
<span> {{ 'SETTING.FEATURES.STATES.DISABLED' | translate }}</span>
|
||||
</div>
|
||||
</mat-button-toggle>
|
||||
<mat-button-toggle [value]="ToggleState.ENABLED">
|
||||
<div class="toggle-row">
|
||||
<span> {{ 'SETTING.FEATURES.STATES.ENABLED' | translate }}</span>
|
||||
</div>
|
||||
</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
<cnsl-info-section class="feature-info">{{
|
||||
'SETTING.FEATURES.CONSOLEUSEV2USERAPI_DESCRIPTION' | translate
|
||||
}}</cnsl-info-section>
|
||||
</div>
|
||||
</div>
|
||||
</cnsl-card>
|
||||
</div>
|
||||
|
@@ -16,13 +16,14 @@ import { InfoSectionModule } from 'src/app/modules/info-section/info-section.mod
|
||||
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
|
||||
import { Event } from 'src/app/proto/generated/zitadel/event_pb';
|
||||
import { Source } from 'src/app/proto/generated/zitadel/feature/v2beta/feature_pb';
|
||||
import {
|
||||
GetInstanceFeaturesResponse,
|
||||
SetInstanceFeaturesRequest,
|
||||
} from 'src/app/proto/generated/zitadel/feature/v2beta/instance_pb';
|
||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||
import { FeatureService } from 'src/app/services/feature.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import {
|
||||
GetInstanceFeaturesResponse,
|
||||
SetInstanceFeaturesRequest,
|
||||
} from '../../proto/generated/zitadel/feature/v2/instance_pb';
|
||||
import { withIdentifier } from 'codelyzer/util/astQuery';
|
||||
|
||||
enum ToggleState {
|
||||
ENABLED = 'ENABLED',
|
||||
@@ -39,6 +40,7 @@ type ToggleStates = {
|
||||
oidcTokenExchange?: FeatureState;
|
||||
actions?: FeatureState;
|
||||
oidcSingleV1SessionTermination?: FeatureState;
|
||||
consoleUseV2UserApi?: FeatureState;
|
||||
};
|
||||
|
||||
@Component({
|
||||
@@ -142,6 +144,7 @@ export class FeaturesComponent implements OnDestroy {
|
||||
);
|
||||
changed = true;
|
||||
}
|
||||
req.setConsoleUseV2UserApi(this.toggleStates?.consoleUseV2UserApi?.state === ToggleState.ENABLED);
|
||||
|
||||
if (changed) {
|
||||
this.featureService
|
||||
@@ -232,6 +235,10 @@ export class FeaturesComponent implements OnDestroy {
|
||||
? ToggleState.ENABLED
|
||||
: ToggleState.DISABLED,
|
||||
},
|
||||
consoleUseV2UserApi: {
|
||||
source: this.featureData.consoleUseV2UserApi?.source || Source.SOURCE_INSTANCE,
|
||||
state: this.featureData.consoleUseV2UserApi?.enabled ? ToggleState.ENABLED : ToggleState.DISABLED,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@@ -1,18 +1,17 @@
|
||||
import { Directive, Input, OnDestroy, TemplateRef, ViewContainerRef } from '@angular/core';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { DestroyRef, Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Directive({
|
||||
selector: '[cnslHasRole]',
|
||||
})
|
||||
export class HasRoleDirective implements OnDestroy {
|
||||
private destroy$: Subject<void> = new Subject();
|
||||
export class HasRoleDirective {
|
||||
private hasView: boolean = false;
|
||||
@Input() public set hasRole(roles: string[] | RegExp[] | undefined) {
|
||||
if (roles && roles.length > 0) {
|
||||
this.authService
|
||||
.isAllowed(roles)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((isAllowed) => {
|
||||
if (isAllowed && !this.hasView) {
|
||||
if (this.viewContainerRef.length !== 0) {
|
||||
@@ -38,10 +37,6 @@ export class HasRoleDirective implements OnDestroy {
|
||||
private authService: GrpcAuthService,
|
||||
protected templateRef: TemplateRef<any>,
|
||||
protected viewContainerRef: ViewContainerRef,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
) {}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import { AuthConfig } from 'angular-oauth2-oidc';
|
||||
import { Session, User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { AuthenticationService } from 'src/app/services/authentication.service';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-accounts-card',
|
||||
@@ -18,6 +19,8 @@ export class AccountsCardComponent implements OnInit {
|
||||
public sessions: Session.AsObject[] = [];
|
||||
public loadingUsers: boolean = false;
|
||||
public UserState: any = UserState;
|
||||
private labelpolicy = toSignal(this.userService.labelpolicy$, { initialValue: undefined });
|
||||
|
||||
constructor(
|
||||
public authService: AuthenticationService,
|
||||
private router: Router,
|
||||
@@ -68,7 +71,7 @@ export class AccountsCardComponent implements OnInit {
|
||||
}
|
||||
|
||||
public logout(): void {
|
||||
const lP = JSON.stringify(this.userService.labelpolicy.getValue());
|
||||
const lP = JSON.stringify(this.labelpolicy());
|
||||
localStorage.setItem('labelPolicyOnSignout', lP);
|
||||
|
||||
this.authService.signout();
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<div class="footer-wrapper">
|
||||
<div class="footer-row">
|
||||
<div class="footer-links" *ngIf="authService.privacypolicy | async as pP">
|
||||
<div class="footer-links" *ngIf="authService.privacypolicy$ | async as pP">
|
||||
<a target="_blank" *ngIf="pP?.tosLink" rel="noreferrer" [href]="pP?.tosLink" external>
|
||||
<span>{{ 'FOOTER.LINKS.TOS' | translate }}</span>
|
||||
<i class="las la-external-link-alt"></i>
|
||||
@@ -11,7 +11,7 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="footer-socials" *ngIf="authService.labelpolicy | async as lP">
|
||||
<div class="footer-socials" *ngIf="authService.labelpolicy$ | async as lP">
|
||||
<ng-container *ngIf="lP?.disableWatermark === false">
|
||||
<a target="_blank" rel="noreferrer" href="https://github.com/zitadel">
|
||||
<i class="text-3xl lab la-github"></i>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<mat-spinner [diameter]="20"></mat-spinner>
|
||||
</div>
|
||||
<ng-template #logo>
|
||||
<a class="title custom" [routerLink]="['/']" *ngIf="authService.labelpolicy | async as lP; else defaultHome">
|
||||
<a class="title custom" [routerLink]="['/']" *ngIf="authService.labelpolicy$ | async as lP; else defaultHome">
|
||||
<img
|
||||
class="logo"
|
||||
alt="home logo"
|
||||
@@ -24,7 +24,7 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #defaultHome>
|
||||
<a class="title" [routerLink]="authService.zitadelPermissions.getValue().length === 0 ? ['/users', 'me'] : ['/']">
|
||||
<a class="title" [routerLink]="(authService.zitadelPermissions | async)?.length ? ['/'] : ['/users', 'me']">
|
||||
<img
|
||||
class="logo"
|
||||
alt="zitadel logo"
|
||||
@@ -168,7 +168,7 @@
|
||||
|
||||
<span class="fill-space"></span>
|
||||
|
||||
<ng-container *ngIf="authService.privacypolicy | async as pP">
|
||||
<ng-container *ngIf="authService.privacypolicy$ | async as pP">
|
||||
<a class="custom-link" *ngIf="pP.customLink" href="{{ pP.customLink }}" mat-stroked-button target="_blank">
|
||||
{{ pP.customLinkText }}
|
||||
</a>
|
||||
|
@@ -9,26 +9,26 @@
|
||||
inactive: user.state === UserState.USER_STATE_INACTIVE,
|
||||
}"
|
||||
>
|
||||
{{ 'USER.DATA.STATE' + user.state | translate }}
|
||||
{{ (isV2(user) ? 'USER.STATEV2.' : 'USER.STATE.') + user.state | translate }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'USER.ID' | translate }}</p>
|
||||
<p *ngIf="user && user.id" class="info-row-desc">{{ user.id }}</p>
|
||||
<p *ngIf="userId" class="info-row-desc">{{ userId }}</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'USER.DETAILS.DATECREATED' | translate }}</p>
|
||||
<p *ngIf="user && user.details && user.details.creationDate" class="info-row-desc">
|
||||
{{ user.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
<p *ngIf="creationDate" class="info-row-desc">
|
||||
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'USER.DETAILS.DATECHANGED' | translate }}</p>
|
||||
<p *ngIf="user && user.details && user.details.changeDate" class="info-row-desc">
|
||||
{{ user.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
<p *ngIf="changeDate" class="info-row-desc">
|
||||
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
@@ -6,6 +6,9 @@ import { Org, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { LoginPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
||||
import { GrantedProject, Project, ProjectGrantState, ProjectState } from 'src/app/proto/generated/zitadel/project_pb';
|
||||
import { User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { User as UserV1 } from '@zitadel/proto/zitadel/user_pb';
|
||||
import { User as UserV2 } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { LoginPolicy as LoginPolicyV2 } from '@zitadel/proto/zitadel/policy_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-info-row',
|
||||
@@ -13,14 +16,14 @@ import { User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
styleUrls: ['./info-row.component.scss'],
|
||||
})
|
||||
export class InfoRowComponent {
|
||||
@Input() public user!: User.AsObject;
|
||||
@Input() public user?: User.AsObject | UserV2 | UserV1;
|
||||
@Input() public org!: Org.AsObject;
|
||||
@Input() public instance!: InstanceDetail.AsObject;
|
||||
@Input() public app!: App.AsObject;
|
||||
@Input() public idp!: IDP.AsObject;
|
||||
@Input() public project!: Project.AsObject;
|
||||
@Input() public grantedProject!: GrantedProject.AsObject;
|
||||
@Input() public loginPolicy?: LoginPolicy.AsObject;
|
||||
@Input() public loginPolicy?: LoginPolicy.AsObject | LoginPolicyV2;
|
||||
|
||||
public UserState: any = UserState;
|
||||
public State: any = State;
|
||||
@@ -35,25 +38,77 @@ export class InfoRowComponent {
|
||||
constructor() {}
|
||||
|
||||
public get loginMethods(): Set<string> {
|
||||
const methods = this.user?.loginNamesList;
|
||||
let email: string = '';
|
||||
let phone: string = '';
|
||||
if (this.loginPolicy) {
|
||||
if (
|
||||
!this.loginPolicy?.disableLoginWithEmail &&
|
||||
this.user.human?.email?.email &&
|
||||
this.user.human.email.isEmailVerified
|
||||
) {
|
||||
email = this.user.human?.email?.email;
|
||||
if (!this.user) {
|
||||
return new Set();
|
||||
}
|
||||
if (
|
||||
!this.loginPolicy?.disableLoginWithPhone &&
|
||||
this.user.human?.phone?.phone &&
|
||||
this.user.human.phone.isPhoneVerified
|
||||
) {
|
||||
phone = this.user.human?.phone?.phone;
|
||||
}
|
||||
}
|
||||
return new Set([email, phone, ...methods].filter((method) => !!method));
|
||||
|
||||
const methods = '$typeName' in this.user ? this.user.loginNames : this.user.loginNamesList;
|
||||
|
||||
const loginPolicy = this.loginPolicy;
|
||||
if (!loginPolicy) {
|
||||
return new Set([...methods]);
|
||||
}
|
||||
|
||||
let email = !loginPolicy.disableLoginWithEmail ? this.getEmail(this.user) : '';
|
||||
let phone = !loginPolicy.disableLoginWithPhone ? this.getPhone(this.user) : '';
|
||||
|
||||
return new Set([email, phone, ...methods].filter(Boolean));
|
||||
}
|
||||
|
||||
public get userId() {
|
||||
if (!this.user) {
|
||||
return undefined;
|
||||
}
|
||||
if ('$typeName' in this.user && this.user.$typeName === 'zitadel.user.v2.User') {
|
||||
return this.user.userId;
|
||||
}
|
||||
return this.user.id;
|
||||
}
|
||||
|
||||
public get changeDate() {
|
||||
return this?.user?.details?.changeDate;
|
||||
}
|
||||
|
||||
public get creationDate() {
|
||||
if (!this.user) {
|
||||
return undefined;
|
||||
}
|
||||
return '$typeName' in this.user ? undefined : this.user.details?.creationDate;
|
||||
}
|
||||
|
||||
private getEmail(user: User.AsObject | UserV2 | UserV1) {
|
||||
const human = this.human(user);
|
||||
if (!human) {
|
||||
return '';
|
||||
}
|
||||
if ('$typeName' in human && human.$typeName === 'zitadel.user.v2.HumanUser') {
|
||||
return human.email?.isVerified ? human.email.email : '';
|
||||
}
|
||||
return human.email?.isEmailVerified ? human.email.email : '';
|
||||
}
|
||||
|
||||
private getPhone(user: User.AsObject | UserV2 | UserV1) {
|
||||
const human = this.human(user);
|
||||
if (!human) {
|
||||
return '';
|
||||
}
|
||||
if ('$typeName' in human && human.$typeName === 'zitadel.user.v2.HumanUser') {
|
||||
return human.phone?.isVerified ? human.phone.phone : '';
|
||||
}
|
||||
return human.phone?.isPhoneVerified ? human.phone.phone : '';
|
||||
}
|
||||
|
||||
public human(user: User.AsObject | UserV2 | UserV1) {
|
||||
if (!('$typeName' in user)) {
|
||||
return user.human;
|
||||
}
|
||||
return user.type.case === 'human' ? user.type.value : undefined;
|
||||
}
|
||||
|
||||
public isV2(user: User.AsObject | UserV2 | UserV1) {
|
||||
if ('$typeName' in user) {
|
||||
return user.$typeName === 'zitadel.user.v2.User';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,16 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
|
||||
import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb';
|
||||
import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
export type MetadataDialogData = {
|
||||
metadata: (Metadata.AsObject | MetadataV2)[];
|
||||
setFcn: (key: string, value: string) => Promise<any>;
|
||||
removeFcn: (key: string) => Promise<any>;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-metadata-dialog',
|
||||
@@ -10,72 +18,63 @@ import { ToastService } from 'src/app/services/toast.service';
|
||||
styleUrls: ['./metadata-dialog.component.scss'],
|
||||
})
|
||||
export class MetadataDialogComponent {
|
||||
public metadata: Partial<Metadata.AsObject>[] = [];
|
||||
public metadata: { key: string; value: string }[] = [];
|
||||
public ts!: Timestamp.AsObject | undefined;
|
||||
|
||||
constructor(
|
||||
private toast: ToastService,
|
||||
public dialogRef: MatDialogRef<MetadataDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
@Inject(MAT_DIALOG_DATA) public data: MetadataDialogData,
|
||||
) {
|
||||
this.metadata = data.metadata;
|
||||
this.metadata = data.metadata.map(({ key, value }) => ({
|
||||
key,
|
||||
value: typeof value === 'string' ? value : Buffer.from(value as unknown as string, 'base64').toString('utf8'),
|
||||
}));
|
||||
}
|
||||
|
||||
public addEntry(): void {
|
||||
const newGroup = {
|
||||
this.metadata.push({
|
||||
key: '',
|
||||
value: '',
|
||||
};
|
||||
|
||||
this.metadata.push(newGroup);
|
||||
});
|
||||
}
|
||||
|
||||
public removeEntry(index: number): void {
|
||||
public async removeEntry(index: number) {
|
||||
const key = this.metadata[index].key;
|
||||
if (key) {
|
||||
this.removeMetadata(key).then(() => {
|
||||
if (!key) {
|
||||
this.metadata.splice(index, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.data.removeFcn(key);
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.toast.showInfo('METADATA.REMOVESUCCESS', true);
|
||||
this.metadata.splice(index, 1);
|
||||
if (this.metadata.length === 0) {
|
||||
this.addEntry();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.metadata.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public saveElement(index: number): void {
|
||||
const metadataElement = this.metadata[index];
|
||||
public async saveElement(index: number) {
|
||||
const { key, value } = this.metadata[index];
|
||||
|
||||
if (metadataElement.key && metadataElement.value) {
|
||||
this.setMetadata(metadataElement.key, metadataElement.value as string);
|
||||
}
|
||||
if (!key || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
public setMetadata(key: string, value: string): void {
|
||||
if (key && value) {
|
||||
this.data
|
||||
.setFcn(key, value)
|
||||
.then(() => {
|
||||
try {
|
||||
await this.data.setFcn(key, value);
|
||||
this.toast.showInfo('METADATA.SETSUCCESS', true);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public removeMetadata(key: string): Promise<void> {
|
||||
return this.data
|
||||
.removeFcn(key)
|
||||
.then((resp: any) => {
|
||||
this.toast.showInfo('METADATA.REMOVESUCCESS', true);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
closeDialog(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
@@ -1,7 +1,12 @@
|
||||
<cnsl-card class="metadata-details" [title]="'DESCRIPTIONS.METADATA_TITLE' | translate" [description]="description">
|
||||
<mat-spinner card-actions class="spinner" diameter="20" *ngIf="loading"></mat-spinner>
|
||||
|
||||
<cnsl-refresh-table [loading]="loading$ | async" (refreshed)="refresh.emit()" [dataSize]="dataSource.data.length">
|
||||
<cnsl-refresh-table
|
||||
*ngIf="dataSource$ | async as dataSource"
|
||||
[loading]="loading"
|
||||
(refreshed)="refresh.emit()"
|
||||
[dataSize]="dataSource.data.length"
|
||||
>
|
||||
<button actions [disabled]="disabled" mat-raised-button color="primary" class="edit" (click)="editClicked.emit()">
|
||||
{{ 'ACTIONS.EDIT' | translate }}
|
||||
</button>
|
||||
@@ -28,7 +33,7 @@
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
|
||||
<div *ngIf="!loading && !dataSource?.data?.length" class="no-content-row">
|
||||
<i class="las la-exclamation"></i>
|
||||
<span>{{ 'USER.MFA.EMPTY' | translate }}</span>
|
||||
</div>
|
||||
|
@@ -1,16 +1,26 @@
|
||||
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTable, MatTableDataSource } from '@angular/material/table';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Observable, ReplaySubject } from 'rxjs';
|
||||
import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb';
|
||||
import { map, startWith } from 'rxjs/operators';
|
||||
import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
type StringMetadata = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-metadata',
|
||||
templateUrl: './metadata.component.html',
|
||||
styleUrls: ['./metadata.component.scss'],
|
||||
})
|
||||
export class MetadataComponent implements OnChanges {
|
||||
@Input() public metadata: Metadata.AsObject[] = [];
|
||||
export class MetadataComponent implements OnInit {
|
||||
@Input({ required: true }) public set metadata(metadata: (Metadata.AsObject | MetadataV2)[]) {
|
||||
this.metadata$.next(metadata);
|
||||
}
|
||||
@Input() public disabled: boolean = false;
|
||||
@Input() public loading: boolean = false;
|
||||
@Input({ required: true }) public description!: string;
|
||||
@@ -18,18 +28,23 @@ export class MetadataComponent implements OnChanges {
|
||||
@Output() public refresh: EventEmitter<void> = new EventEmitter();
|
||||
|
||||
public displayedColumns: string[] = ['key', 'value'];
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
public metadata$ = new ReplaySubject<(Metadata.AsObject | MetadataV2)[]>(1);
|
||||
public dataSource$?: Observable<MatTableDataSource<StringMetadata>>;
|
||||
|
||||
@ViewChild(MatTable) public table!: MatTable<Metadata.AsObject>;
|
||||
@ViewChild(MatSort) public sort!: MatSort;
|
||||
public dataSource: MatTableDataSource<Metadata.AsObject> = new MatTableDataSource<Metadata.AsObject>([]);
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['metadata']?.currentValue) {
|
||||
this.dataSource = new MatTableDataSource<Metadata.AsObject>(changes['metadata'].currentValue);
|
||||
}
|
||||
ngOnInit() {
|
||||
this.dataSource$ = this.metadata$.pipe(
|
||||
map((metadata) =>
|
||||
metadata.map(({ key, value }) => ({
|
||||
key,
|
||||
value: Buffer.from(value as any as string, 'base64').toString('utf-8'),
|
||||
})),
|
||||
),
|
||||
startWith([] as StringMetadata[]),
|
||||
map((metadata) => new MatTableDataSource(metadata)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Timestamp } from 'src/app/proto/generated/google/protobuf/timestamp_pb';
|
||||
import { Timestamp as ConnectTimestamp } from '@bufbuild/protobuf/wkt';
|
||||
|
||||
export interface PageEvent {
|
||||
length: number;
|
||||
@@ -14,7 +15,7 @@ export interface PageEvent {
|
||||
styleUrls: ['./paginator.component.scss'],
|
||||
})
|
||||
export class PaginatorComponent {
|
||||
@Input() public timestamp: Timestamp.AsObject | undefined = undefined;
|
||||
@Input() public timestamp: Timestamp.AsObject | ConnectTimestamp | undefined = undefined;
|
||||
@Input() public length: number = 0;
|
||||
@Input() public pageSize: number = 10;
|
||||
@Input() public pageIndex: number = 0;
|
||||
|
@@ -3,6 +3,7 @@ import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
|
||||
import { RefreshService } from 'src/app/services/refresh.service';
|
||||
import { Timestamp as ConnectTimestamp } from '@bufbuild/protobuf/wkt';
|
||||
|
||||
import { ActionKeysType } from '../action-keys/action-keys.component';
|
||||
|
||||
@@ -27,7 +28,7 @@ const rotate = animation([
|
||||
})
|
||||
export class RefreshTableComponent implements OnInit {
|
||||
@Input() public selection: SelectionModel<any> = new SelectionModel<any>(true, []);
|
||||
@Input() public timestamp: Timestamp.AsObject | undefined = undefined;
|
||||
@Input() public timestamp: Timestamp.AsObject | ConnectTimestamp | undefined = undefined;
|
||||
@Input() public dataSize: number = 0;
|
||||
@Input() public emitRefreshAfterTimeoutInMs: number = 0;
|
||||
@Input() public loading: boolean | null = false;
|
||||
|
@@ -51,7 +51,8 @@ export class SidenavComponent implements ControlValueAccessor {
|
||||
}
|
||||
|
||||
if (this.queryParam && setting) {
|
||||
this.router.navigate([], {
|
||||
this.router
|
||||
.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: {
|
||||
[this.queryParam]: setting,
|
||||
@@ -59,7 +60,8 @@ export class SidenavComponent implements ControlValueAccessor {
|
||||
replaceUrl: true,
|
||||
queryParamsHandling: 'merge',
|
||||
skipLocationChange: false,
|
||||
});
|
||||
})
|
||||
.then();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -19,16 +19,21 @@ export class SignedoutComponent {
|
||||
|
||||
const lP = localStorage.getItem(LABELPOLICY_LOCALSTORAGE_KEY);
|
||||
|
||||
if (lP) {
|
||||
if (!lP) {
|
||||
authService.labelPolicyLoading$.next(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(lP);
|
||||
localStorage.removeItem(LABELPOLICY_LOCALSTORAGE_KEY);
|
||||
if (parsed) {
|
||||
|
||||
if (!parsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.labelpolicy = parsed;
|
||||
authService.labelpolicy.next(parsed);
|
||||
authService.labelPolicyLoading$.next(false);
|
||||
}
|
||||
} else {
|
||||
// todo: figure this one out
|
||||
// authService.labelpolicy.next(parsed);
|
||||
authService.labelPolicyLoading$.next(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,12 +2,17 @@
|
||||
title="{{ 'USER.CREATE.TITLE' | translate }}"
|
||||
[createSteps]="1"
|
||||
[currentCreateStep]="1"
|
||||
(closed)="close()"
|
||||
(closed)="location.back()"
|
||||
>
|
||||
<div class="user-create-main-content">
|
||||
<mat-progress-bar *ngIf="loading" color="primary" mode="indeterminate"></mat-progress-bar>
|
||||
|
||||
<form *ngIf="userForm" [formGroup]="userForm" (ngSubmit)="createUser()" class="user-create-form">
|
||||
<form
|
||||
*ngIf="pwdForm$ | async as pwdForm"
|
||||
[formGroup]="userForm"
|
||||
(ngSubmit)="createUser(pwdForm)"
|
||||
class="user-create-form"
|
||||
>
|
||||
<div class="user-create-content">
|
||||
<p class="user-create-section">{{ 'USER.CREATE.NAMEANDEMAILSECTION' | translate }}</p>
|
||||
|
||||
@@ -19,12 +24,13 @@
|
||||
<cnsl-form-field>
|
||||
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}</cnsl-label>
|
||||
<input
|
||||
*ngIf="suffixPadding$ | async as suffixPadding"
|
||||
cnslInput
|
||||
formControlName="userName"
|
||||
required
|
||||
[ngStyle]="{ 'padding-right': suffixPadding ? suffixPadding : '10px' }"
|
||||
[ngStyle]="{ 'padding-right': suffixPadding }"
|
||||
/>
|
||||
<span #suffix *ngIf="envSuffix" cnslSuffix>{{ envSuffix }}</span>
|
||||
<span #suffix *ngIf="envSuffix$ | async as envSuffix" cnslSuffix>{{ envSuffix }}</span>
|
||||
</cnsl-form-field>
|
||||
|
||||
<cnsl-form-field>
|
||||
@@ -42,28 +48,39 @@
|
||||
</div>
|
||||
|
||||
<div class="email-is-verified">
|
||||
<mat-checkbox class="block-checkbox" formControlName="isVerified">
|
||||
<mat-checkbox class="block-checkbox" formControlName="emailVerified">
|
||||
{{ 'USER.LOGINMETHODS.EMAIL.ISVERIFIED' | translate }}
|
||||
</mat-checkbox>
|
||||
<mat-checkbox
|
||||
*ngIf="((useV2Api$ | async) && !usePassword) || !userForm.controls.emailVerified.value"
|
||||
class="block-checkbox"
|
||||
formControlName="sendEmail"
|
||||
>
|
||||
{{ 'USER.PROFILE.SEND_EMAIL' | translate }}
|
||||
</mat-checkbox>
|
||||
<mat-checkbox class="block-checkbox" [(ngModel)]="usePassword" [ngModelOptions]="{ standalone: true }">
|
||||
{{ 'ORG.PAGES.USEPASSWORD' | translate }}
|
||||
</mat-checkbox>
|
||||
<cnsl-info-section class="full-width desc">
|
||||
<cnsl-info-section *ngIf="(useV2Api$ | async) === false" class="full-width desc">
|
||||
<span>{{ 'USER.CREATE.INITMAILDESCRIPTION' | translate }}</span>
|
||||
</cnsl-info-section>
|
||||
</div>
|
||||
|
||||
<div class="pwd-section" *ngIf="usePassword && pwdForm">
|
||||
<cnsl-password-complexity-view class="complexity-view" [policy]="this.policy" [password]="password">
|
||||
<div class="pwd-section" *ngIf="usePassword && (passwordComplexityPolicy$ | async) as passwordComplexityPolicy">
|
||||
<cnsl-password-complexity-view
|
||||
class="complexity-view"
|
||||
[policy]="passwordComplexityPolicy"
|
||||
[password]="pwdForm.controls.password"
|
||||
>
|
||||
</cnsl-password-complexity-view>
|
||||
|
||||
<form [formGroup]="pwdForm">
|
||||
<div class="user-create-grid">
|
||||
<cnsl-form-field *ngIf="password">
|
||||
<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 *ngIf="confirmPassword">
|
||||
<cnsl-form-field>
|
||||
<cnsl-label>{{ 'USER.PASSWORD.CONFIRMINITIAL' | translate }}</cnsl-label>
|
||||
<input
|
||||
cnslInput
|
||||
@@ -128,6 +145,18 @@
|
||||
<input cnslInput formControlName="phone" matTooltip="{{ 'USER.PROFILE.PHONE_HINT' | translate }}" />
|
||||
</cnsl-form-field>
|
||||
</div>
|
||||
<div class="phone-is-verified">
|
||||
<mat-checkbox *ngIf="useV2Api$ | async" class="block-checkbox" formControlName="phoneVerified">
|
||||
{{ 'USER.PROFILE.PHONE_VERIFIED' | translate }}
|
||||
</mat-checkbox>
|
||||
<mat-checkbox
|
||||
*ngIf="(useV2Api$ | async) && !userForm.controls.phoneVerified.value"
|
||||
class="block-checkbox"
|
||||
formControlName="sendSms"
|
||||
>
|
||||
{{ 'USER.PROFILE.SEND_SMS' | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-create-btn-container">
|
||||
<button
|
||||
|
@@ -26,6 +26,7 @@
|
||||
}
|
||||
|
||||
.email-is-verified,
|
||||
.phone-is-verified,
|
||||
.use-password-block {
|
||||
flex-basis: 100%;
|
||||
margin-top: 1.5rem;
|
||||
|
@@ -1,10 +1,19 @@
|
||||
import { Location } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms';
|
||||
import { Component, DestroyRef, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormBuilder, FormControl, ValidatorFn } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { Subject, debounceTime, Observable } from 'rxjs';
|
||||
import { AddHumanUserRequest } from 'src/app/proto/generated/zitadel/management_pb';
|
||||
import { Domain } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import {
|
||||
debounceTime,
|
||||
defer,
|
||||
of,
|
||||
Observable,
|
||||
shareReplay,
|
||||
firstValueFrom,
|
||||
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';
|
||||
@@ -23,249 +32,354 @@ import {
|
||||
passwordConfirmValidator,
|
||||
phoneValidator,
|
||||
requiredValidator,
|
||||
} from '../../../modules/form-field/validators/validators';
|
||||
import { LanguagesService } from '../../../services/languages.service';
|
||||
} 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 { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-user-create',
|
||||
templateUrl: './user-create.component.html',
|
||||
styleUrls: ['./user-create.component.scss'],
|
||||
})
|
||||
export class UserCreateComponent implements OnInit, OnDestroy {
|
||||
public user: AddHumanUserRequest.AsObject = new AddHumanUserRequest().toObject();
|
||||
public genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED];
|
||||
export class UserCreateComponent implements OnInit {
|
||||
public readonly genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED];
|
||||
public selected: CountryPhoneCode | undefined = {
|
||||
countryCallingCode: '1',
|
||||
countryCode: 'US',
|
||||
countryName: 'United States of America',
|
||||
};
|
||||
public countryPhoneCodes: CountryPhoneCode[] = [];
|
||||
public userForm!: UntypedFormGroup;
|
||||
public pwdForm!: UntypedFormGroup;
|
||||
private destroyed$: Subject<void> = new Subject();
|
||||
public readonly countryPhoneCodes: CountryPhoneCode[];
|
||||
|
||||
public userLoginMustBeDomain: boolean = false;
|
||||
public loading: boolean = false;
|
||||
public loading = false;
|
||||
|
||||
private readonly suffix$ = new ReplaySubject<HTMLSpanElement>(1);
|
||||
@ViewChild('suffix') public set suffix(suffix: ElementRef<HTMLSpanElement>) {
|
||||
this.suffix$.next(suffix.nativeElement);
|
||||
}
|
||||
|
||||
@ViewChild('suffix') public suffix!: any;
|
||||
private primaryDomain!: Domain.AsObject;
|
||||
public usePassword: boolean = false;
|
||||
public policy!: PasswordComplexityPolicy.AsObject;
|
||||
protected readonly useV2Api$: Observable<boolean>;
|
||||
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 suffixPadding$: Observable<string>;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private toast: ToastService,
|
||||
private fb: UntypedFormBuilder,
|
||||
private mgmtService: ManagementService,
|
||||
private changeDetRef: ChangeDetectorRef,
|
||||
private _location: Location,
|
||||
private countryCallingCodesService: CountryCallingCodesService,
|
||||
public langSvc: LanguagesService,
|
||||
breadcrumbService: BreadcrumbService,
|
||||
private readonly router: Router,
|
||||
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,
|
||||
countryCallingCodesService: CountryCallingCodesService,
|
||||
) {
|
||||
breadcrumbService.setBreadcrumb([
|
||||
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 }));
|
||||
|
||||
this.userForm = this.buildUserForm();
|
||||
this.pwdForm$ = this.buildPwdForm(this.passwordComplexityPolicy$);
|
||||
|
||||
this.countryPhoneCodes = countryCallingCodesService.getCountryCallingCodes();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// already start loading if we should use v2 api
|
||||
this.useV2Api$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe();
|
||||
this.watchPhoneChanges();
|
||||
|
||||
this.breadcrumbService.setBreadcrumb([
|
||||
new Breadcrumb({
|
||||
type: BreadcrumbType.ORG,
|
||||
routerLink: ['/org'],
|
||||
}),
|
||||
]);
|
||||
this.loading = true;
|
||||
this.loadOrg();
|
||||
this.mgmtService
|
||||
.getDomainPolicy()
|
||||
.then((resp) => {
|
||||
if (resp.policy?.userLoginMustBeDomain) {
|
||||
this.userLoginMustBeDomain = resp.policy.userLoginMustBeDomain;
|
||||
}
|
||||
this.initForm();
|
||||
this.loading = false;
|
||||
this.changeDetRef.detectChanges();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
this.initForm();
|
||||
this.loading = false;
|
||||
this.changeDetRef.detectChanges();
|
||||
|
||||
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());
|
||||
|
||||
return forkJoin([domainPolicy$, orgDomains$]).pipe(
|
||||
map(([policy, domains]) => {
|
||||
const userLoginMustBeDomain = policy.policy?.userLoginMustBeDomain;
|
||||
const primaryDomain = domains.resultList.find((resp) => resp.isPrimary);
|
||||
if (userLoginMustBeDomain && primaryDomain) {
|
||||
return `@${primaryDomain.domainName}`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}),
|
||||
catchError(() => of('')),
|
||||
);
|
||||
}
|
||||
|
||||
private getSuffixPadding() {
|
||||
return this.suffix$.pipe(
|
||||
map((suffix) => `${suffix.offsetWidth + 10}px`),
|
||||
startWith('10px'),
|
||||
);
|
||||
}
|
||||
|
||||
private getPasswordComplexityPolicy() {
|
||||
return defer(() => this.mgmtService.getPasswordComplexityPolicy()).pipe(
|
||||
map(({ policy }) => policy),
|
||||
filter(Boolean),
|
||||
catchError((error) => {
|
||||
this.toast.showError(error);
|
||||
return EMPTY;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public buildUserForm() {
|
||||
return this.fb.group({
|
||||
email: new FormControl('', { nonNullable: true, validators: [requiredValidator, emailValidator] }),
|
||||
userName: new FormControl('', { nonNullable: true, validators: [requiredValidator, minLengthValidator(2)] }),
|
||||
firstName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
lastName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
nickName: new FormControl('', { nonNullable: true }),
|
||||
gender: new FormControl(Gender.GENDER_UNSPECIFIED, { nonNullable: true, validators: [requiredValidator] }),
|
||||
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 close(): void {
|
||||
this._location.back();
|
||||
public buildPwdForm(passwordComplexityPolicy$: Observable<PasswordComplexityPolicy.AsObject>) {
|
||||
return passwordComplexityPolicy$.pipe(
|
||||
map((policy) => {
|
||||
const validators: [ValidatorFn] = [requiredValidator];
|
||||
if (policy.minLength) {
|
||||
validators.push(minLengthValidator(policy.minLength));
|
||||
}
|
||||
|
||||
private async loadOrg(): Promise<void> {
|
||||
const domains = await this.mgmtService.listOrgDomains();
|
||||
const found = domains.resultList.find((resp) => resp.isPrimary);
|
||||
if (found) {
|
||||
this.primaryDomain = found;
|
||||
}
|
||||
}
|
||||
|
||||
private initForm(): void {
|
||||
this.userForm = this.fb.group({
|
||||
email: ['', [requiredValidator, emailValidator]],
|
||||
userName: ['', [requiredValidator, minLengthValidator(2)]],
|
||||
firstName: ['', requiredValidator],
|
||||
lastName: ['', requiredValidator],
|
||||
nickName: [''],
|
||||
gender: [],
|
||||
preferredLanguage: [''],
|
||||
phone: ['', phoneValidator],
|
||||
isVerified: [false, []],
|
||||
});
|
||||
|
||||
const validators: Validators[] = [requiredValidator];
|
||||
|
||||
this.mgmtService.getPasswordComplexityPolicy().then((data) => {
|
||||
if (data.policy) {
|
||||
this.policy = data.policy;
|
||||
|
||||
if (this.policy.minLength) {
|
||||
validators.push(minLengthValidator(this.policy.minLength));
|
||||
}
|
||||
if (this.policy.hasLowercase) {
|
||||
if (policy.hasLowercase) {
|
||||
validators.push(containsLowerCaseValidator);
|
||||
}
|
||||
if (this.policy.hasUppercase) {
|
||||
if (policy.hasUppercase) {
|
||||
validators.push(containsUpperCaseValidator);
|
||||
}
|
||||
if (this.policy.hasNumber) {
|
||||
if (policy.hasNumber) {
|
||||
validators.push(containsNumberValidator);
|
||||
}
|
||||
if (this.policy.hasSymbol) {
|
||||
if (policy.hasSymbol) {
|
||||
validators.push(containsSymbolValidator);
|
||||
}
|
||||
const pwdValidators = [...validators] as ValidatorFn[];
|
||||
const confirmPwdValidators = [requiredValidator, passwordConfirmValidator()] as ValidatorFn[];
|
||||
|
||||
this.pwdForm = this.fb.group({
|
||||
password: ['', pwdValidators],
|
||||
confirmPassword: ['', confirmPwdValidators],
|
||||
return this.fb.group({
|
||||
password: new FormControl('', { nonNullable: true, validators }),
|
||||
confirmPassword: new FormControl('', {
|
||||
nonNullable: true,
|
||||
validators: [requiredValidator, passwordConfirmValidator()],
|
||||
}),
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.phone?.valueChanges.pipe(debounceTime(200)).subscribe((value: string) => {
|
||||
private watchPhoneChanges(): void {
|
||||
const phone = this.userForm.controls.phone;
|
||||
|
||||
phone.valueChanges.pipe(debounceTime(200), takeUntilDestroyed(this.destroyRef)).subscribe((value) => {
|
||||
const phoneNumber = formatPhone(value);
|
||||
if (phoneNumber) {
|
||||
this.selected = this.countryPhoneCodes.find((code) => code.countryCode === phoneNumber.country);
|
||||
this.phone?.setValue(phoneNumber.phone);
|
||||
phone.setValue(phoneNumber.phone);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public createUser(): void {
|
||||
this.user = this.userForm.value;
|
||||
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> {
|
||||
this.loading = true;
|
||||
|
||||
const controls = this.userForm.controls;
|
||||
const profileReq = new AddHumanUserRequest.Profile();
|
||||
profileReq.setFirstName(this.firstName?.value);
|
||||
profileReq.setLastName(this.lastName?.value);
|
||||
profileReq.setNickName(this.nickName?.value);
|
||||
profileReq.setPreferredLanguage(this.preferredLanguage?.value);
|
||||
profileReq.setGender(this.gender?.value);
|
||||
profileReq.setFirstName(controls.firstName.value);
|
||||
profileReq.setLastName(controls.lastName.value);
|
||||
profileReq.setNickName(controls.nickName.value);
|
||||
profileReq.setPreferredLanguage(controls.preferredLanguage.value);
|
||||
profileReq.setGender(controls.gender.value);
|
||||
|
||||
const humanReq = new AddHumanUserRequest();
|
||||
humanReq.setUserName(this.userName?.value);
|
||||
humanReq.setUserName(controls.userName.value);
|
||||
humanReq.setProfile(profileReq);
|
||||
|
||||
const emailreq = new AddHumanUserRequest.Email();
|
||||
emailreq.setEmail(this.email?.value);
|
||||
emailreq.setIsEmailVerified(this.isVerified?.value);
|
||||
emailreq.setEmail(controls.email.value);
|
||||
emailreq.setIsEmailVerified(controls.emailVerified.value);
|
||||
humanReq.setEmail(emailreq);
|
||||
|
||||
if (this.usePassword && this.password?.value) {
|
||||
humanReq.setInitialPassword(this.password.value);
|
||||
if (this.usePassword) {
|
||||
humanReq.setInitialPassword(pwdForm.controls.password.value);
|
||||
}
|
||||
|
||||
if (this.phone && this.phone.value) {
|
||||
if (controls.phone.value) {
|
||||
// Try to parse number and format it according to country
|
||||
const phoneNumber = formatPhone(this.phone.value);
|
||||
const phoneNumber = formatPhone(controls.phone.value);
|
||||
if (phoneNumber) {
|
||||
this.selected = this.countryPhoneCodes.find((code) => code.countryCode === phoneNumber.country);
|
||||
humanReq.setPhone(new AddHumanUserRequest.Phone().setPhone(phoneNumber.phone));
|
||||
}
|
||||
}
|
||||
|
||||
this.mgmtService
|
||||
.addHumanUser(humanReq)
|
||||
.then((data) => {
|
||||
this.loading = false;
|
||||
try {
|
||||
const data = await this.mgmtService.addHumanUser(humanReq);
|
||||
this.toast.showInfo('USER.TOAST.CREATED', true);
|
||||
this.router.navigate(['users', data.userId], { queryParams: { new: true } });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.loading = false;
|
||||
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);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public get sendEmailAfterCreation() {
|
||||
const controls = this.userForm.controls;
|
||||
|
||||
const sendEmailAfterCreationIsAOption = controls.emailVerified.value && !this.usePassword;
|
||||
return sendEmailAfterCreationIsAOption && controls.sendEmail.value;
|
||||
}
|
||||
|
||||
public setCountryCallingCode(): void {
|
||||
let value = (this.phone?.value as string) || '';
|
||||
let value = this.userForm.controls.phone.value;
|
||||
this.countryPhoneCodes.forEach((code) => (value = value.replace(`+${code.countryCallingCode}`, '')));
|
||||
value = value.trim();
|
||||
this.phone?.setValue('+' + this.selected?.countryCallingCode + ' ' + value);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.countryPhoneCodes = this.countryCallingCodesService.getCountryCallingCodes();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
|
||||
public get email(): AbstractControl | null {
|
||||
return this.userForm.get('email');
|
||||
}
|
||||
public get isVerified(): AbstractControl | null {
|
||||
return this.userForm.get('isVerified');
|
||||
}
|
||||
public get userName(): AbstractControl | null {
|
||||
return this.userForm.get('userName');
|
||||
}
|
||||
public get firstName(): AbstractControl | null {
|
||||
return this.userForm.get('firstName');
|
||||
}
|
||||
public get lastName(): AbstractControl | null {
|
||||
return this.userForm.get('lastName');
|
||||
}
|
||||
public get nickName(): AbstractControl | null {
|
||||
return this.userForm.get('nickName');
|
||||
}
|
||||
public get gender(): AbstractControl | null {
|
||||
return this.userForm.get('gender');
|
||||
}
|
||||
public get preferredLanguage(): AbstractControl | null {
|
||||
return this.userForm.get('preferredLanguage');
|
||||
}
|
||||
public get phone(): AbstractControl | null {
|
||||
return this.userForm.get('phone');
|
||||
}
|
||||
|
||||
public get password(): AbstractControl | null {
|
||||
return this.pwdForm.get('password');
|
||||
}
|
||||
public get confirmPassword(): AbstractControl | null {
|
||||
return this.pwdForm.get('confirmPassword');
|
||||
}
|
||||
|
||||
public get envSuffix(): string {
|
||||
if (this.userLoginMustBeDomain && this.primaryDomain?.domainName) {
|
||||
return `@${this.primaryDomain.domainName}`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public get suffixPadding(): string | undefined {
|
||||
if (this.suffix?.nativeElement.offsetWidth) {
|
||||
return `${(this.suffix.nativeElement as HTMLElement).offsetWidth + 10}px`;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
this.userForm.controls.phone.setValue('+' + this.selected?.countryCallingCode + ' ' + value);
|
||||
}
|
||||
|
||||
public compareCountries(i1: CountryPhoneCode, i2: CountryPhoneCode) {
|
||||
|
@@ -1,54 +1,59 @@
|
||||
<ng-container *ngIf="user$ | async as userQuery">
|
||||
<cnsl-top-view
|
||||
title="{{ user && user.human ? user.human.profile?.displayName : user?.machine?.name }}"
|
||||
sub="{{ user?.preferredLoginName }}"
|
||||
[isActive]="user?.state === UserState.USER_STATE_ACTIVE"
|
||||
[isInactive]="user?.state === UserState.USER_STATE_INACTIVE"
|
||||
stateTooltip="{{ 'USER.STATE.' + user?.state | translate }}"
|
||||
title="{{ userName$ | async }}"
|
||||
sub="{{ user(userQuery)?.preferredLoginName }}"
|
||||
[isActive]="user(userQuery)?.state === UserState.USER_STATE_ACTIVE"
|
||||
[isInactive]="user(userQuery)?.state === UserState.USER_STATE_INACTIVE"
|
||||
stateTooltip="{{ 'USER.STATE.' + user(userQuery)?.state | translate }}"
|
||||
[hasBackButton]="['org.read'] | hasRole | async"
|
||||
(backRouterLink)="(['/'])"
|
||||
>
|
||||
<span *ngIf="!loading && !user">{{ 'USER.PAGES.NOUSER' | translate }}</span>
|
||||
<cnsl-info-row topContent *ngIf="user" [user]="user" [loginPolicy]="loginPolicy"></cnsl-info-row>
|
||||
<span *ngIf="userQuery.state === 'notfound'">{{ 'USER.PAGES.NOUSER' | translate }}</span>
|
||||
<cnsl-info-row
|
||||
topContent
|
||||
*ngIf="user(userQuery) as user"
|
||||
[user]="user"
|
||||
[loginPolicy]="(loginPolicy$ | async) ?? undefined"
|
||||
></cnsl-info-row>
|
||||
</cnsl-top-view>
|
||||
|
||||
<div *ngIf="loading" class="max-width-container">
|
||||
<div *ngIf="(user$ | async)?.state === 'loading'" class="max-width-container">
|
||||
<div class="user-spinner-wrapper">
|
||||
<mat-progress-spinner diameter="25" color="primary" mode="indeterminate"></mat-progress-spinner>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-width-container">
|
||||
<cnsl-meta-layout>
|
||||
<cnsl-meta-layout *ngIf="user(userQuery) as user">
|
||||
<cnsl-sidenav
|
||||
[(ngModel)]="currentSetting"
|
||||
*ngIf="currentSetting$ | async as currentSetting"
|
||||
[ngModel]="currentSetting"
|
||||
(ngModelChange)="goToSetting($event)"
|
||||
[settingsList]="settingsList"
|
||||
queryParam="id"
|
||||
(ngModelChange)="settingChanged()"
|
||||
>
|
||||
<ng-container *ngIf="currentSetting === 'general'">
|
||||
<ng-container *ngIf="currentSetting === 'general' && humanUser(userQuery) as humanUser">
|
||||
<cnsl-card
|
||||
*ngIf="user && user.human && user.human.profile"
|
||||
*ngIf="humanUser.type.value.profile as profile"
|
||||
class="app-card"
|
||||
title="{{ 'USER.PROFILE.TITLE' | translate }}"
|
||||
>
|
||||
<cnsl-detail-form
|
||||
[showEditImage]="true"
|
||||
[preferredLoginName]="user.preferredLoginName"
|
||||
[disabled]="false"
|
||||
[genders]="genders"
|
||||
[languages]="(langSvc.supported$ | async) || []"
|
||||
[username]="user.userName"
|
||||
[user]="user.human"
|
||||
[disabled]="false"
|
||||
[profile]="profile"
|
||||
[showEditImage]="true"
|
||||
(changedLanguage)="changedLanguage($event)"
|
||||
(changeUsernameClicked)="changeUsername()"
|
||||
(submitData)="saveProfile($event)"
|
||||
(avatarChanged)="refreshUser()"
|
||||
(changeUsernameClicked)="changeUsername(user)"
|
||||
(submitData)="saveProfile(user, $event)"
|
||||
(avatarChanged)="refreshChanges$.emit()"
|
||||
>
|
||||
</cnsl-detail-form>
|
||||
</cnsl-card>
|
||||
|
||||
<cnsl-card
|
||||
*ngIf="user"
|
||||
title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}"
|
||||
description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}"
|
||||
>
|
||||
@@ -56,22 +61,20 @@
|
||||
class="icon-button"
|
||||
card-actions
|
||||
mat-icon-button
|
||||
(click)="refreshUser()"
|
||||
(click)="refreshChanges$.emit()"
|
||||
matTooltip="{{ 'ACTIONS.REFRESH' | translate }}"
|
||||
>
|
||||
<mat-icon class="icon">refresh</mat-icon>
|
||||
</button>
|
||||
<cnsl-contact
|
||||
*ngIf="user.human"
|
||||
[human]="user.human"
|
||||
[human]="humanUser.type.value"
|
||||
[username]="user.preferredLoginName"
|
||||
[state]="user.state"
|
||||
[canWrite]="true"
|
||||
(editType)="openEditDialog($event)"
|
||||
(editType)="openEditDialog(humanUser, $event)"
|
||||
(enteredPhoneCode)="enteredPhoneCode($event)"
|
||||
(deletedPhone)="deletePhone()"
|
||||
(resendEmailVerification)="resendEmailVerification()"
|
||||
(resendPhoneVerification)="resendPhoneVerification()"
|
||||
(deletedPhone)="deletePhone(user)"
|
||||
(resendEmailVerification)="resendEmailVerification(user)"
|
||||
(resendPhoneVerification)="resendPhoneVerification(user)"
|
||||
>
|
||||
</cnsl-contact>
|
||||
</cnsl-card>
|
||||
@@ -81,7 +84,7 @@
|
||||
<p>{{ 'USER.PAGES.DELETEACCOUNT_DESC' | translate }}</p>
|
||||
|
||||
<div class="delete-account-wrapper">
|
||||
<button color="warn" mat-raised-button (click)="deleteAccount()">
|
||||
<button color="warn" mat-raised-button (click)="deleteUser(user)">
|
||||
{{ 'USER.PAGES.DELETEACCOUNT_BTN' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -90,11 +93,11 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'idp'">
|
||||
<cnsl-external-idps *ngIf="user && user.id" [userId]="user.id" [service]="userService"></cnsl-external-idps>
|
||||
<cnsl-external-idps [userId]="user.id" [service]="grpcAuthService"></cnsl-external-idps>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'security'">
|
||||
<cnsl-card *ngIf="user && user.human" title="{{ 'USER.PASSWORD.TITLE' | translate }}">
|
||||
<cnsl-card *ngIf="humanUser(userQuery) as humanUser" title="{{ 'USER.PASSWORD.TITLE' | translate }}">
|
||||
<div class="contact-method-col">
|
||||
<div class="contact-method-row">
|
||||
<div class="left">
|
||||
@@ -108,7 +111,7 @@
|
||||
<a
|
||||
matTooltip="{{ 'USER.PASSWORD.SET' | translate }}"
|
||||
[routerLink]="['password']"
|
||||
[queryParams]="{ username: user.preferredLoginName }"
|
||||
[queryParams]="{ username: humanUser.preferredLoginName }"
|
||||
mat-icon-button
|
||||
>
|
||||
<i class="las la-pen"></i>
|
||||
@@ -118,21 +121,16 @@
|
||||
</div>
|
||||
</cnsl-card>
|
||||
|
||||
<cnsl-auth-passwordless *ngIf="user" #mfaComponent></cnsl-auth-passwordless>
|
||||
<cnsl-auth-passwordless #mfaComponent></cnsl-auth-passwordless>
|
||||
|
||||
<cnsl-auth-user-mfa
|
||||
[phoneVerified]="user.human?.phone?.isPhoneVerified ?? false"
|
||||
*ngIf="user"
|
||||
[phoneVerified]="humanUser(userQuery)?.type?.value?.phone?.isPhoneVerified ?? false"
|
||||
#mfaComponent
|
||||
></cnsl-auth-user-mfa>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'grants'">
|
||||
<cnsl-card
|
||||
*ngIf="user && user.id"
|
||||
title="{{ 'GRANTS.USER.TITLE' | translate }}"
|
||||
description="{{ 'GRANTS.USER.DESCRIPTION' | translate }}"
|
||||
>
|
||||
<cnsl-card title="{{ 'GRANTS.USER.TITLE' | translate }}" description="{{ 'GRANTS.USER.DESCRIPTION' | translate }}">
|
||||
<cnsl-user-grants
|
||||
[userId]="user.id"
|
||||
[context]="USERGRANTCONTEXT"
|
||||
@@ -155,7 +153,6 @@
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'memberships'">
|
||||
<cnsl-card
|
||||
*ngIf="user?.id"
|
||||
title="{{ 'USER.MEMBERSHIPS.TITLE' | translate }}"
|
||||
description="{{ 'USER.MEMBERSHIPS.DESCRIPTION' | translate }}"
|
||||
>
|
||||
@@ -163,14 +160,15 @@
|
||||
</cnsl-card>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'metadata'">
|
||||
<ng-container *ngIf="currentSetting === 'metadata' && (metadata$ | async) as metadataQuery">
|
||||
<cnsl-metadata
|
||||
[metadata]="metadata"
|
||||
*ngIf="metadataQuery.state !== 'error'"
|
||||
[metadata]="metadataQuery.value"
|
||||
[description]="'DESCRIPTIONS.USERS.SELF.METADATA' | translate"
|
||||
[disabled]="(['user.write:' + user.id, 'user.write'] | hasRole | async) === false"
|
||||
*ngIf="user && user.id"
|
||||
(editClicked)="editMetadata()"
|
||||
(refresh)="loadMetadata()"
|
||||
(editClicked)="editMetadata(user, metadataQuery.value)"
|
||||
(refresh)="refreshMetadata$.next(true)"
|
||||
[loading]="metadataQuery.state === 'loading'"
|
||||
></cnsl-metadata>
|
||||
</ng-container>
|
||||
</cnsl-sidenav>
|
||||
@@ -180,3 +178,4 @@
|
||||
</div>
|
||||
</cnsl-meta-layout>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@@ -1,45 +1,73 @@
|
||||
import { MediaMatcher } from '@angular/cdk/layout';
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, EventEmitter, OnDestroy } from '@angular/core';
|
||||
import { Component, DestroyRef, EventEmitter, OnInit } from '@angular/core';
|
||||
import { Validators } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Buffer } from 'buffer';
|
||||
import { from, Observable, Subscription, take } from 'rxjs';
|
||||
import {
|
||||
combineLatestWith,
|
||||
defer,
|
||||
EMPTY,
|
||||
fromEvent,
|
||||
mergeWith,
|
||||
Observable,
|
||||
of,
|
||||
shareReplay,
|
||||
Subject,
|
||||
switchMap,
|
||||
take,
|
||||
} from 'rxjs';
|
||||
import { ChangeType } from 'src/app/modules/changes/changes.component';
|
||||
import { phoneValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators';
|
||||
import { InfoDialogComponent } from 'src/app/modules/info-dialog/info-dialog.component';
|
||||
import { MetadataDialogComponent } from 'src/app/modules/metadata/metadata-dialog/metadata-dialog.component';
|
||||
import {
|
||||
MetadataDialogComponent,
|
||||
MetadataDialogData,
|
||||
} from 'src/app/modules/metadata/metadata-dialog/metadata-dialog.component';
|
||||
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
|
||||
import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component';
|
||||
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
|
||||
import { LoginPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
||||
import { Email, Gender, Phone, Profile, User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { AuthenticationService } from 'src/app/services/authentication.service';
|
||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { formatPhone } from 'src/app/utils/formatPhone';
|
||||
import { EditDialogComponent, EditDialogType } from './edit-dialog/edit-dialog.component';
|
||||
import { LanguagesService } from '../../../../services/languages.service';
|
||||
import { EditDialogComponent, EditDialogData, EditDialogResult, EditDialogType } from './edit-dialog/edit-dialog.component';
|
||||
import { LanguagesService } from 'src/app/services/languages.service';
|
||||
import { Gender, HumanProfile } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { catchError, filter, map, startWith, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import { pairwiseStartWith } from 'src/app/utils/pairwiseStartWith';
|
||||
import { NewAuthService } from 'src/app/services/new-auth.service';
|
||||
import { Human, User, UserState } from '@zitadel/proto/zitadel/user_pb';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
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';
|
||||
|
||||
type UserQuery =
|
||||
| { state: 'success'; value: User }
|
||||
| { state: 'error'; value: string }
|
||||
| { state: 'loading'; value?: User }
|
||||
| { state: 'notfound' };
|
||||
|
||||
type MetadataQuery =
|
||||
| { state: 'success'; value: Metadata[] }
|
||||
| { state: 'loading'; value: Metadata[] }
|
||||
| { state: 'error'; value: string };
|
||||
|
||||
type UserWithHumanType = Omit<User, 'type'> & { type: { case: 'human'; value: Human } };
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-auth-user-detail',
|
||||
templateUrl: './auth-user-detail.component.html',
|
||||
styleUrls: ['./auth-user-detail.component.scss'],
|
||||
})
|
||||
export class AuthUserDetailComponent implements OnDestroy {
|
||||
public user?: User.AsObject;
|
||||
public genders: Gender[] = [Gender.GENDER_MALE, Gender.GENDER_FEMALE, Gender.GENDER_DIVERSE];
|
||||
|
||||
private subscription: Subscription = new Subscription();
|
||||
|
||||
public loading: boolean = false;
|
||||
public loadingMetadata: boolean = false;
|
||||
export class AuthUserDetailComponent implements OnInit {
|
||||
public genders: Gender[] = [Gender.MALE, Gender.FEMALE, Gender.DIVERSE];
|
||||
|
||||
public ChangeType: any = ChangeType;
|
||||
public userLoginMustBeDomain: boolean = false;
|
||||
@@ -47,8 +75,7 @@ export class AuthUserDetailComponent implements OnDestroy {
|
||||
|
||||
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.AUTHUSER;
|
||||
public refreshChanges$: EventEmitter<void> = new EventEmitter();
|
||||
|
||||
public metadata: Metadata.AsObject[] = [];
|
||||
public refreshMetadata$ = new Subject<true>();
|
||||
|
||||
public settingsList: SidenavSetting[] = [
|
||||
{ id: 'general', i18nKey: 'USER.SETTINGS.GENERAL' },
|
||||
@@ -62,182 +89,230 @@ export class AuthUserDetailComponent implements OnDestroy {
|
||||
requiredRoles: { [PolicyComponentServiceType.MGMT]: ['user.read'] },
|
||||
},
|
||||
];
|
||||
public currentSetting: string | undefined = this.settingsList[0].id;
|
||||
public loginPolicy?: LoginPolicy.AsObject;
|
||||
private savedLanguage?: string;
|
||||
protected readonly user$: Observable<UserQuery>;
|
||||
protected readonly metadata$: Observable<MetadataQuery>;
|
||||
private readonly savedLanguage$: Observable<string>;
|
||||
protected currentSetting$: Observable<string | undefined>;
|
||||
public loginPolicy$: Observable<LoginPolicy>;
|
||||
protected userName$: Observable<string>;
|
||||
|
||||
constructor(
|
||||
public translate: TranslateService,
|
||||
private toast: ToastService,
|
||||
public userService: GrpcAuthService,
|
||||
public grpcAuthService: GrpcAuthService,
|
||||
private dialog: MatDialog,
|
||||
private auth: AuthenticationService,
|
||||
private mgmt: ManagementService,
|
||||
private breadcrumbService: BreadcrumbService,
|
||||
private mediaMatcher: MediaMatcher,
|
||||
private _location: Location,
|
||||
activatedRoute: ActivatedRoute,
|
||||
public langSvc: LanguagesService,
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly newAuthService: NewAuthService,
|
||||
private readonly newMgmtService: NewMgmtService,
|
||||
private readonly userService: UserService,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly router: Router,
|
||||
) {
|
||||
activatedRoute.queryParams.pipe(take(1)).subscribe((params: Params) => {
|
||||
const { id } = params;
|
||||
if (id) {
|
||||
this.cleanupTranslation();
|
||||
this.currentSetting = id;
|
||||
}
|
||||
});
|
||||
this.currentSetting$ = this.getCurrentSetting$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.user$ = this.getUser$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.userName$ = this.getUserName(this.user$);
|
||||
this.savedLanguage$ = this.getSavedLanguage$(this.user$);
|
||||
this.metadata$ = this.getMetadata$(this.user$).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
|
||||
const mediaq: string = '(max-width: 500px)';
|
||||
const small = this.mediaMatcher.matchMedia(mediaq).matches;
|
||||
if (small) {
|
||||
this.changeSelection(small);
|
||||
}
|
||||
this.mediaMatcher.matchMedia(mediaq).onchange = (small) => {
|
||||
this.changeSelection(small.matches);
|
||||
};
|
||||
|
||||
this.loading = true;
|
||||
this.refreshUser();
|
||||
|
||||
this.userService.getMyLoginPolicy().then((policy) => {
|
||||
if (policy.policy) {
|
||||
this.loginPolicy = policy.policy;
|
||||
}
|
||||
});
|
||||
this.loginPolicy$ = defer(() => this.newMgmtService.getLoginPolicy()).pipe(
|
||||
catchError(() => EMPTY),
|
||||
map(({ policy }) => policy),
|
||||
filter(Boolean),
|
||||
);
|
||||
}
|
||||
|
||||
private changeSelection(small: boolean): void {
|
||||
this.cleanupTranslation();
|
||||
if (small) {
|
||||
this.currentSetting = undefined;
|
||||
} else {
|
||||
this.currentSetting = this.currentSetting === undefined ? this.settingsList[0].id : this.currentSetting;
|
||||
getUserName(user$: Observable<UserQuery>) {
|
||||
return user$.pipe(
|
||||
map((query) => {
|
||||
const user = this.user(query);
|
||||
if (!user) {
|
||||
return '';
|
||||
}
|
||||
if (user.type.case === 'human') {
|
||||
return user.type.value.profile?.displayName ?? '';
|
||||
}
|
||||
if (user.type.case === 'machine') {
|
||||
return user.type.value.name;
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public navigateBack(): void {
|
||||
this._location.back();
|
||||
getSavedLanguage$(user$: Observable<UserQuery>) {
|
||||
return user$.pipe(
|
||||
switchMap((query) => {
|
||||
if (query.state !== 'success' || query.value.type.case !== 'human') {
|
||||
return EMPTY;
|
||||
}
|
||||
return query.value.type.value.profile?.preferredLanguage ?? EMPTY;
|
||||
}),
|
||||
startWith(this.translate.defaultLang),
|
||||
);
|
||||
}
|
||||
|
||||
refreshUser(): void {
|
||||
this.refreshChanges$.emit();
|
||||
this.userService
|
||||
.getMyUser()
|
||||
.then((resp) => {
|
||||
if (resp.user) {
|
||||
this.user = resp.user;
|
||||
|
||||
this.loadMetadata();
|
||||
|
||||
ngOnInit(): void {
|
||||
this.user$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((query) => {
|
||||
if ((query.state === 'loading' || query.state === 'success') && query.value?.type.case === 'human') {
|
||||
this.breadcrumbService.setBreadcrumb([
|
||||
new Breadcrumb({
|
||||
type: BreadcrumbType.AUTHUSER,
|
||||
name: this.user.human?.profile?.displayName,
|
||||
name: query.value.type.value.profile?.displayName,
|
||||
routerLink: ['/users', 'me'],
|
||||
}),
|
||||
]);
|
||||
}
|
||||
this.savedLanguage = resp.user?.human?.profile?.preferredLanguage;
|
||||
this.loading = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
this.loading = false;
|
||||
});
|
||||
this.user$.pipe(mergeWith(this.metadata$), takeUntilDestroyed(this.destroyRef)).subscribe((query) => {
|
||||
if (query.state == 'error') {
|
||||
this.toast.showError(query.value);
|
||||
}
|
||||
});
|
||||
|
||||
this.savedLanguage$
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((savedLanguage) => this.translate.use(savedLanguage));
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.cleanupTranslation();
|
||||
this.subscription.unsubscribe();
|
||||
private getCurrentSetting$(): Observable<string | undefined> {
|
||||
const mediaq: string = '(max-width: 500px)';
|
||||
const matcher = this.mediaMatcher.matchMedia(mediaq);
|
||||
const small$ = fromEvent(matcher, 'change', ({ matches }: MediaQueryListEvent) => matches).pipe(
|
||||
startWith(matcher.matches),
|
||||
);
|
||||
|
||||
return this.route.queryParamMap.pipe(
|
||||
map((params) => params.get('id')),
|
||||
filter(Boolean),
|
||||
startWith('general'),
|
||||
withLatestFrom(small$),
|
||||
map(([id, small]) => (small ? undefined : id)),
|
||||
);
|
||||
}
|
||||
|
||||
public settingChanged(): void {
|
||||
this.cleanupTranslation();
|
||||
private getUser$(): Observable<UserQuery> {
|
||||
return this.refreshChanges$.pipe(
|
||||
startWith(true),
|
||||
switchMap(() => this.getMyUser()),
|
||||
pairwiseStartWith(undefined),
|
||||
map(([prev, curr]) => {
|
||||
if (prev?.state === 'success' && curr.state === 'loading') {
|
||||
return { state: 'loading', value: prev.value } as const;
|
||||
}
|
||||
return curr;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private cleanupTranslation(): void {
|
||||
if (this?.savedLanguage) {
|
||||
this.translate.use(this?.savedLanguage);
|
||||
} else {
|
||||
this.translate.use(this.translate.defaultLang);
|
||||
private getMyUser(): Observable<UserQuery> {
|
||||
return defer(() => this.newAuthService.getMyUser()).pipe(
|
||||
map(({ user }) => {
|
||||
if (user) {
|
||||
return { state: 'success', value: user } as const;
|
||||
}
|
||||
return { state: 'notfound' } as const;
|
||||
}),
|
||||
catchError((error) => of({ state: 'error', value: error.message ?? '' } as const)),
|
||||
startWith({ state: 'loading' } as const),
|
||||
);
|
||||
}
|
||||
|
||||
public changeUsername(): void {
|
||||
const dialogRef = this.dialog.open(EditDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.CHANGE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
labelKey: 'ACTIONS.NEWVALUE',
|
||||
titleKey: 'USER.PROFILE.CHANGEUSERNAME_TITLE',
|
||||
descriptionKey: 'USER.PROFILE.CHANGEUSERNAME_DESC',
|
||||
value: this.user?.userName,
|
||||
},
|
||||
getMetadata$(user$: Observable<UserQuery>): Observable<MetadataQuery> {
|
||||
return this.refreshMetadata$.pipe(
|
||||
startWith(true),
|
||||
combineLatestWith(user$),
|
||||
switchMap(([_, user]) => {
|
||||
if (!(user.state === 'success' || user.state === 'loading')) {
|
||||
return EMPTY;
|
||||
}
|
||||
if (!user.value) {
|
||||
return EMPTY;
|
||||
}
|
||||
return this.getMetadataById(user.value.id);
|
||||
}),
|
||||
pairwiseStartWith(undefined),
|
||||
map(([prev, curr]) => {
|
||||
if (prev?.state === 'success' && curr.state === 'loading') {
|
||||
return { state: 'loading', value: prev.value } as const;
|
||||
}
|
||||
return curr;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getMetadataById(userId: string): Observable<MetadataQuery> {
|
||||
return defer(() => this.newMgmtService.listUserMetadata(userId)).pipe(
|
||||
map((metadata) => ({ state: 'success', value: metadata.result }) as const),
|
||||
startWith({ state: 'loading', value: [] as Metadata[] } as const),
|
||||
catchError((err) => of({ state: 'error', value: err.message ?? '' } as const)),
|
||||
);
|
||||
}
|
||||
|
||||
public changeUsername(user: User): void {
|
||||
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,
|
||||
value: user.userName,
|
||||
};
|
||||
const dialogRef = this.dialog.open<EditDialogComponent, typeof data, EditDialogResult>(EditDialogComponent, {
|
||||
data,
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((resp: { value: string }) => {
|
||||
if (resp && resp.value && resp.value !== this.user?.userName) {
|
||||
this.userService
|
||||
.updateMyUserName(resp.value)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.USERNAMECHANGED', true);
|
||||
this.refreshUser();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public saveProfile(profileData: Profile.AsObject): void {
|
||||
if (this.user?.human) {
|
||||
this.user.human.profile = profileData;
|
||||
|
||||
this.userService
|
||||
.updateMyProfile(
|
||||
this.user.human.profile?.firstName,
|
||||
this.user.human.profile?.lastName,
|
||||
this.user.human.profile?.nickName,
|
||||
this.user.human.profile?.displayName,
|
||||
this.user.human.profile?.preferredLanguage,
|
||||
this.user.human.profile?.gender,
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
map((value) => value?.value),
|
||||
filter(Boolean),
|
||||
filter((value) => user.userName != value),
|
||||
switchMap((username) => this.userService.updateUser({ userId: user.id, username })),
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toast.showInfo('USER.TOAST.USERNAMECHANGED', true);
|
||||
this.refreshChanges$.emit();
|
||||
},
|
||||
error: (error) => {
|
||||
this.toast.showError(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public saveProfile(user: User, profile: HumanProfile): void {
|
||||
this.userService
|
||||
.updateUser({
|
||||
userId: user.id,
|
||||
profile: {
|
||||
givenName: profile.givenName,
|
||||
familyName: profile.familyName,
|
||||
nickName: profile.nickName,
|
||||
displayName: profile.displayName,
|
||||
preferredLanguage: profile.preferredLanguage,
|
||||
gender: profile.gender,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.SAVED', true);
|
||||
this.savedLanguage = this.user?.human?.profile?.preferredLanguage;
|
||||
this.refreshChanges$.emit();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public saveEmail(email: string): void {
|
||||
this.userService
|
||||
.setMyEmail(email)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
|
||||
if (this.user?.human) {
|
||||
const mailToSet = new Email();
|
||||
mailToSet.setEmail(email);
|
||||
this.user.human.email = mailToSet.toObject();
|
||||
this.refreshUser();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public enteredPhoneCode(code: string): void {
|
||||
this.userService
|
||||
this.newAuthService
|
||||
.verifyMyPhone(code)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
|
||||
this.refreshUser();
|
||||
this.refreshChanges$.emit();
|
||||
this.promptSetupforSMSOTP();
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -256,17 +331,16 @@ export class AuthUserDetailComponent implements OnDestroy {
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.userService.addMyAuthFactorOTPSMS().then(() => {
|
||||
this.translate
|
||||
.get('USER.MFA.OTPSMSSUCCESS')
|
||||
.pipe(take(1))
|
||||
.subscribe((msg) => {
|
||||
this.toast.showInfo(msg);
|
||||
});
|
||||
});
|
||||
}
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -274,21 +348,9 @@ export class AuthUserDetailComponent implements OnDestroy {
|
||||
this.translate.use(language);
|
||||
}
|
||||
|
||||
public resendPhoneVerification(): void {
|
||||
this.userService
|
||||
.resendMyPhoneVerification()
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
|
||||
this.refreshChanges$.emit();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public resendEmailVerification(): void {
|
||||
this.userService
|
||||
.resendMyEmailVerification()
|
||||
public resendEmailVerification(user: User): void {
|
||||
this.newMgmtService
|
||||
.resendHumanEmailVerification(user.id)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
|
||||
this.refreshChanges$.emit();
|
||||
@@ -298,161 +360,187 @@ export class AuthUserDetailComponent implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
public deletePhone(): void {
|
||||
public resendPhoneVerification(user: User): void {
|
||||
this.newMgmtService
|
||||
.resendHumanPhoneVerification(user.id)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
|
||||
this.refreshChanges$.emit();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public deletePhone(user: User): void {
|
||||
this.userService
|
||||
.removeMyPhone()
|
||||
.removePhone(user.id)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
|
||||
if (this.user?.human?.phone) {
|
||||
const phone = new Phone();
|
||||
this.user.human.phone = phone.toObject();
|
||||
this.refreshUser();
|
||||
}
|
||||
this.refreshChanges$.emit();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public savePhone(phone: string): void {
|
||||
if (this.user?.human) {
|
||||
// Format phone before save (add +)
|
||||
const formattedPhone = formatPhone(phone);
|
||||
if (formattedPhone) {
|
||||
phone = formattedPhone.phone;
|
||||
}
|
||||
|
||||
this.userService
|
||||
.setMyPhone(phone)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
|
||||
if (this.user?.human) {
|
||||
const phoneToSet = new Phone();
|
||||
phoneToSet.setPhone(phone);
|
||||
this.user.human.phone = phoneToSet.toObject();
|
||||
this.refreshUser();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public openEditDialog(type: EditDialogType): void {
|
||||
public openEditDialog(user: UserWithHumanType, type: EditDialogType): void {
|
||||
switch (type) {
|
||||
case EditDialogType.PHONE:
|
||||
const dialogRefPhone = this.dialog.open(EditDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.SAVE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
labelKey: 'USER.LOGINMETHODS.PHONE.EDITVALUE',
|
||||
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
|
||||
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
|
||||
value: this.user?.human?.phone?.phone,
|
||||
type: type,
|
||||
validator: Validators.compose([phoneValidator, requiredValidator]),
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRefPhone.afterClosed().subscribe((resp: { value: string; isVerified: boolean }) => {
|
||||
if (resp && resp.value) {
|
||||
this.savePhone(resp.value);
|
||||
}
|
||||
});
|
||||
break;
|
||||
this.openEditPhoneDialog(user);
|
||||
return;
|
||||
case EditDialogType.EMAIL:
|
||||
const dialogRefEmail = this.dialog.open(EditDialogComponent, {
|
||||
data: {
|
||||
this.openEditEmailDialog(user);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private openEditEmailDialog(user: UserWithHumanType) {
|
||||
const data: EditDialogData = {
|
||||
confirmKey: 'ACTIONS.SAVE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
labelKey: 'ACTIONS.NEWVALUE',
|
||||
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE',
|
||||
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
|
||||
value: this.user?.human?.email?.email,
|
||||
type: type,
|
||||
},
|
||||
value: user.type.value?.email?.email,
|
||||
type: EditDialogType.EMAIL,
|
||||
} as const;
|
||||
|
||||
const dialogRefEmail = this.dialog.open<EditDialogComponent, EditDialogData, EditDialogResult>(EditDialogComponent, {
|
||||
data,
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRefEmail.afterClosed().subscribe((resp: { value: string; isVerified: boolean }) => {
|
||||
if (resp && resp.value) {
|
||||
this.saveEmail(resp.value);
|
||||
}
|
||||
dialogRefEmail
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
filter((resp): resp is Required<EditDialogResult> => !!resp?.value),
|
||||
switchMap(({ value, isVerified }) =>
|
||||
this.userService.setEmail({
|
||||
userId: user.id,
|
||||
email: value,
|
||||
verification: isVerified ? { case: 'isVerified', value: isVerified } : { case: undefined },
|
||||
}),
|
||||
),
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
|
||||
this.refreshChanges$.emit();
|
||||
},
|
||||
error: (error) => this.toast.showError(error),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public deleteAccount(): void {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
private openEditPhoneDialog(user: UserWithHumanType) {
|
||||
const data = {
|
||||
confirmKey: 'ACTIONS.SAVE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
labelKey: 'ACTIONS.NEWVALUE',
|
||||
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
|
||||
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' },
|
||||
);
|
||||
|
||||
dialogRefPhone
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
map((resp) => formatPhone(resp?.value)),
|
||||
filter(Boolean),
|
||||
switchMap(({ phone }) => this.userService.setPhone({ userId: user.id, phone })),
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
|
||||
this.refreshChanges$.emit();
|
||||
},
|
||||
error: (error) => {
|
||||
this.toast.showError(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public deleteUser(user: User): void {
|
||||
const data = {
|
||||
confirmKey: 'USER.DIALOG.DELETE_BTN',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
titleKey: 'USER.DIALOG.DELETE_TITLE',
|
||||
descriptionKey: 'USER.DIALOG.DELETE_AUTH_DESCRIPTION',
|
||||
},
|
||||
};
|
||||
|
||||
const dialogRef = this.dialog.open<WarnDialogComponent, typeof data, boolean>(WarnDialogComponent, {
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.userService
|
||||
.RemoveMyUser()
|
||||
.then(() => {
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() => this.userService.deleteUser(user.id)),
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toast.showInfo('USER.PAGES.DELETEACCOUNT_SUCCESS', true);
|
||||
this.auth.signout();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
},
|
||||
error: (error) => this.toast.showError(error),
|
||||
});
|
||||
}
|
||||
|
||||
public loadMetadata(): void {
|
||||
if (this.user) {
|
||||
this.userService.isAllowed(['user.read']).subscribe((allowed) => {
|
||||
if (allowed) {
|
||||
this.loadingMetadata = true;
|
||||
this.mgmt
|
||||
.listUserMetadata(this.user?.id ?? '')
|
||||
.then((resp) => {
|
||||
this.loadingMetadata = false;
|
||||
this.metadata = resp.resultList.map((md) => {
|
||||
return {
|
||||
key: md.key,
|
||||
value: Buffer.from(md.value as string, 'base64').toString('utf8'),
|
||||
};
|
||||
public editMetadata(user: User, metadata: Metadata[]): void {
|
||||
const setFcn = (key: string, value: string) =>
|
||||
this.newMgmtService.setUserMetadata({
|
||||
key,
|
||||
value: Buffer.from(value),
|
||||
id: user.id,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.loadingMetadata = false;
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
const removeFcn = (key: string): Promise<any> => this.newMgmtService.removeUserMetadata({ key, id: user.id });
|
||||
|
||||
public editMetadata(): void {
|
||||
if (this.user && this.user.id) {
|
||||
const setFcn = (key: string, value: string): Promise<any> =>
|
||||
this.mgmt.setUserMetadata(key, Buffer.from(value).toString('base64'), this.user?.id ?? '');
|
||||
const removeFcn = (key: string): Promise<any> => this.mgmt.removeUserMetadata(key, this.user?.id ?? '');
|
||||
|
||||
const dialogRef = this.dialog.open(MetadataDialogComponent, {
|
||||
const dialogRef = this.dialog.open<MetadataDialogComponent, MetadataDialogData>(MetadataDialogComponent, {
|
||||
data: {
|
||||
metadata: this.metadata,
|
||||
metadata: [...metadata],
|
||||
setFcn: setFcn,
|
||||
removeFcn: removeFcn,
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.loadMetadata();
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(() => {
|
||||
this.refreshMetadata$.next(true);
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly query = query;
|
||||
|
||||
protected user(user: UserQuery): User | undefined {
|
||||
if (user.state === 'success' || user.state === 'loading') {
|
||||
return user.value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
public async goToSetting(setting: string) {
|
||||
await this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { id: setting },
|
||||
queryParamsHandling: 'merge',
|
||||
skipLocationChange: true,
|
||||
});
|
||||
}
|
||||
|
||||
public humanUser(userQuery: UserQuery): UserWithHumanType | undefined {
|
||||
const user = this.user(userQuery);
|
||||
if (user?.type.case === 'human') {
|
||||
return { ...user, type: user.type };
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@@ -47,7 +47,7 @@
|
||||
{{ data.isVerifiedTextKey | translate }}
|
||||
</mat-checkbox>
|
||||
<cnsl-info-section class="full-width desc">
|
||||
<span>{{ data.isVerifiedTextDescKey | translate }}</span>
|
||||
<span>{{ data.isVerifiedTextDescKey ?? '' | translate }}</span>
|
||||
</cnsl-info-section>
|
||||
</ng-container>
|
||||
</form>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { FormGroup, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
|
||||
import { FormGroup, UntypedFormControl, UntypedFormGroup, ValidatorFn } from '@angular/forms';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { debounceTime } from 'rxjs';
|
||||
import { requiredValidator } from 'src/app/modules/form-field/validators/validators';
|
||||
@@ -11,6 +11,24 @@ export enum EditDialogType {
|
||||
EMAIL = 2,
|
||||
}
|
||||
|
||||
export type EditDialogData = {
|
||||
confirmKey: 'ACTIONS.SAVE' | 'ACTIONS.CHANGE';
|
||||
cancelKey: 'ACTIONS.CANCEL';
|
||||
labelKey: 'ACTIONS.NEWVALUE';
|
||||
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE';
|
||||
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC';
|
||||
isVerifiedTextKey?: 'USER.LOGINMETHODS.EMAIL.ISVERIFIED';
|
||||
isVerifiedTextDescKey?: 'USER.LOGINMETHODS.EMAIL.ISVERIFIEDDESC';
|
||||
value: string | undefined;
|
||||
type: EditDialogType;
|
||||
validator?: ValidatorFn;
|
||||
};
|
||||
|
||||
export type EditDialogResult = {
|
||||
value?: string;
|
||||
isVerified: boolean;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-edit-dialog',
|
||||
templateUrl: './edit-dialog.component.html',
|
||||
@@ -31,7 +49,7 @@ export class EditDialogComponent implements OnInit {
|
||||
public countryPhoneCodes: CountryPhoneCode[] = [];
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<EditDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
@Inject(MAT_DIALOG_DATA) public data: EditDialogData,
|
||||
private countryCallingCodesService: CountryCallingCodesService,
|
||||
) {
|
||||
if (data.type === EditDialogType.PHONE) {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<h1 mat-dialog-title>
|
||||
<span class="title">{{ 'USER.SENDEMAILDIALOG.TITLE' | translate }} {{ data?.number }}</span>
|
||||
<span class="title">{{ 'USER.SENDEMAILDIALOG.TITLE' | translate }}</span>
|
||||
</h1>
|
||||
<p class="desc cnsl-secondary-text">{{ 'USER.SENDEMAILDIALOG.DESCRIPTION' | translate }}</p>
|
||||
<div mat-dialog-content>
|
||||
|
@@ -1,6 +1,12 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
||||
export type ResendEmailDialogData = {
|
||||
email: string | '';
|
||||
};
|
||||
|
||||
export type ResendEmailDialogResult = { send: true; email: string } | { send: false };
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-resend-email-dialog',
|
||||
templateUrl: './resend-email-dialog.component.html',
|
||||
@@ -9,16 +15,16 @@ import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
export class ResendEmailDialogComponent {
|
||||
public email: string = '';
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ResendEmailDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
public dialogRef: MatDialogRef<ResendEmailDialogComponent, ResendEmailDialogResult>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: ResendEmailDialogData,
|
||||
) {
|
||||
if (data.email) {
|
||||
this.email = data.email;
|
||||
}
|
||||
}
|
||||
|
||||
closeDialog(email: string = ''): void {
|
||||
this.dialogRef.close(email);
|
||||
closeDialog(): void {
|
||||
this.dialogRef.close({ send: false });
|
||||
}
|
||||
|
||||
closeDialogWithSend(email: string = ''): void {
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<div class="contact-method-col" *ngIf="human">
|
||||
<div class="contact-method-col">
|
||||
<div class="contact-method-row">
|
||||
<div class="left">
|
||||
<span class="label cnsl-secondary-text">{{ 'USER.EMAIL' | translate }}</span>
|
||||
<span class="name">{{ human.email?.email }}</span>
|
||||
<span *ngIf="human.email?.isEmailVerified" class="contact-state verified">{{ 'USER.EMAILVERIFIED' | translate }}</span>
|
||||
<div *ngIf="!human.email?.isEmailVerified" class="block">
|
||||
<span *ngIf="isEmailVerified" class="contact-state verified">{{ 'USER.EMAILVERIFIED' | translate }}</span>
|
||||
<div *ngIf="!isEmailVerified" class="block">
|
||||
<span class="contact-state notverified">{{ 'USER.NOTVERIFIED' | translate }}</span>
|
||||
|
||||
<ng-container *ngIf="human.email">
|
||||
@@ -37,8 +37,8 @@
|
||||
<div class="left">
|
||||
<span class="label cnsl-secondary-text">{{ 'USER.PHONE' | translate }}</span>
|
||||
<cnsl-phone-detail [phone]="human.phone?.phone"></cnsl-phone-detail>
|
||||
<span *ngIf="human.phone?.isPhoneVerified" class="contact-state verified">{{ 'USER.PHONEVERIFIED' | translate }}</span>
|
||||
<div *ngIf="human.phone?.phone && !human.phone?.isPhoneVerified" class="block">
|
||||
<span *ngIf="isPhoneVerified" class="contact-state verified">{{ 'USER.PHONEVERIFIED' | translate }}</span>
|
||||
<div *ngIf="human.phone?.phone && !isPhoneVerified" class="block">
|
||||
<span class="contact-state notverified">{{ 'USER.NOTVERIFIED' | translate }}</span>
|
||||
|
||||
<ng-container *ngIf="human.phone?.phone">
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
import { Human, UserState } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { CodeDialogComponent } from '../auth-user-detail/code-dialog/code-dialog.component';
|
||||
import { EditDialogType } from '../auth-user-detail/edit-dialog/edit-dialog.component';
|
||||
import { HumanUser, UserState } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { Human } from '@zitadel/proto/zitadel/user_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-contact',
|
||||
@@ -15,15 +16,14 @@ import { EditDialogType } from '../auth-user-detail/edit-dialog/edit-dialog.comp
|
||||
export class ContactComponent {
|
||||
@Input() disablePhoneCode: boolean = false;
|
||||
@Input() canWrite: boolean | null = false;
|
||||
@Input() human?: Human.AsObject;
|
||||
@Input({ required: true }) human!: HumanUser | Human;
|
||||
@Input() username: string = '';
|
||||
@Input() state!: UserState;
|
||||
@Output() editType: EventEmitter<EditDialogType> = new EventEmitter<EditDialogType>();
|
||||
@Output() resendEmailVerification: EventEmitter<void> = new EventEmitter<void>();
|
||||
@Output() resendPhoneVerification: EventEmitter<void> = new EventEmitter<void>();
|
||||
@Output() enteredPhoneCode: EventEmitter<string> = new EventEmitter<string>();
|
||||
@Output() deletedPhone: EventEmitter<void> = new EventEmitter<void>();
|
||||
public UserState: any = UserState;
|
||||
public UserState = UserState;
|
||||
|
||||
public EditDialogType: any = EditDialogType;
|
||||
constructor(
|
||||
@@ -81,4 +81,18 @@ export class ContactComponent {
|
||||
public openEditDialog(type: EditDialogType): void {
|
||||
this.editType.emit(type);
|
||||
}
|
||||
|
||||
protected get isPhoneVerified() {
|
||||
if (this.human.$typeName === 'zitadel.user.v2.HumanUser') {
|
||||
return !!this.human.phone?.isVerified;
|
||||
}
|
||||
return this.human.phone?.isPhoneVerified;
|
||||
}
|
||||
|
||||
protected get isEmailVerified() {
|
||||
if (this.human.$typeName === 'zitadel.user.v2.HumanUser') {
|
||||
return !!this.human.email?.isVerified;
|
||||
}
|
||||
return this.human.email?.isEmailVerified;
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<form [formGroup]="machineForm" *ngIf="machineForm" (ngSubmit)="submitForm()">
|
||||
<form [formGroup]="machineForm" (ngSubmit)="submitForm()">
|
||||
<div class="content">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}</cnsl-label>
|
||||
|
@@ -1,55 +1,77 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Component, DestroyRef, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { FormBuilder, FormControl } from '@angular/forms';
|
||||
import { combineLatestWith, distinctUntilChanged, ReplaySubject } from 'rxjs';
|
||||
import { requiredValidator } from 'src/app/modules/form-field/validators/validators';
|
||||
import { AccessTokenType, Human, Machine } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { AccessTokenType, MachineUser } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { startWith } from 'rxjs/operators';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-detail-form-machine',
|
||||
templateUrl: './detail-form-machine.component.html',
|
||||
styleUrls: ['./detail-form-machine.component.scss'],
|
||||
})
|
||||
export class DetailFormMachineComponent implements OnInit, OnDestroy {
|
||||
@Input() public username!: string;
|
||||
@Input() public user!: Human.AsObject | Machine.AsObject;
|
||||
@Input() public disabled: boolean = false;
|
||||
@Output() public submitData: EventEmitter<any> = new EventEmitter<any>();
|
||||
export class DetailFormMachineComponent {
|
||||
@Input({ required: true }) public set username(username: string) {
|
||||
this.username$.next(username);
|
||||
}
|
||||
@Input({ required: true }) public set user(user: MachineUser) {
|
||||
this.user$.next(user);
|
||||
}
|
||||
@Input() public set disabled(disabled: boolean) {
|
||||
this.disabled$.next(disabled);
|
||||
}
|
||||
|
||||
public machineForm!: UntypedFormGroup;
|
||||
private username$ = new ReplaySubject<string>(1);
|
||||
private user$ = new ReplaySubject<MachineUser>(1);
|
||||
private disabled$ = new ReplaySubject<boolean>(1);
|
||||
|
||||
public accessTokenTypes: AccessTokenType[] = [
|
||||
AccessTokenType.ACCESS_TOKEN_TYPE_BEARER,
|
||||
AccessTokenType.ACCESS_TOKEN_TYPE_JWT,
|
||||
];
|
||||
public machineForm: ReturnType<typeof this.buildForm>;
|
||||
|
||||
private sub: Subscription = new Subscription();
|
||||
@Output() public submitData = new EventEmitter<ReturnType<(typeof this.machineForm)['getRawValue']>>();
|
||||
|
||||
constructor(private fb: UntypedFormBuilder) {
|
||||
this.machineForm = this.fb.group({
|
||||
userName: [{ value: '', disabled: true }, [requiredValidator]],
|
||||
name: [{ value: '', disabled: this.disabled }, requiredValidator],
|
||||
description: [{ value: '', disabled: this.disabled }],
|
||||
accessTokenType: [AccessTokenType.ACCESS_TOKEN_TYPE_BEARER, [requiredValidator]],
|
||||
public accessTokenTypes: AccessTokenType[] = [AccessTokenType.BEARER, AccessTokenType.JWT];
|
||||
|
||||
constructor(
|
||||
private readonly fb: FormBuilder,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
) {
|
||||
this.machineForm = this.buildForm();
|
||||
}
|
||||
|
||||
private buildForm() {
|
||||
const form = this.fb.group({
|
||||
username: new FormControl('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
name: new FormControl('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
description: new FormControl('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
accessTokenType: new FormControl(AccessTokenType.BEARER, { nonNullable: true, validators: [requiredValidator] }),
|
||||
});
|
||||
|
||||
form.controls.username.disable();
|
||||
this.disabled$
|
||||
.pipe(startWith(false), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((disabled) => {
|
||||
this.toggleFormControl(form.controls.name, disabled);
|
||||
this.toggleFormControl(form.controls.description, disabled);
|
||||
this.toggleFormControl(form.controls.accessTokenType, disabled);
|
||||
});
|
||||
|
||||
this.username$.pipe(combineLatestWith(this.user$), takeUntilDestroyed(this.destroyRef)).subscribe(([username, user]) => {
|
||||
this.machineForm.patchValue({ ...user, username });
|
||||
});
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.machineForm.patchValue({ ...this.user, userName: this.username });
|
||||
public toggleFormControl<T>(control: FormControl<T>, disabled: boolean) {
|
||||
if (disabled) {
|
||||
control.disable();
|
||||
return;
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.sub.unsubscribe();
|
||||
control.enable();
|
||||
}
|
||||
|
||||
public submitForm(): void {
|
||||
this.submitData.emit(this.machineForm.value);
|
||||
}
|
||||
|
||||
public get name(): AbstractControl | null {
|
||||
return this.machineForm.get('name');
|
||||
}
|
||||
|
||||
public get userName(): AbstractControl | null {
|
||||
return this.machineForm.get('userName');
|
||||
this.submitData.emit(this.machineForm.getRawValue());
|
||||
}
|
||||
}
|
||||
|
@@ -1,20 +1,20 @@
|
||||
<form [formGroup]="profileForm" *ngIf="profileForm" (ngSubmit)="submitForm()">
|
||||
<form *ngIf="profile$ | async as profile" [formGroup]="profileForm" (ngSubmit)="submitForm(profile)">
|
||||
<div class="user-top-content">
|
||||
<div class="user-form-content">
|
||||
<button
|
||||
[disabled]="user && disabled"
|
||||
[disabled]="disabled$ | async"
|
||||
class="camera-wrapper"
|
||||
type="button"
|
||||
(click)="showEditImage ? openUploadDialog() : null"
|
||||
(click)="showEditImage ? openUploadDialog(profile) : null"
|
||||
>
|
||||
<div class="i-wrapper" *ngIf="showEditImage">
|
||||
<i class="las la-camera"></i>
|
||||
</div>
|
||||
<cnsl-avatar
|
||||
*ngIf="user && user.profile"
|
||||
*ngIf="profile.displayName"
|
||||
class="avatar"
|
||||
[name]="user.profile.displayName"
|
||||
[avatarUrl]="user.profile.avatarUrl || ''"
|
||||
[name]="profile.displayName"
|
||||
[avatarUrl]="profile.avatarUrl || ''"
|
||||
[forColor]="preferredLoginName"
|
||||
[size]="80"
|
||||
>
|
||||
@@ -24,9 +24,9 @@
|
||||
<div class="usernamediv">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="userName" />
|
||||
<input cnslInput formControlName="username" />
|
||||
</cnsl-form-field>
|
||||
<button [disabled]="user && disabled" type="button" mat-stroked-button class="edit" (click)="changeUsername()">
|
||||
<button [disabled]="disabled$ | async" type="button" mat-stroked-button class="edit" (click)="changeUsername()">
|
||||
{{ 'USER.PROFILE.CHANGEUSERNAME' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -34,11 +34,11 @@
|
||||
<div class="user-grid">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="firstName" />
|
||||
<input cnslInput formControlName="givenName" />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'USER.PROFILE.LASTNAME' | translate }}</cnsl-label>
|
||||
<input cnslInput formControlName="lastName" />
|
||||
<input cnslInput formControlName="familyName" />
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'USER.PROFILE.NICKNAME' | translate }}</cnsl-label>
|
||||
@@ -67,7 +67,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-container">
|
||||
<button [disabled]="disabled" class="submit-button" type="submit" color="primary" mat-raised-button>
|
||||
<button [disabled]="disabled$ | async" class="submit-button" type="submit" color="primary" mat-raised-button>
|
||||
{{ 'ACTIONS.SAVE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -1,116 +1,138 @@
|
||||
import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output } from '@angular/core';
|
||||
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { Component, DestroyRef, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormBuilder, FormControl } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { combineLatestWith, distinctUntilChanged, ReplaySubject } from 'rxjs';
|
||||
import { requiredValidator } from 'src/app/modules/form-field/validators/validators';
|
||||
import { Gender, Human, Profile } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
|
||||
import { ProfilePictureComponent } from './profile-picture/profile-picture.component';
|
||||
import { Gender, HumanProfile, HumanProfileSchema } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { filter, startWith } from 'rxjs/operators';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Profile } from '@zitadel/proto/zitadel/user_pb';
|
||||
import { create } from '@bufbuild/protobuf';
|
||||
|
||||
function toHumanProfile(profile: HumanProfile | Profile): HumanProfile {
|
||||
if (profile.$typeName === 'zitadel.user.v2.HumanProfile') {
|
||||
return profile;
|
||||
}
|
||||
return create(HumanProfileSchema, {
|
||||
givenName: profile.firstName,
|
||||
familyName: profile.lastName,
|
||||
nickName: profile.nickName,
|
||||
displayName: profile.displayName,
|
||||
preferredLanguage: profile.preferredLanguage,
|
||||
gender: profile.gender,
|
||||
avatarUrl: profile.avatarUrl,
|
||||
});
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-detail-form',
|
||||
templateUrl: './detail-form.component.html',
|
||||
styleUrls: ['./detail-form.component.scss'],
|
||||
})
|
||||
export class DetailFormComponent implements OnDestroy, OnChanges {
|
||||
export class DetailFormComponent implements OnInit {
|
||||
@Input() public showEditImage: boolean = false;
|
||||
@Input() public preferredLoginName: string = '';
|
||||
@Input() public username!: string;
|
||||
@Input() public user!: Human.AsObject;
|
||||
@Input() public disabled: boolean = true;
|
||||
@Input({ required: true }) public set username(username: string) {
|
||||
this.username$.next(username);
|
||||
}
|
||||
@Input({ required: true }) public set profile(profile: HumanProfile | Profile) {
|
||||
this.profile$.next(toHumanProfile(profile));
|
||||
}
|
||||
@Input() public set disabled(disabled: boolean) {
|
||||
this.disabled$.next(disabled);
|
||||
}
|
||||
@Input() public genders: Gender[] = [];
|
||||
@Input() public languages: string[] = ['de', 'en'];
|
||||
@Output() public submitData: EventEmitter<Profile.AsObject> = new EventEmitter<Profile.AsObject>();
|
||||
@Output() public changedLanguage: EventEmitter<string> = new EventEmitter<string>();
|
||||
@Output() public changeUsernameClicked: EventEmitter<void> = new EventEmitter();
|
||||
@Output() public avatarChanged: EventEmitter<void> = new EventEmitter();
|
||||
|
||||
public profileForm!: UntypedFormGroup;
|
||||
|
||||
private sub: Subscription = new Subscription();
|
||||
private username$ = new ReplaySubject<string>(1);
|
||||
public profile$ = new ReplaySubject<HumanProfile>(1);
|
||||
public profileForm!: ReturnType<typeof this.buildForm>;
|
||||
public disabled$ = new ReplaySubject<boolean>(1);
|
||||
@Output() public submitData = new EventEmitter<HumanProfile>();
|
||||
|
||||
constructor(
|
||||
private fb: UntypedFormBuilder,
|
||||
private dialog: MatDialog,
|
||||
private readonly fb: FormBuilder,
|
||||
private readonly dialog: MatDialog,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
) {
|
||||
this.profileForm = this.fb.group({
|
||||
userName: [{ value: '', disabled: true }, [requiredValidator]],
|
||||
firstName: [{ value: '', disabled: this.disabled }, requiredValidator],
|
||||
lastName: [{ value: '', disabled: this.disabled }, requiredValidator],
|
||||
nickName: [{ value: '', disabled: this.disabled }],
|
||||
displayName: [{ value: '', disabled: this.disabled }, requiredValidator],
|
||||
gender: [{ value: 0, disabled: this.disabled }],
|
||||
preferredLanguage: [{ value: '', disabled: this.disabled }],
|
||||
});
|
||||
this.profileForm = this.buildForm();
|
||||
}
|
||||
|
||||
public ngOnChanges(): void {
|
||||
this.profileForm = this.fb.group({
|
||||
userName: [{ value: '', disabled: true }, [requiredValidator]],
|
||||
firstName: [{ value: '', disabled: this.disabled }, requiredValidator],
|
||||
lastName: [{ value: '', disabled: this.disabled }, requiredValidator],
|
||||
nickName: [{ value: '', disabled: this.disabled }],
|
||||
displayName: [{ value: '', disabled: this.disabled }, requiredValidator],
|
||||
gender: [{ value: 0, disabled: this.disabled }],
|
||||
preferredLanguage: [{ value: '', disabled: this.disabled }],
|
||||
ngOnInit(): void {
|
||||
this.profileForm.controls.preferredLanguage.valueChanges
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((value) => this.changedLanguage.emit(value));
|
||||
}
|
||||
|
||||
private buildForm() {
|
||||
const form = this.fb.group({
|
||||
username: new FormControl('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
givenName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
familyName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
nickName: new FormControl('', { nonNullable: true }),
|
||||
displayName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }),
|
||||
preferredLanguage: new FormControl('', { nonNullable: true }),
|
||||
gender: new FormControl(Gender.UNSPECIFIED, { nonNullable: true }),
|
||||
});
|
||||
|
||||
this.profileForm.patchValue({ userName: this.username, ...this.user.profile });
|
||||
|
||||
if (this.preferredLanguage) {
|
||||
this.sub = this.preferredLanguage.valueChanges.subscribe((value) => {
|
||||
this.changedLanguage.emit(value);
|
||||
form.controls.username.disable();
|
||||
this.disabled$
|
||||
.pipe(startWith(true), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((disabled) => {
|
||||
this.toggleFormControl(form.controls.givenName, disabled);
|
||||
this.toggleFormControl(form.controls.familyName, disabled);
|
||||
this.toggleFormControl(form.controls.nickName, disabled);
|
||||
this.toggleFormControl(form.controls.displayName, disabled);
|
||||
this.toggleFormControl(form.controls.gender, disabled);
|
||||
this.toggleFormControl(form.controls.preferredLanguage, disabled);
|
||||
});
|
||||
}
|
||||
|
||||
this.username$
|
||||
.pipe(combineLatestWith(this.profile$), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(([username, profile]) => {
|
||||
form.patchValue({
|
||||
username: username,
|
||||
...profile,
|
||||
});
|
||||
});
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
|
||||
public submitForm(): void {
|
||||
this.submitData.emit(this.profileForm.value);
|
||||
public submitForm(profile: HumanProfile): void {
|
||||
this.submitData.emit({ ...profile, ...this.profileForm.getRawValue() });
|
||||
}
|
||||
|
||||
public changeUsername(): void {
|
||||
this.changeUsernameClicked.emit();
|
||||
}
|
||||
|
||||
public openUploadDialog(): void {
|
||||
const dialogRef = this.dialog.open(ProfilePictureComponent, {
|
||||
data: {
|
||||
profilePic: this.user.profile?.avatarUrl,
|
||||
},
|
||||
public openUploadDialog(profile: HumanProfile): void {
|
||||
const data = {
|
||||
profilePic: profile.avatarUrl,
|
||||
};
|
||||
|
||||
const dialogRef = this.dialog.open<ProfilePictureComponent, typeof data, boolean>(ProfilePictureComponent, {
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((shouldReload) => {
|
||||
if (shouldReload) {
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => {
|
||||
this.avatarChanged.emit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public get userName(): AbstractControl | null {
|
||||
return this.profileForm.get('userName');
|
||||
public toggleFormControl<T>(control: FormControl<T>, disabled: boolean) {
|
||||
if (disabled) {
|
||||
control.disable();
|
||||
return;
|
||||
}
|
||||
|
||||
public get firstName(): AbstractControl | null {
|
||||
return this.profileForm.get('firstName');
|
||||
}
|
||||
public get lastName(): AbstractControl | null {
|
||||
return this.profileForm.get('lastName');
|
||||
}
|
||||
public get nickName(): AbstractControl | null {
|
||||
return this.profileForm.get('nickName');
|
||||
}
|
||||
public get displayName(): AbstractControl | null {
|
||||
return this.profileForm.get('displayName');
|
||||
}
|
||||
public get gender(): AbstractControl | null {
|
||||
return this.profileForm.get('gender');
|
||||
}
|
||||
public get preferredLanguage(): AbstractControl | null {
|
||||
return this.profileForm.get('preferredLanguage');
|
||||
control.enable();
|
||||
}
|
||||
}
|
||||
|
@@ -3,14 +3,14 @@ import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { BehaviorSubject, firstValueFrom, Observable } from 'rxjs';
|
||||
import { PageEvent, PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
import { IDPUserLink } from 'src/app/proto/generated/zitadel/idp_pb';
|
||||
|
||||
import { GrpcAuthService } from '../../../../services/grpc-auth.service';
|
||||
import { ManagementService } from '../../../../services/mgmt.service';
|
||||
import { ToastService } from '../../../../services/toast.service';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-external-idps',
|
||||
@@ -18,7 +18,7 @@ import { ToastService } from '../../../../services/toast.service';
|
||||
styleUrls: ['./external-idps.component.scss'],
|
||||
})
|
||||
export class ExternalIdpsComponent implements OnInit, OnDestroy {
|
||||
@Input() service!: GrpcAuthService | ManagementService;
|
||||
@Input({ required: true }) service!: GrpcAuthService | ManagementService;
|
||||
@Input() userId!: string;
|
||||
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
|
||||
public totalResult: number = 0;
|
||||
@@ -41,7 +41,7 @@ export class ExternalIdpsComponent implements OnInit, OnDestroy {
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getData(10, 0);
|
||||
this.getData(10, 0).then();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -65,16 +65,20 @@ export class ExternalIdpsComponent implements OnInit, OnDestroy {
|
||||
private async getData(limit: number, offset: number): Promise<void> {
|
||||
this.loadingSubject.next(true);
|
||||
|
||||
let promise;
|
||||
if (this.service instanceof ManagementService) {
|
||||
promise = (this.service as ManagementService).listHumanLinkedIDPs(this.userId, limit, offset);
|
||||
} else if (this.service instanceof GrpcAuthService) {
|
||||
promise = (this.service as GrpcAuthService).listMyLinkedIDPs(limit, offset);
|
||||
const promise =
|
||||
this.service instanceof ManagementService
|
||||
? (this.service as ManagementService).listHumanLinkedIDPs(this.userId, limit, offset)
|
||||
: (this.service as GrpcAuthService).listMyLinkedIDPs(limit, offset);
|
||||
|
||||
let resp;
|
||||
try {
|
||||
resp = await promise;
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
this.loadingSubject.next(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (promise) {
|
||||
promise
|
||||
.then((resp) => {
|
||||
this.dataSource.data = resp.resultList;
|
||||
if (resp.details?.viewTimestamp) {
|
||||
this.viewTimestamp = resp.details.viewTimestamp;
|
||||
@@ -85,19 +89,13 @@ export class ExternalIdpsComponent implements OnInit, OnDestroy {
|
||||
this.totalResult = 0;
|
||||
}
|
||||
this.loadingSubject.next(false);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
this.toast.showError(error);
|
||||
this.loadingSubject.next(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public refreshPage(): void {
|
||||
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize);
|
||||
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize).then();
|
||||
}
|
||||
|
||||
public removeExternalIdp(idp: IDPUserLink.AsObject): void {
|
||||
public async removeExternalIdp(idp: IDPUserLink.AsObject): Promise<void> {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.REMOVE',
|
||||
@@ -108,27 +106,23 @@ export class ExternalIdpsComponent implements OnInit, OnDestroy {
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
let promise;
|
||||
if (this.service instanceof ManagementService) {
|
||||
promise = (this.service as ManagementService).removeHumanLinkedIDP(idp.idpId, idp.providedUserId, idp.userId);
|
||||
} else if (this.service instanceof GrpcAuthService) {
|
||||
promise = (this.service as GrpcAuthService).removeMyLinkedIDP(idp.idpId, idp.providedUserId);
|
||||
const resp = await firstValueFrom(dialogRef.afterClosed());
|
||||
if (!resp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (promise) {
|
||||
promise
|
||||
.then((_) => {
|
||||
const promise =
|
||||
this.service instanceof ManagementService
|
||||
? (this.service as ManagementService).removeHumanLinkedIDP(idp.idpId, idp.providedUserId, idp.userId)
|
||||
: (this.service as GrpcAuthService).removeMyLinkedIDP(idp.idpId, idp.providedUserId);
|
||||
|
||||
try {
|
||||
await promise;
|
||||
setTimeout(() => {
|
||||
this.refreshPage();
|
||||
}, 1000);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
<cnsl-detail-layout [hasBackButton]="true" title="{{ 'USER.PASSWORD.TITLE' | translate }}">
|
||||
<cnsl-detail-layout *ngIf="form$ | async as form" [hasBackButton]="true" title="{{ 'USER.PASSWORD.TITLE' | translate }}">
|
||||
<p class="password-info cnsl-secondary-text" sub>{{ 'USER.PASSWORD.DESCRIPTION' | translate }}</p>
|
||||
<ng-container *ngIf="userId; else authUser">
|
||||
<form *ngIf="passwordForm" [formGroup]="passwordForm" (ngSubmit)="setInitialPassword(userId)">
|
||||
<ng-container *ngIf="id$ | async as id">
|
||||
<form [formGroup]="form" (ngSubmit)="setInitialPassword(id, form)">
|
||||
<input
|
||||
*ngIf="username"
|
||||
*ngIf="username$ | async as username"
|
||||
class="hiddeninput"
|
||||
type="hidden"
|
||||
autocomplete="username"
|
||||
@@ -24,21 +24,22 @@
|
||||
</cnsl-form-field>
|
||||
</div>
|
||||
|
||||
<div class="validation" *ngIf="this.policy">
|
||||
<cnsl-password-complexity-view [policy]="this.policy" [password]="password"> </cnsl-password-complexity-view>
|
||||
<div class="validation" *ngIf="passwordPolicy$ | async as passwordPolicy">
|
||||
<cnsl-password-complexity-view [policy]="passwordPolicy" [password]="password(form)">
|
||||
</cnsl-password-complexity-view>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="submit-button" [disabled]="passwordForm.invalid" mat-raised-button color="primary">
|
||||
<button class="submit-button" [disabled]="form.invalid" mat-raised-button color="primary">
|
||||
{{ 'USER.PASSWORD.SET' | translate }}
|
||||
</button>
|
||||
</form>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #authUser>
|
||||
<form *ngIf="passwordForm" [formGroup]="passwordForm" (ngSubmit)="setPassword()">
|
||||
<ng-container *ngIf="(id$ | async) === undefined && (user$ | async) as user">
|
||||
<form [formGroup]="form" (ngSubmit)="setPassword(form, user)">
|
||||
<input
|
||||
*ngIf="username"
|
||||
*ngIf="username$ | async as username"
|
||||
class="hiddeninput"
|
||||
type="hidden"
|
||||
autocomplete="username"
|
||||
@@ -53,8 +54,9 @@
|
||||
<input cnslInput autocomplete="off" name="password" type="password" formControlName="currentPassword" />
|
||||
</cnsl-form-field>
|
||||
|
||||
<div class="validation between" *ngIf="this.policy">
|
||||
<cnsl-password-complexity-view [policy]="this.policy" [password]="newPassword"> </cnsl-password-complexity-view>
|
||||
<div class="validation between" *ngIf="passwordPolicy$ | async as passwordPolicy">
|
||||
<cnsl-password-complexity-view [policy]="passwordPolicy" [password]="newPassword(form)">
|
||||
</cnsl-password-complexity-view>
|
||||
</div>
|
||||
|
||||
<div class="side-by-side">
|
||||
@@ -74,9 +76,9 @@
|
||||
</cnsl-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<button class="submit-button" [disabled]="passwordForm.invalid" mat-raised-button color="primary">
|
||||
<button class="submit-button" [disabled]="form.invalid" mat-raised-button color="primary">
|
||||
{{ 'USER.PASSWORD.RESET' | translate }}
|
||||
</button>
|
||||
</form>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</cnsl-detail-layout>
|
||||
|
@@ -1,7 +1,18 @@
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { Component, DestroyRef, OnInit } from '@angular/core';
|
||||
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Subject, Subscription, take, takeUntil } from 'rxjs';
|
||||
import {
|
||||
take,
|
||||
map,
|
||||
switchMap,
|
||||
firstValueFrom,
|
||||
mergeWith,
|
||||
Observable,
|
||||
defer,
|
||||
of,
|
||||
shareReplay,
|
||||
combineLatestWith,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
containsLowerCaseValidator,
|
||||
containsNumberValidator,
|
||||
@@ -14,161 +25,200 @@ import {
|
||||
import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { catchError, filter } from 'rxjs/operators';
|
||||
import { User } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { UserService } from '../../../../services/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-password',
|
||||
templateUrl: './password.component.html',
|
||||
styleUrls: ['./password.component.scss'],
|
||||
})
|
||||
export class PasswordComponent implements OnDestroy {
|
||||
userId: string = '';
|
||||
public username: string = '';
|
||||
|
||||
public policy!: PasswordComplexityPolicy.AsObject;
|
||||
public passwordForm!: UntypedFormGroup;
|
||||
|
||||
private formSub: Subscription = new Subscription();
|
||||
private destroy$: Subject<void> = new Subject();
|
||||
export class PasswordComponent implements OnInit {
|
||||
private readonly breadcrumb$: Observable<Breadcrumb[]>;
|
||||
protected readonly username$: Observable<string>;
|
||||
protected readonly id$: Observable<string | undefined>;
|
||||
protected readonly form$: Observable<UntypedFormGroup>;
|
||||
protected readonly passwordPolicy$: Observable<PasswordComplexityPolicy.AsObject | undefined>;
|
||||
protected readonly user$: Observable<User.AsObject>;
|
||||
|
||||
constructor(
|
||||
activatedRoute: ActivatedRoute,
|
||||
private fb: UntypedFormBuilder,
|
||||
private authService: GrpcAuthService,
|
||||
private mgmtUserService: ManagementService,
|
||||
private toast: ToastService,
|
||||
private breadcrumbService: BreadcrumbService,
|
||||
private readonly fb: UntypedFormBuilder,
|
||||
private readonly authService: GrpcAuthService,
|
||||
private readonly userService: UserService,
|
||||
private readonly toast: ToastService,
|
||||
private readonly breadcrumbService: BreadcrumbService,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
) {
|
||||
activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((data) => {
|
||||
const { username } = data;
|
||||
this.username = username;
|
||||
});
|
||||
activatedRoute.params.pipe(takeUntil(this.destroy$)).subscribe((data) => {
|
||||
const { id } = data;
|
||||
const usernameParam$ = activatedRoute.queryParamMap.pipe(
|
||||
map((params) => params.get('username')),
|
||||
filter(Boolean),
|
||||
);
|
||||
this.id$ = activatedRoute.paramMap.pipe(map((params) => params.get('id') ?? undefined));
|
||||
|
||||
this.user$ = this.authService.user.pipe(take(1), filter(Boolean));
|
||||
this.username$ = usernameParam$.pipe(mergeWith(this.user$.pipe(map((user) => user.preferredLoginName))));
|
||||
|
||||
this.breadcrumb$ = this.getBreadcrumb$(this.id$, this.user$);
|
||||
this.passwordPolicy$ = this.getPasswordPolicy$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
const validators$ = this.getValidators$(this.passwordPolicy$);
|
||||
this.form$ = this.getForm$(this.id$, validators$);
|
||||
}
|
||||
|
||||
private getBreadcrumb$(id$: Observable<string | undefined>, user$: Observable<User.AsObject>): Observable<Breadcrumb[]> {
|
||||
return id$.pipe(
|
||||
switchMap(async (id) => {
|
||||
if (id) {
|
||||
this.userId = id;
|
||||
breadcrumbService.setBreadcrumb([
|
||||
return [
|
||||
new Breadcrumb({
|
||||
type: BreadcrumbType.ORG,
|
||||
routerLink: ['/org'],
|
||||
}),
|
||||
]);
|
||||
} else {
|
||||
this.authService.user.pipe(take(1)).subscribe((user) => {
|
||||
if (user) {
|
||||
this.username = user.preferredLoginName;
|
||||
this.breadcrumbService.setBreadcrumb([
|
||||
];
|
||||
}
|
||||
const user = await firstValueFrom(user$);
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
new Breadcrumb({
|
||||
type: BreadcrumbType.AUTHUSER,
|
||||
name: user.human?.profile?.displayName,
|
||||
routerLink: ['/users', 'me'],
|
||||
}),
|
||||
]);
|
||||
}
|
||||
});
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getValidators$(
|
||||
passwordPolicy$: Observable<PasswordComplexityPolicy.AsObject | undefined>,
|
||||
): Observable<Validators[]> {
|
||||
return passwordPolicy$.pipe(
|
||||
map((policy) => {
|
||||
const validators: Validators[] = [requiredValidator];
|
||||
this.authService
|
||||
.getMyPasswordComplexityPolicy()
|
||||
.then((resp) => {
|
||||
if (resp.policy) {
|
||||
this.policy = resp.policy;
|
||||
if (!policy) {
|
||||
return validators;
|
||||
}
|
||||
if (this.policy.minLength) {
|
||||
validators.push(minLengthValidator(this.policy.minLength));
|
||||
if (policy.minLength) {
|
||||
validators.push(minLengthValidator(policy.minLength));
|
||||
}
|
||||
if (this.policy.hasLowercase) {
|
||||
if (policy.hasLowercase) {
|
||||
validators.push(containsLowerCaseValidator);
|
||||
}
|
||||
if (this.policy.hasUppercase) {
|
||||
if (policy.hasUppercase) {
|
||||
validators.push(containsUpperCaseValidator);
|
||||
}
|
||||
if (this.policy.hasNumber) {
|
||||
if (policy.hasNumber) {
|
||||
validators.push(containsNumberValidator);
|
||||
}
|
||||
if (this.policy.hasSymbol) {
|
||||
if (policy.hasSymbol) {
|
||||
validators.push(containsSymbolValidator);
|
||||
}
|
||||
|
||||
this.setupForm(validators);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setupForm(validators);
|
||||
});
|
||||
});
|
||||
return validators;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.formSub.unsubscribe();
|
||||
}
|
||||
|
||||
setupForm(validators: Validators[]): void {
|
||||
if (this.userId) {
|
||||
this.passwordForm = this.fb.group({
|
||||
private getForm$(
|
||||
id$: Observable<string | undefined>,
|
||||
validators$: Observable<Validators[]>,
|
||||
): Observable<UntypedFormGroup> {
|
||||
return id$.pipe(
|
||||
combineLatestWith(validators$),
|
||||
map(([id, validators]) => {
|
||||
if (id) {
|
||||
return this.fb.group({
|
||||
password: ['', validators],
|
||||
confirmPassword: ['', [requiredValidator, passwordConfirmValidator()]],
|
||||
});
|
||||
} else {
|
||||
this.passwordForm = this.fb.group({
|
||||
return this.fb.group({
|
||||
currentPassword: ['', requiredValidator],
|
||||
newPassword: ['', validators],
|
||||
confirmPassword: ['', [requiredValidator, passwordConfirmValidator()]],
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public setInitialPassword(userId: string): void {
|
||||
if (this.passwordForm.valid && this.password && this.password.value) {
|
||||
this.mgmtUserService
|
||||
.setHumanInitialPassword(userId, this.password.value)
|
||||
.then((data: any) => {
|
||||
private getPasswordPolicy$(): Observable<PasswordComplexityPolicy.AsObject | undefined> {
|
||||
return defer(() => this.authService.getMyPasswordComplexityPolicy()).pipe(
|
||||
map((resp) => resp.policy),
|
||||
catchError(() => of(undefined)),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.breadcrumb$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((breadcrumbs) => {
|
||||
this.breadcrumbService.setBreadcrumb(breadcrumbs);
|
||||
});
|
||||
}
|
||||
|
||||
public async setInitialPassword(userId: string, form: UntypedFormGroup): Promise<void> {
|
||||
const password = this.password(form)?.value;
|
||||
|
||||
if (form.invalid || !password) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.userService.setPassword({
|
||||
userId,
|
||||
newPassword: {
|
||||
password,
|
||||
changeRequired: false,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
return;
|
||||
}
|
||||
this.toast.showInfo('USER.TOAST.INITIALPASSWORDSET', true);
|
||||
window.history.back();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public setPassword(): void {
|
||||
if (
|
||||
this.passwordForm.valid &&
|
||||
this.currentPassword &&
|
||||
this.currentPassword.value &&
|
||||
this.newPassword &&
|
||||
this.newPassword.value &&
|
||||
this.newPassword.valid
|
||||
) {
|
||||
this.authService
|
||||
.updateMyPassword(this.currentPassword.value, this.newPassword.value)
|
||||
.then((data: any) => {
|
||||
public async setPassword(form: UntypedFormGroup, user: User.AsObject): Promise<void> {
|
||||
const currentPassword = this.currentPassword(form);
|
||||
const newPassword = this.newPassword(form);
|
||||
|
||||
if (form.invalid || !currentPassword?.value || !newPassword?.value || newPassword?.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.userService.setPassword({
|
||||
userId: user.id,
|
||||
newPassword: {
|
||||
password: newPassword.value,
|
||||
changeRequired: false,
|
||||
},
|
||||
verification: {
|
||||
case: 'currentPassword',
|
||||
value: currentPassword.value,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
return;
|
||||
}
|
||||
this.toast.showInfo('USER.TOAST.PASSWORDCHANGED', true);
|
||||
window.history.back();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get password(): AbstractControl | null {
|
||||
return this.passwordForm.get('password');
|
||||
public password(form: UntypedFormGroup): AbstractControl | null {
|
||||
return form.get('password');
|
||||
}
|
||||
|
||||
public get newPassword(): AbstractControl | null {
|
||||
return this.passwordForm.get('newPassword');
|
||||
public newPassword(form: UntypedFormGroup): AbstractControl | null {
|
||||
return form.get('newPassword');
|
||||
}
|
||||
|
||||
public get currentPassword(): AbstractControl | null {
|
||||
return this.passwordForm.get('currentPassword');
|
||||
}
|
||||
|
||||
public get confirmPassword(): AbstractControl | null {
|
||||
return this.passwordForm.get('confirmPassword');
|
||||
public currentPassword(form: UntypedFormGroup): AbstractControl | null {
|
||||
return form.get('currentPassword');
|
||||
}
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@
|
||||
<cnsl-refresh-table [hideRefresh]="true" [loading]="loading$ | async" [dataSize]="dataSource.data.length">
|
||||
<button
|
||||
actions
|
||||
[disabled]="user && disabled"
|
||||
[disabled]="disabled"
|
||||
class="button"
|
||||
(click)="sendPasswordlessRegistration()"
|
||||
mat-raised-button
|
||||
@@ -31,7 +31,7 @@
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'USER.PASSWORDLESS.NAME' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let mfa">
|
||||
<span *ngIf="mfa?.name" class="centered">
|
||||
{{ mfa?.name }}
|
||||
{{ mfa.name }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -42,8 +42,8 @@
|
||||
<span
|
||||
class="state"
|
||||
[ngClass]="{
|
||||
active: mfa.state === AuthFactorState.AUTH_FACTOR_STATE_READY,
|
||||
inactive: mfa.state === AuthFactorState.AUTH_FACTOR_STATE_NOT_READY,
|
||||
active: mfa.state === AuthFactorState.READY,
|
||||
inactive: mfa.state === AuthFactorState.NOT_READY,
|
||||
}"
|
||||
>{{ 'USER.PASSWORDLESS.STATE.' + mfa.state | translate }}</span
|
||||
>
|
||||
|
@@ -1,12 +1,13 @@
|
||||
import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTable, MatTableDataSource } from '@angular/material/table';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { BehaviorSubject, Observable, switchMap } from 'rxjs';
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
import { AuthFactorState, User, WebAuthNToken } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { AuthFactorState, Passkey, User } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { UserService } from 'src/app/services/user.service';
|
||||
import { filter } from 'rxjs/operators';
|
||||
|
||||
export interface WebAuthNOptions {
|
||||
challenge: string;
|
||||
@@ -24,23 +25,22 @@ export interface WebAuthNOptions {
|
||||
styleUrls: ['./passwordless.component.scss'],
|
||||
})
|
||||
export class PasswordlessComponent implements OnInit, OnDestroy {
|
||||
@Input() public user!: User.AsObject;
|
||||
@Input({ required: true }) public user!: User;
|
||||
@Input() public disabled: boolean = true;
|
||||
public displayedColumns: string[] = ['name', 'state', 'actions'];
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
|
||||
@ViewChild(MatTable) public table!: MatTable<WebAuthNToken.AsObject>;
|
||||
@ViewChild(MatSort) public sort!: MatSort;
|
||||
public dataSource: MatTableDataSource<WebAuthNToken.AsObject> = new MatTableDataSource<WebAuthNToken.AsObject>([]);
|
||||
public dataSource: MatTableDataSource<Passkey> = new MatTableDataSource<Passkey>([]);
|
||||
|
||||
public AuthFactorState: any = AuthFactorState;
|
||||
public AuthFactorState = AuthFactorState;
|
||||
public error: string = '';
|
||||
|
||||
constructor(
|
||||
private service: ManagementService,
|
||||
private toast: ToastService,
|
||||
private dialog: MatDialog,
|
||||
private userService: UserService,
|
||||
) {}
|
||||
|
||||
public ngOnInit(): void {
|
||||
@@ -52,10 +52,10 @@ export class PasswordlessComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
public getPasswordless(): void {
|
||||
this.service
|
||||
.listHumanPasswordless(this.user.id)
|
||||
this.userService
|
||||
.listPasskeys({ userId: this.user.userId })
|
||||
.then((passwordless) => {
|
||||
this.dataSource = new MatTableDataSource(passwordless.resultList);
|
||||
this.dataSource = new MatTableDataSource(passwordless.result);
|
||||
this.dataSource.sort = this.sort;
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -63,7 +63,7 @@ export class PasswordlessComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
public deletePasswordless(id?: string): void {
|
||||
public deletePasswordless(passkeyId: string): void {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.DELETE',
|
||||
@@ -74,24 +74,26 @@ export class PasswordlessComponent implements OnInit, OnDestroy {
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp && id) {
|
||||
this.service
|
||||
.removeHumanPasswordless(id, this.user.id)
|
||||
.then(() => {
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() => this.userService.removePasskeys({ userId: this.user.userId, passkeyId })),
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toast.showInfo('USER.TOAST.PASSWORDLESSREMOVED', true);
|
||||
this.getPasswordless();
|
||||
})
|
||||
.catch((error) => {
|
||||
},
|
||||
error: (error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public sendPasswordlessRegistration(): void {
|
||||
this.service
|
||||
.sendPasswordlessRegistration(this.user.id)
|
||||
this.userService
|
||||
.createPasskeyRegistrationLink({ userId: this.user.userId, medium: { case: 'sendLink', value: {} } })
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PASSWORDLESSREGISTRATIONSENT', true);
|
||||
})
|
||||
|
@@ -1,99 +1,101 @@
|
||||
<ng-container *ngIf="user$ | async as userQuery">
|
||||
<cnsl-top-view
|
||||
*ngIf="user"
|
||||
title="{{ user.human ? user.human.profile?.displayName : user.machine?.name }}"
|
||||
*ngIf="(userQuery.state === 'success' || userQuery.state === 'loading') && userQuery.value as user"
|
||||
title="{{ user.type.case === 'human' ? user.type.value.profile?.displayName : user.type.value?.name }}"
|
||||
docLink="https://zitadel.com/docs/guides/manage/console/users"
|
||||
sub="{{ user.preferredLoginName }}"
|
||||
[isActive]="user.state === UserState.USER_STATE_ACTIVE"
|
||||
[isInactive]="user.state === UserState.USER_STATE_INACTIVE"
|
||||
[isActive]="user.state === UserState.ACTIVE"
|
||||
[isInactive]="user.state === UserState.INACTIVE"
|
||||
stateTooltip="{{ 'USER.STATE.' + user.state | translate }}"
|
||||
(backClicked)="navigateBack()"
|
||||
[hasActions]="['user.write$', 'user.write:' + user.id] | hasRole | async"
|
||||
[hasActions]="['user.write$', 'user.write:' + user.userId] | hasRole | async"
|
||||
>
|
||||
<ng-template topActions cnslHasRole [hasRole]="['user.write$', 'user.write:' + user.id]">
|
||||
<button mat-menu-item color="warn" *ngIf="user?.machine" (click)="generateMachineSecret()">
|
||||
<ng-template topActions cnslHasRole [hasRole]="['user.write$', 'user.write:' + user.userId]">
|
||||
<ng-container *ngIf="user.type.case === 'machine'">
|
||||
<button mat-menu-item color="warn" (click)="generateMachineSecret(user)">
|
||||
{{ 'USER.PAGES.GENERATESECRET' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item color="warn" *ngIf="user?.machine?.hasSecret" (click)="removeMachineSecret()">
|
||||
<button mat-menu-item color="warn" *ngIf="user.type.value.hasSecret" (click)="removeMachineSecret(user)">
|
||||
{{ 'USER.PAGES.REMOVESECRET' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item color="warn" *ngIf="user?.state === UserState.USER_STATE_LOCKED" (click)="unlockUser()">
|
||||
</ng-container>
|
||||
<button mat-menu-item color="warn" *ngIf="user?.state === UserState.LOCKED" (click)="unlockUser(user)">
|
||||
{{ 'USER.PAGES.UNLOCK' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="user?.state === UserState.USER_STATE_ACTIVE"
|
||||
(click)="changeState(UserState.USER_STATE_INACTIVE)"
|
||||
>
|
||||
<button mat-menu-item *ngIf="user.state === UserState.ACTIVE" (click)="changeState(user, UserState.INACTIVE)">
|
||||
{{ 'USER.PAGES.DEACTIVATE' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngIf="user?.state === UserState.USER_STATE_INACTIVE"
|
||||
(click)="changeState(UserState.USER_STATE_ACTIVE)"
|
||||
>
|
||||
<button mat-menu-item *ngIf="user.state === UserState.INACTIVE" (click)="changeState(user, UserState.ACTIVE)">
|
||||
{{ 'USER.PAGES.REACTIVATE' | translate }}
|
||||
</button>
|
||||
<ng-template cnslHasRole [hasRole]="['user.delete$', 'user.delete:' + user.id]">
|
||||
<button mat-menu-item matTooltip="{{ 'USER.PAGES.DELETE' | translate }}" (click)="deleteUser()">
|
||||
<ng-template cnslHasRole [hasRole]="['user.delete$', 'user.delete:' + user.userId]">
|
||||
<button mat-menu-item matTooltip="{{ 'USER.PAGES.DELETE' | translate }}" (click)="deleteUser(user)">
|
||||
<span [style.color]="'var(--warn)'">{{ 'USER.PAGES.DELETE' | translate }}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<cnsl-info-row topContent *ngIf="user" [user]="user" [loginPolicy]="loginPolicy"></cnsl-info-row>
|
||||
<cnsl-info-row topContent [user]="user" [loginPolicy]="(loginPolicy$ | async) ?? undefined"></cnsl-info-row>
|
||||
</cnsl-top-view>
|
||||
|
||||
<div *ngIf="loading" class="max-width-container">
|
||||
<div *ngIf="userQuery.state === 'loading'" class="max-width-container">
|
||||
<div class="sp-wrapper">
|
||||
<mat-progress-spinner diameter="25" color="primary" mode="indeterminate"></mat-progress-spinner>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!loading && !user" class="max-width-container">
|
||||
<div *ngIf="userQuery.state === 'notfound'" class="max-width-container">
|
||||
<p class="no-user-error">{{ 'USER.PAGES.NOUSER' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="max-width-container" *ngIf="user && (['user.write$', 'user.write:' + user.id] | hasRole) as canWrite$">
|
||||
<ng-container *ngIf="(userQuery.state === 'success' || userQuery.state === 'loading') && userQuery.value as user">
|
||||
<div class="max-width-container" *ngIf="['user.write$', 'user.write:' + user.userId] | hasRole as canWrite$">
|
||||
<cnsl-meta-layout>
|
||||
<cnsl-sidenav [(ngModel)]="currentSetting" [settingsList]="settingsList" queryParam="id">
|
||||
<div *ngIf="error" class="max-width-container">
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<cnsl-sidenav
|
||||
*ngIf="settingsList$ | async as settingsList"
|
||||
[ngModel]="currentSetting$ | async"
|
||||
(ngModelChange)="goToSetting($event)"
|
||||
[settingsList]="settingsList"
|
||||
queryParam="id"
|
||||
>
|
||||
<div class="max-width-container">
|
||||
<cnsl-info-section class="locked" *ngIf="user?.state === UserState.USER_STATE_LOCKED" [type]="InfoSectionType.WARN">
|
||||
<cnsl-info-section class="locked" *ngIf="user?.state === UserState.LOCKED" [type]="InfoSectionType.WARN">
|
||||
{{ 'USER.PAGES.LOCKEDDESCRIPTION' | translate }}</cnsl-info-section
|
||||
>
|
||||
<span *ngIf="!loading && !user">{{ 'USER.PAGES.NOUSER' | translate }}</span>
|
||||
|
||||
<div *ngIf="user && user.state === UserState.USER_STATE_INITIAL">
|
||||
<div *ngIf="user && user.state === UserState.INITIAL">
|
||||
<cnsl-info-section class="is-initial-info-section" [type]="InfoSectionType.ALERT">
|
||||
<div class="is-initial-row">
|
||||
<span>{{ 'USER.ISINITIAL' | translate }}</span>
|
||||
<button [disabled]="(canWrite$ | async) === false" mat-stroked-button (click)="resendInitEmail()">
|
||||
<button [disabled]="(canWrite$ | async) === false" mat-stroked-button (click)="resendInitEmail(user)">
|
||||
{{ 'USER.RESENDINITIALEMAIL' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</cnsl-info-section>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="currentSetting === 'general'">
|
||||
<ng-template cnslHasRole [hasRole]="['user.read$', 'user.read:' + user.id]">
|
||||
<cnsl-card *ngIf="user.human" title="{{ user.preferredLoginName }} - {{ 'USER.PROFILE.TITLE' | translate }}">
|
||||
<ng-container *ngIf="(currentSetting$ | async) === 'general'">
|
||||
<ng-template
|
||||
*ngIf="humanUser(user) as user"
|
||||
cnslHasRole
|
||||
[hasRole]="['user.read$', 'user.read:' + user.userId]"
|
||||
>
|
||||
<cnsl-card
|
||||
*ngIf="user.type.value.profile as profile"
|
||||
title="{{ user.preferredLoginName }} - {{ 'USER.PROFILE.TITLE' | translate }}"
|
||||
>
|
||||
<cnsl-detail-form
|
||||
[preferredLoginName]="user.preferredLoginName"
|
||||
[disabled]="(canWrite$ | async) === false"
|
||||
[genders]="genders"
|
||||
[languages]="(langSvc.supported$ | async) || []"
|
||||
[username]="user.userName"
|
||||
[user]="user.human"
|
||||
(submitData)="saveProfile($event)"
|
||||
(changeUsernameClicked)="changeUsername()"
|
||||
[username]="user.username"
|
||||
[profile]="profile"
|
||||
(submitData)="saveProfile(user, $event)"
|
||||
(changeUsernameClicked)="changeUsername(user)"
|
||||
>
|
||||
</cnsl-detail-form>
|
||||
</cnsl-card>
|
||||
|
||||
<cnsl-card
|
||||
*ngIf="user.human"
|
||||
title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}"
|
||||
description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}"
|
||||
>
|
||||
@@ -101,36 +103,34 @@
|
||||
card-actions
|
||||
class="icon-button"
|
||||
mat-icon-button
|
||||
(click)="refreshUser()"
|
||||
(click)="refreshChanges$.emit()"
|
||||
matTooltip="{{ 'ACTIONS.REFRESH' | translate }}"
|
||||
>
|
||||
<mat-icon class="icon">refresh</mat-icon>
|
||||
</button>
|
||||
<cnsl-contact
|
||||
[disablePhoneCode]="true"
|
||||
[state]="user.state"
|
||||
[username]="user.preferredLoginName"
|
||||
[canWrite]="['user.write:' + user.id, 'user.write$'] | hasRole | async"
|
||||
*ngIf="user?.human"
|
||||
[human]="user.human"
|
||||
[canWrite]="['user.write:' + user.userId, 'user.write$'] | hasRole | async"
|
||||
[human]="user.type.value"
|
||||
(editType)="
|
||||
user.state === UserState.USER_STATE_INITIAL && $event === EditDialogType.EMAIL
|
||||
? resendInitEmail()
|
||||
: openEditDialog($event)
|
||||
user.state === UserState.INITIAL && $event === EditDialogType.EMAIL
|
||||
? resendInitEmail(user)
|
||||
: openEditDialog(user, $event)
|
||||
"
|
||||
(deletedPhone)="deletePhone()"
|
||||
(resendEmailVerification)="resendEmailVerification()"
|
||||
(resendPhoneVerification)="resendPhoneVerification()"
|
||||
(deletedPhone)="deletePhone(user)"
|
||||
(resendEmailVerification)="resendEmailVerification(user)"
|
||||
(resendPhoneVerification)="resendPhoneVerification(user)"
|
||||
>
|
||||
<button
|
||||
pwdAction
|
||||
[disabled]="(canWrite$ | async) === false"
|
||||
(click)="sendSetPasswordNotification()"
|
||||
(click)="sendSetPasswordNotification(user)"
|
||||
mat-stroked-button
|
||||
*ngIf="
|
||||
user.state !== UserState.USER_STATE_LOCKED &&
|
||||
user.state !== UserState.USER_STATE_INACTIVE &&
|
||||
user.state !== UserState.USER_STATE_INITIAL
|
||||
user.state !== UserState.LOCKED &&
|
||||
user.state !== UserState.INACTIVE &&
|
||||
user.state !== UserState.INITIAL
|
||||
"
|
||||
>
|
||||
{{ 'USER.PASSWORD.RESENDNOTIFICATION' | translate }}
|
||||
@@ -140,49 +140,50 @@
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting && currentSetting === 'idp'">
|
||||
<cnsl-external-idps *ngIf="user && user.human && user.id" [userId]="user.id" [service]="mgmtUserService">
|
||||
</cnsl-external-idps>
|
||||
</ng-container>
|
||||
<cnsl-external-idps
|
||||
*ngIf="(currentSetting$ | async) === 'idp' && user.type.case === 'human' && user.userId"
|
||||
[userId]="user.userId"
|
||||
[service]="mgmtService"
|
||||
/>
|
||||
|
||||
<ng-container *ngIf="currentSetting && currentSetting === 'general'">
|
||||
<cnsl-card *ngIf="user.machine" title="{{ 'USER.MACHINE.TITLE' | translate }}">
|
||||
<cnsl-card
|
||||
*ngIf="(currentSetting$ | async) === 'general' && user.type.case === 'machine'"
|
||||
title="{{ 'USER.MACHINE.TITLE' | translate }}"
|
||||
>
|
||||
<cnsl-detail-form-machine
|
||||
[disabled]="(canWrite$ | async) === false"
|
||||
[username]="user.userName"
|
||||
[user]="user.machine"
|
||||
(submitData)="saveMachine($event)"
|
||||
>
|
||||
</cnsl-detail-form-machine>
|
||||
[username]="user.username"
|
||||
[user]="user.type.value"
|
||||
(submitData)="saveMachine(user, $event)"
|
||||
/>
|
||||
</cnsl-card>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting && currentSetting === 'pat'">
|
||||
<ng-template cnslHasRole [hasRole]="['user.read$', 'user.read:' + user.id]">
|
||||
<ng-container *ngIf="(currentSetting$ | async) === 'pat'">
|
||||
<ng-template cnslHasRole [hasRole]="['user.read$', 'user.read:' + user.userId]">
|
||||
<cnsl-card
|
||||
*ngIf="user.machine && user.id"
|
||||
*ngIf="user.type.case === 'machine' && user.userId"
|
||||
title="{{ 'USER.MACHINE.TOKENSTITLE' | translate }}"
|
||||
description="{{ 'USER.MACHINE.TOKENSDESC' | translate }}"
|
||||
>
|
||||
<cnsl-personal-access-tokens [userId]="user.id"></cnsl-personal-access-tokens>
|
||||
<cnsl-personal-access-tokens [userId]="user.userId" />
|
||||
</cnsl-card>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting && currentSetting === 'keys'">
|
||||
<ng-template cnslHasRole [hasRole]="['user.read$', 'user.read:' + user.id]">
|
||||
<ng-container *ngIf="(currentSetting$ | async) === 'keys'">
|
||||
<ng-template cnslHasRole [hasRole]="['user.read$', 'user.read:' + user.userId]">
|
||||
<cnsl-card
|
||||
*ngIf="user.machine && user.id"
|
||||
*ngIf="user.type.case === 'machine' && user.userId"
|
||||
title="{{ 'USER.MACHINE.KEYSTITLE' | translate }}"
|
||||
description="{{ 'USER.MACHINE.KEYSDESC' | translate }}"
|
||||
>
|
||||
<cnsl-machine-keys [userId]="user.id"></cnsl-machine-keys>
|
||||
<cnsl-machine-keys [userId]="user.userId" />
|
||||
</cnsl-card>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting && currentSetting === 'security'">
|
||||
<cnsl-card *ngIf="user.human" title="{{ 'USER.PASSWORD.TITLE' | translate }}">
|
||||
<ng-container *ngIf="(currentSetting$ | async) === 'security'">
|
||||
<cnsl-card *ngIf="user.type.case === 'human'" title="{{ 'USER.PASSWORD.TITLE' | translate }}">
|
||||
<div class="contact-method-col">
|
||||
<div class="contact-method-row">
|
||||
<div class="left">
|
||||
@@ -195,7 +196,7 @@
|
||||
<div class="right">
|
||||
<a
|
||||
matTooltip="{{ 'USER.PASSWORD.SET' | translate }}"
|
||||
[disabled]="(['user.write:' + user.id, 'user.write$'] | hasRole | async) === false"
|
||||
[disabled]="(['user.write:' + user.userId, 'user.write$'] | hasRole | async) === false"
|
||||
[routerLink]="['password']"
|
||||
[queryParams]="{ username: user.preferredLoginName }"
|
||||
mat-icon-button
|
||||
@@ -207,22 +208,30 @@
|
||||
</div>
|
||||
</cnsl-card>
|
||||
|
||||
<cnsl-passwordless *ngIf="user && !!user.human" [user]="user" [disabled]="(canWrite$ | async) === false">
|
||||
<cnsl-passwordless *ngIf="user.type.case === 'human'" [user]="user" [disabled]="(canWrite$ | async) === false">
|
||||
</cnsl-passwordless>
|
||||
|
||||
<cnsl-user-mfa *ngIf="user && user.human" [user]="user"></cnsl-user-mfa>
|
||||
<cnsl-user-mfa *ngIf="user.type.case === 'human'" [user]="user"></cnsl-user-mfa>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting && currentSetting === 'grants'">
|
||||
<ng-container *ngIf="(currentSetting$ | async) === 'grants'">
|
||||
<cnsl-card
|
||||
*ngIf="user?.id"
|
||||
*ngIf="user.userId"
|
||||
title="{{ 'GRANTS.USER.TITLE' | translate }}"
|
||||
description="{{ 'GRANTS.USER.DESCRIPTION' | translate }}"
|
||||
>
|
||||
<cnsl-user-grants
|
||||
[userId]="user.id"
|
||||
[userId]="user.userId"
|
||||
[context]="USERGRANTCONTEXT"
|
||||
[displayedColumns]="['select', 'projectId', 'creationDate', 'changeDate', 'state', 'roleNamesList', 'actions']"
|
||||
[displayedColumns]="[
|
||||
'select',
|
||||
'projectId',
|
||||
'creationDate',
|
||||
'changeDate',
|
||||
'state',
|
||||
'roleNamesList',
|
||||
'actions',
|
||||
]"
|
||||
[disableWrite]="(['user.grant.write$'] | hasRole | async) === false"
|
||||
[disableDelete]="(['user.grant.delete$'] | hasRole | async) === false"
|
||||
>
|
||||
@@ -230,33 +239,39 @@
|
||||
</cnsl-card>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting && currentSetting === 'memberships'">
|
||||
<ng-container *ngIf="(currentSetting$ | async) === 'memberships'">
|
||||
<cnsl-card
|
||||
*ngIf="user?.id"
|
||||
*ngIf="user.userId"
|
||||
title="{{ 'USER.MEMBERSHIPS.TITLE' | translate }}"
|
||||
description="{{ 'USER.MEMBERSHIPS.DESCRIPTION' | translate }}"
|
||||
>
|
||||
<cnsl-memberships-table [userId]="user.id"></cnsl-memberships-table>
|
||||
<cnsl-memberships-table [userId]="user.userId"></cnsl-memberships-table>
|
||||
</cnsl-card>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentSetting && currentSetting === 'metadata'">
|
||||
<ng-container *ngIf="(currentSetting$ | async) === 'metadata' && (metadata$ | async) as metadataQuery">
|
||||
<cnsl-metadata
|
||||
[metadata]="metadata"
|
||||
*ngIf="user.userId && metadataQuery.state !== 'error'"
|
||||
[metadata]="metadataQuery.value"
|
||||
[description]="
|
||||
(user.machine ? 'DESCRIPTIONS.USERS.MACHINES.METADATA' : 'DESCRIPTIONS.USERS.HUMANS.METADATA') | translate
|
||||
(user.type.case === 'machine'
|
||||
? 'DESCRIPTIONS.USERS.MACHINES.METADATA'
|
||||
: 'DESCRIPTIONS.USERS.HUMANS.METADATA'
|
||||
) | translate
|
||||
"
|
||||
[disabled]="(['user.write:' + user.id, 'user.write'] | hasRole | async) === false"
|
||||
*ngIf="user && user.id"
|
||||
(editClicked)="editMetadata()"
|
||||
(refresh)="loadMetadata(user.id)"
|
||||
[loading]="metadataQuery.state === 'loading'"
|
||||
[disabled]="(['user.write:' + user.userId, 'user.write'] | hasRole | async) === false"
|
||||
(editClicked)="editMetadata(user, metadataQuery.value)"
|
||||
(refresh)="refreshMetadata$.next(true)"
|
||||
></cnsl-metadata>
|
||||
</ng-container>
|
||||
</div>
|
||||
</cnsl-sidenav>
|
||||
<div metainfo>
|
||||
<cnsl-changes class="changes" [refresh]="refreshChanges$" [changeType]="ChangeType.USER" [id]="user.id">
|
||||
<cnsl-changes class="changes" [refresh]="refreshChanges$" [changeType]="ChangeType.USER" [id]="user.userId">
|
||||
</cnsl-changes>
|
||||
</div>
|
||||
</cnsl-meta-layout>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
@@ -1,32 +1,61 @@
|
||||
import { MediaMatcher } from '@angular/cdk/layout';
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, EventEmitter, OnInit } from '@angular/core';
|
||||
import { Component, DestroyRef, EventEmitter, OnInit } from '@angular/core';
|
||||
import { Validators } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Buffer } from 'buffer';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { catchError, filter, map, startWith, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import { ChangeType } from 'src/app/modules/changes/changes.component';
|
||||
import { phoneValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators';
|
||||
import { InfoSectionType } from 'src/app/modules/info-section/info-section.component';
|
||||
import { MetadataDialogComponent } from 'src/app/modules/metadata/metadata-dialog/metadata-dialog.component';
|
||||
import {
|
||||
MetadataDialogComponent,
|
||||
MetadataDialogData,
|
||||
} from 'src/app/modules/metadata/metadata-dialog/metadata-dialog.component';
|
||||
import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component';
|
||||
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
import { SendHumanResetPasswordNotificationRequest, UnlockUserRequest } from 'src/app/proto/generated/zitadel/management_pb';
|
||||
import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
|
||||
import { LoginPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
||||
import { Email, Gender, Machine, Phone, Profile, User, UserState } 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 { formatPhone } from 'src/app/utils/formatPhone';
|
||||
import { EditDialogComponent, EditDialogType } from '../auth-user-detail/edit-dialog/edit-dialog.component';
|
||||
import { ResendEmailDialogComponent } from '../auth-user-detail/resend-email-dialog/resend-email-dialog.component';
|
||||
import {
|
||||
EditDialogData,
|
||||
EditDialogResult,
|
||||
EditDialogComponent,
|
||||
EditDialogType,
|
||||
} from '../auth-user-detail/edit-dialog/edit-dialog.component';
|
||||
import {
|
||||
ResendEmailDialogComponent,
|
||||
ResendEmailDialogData,
|
||||
ResendEmailDialogResult,
|
||||
} from '../auth-user-detail/resend-email-dialog/resend-email-dialog.component';
|
||||
import { MachineSecretDialogComponent } from './machine-secret-dialog/machine-secret-dialog.component';
|
||||
import { Observable } from 'rxjs';
|
||||
import { LanguagesService } from '../../../../services/languages.service';
|
||||
import { LanguagesService } from 'src/app/services/languages.service';
|
||||
import { UserService } from 'src/app/services/user.service';
|
||||
import { Gender, HumanProfile, HumanUser, User as UserV2, UserState } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import {
|
||||
combineLatestWith,
|
||||
defer,
|
||||
EMPTY,
|
||||
fromEvent,
|
||||
mergeWith,
|
||||
Observable,
|
||||
ObservedValueOf,
|
||||
of,
|
||||
shareReplay,
|
||||
Subject,
|
||||
switchMap,
|
||||
} from 'rxjs';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { DetailFormMachineComponent } from '../detail-form-machine/detail-form-machine.component';
|
||||
import { NewMgmtService } from 'src/app/services/new-mgmt.service';
|
||||
import { LoginPolicy } from '@zitadel/proto/zitadel/policy_pb';
|
||||
import { SendHumanResetPasswordNotificationRequest_Type } from '@zitadel/proto/zitadel/management_pb';
|
||||
import { pairwiseStartWith } from 'src/app/utils/pairwiseStartWith';
|
||||
import { Metadata } from '@zitadel/proto/zitadel/metadata_pb';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
|
||||
const GENERAL: SidenavSetting = { id: 'general', i18nKey: 'USER.SETTINGS.GENERAL' };
|
||||
const GRANTS: SidenavSetting = { id: 'grants', i18nKey: 'USER.SETTINGS.USERGRANTS' };
|
||||
@@ -37,22 +66,31 @@ const PERSONALACCESSTOKEN: SidenavSetting = { id: 'pat', i18nKey: 'USER.SETTINGS
|
||||
const KEYS: SidenavSetting = { id: 'keys', i18nKey: 'USER.SETTINGS.KEYS' };
|
||||
const MEMBERSHIPS: SidenavSetting = { id: 'memberships', i18nKey: 'USER.SETTINGS.MEMBERSHIPS' };
|
||||
|
||||
type UserQuery =
|
||||
| { state: 'success'; value: UserV2 }
|
||||
| { state: 'error'; value: string }
|
||||
| { state: 'loading'; value?: UserV2 }
|
||||
| { state: 'notfound' };
|
||||
|
||||
type MetadataQuery =
|
||||
| { state: 'success'; value: Metadata[] }
|
||||
| { state: 'loading'; value: Metadata[] }
|
||||
| { state: 'error'; value: string };
|
||||
|
||||
type UserWithHumanType = Omit<UserV2, 'type'> & { type: { case: 'human'; value: HumanUser } };
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-user-detail',
|
||||
templateUrl: './user-detail.component.html',
|
||||
styleUrls: ['./user-detail.component.scss'],
|
||||
})
|
||||
export class UserDetailComponent implements OnInit {
|
||||
public user!: User.AsObject;
|
||||
public metadata: Metadata.AsObject[] = [];
|
||||
public genders: Gender[] = [Gender.GENDER_MALE, Gender.GENDER_FEMALE, Gender.GENDER_DIVERSE];
|
||||
public user$: Observable<UserQuery>;
|
||||
public genders: Gender[] = [Gender.MALE, Gender.FEMALE, Gender.DIVERSE];
|
||||
|
||||
public ChangeType: any = ChangeType;
|
||||
|
||||
public loading: boolean = true;
|
||||
public loadingMetadata: boolean = true;
|
||||
|
||||
public UserState: any = UserState;
|
||||
public UserState = UserState;
|
||||
public copied: string = '';
|
||||
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER;
|
||||
|
||||
@@ -60,32 +98,27 @@ export class UserDetailComponent implements OnInit {
|
||||
public refreshChanges$: EventEmitter<void> = new EventEmitter();
|
||||
public InfoSectionType: any = InfoSectionType;
|
||||
|
||||
public error: string = '';
|
||||
|
||||
public settingsList: SidenavSetting[] = [GENERAL, GRANTS, MEMBERSHIPS, METADATA];
|
||||
public currentSetting: string | undefined = 'general';
|
||||
public loginPolicy?: LoginPolicy.AsObject;
|
||||
public currentSetting$: Observable<string | undefined>;
|
||||
public settingsList$: Observable<SidenavSetting[]>;
|
||||
public metadata$: Observable<MetadataQuery>;
|
||||
public loginPolicy$: Observable<LoginPolicy>;
|
||||
public refreshMetadata$ = new Subject<true>();
|
||||
|
||||
constructor(
|
||||
public translate: TranslateService,
|
||||
private route: ActivatedRoute,
|
||||
private readonly route: ActivatedRoute,
|
||||
private toast: ToastService,
|
||||
public mgmtUserService: ManagementService,
|
||||
private _location: Location,
|
||||
private dialog: MatDialog,
|
||||
private router: Router,
|
||||
activatedRoute: ActivatedRoute,
|
||||
private mediaMatcher: MediaMatcher,
|
||||
public langSvc: LanguagesService,
|
||||
private readonly userService: UserService,
|
||||
private readonly newMgmtService: NewMgmtService,
|
||||
public readonly mgmtService: ManagementService,
|
||||
breadcrumbService: BreadcrumbService,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
) {
|
||||
activatedRoute.queryParams.pipe(take(1)).subscribe((params: Params) => {
|
||||
const { id } = params;
|
||||
if (id) {
|
||||
this.currentSetting = id;
|
||||
}
|
||||
});
|
||||
|
||||
breadcrumbService.setBreadcrumb([
|
||||
new Breadcrumb({
|
||||
type: BreadcrumbType.ORG,
|
||||
@@ -93,63 +126,127 @@ export class UserDetailComponent implements OnInit {
|
||||
}),
|
||||
]);
|
||||
|
||||
this.currentSetting$ = this.getCurrentSetting$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.user$ = this.getUser$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.settingsList$ = this.getSettingsList$(this.user$);
|
||||
this.metadata$ = this.getMetadata$(this.user$).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
|
||||
this.loginPolicy$ = defer(() => this.newMgmtService.getLoginPolicy()).pipe(
|
||||
catchError(() => EMPTY),
|
||||
map(({ policy }) => policy),
|
||||
filter(Boolean),
|
||||
);
|
||||
}
|
||||
|
||||
private getId$(): Observable<string> {
|
||||
return this.route.paramMap.pipe(
|
||||
map((params) => params.get('id')),
|
||||
filter(Boolean),
|
||||
);
|
||||
}
|
||||
|
||||
private getUser$(): Observable<UserQuery> {
|
||||
return this.getId$().pipe(
|
||||
combineLatestWith(this.refreshChanges$.pipe(startWith(undefined))),
|
||||
switchMap(([id]) => this.getUserById(id)),
|
||||
pairwiseStartWith(undefined),
|
||||
map(([prev, curr]) => {
|
||||
if (prev?.state === 'success' && curr.state === 'loading') {
|
||||
return { state: 'loading', value: prev.value } as const;
|
||||
}
|
||||
return curr;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getSettingsList$(user$: Observable<UserQuery>): Observable<SidenavSetting[]> {
|
||||
return user$.pipe(
|
||||
switchMap((user) => {
|
||||
if (user.state !== 'success') {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
if (user.value.type.case === 'human') {
|
||||
return of([GENERAL, SECURITY, IDP, GRANTS, MEMBERSHIPS, METADATA]);
|
||||
} else if (user.value.type.case === 'machine') {
|
||||
return of([GENERAL, GRANTS, MEMBERSHIPS, PERSONALACCESSTOKEN, KEYS, METADATA]);
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
startWith([GENERAL, GRANTS, MEMBERSHIPS, METADATA]),
|
||||
);
|
||||
}
|
||||
|
||||
private getCurrentSetting$(): Observable<string | undefined> {
|
||||
const mediaq: string = '(max-width: 500px)';
|
||||
const small = this.mediaMatcher.matchMedia(mediaq).matches;
|
||||
if (small) {
|
||||
this.changeSelection(small);
|
||||
}
|
||||
this.mediaMatcher.matchMedia(mediaq).onchange = (small) => {
|
||||
this.changeSelection(small.matches);
|
||||
};
|
||||
const matcher = this.mediaMatcher.matchMedia(mediaq);
|
||||
const small$ = fromEvent(matcher, 'change', ({ matches }: MediaQueryListEvent) => matches).pipe(
|
||||
startWith(matcher.matches),
|
||||
);
|
||||
|
||||
return this.route.queryParamMap.pipe(
|
||||
map((params) => params.get('id')),
|
||||
filter(Boolean),
|
||||
startWith('general'),
|
||||
withLatestFrom(small$),
|
||||
map(([id, small]) => (small ? undefined : id)),
|
||||
);
|
||||
}
|
||||
|
||||
private changeSelection(small: boolean): void {
|
||||
if (small) {
|
||||
this.currentSetting = undefined;
|
||||
} else {
|
||||
this.currentSetting = this.currentSetting === undefined ? 'general' : this.currentSetting;
|
||||
}
|
||||
}
|
||||
|
||||
refreshUser(): void {
|
||||
this.refreshChanges$.emit();
|
||||
this.route.params.pipe(take(1)).subscribe((params) => {
|
||||
this.loading = true;
|
||||
const { id } = params;
|
||||
this.mgmtUserService
|
||||
.getUserByID(id)
|
||||
.then((resp) => {
|
||||
this.loadMetadata(id);
|
||||
this.loading = false;
|
||||
if (resp.user) {
|
||||
this.user = resp.user;
|
||||
|
||||
if (this.user.human) {
|
||||
this.settingsList = [GENERAL, SECURITY, IDP, GRANTS, MEMBERSHIPS, METADATA];
|
||||
} else if (this.user.machine) {
|
||||
this.settingsList = [GENERAL, GRANTS, MEMBERSHIPS, PERSONALACCESSTOKEN, KEYS, METADATA];
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.error = err.message ?? '';
|
||||
this.loading = false;
|
||||
this.toast.showError(err);
|
||||
});
|
||||
public async goToSetting(setting: string) {
|
||||
await this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { id: setting },
|
||||
queryParamsHandling: 'merge',
|
||||
skipLocationChange: true,
|
||||
});
|
||||
}
|
||||
|
||||
private getUserById(userId: string): Observable<UserQuery> {
|
||||
return defer(() => this.userService.getUserById(userId)).pipe(
|
||||
map(({ user }) => {
|
||||
if (user) {
|
||||
return { state: 'success', value: user } as const;
|
||||
}
|
||||
return { state: 'notfound' } as const;
|
||||
}),
|
||||
catchError((error) => of({ state: 'error', value: error.message ?? '' } as const)),
|
||||
startWith({ state: 'loading' } as const),
|
||||
);
|
||||
}
|
||||
|
||||
getMetadata$(user$: Observable<UserQuery>): Observable<MetadataQuery> {
|
||||
return this.refreshMetadata$.pipe(
|
||||
startWith(true),
|
||||
combineLatestWith(user$),
|
||||
switchMap(([_, user]) => {
|
||||
if (!(user.state === 'success' || user.state === 'loading')) {
|
||||
return EMPTY;
|
||||
}
|
||||
if (!user.value) {
|
||||
return EMPTY;
|
||||
}
|
||||
return this.getMetadataById(user.value.userId);
|
||||
}),
|
||||
pairwiseStartWith(undefined),
|
||||
map(([prev, curr]) => {
|
||||
if (prev?.state === 'success' && curr.state === 'loading') {
|
||||
return { state: 'loading', value: prev.value } as const;
|
||||
}
|
||||
return curr;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.refreshUser();
|
||||
|
||||
this.mgmtUserService.getLoginPolicy().then((policy) => {
|
||||
if (policy.policy) {
|
||||
this.loginPolicy = policy.policy;
|
||||
this.user$.pipe(mergeWith(this.metadata$), takeUntilDestroyed(this.destroyRef)).subscribe((query) => {
|
||||
if (query.state == 'error') {
|
||||
this.toast.showError(query.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public changeUsername(): void {
|
||||
public changeUsername(user: UserV2): void {
|
||||
const dialogRef = this.dialog.open(EditDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.CHANGE',
|
||||
@@ -157,43 +254,45 @@ export class UserDetailComponent implements OnInit {
|
||||
labelKey: 'ACTIONS.NEWVALUE',
|
||||
titleKey: 'USER.PROFILE.CHANGEUSERNAME_TITLE',
|
||||
descriptionKey: 'USER.PROFILE.CHANGEUSERNAME_DESC',
|
||||
value: this.user.userName,
|
||||
value: user.username,
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((resp: { value: string }) => {
|
||||
if (resp.value && resp.value !== this.user.userName) {
|
||||
this.mgmtUserService
|
||||
.updateUserName(this.user.id, resp.value)
|
||||
.then(() => {
|
||||
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.refreshUser();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.refreshChanges$.emit();
|
||||
},
|
||||
error: (error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public unlockUser(): void {
|
||||
const req = new UnlockUserRequest();
|
||||
req.setId(this.user.id);
|
||||
this.mgmtUserService
|
||||
.unlockUser(req)
|
||||
public unlockUser(user: UserV2): void {
|
||||
this.userService
|
||||
.unlockUser(user.userId)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.UNLOCKED', true);
|
||||
this.refreshUser();
|
||||
this.refreshChanges$.emit();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public generateMachineSecret(): void {
|
||||
this.mgmtUserService
|
||||
.generateMachineSecret(this.user.id)
|
||||
public generateMachineSecret(user: UserV2): void {
|
||||
this.newMgmtService
|
||||
.generateMachineSecret(user.userId)
|
||||
.then((resp) => {
|
||||
this.toast.showInfo('USER.TOAST.SECRETGENERATED', true);
|
||||
this.dialog.open(MachineSecretDialogComponent, {
|
||||
@@ -203,64 +302,41 @@ export class UserDetailComponent implements OnInit {
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
this.refreshUser();
|
||||
this.refreshChanges$.emit();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public removeMachineSecret(): void {
|
||||
this.mgmtUserService
|
||||
.removeMachineSecret(this.user.id)
|
||||
.then((resp) => {
|
||||
public removeMachineSecret(user: UserV2): void {
|
||||
this.newMgmtService
|
||||
.removeMachineSecret(user.userId)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.SECRETREMOVED', true);
|
||||
this.refreshUser();
|
||||
this.refreshChanges$.emit();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public changeState(newState: UserState): void {
|
||||
if (newState === UserState.USER_STATE_ACTIVE) {
|
||||
this.mgmtUserService
|
||||
.reactivateUser(this.user.id)
|
||||
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.user.state = newState;
|
||||
this.refreshChanges$.emit();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else if (newState === UserState.USER_STATE_INACTIVE) {
|
||||
this.mgmtUserService
|
||||
.deactivateUser(this.user.id)
|
||||
} else if (newState === UserState.INACTIVE) {
|
||||
this.userService
|
||||
.deactivateUser(user.userId)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.DEACTIVATED', true);
|
||||
this.user.state = newState;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public saveProfile(profileData: Profile.AsObject): void {
|
||||
if (this.user.human) {
|
||||
this.user.human.profile = profileData;
|
||||
this.mgmtUserService
|
||||
.updateHumanProfile(
|
||||
this.user.id,
|
||||
this.user.human.profile.firstName,
|
||||
this.user.human.profile.lastName,
|
||||
this.user.human.profile.nickName,
|
||||
this.user.human.profile.displayName,
|
||||
this.user.human.profile.preferredLanguage,
|
||||
this.user.human.profile.gender,
|
||||
)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.SAVED', true);
|
||||
this.refreshChanges$.emit();
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -269,19 +345,19 @@ export class UserDetailComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public saveMachine(machineData: Machine.AsObject): void {
|
||||
if (this.user.machine) {
|
||||
this.user.machine.name = machineData.name;
|
||||
this.user.machine.description = machineData.description;
|
||||
this.user.machine.accessTokenType = machineData.accessTokenType;
|
||||
|
||||
this.mgmtUserService
|
||||
.updateMachine(
|
||||
this.user.id,
|
||||
this.user.machine.name,
|
||||
this.user.machine.description,
|
||||
this.user.machine.accessTokenType,
|
||||
)
|
||||
public saveProfile(user: UserV2, profile: HumanProfile): void {
|
||||
this.userService
|
||||
.updateUser({
|
||||
userId: user.userId,
|
||||
profile: {
|
||||
givenName: profile.givenName,
|
||||
familyName: profile.familyName,
|
||||
nickName: profile.nickName,
|
||||
displayName: profile.displayName,
|
||||
preferredLanguage: profile.preferredLanguage,
|
||||
gender: profile.gender,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.SAVED', true);
|
||||
this.refreshChanges$.emit();
|
||||
@@ -290,11 +366,27 @@ export class UserDetailComponent implements OnInit {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public saveMachine(user: UserV2, form: ObservedValueOf<DetailFormMachineComponent['submitData']>): void {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
public resendEmailVerification(): void {
|
||||
this.mgmtUserService
|
||||
.resendHumanEmailVerification(this.user.id)
|
||||
public resendEmailVerification(user: UserV2): void {
|
||||
this.newMgmtService
|
||||
.resendHumanEmailVerification(user.userId)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
|
||||
this.refreshChanges$.emit();
|
||||
@@ -304,9 +396,9 @@ export class UserDetailComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public resendPhoneVerification(): void {
|
||||
this.mgmtUserService
|
||||
.resendHumanPhoneVerification(this.user.id)
|
||||
public resendPhoneVerification(user: UserV2): void {
|
||||
this.newMgmtService
|
||||
.resendHumanPhoneVerification(user.userId)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
|
||||
this.refreshChanges$.emit();
|
||||
@@ -316,79 +408,25 @@ export class UserDetailComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public deletePhone(): void {
|
||||
this.mgmtUserService
|
||||
.removeHumanPhone(this.user.id)
|
||||
public deletePhone(user: UserV2): void {
|
||||
this.userService
|
||||
.removePhone(user.userId)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
|
||||
if (this.user.human) {
|
||||
this.user.human.phone = new Phone().setPhone('').toObject();
|
||||
this.refreshUser();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public saveEmail(email: string, isVerified: boolean): void {
|
||||
if (this.user.id && email) {
|
||||
this.mgmtUserService
|
||||
.updateHumanEmail(this.user.id, email, isVerified)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
|
||||
if (this.user.state === UserState.USER_STATE_INITIAL) {
|
||||
this.mgmtUserService
|
||||
.resendHumanInitialization(this.user.id, email ?? '')
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.INITEMAILSENT', true);
|
||||
this.refreshChanges$.emit();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
if (this.user.human) {
|
||||
this.user.human.email = new Email().setEmail(email).toObject();
|
||||
this.refreshUser();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public savePhone(phone: string): void {
|
||||
if (this.user.id && phone) {
|
||||
// Format phone before save (add +)
|
||||
const formattedPhone = formatPhone(phone);
|
||||
if (formattedPhone) {
|
||||
phone = formattedPhone.phone;
|
||||
}
|
||||
|
||||
this.mgmtUserService
|
||||
.updateHumanPhone(this.user.id, phone)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
|
||||
if (this.user.human) {
|
||||
this.user.human.phone = new Phone().setPhone(phone).toObject();
|
||||
this.refreshUser();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public navigateBack(): void {
|
||||
this._location.back();
|
||||
}
|
||||
|
||||
public sendSetPasswordNotification(): void {
|
||||
this.mgmtUserService
|
||||
.sendHumanResetPasswordNotification(this.user.id, SendHumanResetPasswordNotificationRequest.Type.TYPE_EMAIL)
|
||||
public sendSetPasswordNotification(user: UserV2): void {
|
||||
this.newMgmtService
|
||||
.sendHumanResetPasswordNotification(user.userId, SendHumanResetPasswordNotificationRequest_Type.EMAIL)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.PASSWORDNOTIFICATIONSENT', true);
|
||||
this.refreshChanges$.emit();
|
||||
@@ -398,85 +436,76 @@ export class UserDetailComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public deleteUser(): void {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
public deleteUser(user: UserV2): void {
|
||||
const data = {
|
||||
confirmKey: 'ACTIONS.DELETE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
titleKey: 'USER.DIALOG.DELETE_TITLE',
|
||||
descriptionKey: 'USER.DIALOG.DELETE_DESCRIPTION',
|
||||
},
|
||||
};
|
||||
|
||||
const dialogRef = this.dialog.open<WarnDialogComponent, typeof data, boolean>(WarnDialogComponent, {
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.mgmtUserService
|
||||
.removeUser(this.user.id)
|
||||
.then(() => {
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() => this.userService.deleteUser(user.userId)),
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
const params: Params = {
|
||||
deferredReload: true,
|
||||
type: this.user.human ? 'humans' : 'machines',
|
||||
type: user.type.case === 'human' ? 'humans' : 'machines',
|
||||
};
|
||||
this.router.navigate(['/users'], { queryParams: params });
|
||||
this.router.navigate(['/users'], { queryParams: params }).then();
|
||||
this.toast.showInfo('USER.TOAST.DELETED', true);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
},
|
||||
error: (error) => this.toast.showError(error),
|
||||
});
|
||||
}
|
||||
|
||||
public resendInitEmail(): void {
|
||||
const dialogRef = this.dialog.open(ResendEmailDialogComponent, {
|
||||
public resendInitEmail(user: UserV2): void {
|
||||
const dialogRef = this.dialog.open<ResendEmailDialogComponent, ResendEmailDialogData, ResendEmailDialogResult>(
|
||||
ResendEmailDialogComponent,
|
||||
{
|
||||
width: '400px',
|
||||
data: {
|
||||
email: this.user.human?.email?.email ?? '',
|
||||
email: user.type.case === 'human' ? (user.type.value.email?.email ?? '') : '',
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp.send && this.user.id) {
|
||||
this.mgmtUserService
|
||||
.resendHumanInitialization(this.user.id, resp.email ?? '')
|
||||
.then(() => {
|
||||
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();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
},
|
||||
error: (error) => this.toast.showError(error),
|
||||
});
|
||||
}
|
||||
|
||||
public openEditDialog(type: EditDialogType): void {
|
||||
public openEditDialog(user: UserWithHumanType, type: EditDialogType): void {
|
||||
switch (type) {
|
||||
case EditDialogType.PHONE:
|
||||
const dialogRefPhone = this.dialog.open(EditDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.SAVE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
labelKey: 'ACTIONS.NEWVALUE',
|
||||
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
|
||||
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
|
||||
value: this.user.human?.phone?.phone,
|
||||
type: EditDialogType.PHONE,
|
||||
validator: Validators.compose([phoneValidator, requiredValidator]),
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRefPhone.afterClosed().subscribe((resp: { value: string; isVerified: boolean }) => {
|
||||
if (resp && resp.value) {
|
||||
this.savePhone(resp.value);
|
||||
}
|
||||
});
|
||||
break;
|
||||
this.openEditPhoneDialog(user);
|
||||
return;
|
||||
case EditDialogType.EMAIL:
|
||||
const dialogRefEmail = this.dialog.open(EditDialogComponent, {
|
||||
data: {
|
||||
this.openEditEmailDialog(user);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private openEditEmailDialog(user: UserWithHumanType) {
|
||||
const data: EditDialogData = {
|
||||
confirmKey: 'ACTIONS.SAVE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
labelKey: 'ACTIONS.NEWVALUE',
|
||||
@@ -484,57 +513,112 @@ export class UserDetailComponent implements OnInit {
|
||||
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
|
||||
isVerifiedTextKey: 'USER.LOGINMETHODS.EMAIL.ISVERIFIED',
|
||||
isVerifiedTextDescKey: 'USER.LOGINMETHODS.EMAIL.ISVERIFIEDDESC',
|
||||
value: this.user.human?.email?.email,
|
||||
value: user.type.value?.email?.email,
|
||||
type: EditDialogType.EMAIL,
|
||||
},
|
||||
} as const;
|
||||
|
||||
const dialogRefEmail = this.dialog.open<EditDialogComponent, EditDialogData, EditDialogResult>(EditDialogComponent, {
|
||||
data,
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRefEmail.afterClosed().subscribe((resp: { value: string; isVerified: boolean }) => {
|
||||
if (resp && resp.value) {
|
||||
this.saveEmail(resp.value, resp.isVerified);
|
||||
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),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public loadMetadata(id: string): Promise<any> | void {
|
||||
this.loadingMetadata = true;
|
||||
return this.mgmtUserService
|
||||
.listUserMetadata(id)
|
||||
.then((resp) => {
|
||||
this.loadingMetadata = false;
|
||||
this.metadata = resp.resultList.map((md) => {
|
||||
return {
|
||||
key: md.key,
|
||||
value: Buffer.from(md.value as string, 'base64').toString('utf-8'),
|
||||
private openEditPhoneDialog(user: UserWithHumanType) {
|
||||
const data = {
|
||||
confirmKey: 'ACTIONS.SAVE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
labelKey: 'ACTIONS.NEWVALUE',
|
||||
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
|
||||
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
|
||||
value: user.type.value.phone?.phone,
|
||||
type: EditDialogType.PHONE,
|
||||
validator: Validators.compose([phoneValidator, requiredValidator]),
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.loadingMetadata = false;
|
||||
const dialogRefPhone = this.dialog.open<EditDialogComponent, typeof data, { value: string; isVerified: boolean }>(
|
||||
EditDialogComponent,
|
||||
{ data, width: '400px' },
|
||||
);
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public editMetadata(): void {
|
||||
if (this.user) {
|
||||
const setFcn = (key: string, value: string): Promise<any> =>
|
||||
this.mgmtUserService.setUserMetadata(key, Buffer.from(value).toString('base64'), this.user.id);
|
||||
const removeFcn = (key: string): Promise<any> => this.mgmtUserService.removeUserMetadata(key, this.user.id);
|
||||
private getMetadataById(userId: string): Observable<MetadataQuery> {
|
||||
return defer(() => this.newMgmtService.listUserMetadata(userId)).pipe(
|
||||
map((metadata) => ({ state: 'success', value: metadata.result }) as const),
|
||||
startWith({ state: 'loading', value: [] as Metadata[] } as const),
|
||||
catchError((err) => of({ state: 'error', value: err.message ?? '' } as const)),
|
||||
);
|
||||
}
|
||||
|
||||
const dialogRef = this.dialog.open(MetadataDialogComponent, {
|
||||
public editMetadata(user: UserV2, metadata: Metadata[]): void {
|
||||
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 dialogRef = this.dialog.open<MetadataDialogComponent, MetadataDialogData>(MetadataDialogComponent, {
|
||||
data: {
|
||||
metadata: this.metadata,
|
||||
metadata: [...metadata],
|
||||
setFcn: setFcn,
|
||||
removeFcn: removeFcn,
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.loadMetadata(this.user.id);
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(() => {
|
||||
this.refreshMetadata$.next(true);
|
||||
});
|
||||
}
|
||||
|
||||
public humanUser(user: UserV2): UserWithHumanType | undefined {
|
||||
if (user.type.case === 'human') {
|
||||
return { ...user, type: user.type };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,12 @@
|
||||
<cnsl-card title="{{ 'USER.MFA.TITLE' | translate }}" description="{{ 'USER.MFA.DESCRIPTION' | translate }}">
|
||||
<cnsl-card
|
||||
*ngIf="mfaQuery$ | async as mfaQuery"
|
||||
title="{{ 'USER.MFA.TITLE' | translate }}"
|
||||
description="{{ 'USER.MFA.DESCRIPTION' | translate }}"
|
||||
>
|
||||
<button
|
||||
card-actions
|
||||
mat-icon-button
|
||||
(click)="getMFAs()"
|
||||
(click)="refresh$.next(true)"
|
||||
class="icon-button"
|
||||
matTooltip="{{ 'ACTIONS.REFRESH' | translate }}"
|
||||
>
|
||||
@@ -10,26 +14,26 @@
|
||||
</button>
|
||||
<cnsl-refresh-table
|
||||
[hideRefresh]="true"
|
||||
[loading]="loading$ | async"
|
||||
(refreshed)="getMFAs()"
|
||||
[dataSize]="dataSource.data.length"
|
||||
[loading]="mfaQuery.state === 'loading'"
|
||||
(refreshed)="refresh$.next(true)"
|
||||
[dataSize]="mfaQuery.value.data.length"
|
||||
>
|
||||
<table class="table" mat-table [dataSource]="dataSource">
|
||||
<table class="table" mat-table [dataSource]="mfaQuery.value">
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'USER.MFA.TABLETYPE' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let mfa">
|
||||
<span *ngIf="mfa.otp !== undefined">TOTP (Time-based One-Time Password)</span>
|
||||
<span *ngIf="mfa.u2f !== undefined">U2F (Universal 2nd Factor)</span>
|
||||
<span *ngIf="mfa.otpSms !== undefined">One-Time Password SMS</span>
|
||||
<span *ngIf="mfa.otpEmail !== undefined">One-Time Password Email</span>
|
||||
<span *ngIf="mfa.type.case === 'otp'">TOTP (Time-based One-Time Password)</span>
|
||||
<span *ngIf="mfa.type.case === 'u2f'">U2F (Universal 2nd Factor)</span>
|
||||
<span *ngIf="mfa.type.case === 'otpSms'">One-Time Password SMS</span>
|
||||
<span *ngIf="mfa.type.case === 'otpEmail'">One-Time Password Email</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'USER.MFA.NAME' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let mfa">
|
||||
<span *ngIf="mfa?.u2f?.name" class="centered">
|
||||
{{ mfa.u2f.name }}
|
||||
<span *ngIf="mfa.type.case === 'u2f'" class="centered">
|
||||
{{ mfa.type.value.name }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -40,8 +44,8 @@
|
||||
<span
|
||||
class="state"
|
||||
[ngClass]="{
|
||||
active: mfa.state === AuthFactorState.AUTH_FACTOR_STATE_READY,
|
||||
inactive: mfa.state === AuthFactorState.AUTH_FACTOR_STATE_NOT_READY,
|
||||
active: mfa.state === AuthFactorState.READY,
|
||||
inactive: mfa.state === AuthFactorState.NOT_READY,
|
||||
}"
|
||||
>
|
||||
{{ 'USER.MFA.STATE.' + mfa.state | translate }}
|
||||
@@ -58,7 +62,7 @@
|
||||
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
|
||||
color="warn"
|
||||
mat-icon-button
|
||||
(click)="deleteMFA(mfa)"
|
||||
(click)="deleteMFA(mfaQuery.user, mfa)"
|
||||
>
|
||||
<i class="las la-trash"></i>
|
||||
</button>
|
||||
@@ -69,13 +73,13 @@
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
|
||||
<div *ngIf="mfaQuery.state === 'success' && !mfaQuery.value.data.length" class="no-content-row">
|
||||
<i class="las la-exclamation"></i>
|
||||
<span>{{ 'USER.MFA.EMPTY' | translate }}</span>
|
||||
</div>
|
||||
</cnsl-refresh-table>
|
||||
<div class="table-wrapper">
|
||||
<div class="spinner-container" *ngIf="loading$ | async">
|
||||
<div class="spinner-container" *ngIf="mfaQuery.state === 'loading'">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,65 +1,104 @@
|
||||
import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { Component, Input, ViewChild } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTable, MatTableDataSource } from '@angular/material/table';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { combineLatestWith, defer, EMPTY, Observable, ReplaySubject, Subject, switchMap } from 'rxjs';
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
import { AuthFactor, AuthFactorState, User } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { UserService } from 'src/app/services/user.service';
|
||||
import { AuthFactor, AuthFactorState, User } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { catchError, filter, map, startWith } from 'rxjs/operators';
|
||||
import { pairwiseStartWith } from 'src/app/utils/pairwiseStartWith';
|
||||
|
||||
export interface MFAItem {
|
||||
name: string;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
type MFAQuery =
|
||||
| { state: 'success'; value: MatTableDataSource<AuthFactor>; user: User }
|
||||
| { state: 'loading'; value: MatTableDataSource<AuthFactor>; user: User };
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-user-mfa',
|
||||
templateUrl: './user-mfa.component.html',
|
||||
styleUrls: ['./user-mfa.component.scss'],
|
||||
})
|
||||
export class UserMfaComponent implements OnInit, OnDestroy {
|
||||
public displayedColumns: string[] = ['type', 'name', 'state', 'actions'];
|
||||
@Input() public user!: User.AsObject;
|
||||
public mfaSubject: BehaviorSubject<AuthFactor.AsObject[]> = new BehaviorSubject<AuthFactor.AsObject[]>([]);
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
export class UserMfaComponent {
|
||||
@Input({ required: true }) public set user(user: User) {
|
||||
this.user$.next(user);
|
||||
}
|
||||
|
||||
@ViewChild(MatTable) public table!: MatTable<AuthFactor.AsObject>;
|
||||
@ViewChild(MatSort) public sort!: MatSort;
|
||||
public dataSource: MatTableDataSource<AuthFactor.AsObject> = new MatTableDataSource<AuthFactor.AsObject>([]);
|
||||
public dataSource = new MatTableDataSource<AuthFactor>([]);
|
||||
|
||||
public AuthFactorState: any = AuthFactorState;
|
||||
public displayedColumns: string[] = ['type', 'name', 'state', 'actions'];
|
||||
private user$ = new ReplaySubject<User>(1);
|
||||
public mfaQuery$: Observable<MFAQuery>;
|
||||
public refresh$ = new Subject<true>();
|
||||
public AuthFactorState = AuthFactorState;
|
||||
|
||||
public error: string = '';
|
||||
constructor(
|
||||
private mgmtUserService: ManagementService,
|
||||
private dialog: MatDialog,
|
||||
private toast: ToastService,
|
||||
) {}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.getMFAs();
|
||||
private readonly dialog: MatDialog,
|
||||
private readonly toast: ToastService,
|
||||
private readonly userService: UserService,
|
||||
) {
|
||||
this.mfaQuery$ = this.user$.pipe(
|
||||
combineLatestWith(this.refresh$.pipe(startWith(true))),
|
||||
switchMap(([user]) => this.listAuthenticationFactors(user)),
|
||||
pairwiseStartWith(undefined),
|
||||
map(([prev, curr]) => {
|
||||
if (prev?.state === 'success' && curr.state === 'loading') {
|
||||
return { ...prev, state: 'loading' } as const;
|
||||
}
|
||||
return curr;
|
||||
}),
|
||||
catchError((error) => {
|
||||
this.toast.showError(error);
|
||||
return EMPTY;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.mfaSubject.complete();
|
||||
this.loadingSubject.complete();
|
||||
private listAuthenticationFactors(user: User): Observable<MFAQuery> {
|
||||
return defer(() => this.userService.listAuthenticationFactors({ userId: user.userId })).pipe(
|
||||
map(
|
||||
({ result }) =>
|
||||
({
|
||||
state: 'success',
|
||||
value: new MatTableDataSource<AuthFactor>(result),
|
||||
user,
|
||||
}) as const,
|
||||
),
|
||||
startWith({
|
||||
state: 'loading',
|
||||
value: new MatTableDataSource<AuthFactor>([]),
|
||||
user,
|
||||
} as const),
|
||||
);
|
||||
}
|
||||
|
||||
public getMFAs(): void {
|
||||
this.mgmtUserService
|
||||
.listHumanMultiFactors(this.user.id)
|
||||
.then((mfas) => {
|
||||
this.dataSource = new MatTableDataSource(mfas.resultList);
|
||||
this.dataSource.sort = this.sort;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.error = error.message;
|
||||
});
|
||||
private async removeTOTP(user: User) {
|
||||
await this.userService.removeTOTP(user.userId);
|
||||
return ['USER.TOAST.OTPREMOVED', 'otp'] as const;
|
||||
}
|
||||
|
||||
public deleteMFA(factor: AuthFactor.AsObject): void {
|
||||
private async removeU2F(user: User, u2fId: string) {
|
||||
await this.userService.removeU2F(user.userId, u2fId);
|
||||
return ['USER.TOAST.U2FREMOVED', 'u2f'] as const;
|
||||
}
|
||||
|
||||
private async removeOTPEmail(user: User) {
|
||||
await this.userService.removeOTPEmail(user.userId);
|
||||
return ['USER.TOAST.OTPREMOVED', 'otpEmail'] as const;
|
||||
}
|
||||
|
||||
private async removeOTPSMS(user: User) {
|
||||
await this.userService.removeOTPSMS(user.userId);
|
||||
return ['USER.TOAST.OTPREMOVED', 'otpSms'] as const;
|
||||
}
|
||||
|
||||
public deleteMFA(user: User, factor: AuthFactor): void {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.DELETE',
|
||||
@@ -70,70 +109,35 @@ export class UserMfaComponent implements OnInit, OnDestroy {
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
if (factor.otp) {
|
||||
this.mgmtUserService
|
||||
.removeHumanMultiFactorOTP(this.user.id)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.OTPREMOVED', true);
|
||||
|
||||
const index = this.dataSource.data.findIndex((mfa) => !!mfa.otp);
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() => {
|
||||
switch (factor.type.case) {
|
||||
case 'otp':
|
||||
return this.removeTOTP(user);
|
||||
case 'u2f':
|
||||
return this.removeU2F(user, factor.type.value.id);
|
||||
case 'otpEmail':
|
||||
return this.removeOTPEmail(user);
|
||||
case 'otpSms':
|
||||
return this.removeOTPSMS(user);
|
||||
default:
|
||||
throw new Error('Unknown MFA type');
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next: ([translation, caseId]) => {
|
||||
this.toast.showInfo(translation, true);
|
||||
const index = this.dataSource.data.findIndex((mfa) => mfa.type.case === caseId);
|
||||
if (index > -1) {
|
||||
this.dataSource.data.splice(index, 1);
|
||||
}
|
||||
this.getMFAs();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else if (factor.u2f) {
|
||||
this.mgmtUserService
|
||||
.removeHumanAuthFactorU2F(this.user.id, factor.u2f.id)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.U2FREMOVED', true);
|
||||
|
||||
const index = this.dataSource.data.findIndex((mfa) => !!mfa.u2f);
|
||||
if (index > -1) {
|
||||
this.dataSource.data.splice(index, 1);
|
||||
}
|
||||
this.getMFAs();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else if (factor.otpEmail) {
|
||||
this.mgmtUserService
|
||||
.removeHumanAuthFactorOTPEmail(this.user.id)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.OTPREMOVED', true);
|
||||
|
||||
const index = this.dataSource.data.findIndex((mfa) => !!mfa.otpEmail);
|
||||
if (index > -1) {
|
||||
this.dataSource.data.splice(index, 1);
|
||||
}
|
||||
this.getMFAs();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else if (factor.otpSms) {
|
||||
this.mgmtUserService
|
||||
.removeHumanAuthFactorOTPSMS(this.user.id)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.OTPREMOVED', true);
|
||||
|
||||
const index = this.dataSource.data.findIndex((mfa) => !!mfa.otpSms);
|
||||
if (index > -1) {
|
||||
this.dataSource.data.splice(index, 1);
|
||||
}
|
||||
this.getMFAs();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
this.refresh$.next(true);
|
||||
},
|
||||
error: (error) => this.toast.showError(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -11,21 +11,20 @@
|
||||
<div leftActions class="user-toggle-group">
|
||||
<cnsl-nav-toggle
|
||||
label="{{ 'DESCRIPTIONS.USERS.HUMANS.TITLE' | translate }}"
|
||||
(clicked)="setType(Type.TYPE_HUMAN)"
|
||||
[active]="type === Type.TYPE_HUMAN"
|
||||
(clicked)="setType(Type.HUMAN)"
|
||||
[active]="type === Type.HUMAN"
|
||||
data-e2e="list-humans"
|
||||
></cnsl-nav-toggle>
|
||||
<cnsl-nav-toggle
|
||||
label="{{ 'DESCRIPTIONS.USERS.MACHINES.TITLE' | translate }}"
|
||||
(clicked)="setType(Type.TYPE_MACHINE)"
|
||||
[active]="type === Type.TYPE_MACHINE"
|
||||
(clicked)="setType(Type.MACHINE)"
|
||||
[active]="type === Type.MACHINE"
|
||||
data-e2e="list-machines"
|
||||
></cnsl-nav-toggle>
|
||||
</div>
|
||||
<p class="user-sub cnsl-secondary-text">
|
||||
{{
|
||||
(type === Type.TYPE_HUMAN ? 'DESCRIPTIONS.USERS.HUMANS.DESCRIPTION' : 'DESCRIPTIONS.USERS.MACHINES.DESCRIPTION')
|
||||
| translate
|
||||
(type === Type.HUMAN ? 'DESCRIPTIONS.USERS.HUMANS.DESCRIPTION' : 'DESCRIPTIONS.USERS.MACHINES.DESCRIPTION') | translate
|
||||
}}
|
||||
</p>
|
||||
<ng-template cnslHasRole [hasRole]="['user.write']" actions>
|
||||
@@ -66,7 +65,7 @@
|
||||
></cnsl-filter-user>
|
||||
<ng-template cnslHasRole [hasRole]="['user.write']" actions>
|
||||
<button
|
||||
(click)="gotoRouterLink(['/users', type === Type.TYPE_HUMAN ? 'create' : 'create-machine'])"
|
||||
(click)="gotoRouterLink(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
||||
color="primary"
|
||||
mat-raised-button
|
||||
[disabled]="(canWrite$ | async) === false"
|
||||
@@ -78,7 +77,7 @@
|
||||
<span>{{ 'ACTIONS.NEW' | translate }}</span>
|
||||
<cnsl-action-keys
|
||||
*ngIf="!filterOpen"
|
||||
(actionTriggered)="gotoRouterLink(['/users', type === Type.TYPE_HUMAN ? 'create' : 'create-machine'])"
|
||||
(actionTriggered)="gotoRouterLink(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
|
||||
>
|
||||
</cnsl-action-keys>
|
||||
</div>
|
||||
@@ -115,10 +114,10 @@
|
||||
[checked]="selection.isSelected(user)"
|
||||
>
|
||||
<cnsl-avatar
|
||||
*ngIf="user.human && user.human.profile; else cog"
|
||||
*ngIf="user.type.case === 'human' && user.type.value.profile; else cog"
|
||||
class="avatar"
|
||||
[name]="user.human.profile.displayName"
|
||||
[avatarUrl]="user.human.profile.avatarUrl || ''"
|
||||
[name]="user.type.value.profile.displayName"
|
||||
[avatarUrl]="user.type.value.profile.avatarUrl || ''"
|
||||
[forColor]="user.preferredLoginName"
|
||||
[size]="32"
|
||||
>
|
||||
@@ -142,9 +141,9 @@
|
||||
>
|
||||
{{ 'USER.PROFILE.DISPLAYNAME' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id] : null">
|
||||
<span *ngIf="user.human">{{ user.human.profile?.displayName }}</span>
|
||||
<span *ngIf="user.machine">{{ user.machine.name }}</span>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<span *ngIf="user.type.case === 'human'">{{ user.type.value?.profile?.displayName }}</span>
|
||||
<span *ngIf="user.type.case === 'machine'">{{ user.type.value.name }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -157,8 +156,8 @@
|
||||
>
|
||||
{{ 'USER.PROFILE.PREFERREDLOGINNAME' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id] : null">
|
||||
<span *ngIf="user.human">{{ user.preferredLoginName }}</span>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<span *ngIf="user.type.case === 'human'">{{ user.preferredLoginName }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -171,8 +170,8 @@
|
||||
>
|
||||
{{ 'USER.PROFILE.USERNAME' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id] : null">
|
||||
{{ user.userName }}
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
{{ user.username }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -185,36 +184,36 @@
|
||||
>
|
||||
{{ 'USER.EMAIL' | translate }}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id] : null">
|
||||
<span *ngIf="user.human?.email?.email">{{ user.human?.email.email }}</span>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<span *ngIf="user.type?.value?.email?.email">{{ user.type.value.email.email }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="state">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'USER.DATA.STATE' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id] : null">
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<span
|
||||
class="state"
|
||||
[ngClass]="{
|
||||
active: user.state === UserState.USER_STATE_ACTIVE,
|
||||
inactive: user.state === UserState.USER_STATE_INACTIVE,
|
||||
active: user.state === UserState.ACTIVE,
|
||||
inactive: user.state === UserState.INACTIVE,
|
||||
}"
|
||||
>
|
||||
{{ 'USER.DATA.STATE' + user.state | translate }}
|
||||
{{ 'USER.STATEV2.' + user.state | translate }}
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="creationDate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'USER.TABLE.CREATIONDATE' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id] : null">
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<span class="no-break">{{ user.details.creationDate | timestampToDate | localizedDate: 'regular' }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="changeDate">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'USER.TABLE.CHANGEDATE' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id] : null">
|
||||
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
|
||||
<span class="no-break">{{ user.details.changeDate | timestampToDate | localizedDate: 'regular' }}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -242,11 +241,11 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="type === Type.TYPE_HUMAN ? displayedColumnsHuman : displayedColumnsMachine"></tr>
|
||||
<tr mat-header-row *matHeaderRowDef="type === Type.HUMAN ? displayedColumnsHuman : displayedColumnsMachine"></tr>
|
||||
<tr
|
||||
class="highlight pointer"
|
||||
mat-row
|
||||
*matRowDef="let user; columns: type === Type.TYPE_HUMAN ? displayedColumnsHuman : displayedColumnsMachine"
|
||||
*matRowDef="let user; columns: type === Type.HUMAN ? displayedColumnsHuman : displayedColumnsMachine"
|
||||
></tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -258,7 +257,6 @@
|
||||
<cnsl-paginator
|
||||
#paginator
|
||||
class="paginator"
|
||||
[timestamp]="viewTimestamp"
|
||||
[length]="totalResult || 0"
|
||||
[pageSize]="INITIAL_PAGE_SIZE"
|
||||
[timestamp]="viewTimestamp"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnInit, Output, Signal, ViewChild } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSort, Sort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
@@ -12,11 +12,15 @@ import { enterAnimations } from 'src/app/animations';
|
||||
import { ActionKeysType } from 'src/app/modules/action-keys/action-keys.component';
|
||||
import { PageEvent, PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
import { Timestamp } from 'src/app/proto/generated/google/protobuf/timestamp_pb';
|
||||
import { SearchQuery, Type, TypeQuery, User, UserFieldName, UserState } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { UserService } from 'src/app/services/user.service';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { User } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { SearchQuery, SearchQuerySchema, Type, UserFieldName } from '@zitadel/proto/zitadel/user/v2/query_pb';
|
||||
import { UserState, User as UserV2 } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { create } from '@bufbuild/protobuf';
|
||||
import { Timestamp } from '@bufbuild/protobuf/wkt';
|
||||
|
||||
enum UserListSearchKey {
|
||||
FIRST_NAME,
|
||||
@@ -34,20 +38,22 @@ enum UserListSearchKey {
|
||||
})
|
||||
export class UserTableComponent implements OnInit {
|
||||
public userSearchKey: UserListSearchKey | undefined = undefined;
|
||||
public Type: any = Type;
|
||||
@Input() public type: Type = Type.TYPE_HUMAN;
|
||||
public Type = Type;
|
||||
@Input() public type: Type = Type.HUMAN;
|
||||
@Input() refreshOnPreviousRoutes: string[] = [];
|
||||
@Input() public canWrite$: Observable<boolean> = of(false);
|
||||
@Input() public canDelete$: Observable<boolean> = of(false);
|
||||
|
||||
private user: Signal<User.AsObject | undefined> = toSignal(this.authService.user, { requireSync: true });
|
||||
|
||||
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
|
||||
@ViewChild(MatSort) public sort!: MatSort;
|
||||
public INITIAL_PAGE_SIZE: number = 20;
|
||||
|
||||
public viewTimestamp!: Timestamp.AsObject;
|
||||
public viewTimestamp!: Timestamp;
|
||||
public totalResult: number = 0;
|
||||
public dataSource: MatTableDataSource<User.AsObject> = new MatTableDataSource<User.AsObject>();
|
||||
public selection: SelectionModel<User.AsObject> = new SelectionModel<User.AsObject>(true, []);
|
||||
public dataSource: MatTableDataSource<UserV2> = new MatTableDataSource<UserV2>();
|
||||
public selection: SelectionModel<UserV2> = new SelectionModel<UserV2>(true, []);
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
@Input() public displayedColumnsHuman: string[] = [
|
||||
@@ -70,7 +76,7 @@ export class UserTableComponent implements OnInit {
|
||||
'actions',
|
||||
];
|
||||
|
||||
@Output() public changedSelection: EventEmitter<Array<User.AsObject>> = new EventEmitter();
|
||||
@Output() public changedSelection: EventEmitter<Array<UserV2>> = new EventEmitter();
|
||||
|
||||
public UserState: any = UserState;
|
||||
public UserListSearchKey: any = UserListSearchKey;
|
||||
@@ -83,7 +89,7 @@ export class UserTableComponent implements OnInit {
|
||||
private router: Router,
|
||||
public translate: TranslateService,
|
||||
private authService: GrpcAuthService,
|
||||
private userService: ManagementService,
|
||||
private userService: UserService,
|
||||
private toast: ToastService,
|
||||
private dialog: MatDialog,
|
||||
private route: ActivatedRoute,
|
||||
@@ -97,12 +103,12 @@ export class UserTableComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.route.queryParams.pipe(take(1)).subscribe((params) => {
|
||||
if (!params['filter']) {
|
||||
this.getData(this.INITIAL_PAGE_SIZE, 0, this.type, this.searchQueries);
|
||||
this.getData(this.INITIAL_PAGE_SIZE, 0, this.type, this.searchQueries).then();
|
||||
}
|
||||
|
||||
if (params['deferredReload']) {
|
||||
setTimeout(() => {
|
||||
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type);
|
||||
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type).then();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
@@ -110,16 +116,23 @@ export class UserTableComponent implements OnInit {
|
||||
|
||||
public setType(type: Type): void {
|
||||
this.type = type;
|
||||
this.router.navigate([], {
|
||||
this.router
|
||||
.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: {
|
||||
type: type === Type.TYPE_HUMAN ? 'human' : type === Type.TYPE_MACHINE ? 'machine' : 'human',
|
||||
type: type === Type.HUMAN ? 'human' : type === Type.MACHINE ? 'machine' : 'human',
|
||||
},
|
||||
replaceUrl: true,
|
||||
queryParamsHandling: 'merge',
|
||||
skipLocationChange: false,
|
||||
});
|
||||
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type, this.searchQueries);
|
||||
})
|
||||
.then();
|
||||
this.getData(
|
||||
this.paginator.pageSize,
|
||||
this.paginator.pageIndex * this.paginator.pageSize,
|
||||
this.type,
|
||||
this.searchQueries,
|
||||
).then();
|
||||
}
|
||||
|
||||
public isAllSelected(): boolean {
|
||||
@@ -134,17 +147,17 @@ export class UserTableComponent implements OnInit {
|
||||
|
||||
public changePage(event: PageEvent): void {
|
||||
this.selection.clear();
|
||||
this.getData(event.pageSize, event.pageIndex * event.pageSize, this.type, this.searchQueries);
|
||||
this.getData(event.pageSize, event.pageIndex * event.pageSize, this.type, this.searchQueries).then();
|
||||
}
|
||||
|
||||
public deactivateSelectedUsers(): void {
|
||||
Promise.all(
|
||||
this.selection.selected
|
||||
.filter((u) => u.state === UserState.USER_STATE_ACTIVE)
|
||||
const usersToDeactivate = this.selection.selected
|
||||
.filter((u) => u.state === UserState.ACTIVE)
|
||||
.map((value) => {
|
||||
return this.userService.deactivateUser(value.id);
|
||||
}),
|
||||
)
|
||||
return this.userService.deactivateUser(value.userId);
|
||||
});
|
||||
|
||||
Promise.all(usersToDeactivate)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true);
|
||||
this.selection.clear();
|
||||
@@ -158,13 +171,13 @@ export class UserTableComponent implements OnInit {
|
||||
}
|
||||
|
||||
public reactivateSelectedUsers(): void {
|
||||
Promise.all(
|
||||
this.selection.selected
|
||||
.filter((u) => u.state === UserState.USER_STATE_INACTIVE)
|
||||
const usersToReactivate = this.selection.selected
|
||||
.filter((u) => u.state === UserState.INACTIVE)
|
||||
.map((value) => {
|
||||
return this.userService.reactivateUser(value.id);
|
||||
}),
|
||||
)
|
||||
return this.userService.reactivateUser(value.userId);
|
||||
});
|
||||
|
||||
Promise.all(usersToReactivate)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.SELECTEDREACTIVATED', true);
|
||||
this.selection.clear();
|
||||
@@ -177,39 +190,43 @@ export class UserTableComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public gotoRouterLink(rL: any): void {
|
||||
this.router.navigate(rL);
|
||||
public gotoRouterLink(rL: any): Promise<boolean> {
|
||||
return this.router.navigate(rL);
|
||||
}
|
||||
|
||||
private async getData(limit: number, offset: number, type: Type, searchQueries?: SearchQuery[]): Promise<void> {
|
||||
this.loadingSubject.next(true);
|
||||
|
||||
let queryT = new SearchQuery();
|
||||
const typeQuery = new TypeQuery();
|
||||
typeQuery.setType(type);
|
||||
queryT.setTypeQuery(typeQuery);
|
||||
let queryT = create(SearchQuerySchema, {
|
||||
query: {
|
||||
case: 'typeQuery',
|
||||
value: {
|
||||
type,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let sortingField: UserFieldName | undefined = undefined;
|
||||
if (this.sort?.active && this.sort?.direction)
|
||||
switch (this.sort.active) {
|
||||
case 'displayName':
|
||||
sortingField = UserFieldName.USER_FIELD_NAME_DISPLAY_NAME;
|
||||
sortingField = UserFieldName.DISPLAY_NAME;
|
||||
break;
|
||||
case 'username':
|
||||
sortingField = UserFieldName.USER_FIELD_NAME_USER_NAME;
|
||||
sortingField = UserFieldName.USER_NAME;
|
||||
break;
|
||||
case 'preferredLoginName':
|
||||
// TODO: replace with preferred username sorting once implemented
|
||||
sortingField = UserFieldName.USER_FIELD_NAME_USER_NAME;
|
||||
sortingField = UserFieldName.USER_NAME;
|
||||
break;
|
||||
case 'email':
|
||||
sortingField = UserFieldName.USER_FIELD_NAME_EMAIL;
|
||||
sortingField = UserFieldName.EMAIL;
|
||||
break;
|
||||
case 'state':
|
||||
sortingField = UserFieldName.USER_FIELD_NAME_STATE;
|
||||
sortingField = UserFieldName.STATE;
|
||||
break;
|
||||
case 'creationDate':
|
||||
sortingField = UserFieldName.USER_FIELD_NAME_CREATION_DATE;
|
||||
sortingField = UserFieldName.CREATION_DATE;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -223,14 +240,14 @@ export class UserTableComponent implements OnInit {
|
||||
)
|
||||
.then((resp) => {
|
||||
if (resp.details?.totalResult) {
|
||||
this.totalResult = resp.details?.totalResult;
|
||||
this.totalResult = Number(resp.details.totalResult);
|
||||
} else {
|
||||
this.totalResult = 0;
|
||||
}
|
||||
if (resp.details?.viewTimestamp) {
|
||||
this.viewTimestamp = resp.details?.viewTimestamp;
|
||||
if (resp.details?.timestamp) {
|
||||
this.viewTimestamp = resp.details?.timestamp;
|
||||
}
|
||||
this.dataSource.data = resp.resultList;
|
||||
this.dataSource.data = resp.result;
|
||||
this.loadingSubject.next(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -240,15 +257,20 @@ export class UserTableComponent implements OnInit {
|
||||
}
|
||||
|
||||
public refreshPage(): void {
|
||||
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type, this.searchQueries);
|
||||
this.getData(
|
||||
this.paginator.pageSize,
|
||||
this.paginator.pageIndex * this.paginator.pageSize,
|
||||
this.type,
|
||||
this.searchQueries,
|
||||
).then();
|
||||
}
|
||||
|
||||
public sortChange(sortState: Sort) {
|
||||
if (sortState.direction && sortState.active) {
|
||||
this._liveAnnouncer.announce(`Sorted ${sortState.direction} ending`);
|
||||
this._liveAnnouncer.announce(`Sorted ${sortState.direction} ending`).then();
|
||||
this.refreshPage();
|
||||
} else {
|
||||
this._liveAnnouncer.announce('Sorting cleared');
|
||||
this._liveAnnouncer.announce('Sorting cleared').then();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,10 +282,10 @@ export class UserTableComponent implements OnInit {
|
||||
this.paginator ? this.paginator.pageIndex * this.paginator.pageSize : 0,
|
||||
this.type,
|
||||
searchQueries,
|
||||
);
|
||||
).then();
|
||||
}
|
||||
|
||||
public deleteUser(user: User.AsObject): void {
|
||||
public deleteUser(user: UserV2): void {
|
||||
const authUserData = {
|
||||
confirmKey: 'ACTIONS.DELETE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
@@ -286,9 +308,9 @@ export class UserTableComponent implements OnInit {
|
||||
confirmation: user.preferredLoginName,
|
||||
};
|
||||
|
||||
if (user && user.id) {
|
||||
const authUser = this.authService.userSubject.getValue();
|
||||
const isMe = authUser?.id === user.id;
|
||||
if (user?.userId) {
|
||||
const authUser = this.user();
|
||||
const isMe = authUser?.id === user.userId;
|
||||
|
||||
let dialogRef;
|
||||
|
||||
@@ -307,7 +329,7 @@ export class UserTableComponent implements OnInit {
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.userService
|
||||
.removeUser(user.id)
|
||||
.deleteUser(user.userId)
|
||||
.then(() => {
|
||||
setTimeout(() => {
|
||||
this.refreshPage();
|
||||
@@ -325,11 +347,11 @@ export class UserTableComponent implements OnInit {
|
||||
|
||||
public get multipleActivatePossible(): boolean {
|
||||
const selected = this.selection.selected;
|
||||
return selected ? selected.findIndex((user) => user.state !== UserState.USER_STATE_ACTIVE) > -1 : false;
|
||||
return selected ? selected.findIndex((user) => user.state !== UserState.ACTIVE) > -1 : false;
|
||||
}
|
||||
|
||||
public get multipleDeactivatePossible(): boolean {
|
||||
const selected = this.selection.selected;
|
||||
return selected ? selected.findIndex((user) => user.state !== UserState.USER_STATE_INACTIVE) > -1 : false;
|
||||
return selected ? selected.findIndex((user) => user.state !== UserState.INACTIVE) > -1 : false;
|
||||
}
|
||||
}
|
||||
|
@@ -1,18 +1,18 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
|
||||
import { Timestamp as ConnectTimestamp } from '@bufbuild/protobuf/wkt';
|
||||
import { Timestamp } from 'src/app/proto/generated/google/protobuf/timestamp_pb';
|
||||
|
||||
@Pipe({
|
||||
name: 'timestampToDate',
|
||||
})
|
||||
export class TimestampToDatePipe implements PipeTransform {
|
||||
transform(value: Timestamp.AsObject, ...args: unknown[]): unknown {
|
||||
transform(value: ConnectTimestamp | Timestamp.AsObject, ...args: unknown[]): unknown {
|
||||
return this.dateFromTimestamp(value);
|
||||
}
|
||||
|
||||
private dateFromTimestamp(date: Timestamp.AsObject): any {
|
||||
private dateFromTimestamp(date: ConnectTimestamp | Timestamp.AsObject): any {
|
||||
if (date?.seconds !== undefined && date?.nanos !== undefined) {
|
||||
const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000 / 1000);
|
||||
return ts;
|
||||
return new Date(Number(date.seconds) * 1000 + date.nanos / 1000 / 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { GrpcService } from './grpc.service';
|
||||
|
||||
import {
|
||||
GetInstanceFeaturesRequest,
|
||||
GetInstanceFeaturesResponse,
|
||||
ResetInstanceFeaturesRequest,
|
||||
SetInstanceFeaturesRequest,
|
||||
SetInstanceFeaturesResponse,
|
||||
} from '../proto/generated/zitadel/feature/v2beta/instance_pb';
|
||||
import {
|
||||
GetOrganizationFeaturesRequest,
|
||||
GetOrganizationFeaturesResponse,
|
||||
} from '../proto/generated/zitadel/feature/v2beta/organization_pb';
|
||||
import { GetUserFeaturesRequest, GetUserFeaturesResponse } from '../proto/generated/zitadel/feature/v2beta/user_pb';
|
||||
import { GetSystemFeaturesRequest, GetSystemFeaturesResponse } from '../proto/generated/zitadel/feature/v2beta/system_pb';
|
||||
import {
|
||||
GetInstanceFeaturesRequest,
|
||||
GetInstanceFeaturesResponse,
|
||||
ResetInstanceFeaturesRequest,
|
||||
SetInstanceFeaturesRequest,
|
||||
SetInstanceFeaturesResponse,
|
||||
} from '../proto/generated/zitadel/feature/v2/instance_pb';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
@@ -1,8 +1,22 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SortDirection } from '@angular/material/sort';
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
import { BehaviorSubject, forkJoin, from, Observable, of, Subject } from 'rxjs';
|
||||
import { catchError, distinctUntilChanged, filter, finalize, map, switchMap, timeout, withLatestFrom } from 'rxjs/operators';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatestWith,
|
||||
defer,
|
||||
distinctUntilKeyChanged,
|
||||
EMPTY,
|
||||
forkJoin,
|
||||
mergeWith,
|
||||
NEVER,
|
||||
Observable,
|
||||
of,
|
||||
shareReplay,
|
||||
Subject,
|
||||
TimeoutError,
|
||||
} from 'rxjs';
|
||||
import { catchError, distinctUntilChanged, filter, finalize, map, startWith, switchMap, tap, timeout } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
AddMyAuthFactorOTPEmailRequest,
|
||||
@@ -110,41 +124,17 @@ const ORG_LIMIT = 10;
|
||||
})
|
||||
export class GrpcAuthService {
|
||||
private _activeOrgChanged: Subject<Org.AsObject | undefined> = new Subject();
|
||||
public user!: Observable<User.AsObject | undefined>;
|
||||
public userSubject: BehaviorSubject<User.AsObject | undefined> = new BehaviorSubject<User.AsObject | undefined>(undefined);
|
||||
public user: Observable<User.AsObject | undefined>;
|
||||
private triggerPermissionsRefresh: Subject<void> = new Subject();
|
||||
public zitadelPermissions$: Observable<string[]> = this.triggerPermissionsRefresh.pipe(
|
||||
switchMap(() =>
|
||||
from(this.listMyZitadelPermissions()).pipe(
|
||||
map((rolesResp) => rolesResp.resultList),
|
||||
filter((roles) => !!roles.length),
|
||||
catchError((_) => {
|
||||
return of([]);
|
||||
}),
|
||||
distinctUntilChanged((a, b) => {
|
||||
return JSON.stringify(a.sort()) === JSON.stringify(b.sort());
|
||||
}),
|
||||
finalize(() => {
|
||||
this.fetchedZitadelPermissions.next(true);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
public zitadelPermissions: Observable<string[]>;
|
||||
|
||||
public labelpolicy$!: Observable<LabelPolicy.AsObject>;
|
||||
public labelpolicy: BehaviorSubject<LabelPolicy.AsObject | undefined> = new BehaviorSubject<
|
||||
LabelPolicy.AsObject | undefined
|
||||
>(undefined);
|
||||
labelPolicyLoading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||
|
||||
public privacypolicy$!: Observable<PrivacyPolicy.AsObject>;
|
||||
public privacypolicy$: Observable<PrivacyPolicy.AsObject>;
|
||||
public privacypolicy: BehaviorSubject<PrivacyPolicy.AsObject | undefined> = new BehaviorSubject<
|
||||
PrivacyPolicy.AsObject | undefined
|
||||
>(undefined);
|
||||
privacyPolicyLoading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||
|
||||
public zitadelPermissions: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
|
||||
public readonly fetchedZitadelPermissions: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
public cachedOrgs: BehaviorSubject<Org.AsObject[]> = new BehaviorSubject<Org.AsObject[]>([]);
|
||||
private cachedLabelPolicies: { [orgId: string]: LabelPolicy.AsObject } = {};
|
||||
@@ -155,74 +145,63 @@ export class GrpcAuthService {
|
||||
private oauthService: OAuthService,
|
||||
private storage: StorageService,
|
||||
) {
|
||||
this.zitadelPermissions$.subscribe(this.zitadelPermissions);
|
||||
|
||||
this.labelpolicy$ = this.activeOrgChanged.pipe(
|
||||
switchMap((org) => {
|
||||
this.labelPolicyLoading$.next(true);
|
||||
return from(this.getMyLabelPolicy(org ? org.id : ''));
|
||||
}),
|
||||
tap(() => this.labelPolicyLoading$.next(true)),
|
||||
switchMap((org) => this.getMyLabelPolicy(org ? org.id : '')),
|
||||
tap(() => this.labelPolicyLoading$.next(false)),
|
||||
finalize(() => this.labelPolicyLoading$.next(false)),
|
||||
filter((policy) => !!policy),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
this.labelpolicy$.subscribe({
|
||||
next: (policy) => {
|
||||
this.labelpolicy.next(policy);
|
||||
this.labelPolicyLoading$.next(false);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error(error);
|
||||
this.labelPolicyLoading$.next(false);
|
||||
},
|
||||
});
|
||||
|
||||
this.privacypolicy$ = this.activeOrgChanged.pipe(
|
||||
switchMap((org) => {
|
||||
this.privacyPolicyLoading$.next(true);
|
||||
return from(this.getMyPrivacyPolicy(org ? org.id : ''));
|
||||
}),
|
||||
switchMap((org) => this.getMyPrivacyPolicy(org ? org.id : '')),
|
||||
filter((policy) => !!policy),
|
||||
catchError((err) => {
|
||||
console.error(err);
|
||||
return EMPTY;
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
this.privacypolicy$.subscribe({
|
||||
next: (policy) => {
|
||||
this.privacypolicy.next(policy);
|
||||
this.privacyPolicyLoading$.next(false);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error(error);
|
||||
this.privacyPolicyLoading$.next(false);
|
||||
},
|
||||
});
|
||||
|
||||
this.user = forkJoin([
|
||||
of(this.oauthService.getAccessToken()),
|
||||
defer(() => of(this.oauthService.getAccessToken())),
|
||||
this.oauthService.events.pipe(
|
||||
filter((e) => e.type === 'token_received'),
|
||||
timeout(this.oauthService.waitForTokenInMsec || 0),
|
||||
catchError((_) => of(null)), // timeout is not an error
|
||||
timeout(this.oauthService.waitForTokenInMsec ?? 0),
|
||||
catchError((err) => {
|
||||
if (err instanceof TimeoutError) {
|
||||
return of(null);
|
||||
}
|
||||
throw err;
|
||||
}), // timeout is not an error
|
||||
map((_) => this.oauthService.getAccessToken()),
|
||||
),
|
||||
]).pipe(
|
||||
filter((token) => (token ? true : false)),
|
||||
distinctUntilChanged(),
|
||||
switchMap(() => {
|
||||
return from(
|
||||
this.getMyUser().then((resp) => {
|
||||
return resp.user;
|
||||
}),
|
||||
);
|
||||
}),
|
||||
finalize(() => {
|
||||
this.loadPermissions();
|
||||
}),
|
||||
filter(([_, token]) => !!token),
|
||||
distinctUntilKeyChanged(1),
|
||||
switchMap(() => this.getMyUser().then((resp) => resp.user)),
|
||||
startWith(undefined),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
this.user.subscribe(this.userSubject);
|
||||
|
||||
this.activeOrgChanged.subscribe(() => {
|
||||
this.loadPermissions();
|
||||
});
|
||||
this.zitadelPermissions = this.user.pipe(
|
||||
combineLatestWith(this.activeOrgChanged),
|
||||
// ignore errors from observables
|
||||
catchError(() => of(true)),
|
||||
// make sure observable never completes
|
||||
mergeWith(NEVER),
|
||||
switchMap(() =>
|
||||
this.listMyZitadelPermissions()
|
||||
.then((resp) => resp.resultList)
|
||||
.catch(() => <string[]>[]),
|
||||
),
|
||||
filter((roles) => !!roles.length),
|
||||
distinctUntilChanged((a, b) => {
|
||||
return JSON.stringify(a.sort()) === JSON.stringify(b.sort());
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
}
|
||||
|
||||
public listMyMetadata(
|
||||
@@ -309,7 +288,7 @@ export class GrpcAuthService {
|
||||
}
|
||||
|
||||
public get activeOrgChanged(): Observable<Org.AsObject | undefined> {
|
||||
return this._activeOrgChanged;
|
||||
return this._activeOrgChanged.asObservable();
|
||||
}
|
||||
|
||||
public setActiveOrg(org: Org.AsObject): void {
|
||||
@@ -328,18 +307,15 @@ export class GrpcAuthService {
|
||||
* @param roles roles of the user
|
||||
*/
|
||||
public isAllowed(roles: string[] | RegExp[], requiresAll: boolean = false): Observable<boolean> {
|
||||
if (roles && roles.length > 0) {
|
||||
return this.fetchedZitadelPermissions.pipe(
|
||||
withLatestFrom(this.zitadelPermissions),
|
||||
filter(([hL, p]) => {
|
||||
return hL === true && !!p.length;
|
||||
}),
|
||||
map(([_, zroles]) => this.hasRoles(zroles, roles, requiresAll)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
} else {
|
||||
if (!roles?.length) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
return this.zitadelPermissions.pipe(
|
||||
filter((permissions) => !!permissions.length),
|
||||
map((permissions) => this.hasRoles(permissions, roles, requiresAll)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -353,17 +329,14 @@ export class GrpcAuthService {
|
||||
mapper: (attr: any) => string[] | RegExp[],
|
||||
requiresAll: boolean = false,
|
||||
): Observable<T[]> {
|
||||
return this.fetchedZitadelPermissions.pipe(
|
||||
withLatestFrom(this.zitadelPermissions),
|
||||
filter(([hL, p]) => {
|
||||
return hL === true && !!p.length;
|
||||
}),
|
||||
map(([_, zroles]) => {
|
||||
return objects.filter((obj) => {
|
||||
return this.zitadelPermissions.pipe(
|
||||
filter((permissions) => !!permissions.length),
|
||||
map((permissions) =>
|
||||
objects.filter((obj) => {
|
||||
const roles = mapper(obj);
|
||||
return this.hasRoles(zroles, roles, requiresAll);
|
||||
});
|
||||
return this.hasRoles(permissions, roles, requiresAll);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -395,19 +368,6 @@ export class GrpcAuthService {
|
||||
.then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public loadMyUser(): void {
|
||||
from(this.getMyUser())
|
||||
.pipe(
|
||||
map((resp) => resp.user),
|
||||
catchError((_) => {
|
||||
return of(undefined);
|
||||
}),
|
||||
)
|
||||
.subscribe((user) => {
|
||||
this.userSubject.next(user);
|
||||
});
|
||||
}
|
||||
|
||||
public getMyUser(): Promise<GetMyUserResponse.AsObject> {
|
||||
return this.grpcService.auth.getMyUser(new GetMyUserRequest(), null).then((resp) => resp.toObject());
|
||||
}
|
||||
@@ -728,40 +688,39 @@ export class GrpcAuthService {
|
||||
public getMyLabelPolicy(orgIdForCache?: string): Promise<LabelPolicy.AsObject> {
|
||||
if (orgIdForCache && this.cachedLabelPolicies[orgIdForCache]) {
|
||||
return Promise.resolve(this.cachedLabelPolicies[orgIdForCache]);
|
||||
} else {
|
||||
}
|
||||
|
||||
return this.grpcService.auth
|
||||
.getMyLabelPolicy(new GetMyLabelPolicyRequest(), null)
|
||||
.then((resp) => resp.toObject())
|
||||
.then((resp) => {
|
||||
if (resp.policy) {
|
||||
if (!resp.policy) {
|
||||
return Promise.reject();
|
||||
}
|
||||
if (orgIdForCache) {
|
||||
this.cachedLabelPolicies[orgIdForCache] = resp.policy;
|
||||
}
|
||||
return Promise.resolve(resp.policy);
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
return resp.policy;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public getMyPrivacyPolicy(orgIdForCache?: string): Promise<PrivacyPolicy.AsObject> {
|
||||
if (orgIdForCache && this.cachedPrivacyPolicies[orgIdForCache]) {
|
||||
return Promise.resolve(this.cachedPrivacyPolicies[orgIdForCache]);
|
||||
} else {
|
||||
}
|
||||
|
||||
return this.grpcService.auth
|
||||
.getMyPrivacyPolicy(new GetMyPrivacyPolicyRequest(), null)
|
||||
.then((resp) => resp.toObject())
|
||||
.then((resp) => {
|
||||
if (resp.policy) {
|
||||
if (!resp.policy) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
if (orgIdForCache) {
|
||||
this.cachedPrivacyPolicies[orgIdForCache] = resp.policy;
|
||||
}
|
||||
return Promise.resolve(resp.policy);
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
return resp.policy;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import { PlatformLocation } from '@angular/common';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { AuthConfig } from 'angular-oauth2-oidc';
|
||||
import { catchError, switchMap, tap, throwError } from 'rxjs';
|
||||
import { catchError, firstValueFrom, switchMap, tap } from 'rxjs';
|
||||
|
||||
import { AdminServiceClient } from '../proto/generated/zitadel/AdminServiceClientPb';
|
||||
import { AuthServiceClient } from '../proto/generated/zitadel/AuthServiceClientPb';
|
||||
@@ -12,13 +11,18 @@ import { fallbackLanguage, supportedLanguagesRegexp } from '../utils/language';
|
||||
import { AuthenticationService } from './authentication.service';
|
||||
import { EnvironmentService } from './environment.service';
|
||||
import { ExhaustedService } from './exhausted.service';
|
||||
import { AuthInterceptor } from './interceptors/auth.interceptor';
|
||||
import { AuthInterceptor, AuthInterceptorProvider, NewConnectWebAuthInterceptor } from './interceptors/auth.interceptor';
|
||||
import { ExhaustedGrpcInterceptor } from './interceptors/exhausted.grpc.interceptor';
|
||||
import { I18nInterceptor } from './interceptors/i18n.interceptor';
|
||||
import { OrgInterceptor } from './interceptors/org.interceptor';
|
||||
import { StorageService } from './storage.service';
|
||||
import { FeatureServiceClient } from '../proto/generated/zitadel/feature/v2beta/Feature_serviceServiceClientPb';
|
||||
import { GrpcAuthService } from './grpc-auth.service';
|
||||
import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb';
|
||||
//@ts-ignore
|
||||
import { createUserServiceClient } from '@zitadel/client/v2';
|
||||
//@ts-ignore
|
||||
import { createAuthServiceClient, createManagementServiceClient } from '@zitadel/client/v1';
|
||||
import { createGrpcWebTransport } from '@connectrpc/connect-web';
|
||||
import { FeatureServiceClient } from '../proto/generated/zitadel/feature/v2/Feature_serviceServiceClientPb';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -28,15 +32,20 @@ export class GrpcService {
|
||||
public mgmt!: ManagementServiceClient;
|
||||
public admin!: AdminServiceClient;
|
||||
public feature!: FeatureServiceClient;
|
||||
public user!: UserServiceClient;
|
||||
public userNew!: ReturnType<typeof createUserServiceClient>;
|
||||
public mgmtNew!: ReturnType<typeof createManagementServiceClient>;
|
||||
public authNew!: ReturnType<typeof createAuthServiceClient>;
|
||||
|
||||
constructor(
|
||||
private envService: EnvironmentService,
|
||||
private platformLocation: PlatformLocation,
|
||||
private authenticationService: AuthenticationService,
|
||||
private storageService: StorageService,
|
||||
private dialog: MatDialog,
|
||||
private translate: TranslateService,
|
||||
private exhaustedService: ExhaustedService,
|
||||
private readonly envService: EnvironmentService,
|
||||
private readonly platformLocation: PlatformLocation,
|
||||
private readonly authenticationService: AuthenticationService,
|
||||
private readonly storageService: StorageService,
|
||||
private readonly translate: TranslateService,
|
||||
private readonly exhaustedService: ExhaustedService,
|
||||
private readonly authInterceptor: AuthInterceptor,
|
||||
private readonly authInterceptorProvider: AuthInterceptorProvider,
|
||||
) {}
|
||||
|
||||
public loadAppEnvironment(): Promise<any> {
|
||||
@@ -44,9 +53,7 @@ export class GrpcService {
|
||||
|
||||
const browserLanguage = this.translate.getBrowserLang();
|
||||
const language = browserLanguage?.match(supportedLanguagesRegexp) ? browserLanguage : fallbackLanguage;
|
||||
return this.translate
|
||||
.use(language || this.translate.defaultLang)
|
||||
.pipe(
|
||||
const init = this.translate.use(language || this.translate.defaultLang).pipe(
|
||||
switchMap(() => this.envService.env),
|
||||
tap((env) => {
|
||||
if (!env?.api || !env?.issuer) {
|
||||
@@ -56,7 +63,7 @@ export class GrpcService {
|
||||
unaryInterceptors: [
|
||||
new ExhaustedGrpcInterceptor(this.exhaustedService, this.envService),
|
||||
new OrgInterceptor(this.storageService),
|
||||
new AuthInterceptor(this.authenticationService, this.storageService, this.dialog),
|
||||
this.authInterceptor,
|
||||
new I18nInterceptor(this.translate),
|
||||
],
|
||||
};
|
||||
@@ -85,6 +92,20 @@ export class GrpcService {
|
||||
// @ts-ignore
|
||||
interceptors,
|
||||
);
|
||||
this.user = new UserServiceClient(
|
||||
env.api,
|
||||
null,
|
||||
// @ts-ignore
|
||||
interceptors,
|
||||
);
|
||||
|
||||
const transport = createGrpcWebTransport({
|
||||
baseUrl: env.api,
|
||||
interceptors: [NewConnectWebAuthInterceptor(this.authInterceptorProvider)],
|
||||
});
|
||||
this.userNew = createUserServiceClient(transport);
|
||||
this.mgmtNew = createManagementServiceClient(transport);
|
||||
this.authNew = createAuthServiceClient(transport);
|
||||
|
||||
const authConfig: AuthConfig = {
|
||||
scope: 'openid profile email',
|
||||
@@ -101,9 +122,10 @@ export class GrpcService {
|
||||
}),
|
||||
catchError((err) => {
|
||||
console.error('Failed to load environment from assets', err);
|
||||
return throwError(() => err);
|
||||
throw err;
|
||||
}),
|
||||
)
|
||||
.toPromise();
|
||||
);
|
||||
|
||||
return firstValueFrom(init);
|
||||
}
|
||||
}
|
||||
|
@@ -1,59 +1,53 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DestroyRef, Injectable } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Request, UnaryInterceptor, UnaryResponse } from 'grpc-web';
|
||||
import { Subject } from 'rxjs';
|
||||
import { debounceTime, filter, first, map, take, tap } from 'rxjs/operators';
|
||||
import { Request, RpcError, UnaryInterceptor, UnaryResponse } from 'grpc-web';
|
||||
import { firstValueFrom, identity, lastValueFrom, Observable, Subject } from 'rxjs';
|
||||
import { debounceTime, filter, map } from 'rxjs/operators';
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
|
||||
import { AuthenticationService } from '../authentication.service';
|
||||
import { StorageService } from '../storage.service';
|
||||
import { AuthConfig } from 'angular-oauth2-oidc';
|
||||
import { GrpcAuthService } from '../grpc-auth.service';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { ConnectError, Interceptor } from '@connectrpc/connect';
|
||||
|
||||
const authorizationKey = 'Authorization';
|
||||
const bearerPrefix = 'Bearer';
|
||||
const accessTokenStorageKey = 'access_token';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
/**
|
||||
* Set the authentication token
|
||||
*/
|
||||
export class AuthInterceptor<TReq = unknown, TResp = unknown> implements UnaryInterceptor<TReq, TResp> {
|
||||
export class AuthInterceptorProvider {
|
||||
public triggerDialog: Subject<boolean> = new Subject();
|
||||
|
||||
constructor(
|
||||
private authenticationService: AuthenticationService,
|
||||
private storageService: StorageService,
|
||||
private dialog: MatDialog,
|
||||
private destroyRef: DestroyRef,
|
||||
) {
|
||||
this.triggerDialog.pipe(debounceTime(1000)).subscribe(() => {
|
||||
this.openDialog();
|
||||
});
|
||||
this.triggerDialog.pipe(debounceTime(1000), takeUntilDestroyed(this.destroyRef)).subscribe(() => this.openDialog());
|
||||
}
|
||||
|
||||
public async intercept(request: Request<TReq, TResp>, invoker: any): Promise<UnaryResponse<TReq, TResp>> {
|
||||
await this.authenticationService.authenticationChanged
|
||||
.pipe(
|
||||
filter((authed) => !!authed),
|
||||
first(),
|
||||
)
|
||||
.toPromise();
|
||||
getToken(): Observable<string> {
|
||||
return this.authenticationService.authenticationChanged.pipe(
|
||||
filter(identity),
|
||||
map(() => this.storageService.getItem(accessTokenStorageKey)),
|
||||
map((token) => `${bearerPrefix} ${token}`),
|
||||
);
|
||||
}
|
||||
|
||||
const metadata = request.getMetadata();
|
||||
const accessToken = this.storageService.getItem(accessTokenStorageKey);
|
||||
metadata[authorizationKey] = `${bearerPrefix} ${accessToken}`;
|
||||
handleError = (error: any): never => {
|
||||
if (!(error instanceof RpcError) && !(error instanceof ConnectError)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return invoker(request)
|
||||
.then((response: any) => {
|
||||
return response;
|
||||
})
|
||||
.catch(async (error: any) => {
|
||||
if (error.code === 16 || (error.code === 7 && error.message === 'mfa required (AUTHZ-Kl3p0)')) {
|
||||
this.triggerDialog.next(true);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
};
|
||||
|
||||
private openDialog(): void {
|
||||
private async openDialog(): Promise<void> {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.LOGIN',
|
||||
@@ -64,19 +58,47 @@ export class AuthInterceptor<TReq = unknown, TResp = unknown> implements UnaryIn
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(take(1))
|
||||
.subscribe((resp) => {
|
||||
if (resp) {
|
||||
const resp = await lastValueFrom(dialogRef.afterClosed());
|
||||
if (!resp) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idToken = this.authenticationService.getIdToken();
|
||||
const configWithPrompt: Partial<AuthConfig> = {
|
||||
customQueryParams: {
|
||||
id_token_hint: idToken,
|
||||
},
|
||||
};
|
||||
this.authenticationService.authenticate(configWithPrompt, true);
|
||||
}
|
||||
});
|
||||
|
||||
await this.authenticationService.authenticate(configWithPrompt, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
/**
|
||||
* Set the authentication token
|
||||
*/
|
||||
export class AuthInterceptor<TReq = unknown, TResp = unknown> implements UnaryInterceptor<TReq, TResp> {
|
||||
constructor(private readonly authInterceptorProvider: AuthInterceptorProvider) {}
|
||||
|
||||
public async intercept(
|
||||
request: Request<TReq, TResp>,
|
||||
invoker: (request: Request<TReq, TResp>) => Promise<UnaryResponse<TReq, TResp>>,
|
||||
): Promise<UnaryResponse<TReq, TResp>> {
|
||||
const metadata = request.getMetadata();
|
||||
metadata[authorizationKey] = await firstValueFrom(this.authInterceptorProvider.getToken());
|
||||
|
||||
return invoker(request).catch(this.authInterceptorProvider.handleError);
|
||||
}
|
||||
}
|
||||
|
||||
export function NewConnectWebAuthInterceptor(authInterceptorProvider: AuthInterceptorProvider): Interceptor {
|
||||
return (next) => async (req) => {
|
||||
if (!req.header.get('Authorization')) {
|
||||
const token = await firstValueFrom(authInterceptorProvider.getToken());
|
||||
req.header.set('Authorization', token);
|
||||
}
|
||||
|
||||
return next(req).catch(authInterceptorProvider.handleError);
|
||||
};
|
||||
}
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Request, StatusCode, UnaryInterceptor, UnaryResponse } from 'grpc-web';
|
||||
import { Request, RpcError, StatusCode, UnaryInterceptor, UnaryResponse } from 'grpc-web';
|
||||
import { EnvironmentService } from '../environment.service';
|
||||
import { ExhaustedService } from '../exhausted.service';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
/**
|
||||
* ExhaustedGrpcInterceptor shows the exhausted dialog after receiving a gRPC response status 8.
|
||||
@@ -17,16 +18,13 @@ export class ExhaustedGrpcInterceptor<TReq = unknown, TResp = unknown> implement
|
||||
request: Request<TReq, TResp>,
|
||||
invoker: (request: Request<TReq, TResp>) => Promise<UnaryResponse<TReq, TResp>>,
|
||||
): Promise<UnaryResponse<TReq, TResp>> {
|
||||
return invoker(request).catch((error: any) => {
|
||||
if (error.code === StatusCode.RESOURCE_EXHAUSTED) {
|
||||
return this.exhaustedSvc
|
||||
.showExhaustedDialog(this.envSvc.env)
|
||||
.toPromise()
|
||||
.then(() => {
|
||||
throw error;
|
||||
});
|
||||
try {
|
||||
return await invoker(request);
|
||||
} catch (error: any) {
|
||||
if (error instanceof RpcError && error.code === StatusCode.RESOURCE_EXHAUSTED) {
|
||||
await lastValueFrom(this.exhaustedSvc.showExhaustedDialog(this.envSvc.env));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ const i18nHeader = 'Accept-Language';
|
||||
export class I18nInterceptor<TReq = unknown, TResp = unknown> implements UnaryInterceptor<TReq, TResp> {
|
||||
constructor(private translate: TranslateService) {}
|
||||
|
||||
public async intercept(request: Request<TReq, TResp>, invoker: any): Promise<UnaryResponse<TReq, TResp>> {
|
||||
public intercept(request: Request<TReq, TResp>, invoker: any): Promise<UnaryResponse<TReq, TResp>> {
|
||||
const metadata = request.getMetadata();
|
||||
|
||||
const navLang = this.translate.currentLang ?? navigator.language;
|
||||
@@ -18,12 +18,6 @@ export class I18nInterceptor<TReq = unknown, TResp = unknown> implements UnaryIn
|
||||
metadata[i18nHeader] = navLang;
|
||||
}
|
||||
|
||||
return invoker(request)
|
||||
.then((response: any) => {
|
||||
return response;
|
||||
})
|
||||
.catch((error: any) => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
return invoker(request);
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Request, UnaryInterceptor, UnaryResponse } from 'grpc-web';
|
||||
import { Request, RpcError, StatusCode, UnaryInterceptor, UnaryResponse } from 'grpc-web';
|
||||
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
|
||||
import { StorageKey, StorageLocation, StorageService } from '../storage.service';
|
||||
@@ -9,7 +9,7 @@ const ORG_HEADER_KEY = 'x-zitadel-orgid';
|
||||
export class OrgInterceptor<TReq = unknown, TResp = unknown> implements UnaryInterceptor<TReq, TResp> {
|
||||
constructor(private readonly storageService: StorageService) {}
|
||||
|
||||
public intercept(request: Request<TReq, TResp>, invoker: any): Promise<UnaryResponse<TReq, TResp>> {
|
||||
public async intercept(request: Request<TReq, TResp>, invoker: any): Promise<UnaryResponse<TReq, TResp>> {
|
||||
const metadata = request.getMetadata();
|
||||
|
||||
const org: Org.AsObject | null = this.storageService.getItem(StorageKey.organization, StorageLocation.session);
|
||||
@@ -18,15 +18,17 @@ export class OrgInterceptor<TReq = unknown, TResp = unknown> implements UnaryInt
|
||||
metadata[ORG_HEADER_KEY] = `${org.id}`;
|
||||
}
|
||||
|
||||
return invoker(request)
|
||||
.then((response: any) => {
|
||||
return response;
|
||||
})
|
||||
.catch((error: any) => {
|
||||
if (error.code === 7 && error.message.startsWith("Organisation doesn't exist")) {
|
||||
try {
|
||||
return await invoker(request);
|
||||
} catch (error: any) {
|
||||
if (
|
||||
error instanceof RpcError &&
|
||||
error.code === StatusCode.PERMISSION_DENIED &&
|
||||
error.message.startsWith("Organisation doesn't exist")
|
||||
) {
|
||||
this.storageService.removeItem(StorageKey.organization, StorageLocation.session);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
30
console/src/app/services/new-auth.service.ts
Normal file
30
console/src/app/services/new-auth.service.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { GrpcService } from './grpc.service';
|
||||
import { create } from '@bufbuild/protobuf';
|
||||
import {
|
||||
AddMyAuthFactorOTPSMSRequestSchema,
|
||||
AddMyAuthFactorOTPSMSResponse,
|
||||
GetMyUserRequestSchema,
|
||||
GetMyUserResponse,
|
||||
VerifyMyPhoneRequestSchema,
|
||||
VerifyMyPhoneResponse,
|
||||
} from '@zitadel/proto/zitadel/auth_pb';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class NewAuthService {
|
||||
constructor(private readonly grpcService: GrpcService) {}
|
||||
|
||||
public getMyUser(): Promise<GetMyUserResponse> {
|
||||
return this.grpcService.authNew.getMyUser(create(GetMyUserRequestSchema));
|
||||
}
|
||||
|
||||
public verifyMyPhone(code: string): Promise<VerifyMyPhoneResponse> {
|
||||
return this.grpcService.authNew.verifyMyPhone(create(VerifyMyPhoneRequestSchema, { code }));
|
||||
}
|
||||
|
||||
public addMyAuthFactorOTPSMS(): Promise<AddMyAuthFactorOTPSMSResponse> {
|
||||
return this.grpcService.authNew.addMyAuthFactorOTPSMS(create(AddMyAuthFactorOTPSMSRequestSchema));
|
||||
}
|
||||
}
|
92
console/src/app/services/new-mgmt.service.ts
Normal file
92
console/src/app/services/new-mgmt.service.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { GrpcService } from './grpc.service';
|
||||
import {
|
||||
GenerateMachineSecretRequestSchema,
|
||||
GenerateMachineSecretResponse,
|
||||
GetLoginPolicyRequestSchema,
|
||||
GetLoginPolicyResponse,
|
||||
ListUserMetadataRequestSchema,
|
||||
ListUserMetadataResponse,
|
||||
RemoveMachineSecretRequestSchema,
|
||||
RemoveMachineSecretResponse,
|
||||
RemoveUserMetadataRequestSchema,
|
||||
RemoveUserMetadataResponse,
|
||||
ResendHumanEmailVerificationRequestSchema,
|
||||
ResendHumanEmailVerificationResponse,
|
||||
ResendHumanInitializationRequestSchema,
|
||||
ResendHumanInitializationResponse,
|
||||
ResendHumanPhoneVerificationRequestSchema,
|
||||
ResendHumanPhoneVerificationResponse,
|
||||
SendHumanResetPasswordNotificationRequest_Type,
|
||||
SendHumanResetPasswordNotificationRequestSchema,
|
||||
SendHumanResetPasswordNotificationResponse,
|
||||
SetUserMetadataRequestSchema,
|
||||
SetUserMetadataResponse,
|
||||
UpdateMachineRequestSchema,
|
||||
UpdateMachineResponse,
|
||||
} from '@zitadel/proto/zitadel/management_pb';
|
||||
import { MessageInitShape, create } from '@bufbuild/protobuf';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class NewMgmtService {
|
||||
constructor(private readonly grpcService: GrpcService) {}
|
||||
|
||||
public getLoginPolicy(): Promise<GetLoginPolicyResponse> {
|
||||
return this.grpcService.mgmtNew.getLoginPolicy(create(GetLoginPolicyRequestSchema));
|
||||
}
|
||||
|
||||
public generateMachineSecret(userId: string): Promise<GenerateMachineSecretResponse> {
|
||||
return this.grpcService.mgmtNew.generateMachineSecret(create(GenerateMachineSecretRequestSchema, { userId }));
|
||||
}
|
||||
|
||||
public removeMachineSecret(userId: string): Promise<RemoveMachineSecretResponse> {
|
||||
return this.grpcService.mgmtNew.removeMachineSecret(create(RemoveMachineSecretRequestSchema, { userId }));
|
||||
}
|
||||
|
||||
public updateMachine(req: MessageInitShape<typeof UpdateMachineRequestSchema>): Promise<UpdateMachineResponse> {
|
||||
return this.grpcService.mgmtNew.updateMachine(create(UpdateMachineRequestSchema, req));
|
||||
}
|
||||
|
||||
public resendHumanEmailVerification(userId: string): Promise<ResendHumanEmailVerificationResponse> {
|
||||
return this.grpcService.mgmtNew.resendHumanEmailVerification(
|
||||
create(ResendHumanEmailVerificationRequestSchema, { userId }),
|
||||
);
|
||||
}
|
||||
|
||||
public resendHumanPhoneVerification(userId: string): Promise<ResendHumanPhoneVerificationResponse> {
|
||||
return this.grpcService.mgmtNew.resendHumanPhoneVerification(
|
||||
create(ResendHumanPhoneVerificationRequestSchema, { userId }),
|
||||
);
|
||||
}
|
||||
|
||||
public sendHumanResetPasswordNotification(
|
||||
userId: string,
|
||||
type: SendHumanResetPasswordNotificationRequest_Type,
|
||||
): Promise<SendHumanResetPasswordNotificationResponse> {
|
||||
return this.grpcService.mgmtNew.sendHumanResetPasswordNotification(
|
||||
create(SendHumanResetPasswordNotificationRequestSchema, { userId, type }),
|
||||
);
|
||||
}
|
||||
|
||||
public resendHumanInitialization(userId: string, email: string = ''): Promise<ResendHumanInitializationResponse> {
|
||||
return this.grpcService.mgmtNew.resendHumanInitialization(
|
||||
create(ResendHumanInitializationRequestSchema, { userId, email }),
|
||||
);
|
||||
}
|
||||
|
||||
public listUserMetadata(id: string): Promise<ListUserMetadataResponse> {
|
||||
return this.grpcService.mgmtNew.listUserMetadata(create(ListUserMetadataRequestSchema, { id }));
|
||||
}
|
||||
|
||||
public setUserMetadata(req: MessageInitShape<typeof SetUserMetadataRequestSchema>): Promise<SetUserMetadataResponse> {
|
||||
return this.grpcService.mgmtNew.setUserMetadata(create(SetUserMetadataRequestSchema, req));
|
||||
}
|
||||
|
||||
public removeUserMetadata(
|
||||
req: MessageInitShape<typeof RemoveUserMetadataRequestSchema>,
|
||||
): Promise<RemoveUserMetadataResponse> {
|
||||
return this.grpcService.mgmtNew.removeUserMetadata(create(RemoveUserMetadataRequestSchema, req));
|
||||
}
|
||||
}
|
302
console/src/app/services/user.service.ts
Normal file
302
console/src/app/services/user.service.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { GrpcService } from './grpc.service';
|
||||
import {
|
||||
AddHumanUserRequestSchema,
|
||||
AddHumanUserResponse,
|
||||
CreateInviteCodeRequestSchema,
|
||||
CreateInviteCodeResponse,
|
||||
CreatePasskeyRegistrationLinkRequestSchema,
|
||||
CreatePasskeyRegistrationLinkResponse,
|
||||
DeactivateUserRequestSchema,
|
||||
DeactivateUserResponse,
|
||||
DeleteUserRequestSchema,
|
||||
DeleteUserResponse,
|
||||
GetUserByIDRequestSchema,
|
||||
GetUserByIDResponse,
|
||||
ListAuthenticationFactorsRequestSchema,
|
||||
ListAuthenticationFactorsResponse,
|
||||
ListPasskeysRequestSchema,
|
||||
ListPasskeysResponse,
|
||||
ListUsersRequestSchema,
|
||||
ListUsersResponse,
|
||||
LockUserRequestSchema,
|
||||
LockUserResponse,
|
||||
PasswordResetRequestSchema,
|
||||
ReactivateUserRequestSchema,
|
||||
ReactivateUserResponse,
|
||||
RemoveOTPEmailRequestSchema,
|
||||
RemoveOTPEmailResponse,
|
||||
RemoveOTPSMSRequestSchema,
|
||||
RemoveOTPSMSResponse,
|
||||
RemovePasskeyRequestSchema,
|
||||
RemovePasskeyResponse,
|
||||
RemovePhoneRequestSchema,
|
||||
RemovePhoneResponse,
|
||||
RemoveTOTPRequestSchema,
|
||||
RemoveTOTPResponse,
|
||||
RemoveU2FRequestSchema,
|
||||
RemoveU2FResponse,
|
||||
ResendInviteCodeRequestSchema,
|
||||
ResendInviteCodeResponse,
|
||||
SetEmailRequestSchema,
|
||||
SetEmailResponse,
|
||||
SetPasswordRequestSchema,
|
||||
SetPasswordResponse,
|
||||
SetPhoneRequestSchema,
|
||||
SetPhoneResponse,
|
||||
UnlockUserRequestSchema,
|
||||
UnlockUserResponse,
|
||||
UpdateHumanUserRequestSchema,
|
||||
UpdateHumanUserResponse,
|
||||
} from '@zitadel/proto/zitadel/user/v2/user_service_pb';
|
||||
import type { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import {
|
||||
AccessTokenType,
|
||||
Gender,
|
||||
HumanProfile,
|
||||
HumanProfileSchema,
|
||||
HumanUser,
|
||||
HumanUserSchema,
|
||||
MachineUser,
|
||||
MachineUserSchema,
|
||||
User as UserV2,
|
||||
UserSchema,
|
||||
UserState,
|
||||
} from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
import { create } from '@bufbuild/protobuf';
|
||||
import { Timestamp as TimestampV2, TimestampSchema } from '@bufbuild/protobuf/wkt';
|
||||
import { Details, DetailsSchema, ListQuerySchema } from '@zitadel/proto/zitadel/object/v2/object_pb';
|
||||
import { SearchQuery, UserFieldName } from '@zitadel/proto/zitadel/user/v2/query_pb';
|
||||
import { SortDirection } from '@angular/material/sort';
|
||||
import { Human, Machine, Phone, Profile, User } from '../proto/generated/zitadel/user_pb';
|
||||
import { ObjectDetails } from '../proto/generated/zitadel/object_pb';
|
||||
import { Timestamp } from '../proto/generated/google/protobuf/timestamp_pb';
|
||||
import { HumanPhone, HumanPhoneSchema } from '@zitadel/proto/zitadel/user/v2/phone_pb';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UserService {
|
||||
constructor(private readonly grpcService: GrpcService) {}
|
||||
|
||||
public addHumanUser(req: MessageInitShape<typeof AddHumanUserRequestSchema>): Promise<AddHumanUserResponse> {
|
||||
return this.grpcService.userNew.addHumanUser(create(AddHumanUserRequestSchema, req));
|
||||
}
|
||||
|
||||
public listUsers(
|
||||
limit: number,
|
||||
offset: number,
|
||||
queriesList?: SearchQuery[],
|
||||
sortingColumn?: UserFieldName,
|
||||
sortingDirection?: SortDirection,
|
||||
): Promise<ListUsersResponse> {
|
||||
const query = create(ListQuerySchema);
|
||||
|
||||
if (limit) {
|
||||
query.limit = limit;
|
||||
}
|
||||
if (offset) {
|
||||
query.offset = BigInt(offset);
|
||||
}
|
||||
if (sortingDirection) {
|
||||
query.asc = sortingDirection === 'asc';
|
||||
}
|
||||
|
||||
const req = create(ListUsersRequestSchema, {
|
||||
query,
|
||||
});
|
||||
|
||||
if (sortingColumn) {
|
||||
req.sortingColumn = sortingColumn;
|
||||
}
|
||||
|
||||
if (queriesList) {
|
||||
req.queries = queriesList;
|
||||
}
|
||||
|
||||
return this.grpcService.userNew.listUsers(req);
|
||||
}
|
||||
|
||||
public getUserById(userId: string): Promise<GetUserByIDResponse> {
|
||||
return this.grpcService.userNew.getUserByID(create(GetUserByIDRequestSchema, { userId }));
|
||||
}
|
||||
|
||||
public deactivateUser(userId: string): Promise<DeactivateUserResponse> {
|
||||
return this.grpcService.userNew.deactivateUser(create(DeactivateUserRequestSchema, { userId }));
|
||||
}
|
||||
|
||||
public reactivateUser(userId: string): Promise<ReactivateUserResponse> {
|
||||
return this.grpcService.userNew.reactivateUser(create(ReactivateUserRequestSchema, { userId }));
|
||||
}
|
||||
|
||||
public deleteUser(userId: string): Promise<DeleteUserResponse> {
|
||||
return this.grpcService.userNew.deleteUser(create(DeleteUserRequestSchema, { userId }));
|
||||
}
|
||||
|
||||
public updateUser(req: MessageInitShape<typeof UpdateHumanUserRequestSchema>): Promise<UpdateHumanUserResponse> {
|
||||
return this.grpcService.userNew.updateHumanUser(create(UpdateHumanUserRequestSchema, req));
|
||||
}
|
||||
|
||||
public lockUser(userId: string): Promise<LockUserResponse> {
|
||||
return this.grpcService.userNew.lockUser(create(LockUserRequestSchema, { userId }));
|
||||
}
|
||||
|
||||
public unlockUser(userId: string): Promise<UnlockUserResponse> {
|
||||
return this.grpcService.userNew.unlockUser(create(UnlockUserRequestSchema, { userId }));
|
||||
}
|
||||
|
||||
public listAuthenticationFactors(
|
||||
req: MessageInitShape<typeof ListAuthenticationFactorsRequestSchema>,
|
||||
): Promise<ListAuthenticationFactorsResponse> {
|
||||
return this.grpcService.userNew.listAuthenticationFactors(create(ListAuthenticationFactorsRequestSchema, req));
|
||||
}
|
||||
|
||||
public listPasskeys(req: MessageInitShape<typeof ListPasskeysRequestSchema>): Promise<ListPasskeysResponse> {
|
||||
return this.grpcService.userNew.listPasskeys(create(ListPasskeysRequestSchema, req));
|
||||
}
|
||||
|
||||
public removePasskeys(req: MessageInitShape<typeof RemovePasskeyRequestSchema>): Promise<RemovePasskeyResponse> {
|
||||
return this.grpcService.userNew.removePasskey(create(RemovePasskeyRequestSchema, req));
|
||||
}
|
||||
|
||||
public createPasskeyRegistrationLink(
|
||||
req: MessageInitShape<typeof CreatePasskeyRegistrationLinkRequestSchema>,
|
||||
): Promise<CreatePasskeyRegistrationLinkResponse> {
|
||||
return this.grpcService.userNew.createPasskeyRegistrationLink(create(CreatePasskeyRegistrationLinkRequestSchema, req));
|
||||
}
|
||||
|
||||
public removePhone(userId: string): Promise<RemovePhoneResponse> {
|
||||
return this.grpcService.userNew.removePhone(create(RemovePhoneRequestSchema, { userId }));
|
||||
}
|
||||
|
||||
public setPhone(req: MessageInitShape<typeof SetPhoneRequestSchema>): Promise<SetPhoneResponse> {
|
||||
return this.grpcService.userNew.setPhone(create(SetPhoneRequestSchema, req));
|
||||
}
|
||||
|
||||
public setEmail(req: MessageInitShape<typeof SetEmailRequestSchema>): Promise<SetEmailResponse> {
|
||||
return this.grpcService.userNew.setEmail(create(SetEmailRequestSchema, req));
|
||||
}
|
||||
|
||||
public removeTOTP(userId: string): Promise<RemoveTOTPResponse> {
|
||||
return this.grpcService.userNew.removeTOTP(create(RemoveTOTPRequestSchema, { userId }));
|
||||
}
|
||||
|
||||
public removeU2F(userId: string, u2fId: string): Promise<RemoveU2FResponse> {
|
||||
return this.grpcService.userNew.removeU2F(create(RemoveU2FRequestSchema, { userId, u2fId }));
|
||||
}
|
||||
|
||||
public removeOTPSMS(userId: string): Promise<RemoveOTPSMSResponse> {
|
||||
return this.grpcService.userNew.removeOTPSMS(create(RemoveOTPSMSRequestSchema, { userId }));
|
||||
}
|
||||
|
||||
public removeOTPEmail(userId: string): Promise<RemoveOTPEmailResponse> {
|
||||
return this.grpcService.userNew.removeOTPEmail(create(RemoveOTPEmailRequestSchema, { userId }));
|
||||
}
|
||||
|
||||
public resendInviteCode(userId: string): Promise<ResendInviteCodeResponse> {
|
||||
return this.grpcService.userNew.resendInviteCode(create(ResendInviteCodeRequestSchema, { userId }));
|
||||
}
|
||||
|
||||
public createInviteCode(req: MessageInitShape<typeof CreateInviteCodeRequestSchema>): Promise<CreateInviteCodeResponse> {
|
||||
return this.grpcService.userNew.createInviteCode(create(CreateInviteCodeRequestSchema, req));
|
||||
}
|
||||
|
||||
public passwordReset(req: MessageInitShape<typeof PasswordResetRequestSchema>) {
|
||||
return this.grpcService.userNew.passwordReset(create(PasswordResetRequestSchema, req));
|
||||
}
|
||||
|
||||
public setPassword(req: MessageInitShape<typeof SetPasswordRequestSchema>): Promise<SetPasswordResponse> {
|
||||
return this.grpcService.userNew.setPassword(create(SetPasswordRequestSchema, req));
|
||||
}
|
||||
}
|
||||
|
||||
function userToV2(user: User): UserV2 {
|
||||
const details = user.getDetails();
|
||||
return create(UserSchema, {
|
||||
userId: user.getId(),
|
||||
details: details && detailsToV2(details),
|
||||
state: user.getState() as number as UserState,
|
||||
username: user.getUserName(),
|
||||
loginNames: user.getLoginNamesList(),
|
||||
preferredLoginName: user.getPreferredLoginName(),
|
||||
type: typeToV2(user),
|
||||
});
|
||||
}
|
||||
|
||||
function detailsToV2(details: ObjectDetails): Details {
|
||||
const changeDate = details.getChangeDate();
|
||||
return create(DetailsSchema, {
|
||||
sequence: BigInt(details.getSequence()),
|
||||
changeDate: changeDate && timestampToV2(changeDate),
|
||||
resourceOwner: details.getResourceOwner(),
|
||||
});
|
||||
}
|
||||
|
||||
function timestampToV2(timestamp: Timestamp): TimestampV2 {
|
||||
return create(TimestampSchema, {
|
||||
seconds: BigInt(timestamp.getSeconds()),
|
||||
nanos: timestamp.getNanos(),
|
||||
});
|
||||
}
|
||||
|
||||
function typeToV2(user: User): UserV2['type'] {
|
||||
const human = user.getHuman();
|
||||
if (human) {
|
||||
return { case: 'human', value: humanToV2(user, human) };
|
||||
}
|
||||
|
||||
const machine = user.getMachine();
|
||||
if (machine) {
|
||||
return { case: 'machine', value: machineToV2(machine) };
|
||||
}
|
||||
|
||||
return { case: undefined };
|
||||
}
|
||||
|
||||
function humanToV2(user: User, human: Human): HumanUser {
|
||||
const profile = human.getProfile();
|
||||
const email = human.getEmail()?.getEmail();
|
||||
const phone = human.getPhone();
|
||||
const passwordChanged = human.getPasswordChanged();
|
||||
|
||||
return create(HumanUserSchema, {
|
||||
userId: user.getId(),
|
||||
state: user.getState() as number as UserState,
|
||||
username: user.getUserName(),
|
||||
loginNames: user.getLoginNamesList(),
|
||||
preferredLoginName: user.getPreferredLoginName(),
|
||||
profile: profile && humanProfileToV2(profile),
|
||||
email: { email },
|
||||
phone: phone && humanPhoneToV2(phone),
|
||||
passwordChangeRequired: false,
|
||||
passwordChanged: passwordChanged && timestampToV2(passwordChanged),
|
||||
});
|
||||
}
|
||||
|
||||
function humanProfileToV2(profile: Profile): HumanProfile {
|
||||
return create(HumanProfileSchema, {
|
||||
givenName: profile.getFirstName(),
|
||||
familyName: profile.getLastName(),
|
||||
nickName: profile.getNickName(),
|
||||
displayName: profile.getDisplayName(),
|
||||
preferredLanguage: profile.getPreferredLanguage(),
|
||||
gender: profile.getGender() as number as Gender,
|
||||
avatarUrl: profile.getAvatarUrl(),
|
||||
});
|
||||
}
|
||||
|
||||
function humanPhoneToV2(phone: Phone): HumanPhone {
|
||||
return create(HumanPhoneSchema, {
|
||||
phone: phone.getPhone(),
|
||||
isVerified: phone.getIsPhoneVerified(),
|
||||
});
|
||||
}
|
||||
|
||||
function machineToV2(machine: Machine): MachineUser {
|
||||
return create(MachineUserSchema, {
|
||||
name: machine.getName(),
|
||||
description: machine.getDescription(),
|
||||
hasSecret: machine.getHasSecret(),
|
||||
accessTokenType: machine.getAccessTokenType() as number as AccessTokenType,
|
||||
});
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
import { CountryCode, parsePhoneNumber } from 'libphonenumber-js';
|
||||
|
||||
export function formatPhone(phone: string): { phone: string; country: CountryCode } | null {
|
||||
export function formatPhone(phone?: string): { phone: string; country: CountryCode } | null {
|
||||
const defaultCountry = 'US';
|
||||
|
||||
if (phone) {
|
||||
|
11
console/src/app/utils/pairwiseStartWith.ts
Normal file
11
console/src/app/utils/pairwiseStartWith.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, pairwise, startWith } from 'rxjs/operators';
|
||||
|
||||
export function pairwiseStartWith<T, R>(start: T) {
|
||||
return (source: Observable<R>) =>
|
||||
source.pipe(
|
||||
startWith(start),
|
||||
pairwise(),
|
||||
map(([prev, curr]) => [prev, curr] as [T | R, R]),
|
||||
);
|
||||
}
|
@@ -808,6 +808,9 @@
|
||||
"EMAIL": "Електронна поща",
|
||||
"PHONE": "Телефонен номер",
|
||||
"PHONE_HINT": "Използвайте символа +, последван от кода на държавата, на която се обаждате, или изберете държавата от падащото меню и накрая въведете телефонния номер",
|
||||
"PHONE_VERIFIED": "Телефонният номер е потвърден",
|
||||
"SEND_SMS": "Изпрати SMS за потвърждение",
|
||||
"SEND_EMAIL": "Изпрати имейл",
|
||||
"USERNAME": "Потребителско име",
|
||||
"CHANGEUSERNAME": "променям",
|
||||
"CHANGEUSERNAME_TITLE": "Промяна на потребителското име",
|
||||
@@ -948,6 +951,14 @@
|
||||
"5": "Спряно",
|
||||
"6": "Първоначално"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "неизвестен",
|
||||
"1": "Активен",
|
||||
"2": "Неактивен",
|
||||
"3": "Изтрито",
|
||||
"4": "Заключено",
|
||||
"5": "Първоначално"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "Име за вход (текуща организация)",
|
||||
"ADDITIONAL-EXTERNAL": "Име за вход (външна организация)"
|
||||
@@ -1485,7 +1496,9 @@
|
||||
"ENABLED": "„Активирано“ се наследява",
|
||||
"DISABLED": "„Деактивирано“ се наследява"
|
||||
},
|
||||
"RESET": "Задай всички на наследено"
|
||||
"RESET": "Задай всички на наследено",
|
||||
"CONSOLEUSEV2USERAPI": "Използвайте V2 API в конзолата за създаване на потребител",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "Когато този флаг е активиран, конзолата използва V2 User API за създаване на нови потребители. С V2 API новосъздадените потребители започват без начален статус."
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -809,6 +809,9 @@
|
||||
"EMAIL": "E-mail",
|
||||
"PHONE": "Telefonní číslo",
|
||||
"PHONE_HINT": "Použijte symbol + následovaný mezinárodním kódem země, nebo ze seznamu vyberte zemi a nakonec zadejte telefonní číslo",
|
||||
"PHONE_VERIFIED": "Telefonní číslo ověřeno",
|
||||
"SEND_SMS": "Odeslat ověřovací SMS",
|
||||
"SEND_EMAIL": "Odeslat E-mail",
|
||||
"USERNAME": "Uživatelské jméno",
|
||||
"CHANGEUSERNAME": "upravit",
|
||||
"CHANGEUSERNAME_TITLE": "Změna uživatelského jména",
|
||||
@@ -949,6 +952,14 @@
|
||||
"5": "Pozastavený",
|
||||
"6": "Počáteční"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "Neznámý",
|
||||
"1": "Aktivní",
|
||||
"2": "Neaktivní",
|
||||
"3": "Smazaný",
|
||||
"4": "Uzamčený",
|
||||
"5": "Počáteční"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "Přihlašovací jméno (současná organizace)",
|
||||
"ADDITIONAL-EXTERNAL": "Přihlašovací jméno (externí organizace)"
|
||||
@@ -1486,7 +1497,9 @@
|
||||
"ENABLED": "„Povoleno“ je zděděno",
|
||||
"DISABLED": "„Zakázáno“ je zděděno"
|
||||
},
|
||||
"RESET": "Nastavit vše na děděné"
|
||||
"RESET": "Nastavit vše na děděné",
|
||||
"CONSOLEUSEV2USERAPI": "Použijte V2 API v konzoli pro vytvoření uživatele",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "Když je tato příznak povolen, konzole používá V2 User API k vytvoření nových uživatelů. S V2 API nově vytvoření uživatelé začínají bez počátečního stavu."
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -809,6 +809,9 @@
|
||||
"EMAIL": "E-Mail",
|
||||
"PHONE": "Telefonnummer",
|
||||
"PHONE_HINT": "Verwenden das Symbol + gefolgt von der Landesvorwahl des Anrufers oder wähle das Land aus der Dropdown-Liste aus und gebe anschließend die Telefonnummer ein.",
|
||||
"PHONE_VERIFIED": "Telefonnummer verifiziert",
|
||||
"SEND_SMS": "Bestätigungs-SMS senden",
|
||||
"SEND_EMAIL": "E-Mail senden",
|
||||
"USERNAME": "Benutzername",
|
||||
"CHANGEUSERNAME": "bearbeiten",
|
||||
"CHANGEUSERNAME_TITLE": "Benutzername ändern",
|
||||
@@ -949,6 +952,14 @@
|
||||
"5": "Suspendiert",
|
||||
"6": "Initialisiert"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "Unbekannt",
|
||||
"1": "Aktiv",
|
||||
"2": "Inaktiv",
|
||||
"3": "Gelöscht",
|
||||
"4": "Gesperrt",
|
||||
"5": "Initialisiert"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "Benutzer Name (eigene Organisation)",
|
||||
"ADDITIONAL-EXTERNAL": "Loginname (externe Organisation)"
|
||||
@@ -1486,7 +1497,9 @@
|
||||
"ENABLED": "„Aktiviert“ wird vererbt",
|
||||
"DISABLED": "„Deaktiviert“ wird vererbt"
|
||||
},
|
||||
"RESET": "Alle auf Erben setzen"
|
||||
"RESET": "Alle auf Erben setzen",
|
||||
"CONSOLEUSEV2USERAPI": "Verwende die V2-API in der Konsole zur Erstellung von Benutzern",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "Wenn diese Option aktiviert ist, verwendet die Konsole die V2 User API, um neue Benutzer zu erstellen. Mit der V2 API starten neu erstellte Benutzer nicht im Initial Zustand."
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -809,6 +809,9 @@
|
||||
"EMAIL": "E-mail",
|
||||
"PHONE": "Phone number",
|
||||
"PHONE_HINT": "Use the + symbol followed by the calling country code, or select the country from the dropdown and finally enter the phone number",
|
||||
"PHONE_VERIFIED": "Phone Number Verified",
|
||||
"SEND_SMS": "Send Verification SMS",
|
||||
"SEND_EMAIL": "Send E-Mail",
|
||||
"USERNAME": "User Name",
|
||||
"CHANGEUSERNAME": "modify",
|
||||
"CHANGEUSERNAME_TITLE": "Change username",
|
||||
@@ -949,6 +952,14 @@
|
||||
"5": "Suspended",
|
||||
"6": "Initial"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "Unknown",
|
||||
"1": "Active",
|
||||
"2": "Inactive",
|
||||
"3": "Deleted",
|
||||
"4": "Locked",
|
||||
"5": "Initial"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "Loginname (current organization)",
|
||||
"ADDITIONAL-EXTERNAL": "Loginname (external organization)"
|
||||
@@ -1486,7 +1497,9 @@
|
||||
"ENABLED": "\"Enabled\" is inherited",
|
||||
"DISABLED": "\"Disabled\" is inherited"
|
||||
},
|
||||
"RESET": "Set all to inherit"
|
||||
"RESET": "Set all to inherit",
|
||||
"CONSOLEUSEV2USERAPI": "Use V2 Api in Console for User creation",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "When this flag is enabled, the console uses the V2 User API to create new users. With the V2 API, newly created users start without an initial state."
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -809,6 +809,9 @@
|
||||
"EMAIL": "Email",
|
||||
"PHONE": "Número de teléfono",
|
||||
"PHONE_HINT": "Usa el símbolo + seguido del prefijo del país o selecciona el país del menú desplegable y finalmente introduce el número de teléfono",
|
||||
"PHONE_VERIFIED": "Número de teléfono verificado",
|
||||
"SEND_SMS": "Enviar SMS de verificación",
|
||||
"SEND_EMAIL": "Enviar correo electrónico",
|
||||
"USERNAME": "Nombre de usuario",
|
||||
"CHANGEUSERNAME": "modificar",
|
||||
"CHANGEUSERNAME_TITLE": "Cambiar nombre de usuario",
|
||||
@@ -949,6 +952,14 @@
|
||||
"5": "Suspendido",
|
||||
"6": "Inicial"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "Desconocido",
|
||||
"1": "Activo",
|
||||
"2": "Inactivo",
|
||||
"3": "Borrado",
|
||||
"4": "Bloqueado",
|
||||
"5": "Inicial"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "Nombre de inicio de sesión (organización actual)",
|
||||
"ADDITIONAL-EXTERNAL": "Nombre de inicio de sesión (organización externa)"
|
||||
@@ -1487,7 +1498,9 @@
|
||||
"ENABLED": "\"Habilitado\" se hereda",
|
||||
"DISABLED": "\"Deshabilitado\" se hereda"
|
||||
},
|
||||
"RESET": "Establecer todo a heredado"
|
||||
"RESET": "Establecer todo a heredado",
|
||||
"CONSOLEUSEV2USERAPI": "Utilice la API V2 en la consola para la creación de usuarios",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "Cuando esta opción está habilitada, la consola utiliza la API V2 de usuario para crear nuevos usuarios. Con la API V2, los usuarios recién creados comienzan sin un estado inicial."
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -809,6 +809,9 @@
|
||||
"EMAIL": "Courriel",
|
||||
"PHONE": "Numéro de téléphone",
|
||||
"PHONE_HINT": "Utilisez le symbole + suivi de l'indicatif du pays de l'appelant, ou sélectionnez le pays dans la liste déroulante et saisissez enfin le numéro de téléphone.",
|
||||
"PHONE_VERIFIED": "Numéro de téléphone vérifié",
|
||||
"SEND_SMS": "Envoyer un SMS de vérification",
|
||||
"SEND_EMAIL": "Envoyer un Courriel",
|
||||
"USERNAME": "Nom de l'utilisateur",
|
||||
"CHANGEUSERNAME": "modifier",
|
||||
"CHANGEUSERNAME_TITLE": "Modifier le nom d'utilisateur",
|
||||
@@ -949,6 +952,14 @@
|
||||
"5": "Suspendu",
|
||||
"6": "Initial"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "Inconnu",
|
||||
"1": "Actif",
|
||||
"2": "Inactif",
|
||||
"3": "Supprimé",
|
||||
"4": "Verrouillé",
|
||||
"5": "Initial"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "Nom de connexion (organisation actuelle)",
|
||||
"ADDITIONAL-EXTERNAL": "Nom de connexion (organisation externe)"
|
||||
@@ -1486,7 +1497,9 @@
|
||||
"ENABLED": "\"Activé\" est hérité",
|
||||
"DISABLED": "\"Désactivé\" est hérité"
|
||||
},
|
||||
"RESET": "Réinitialiser tout sur hérité"
|
||||
"RESET": "Réinitialiser tout sur hérité",
|
||||
"CONSOLEUSEV2USERAPI": "Utilisez l'API V2 dans la console pour la création d'utilisateurs",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "Lorsque ce drapeau est activé, la console utilise l'API V2 User pour créer de nouveaux utilisateurs. Avec l'API V2, les nouveaux utilisateurs commencent sans état initial."
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -809,6 +809,9 @@
|
||||
"EMAIL": "E-mail",
|
||||
"PHONE": "Telefonszám",
|
||||
"PHONE_HINT": "Használd a + szimbólumot az ország hívókódja előtt, vagy válaszd ki az országot a legördülő menüből, és végül add meg a telefonszámot",
|
||||
"PHONE_VERIFIED": "Telefonszám ellenőrizve",
|
||||
"SEND_SMS": "Ellenőrző SMS küldése",
|
||||
"SEND_EMAIL": "E-mail küldése",
|
||||
"USERNAME": "Felhasználónév",
|
||||
"CHANGEUSERNAME": "módosít",
|
||||
"CHANGEUSERNAME_TITLE": "Felhasználónév megváltoztatása",
|
||||
@@ -949,6 +952,14 @@
|
||||
"5": "Felfüggesztett",
|
||||
"6": "Kezdeti"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "Ismeretlen",
|
||||
"1": "Aktív",
|
||||
"2": "Inaktív",
|
||||
"3": "Törölt",
|
||||
"4": "Zárolt",
|
||||
"5": "Kezdeti"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "Belépési név (jelenlegi szervezet)",
|
||||
"ADDITIONAL-EXTERNAL": "Belépési név (külső szervezet)"
|
||||
@@ -1484,7 +1495,9 @@
|
||||
"ENABLED": "\"Engedélyezve\" öröklődik",
|
||||
"DISABLED": "\"Letiltva\" öröklődik"
|
||||
},
|
||||
"RESET": "Mindent állíts öröklésre"
|
||||
"RESET": "Mindent állíts öröklésre",
|
||||
"CONSOLEUSEV2USERAPI": "Használja a V2 API-t a konzolban felhasználók létrehozásához",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "Ha ez a jelző engedélyezve van, a konzol a V2 User API-t használja új felhasználók létrehozásához. A V2 API-val az újonnan létrehozott felhasználók kezdeti állapot nélkül indulnak."
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -749,6 +749,9 @@
|
||||
"EMAIL": "E-mail",
|
||||
"PHONE": "Nomor telepon",
|
||||
"PHONE_HINT": "Gunakan simbol + diikuti dengan kode negara pemanggil, atau pilih negara dari dropdown dan terakhir masukkan nomor telepon",
|
||||
"PHONE_VERIFIED": "Nomor telepon terverifikasi",
|
||||
"SEND_SMS": "Kirim SMS verifikasi",
|
||||
"SEND_EMAIL": "Kirim E-mail",
|
||||
"USERNAME": "Nama belakang",
|
||||
"CHANGEUSERNAME": "memodifikasi",
|
||||
"CHANGEUSERNAME_TITLE": "Ubah nama pengguna",
|
||||
@@ -878,6 +881,14 @@
|
||||
"5": "Tergantung",
|
||||
"6": "Awal"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "Tidak dikenal",
|
||||
"1": "Aktif",
|
||||
"2": "Tidak aktif",
|
||||
"3": "Dihapus",
|
||||
"4": "Terkunci",
|
||||
"5": "Awal"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "Nama login (organisasi saat ini)",
|
||||
"ADDITIONAL-EXTERNAL": "Nama login (organisasi eksternal)"
|
||||
@@ -1354,7 +1365,9 @@
|
||||
"ENABLED": "\"Diaktifkan\" diwariskan",
|
||||
"DISABLED": "\"Dinonaktifkan\" diwariskan"
|
||||
},
|
||||
"RESET": "Tetapkan semua untuk diwarisi"
|
||||
"RESET": "Tetapkan semua untuk diwarisi",
|
||||
"CONSOLEUSEV2USERAPI": "Gunakan API V2 di konsol untuk pembuatan pengguna",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "Ketika flag ini diaktifkan, konsol menggunakan API Pengguna V2 untuk membuat pengguna baru. Dengan API V2, pengguna yang baru dibuat dimulai tanpa keadaan awal."
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -808,6 +808,9 @@
|
||||
"EMAIL": "E-mail",
|
||||
"PHONE": "Numero di telefono",
|
||||
"PHONE_HINT": "Utilizza il simbolo + seguito dal prefisso del paese, o seleziona il paese ed inserisci il numero di telefono",
|
||||
"PHONE_VERIFIED": "Numero di telefono verificato",
|
||||
"SEND_SMS": "Invia SMS di verifica",
|
||||
"SEND_EMAIL": "Invia E-mail",
|
||||
"USERNAME": "Nome utente",
|
||||
"CHANGEUSERNAME": "cambia",
|
||||
"CHANGEUSERNAME_TITLE": "Cambia nome utente",
|
||||
@@ -948,6 +951,14 @@
|
||||
"5": "Sospeso",
|
||||
"6": "Initializzato"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "Sconosciuto",
|
||||
"1": "Attivo",
|
||||
"2": "Inattivo",
|
||||
"3": "Rimosso",
|
||||
"4": "Bloccato",
|
||||
"5": "Initializzato"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "Nome (organizzazione corrente)",
|
||||
"ADDITIONAL-EXTERNAL": "Loginname (organizzazione esterna)"
|
||||
@@ -1486,7 +1497,9 @@
|
||||
"ENABLED": "\"Abilitato\" viene ereditato",
|
||||
"DISABLED": "\"Disabilitato\" viene ereditato"
|
||||
},
|
||||
"RESET": "Imposta tutto su predefinito"
|
||||
"RESET": "Imposta tutto su predefinito",
|
||||
"CONSOLEUSEV2USERAPI": "Utilizza l'API V2 nella console per la creazione degli utenti",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "Quando questa opzione è abilitata, la console utilizza l'API V2 User per creare nuovi utenti. Con l'API V2, i nuovi utenti creati iniziano senza uno stato iniziale."
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -809,6 +809,9 @@
|
||||
"EMAIL": "Eメール",
|
||||
"PHONE": "電話番号",
|
||||
"PHONE_HINT": "+ マークに続いて電話をかけたい国コードを入力するか、ドロップダウンから国を選択して電話番号を入力します。",
|
||||
"PHONE_VERIFIED": "電話番号が確認されました",
|
||||
"SEND_SMS": "認証SMSを送信",
|
||||
"SEND_EMAIL": "メールを送信",
|
||||
"USERNAME": "ユーザー名",
|
||||
"CHANGEUSERNAME": "変更",
|
||||
"CHANGEUSERNAME_TITLE": "ユーザー名の変更",
|
||||
@@ -949,6 +952,14 @@
|
||||
"5": "停止",
|
||||
"6": "初期化待ち"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "不明",
|
||||
"1": "アクティブ",
|
||||
"2": "非アクティブ",
|
||||
"3": "削除",
|
||||
"4": "ロック",
|
||||
"5": "初期化待ち"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "ログインネーム(現在の組織)",
|
||||
"ADDITIONAL-EXTERNAL": "ログインネーム(外部の組織)"
|
||||
@@ -1486,7 +1497,9 @@
|
||||
"ENABLED": "有効は継承されます",
|
||||
"DISABLED": "無効は継承されます"
|
||||
},
|
||||
"RESET": "すべて継承に設定"
|
||||
"RESET": "すべて継承に設定",
|
||||
"CONSOLEUSEV2USERAPI": "コンソールでユーザー作成のためにV2 APIを使用してください。",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "このフラグが有効化されると、コンソールはV2ユーザーAPIを使用して新しいユーザーを作成します。V2 APIでは、新しく作成されたユーザーは初期状態なしで開始します。"
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -809,6 +809,9 @@
|
||||
"EMAIL": "이메일",
|
||||
"PHONE": "전화번호",
|
||||
"PHONE_HINT": "+ 기호 다음에 국가 코드를 입력하거나 드롭다운에서 국가를 선택한 후 전화번호를 입력하세요.",
|
||||
"PHONE_VERIFIED": "전화번호 확인됨",
|
||||
"SEND_SMS": "인증 SMS 보내기",
|
||||
"SEND_EMAIL": "이메일 보내기",
|
||||
"USERNAME": "사용자 이름",
|
||||
"CHANGEUSERNAME": "수정",
|
||||
"CHANGEUSERNAME_TITLE": "사용자 이름 변경",
|
||||
@@ -949,6 +952,14 @@
|
||||
"5": "일시 중단됨",
|
||||
"6": "초기"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "알 수 없음",
|
||||
"1": "활성",
|
||||
"2": "비활성",
|
||||
"3": "삭제됨",
|
||||
"4": "잠김",
|
||||
"5": "초기"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "로그인 이름 (현재 조직)",
|
||||
"ADDITIONAL-EXTERNAL": "로그인 이름 (외부 조직)"
|
||||
@@ -1486,7 +1497,9 @@
|
||||
"ENABLED": "\"활성화됨\"은 상속되었습니다.",
|
||||
"DISABLED": "\"비활성화됨\"은 상속되었습니다."
|
||||
},
|
||||
"RESET": "모두 상속으로 설정"
|
||||
"RESET": "모두 상속으로 설정",
|
||||
"CONSOLEUSEV2USERAPI": "콘솔에서 사용자 생성을 위해 V2 API를 사용하세요",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "이 플래그가 활성화되면 콘솔은 V2 사용자 API를 사용하여 새 사용자를 생성합니다. V2 API를 사용하면 새로 생성된 사용자는 초기 상태 없이 시작합니다."
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -809,6 +809,9 @@
|
||||
"EMAIL": "Е-пошта",
|
||||
"PHONE": "Телефонски број",
|
||||
"PHONE_HINT": "Користете + и потоа дополнителниот број на земјата, или изберете ја земјата од листата и на крај внесете го телефонскиот број",
|
||||
"PHONE_VERIFIED": "Телефонскиот број е потврден",
|
||||
"SEND_SMS": "Испрати СМС за верификација",
|
||||
"SEND_EMAIL": "Испрати е-пошта",
|
||||
"USERNAME": "Корисничко име",
|
||||
"CHANGEUSERNAME": "промени",
|
||||
"CHANGEUSERNAME_TITLE": "Промени корисничко име",
|
||||
@@ -949,6 +952,14 @@
|
||||
"5": "Суспендирано",
|
||||
"6": "Иницијално"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "Непознато",
|
||||
"1": "Активно",
|
||||
"2": "Неактивно",
|
||||
"3": "Избришано",
|
||||
"4": "Заклучено",
|
||||
"5": "Иницијално"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "Корисничко име (тековна организација)",
|
||||
"ADDITIONAL-EXTERNAL": "Корисничко име (надворешна организација)"
|
||||
@@ -1487,7 +1498,9 @@
|
||||
"ENABLED": "„Овозможено“ е наследено",
|
||||
"DISABLED": "„Оневозможено“ е наследено"
|
||||
},
|
||||
"RESET": "Поставете ги сите да наследат"
|
||||
"RESET": "Поставете ги сите да наследат",
|
||||
"CONSOLEUSEV2USERAPI": "Користете V2 API во конзолата за креирање на корисници",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "Кога ова знаме е овозможено, конзолата го користи V2 User API за креирање на нови корисници. Со V2 API, новосоздадените корисници започнуваат без почетна состојба."
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -809,6 +809,9 @@
|
||||
"EMAIL": "E-mail",
|
||||
"PHONE": "Telefoonnummer",
|
||||
"PHONE_HINT": "Gebruik het + symbool gevolgd door de landcode, of selecteer het land uit de dropdown en voer ten slotte het telefoonnummer in",
|
||||
"PHONE_VERIFIED": "Telefoonnummer geverifieerd",
|
||||
"SEND_SMS": "Verificatie-SMS verzenden",
|
||||
"SEND_EMAIL": "E-mail verzenden",
|
||||
"USERNAME": "Gebruikersnaam",
|
||||
"CHANGEUSERNAME": "wijzigen",
|
||||
"CHANGEUSERNAME_TITLE": "Gebruikersnaam wijzigen",
|
||||
@@ -949,6 +952,14 @@
|
||||
"5": "Opgeschort",
|
||||
"6": "Initieel"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "Onbekend",
|
||||
"1": "Actief",
|
||||
"2": "Inactief",
|
||||
"3": "Verwijderd",
|
||||
"4": "Vergrendeld",
|
||||
"5": "Initieel"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "Loginnaam (huidige organisatie)",
|
||||
"ADDITIONAL-EXTERNAL": "Loginnaam (externe organisatie)"
|
||||
@@ -1484,7 +1495,9 @@
|
||||
"ENABLED": "\"Ingeschakeld\" wordt overgenomen",
|
||||
"DISABLED": "\"Uitgeschakeld\" wordt overgenomen"
|
||||
},
|
||||
"RESET": "Alles instellen op overgenomen"
|
||||
"RESET": "Alles instellen op overgenomen",
|
||||
"CONSOLEUSEV2USERAPI": "Gebruik de V2 API in de console voor het aanmaken van gebruikers",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "Wanneer deze vlag is ingeschakeld, gebruikt de console de V2 User API om nieuwe gebruikers aan te maken. Met de V2 API beginnen nieuw aangemaakte gebruikers zonder een initiële status."
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -808,6 +808,9 @@
|
||||
"EMAIL": "E-mail",
|
||||
"PHONE": "Numer telefonu",
|
||||
"PHONE_HINT": "Użyj symbolu +, a następnie kodu kraju, z którego dzwonisz, lub wybierz kraj z listy rozwijanej i wprowadź numer telefonu.",
|
||||
"PHONE_VERIFIED": "Numer telefonu zweryfikowany",
|
||||
"SEND_SMS": "Wyślij SMS weryfikacyjny",
|
||||
"SEND_EMAIL": "Wyślij E-mail",
|
||||
"USERNAME": "Nazwa użytkownika",
|
||||
"CHANGEUSERNAME": "modyfikuj",
|
||||
"CHANGEUSERNAME_TITLE": "Zmień nazwę użytkownika",
|
||||
@@ -948,6 +951,14 @@
|
||||
"5": "Zawieszony",
|
||||
"6": "Początkowy"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "Nieznany",
|
||||
"1": "Aktywny",
|
||||
"2": "Nieaktywny",
|
||||
"3": "Usunięty",
|
||||
"4": "Zablokowany",
|
||||
"5": "Początkowy"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "Nazwa użytkownika (obecna organizacja)",
|
||||
"ADDITIONAL-EXTERNAL": "Nazwa użytkownika (organizacja zewnętrzna)"
|
||||
@@ -1485,7 +1496,9 @@
|
||||
"ENABLED": "„Włączony” jest dziedziczone",
|
||||
"DISABLED": "„Wyłączony” jest dziedziczone"
|
||||
},
|
||||
"RESET": "Ustaw wszystko na dziedziczone"
|
||||
"RESET": "Ustaw wszystko na dziedziczone",
|
||||
"CONSOLEUSEV2USERAPI": "Użyj API V2 w konsoli do tworzenia użytkowników",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "Gdy ta flaga jest włączona, konsola używa API V2 User do tworzenia nowych użytkowników. W przypadku API V2 nowo utworzeni użytkownicy rozpoczynają bez stanu początkowego."
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -809,6 +809,9 @@
|
||||
"EMAIL": "E-mail",
|
||||
"PHONE": "Número de Telefone",
|
||||
"PHONE_HINT": "Use o símbolo + seguido do código de chamada do país, ou selecione o país na lista suspensa e, em seguida, insira o número de telefone",
|
||||
"PHONE_VERIFIED": "Número de telefone verificado",
|
||||
"SEND_SMS": "Enviar SMS de verificação",
|
||||
"SEND_EMAIL": "Enviar E-mail",
|
||||
"USERNAME": "Nome de Usuário",
|
||||
"CHANGEUSERNAME": "modificar",
|
||||
"CHANGEUSERNAME_TITLE": "Alterar nome de usuário",
|
||||
@@ -949,6 +952,14 @@
|
||||
"5": "Suspenso",
|
||||
"6": "Inicial"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "Desconhecido",
|
||||
"1": "Ativo",
|
||||
"2": "Inativo",
|
||||
"3": "Excluído",
|
||||
"4": "Bloqueado",
|
||||
"5": "Inicial"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "Nome de usuário (organização atual)",
|
||||
"ADDITIONAL-EXTERNAL": "Nome de usuário (organização externa)"
|
||||
@@ -1487,7 +1498,9 @@
|
||||
"ENABLED": "\"Habilitado\" é herdado",
|
||||
"DISABLED": "\"Desabilitado\" é herdado"
|
||||
},
|
||||
"RESET": "Definir tudo para herdar"
|
||||
"RESET": "Definir tudo para herdar",
|
||||
"CONSOLEUSEV2USERAPI": "Use a API V2 no console para criação de usuários",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "Quando esta opção está ativada, o console utiliza a API V2 de Usuários para criar novos usuários. Com a API V2, os novos usuários criados começam sem um estado inicial."
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -816,6 +816,9 @@
|
||||
"EMAIL": "Электронная почта",
|
||||
"PHONE": "Номер телефона",
|
||||
"PHONE_HINT": "Используйте 00 или символ +, за которым следует код страны вызываемого абонента, или выберите страну из раскрывающегося списка и введите номер телефона.",
|
||||
"PHONE_VERIFIED": "Номер телефона подтвержден",
|
||||
"SEND_SMS": "Отправить проверочный SMS",
|
||||
"SEND_EMAIL": "Отправить e-mail",
|
||||
"USERNAME": "Имя пользователя",
|
||||
"CHANGEUSERNAME": "Изменить",
|
||||
"CHANGEUSERNAME_TITLE": "Изменить имя пользователя",
|
||||
@@ -967,6 +970,14 @@
|
||||
"5": "Приостановлен",
|
||||
"6": "Начальный"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "Неизвестен",
|
||||
"1": "Активен",
|
||||
"2": "Неактивен",
|
||||
"3": "Удалён",
|
||||
"4": "Заблокирован",
|
||||
"5": "Начальный"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "Логин (текущая организация)",
|
||||
"ADDITIONAL-EXTERNAL": "Логин (внешняя организация)"
|
||||
@@ -1538,7 +1549,9 @@
|
||||
"ENABLED": "«Включено» наследуется",
|
||||
"DISABLED": "«Выключено» передается по наследству"
|
||||
},
|
||||
"RESET": "Установить все по умолчанию"
|
||||
"RESET": "Установить все по умолчанию",
|
||||
"CONSOLEUSEV2USERAPI": "Используйте V2 API в консоли для создания пользователей",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "Когда этот флаг включен, консоль использует V2 User API для создания новых пользователей. С API V2 новые пользователи создаются без начального состояния."
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -809,6 +809,9 @@
|
||||
"EMAIL": "E-post",
|
||||
"PHONE": "Telefonnummer",
|
||||
"PHONE_HINT": "Använd + symbolen följt av landskoden, eller välj landet från rullgardinsmenyn och ange sedan telefonnumret",
|
||||
"PHONE_VERIFIED": "Telefonnummer verifierat",
|
||||
"SEND_SMS": "Skicka verifierings-SMS",
|
||||
"SEND_EMAIL": "Skicka E-post",
|
||||
"USERNAME": "Användarnamn",
|
||||
"CHANGEUSERNAME": "ändra",
|
||||
"CHANGEUSERNAME_TITLE": "Ändra användarnamn",
|
||||
@@ -949,6 +952,14 @@
|
||||
"5": "Suspenderad",
|
||||
"6": "Initial"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "Okänd",
|
||||
"1": "Aktiv",
|
||||
"2": "Inaktiv",
|
||||
"3": "Raderad",
|
||||
"4": "Låst",
|
||||
"5": "Initial"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "Inloggningsnamn (nuvarande organisation)",
|
||||
"ADDITIONAL-EXTERNAL": "Inloggningsnamn (extern organisation)"
|
||||
@@ -1490,7 +1501,9 @@
|
||||
"ENABLED": "\"Aktiverad\" ärvs",
|
||||
"DISABLED": "\"Inaktiverad\" ärvs"
|
||||
},
|
||||
"RESET": "Återställ allt till arv"
|
||||
"RESET": "Återställ allt till arv",
|
||||
"CONSOLEUSEV2USERAPI": "Använd V2 API i konsolen för att skapa användare",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "När denna flagga är aktiverad använder konsolen V2 User API för att skapa nya användare. Med V2 API startar nyligen skapade användare utan ett initialt tillstånd."
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -809,6 +809,9 @@
|
||||
"EMAIL": "电子邮件",
|
||||
"PHONE": "手机号码",
|
||||
"PHONE_HINT": "使用+号,后跟呼叫者的国家/地区代码,或从下拉列表中选择国家/地区,最后输入电话号码",
|
||||
"PHONE_VERIFIED": "电话号码已验证",
|
||||
"SEND_SMS": "发送验证短信",
|
||||
"SEND_EMAIL": "发送电子邮件",
|
||||
"USERNAME": "用户名",
|
||||
"CHANGEUSERNAME": "修改",
|
||||
"CHANGEUSERNAME_TITLE": "修改用户名称",
|
||||
@@ -949,6 +952,14 @@
|
||||
"5": "已暂停",
|
||||
"6": "初始化"
|
||||
},
|
||||
"STATEV2": {
|
||||
"0": "未知",
|
||||
"1": "启用",
|
||||
"2": "停用",
|
||||
"3": "已删除",
|
||||
"4": "已锁定",
|
||||
"5": "初始化"
|
||||
},
|
||||
"SEARCH": {
|
||||
"ADDITIONAL": "登录名 (当前组织)",
|
||||
"ADDITIONAL-EXTERNAL": "登录名 (外部组织)"
|
||||
@@ -1486,7 +1497,9 @@
|
||||
"ENABLED": "“已启用” 是继承的",
|
||||
"DISABLED": "“已禁用” 是继承的"
|
||||
},
|
||||
"RESET": "全部设置为继承"
|
||||
"RESET": "全部设置为继承",
|
||||
"CONSOLEUSEV2USERAPI": "在控制台中使用V2 API创建用户。",
|
||||
"CONSOLEUSEV2USERAPI_DESCRIPTION": "启用此标志时,控制台使用V2用户API创建新用户。使用V2 API,新创建的用户将以无初始状态开始。"
|
||||
},
|
||||
"DIALOG": {
|
||||
"RESET": {
|
||||
|
@@ -1501,11 +1501,31 @@
|
||||
"@bufbuild/buf-win32-arm64" "1.43.0"
|
||||
"@bufbuild/buf-win32-x64" "1.43.0"
|
||||
|
||||
"@bufbuild/protobuf@^2.2.2":
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-2.2.3.tgz#9cd136f6b687e63e9b517b3a54211ece942897ee"
|
||||
integrity sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==
|
||||
|
||||
"@colors/colors@1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
||||
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
|
||||
|
||||
"@connectrpc/connect-node@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@connectrpc/connect-node/-/connect-node-2.0.0.tgz#1ea4eff7f2633fbe3d80378e1420bd213d9d83a4"
|
||||
integrity sha512-DoI5T+SUvlS/8QBsxt2iDoUg15dSxqhckegrgZpWOtADtmGohBIVbx1UjtWmjLBrP4RdD0FeBw+XyRUSbpKnJQ==
|
||||
|
||||
"@connectrpc/connect-web@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@connectrpc/connect-web/-/connect-web-2.0.0.tgz#14055c933bfe846e75856bce49ecd0250cbec3b2"
|
||||
integrity sha512-oeCxqHXLXlWJdmcvp9L3scgAuK+FjNSn+twyhUxc8yvDbTumnt5Io+LnBzSYxAdUdYqTw5yHfTSCJ4hj0QID0g==
|
||||
|
||||
"@connectrpc/connect@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@connectrpc/connect/-/connect-2.0.0.tgz#0a34d934bdce5fd3723f79b70aa843d4c1213df8"
|
||||
integrity sha512-Usm8jgaaULANJU8vVnhWssSA6nrZ4DJEAbkNtXSoZay2YD5fDyMukCxu8NEhCvFzfHvrhxhcjttvgpyhOM7xAQ==
|
||||
|
||||
"@ctrl/ngx-codemirror@^6.1.0":
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ctrl/ngx-codemirror/-/ngx-codemirror-6.1.0.tgz#9324a56e4b709be9c515364d21e05e1d7589f009"
|
||||
@@ -3496,6 +3516,25 @@
|
||||
js-yaml "^3.10.0"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@zitadel/client@^1.0.6":
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@zitadel/client/-/client-1.0.6.tgz#9fe44ff7c757e8f38fa08d25083dc036afebf5cb"
|
||||
integrity sha512-MG6RAApoI2Y3QGRfKByISOqGTSFsMr5YtKQYPFDAJhivYK32d7hUiMEv+WzShfGHEI38336FbKz9vg/4M961Lg==
|
||||
dependencies:
|
||||
"@bufbuild/protobuf" "^2.2.2"
|
||||
"@connectrpc/connect" "^2.0.0"
|
||||
"@connectrpc/connect-node" "^2.0.0"
|
||||
"@connectrpc/connect-web" "^2.0.0"
|
||||
"@zitadel/proto" "1.0.3"
|
||||
jose "^5.3.0"
|
||||
|
||||
"@zitadel/proto@1.0.3", "@zitadel/proto@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.0.3.tgz#28721710d9e87009adf14f90e0c8cb9bae5275ec"
|
||||
integrity sha512-95XPGgFgfTwU1A3oQYxTv4p+Qy/9yMO/o21VRtPBfVhPusFFCW0ddg4YoKTKpQl9FbIG7VYMLmRyuJBPuf3r+g==
|
||||
dependencies:
|
||||
"@bufbuild/protobuf" "^2.2.2"
|
||||
|
||||
"@zkochan/js-yaml@0.0.6":
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz#975f0b306e705e28b8068a07737fa46d3fc04826"
|
||||
@@ -6538,6 +6577,11 @@ jiti@^1.18.2:
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268"
|
||||
integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==
|
||||
|
||||
jose@^5.3.0:
|
||||
version "5.9.6"
|
||||
resolved "https://registry.yarnpkg.com/jose/-/jose-5.9.6.tgz#77f1f901d88ebdc405e57cce08d2a91f47521883"
|
||||
integrity sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==
|
||||
|
||||
js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
|
@@ -56,8 +56,8 @@ describe('machines', () => {
|
||||
loginName = loginname(machine.removeName, Cypress.env('ORGANIZATION'));
|
||||
}
|
||||
it('should delete a machine', () => {
|
||||
const rowSelector = `tr:contains(${machine.removeName})`;
|
||||
cy.get(rowSelector).find('[data-e2e="enabled-delete-button"]').click({ force: true });
|
||||
const rowSelector = `tr:contains('${machine.removeName}')`;
|
||||
cy.get(rowSelector).should('be.visible').find('[data-e2e="enabled-delete-button"]').click({ force: true });
|
||||
cy.get('[data-e2e="confirm-dialog-input"]').focus().should('be.enabled').type(loginName);
|
||||
cy.get('[data-e2e="confirm-dialog-button"]').click();
|
||||
cy.shouldConfirmSuccess();
|
||||
|
@@ -71,6 +71,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) (*com
|
||||
EnableBackChannelLogout: req.EnableBackChannelLogout,
|
||||
LoginV2: loginV2,
|
||||
PermissionCheckV2: req.PermissionCheckV2,
|
||||
ConsoleUseV2UserApi: req.ConsoleUseV2UserApi,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -91,6 +92,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat
|
||||
EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout),
|
||||
LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2),
|
||||
PermissionCheckV2: featureSourceToFlagPb(&f.PermissionCheckV2),
|
||||
ConsoleUseV2UserApi: featureSourceToFlagPb(&f.ConsoleUseV2UserApi),
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -183,6 +183,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
|
||||
Required: true,
|
||||
BaseUri: gu.Ptr("https://login.com"),
|
||||
},
|
||||
ConsoleUseV2UserApi: gu.Ptr(true),
|
||||
}
|
||||
want := &command.InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
@@ -200,6 +201,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
|
||||
Required: true,
|
||||
BaseURI: &url.URL{Scheme: "https", Host: "login.com"},
|
||||
},
|
||||
ConsoleUseV2UserApi: gu.Ptr(true),
|
||||
}
|
||||
got, err := instanceFeaturesToCommand(arg)
|
||||
assert.Equal(t, want, got)
|
||||
@@ -264,6 +266,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
|
||||
Level: feature.LevelInstance,
|
||||
Value: true,
|
||||
},
|
||||
ConsoleUseV2UserApi: query.FeatureSource[bool]{
|
||||
Level: feature.LevelInstance,
|
||||
Value: true,
|
||||
},
|
||||
}
|
||||
want := &feature_pb.GetInstanceFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
@@ -328,6 +334,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
|
||||
Enabled: true,
|
||||
Source: feature_pb.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
ConsoleUseV2UserApi: &feature_pb.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature_pb.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
}
|
||||
got := instanceFeaturesToPb(arg)
|
||||
assert.Equal(t, want, got)
|
||||
|
@@ -30,6 +30,7 @@ type InstanceFeatures struct {
|
||||
EnableBackChannelLogout *bool
|
||||
LoginV2 *feature.LoginV2
|
||||
PermissionCheckV2 *bool
|
||||
ConsoleUseV2UserApi *bool
|
||||
}
|
||||
|
||||
func (m *InstanceFeatures) isEmpty() bool {
|
||||
@@ -47,7 +48,7 @@ func (m *InstanceFeatures) isEmpty() bool {
|
||||
m.DisableUserTokenEvent == nil &&
|
||||
m.EnableBackChannelLogout == nil &&
|
||||
m.LoginV2 == nil &&
|
||||
m.PermissionCheckV2 == nil
|
||||
m.PermissionCheckV2 == nil && m.ConsoleUseV2UserApi == nil
|
||||
}
|
||||
|
||||
func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) {
|
||||
|
@@ -80,6 +80,7 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
feature_v2.InstanceEnableBackChannelLogout,
|
||||
feature_v2.InstanceLoginVersion,
|
||||
feature_v2.InstancePermissionCheckV2,
|
||||
feature_v2.InstanceConsoleUseV2UserApi,
|
||||
).
|
||||
Builder().ResourceOwner(m.ResourceOwner)
|
||||
}
|
||||
@@ -133,6 +134,9 @@ func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value an
|
||||
case feature.KeyPermissionCheckV2:
|
||||
v := value.(bool)
|
||||
features.PermissionCheckV2 = &v
|
||||
case feature.KeyConsoleUseV2UserApi:
|
||||
v := value.(bool)
|
||||
features.ConsoleUseV2UserApi = &v
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,5 +157,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.EnableBackChannelLogout, f.EnableBackChannelLogout, feature_v2.InstanceEnableBackChannelLogout)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LoginV2, f.LoginV2, feature_v2.InstanceLoginVersion)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.PermissionCheckV2, f.PermissionCheckV2, feature_v2.InstancePermissionCheckV2)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.ConsoleUseV2UserApi, f.ConsoleUseV2UserApi, feature_v2.InstanceConsoleUseV2UserApi)
|
||||
return cmds
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@ const (
|
||||
KeyEnableBackChannelLogout
|
||||
KeyLoginV2
|
||||
KeyPermissionCheckV2
|
||||
KeyConsoleUseV2UserApi
|
||||
)
|
||||
|
||||
//go:generate enumer -type Level -transform snake -trimprefix Level
|
||||
@@ -54,6 +55,7 @@ type Features struct {
|
||||
EnableBackChannelLogout bool `json:"enable_back_channel_logout,omitempty"`
|
||||
LoginV2 LoginV2 `json:"login_v2,omitempty"`
|
||||
PermissionCheckV2 bool `json:"permission_check_v2,omitempty"`
|
||||
ConsoleUseV2UserApi bool `json:"console_use_v2_user_api,omitempty"`
|
||||
}
|
||||
|
||||
type ImprovedPerformanceType int32
|
||||
|
@@ -7,11 +7,11 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2"
|
||||
const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api"
|
||||
|
||||
var _KeyIndex = [...]uint16{0, 11, 28, 61, 81, 92, 106, 113, 133, 140, 163, 197, 221, 247, 255, 274}
|
||||
var _KeyIndex = [...]uint16{0, 11, 28, 61, 81, 92, 106, 113, 133, 140, 163, 197, 221, 247, 255, 274, 297}
|
||||
|
||||
const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2"
|
||||
const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api"
|
||||
|
||||
func (i Key) String() string {
|
||||
if i < 0 || i >= Key(len(_KeyIndex)-1) {
|
||||
@@ -39,9 +39,10 @@ func _KeyNoOp() {
|
||||
_ = x[KeyEnableBackChannelLogout-(12)]
|
||||
_ = x[KeyLoginV2-(13)]
|
||||
_ = x[KeyPermissionCheckV2-(14)]
|
||||
_ = x[KeyConsoleUseV2UserApi-(15)]
|
||||
}
|
||||
|
||||
var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance, KeyWebKey, KeyDebugOIDCParentError, KeyOIDCSingleV1SessionTermination, KeyDisableUserTokenEvent, KeyEnableBackChannelLogout, KeyLoginV2, KeyPermissionCheckV2}
|
||||
var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance, KeyWebKey, KeyDebugOIDCParentError, KeyOIDCSingleV1SessionTermination, KeyDisableUserTokenEvent, KeyEnableBackChannelLogout, KeyLoginV2, KeyPermissionCheckV2, KeyConsoleUseV2UserApi}
|
||||
|
||||
var _KeyNameToValueMap = map[string]Key{
|
||||
_KeyName[0:11]: KeyUnspecified,
|
||||
@@ -74,6 +75,8 @@ var _KeyNameToValueMap = map[string]Key{
|
||||
_KeyLowerName[247:255]: KeyLoginV2,
|
||||
_KeyName[255:274]: KeyPermissionCheckV2,
|
||||
_KeyLowerName[255:274]: KeyPermissionCheckV2,
|
||||
_KeyName[274:297]: KeyConsoleUseV2UserApi,
|
||||
_KeyLowerName[274:297]: KeyConsoleUseV2UserApi,
|
||||
}
|
||||
|
||||
var _KeyNames = []string{
|
||||
@@ -92,6 +95,7 @@ var _KeyNames = []string{
|
||||
_KeyName[221:247],
|
||||
_KeyName[247:255],
|
||||
_KeyName[255:274],
|
||||
_KeyName[274:297],
|
||||
}
|
||||
|
||||
// KeyString retrieves an enum value from the enum constants string name.
|
||||
|
@@ -23,6 +23,7 @@ type InstanceFeatures struct {
|
||||
EnableBackChannelLogout FeatureSource[bool]
|
||||
LoginV2 FeatureSource[*feature.LoginV2]
|
||||
PermissionCheckV2 FeatureSource[bool]
|
||||
ConsoleUseV2UserApi FeatureSource[bool]
|
||||
}
|
||||
|
||||
func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) {
|
||||
|
@@ -76,6 +76,7 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
|
||||
feature_v2.InstanceEnableBackChannelLogout,
|
||||
feature_v2.InstanceLoginVersion,
|
||||
feature_v2.InstancePermissionCheckV2,
|
||||
feature_v2.InstanceConsoleUseV2UserApi,
|
||||
).
|
||||
Builder().ResourceOwner(m.ResourceOwner)
|
||||
}
|
||||
@@ -142,6 +143,8 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_
|
||||
features.LoginV2.set(level, event.Value)
|
||||
case feature.KeyPermissionCheckV2:
|
||||
features.PermissionCheckV2.set(level, event.Value)
|
||||
case feature.KeyConsoleUseV2UserApi:
|
||||
features.ConsoleUseV2UserApi.set(level, event.Value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@@ -116,6 +116,10 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer {
|
||||
Event: feature_v2.InstancePermissionCheckV2,
|
||||
Reduce: reduceInstanceSetFeature[bool],
|
||||
},
|
||||
{
|
||||
Event: feature_v2.InstanceConsoleUseV2UserApi,
|
||||
Reduce: reduceInstanceSetFeature[bool],
|
||||
},
|
||||
{
|
||||
Event: instance.InstanceRemovedEventType,
|
||||
Reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol),
|
||||
|
@@ -35,4 +35,5 @@ func init() {
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, InstanceEnableBackChannelLogout, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, InstanceLoginVersion, eventstore.GenericEventMapper[SetEvent[*feature.LoginV2]])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, InstancePermissionCheckV2, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||
eventstore.RegisterFilterEventMapper(AggregateType, InstanceConsoleUseV2UserApi, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||
}
|
||||
|
@@ -40,6 +40,7 @@ var (
|
||||
InstanceEnableBackChannelLogout = setEventTypeFromFeature(feature.LevelInstance, feature.KeyEnableBackChannelLogout)
|
||||
InstanceLoginVersion = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginV2)
|
||||
InstancePermissionCheckV2 = setEventTypeFromFeature(feature.LevelInstance, feature.KeyPermissionCheckV2)
|
||||
InstanceConsoleUseV2UserApi = setEventTypeFromFeature(feature.LevelInstance, feature.KeyConsoleUseV2UserApi)
|
||||
)
|
||||
|
||||
const (
|
||||
|
@@ -106,6 +106,13 @@ message SetInstanceFeaturesRequest{
|
||||
description: "Enable a newer, more performant, permission check used for v2 and v3 resource based APIs.";
|
||||
}
|
||||
];
|
||||
|
||||
optional bool console_use_v2_user_api = 15 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "If this is enabled the console web client will use the new User v2 API for certain calls";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message SetInstanceFeaturesResponse {
|
||||
@@ -225,4 +232,11 @@ message GetInstanceFeaturesResponse {
|
||||
description: "Enable a newer, more performant, permission check used for v2 and v3 resource based APIs.";
|
||||
}
|
||||
];
|
||||
|
||||
FeatureFlag console_use_v2_user_api = 16 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "true";
|
||||
description: "If this is enabled the console web client will use the new User v2 API for certain calls";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
Reference in New Issue
Block a user