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 <[email protected]>
This commit is contained in:
Ramon
2025-02-17 19:25:46 +01:00
committed by GitHub
co-authored by Max Peintner
parent ad225836d5
commit 3042bbb993
90 changed files with 3679 additions and 2315 deletions
@@ -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.loginPolicy?.disableLoginWithPhone &&
this.user.human?.phone?.phone &&
this.user.human.phone.isPhoneVerified
) {
phone = this.user.human?.phone?.phone;
}
if (!this.user) {
return new Set();
}
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(() => {
this.metadata.splice(index, 1);
if (this.metadata.length === 0) {
this.addEntry();
}
});
} else {
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();
}
}
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(() => {
this.toast.showInfo('METADATA.SETSUCCESS', true);
})
.catch((error: any) => {
this.toast.showError(error);
});
try {
await this.data.setFcn(key, value);
this.toast.showInfo('METADATA.SETSUCCESS', true);
} 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,15 +51,17 @@ export class SidenavComponent implements ControlValueAccessor {
}
if (this.queryParam && setting) {
this.router.navigate([], {
relativeTo: this.route,
queryParams: {
[this.queryParam]: setting,
},
replaceUrl: true,
queryParamsHandling: 'merge',
skipLocationChange: false,
});
this.router
.navigate([], {
relativeTo: this.route,
queryParams: {
[this.queryParam]: setting,
},
replaceUrl: true,
queryParamsHandling: 'merge',
skipLocationChange: false,
})
.then();
}
}