fix: part console ui (#1430)

* home, user create, list, app list

* org create fix

* fix user bindings, usertable, changes, projects

* fix app grid type, domains

* e.164 international phonenumber validation, create user, phone email dialog

* single authmethod create dialog

* fix timestamp to date pipe, app create secret dialog

* validate e164 number edit dialog

* fix machine keys, timestamp pipe

* rm comment

* projecttype circular deps

* downgrade protoc to 3.13.0 due to deserializer error, fix circular dep

* apptype controlname
This commit is contained in:
Max Peintner 2021-03-18 10:46:22 +01:00 committed by GitHub
parent 3cdaa8193d
commit bc7e650089
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 530 additions and 355 deletions

View File

@ -18,7 +18,7 @@ ENV PROTOC_ARCH x86_64
## protoc and protoc-gen-grpc-web for later use ## protoc and protoc-gen-grpc-web for later use
####################### #######################
FROM ${BUILDARCH}-base AS base FROM ${BUILDARCH}-base AS base
ARG PROTOC_VERSION=3.14.0 ARG PROTOC_VERSION=3.13.0
ARG PROTOC_ZIP=protoc-${PROTOC_VERSION}-linux-${PROTOC_ARCH}.zip ARG PROTOC_ZIP=protoc-${PROTOC_VERSION}-linux-${PROTOC_ARCH}.zip
ARG GRPC_WEB_VERSION=1.2.1 ARG GRPC_WEB_VERSION=1.2.1
# no arm specific version available and x86 works fine at the moment: # no arm specific version available and x86 works fine at the moment:

View File

@ -10721,6 +10721,11 @@
} }
} }
}, },
"libphonenumber-js": {
"version": "1.9.13",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.13.tgz",
"integrity": "sha512-DOvAj9Now6KqP+L1Q3JrM3iNhH/mXiOPTj6kxb9OnJbYsVYRlVdvRY1kCpU3Tz9VegIEi6MgDrviBaAnvB3aSw=="
},
"license-webpack-plugin": { "license-webpack-plugin": {
"version": "2.3.11", "version": "2.3.11",
"resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-2.3.11.tgz", "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-2.3.11.tgz",

View File

@ -35,6 +35,7 @@
"google-protobuf": "^3.13.0", "google-protobuf": "^3.13.0",
"grpc": "^1.24.5", "grpc": "^1.24.5",
"grpc-web": "^1.2.1", "grpc-web": "^1.2.1",
"libphonenumber-js": "^1.9.13",
"moment": "^2.29.1", "moment": "^2.29.1",
"ngx-quicklink": "^0.2.6", "ngx-quicklink": "^0.2.6",
"rxjs": "~6.6.3", "rxjs": "~6.6.3",

View File

@ -58,9 +58,11 @@
<a class="doc-link" href="https://docs.zitadel.ch" mat-stroked-button target="_blank">{{'MENU.DOCUMENTATION' <a class="doc-link" href="https://docs.zitadel.ch" mat-stroked-button target="_blank">{{'MENU.DOCUMENTATION'
| translate}}</a> | translate}}</a>
<div (clickOutside)="closeAccountCard()" class="icon-container"> <div (clickOutside)="closeAccountCard()" class="icon-container">
<app-avatar *ngIf="user && (user.displayName || (user.firstName && user.lastName))" <app-avatar
*ngIf="user && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))"
class="avatar dontcloseonclick" (click)="showAccount = !showAccount" [active]="showAccount" class="avatar dontcloseonclick" (click)="showAccount = !showAccount" [active]="showAccount"
[name]="user.displayName ? user.displayName : (user.firstName + ' '+ user.lastName)" [size]="38"> [name]="user.human.profile.displayName ? user.human.profile.displayName : (user.human.profile.firstName + ' '+ user.human.profile.lastName)"
[size]="38">
</app-avatar> </app-avatar>
<app-accounts-card @accounts class="a_card mat-elevation-z1" *ngIf="showAccount" <app-accounts-card @accounts class="a_card mat-elevation-z1" *ngIf="showAccount"
(close)="showAccount = false" [user]="user" [iamuser]="iamuser$ | async"> (close)="showAccount = false" [user]="user" [iamuser]="iamuser$ | async">

View File

@ -1,5 +1,5 @@
<div class="cnsl-app-card" [ngClass]="{'web': type == OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB, <div class="cnsl-app-card" [ngClass]="{'web': type == OIDCAppType.OIDC_APP_TYPE_WEB,
'useragent': type == OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT, 'useragent': type == OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
'native': type == OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE, 'api': isApiApp}"> 'native': type == OIDCAppType.OIDC_APP_TYPE_NATIVE, 'api': isApiApp}">
<ng-content></ng-content> <ng-content></ng-content>
</div> </div>

View File

@ -10,5 +10,5 @@ export class AppCardComponent {
@Input() public outline: boolean = false; @Input() public outline: boolean = false;
@Input() public type!: OIDCAppType; @Input() public type!: OIDCAppType;
@Input() public isApiApp: boolean = false; @Input() public isApiApp: boolean = false;
public OIDCApplicationType: any = OIDCAppType; public OIDCAppType: any = OIDCAppType;
} }

View File

@ -152,6 +152,7 @@ export class ChangesComponent implements OnInit, OnDestroy {
take(1), take(1),
tap((res: ListChanges) => { tap((res: ListChanges) => {
const values = res.resultList; const values = res.resultList;
console.log(values);
const mapped = this.mapChanges(values); const mapped = this.mapChanges(values);
// update source with new values, done loading // update source with new values, done loading
// this._data.next(values); // this._data.next(values);
@ -184,7 +185,7 @@ export class ChangesComponent implements OnInit, OnDestroy {
const userData: any = { const userData: any = {
editor: change.editorDisplayName, editor: change.editorDisplayName,
editorId: change.editorId, editorId: change.editorId,
editorName: change.editorDisplayName, editorDisplayName: change.editorDisplayName,
dates: [change.changeDate], dates: [change.changeDate],
// data: [change.data], // data: [change.data],
@ -205,7 +206,7 @@ export class ChangesComponent implements OnInit, OnDestroy {
{ {
editor: change.editorDisplayName, editor: change.editorDisplayName,
editorId: change.editorId, editorId: change.editorId,
editorName: change.editorDisplayName, editorDisplayName: change.editorDisplayName,
dates: [change.changeDate], dates: [change.changeDate],
// data: [change.data], // data: [change.data],

View File

@ -41,7 +41,7 @@
<ng-container matColumnDef="creationDate"> <ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MACHINE.CREATIONDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'USER.MACHINE.CREATIONDATE' | translate }} </th>
<td mat-cell *matCellDef="let key"> <td mat-cell *matCellDef="let key">
{{key.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}} {{key.details?.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}}
</td> </td>
</ng-container> </ng-container>

View File

@ -126,6 +126,7 @@ export class MachineKeysComponent implements OnInit {
if (this.userId) { if (this.userId) {
this.mgmtService.listMachineKeys(this.userId, limit, offset).then(resp => { this.mgmtService.listMachineKeys(this.userId, limit, offset).then(resp => {
this.keyResult = resp; this.keyResult = resp;
console.log(resp);
if (resp.resultList) { if (resp.resultList) {
this.dataSource.data = resp.resultList; this.dataSource.data = resp.resultList;
} }

View File

@ -12,6 +12,7 @@ import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy
import { AdminService } from 'src/app/services/admin.service'; import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { CnslLinks } from '../../links/links.component'; import { CnslLinks } from '../../links/links.component';
import { import {
IAM_LABEL_LINK, IAM_LABEL_LINK,
@ -76,6 +77,7 @@ export class PasswordComplexityPolicyComponent implements OnDestroy {
this.getData().then(data => { this.getData().then(data => {
if (data.policy) { if (data.policy) {
this.complexityData = data.policy; this.complexityData = data.policy;
console.log(data.policy);
this.loading = false; this.loading = false;
} }
}); });

View File

@ -6,7 +6,10 @@ import { ListProjectGrantMembersResponse, ListProjectMembersResponse } from 'src
import { Member } from 'src/app/proto/generated/zitadel/member_pb'; import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ProjectType } from './project-members.component'; export enum ProjectType {
PROJECTTYPE_OWNED = "OWNED",
PROJECTTYPE_GRANTED = "GRANTED"
}
/** /**
* Data source for the ProjectMembers view. This class should * Data source for the ProjectMembers view. This class should

View File

@ -11,12 +11,7 @@ import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { CreationType, MemberCreateDialogComponent } from '../add-member-dialog/member-create-dialog.component'; import { CreationType, MemberCreateDialogComponent } from '../add-member-dialog/member-create-dialog.component';
import { ProjectMembersDataSource } from './project-members-datasource'; import { ProjectMembersDataSource, ProjectType } from './project-members-datasource';
export enum ProjectType {
PROJECTTYPE_OWNED = "OWNED",
PROJECTTYPE_GRANTED = "GRANTED"
}
@Component({ @Component({
selector: 'app-project-members', selector: 'app-project-members',

View File

@ -52,7 +52,8 @@
<ng-container matColumnDef="creationDate"> <ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.CREATIONDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.CREATIONDATE' | translate }} </th>
<td (click)="openDetailDialog(role)" mat-cell *matCellDef="let role"> <td (click)="openDetailDialog(role)" mat-cell *matCellDef="let role">
<span>{{role.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span> <span *ngIf="role?.details?.creationDate">{{role.details.creationDate | timestampToDate |
localizedDate: 'dd. MMM, HH:mm' }}</span>
</td> </td>
</ng-container> </ng-container>

View File

@ -1,13 +1,17 @@
<div class="wrapper max-width-container"> <div class="wrapper max-width-container">
<div class="header" *ngIf="(authService.user | async) || {} as user"> <div class="header" *ngIf="(authService.user | async) || {} as user">
<app-avatar [routerLink]="['/users/me']" *ngIf="user && (user.displayName || (user.firstName && user.lastName))" <app-avatar [routerLink]="['/users/me']"
class="avatar" [name]="user.displayName ? user.displayName : (user.firstName + ' '+ user.lastName)" *ngIf="user && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))"
class="avatar"
[name]="user.human?.profile?.displayName ? user.human?.profile?.displayName : (user.human?.profile?.firstName + ' '+ user.human?.profile?.lastName)"
[size]="100"> [size]="100">
</app-avatar> </app-avatar>
<h3 *ngIf="(user.displayName || user.firstName || user.lastName); else loader"> <h3
*ngIf="user && (user.human?.profile?.displayName || user.human?.profile?.firstName || user.human?.profile?.lastName); else loader">
{{'HOME.WELCOME' | translate}}, {{'HOME.WELCOME' | translate}},
{{user?.displayName ? user.displayName : (user.firstName + ' '+ user.lastName)}}</h3> {{user?.human?.profile?.displayName ? user.human?.profile?.displayName : (user.human?.profile?.firstName + '
<p>{{user?.userName}}</p> '+ user.human?.profile?.lastName)}}</h3>
<p>{{user?.human?.profile?.userName}}</p>
<p class="wlc_stnce">{{'HOME.WELCOMESENTENCE' | translate}}</p> <p class="wlc_stnce">{{'HOME.WELCOMESENTENCE' | translate}}</p>
<ng-template #loader> <ng-template #loader>

View File

@ -3,7 +3,6 @@ import { MatDialog } from '@angular/material/dialog';
import { PageEvent } from '@angular/material/paginator'; import { PageEvent } from '@angular/material/paginator';
import { MatSelectChange } from '@angular/material/select'; import { MatSelectChange } from '@angular/material/select';
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component'; import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
import { ProjectType } from 'src/app/modules/project-members/project-members.component';
import { Member } from 'src/app/proto/generated/zitadel/member_pb'; import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { User } from 'src/app/proto/generated/zitadel/user_pb'; import { User } from 'src/app/proto/generated/zitadel/user_pb';
import { AdminService } from 'src/app/services/admin.service'; import { AdminService } from 'src/app/services/admin.service';
@ -18,7 +17,6 @@ import { IamMembersDataSource } from './iam-members-datasource';
}) })
export class IamMembersComponent { export class IamMembersComponent {
public INITIALPAGESIZE: number = 25; public INITIALPAGESIZE: number = 25;
public projectType: ProjectType = ProjectType.PROJECTTYPE_OWNED;
public dataSource!: IamMembersDataSource; public dataSource!: IamMembersDataSource;
public memberRoleOptions: string[] = []; public memberRoleOptions: string[] = [];

View File

@ -116,8 +116,8 @@
[password]="password"> [password]="password">
</app-password-complexity-view> </app-password-complexity-view>
<form [formGroup]="pwdForm" class="form"> <form [formGroup]="pwdForm" class="pwd-form">
<cnsl-form-field class="formfield" *ngIf="password" appearance="outline"> <cnsl-form-field class="pwd" *ngIf="password" appearance="outline">
<cnsl-label>{{ 'USER.PASSWORD.NEW' | translate }}</cnsl-label> <cnsl-label>{{ 'USER.PASSWORD.NEW' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="firstpassword" <input cnslInput autocomplete="off" name="firstpassword"
formControlName="password" type="password" /> formControlName="password" type="password" />
@ -127,7 +127,7 @@
</span> </span>
</cnsl-form-field> </cnsl-form-field>
<cnsl-form-field class="formfield" *ngIf="confirmPassword" appearance="outline"> <cnsl-form-field class="pwd" *ngIf="confirmPassword" appearance="outline">
<cnsl-label>{{ 'USER.PASSWORD.CONFIRM' | translate }}</cnsl-label> <cnsl-label>{{ 'USER.PASSWORD.CONFIRM' | translate }}</cnsl-label>
<input cnslInput autocomplete="off" name="confirmPassword" <input cnslInput autocomplete="off" name="confirmPassword"
formControlName="confirmPassword" type="password" /> formControlName="confirmPassword" type="password" />

View File

@ -110,6 +110,17 @@ h1 {
margin: 0 .5rem; margin: 0 .5rem;
} }
} }
.pwd-form {
display: flex;
flex-direction: row;
flex-wrap: wrap;
.pwd {
flex: 1;
margin: 0 .5rem;
}
}
} }
} }

View File

@ -95,7 +95,7 @@ export class OrgCreateComponent {
createOrgRequest.setDomain(this.domain?.value); createOrgRequest.setDomain(this.domain?.value);
const humanRequest: SetUpOrgRequest.Human = new SetUpOrgRequest.Human(); const humanRequest: SetUpOrgRequest.Human = new SetUpOrgRequest.Human();
humanRequest.setEmail(this.email?.value); humanRequest.setEmail(new SetUpOrgRequest.Human.Email().setEmail(this.email?.value));
humanRequest.setUserName(this.userName?.value); humanRequest.setUserName(this.userName?.value);
const profile: SetUpOrgRequest.Human.Profile = new SetUpOrgRequest.Human.Profile(); const profile: SetUpOrgRequest.Human.Profile = new SetUpOrgRequest.Human.Profile();
@ -105,7 +105,7 @@ export class OrgCreateComponent {
profile.setGender(this.gender?.value); profile.setGender(this.gender?.value);
profile.setPreferredLanguage(this.preferredLanguage?.value); profile.setPreferredLanguage(this.preferredLanguage?.value);
humanRequest.setProfile(this.firstName?.value); humanRequest.setProfile(profile);
if (this.usePassword && this.password) { if (this.usePassword && this.password) {
humanRequest.setPassword(this.password?.value); humanRequest.setPassword(this.password?.value);
} }

View File

@ -7,18 +7,18 @@
description="{{ 'ORG.DOMAINS.DESCRIPTION' | translate }}"> description="{{ 'ORG.DOMAINS.DESCRIPTION' | translate }}">
<div *ngFor="let domain of domains" class="domain"> <div *ngFor="let domain of domains" class="domain">
<span *ngIf="canwrite$ | async" (click)="verifyDomain(domain )" <span *ngIf="canwrite$ | async" (click)="verifyDomain(domain)"
class="title">{{domain.domain}}</span> class="title">{{domain.domainName}}</span>
<span *ngIf="(canwrite$ | async) == false" class="title disabled">{{domain.domain}}</span> <span *ngIf="(canwrite$ | async) == false" class="title disabled">{{domain?.domainName}}</span>
<i matTooltip="verified" *ngIf="domain.verified" class="verified las la-check-circle"></i> <i matTooltip="verified" *ngIf="domain.isVerified" class="verified las la-check-circle"></i>
<i matTooltip="primary" *ngIf="domain.primary" class="primary las la-star"></i> <i matTooltip="primary" *ngIf="domain.isPrimary" class="primary las la-star"></i>
<a *ngIf="!domain.primary && (canwrite$ | async)" class="primaryset" <a *ngIf="!domain.isPrimary && (canwrite$ | async)" class="primaryset"
(click)="setPrimary(domain)">{{'ORG.DOMAINS.SETPRIMARY' | translate}}</a> (click)="setPrimary(domain)">{{'ORG.DOMAINS.SETPRIMARY' | translate}}</a>
<span class="fill-space"></span> <span class="fill-space"></span>
<button class="rem-button" [disabled]="(canwrite$ | async) == false" matTooltip="Remove domain" <button class="rem-button" [disabled]="(canwrite$ | async) == false" matTooltip="Remove domain"
color="warn" mat-icon-button (click)="removeDomain(domain.domain)"><i color="warn" mat-icon-button (click)="removeDomain(domain.domainName)"><i
class="las la-trash"></i></button> class="las la-trash"></i></button>
</div> </div>
<p class="new-desc">{{'ORG.PAGES.ORGDOMAIN.VERIFICATION' | translate}}</p> <p class="new-desc">{{'ORG.PAGES.ORGDOMAIN.VERIFICATION' | translate}}</p>

View File

@ -10,13 +10,17 @@ import { debounceTime, takeUntil } from 'rxjs/operators';
import { RadioItemAuthType } from 'src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component'; import { RadioItemAuthType } from 'src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component';
import { import {
APIAuthMethodType, APIAuthMethodType,
App,
OIDCAppType, OIDCAppType,
OIDCAuthMethodType, OIDCAuthMethodType,
OIDCGrantType, OIDCGrantType,
OIDCResponseType, OIDCResponseType,
} from 'src/app/proto/generated/zitadel/app_pb'; } from 'src/app/proto/generated/zitadel/app_pb';
import { AddAPIAppRequest, AddOIDCAppRequest } from 'src/app/proto/generated/zitadel/management_pb'; import {
AddAPIAppRequest,
AddAPIAppResponse,
AddOIDCAppRequest,
AddOIDCAppResponse,
} from 'src/app/proto/generated/zitadel/management_pb';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@ -269,6 +273,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
private async getData({ projectid }: Params): Promise<void> { private async getData({ projectid }: Params): Promise<void> {
this.projectId = projectid; this.projectId = projectid;
console.log(this.projectId);
this.oidcAppRequest.projectId = projectid; this.oidcAppRequest.projectId = projectid;
this.apiAppRequest.projectId = projectid; this.apiAppRequest.projectId = projectid;
} }
@ -289,11 +294,11 @@ export class AppCreateComponent implements OnInit, OnDestroy {
.addOIDCApp(this.oidcAppRequest) .addOIDCApp(this.oidcAppRequest)
.then((resp) => { .then((resp) => {
this.loading = false; this.loading = false;
// if (resp.oidcConfig?.authMethodType !== OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE) { if (resp.clientId || resp.clientSecret) {
// this.showSavedDialog(resp); this.showSavedDialog(resp);
// } else { } else {
// this.router.navigate(['projects', this.projectId, 'apps', response.id]); this.router.navigate(['projects', this.projectId, 'apps', resp.appId]);
// } }
}) })
.catch(error => { .catch(error => {
this.loading = false; this.loading = false;
@ -305,12 +310,12 @@ export class AppCreateComponent implements OnInit, OnDestroy {
.addAPIApp(this.apiAppRequest) .addAPIApp(this.apiAppRequest)
.then((resp) => { .then((resp) => {
this.loading = false; this.loading = false;
// const response = resp.toObject();
// if (response.apiConfig?.authMethodType == APIAuthMethodType.APIAUTHMETHODTYPE_BASIC) { if (resp.clientId || resp.clientSecret) {
// this.showSavedDialog(resp); this.showSavedDialog(resp);
// } else { } else {
// this.router.navigate(['projects', this.projectId, 'apps', response.id]); this.router.navigate(['projects', this.projectId, 'apps', resp.appId]);
// } }
}) })
.catch(error => { .catch(error => {
this.loading = false; this.loading = false;
@ -319,27 +324,25 @@ export class AppCreateComponent implements OnInit, OnDestroy {
} }
} }
public showSavedDialog(app: App.AsObject): void { public showSavedDialog(added: AddOIDCAppResponse.AsObject | AddAPIAppResponse.AsObject): void {
if (app.oidcConfig?.clientSecret !== undefined) { let clientSecret = '';
const dialogRef = this.dialog.open(AppSecretDialogComponent, { if (added.clientSecret) {
data: app.oidcConfig, clientSecret = added.clientSecret;
});
dialogRef.afterClosed().subscribe(() => {
this.router.navigate(['projects', this.projectId, 'apps', app.id]);
});
} }
else if (app.apiConfig?.clientSecret !== undefined) { let clientId = '';
const dialogRef = this.dialog.open(AppSecretDialogComponent, { if (added.clientId) {
data: app.apiConfig, clientId = added.clientId;
});
dialogRef.afterClosed().subscribe(() => {
this.router.navigate(['projects', this.projectId, 'apps', app.id]);
});
} else {
this.router.navigate(['projects', this.projectId, 'apps', app.id]);
} }
const dialogRef = this.dialog.open(AppSecretDialogComponent, {
data: {
clientSecret: clientSecret,
clientId: clientId
}
});
dialogRef.afterClosed().subscribe(() => {
this.router.navigate(['projects', this.projectId, 'apps', added.appId]);
});
} }
get name(): AbstractControl | null { get name(): AbstractControl | null {

View File

@ -180,7 +180,7 @@
<cnsl-form-field appearance="outline" class="formfield"> <cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'APP.TYPE' | translate }}</cnsl-label> <cnsl-label>{{ 'APP.TYPE' | translate }}</cnsl-label>
<mat-select formControlName="applicationType"> <mat-select formControlName="appType">
<mat-option *ngFor="let type of oidcAppTypes" [value]="type"> <mat-option *ngFor="let type of oidcAppTypes" [value]="type">
{{ 'APP.OIDC.APPTYPE.'+type | translate }} {{ 'APP.OIDC.APPTYPE.'+type | translate }}
</mat-option> </mat-option>

View File

@ -7,7 +7,6 @@ import { BehaviorSubject, from, Observable, of, Subscription } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators'; import { catchError, finalize, map } from 'rxjs/operators';
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component'; import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
import { ChangeType } from 'src/app/modules/changes/changes.component'; import { ChangeType } from 'src/app/modules/changes/changes.component';
import { ProjectType } from 'src/app/modules/project-members/project-members.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource'; import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
import { Member } from 'src/app/proto/generated/zitadel/member_pb'; import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { GrantedProject, ProjectState } from 'src/app/proto/generated/zitadel/project_pb'; import { GrantedProject, ProjectState } from 'src/app/proto/generated/zitadel/project_pb';
@ -26,7 +25,6 @@ export class GrantedProjectDetailComponent implements OnInit, OnDestroy {
public project!: GrantedProject.AsObject; public project!: GrantedProject.AsObject;
public ProjectState: any = ProjectState; public ProjectState: any = ProjectState;
public ProjectType: any = ProjectType;
public ChangeType: any = ChangeType; public ChangeType: any = ChangeType;
private subscription?: Subscription; private subscription?: Subscription;

View File

@ -2,7 +2,6 @@ import { animate, animateChild, query, stagger, style, transition, trigger } fro
import { SelectionModel } from '@angular/cdk/collections'; import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ProjectType } from 'src/app/modules/project-members/project-members.component';
import { Org } from 'src/app/proto/generated/zitadel/org_pb'; import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { GrantedProject, ProjectState } from 'src/app/proto/generated/zitadel/project_pb'; import { GrantedProject, ProjectState } from 'src/app/proto/generated/zitadel/project_pb';
import { StorageKey, StorageService } from 'src/app/services/storage.service'; import { StorageKey, StorageService } from 'src/app/services/storage.service';
@ -41,7 +40,6 @@ export class GrantedProjectGridComponent implements OnChanges {
public showNewProject: boolean = false; public showNewProject: boolean = false;
public ProjectState: any = ProjectState; public ProjectState: any = ProjectState;
public ProjectType: any = ProjectType;
constructor(private storage: StorageService, private router: Router) { constructor(private storage: StorageService, private router: Router) {
this.selection.changed.subscribe(selection => { this.selection.changed.subscribe(selection => {

View File

@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from 'src/app/guards/auth.guard'; import { AuthGuard } from 'src/app/guards/auth.guard';
import { RoleGuard } from 'src/app/guards/role.guard'; import { RoleGuard } from 'src/app/guards/role.guard';
import { ProjectType } from 'src/app/modules/project-members/project-members.component'; import { ProjectType } from 'src/app/modules/project-members/project-members-datasource';
import { GrantedProjectDetailComponent } from './granted-project-detail/granted-project-detail.component'; import { GrantedProjectDetailComponent } from './granted-project-detail/granted-project-detail.component';
import { GrantedProjectsComponent } from './granted-projects.component'; import { GrantedProjectsComponent } from './granted-projects.component';

View File

@ -11,23 +11,20 @@
</div> </div>
<div [routerLink]="['/projects', projectId, 'apps', app.id ]" class="app-wrap" <div [routerLink]="['/projects', projectId, 'apps', app.id ]" class="app-wrap"
*ngFor="let app of appsSubject | async" *ngFor="let app of appsSubject | async" matTooltip="{{'ACTIONS.EDIT' | translate}}">
matTooltip="{{'ACTIONS.EDIT' | translate}}"> <cnsl-app-card class="grid-card" matRipple [type]="app.oidcConfig?.appType"
<cnsl-app-card class="grid-card" matRipple [type]="app.oidcConfig?.applicationType"
[isApiApp]="app.apiConfig !== undefined"> [isApiApp]="app.apiConfig !== undefined">
{{ app.name.charAt(0)}} {{ app.name.charAt(0)}}
<i *ngIf="app.oidcConfig?.applicationType == OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE" <ng-container *ngIf="app.oidcConfig?.appType !== undefined">
class="las la-mobile"></i> <i *ngIf="app.oidcConfig?.appType == OIDCAppType.OIDC_APP_TYPE_NATIVE" class="las la-mobile"></i>
<i *ngIf="app.oidcConfig?.applicationType == OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB" <i *ngIf="app.oidcConfig?.appType == OIDCAppType.OIDC_APP_TYPE_WEB" class="las la-code"></i>
class="las la-code"></i> <i *ngIf="app.oidcConfig?.appType == OIDCAppType.OIDC_APP_TYPE_USER_AGENT" class="las la-code"></i>
<i *ngIf="app.oidcConfig?.applicationType == OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT" <i *ngIf="app.apiConfig" class="las la-code"></i>
class="las la-code"></i> </ng-container>
<i *ngIf="app.apiConfig" class="las la-code"></i>
</cnsl-app-card> </cnsl-app-card>
<span class="name">{{app.name}}</span> <span class="name">{{app.name}}</span>
<span *ngIf="app.oidcConfig?.applicationType !== undefined && app.oidcConfig?.applicationType !== null" <span *ngIf="app.oidcConfig?.appType !== undefined && app.oidcConfig?.appType !== null" class="type">
class="type"> {{'APP.OIDC.APPTYPE.'+app.oidcConfig?.appType | translate}}</span>
{{'APP.OIDC.APPTYPE.'+app.oidcConfig?.applicationType | translate}}</span>
<span *ngIf="app.apiConfig !== undefined" class="type"> API</span> <span *ngIf="app.apiConfig !== undefined" class="type"> API</span>
</div> </div>

View File

@ -1,11 +1,9 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { BehaviorSubject, from, Observable, of } from 'rxjs'; import { BehaviorSubject, from, Observable } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators'; import { finalize, map } from 'rxjs/operators';
import { App, OIDCAppType } from 'src/app/proto/generated/zitadel/app_pb'; import { App, OIDCAppType } from 'src/app/proto/generated/zitadel/app_pb';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { NATIVE_TYPE, USER_AGENT_TYPE, WEB_TYPE } from '../../../apps/authtypes';
@Component({ @Component({
selector: 'app-application-grid', selector: 'app-application-grid',
templateUrl: './application-grid.component.html', templateUrl: './application-grid.component.html',
@ -18,11 +16,7 @@ export class ApplicationGridComponent implements OnInit {
public appsSubject: BehaviorSubject<App.AsObject[]> = new BehaviorSubject<App.AsObject[]>([]); public appsSubject: BehaviorSubject<App.AsObject[]> = new BehaviorSubject<App.AsObject[]>([]);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true); private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
public loading$: Observable<boolean> = this.loadingSubject.asObservable(); public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public OIDCApplicationType: any = OIDCAppType; public OIDCAppType: any = OIDCAppType;
public NATIVE_TYPE: any = NATIVE_TYPE;
public WEB_TYPE: any = WEB_TYPE;
public USER_AGENT_TYPE: any = USER_AGENT_TYPE;
constructor(private mgmtService: ManagementService) { } constructor(private mgmtService: ManagementService) { }
@ -33,11 +27,13 @@ export class ApplicationGridComponent implements OnInit {
public loadApps(): void { public loadApps(): void {
from(this.mgmtService.listApps(this.projectId, 100, 0)).pipe( from(this.mgmtService.listApps(this.projectId, 100, 0)).pipe(
map(resp => { map(resp => {
console.log(resp.resultList);
return resp.resultList; return resp.resultList;
}), }),
catchError(() => of([])), // catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)), finalize(() => this.loadingSubject.next(false)),
).subscribe((apps) => { ).subscribe((apps) => {
console.log(apps);
this.appsSubject.next(apps as App.AsObject[]); this.appsSubject.next(apps as App.AsObject[]);
}); });
} }

View File

@ -23,13 +23,13 @@
<button mat-stroked-button color="warn" <button mat-stroked-button color="warn"
[disabled]="isZitadel || (['project.write$', 'project.write:'+ project.id]| hasRole | async) == false" [disabled]="isZitadel || (['project.write$', 'project.write:'+ project.id]| hasRole | async) == false"
*ngIf="project?.state === ProjectState.PROJECTSTATE_ACTIVE" *ngIf="project?.state === ProjectState.PROJECT_STATE_ACTIVE"
(click)="changeState(ProjectState.PROJECTSTATE_INACTIVE)">{{'PROJECT.TABLE.DEACTIVATE' | (click)="changeState(ProjectState.PROJECT_STATE_INACTIVE)">{{'PROJECT.TABLE.DEACTIVATE' |
translate}}</button> translate}}</button>
<button mat-stroked-button color="warn" <button mat-stroked-button color="warn"
[disabled]="isZitadel || (['project.write$', 'project.write:'+ project.id]| hasRole | async) == false" [disabled]="isZitadel || (['project.write$', 'project.write:'+ project.id]| hasRole | async) == false"
*ngIf="project?.state === ProjectState.PROJECTSTATE_INACTIVE" *ngIf="project?.state === ProjectState.PROJECT_STATE_INACTIVE"
(click)="changeState(ProjectState.PROJECTSTATE_ACTIVE)">{{'PROJECT.TABLE.ACTIVATE' | (click)="changeState(ProjectState.PROJECT_STATE_ACTIVE)">{{'PROJECT.TABLE.ACTIVATE' |
translate}}</button> translate}}</button>
<div class="full-width"> <div class="full-width">
@ -67,8 +67,7 @@
</ng-template> </ng-template>
<ng-container *ngIf="isZitadel == false"> <ng-container *ngIf="isZitadel == false">
<ng-template appHasRole <ng-template appHasRole [appHasRole]="['project.grant.read:' + project.id, 'project.grant.read']">
[appHasRole]="['project.grant.read:' + project.id, 'project.grant.read']">
<app-card title="{{ 'PROJECT.GRANT.TITLE' | translate }}" <app-card title="{{ 'PROJECT.GRANT.TITLE' | translate }}"
description="{{ 'PROJECT.GRANT.DESCRIPTION' | translate }}"> description="{{ 'PROJECT.GRANT.DESCRIPTION' | translate }}">
<app-project-grants <app-project-grants
@ -120,7 +119,7 @@
<div class="meta-row"> <div class="meta-row">
<span class="first">{{'PROJECT.STATE.TITLE' | translate}}:</span> <span class="first">{{'PROJECT.STATE.TITLE' | translate}}:</span>
<span *ngIf="project && project.state !== undefined" class="state" <span *ngIf="project && project.state !== undefined" class="state"
[ngClass]="{'active': project.state === ProjectState.PROJECTSTATE_ACTIVE, 'inactive': project.state === ProjectState.PROJECTSTATE_INACTIVE}">{{'PROJECT.STATE.'+project.state [ngClass]="{'active': project.state === ProjectState.PROJECT_STATE_ACTIVE, 'inactive': project.state === ProjectState.PROJECT_STATE_INACTIVE}">{{'PROJECT.STATE.'+project.state
| translate}}</span> | translate}}</span>
</div> </div>
</div> </div>
@ -139,4 +138,4 @@
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
</div> </div>
</app-meta-layout> </app-meta-layout>

View File

@ -8,7 +8,6 @@ import { BehaviorSubject, from, Observable, of, Subscription } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators'; import { catchError, finalize, map } from 'rxjs/operators';
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component'; import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
import { ChangeType } from 'src/app/modules/changes/changes.component'; import { ChangeType } from 'src/app/modules/changes/changes.component';
import { ProjectType } from 'src/app/modules/project-members/project-members.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource'; import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { App } from 'src/app/proto/generated/zitadel/app_pb'; import { App } from 'src/app/proto/generated/zitadel/app_pb';
@ -35,7 +34,6 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
public appsColumns: string[] = ['name']; public appsColumns: string[] = ['name'];
public ProjectState: any = ProjectState; public ProjectState: any = ProjectState;
public ProjectType: any = ProjectType;
public ChangeType: any = ChangeType; public ChangeType: any = ChangeType;
public grid: boolean = true; public grid: boolean = true;

View File

@ -10,19 +10,19 @@
<p class="n-items" *ngIf="!loading && selection.selected.length > 0">{{'PROJECT.PAGES.PINNED' | translate}}</p> <p class="n-items" *ngIf="!loading && selection.selected.length > 0">{{'PROJECT.PAGES.PINNED' | translate}}</p>
<div class="item card" *ngFor="let item of selection.selected; index as i" <div class="item card" *ngFor="let item of selection.selected; index as i"
(click)="navigateToProject(item.projectId, $event)" (click)="navigateToProject(item.id, $event)"
[ngClass]="{ inactive: item.state !== ProjectState.PROJECTSTATE_ACTIVE}"> [ngClass]="{ inactive: item.state !== ProjectState.PROJECT_STATE_ACTIVE}">
<div class="text-part"> <div class="text-part">
<span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}} <span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
{{ {{
item.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' item.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span> }}</span>
<span class="name" *ngIf="item.name">{{ item.name }}</span> <span class="name" *ngIf="item.name">{{ item.name }}</span>
<span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}} <span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
{{ {{
item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span> }}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
</div> </div>
@ -34,20 +34,19 @@
<div class="container"> <div class="container">
<p class="n-items" *ngIf="!loading && notPinned.length > 0">{{'PROJECT.PAGES.ALL' | translate}}</p> <p class="n-items" *ngIf="!loading && notPinned.length > 0">{{'PROJECT.PAGES.ALL' | translate}}</p>
<div class="item card" *ngFor="let item of notPinned; index as i" <div class="item card" *ngFor="let item of notPinned; index as i" (click)="navigateToProject(item.id, $event)"
(click)="navigateToProject(item.projectId, $event)" [ngClass]="{ inactive: item.state !== ProjectState.PROJECT_STATE_ACTIVE}">
[ngClass]="{ inactive: item.state !== ProjectState.PROJECTSTATE_ACTIVE}">
<div class="text-part"> <div class="text-part">
<span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}} <span *ngIf="item.details?.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
{{ {{
item.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' item.details.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span> }}</span>
<span class="name" *ngIf="item.name">{{ item.name }}</span> <span class="name" *ngIf="item.name">{{ item.name }}</span>
<span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}} <span *ngIf="item.details.creationDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
{{ {{
item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' item.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span> }}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
</div> </div>
@ -66,7 +65,7 @@
</div> </div>
<ng-template #deleteButton let-key="key"> <ng-template #deleteButton let-key="key">
<button *ngIf="key.projectId !== zitadelProjectId" matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn" <button *ngIf="key.id !== zitadelProjectId" matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn"
(click)="deleteProject($event, key)" class="delete-button" mat-icon-button> (click)="deleteProject($event, key)" class="delete-button" mat-icon-button>
<i class="las la-trash"></i> <i class="las la-trash"></i>
</button> </button>

View File

@ -3,7 +3,6 @@ import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ProjectType } from 'src/app/modules/project-members/project-members.component';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { Org } from 'src/app/proto/generated/zitadel/org_pb'; import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { Project, ProjectState } from 'src/app/proto/generated/zitadel/project_pb'; import { Project, ProjectState } from 'src/app/proto/generated/zitadel/project_pb';
@ -51,7 +50,6 @@ export class OwnedProjectGridComponent implements OnChanges {
public showNewProject: boolean = false; public showNewProject: boolean = false;
public ProjectState: any = ProjectState; public ProjectState: any = ProjectState;
public ProjectType: any = ProjectType;
@Input() public zitadelProjectId: string = ''; @Input() public zitadelProjectId: string = '';
constructor( constructor(
private router: Router, private router: Router,

View File

@ -41,16 +41,16 @@
<ng-container matColumnDef="state"> <ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.STATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.STATE' | translate }} </th>
<td mat-cell *matCellDef="let project"><span <td mat-cell *matCellDef="let project"><span *ngIf="project.state">{{'PROJECT.STATE.'+project.state
*ngIf="project.state">{{'PROJECT.STATE.'+project.state | translate}}</span></td> | translate}}</span></td>
</ng-container> </ng-container>
<ng-container matColumnDef="creationDate"> <ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CREATIONDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CREATIONDATE' | translate }} </th>
<td mat-cell *matCellDef="let project"> <td mat-cell *matCellDef="let project">
<span <span *ngIf="project.details.creationDate">{{project.details.creationDate | timestampToDate |
*ngIf="project.creationDate">{{project.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}}</span> localizedDate: 'EEE dd. MMM, HH:mm'}}</span>
</td> </td>
</ng-container> </ng-container>
@ -58,8 +58,8 @@
<ng-container matColumnDef="changeDate"> <ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CHANGEDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CHANGEDATE' | translate }} </th>
<td mat-cell *matCellDef="let project"> <td mat-cell *matCellDef="let project">
<span <span *ngIf="project.details.changeDate">{{project.details.changeDate | timestampToDate |
*ngIf="project.changeDate">{{project.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}}</span> localizedDate: 'EEE dd. MMM, HH:mm'}}</span>
</td> </td>
</ng-container> </ng-container>

View File

@ -1,20 +1,20 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { GrantedProjectListComponent } from './granted-project-list.component'; import { OwnedProjectListComponent } from './owned-project-list.component';
describe('ProjectListComponent', () => { describe('OwnedProjectListComponent', () => {
let component: GrantedProjectListComponent; let component: OwnedProjectListComponent;
let fixture: ComponentFixture<GrantedProjectListComponent>; let fixture: ComponentFixture<OwnedProjectListComponent>;
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [GrantedProjectListComponent], declarations: [OwnedProjectListComponent],
}) })
.compileComponents(); .compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(GrantedProjectListComponent); fixture = TestBed.createComponent(OwnedProjectListComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -109,6 +109,7 @@ export class OwnedProjectListComponent implements OnInit, OnDestroy {
private async getData(limit?: number, offset?: number): Promise<void> { private async getData(limit?: number, offset?: number): Promise<void> {
this.loadingSubject.next(true); this.loadingSubject.next(true);
this.mgmtService.listProjects(limit, offset).then(resp => { this.mgmtService.listProjects(limit, offset).then(resp => {
console.log(resp.resultList);
this.ownedProjectList = resp.resultList; this.ownedProjectList = resp.resultList;
if (resp.details?.totalResult) { if (resp.details?.totalResult) {
this.totalResult = resp.details.totalResult; this.totalResult = resp.details.totalResult;

View File

@ -1,7 +1,7 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { RoleGuard } from 'src/app/guards/role.guard'; import { RoleGuard } from 'src/app/guards/role.guard';
import { ProjectType } from 'src/app/modules/project-members/project-members.component'; import { ProjectType } from 'src/app/modules/project-members/project-members-datasource';
import { OwnedProjectsComponent } from './owned-projects.component'; import { OwnedProjectsComponent } from './owned-projects.component';

View File

@ -3,7 +3,6 @@ import { MatDialog } from '@angular/material/dialog';
import { PageEvent } from '@angular/material/paginator'; import { PageEvent } from '@angular/material/paginator';
import { MatSelectChange } from '@angular/material/select'; import { MatSelectChange } from '@angular/material/select';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { ProjectType } from 'src/app/modules/project-members/project-members.component';
import { Member } from 'src/app/proto/generated/zitadel/member_pb'; import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { GrantedProject, ProjectGrantState, Role } from 'src/app/proto/generated/zitadel/project_pb'; import { GrantedProject, ProjectGrantState, Role } from 'src/app/proto/generated/zitadel/project_pb';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
@ -27,7 +26,6 @@ export class ProjectGrantDetailComponent {
public projectid: string = ''; public projectid: string = '';
public grantid: string = ''; public grantid: string = '';
public projectType: ProjectType = ProjectType.PROJECTTYPE_OWNED;
public disabled: boolean = false; public disabled: boolean = false;
public isZitadel: boolean = false; public isZitadel: boolean = false;

View File

@ -19,7 +19,7 @@
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}*</cnsl-label> <cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}*</cnsl-label>
<input cnslInput formControlName="userName" required <input cnslInput formControlName="userName" required
[ngStyle]="{'padding-right': suffixPadding ? suffixPadding : '10px'}" /> [ngStyle]="{'padding-right': suffixPadding ? suffixPadding : '10px'}" />
<span #suffix *ngIf="envSuffixLabel" cnslSuffix (click)="logsuff()">{{envSuffixLabel}}</span> <span #suffix *ngIf="envSuffixLabel" cnslSuffix>{{envSuffixLabel}}</span>
<span cnsl-error *ngIf="userName?.invalid && userName?.errors?.required"> <span cnsl-error *ngIf="userName?.invalid && userName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }} {{ 'USER.VALIDATION.REQUIRED' | translate }}
@ -88,8 +88,8 @@
</cnsl-form-field> </cnsl-form-field>
</div> </div>
<div class="btn-container"> <div class="btn-container">
<button color="primary" [disabled]="userForm.invalid" type="submit" <button color="primary" [disabled]="userForm.invalid" type="submit" mat-raised-button>{{ 'ACTIONS.CREATE' |
mat-raised-button>{{ 'ACTIONS.CREATE' | translate }}</button> translate }}</button>
</div> </div>
</form> </form>
</app-detail-layout> </app-detail-layout>

View File

@ -1,7 +1,9 @@
import { ChangeDetectorRef, Component, OnDestroy, ViewChild } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, ViewChild } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Subscription } from 'rxjs'; import parsePhoneNumber from 'libphonenumber-js';
import { Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
import { AddHumanUserRequest } from 'src/app/proto/generated/zitadel/management_pb'; import { AddHumanUserRequest } from 'src/app/proto/generated/zitadel/management_pb';
import { Domain } from 'src/app/proto/generated/zitadel/org_pb'; import { Domain } from 'src/app/proto/generated/zitadel/org_pb';
import { Gender } from 'src/app/proto/generated/zitadel/user_pb'; import { Gender } from 'src/app/proto/generated/zitadel/user_pb';
@ -37,7 +39,7 @@ export class UserCreateComponent implements OnDestroy {
public languages: string[] = ['de', 'en']; public languages: string[] = ['de', 'en'];
public userForm!: FormGroup; public userForm!: FormGroup;
public envSuffixLabel: string = ''; public envSuffixLabel: string = '';
private sub: Subscription = new Subscription(); private destroyed$: Subject<void> = new Subject();
public userLoginMustBeDomain: boolean = false; public userLoginMustBeDomain: boolean = false;
public loading: boolean = false; public loading: boolean = false;
@ -92,15 +94,23 @@ export class UserCreateComponent implements OnDestroy {
firstName: ['', Validators.required], firstName: ['', Validators.required],
lastName: ['', Validators.required], lastName: ['', Validators.required],
nickName: [''], nickName: [''],
gender: [Gender.GENDER_UNSPECIFIED], gender: [],
preferredLanguage: [''], preferredLanguage: [''],
phone: [''], phone: [''],
}); });
} this.userForm.controls['phone'].valueChanges.pipe(
takeUntil(this.destroyed$),
public logsuff(): void { debounceTime(300)).subscribe(value => {
console.log((this.suffix.nativeElement as HTMLElement), (this.suffix.nativeElement as HTMLElement).offsetWidth); const phoneNumber = parsePhoneNumber(value ?? '', 'CH');
if (phoneNumber) {
const formmatted = phoneNumber.formatInternational();
const country = phoneNumber.country;
if (this.phone && country && this.phone.value && this.phone.value !== formmatted) {
this.phone.setValue(formmatted);
}
}
});
} }
public createUser(): void { public createUser(): void {
@ -119,8 +129,11 @@ export class UserCreateComponent implements OnDestroy {
humanReq.setUserName(this.userName?.value); humanReq.setUserName(this.userName?.value);
humanReq.setProfile(profileReq); humanReq.setProfile(profileReq);
humanReq.setEmail(this.email?.value); humanReq.setEmail(new AddHumanUserRequest.Email().setEmail(this.email?.value));
humanReq.setPhone(this.phone?.value);
if (this.phone && this.phone.value) {
humanReq.setPhone(new AddHumanUserRequest.Phone().setPhone(this.phone.value));
}
this.mgmtService this.mgmtService
.addHumanUser(humanReq) .addHumanUser(humanReq)
@ -136,7 +149,8 @@ export class UserCreateComponent implements OnDestroy {
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.sub.unsubscribe(); this.destroyed$.next();
this.destroyed$.complete();
} }
public get email(): AbstractControl | null { public get email(): AbstractControl | null {

View File

@ -0,0 +1,50 @@
<h1 mat-dialog-title>
<span class="title">{{'USER.CODEDIALOG.TITLE' | translate}} {{data?.number}}</span>
</h1>
<p class="desc">{{'USER.CODEDIALOG.DESCRIPTION' | translate}}</p>
<div mat-dialog-content>
<div class="type-selection">
<button class="otp" (click)="selectType(AuthFactorType.OTP)">
<mat-icon class="icon" svgIcon="mdi_radar"></mat-icon>
<span>{{'USER.MFA.OTP' | translate}}</span>
</button>
<button class="u2f" (click)="selectType(AuthFactorType.U2F)">
<i class="las la-fingerprint"></i>
<span>{{'USER.MFA.U2F' | translate}}</span>
</button>
</div>
<div class="otp" *ngIf="selectedType == AuthFactorType.OTP">
<p>{{'USER.MFA.OTP_DIALOG_DESCRIPTION' | translate}}</p>
<div class="qrcode-wrapper">
<qrcode *ngIf="otpurl" class="qrcode" [qrdata]="otpurl" [width]="150" [errorCorrectionLevel]="'M'"></qrcode>
</div>
<cnsl-form-field class="form-field" label="Access Code" required="true">
<cnsl-label>Code</cnsl-label>
<input cnslInput [(ngModel)]="otpcode" />
</cnsl-form-field>
</div>
<div class="u2f" *ngIf="selectedType == AuthFactorType.U2F">
<p>{{'USER.MFA.U2F_DIALOG_DESCRIPTION' | translate}}</p>
<cnsl-form-field class="form-field" label="Name" required="true">
<cnsl-label>{{'USER.MFA.U2F_NAME' | translate}}</cnsl-label>
<input cnslInput [(ngModel)]="u2fname" required (keydown.enter)="u2fname ? submitU2F() : null" />
</cnsl-form-field>
<mat-spinner diameter="30" *ngIf="u2fLoading"></mat-spinner>
<p class="error">{{u2fError}}</p>
</div>
</div>
<div mat-dialog-actions class="action">
<button cdkFocusInitial color="primary" mat-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
</button>
<button cdkFocusInitial color="primary" mat-raised-button class="ok-button" (click)="submitAuth()">
{{'ACTIONS.CREATE' | translate}}
</button>
</div>

View File

@ -0,0 +1,35 @@
.type-selection {
display: flex;
margin: 0 -0.5rem;
.otp,
.u2f {
flex: 1;
min-height: 100px;
border-radius: .5rem;
border: 1px solid var(--grey);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 1rem;
margin: .5rem;
}
}
.formfield {
width: 100%;
}
.action {
display: flex;
justify-content: flex-end;
.ok-button {
margin-left: .5rem;
}
button {
border-radius: .5rem;
}
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AuthFactorDialogComponent } from './auth-factor-dialog.component';
describe('CodeDialogComponent', () => {
let component: AuthFactorDialogComponent;
let fixture: ComponentFixture<AuthFactorDialogComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AuthFactorDialogComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AuthFactorDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,138 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { take } from 'rxjs/operators';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
import { _base64ToArrayBuffer } from '../../u2f-util';
import { _arrayBufferToBase64 } from '../u2f_util';
export enum AuthFactorType {
OTP,
U2F,
}
@Component({
selector: 'app-auth-factor-dialog',
templateUrl: './auth-factor-dialog.component.html',
styleUrls: ['./auth-factor-dialog.component.scss'],
})
export class AuthFactorDialogComponent {
public otpurl: string = '';
public otpcode: string = '';
public u2fname: string = '';
public u2fCredentialOptions!: CredentialCreationOptions;
public u2fLoading: boolean = false;
public u2fError: string = '';
AuthFactorType: any = AuthFactorType;
selectedType!: AuthFactorType;
constructor(
private authService: GrpcAuthService,
private toast: ToastService,
private translate: TranslateService,
public dialogRef: MatDialogRef<AuthFactorDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
) { }
closeDialog(code: string = ''): void {
this.dialogRef.close(code);
}
public selectType(type: AuthFactorType): void {
if (type == AuthFactorType.OTP) {
this.authService.addMyMultiFactorOTP().then((otpresp) => {
this.otpurl = otpresp.url;
}, error => {
this.toast.showError(error);
});
} else if (type == AuthFactorType.U2F) {
this.authService.addMyMultiFactorU2F().then((u2fresp) => {
const credOptions: CredentialCreationOptions = JSON.parse(atob(u2fresp.key?.publicKey as string));
if (credOptions.publicKey?.challenge) {
credOptions.publicKey.challenge = _base64ToArrayBuffer(credOptions.publicKey.challenge as any);
credOptions.publicKey.user.id = _base64ToArrayBuffer(credOptions.publicKey.user.id as any);
if (credOptions.publicKey.excludeCredentials) {
credOptions.publicKey.excludeCredentials.map(cred => {
cred.id = _base64ToArrayBuffer(cred.id as any);
return cred;
});
}
this.u2fCredentialOptions = credOptions;
}
}, error => {
this.toast.showError(error);
});
}
}
public submitAuth() {
if (this.selectedType == AuthFactorType.OTP) {
this.submitOTP();
} else if (this.selectedType == AuthFactorType.U2F) {
this.submitU2F();
}
}
public submitOTP(): void {
this.authService.verifyMyMultiFactorOTP(this.otpcode).then(() => {
this.dialogRef.close(true);
}, error => {
this.dialogRef.close(false);
});
}
public submitU2F(): void {
if (this.u2fname && this.u2fCredentialOptions.publicKey) {
// this.data.credOptions.publicKey.rp.id = 'localhost';
navigator.credentials.create(this.data.credOptions).then((resp) => {
if (resp &&
(resp as any).response.attestationObject &&
(resp as any).response.clientDataJSON &&
(resp as any).rawId) {
const attestationObject = (resp as any).response.attestationObject;
const clientDataJSON = (resp as any).response.clientDataJSON;
const rawId = (resp as any).rawId;
const data = JSON.stringify({
id: resp.id,
rawId: _arrayBufferToBase64(rawId),
type: resp.type,
response: {
attestationObject: _arrayBufferToBase64(attestationObject),
clientDataJSON: _arrayBufferToBase64(clientDataJSON),
},
});
const base64 = btoa(data);
this.authService.verifyMyMultiFactorU2F(base64, this.u2fname).then(() => {
this.translate.get('USER.MFA.U2F_SUCCESS').pipe(take(1)).subscribe(msg => {
this.toast.showInfo(msg);
});
this.dialogRef.close(true);
this.u2fLoading = false;
}).catch(error => {
this.u2fLoading = false;
this.toast.showError(error);
});
} else {
this.u2fLoading = false;
this.translate.get('USER.MFA.U2F_ERROR').pipe(take(1)).subscribe(msg => {
this.toast.showInfo(msg);
});
this.dialogRef.close(true);
}
}).catch(error => {
this.u2fLoading = false;
this.u2fError = error;
this.toast.showInfo(error.message);
});
}
}
}

View File

@ -27,9 +27,10 @@
</div> </div>
</app-card> </app-card>
<app-card *ngIf="user" class="app-card" title="{{ 'USER.PROFILE.TITLE' | translate }}"> <app-card *ngIf="user && user.human?.profile?.userName" class=" app-card"
<app-detail-form [genders]="genders" [languages]="languages" [username]="user.userName" [user]="user.human" title="{{ 'USER.PROFILE.TITLE' | translate }}">
(changedLanguage)="changedLanguage($event)" (submitData)="saveProfile($event)"> <app-detail-form [genders]="genders" [languages]="languages" [username]="user.human?.profile?.userName"
[user]="user.human" (changedLanguage)="changedLanguage($event)" (submitData)="saveProfile($event)">
</app-detail-form> </app-detail-form>
</app-card> </app-card>

View File

@ -8,8 +8,7 @@ import { Email, Gender, Phone, Profile, User, UserState } from 'src/app/proto/ge
import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { EditDialogType } from '../user-detail/user-detail.component'; import { EditDialogComponent, EditDialogType } from './edit-dialog/edit-dialog.component';
import { EditDialogComponent } from './edit-dialog/edit-dialog.component';
@Component({ @Component({
selector: 'app-auth-user-detail', selector: 'app-auth-user-detail',
@ -169,7 +168,7 @@ export class AuthUserDetailComponent implements OnDestroy {
labelKey: 'ACTIONS.NEWVALUE', labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE', titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC', descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
value: this.user.human?.phone, value: this.user.human?.phone?.phone,
}, },
width: '400px', width: '400px',
}); });
@ -188,7 +187,7 @@ export class AuthUserDetailComponent implements OnDestroy {
labelKey: 'ACTIONS.NEWVALUE', labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE', titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC', descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
value: this.user.human?.email, value: this.user.human?.email?.email,
}, },
width: '400px', width: '400px',
}); });

View File

@ -39,14 +39,10 @@
</table> </table>
</app-refresh-table> </app-refresh-table>
<div class="add-row"> <div class="add-row">
<button class="button" *ngIf="otpAvailable" (click)="addOTP()" mat-stroked-button color="primary" <button class="button" *ngIf="otpAvailable" (click)="addAuthFactor()" mat-raised-button color="primary"
matTooltip="{{'ACTIONS.NEW' | translate}}"> matTooltip="{{'ACTIONS.NEW' | translate}}">
<mat-icon class="icon" svgIcon="mdi_radar"></mat-icon>{{'USER.MFA.OTP' | translate}} <mat-icon class="icon">add</mat-icon>
</button> {{'USER.MFA.OTP' | translate}}
<button class="button" (click)="addU2F()" mat-stroked-button color="primary"
matTooltip="{{'ACTIONS.NEW' | translate}}">
<i class="las la-fingerprint"></i>
{{'USER.MFA.U2F' | translate}}
</button> </button>
</div> </div>
<div class="table-wrapper"> <div class="table-wrapper">

View File

@ -8,9 +8,7 @@ import { AuthFactor, AuthFactorState } from 'src/app/proto/generated/zitadel/use
import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { _base64ToArrayBuffer } from '../../u2f-util'; import { AuthFactorDialogComponent } from '../auth-factor-dialog/auth-factor-dialog.component';
import { DialogOtpComponent } from '../dialog-otp/dialog-otp.component';
import { DialogU2FComponent, U2FComponentDestination } from '../dialog-u2f/dialog-u2f.component';
export interface WebAuthNOptions { export interface WebAuthNOptions {
challenge: string; challenge: string;
@ -55,59 +53,17 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
this.loadingSubject.complete(); this.loadingSubject.complete();
} }
public addOTP(): void { public addAuthFactor(): void {
this.service.addMyMultiFactorOTP().then((otpresp) => { const dialogRef = this.dialog.open(AuthFactorDialogComponent, {
const otp = otpresp; width: '400px',
const dialogRef = this.dialog.open(DialogOtpComponent, {
data: otp.url,
width: '400px',
});
dialogRef.afterClosed().subscribe((code) => {
if (code) {
this.service.verifyMyMultiFactorOTP(code).then(() => {
this.getMFAs();
});
}
});
}, error => {
this.toast.showError(error);
}); });
}
public addU2F(): void { dialogRef.afterClosed().subscribe((code) => {
this.service.addMyMultiFactorU2F().then((u2fresp) => { if (code) {
const credOptions: CredentialCreationOptions = JSON.parse(atob(u2fresp.key?.publicKey as string)); this.service.verifyMyMultiFactorOTP(code).then(() => {
this.getMFAs();
if (credOptions.publicKey?.challenge) {
credOptions.publicKey.challenge = _base64ToArrayBuffer(credOptions.publicKey.challenge as any);
credOptions.publicKey.user.id = _base64ToArrayBuffer(credOptions.publicKey.user.id as any);
if (credOptions.publicKey.excludeCredentials) {
credOptions.publicKey.excludeCredentials.map(cred => {
cred.id = _base64ToArrayBuffer(cred.id as any);
return cred;
});
}
console.log(credOptions);
const dialogRef = this.dialog.open(DialogU2FComponent, {
width: '400px',
data: {
credOptions,
type: U2FComponentDestination.MFA,
},
});
dialogRef.afterClosed().subscribe(done => {
if (done) {
this.getMFAs();
} else {
this.getMFAs();
}
}); });
} }
}, error => {
this.toast.showError(error);
}); });
} }

View File

@ -1,18 +0,0 @@
<h1 mat-dialog-title>{{'USER.MFA.OTP_DIALOG_TITLE' | translate}}</h1>
<div mat-dialog-content>
<p>{{'USER.MFA.OTP_DIALOG_DESCRIPTION' | translate}}</p>
<div class="qrcode-wrapper">
<qrcode *ngIf="data" class="qrcode" [qrdata]="data" [width]="150" [errorCorrectionLevel]="'M'"></qrcode>
</div>
<cnsl-form-field class="form-field" label="Access Code" required="true">
<cnsl-label>Code</cnsl-label>
<input cnslInput [(ngModel)]="code" />
</cnsl-form-field>
</div>
<div mat-dialog-actions class="action">
<button mat-button (click)="closeDialog()"><span translate>ACTIONS.CLOSE</span></button>
<button mat-raised-button class="ok-button" color="primary" (click)="closeDialogWithCode()"><span
translate>ACTIONS.OK</span>
</button>
</div>

View File

@ -1,22 +0,0 @@
.qrcode-wrapper {
display: flex;
align-content: center;
.qrcode {
margin: 1rem auto;
}
}
.form-field {
width: 100%;
}
.action {
display: flex;
justify-content: flex-end;
button {
margin-left: .5rem;
border-radius: .5rem;
}
}

View File

@ -1,21 +0,0 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-dialog-otp',
templateUrl: './dialog-otp.component.html',
styleUrls: ['./dialog-otp.component.scss'],
})
export class DialogOtpComponent {
public code: string = '';
constructor(public dialogRef: MatDialogRef<DialogOtpComponent>,
@Inject(MAT_DIALOG_DATA) public data: string) { }
public closeDialog(): void {
this.dialogRef.close();
}
public closeDialogWithCode(): void {
this.dialogRef.close(this.code);
}
}

View File

@ -5,17 +5,7 @@ import { take } from 'rxjs/operators';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
export function _arrayBufferToBase64(buffer: any): string { import { _arrayBufferToBase64 } from '../u2f_util';
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
export enum U2FComponentDestination { export enum U2FComponentDestination {
MFA = 'mfa', MFA = 'mfa',

View File

@ -4,8 +4,9 @@
<p class="desc">{{data.descriptionKey | translate}}</p> <p class="desc">{{data.descriptionKey | translate}}</p>
<div mat-dialog-content> <div mat-dialog-content>
<cnsl-form-field class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{data.labelKey | translate }}</cnsl-label> <cnsl-label>{{data.labelKey | translate }} <span *ngIf="phoneCountry">({{ phoneCountry }})</span></cnsl-label>
<input cnslInput [(ngModel)]="value" (keydown.enter)="value ? closeDialogWithValue(value) : null" /> <input cnslInput [(ngModel)]="value" (change)="changeValue($event)"
(keydown.enter)="value ? closeDialogWithValue(value) : null" />
</cnsl-form-field> </cnsl-form-field>
</div> </div>
<div mat-dialog-actions class="action"> <div mat-dialog-actions class="action">

View File

@ -1,5 +1,11 @@
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { parsePhoneNumber } from 'libphonenumber-js';
export enum EditDialogType {
PHONE = 1,
EMAIL = 2,
}
@Component({ @Component({
selector: 'app-edit-email-dialog', selector: 'app-edit-email-dialog',
@ -8,9 +14,26 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
}) })
export class EditDialogComponent { export class EditDialogComponent {
public value: string = ''; public value: string = '';
public isPhone: boolean = false;
public phoneCountry: string = 'CH';
constructor(public dialogRef: MatDialogRef<EditDialogComponent>, constructor(public dialogRef: MatDialogRef<EditDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) { @Inject(MAT_DIALOG_DATA) public data: any) {
this.value = data.value; this.value = data.value;
if (data.type == EditDialogType.PHONE) {
this.isPhone = true;
}
}
changeValue(change: any) {
const value = change.target.value;
if (this.isPhone && value) {
const phoneNumber = parsePhoneNumber(value ?? '', 'CH');
if (phoneNumber) {
const formmatted = phoneNumber.formatInternational();
this.phoneCountry = phoneNumber.country || '';
this.value = formmatted;
}
}
} }
closeDialog(email: string = ''): void { closeDialog(email: string = ''): void {

View File

@ -0,0 +1,12 @@
export function _arrayBufferToBase64(buffer: any): string {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}

View File

@ -18,10 +18,10 @@
<div class="method-row"> <div class="method-row">
<div class="left"> <div class="left">
<span class="label">{{ 'USER.EMAIL' | translate }}</span> <span class="label">{{ 'USER.EMAIL' | translate }}</span>
<span class="name">{{human?.email}}</span> <span class="name">{{human?.email?.email}}</span>
<span *ngIf="human?.profile.email.isEmailVerified" class="contact-state verified">{{'USER.EMAILVERIFIED' | <span *ngIf="human?.email?.isEmailVerified" class="contact-state verified">{{'USER.EMAILVERIFIED' |
translate}}</span> translate}}</span>
<div *ngIf="!human?.profile.email.isEmailVerified" class="block"> <div *ngIf="!human?.email.isEmailVerified" class="block">
<span class="contact-state notverified">{{'USER.NOTVERIFIED' | translate}}</span> <span class="contact-state notverified">{{'USER.NOTVERIFIED' | translate}}</span>
<ng-container *ngIf="human?.email"> <ng-container *ngIf="human?.email">
@ -45,13 +45,13 @@
<div class="method-row"> <div class="method-row">
<div class="left"> <div class="left">
<span class="label">{{ 'USER.PHONE' | translate }}</span> <span class="label">{{ 'USER.PHONE' | translate }}</span>
<span class="name">{{human?.phone ? human.phone : ('USER.PHONEEMPTY' | translate)}}</span> <span class="name">{{human?.phone?.phone ? human.phone?.phone : ('USER.PHONEEMPTY' | translate)}}</span>
<span *ngIf="human?.profile.phone.isPhoneVerified" class="contact-state verified">{{'USER.PHONEVERIFIED' | <span *ngIf="human?.phone.isPhoneVerified" class="contact-state verified">{{'USER.PHONEVERIFIED' |
translate}}</span> translate}}</span>
<div *ngIf="!human?.profile.phone.isPhoneVerified" class="block"> <div *ngIf="human.phone?.phone && !human?.phone.isPhoneVerified" class="block">
<span class="contact-state notverified">{{'USER.NOTVERIFIED' | translate}}</span> <span class="contact-state notverified">{{'USER.NOTVERIFIED' | translate}}</span>
<ng-container *ngIf="human?.phone"> <ng-container *ngIf="human?.phone?.phone">
<a *ngIf="!disablePhoneCode && canWrite" class="verify" <a *ngIf="!disablePhoneCode && canWrite" class="verify"
matTooltip="{{'USER.LOGINMETHODS.ENTERCODE_DESC' | translate}}" matTooltip="{{'USER.LOGINMETHODS.ENTERCODE_DESC' | translate}}"
(click)="enterCode()">{{'USER.LOGINMETHODS.ENTERCODE' | translate}}</a> (click)="enterCode()">{{'USER.LOGINMETHODS.ENTERCODE' | translate}}</a>
@ -64,7 +64,7 @@
</div> </div>
<div class="right"> <div class="right">
<button matTooltip="{{'ACTIONS.DELETE' | translate}}" *ngIf="human && human.phone" color="warn" <button matTooltip="{{'ACTIONS.DELETE' | translate}}" *ngIf="human && human.phone?.phone" color="warn"
(click)="emitDeletePhone()" mat-icon-button> (click)="emitDeletePhone()" mat-icon-button>
<i class="las la-trash"></i> <i class="las la-trash"></i>
</button> </button>

View File

@ -4,7 +4,7 @@ import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.com
import { Human, UserState } from 'src/app/proto/generated/zitadel/user_pb'; import { Human, UserState } from 'src/app/proto/generated/zitadel/user_pb';
import { CodeDialogComponent } from '../auth-user-detail/code-dialog/code-dialog.component'; import { CodeDialogComponent } from '../auth-user-detail/code-dialog/code-dialog.component';
import { EditDialogType } from '../user-detail/user-detail.component'; import { EditDialogType } from '../auth-user-detail/edit-dialog/edit-dialog.component';
@Component({ @Component({
selector: 'app-contact', selector: 'app-contact',

View File

@ -1,7 +1,7 @@
import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { Gender, User } from 'src/app/proto/generated/zitadel/user_pb'; import { Gender, Human, User } from 'src/app/proto/generated/zitadel/user_pb';
@Component({ @Component({
@ -11,7 +11,7 @@ import { Gender, User } from 'src/app/proto/generated/zitadel/user_pb';
}) })
export class DetailFormComponent implements OnDestroy, OnChanges { export class DetailFormComponent implements OnDestroy, OnChanges {
@Input() public username!: string; @Input() public username!: string;
@Input() public user!: User; @Input() public user!: Human.AsObject;
@Input() public disabled: boolean = false; @Input() public disabled: boolean = false;
@Input() public genders: Gender[] = []; @Input() public genders: Gender[] = [];
@Input() public languages: string[] = ['de', 'en']; @Input() public languages: string[] = ['de', 'en'];
@ -47,7 +47,7 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
preferredLanguage: [{ value: '', disabled: this.disabled }], preferredLanguage: [{ value: '', disabled: this.disabled }],
}); });
this.profileForm.patchValue({ userName: this.username, ...this.user }); this.profileForm.patchValue({ userName: this.username, ...this.user.profile });
if (this.preferredLanguage) { if (this.preferredLanguage) {
this.sub = this.preferredLanguage.valueChanges.subscribe(value => { this.sub = this.preferredLanguage.valueChanges.subscribe(value => {

View File

@ -19,6 +19,7 @@ import { CardModule } from 'src/app/modules/card/card.module';
import { ChangesModule } from 'src/app/modules/changes/changes.module'; import { ChangesModule } from 'src/app/modules/changes/changes.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module'; import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { InputModule } from 'src/app/modules/input/input.module'; import { InputModule } from 'src/app/modules/input/input.module';
import { MachineKeysModule } from 'src/app/modules/machine-keys/machine-keys.module';
import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module'; import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
import { PasswordComplexityViewModule } from 'src/app/modules/password-complexity-view/password-complexity-view.module'; import { PasswordComplexityViewModule } from 'src/app/modules/password-complexity-view/password-complexity-view.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module'; import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
@ -29,11 +30,11 @@ import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.mod
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module'; import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module'; import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { AuthFactorDialogComponent } from './auth-user-detail/auth-factor-dialog/auth-factor-dialog.component';
import { AuthPasswordlessComponent } from './auth-user-detail/auth-passwordless/auth-passwordless.component'; import { AuthPasswordlessComponent } from './auth-user-detail/auth-passwordless/auth-passwordless.component';
import { AuthUserDetailComponent } from './auth-user-detail/auth-user-detail.component'; import { AuthUserDetailComponent } from './auth-user-detail/auth-user-detail.component';
import { AuthUserMfaComponent } from './auth-user-detail/auth-user-mfa/auth-user-mfa.component'; import { AuthUserMfaComponent } from './auth-user-detail/auth-user-mfa/auth-user-mfa.component';
import { CodeDialogComponent } from './auth-user-detail/code-dialog/code-dialog.component'; import { CodeDialogComponent } from './auth-user-detail/code-dialog/code-dialog.component';
import { DialogOtpComponent } from './auth-user-detail/dialog-otp/dialog-otp.component';
import { DialogU2FComponent } from './auth-user-detail/dialog-u2f/dialog-u2f.component'; import { DialogU2FComponent } from './auth-user-detail/dialog-u2f/dialog-u2f.component';
import { EditDialogComponent } from './auth-user-detail/edit-dialog/edit-dialog.component'; import { EditDialogComponent } from './auth-user-detail/edit-dialog/edit-dialog.component';
import { ResendEmailDialogComponent } from './auth-user-detail/resend-email-dialog/resend-email-dialog.component'; import { ResendEmailDialogComponent } from './auth-user-detail/resend-email-dialog/resend-email-dialog.component';
@ -48,13 +49,11 @@ import { UserDetailRoutingModule } from './user-detail-routing.module';
import { PasswordlessComponent } from './user-detail/passwordless/passwordless.component'; import { PasswordlessComponent } from './user-detail/passwordless/passwordless.component';
import { UserDetailComponent } from './user-detail/user-detail.component'; import { UserDetailComponent } from './user-detail/user-detail.component';
import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component'; import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
import { MachineKeysModule } from 'src/app/modules/machine-keys/machine-keys.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
AuthUserDetailComponent, AuthUserDetailComponent,
UserDetailComponent, UserDetailComponent,
DialogOtpComponent,
EditDialogComponent, EditDialogComponent,
AuthUserMfaComponent, AuthUserMfaComponent,
AuthPasswordlessComponent, AuthPasswordlessComponent,
@ -68,6 +67,7 @@ import { MachineKeysModule } from 'src/app/modules/machine-keys/machine-keys.mod
ContactComponent, ContactComponent,
ResendEmailDialogComponent, ResendEmailDialogComponent,
DialogU2FComponent, DialogU2FComponent,
AuthFactorDialogComponent,
], ],
imports: [ imports: [
UserDetailRoutingModule, UserDetailRoutingModule,

View File

@ -12,14 +12,9 @@ import { Email, Gender, Machine, Phone, Profile, User, UserState } from 'src/app
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { EditDialogComponent } from '../auth-user-detail/edit-dialog/edit-dialog.component'; 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 { ResendEmailDialogComponent } from '../auth-user-detail/resend-email-dialog/resend-email-dialog.component';
export enum EditDialogType {
PHONE = 1,
EMAIL = 2,
}
@Component({ @Component({
selector: 'app-user-detail', selector: 'app-user-detail',
templateUrl: './user-detail.component.html', templateUrl: './user-detail.component.html',
@ -87,6 +82,7 @@ export class UserDetailComponent implements OnInit {
} }
public saveProfile(profileData: Profile.AsObject): void { public saveProfile(profileData: Profile.AsObject): void {
console.log(profileData);
if (this.user.human) { if (this.user.human) {
this.user.human.profile = profileData; this.user.human.profile = profileData;
this.mgmtUserService this.mgmtUserService
@ -252,7 +248,8 @@ export class UserDetailComponent implements OnInit {
labelKey: 'ACTIONS.NEWVALUE', labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE', titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC', descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
value: this.user.human?.phone, value: this.user.human?.phone?.phone,
type: EditDialogType.PHONE,
}, },
width: '400px', width: '400px',
}); });
@ -271,7 +268,8 @@ export class UserDetailComponent implements OnInit {
labelKey: 'ACTIONS.NEWVALUE', labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE', titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC', descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
value: this.user.human?.email, value: this.user.human?.email?.email,
type: EditDialogType.EMAIL,
}, },
width: '400px', width: '400px',
}); });

View File

@ -32,9 +32,10 @@
<td mat-cell *matCellDef="let user" class="selection"> <td mat-cell *matCellDef="let user" class="selection">
<mat-checkbox [disabled]="disabled" color="primary" (click)="$event.stopPropagation()" <mat-checkbox [disabled]="disabled" color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(user) : null" [checked]="selection.isSelected(user)"> (change)="$event ? selection.toggle(user) : null" [checked]="selection.isSelected(user)">
<app-avatar <app-avatar
*ngIf="user[type] && user[type].displayName && user[type]?.firstName && user[type]?.lastName; else cog" *ngIf="user.human && user.human.profile.displayName && user.human?.profile.firstName && user.human?.profile.lastName; else cog"
class="avatar" [name]="user[type].displayName" [size]="32"> class="avatar" [name]="user.human.profile.displayName" [size]="32">
</app-avatar> </app-avatar>
<ng-template #cog> <ng-template #cog>
<div class="sa-icon"> <div class="sa-icon">
@ -45,28 +46,6 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="firstname">
<th mat-header-cell *matHeaderCellDef
[ngClass]="{'search-active': this.userSearchKey == UserListSearchKey.FIRST_NAME}">
{{ 'USER.PROFILE.FIRSTNAME' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserListSearchKey.FIRST_NAME}"></template>
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
{{user[type]?.firstName}} </td>
</ng-container>
<ng-container matColumnDef="lastname">
<th mat-header-cell *matHeaderCellDef
[ngClass]="{'search-active': this.userSearchKey == UserListSearchKey.LAST_NAME}">
{{ 'USER.PROFILE.LASTNAME' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserListSearchKey.LAST_NAME}"></template>
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
{{user[type]?.lastName}} </td>
</ng-container>
<ng-container matColumnDef="displayName"> <ng-container matColumnDef="displayName">
<th mat-header-cell *matHeaderCellDef <th mat-header-cell *matHeaderCellDef
[ngClass]="{'search-active': this.userSearchKey == UserListSearchKey.USERSEARCHKEY_DISPLAY_NAME}"> [ngClass]="{'search-active': this.userSearchKey == UserListSearchKey.USERSEARCHKEY_DISPLAY_NAME}">
@ -75,7 +54,8 @@
[ngTemplateOutletContext]="{key: UserListSearchKey.USERSEARCHKEY_DISPLAY_NAME}"></template> [ngTemplateOutletContext]="{key: UserListSearchKey.USERSEARCHKEY_DISPLAY_NAME}"></template>
</th> </th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null"> <td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
{{user[type]?.displayName}} </td> {{user.human?.profile?.displayName}}
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
@ -83,13 +63,17 @@
{{ 'USER.MACHINE.NAME' | translate }} {{ 'USER.MACHINE.NAME' | translate }}
</th> </th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null"> <td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
{{user[type]?.name}} </td> <span *ngIf="user.human?.name">{{user.human?.name}}</span>
<span *ngIf="user.machine?.name">{{user.machine?.name}}</span>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="description"> <ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MACHINE.DESCRIPTION' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'USER.MACHINE.DESCRIPTION' | translate }} </th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null"> <td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
{{user[type]?.description}} </td> <span *ngIf="user.human?.description">{{user.human?.description}}</span>
<span *ngIf="user.machine?.description">{{user.machine?.description}}</span>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="username"> <ng-container matColumnDef="username">
@ -111,15 +95,17 @@
[ngTemplateOutletContext]="{key: UserListSearchKey.EMAIL}"></template> [ngTemplateOutletContext]="{key: UserListSearchKey.EMAIL}"></template>
</th> </th>
<td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null"> <td mat-cell *matCellDef="let user" [routerLink]="user.id ? ['/users', user.id ]: null">
{{user[type]?.email}} </td> <span *ngIf="user.human?.email?.email">{{user.human?.email.email}}</span>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="state"> <ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.DATA.STATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ '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.id ? ['/users', user.id ]: null">
<span class="state" <span class="state"
[ngClass]="{'active': user.state === UserState.USERSTATE_ACTIVE, 'inactive': user.state === UserState.USERSTATE_INACTIVE}">{{ [ngClass]="{'active': user.state === UserState.USER_STATE_ACTIVE, 'inactive': user.state === UserState.USER_STATE_INACTIVE}">
'USER.DATA.STATE'+user.state | translate }}</span> {{ 'USER.DATA.STATE'+user.state | translate }}
</span>
</td> </td>
</ng-container> </ng-container>

View File

@ -180,6 +180,8 @@ export class UserTableComponent implements OnInit {
} }
this.userService.listUsers(limit, offset, [query]).then(resp => { this.userService.listUsers(limit, offset, [query]).then(resp => {
console.log(resp);
if (resp.details?.totalResult) { if (resp.details?.totalResult) {
this.totalResult = resp.details?.totalResult; this.totalResult = resp.details?.totalResult;
} }

View File

@ -11,8 +11,10 @@ export class TimestampToDatePipe implements PipeTransform {
} }
private dateFromTimestamp(date: Timestamp.AsObject): any { private dateFromTimestamp(date: Timestamp.AsObject): any {
const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000 / 1000); if (date?.seconds !== undefined && date?.nanos !== undefined) {
return ts; const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000 / 1000);
return ts;
}
} }
} }

View File

@ -1556,7 +1556,6 @@ export class ManagementService {
if (offset) { if (offset) {
query.setOffset(offset); query.setOffset(offset);
} }
req.setQuery(query); req.setQuery(query);
if (queryList) { if (queryList) {
req.setQueriesList(queryList); req.setQueriesList(queryList);

View File

@ -1,7 +1,7 @@
{ {
"authServiceUrl": "https://api.zitadel.dev", "authServiceUrl": "https://api.zitadel.io",
"mgmtServiceUrl": "https://api.zitadel.dev", "mgmtServiceUrl": "https://api.zitadel.io",
"adminServiceUrl":"https://api.zitadel.dev", "adminServiceUrl":"https://api.zitadel.io",
"issuer": "https://issuer.zitadel.dev", "issuer": "https://issuer.zitadel.io",
"clientid": "70669160379706195@zitadel" "clientid": "98804911164221523@zitadel"
} }