feat: Use V2 API's in Console (#9312)

# Which Problems Are Solved
Solves #8976

# Additional Changes
I have done some intensive refactorings and we are using the new
@zitadel/client package for GRPC access.

# Additional Context
- Closes #8976

---------

Co-authored-by: Max Peintner <peintnerm@gmail.com>
This commit is contained in:
Ramon
2025-02-17 19:25:46 +01:00
committed by GitHub
parent ad225836d5
commit 3042bbb993
90 changed files with 3679 additions and 2315 deletions

View File

@@ -1,19 +1,19 @@
import { Injectable } from '@angular/core';
import { GrpcService } from './grpc.service';
import {
GetInstanceFeaturesRequest,
GetInstanceFeaturesResponse,
ResetInstanceFeaturesRequest,
SetInstanceFeaturesRequest,
SetInstanceFeaturesResponse,
} from '../proto/generated/zitadel/feature/v2beta/instance_pb';
import {
GetOrganizationFeaturesRequest,
GetOrganizationFeaturesResponse,
} from '../proto/generated/zitadel/feature/v2beta/organization_pb';
import { GetUserFeaturesRequest, GetUserFeaturesResponse } from '../proto/generated/zitadel/feature/v2beta/user_pb';
import { GetSystemFeaturesRequest, GetSystemFeaturesResponse } from '../proto/generated/zitadel/feature/v2beta/system_pb';
import {
GetInstanceFeaturesRequest,
GetInstanceFeaturesResponse,
ResetInstanceFeaturesRequest,
SetInstanceFeaturesRequest,
SetInstanceFeaturesResponse,
} from '../proto/generated/zitadel/feature/v2/instance_pb';
@Injectable({
providedIn: 'root',

View File

@@ -1,8 +1,22 @@
import { Injectable } from '@angular/core';
import { SortDirection } from '@angular/material/sort';
import { OAuthService } from 'angular-oauth2-oidc';
import { BehaviorSubject, forkJoin, from, Observable, of, Subject } from 'rxjs';
import { catchError, distinctUntilChanged, filter, finalize, map, switchMap, timeout, withLatestFrom } from 'rxjs/operators';
import {
BehaviorSubject,
combineLatestWith,
defer,
distinctUntilKeyChanged,
EMPTY,
forkJoin,
mergeWith,
NEVER,
Observable,
of,
shareReplay,
Subject,
TimeoutError,
} from 'rxjs';
import { catchError, distinctUntilChanged, filter, finalize, map, startWith, switchMap, tap, timeout } from 'rxjs/operators';
import {
AddMyAuthFactorOTPEmailRequest,
@@ -110,41 +124,17 @@ const ORG_LIMIT = 10;
})
export class GrpcAuthService {
private _activeOrgChanged: Subject<Org.AsObject | undefined> = new Subject();
public user!: Observable<User.AsObject | undefined>;
public userSubject: BehaviorSubject<User.AsObject | undefined> = new BehaviorSubject<User.AsObject | undefined>(undefined);
public user: Observable<User.AsObject | undefined>;
private triggerPermissionsRefresh: Subject<void> = new Subject();
public zitadelPermissions$: Observable<string[]> = this.triggerPermissionsRefresh.pipe(
switchMap(() =>
from(this.listMyZitadelPermissions()).pipe(
map((rolesResp) => rolesResp.resultList),
filter((roles) => !!roles.length),
catchError((_) => {
return of([]);
}),
distinctUntilChanged((a, b) => {
return JSON.stringify(a.sort()) === JSON.stringify(b.sort());
}),
finalize(() => {
this.fetchedZitadelPermissions.next(true);
}),
),
),
);
public zitadelPermissions: Observable<string[]>;
public labelpolicy$!: Observable<LabelPolicy.AsObject>;
public labelpolicy: BehaviorSubject<LabelPolicy.AsObject | undefined> = new BehaviorSubject<
LabelPolicy.AsObject | undefined
>(undefined);
labelPolicyLoading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
public privacypolicy$!: Observable<PrivacyPolicy.AsObject>;
public privacypolicy$: Observable<PrivacyPolicy.AsObject>;
public privacypolicy: BehaviorSubject<PrivacyPolicy.AsObject | undefined> = new BehaviorSubject<
PrivacyPolicy.AsObject | undefined
>(undefined);
privacyPolicyLoading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
public zitadelPermissions: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
public readonly fetchedZitadelPermissions: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public cachedOrgs: BehaviorSubject<Org.AsObject[]> = new BehaviorSubject<Org.AsObject[]>([]);
private cachedLabelPolicies: { [orgId: string]: LabelPolicy.AsObject } = {};
@@ -155,74 +145,63 @@ export class GrpcAuthService {
private oauthService: OAuthService,
private storage: StorageService,
) {
this.zitadelPermissions$.subscribe(this.zitadelPermissions);
this.labelpolicy$ = this.activeOrgChanged.pipe(
switchMap((org) => {
this.labelPolicyLoading$.next(true);
return from(this.getMyLabelPolicy(org ? org.id : ''));
}),
tap(() => this.labelPolicyLoading$.next(true)),
switchMap((org) => this.getMyLabelPolicy(org ? org.id : '')),
tap(() => this.labelPolicyLoading$.next(false)),
finalize(() => this.labelPolicyLoading$.next(false)),
filter((policy) => !!policy),
shareReplay({ refCount: true, bufferSize: 1 }),
);
this.labelpolicy$.subscribe({
next: (policy) => {
this.labelpolicy.next(policy);
this.labelPolicyLoading$.next(false);
},
error: (error) => {
console.error(error);
this.labelPolicyLoading$.next(false);
},
});
this.privacypolicy$ = this.activeOrgChanged.pipe(
switchMap((org) => {
this.privacyPolicyLoading$.next(true);
return from(this.getMyPrivacyPolicy(org ? org.id : ''));
}),
switchMap((org) => this.getMyPrivacyPolicy(org ? org.id : '')),
filter((policy) => !!policy),
catchError((err) => {
console.error(err);
return EMPTY;
}),
shareReplay({ refCount: true, bufferSize: 1 }),
);
this.privacypolicy$.subscribe({
next: (policy) => {
this.privacypolicy.next(policy);
this.privacyPolicyLoading$.next(false);
},
error: (error) => {
console.error(error);
this.privacyPolicyLoading$.next(false);
},
});
this.user = forkJoin([
of(this.oauthService.getAccessToken()),
defer(() => of(this.oauthService.getAccessToken())),
this.oauthService.events.pipe(
filter((e) => e.type === 'token_received'),
timeout(this.oauthService.waitForTokenInMsec || 0),
catchError((_) => of(null)), // timeout is not an error
timeout(this.oauthService.waitForTokenInMsec ?? 0),
catchError((err) => {
if (err instanceof TimeoutError) {
return of(null);
}
throw err;
}), // timeout is not an error
map((_) => this.oauthService.getAccessToken()),
),
]).pipe(
filter((token) => (token ? true : false)),
distinctUntilChanged(),
switchMap(() => {
return from(
this.getMyUser().then((resp) => {
return resp.user;
}),
);
}),
finalize(() => {
this.loadPermissions();
}),
filter(([_, token]) => !!token),
distinctUntilKeyChanged(1),
switchMap(() => this.getMyUser().then((resp) => resp.user)),
startWith(undefined),
shareReplay({ refCount: true, bufferSize: 1 }),
);
this.user.subscribe(this.userSubject);
this.activeOrgChanged.subscribe(() => {
this.loadPermissions();
});
this.zitadelPermissions = this.user.pipe(
combineLatestWith(this.activeOrgChanged),
// ignore errors from observables
catchError(() => of(true)),
// make sure observable never completes
mergeWith(NEVER),
switchMap(() =>
this.listMyZitadelPermissions()
.then((resp) => resp.resultList)
.catch(() => <string[]>[]),
),
filter((roles) => !!roles.length),
distinctUntilChanged((a, b) => {
return JSON.stringify(a.sort()) === JSON.stringify(b.sort());
}),
shareReplay({ refCount: true, bufferSize: 1 }),
);
}
public listMyMetadata(
@@ -309,7 +288,7 @@ export class GrpcAuthService {
}
public get activeOrgChanged(): Observable<Org.AsObject | undefined> {
return this._activeOrgChanged;
return this._activeOrgChanged.asObservable();
}
public setActiveOrg(org: Org.AsObject): void {
@@ -328,18 +307,15 @@ export class GrpcAuthService {
* @param roles roles of the user
*/
public isAllowed(roles: string[] | RegExp[], requiresAll: boolean = false): Observable<boolean> {
if (roles && roles.length > 0) {
return this.fetchedZitadelPermissions.pipe(
withLatestFrom(this.zitadelPermissions),
filter(([hL, p]) => {
return hL === true && !!p.length;
}),
map(([_, zroles]) => this.hasRoles(zroles, roles, requiresAll)),
distinctUntilChanged(),
);
} else {
if (!roles?.length) {
return of(false);
}
return this.zitadelPermissions.pipe(
filter((permissions) => !!permissions.length),
map((permissions) => this.hasRoles(permissions, roles, requiresAll)),
distinctUntilChanged(),
);
}
/**
@@ -353,17 +329,14 @@ export class GrpcAuthService {
mapper: (attr: any) => string[] | RegExp[],
requiresAll: boolean = false,
): Observable<T[]> {
return this.fetchedZitadelPermissions.pipe(
withLatestFrom(this.zitadelPermissions),
filter(([hL, p]) => {
return hL === true && !!p.length;
}),
map(([_, zroles]) => {
return objects.filter((obj) => {
return this.zitadelPermissions.pipe(
filter((permissions) => !!permissions.length),
map((permissions) =>
objects.filter((obj) => {
const roles = mapper(obj);
return this.hasRoles(zroles, roles, requiresAll);
});
}),
return this.hasRoles(permissions, roles, requiresAll);
}),
),
);
}
@@ -395,19 +368,6 @@ export class GrpcAuthService {
.then((resp) => resp.toObject());
}
public loadMyUser(): void {
from(this.getMyUser())
.pipe(
map((resp) => resp.user),
catchError((_) => {
return of(undefined);
}),
)
.subscribe((user) => {
this.userSubject.next(user);
});
}
public getMyUser(): Promise<GetMyUserResponse.AsObject> {
return this.grpcService.auth.getMyUser(new GetMyUserRequest(), null).then((resp) => resp.toObject());
}
@@ -728,40 +688,39 @@ export class GrpcAuthService {
public getMyLabelPolicy(orgIdForCache?: string): Promise<LabelPolicy.AsObject> {
if (orgIdForCache && this.cachedLabelPolicies[orgIdForCache]) {
return Promise.resolve(this.cachedLabelPolicies[orgIdForCache]);
} else {
return this.grpcService.auth
.getMyLabelPolicy(new GetMyLabelPolicyRequest(), null)
.then((resp) => resp.toObject())
.then((resp) => {
if (resp.policy) {
if (orgIdForCache) {
this.cachedLabelPolicies[orgIdForCache] = resp.policy;
}
return Promise.resolve(resp.policy);
} else {
return Promise.reject();
}
});
}
return this.grpcService.auth
.getMyLabelPolicy(new GetMyLabelPolicyRequest(), null)
.then((resp) => resp.toObject())
.then((resp) => {
if (!resp.policy) {
return Promise.reject();
}
if (orgIdForCache) {
this.cachedLabelPolicies[orgIdForCache] = resp.policy;
}
return resp.policy;
});
}
public getMyPrivacyPolicy(orgIdForCache?: string): Promise<PrivacyPolicy.AsObject> {
if (orgIdForCache && this.cachedPrivacyPolicies[orgIdForCache]) {
return Promise.resolve(this.cachedPrivacyPolicies[orgIdForCache]);
} else {
return this.grpcService.auth
.getMyPrivacyPolicy(new GetMyPrivacyPolicyRequest(), null)
.then((resp) => resp.toObject())
.then((resp) => {
if (resp.policy) {
if (orgIdForCache) {
this.cachedPrivacyPolicies[orgIdForCache] = resp.policy;
}
return Promise.resolve(resp.policy);
} else {
return Promise.reject();
}
});
}
return this.grpcService.auth
.getMyPrivacyPolicy(new GetMyPrivacyPolicyRequest(), null)
.then((resp) => resp.toObject())
.then((resp) => {
if (!resp.policy) {
return Promise.reject();
}
if (orgIdForCache) {
this.cachedPrivacyPolicies[orgIdForCache] = resp.policy;
}
return resp.policy;
});
}
}

View File

@@ -1,9 +1,8 @@
import { PlatformLocation } from '@angular/common';
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { AuthConfig } from 'angular-oauth2-oidc';
import { catchError, switchMap, tap, throwError } from 'rxjs';
import { catchError, firstValueFrom, switchMap, tap } from 'rxjs';
import { AdminServiceClient } from '../proto/generated/zitadel/AdminServiceClientPb';
import { AuthServiceClient } from '../proto/generated/zitadel/AuthServiceClientPb';
@@ -12,13 +11,18 @@ import { fallbackLanguage, supportedLanguagesRegexp } from '../utils/language';
import { AuthenticationService } from './authentication.service';
import { EnvironmentService } from './environment.service';
import { ExhaustedService } from './exhausted.service';
import { AuthInterceptor } from './interceptors/auth.interceptor';
import { AuthInterceptor, AuthInterceptorProvider, NewConnectWebAuthInterceptor } from './interceptors/auth.interceptor';
import { ExhaustedGrpcInterceptor } from './interceptors/exhausted.grpc.interceptor';
import { I18nInterceptor } from './interceptors/i18n.interceptor';
import { OrgInterceptor } from './interceptors/org.interceptor';
import { StorageService } from './storage.service';
import { FeatureServiceClient } from '../proto/generated/zitadel/feature/v2beta/Feature_serviceServiceClientPb';
import { GrpcAuthService } from './grpc-auth.service';
import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb';
//@ts-ignore
import { createUserServiceClient } from '@zitadel/client/v2';
//@ts-ignore
import { createAuthServiceClient, createManagementServiceClient } from '@zitadel/client/v1';
import { createGrpcWebTransport } from '@connectrpc/connect-web';
import { FeatureServiceClient } from '../proto/generated/zitadel/feature/v2/Feature_serviceServiceClientPb';
@Injectable({
providedIn: 'root',
@@ -28,15 +32,20 @@ export class GrpcService {
public mgmt!: ManagementServiceClient;
public admin!: AdminServiceClient;
public feature!: FeatureServiceClient;
public user!: UserServiceClient;
public userNew!: ReturnType<typeof createUserServiceClient>;
public mgmtNew!: ReturnType<typeof createManagementServiceClient>;
public authNew!: ReturnType<typeof createAuthServiceClient>;
constructor(
private envService: EnvironmentService,
private platformLocation: PlatformLocation,
private authenticationService: AuthenticationService,
private storageService: StorageService,
private dialog: MatDialog,
private translate: TranslateService,
private exhaustedService: ExhaustedService,
private readonly envService: EnvironmentService,
private readonly platformLocation: PlatformLocation,
private readonly authenticationService: AuthenticationService,
private readonly storageService: StorageService,
private readonly translate: TranslateService,
private readonly exhaustedService: ExhaustedService,
private readonly authInterceptor: AuthInterceptor,
private readonly authInterceptorProvider: AuthInterceptorProvider,
) {}
public loadAppEnvironment(): Promise<any> {
@@ -44,66 +53,79 @@ export class GrpcService {
const browserLanguage = this.translate.getBrowserLang();
const language = browserLanguage?.match(supportedLanguagesRegexp) ? browserLanguage : fallbackLanguage;
return this.translate
.use(language || this.translate.defaultLang)
.pipe(
switchMap(() => this.envService.env),
tap((env) => {
if (!env?.api || !env?.issuer) {
return;
}
const interceptors = {
unaryInterceptors: [
new ExhaustedGrpcInterceptor(this.exhaustedService, this.envService),
new OrgInterceptor(this.storageService),
new AuthInterceptor(this.authenticationService, this.storageService, this.dialog),
new I18nInterceptor(this.translate),
],
};
const init = this.translate.use(language || this.translate.defaultLang).pipe(
switchMap(() => this.envService.env),
tap((env) => {
if (!env?.api || !env?.issuer) {
return;
}
const interceptors = {
unaryInterceptors: [
new ExhaustedGrpcInterceptor(this.exhaustedService, this.envService),
new OrgInterceptor(this.storageService),
this.authInterceptor,
new I18nInterceptor(this.translate),
],
};
this.auth = new AuthServiceClient(
env.api,
null,
// @ts-ignore
interceptors,
);
this.mgmt = new ManagementServiceClient(
env.api,
null,
// @ts-ignore
interceptors,
);
this.admin = new AdminServiceClient(
env.api,
null,
// @ts-ignore
interceptors,
);
this.feature = new FeatureServiceClient(
env.api,
null,
// @ts-ignore
interceptors,
);
this.auth = new AuthServiceClient(
env.api,
null,
// @ts-ignore
interceptors,
);
this.mgmt = new ManagementServiceClient(
env.api,
null,
// @ts-ignore
interceptors,
);
this.admin = new AdminServiceClient(
env.api,
null,
// @ts-ignore
interceptors,
);
this.feature = new FeatureServiceClient(
env.api,
null,
// @ts-ignore
interceptors,
);
this.user = new UserServiceClient(
env.api,
null,
// @ts-ignore
interceptors,
);
const authConfig: AuthConfig = {
scope: 'openid profile email',
responseType: 'code',
oidc: true,
clientId: env.clientid,
issuer: env.issuer,
redirectUri: window.location.origin + this.platformLocation.getBaseHrefFromDOM() + 'auth/callback',
postLogoutRedirectUri: window.location.origin + this.platformLocation.getBaseHrefFromDOM() + 'signedout',
requireHttps: false,
};
const transport = createGrpcWebTransport({
baseUrl: env.api,
interceptors: [NewConnectWebAuthInterceptor(this.authInterceptorProvider)],
});
this.userNew = createUserServiceClient(transport);
this.mgmtNew = createManagementServiceClient(transport);
this.authNew = createAuthServiceClient(transport);
this.authenticationService.initConfig(authConfig);
}),
catchError((err) => {
console.error('Failed to load environment from assets', err);
return throwError(() => err);
}),
)
.toPromise();
const authConfig: AuthConfig = {
scope: 'openid profile email',
responseType: 'code',
oidc: true,
clientId: env.clientid,
issuer: env.issuer,
redirectUri: window.location.origin + this.platformLocation.getBaseHrefFromDOM() + 'auth/callback',
postLogoutRedirectUri: window.location.origin + this.platformLocation.getBaseHrefFromDOM() + 'signedout',
requireHttps: false,
};
this.authenticationService.initConfig(authConfig);
}),
catchError((err) => {
console.error('Failed to load environment from assets', err);
throw err;
}),
);
return firstValueFrom(init);
}
}

View File

@@ -1,59 +1,53 @@
import { Injectable } from '@angular/core';
import { DestroyRef, Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Request, UnaryInterceptor, UnaryResponse } from 'grpc-web';
import { Subject } from 'rxjs';
import { debounceTime, filter, first, map, take, tap } from 'rxjs/operators';
import { Request, RpcError, UnaryInterceptor, UnaryResponse } from 'grpc-web';
import { firstValueFrom, identity, lastValueFrom, Observable, Subject } from 'rxjs';
import { debounceTime, filter, map } from 'rxjs/operators';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { AuthenticationService } from '../authentication.service';
import { StorageService } from '../storage.service';
import { AuthConfig } from 'angular-oauth2-oidc';
import { GrpcAuthService } from '../grpc-auth.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ConnectError, Interceptor } from '@connectrpc/connect';
const authorizationKey = 'Authorization';
const bearerPrefix = 'Bearer';
const accessTokenStorageKey = 'access_token';
@Injectable({ providedIn: 'root' })
/**
* Set the authentication token
*/
export class AuthInterceptor<TReq = unknown, TResp = unknown> implements UnaryInterceptor<TReq, TResp> {
export class AuthInterceptorProvider {
public triggerDialog: Subject<boolean> = new Subject();
constructor(
private authenticationService: AuthenticationService,
private storageService: StorageService,
private dialog: MatDialog,
private destroyRef: DestroyRef,
) {
this.triggerDialog.pipe(debounceTime(1000)).subscribe(() => {
this.openDialog();
});
this.triggerDialog.pipe(debounceTime(1000), takeUntilDestroyed(this.destroyRef)).subscribe(() => this.openDialog());
}
public async intercept(request: Request<TReq, TResp>, invoker: any): Promise<UnaryResponse<TReq, TResp>> {
await this.authenticationService.authenticationChanged
.pipe(
filter((authed) => !!authed),
first(),
)
.toPromise();
const metadata = request.getMetadata();
const accessToken = this.storageService.getItem(accessTokenStorageKey);
metadata[authorizationKey] = `${bearerPrefix} ${accessToken}`;
return invoker(request)
.then((response: any) => {
return response;
})
.catch(async (error: any) => {
if (error.code === 16 || (error.code === 7 && error.message === 'mfa required (AUTHZ-Kl3p0)')) {
this.triggerDialog.next(true);
}
return Promise.reject(error);
});
getToken(): Observable<string> {
return this.authenticationService.authenticationChanged.pipe(
filter(identity),
map(() => this.storageService.getItem(accessTokenStorageKey)),
map((token) => `${bearerPrefix} ${token}`),
);
}
private openDialog(): void {
handleError = (error: any): never => {
if (!(error instanceof RpcError) && !(error instanceof ConnectError)) {
throw error;
}
if (error.code === 16 || (error.code === 7 && error.message === 'mfa required (AUTHZ-Kl3p0)')) {
this.triggerDialog.next(true);
}
throw error;
};
private async openDialog(): Promise<void> {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.LOGIN',
@@ -64,19 +58,47 @@ export class AuthInterceptor<TReq = unknown, TResp = unknown> implements UnaryIn
width: '400px',
});
dialogRef
.afterClosed()
.pipe(take(1))
.subscribe((resp) => {
if (resp) {
const idToken = this.authenticationService.getIdToken();
const configWithPrompt: Partial<AuthConfig> = {
customQueryParams: {
id_token_hint: idToken,
},
};
this.authenticationService.authenticate(configWithPrompt, true);
}
});
const resp = await lastValueFrom(dialogRef.afterClosed());
if (!resp) {
return;
}
const idToken = this.authenticationService.getIdToken();
const configWithPrompt: Partial<AuthConfig> = {
customQueryParams: {
id_token_hint: idToken,
},
};
await this.authenticationService.authenticate(configWithPrompt, true);
}
}
@Injectable({ providedIn: 'root' })
/**
* Set the authentication token
*/
export class AuthInterceptor<TReq = unknown, TResp = unknown> implements UnaryInterceptor<TReq, TResp> {
constructor(private readonly authInterceptorProvider: AuthInterceptorProvider) {}
public async intercept(
request: Request<TReq, TResp>,
invoker: (request: Request<TReq, TResp>) => Promise<UnaryResponse<TReq, TResp>>,
): Promise<UnaryResponse<TReq, TResp>> {
const metadata = request.getMetadata();
metadata[authorizationKey] = await firstValueFrom(this.authInterceptorProvider.getToken());
return invoker(request).catch(this.authInterceptorProvider.handleError);
}
}
export function NewConnectWebAuthInterceptor(authInterceptorProvider: AuthInterceptorProvider): Interceptor {
return (next) => async (req) => {
if (!req.header.get('Authorization')) {
const token = await firstValueFrom(authInterceptorProvider.getToken());
req.header.set('Authorization', token);
}
return next(req).catch(authInterceptorProvider.handleError);
};
}

View File

@@ -1,7 +1,8 @@
import { Injectable } from '@angular/core';
import { Request, StatusCode, UnaryInterceptor, UnaryResponse } from 'grpc-web';
import { Request, RpcError, StatusCode, UnaryInterceptor, UnaryResponse } from 'grpc-web';
import { EnvironmentService } from '../environment.service';
import { ExhaustedService } from '../exhausted.service';
import { lastValueFrom } from 'rxjs';
/**
* ExhaustedGrpcInterceptor shows the exhausted dialog after receiving a gRPC response status 8.
@@ -17,16 +18,13 @@ export class ExhaustedGrpcInterceptor<TReq = unknown, TResp = unknown> implement
request: Request<TReq, TResp>,
invoker: (request: Request<TReq, TResp>) => Promise<UnaryResponse<TReq, TResp>>,
): Promise<UnaryResponse<TReq, TResp>> {
return invoker(request).catch((error: any) => {
if (error.code === StatusCode.RESOURCE_EXHAUSTED) {
return this.exhaustedSvc
.showExhaustedDialog(this.envSvc.env)
.toPromise()
.then(() => {
throw error;
});
try {
return await invoker(request);
} catch (error: any) {
if (error instanceof RpcError && error.code === StatusCode.RESOURCE_EXHAUSTED) {
await lastValueFrom(this.exhaustedSvc.showExhaustedDialog(this.envSvc.env));
}
throw error;
});
}
}
}

View File

@@ -10,7 +10,7 @@ const i18nHeader = 'Accept-Language';
export class I18nInterceptor<TReq = unknown, TResp = unknown> implements UnaryInterceptor<TReq, TResp> {
constructor(private translate: TranslateService) {}
public async intercept(request: Request<TReq, TResp>, invoker: any): Promise<UnaryResponse<TReq, TResp>> {
public intercept(request: Request<TReq, TResp>, invoker: any): Promise<UnaryResponse<TReq, TResp>> {
const metadata = request.getMetadata();
const navLang = this.translate.currentLang ?? navigator.language;
@@ -18,12 +18,6 @@ export class I18nInterceptor<TReq = unknown, TResp = unknown> implements UnaryIn
metadata[i18nHeader] = navLang;
}
return invoker(request)
.then((response: any) => {
return response;
})
.catch((error: any) => {
return Promise.reject(error);
});
return invoker(request);
}
}

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { Request, UnaryInterceptor, UnaryResponse } from 'grpc-web';
import { Request, RpcError, StatusCode, UnaryInterceptor, UnaryResponse } from 'grpc-web';
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { StorageKey, StorageLocation, StorageService } from '../storage.service';
@@ -9,7 +9,7 @@ const ORG_HEADER_KEY = 'x-zitadel-orgid';
export class OrgInterceptor<TReq = unknown, TResp = unknown> implements UnaryInterceptor<TReq, TResp> {
constructor(private readonly storageService: StorageService) {}
public intercept(request: Request<TReq, TResp>, invoker: any): Promise<UnaryResponse<TReq, TResp>> {
public async intercept(request: Request<TReq, TResp>, invoker: any): Promise<UnaryResponse<TReq, TResp>> {
const metadata = request.getMetadata();
const org: Org.AsObject | null = this.storageService.getItem(StorageKey.organization, StorageLocation.session);
@@ -18,15 +18,17 @@ export class OrgInterceptor<TReq = unknown, TResp = unknown> implements UnaryInt
metadata[ORG_HEADER_KEY] = `${org.id}`;
}
return invoker(request)
.then((response: any) => {
return response;
})
.catch((error: any) => {
if (error.code === 7 && error.message.startsWith("Organisation doesn't exist")) {
this.storageService.removeItem(StorageKey.organization, StorageLocation.session);
}
return Promise.reject(error);
});
try {
return await invoker(request);
} catch (error: any) {
if (
error instanceof RpcError &&
error.code === StatusCode.PERMISSION_DENIED &&
error.message.startsWith("Organisation doesn't exist")
) {
this.storageService.removeItem(StorageKey.organization, StorageLocation.session);
}
throw error;
}
}
}

View File

@@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import { GrpcService } from './grpc.service';
import { create } from '@bufbuild/protobuf';
import {
AddMyAuthFactorOTPSMSRequestSchema,
AddMyAuthFactorOTPSMSResponse,
GetMyUserRequestSchema,
GetMyUserResponse,
VerifyMyPhoneRequestSchema,
VerifyMyPhoneResponse,
} from '@zitadel/proto/zitadel/auth_pb';
@Injectable({
providedIn: 'root',
})
export class NewAuthService {
constructor(private readonly grpcService: GrpcService) {}
public getMyUser(): Promise<GetMyUserResponse> {
return this.grpcService.authNew.getMyUser(create(GetMyUserRequestSchema));
}
public verifyMyPhone(code: string): Promise<VerifyMyPhoneResponse> {
return this.grpcService.authNew.verifyMyPhone(create(VerifyMyPhoneRequestSchema, { code }));
}
public addMyAuthFactorOTPSMS(): Promise<AddMyAuthFactorOTPSMSResponse> {
return this.grpcService.authNew.addMyAuthFactorOTPSMS(create(AddMyAuthFactorOTPSMSRequestSchema));
}
}

View File

@@ -0,0 +1,92 @@
import { Injectable } from '@angular/core';
import { GrpcService } from './grpc.service';
import {
GenerateMachineSecretRequestSchema,
GenerateMachineSecretResponse,
GetLoginPolicyRequestSchema,
GetLoginPolicyResponse,
ListUserMetadataRequestSchema,
ListUserMetadataResponse,
RemoveMachineSecretRequestSchema,
RemoveMachineSecretResponse,
RemoveUserMetadataRequestSchema,
RemoveUserMetadataResponse,
ResendHumanEmailVerificationRequestSchema,
ResendHumanEmailVerificationResponse,
ResendHumanInitializationRequestSchema,
ResendHumanInitializationResponse,
ResendHumanPhoneVerificationRequestSchema,
ResendHumanPhoneVerificationResponse,
SendHumanResetPasswordNotificationRequest_Type,
SendHumanResetPasswordNotificationRequestSchema,
SendHumanResetPasswordNotificationResponse,
SetUserMetadataRequestSchema,
SetUserMetadataResponse,
UpdateMachineRequestSchema,
UpdateMachineResponse,
} from '@zitadel/proto/zitadel/management_pb';
import { MessageInitShape, create } from '@bufbuild/protobuf';
@Injectable({
providedIn: 'root',
})
export class NewMgmtService {
constructor(private readonly grpcService: GrpcService) {}
public getLoginPolicy(): Promise<GetLoginPolicyResponse> {
return this.grpcService.mgmtNew.getLoginPolicy(create(GetLoginPolicyRequestSchema));
}
public generateMachineSecret(userId: string): Promise<GenerateMachineSecretResponse> {
return this.grpcService.mgmtNew.generateMachineSecret(create(GenerateMachineSecretRequestSchema, { userId }));
}
public removeMachineSecret(userId: string): Promise<RemoveMachineSecretResponse> {
return this.grpcService.mgmtNew.removeMachineSecret(create(RemoveMachineSecretRequestSchema, { userId }));
}
public updateMachine(req: MessageInitShape<typeof UpdateMachineRequestSchema>): Promise<UpdateMachineResponse> {
return this.grpcService.mgmtNew.updateMachine(create(UpdateMachineRequestSchema, req));
}
public resendHumanEmailVerification(userId: string): Promise<ResendHumanEmailVerificationResponse> {
return this.grpcService.mgmtNew.resendHumanEmailVerification(
create(ResendHumanEmailVerificationRequestSchema, { userId }),
);
}
public resendHumanPhoneVerification(userId: string): Promise<ResendHumanPhoneVerificationResponse> {
return this.grpcService.mgmtNew.resendHumanPhoneVerification(
create(ResendHumanPhoneVerificationRequestSchema, { userId }),
);
}
public sendHumanResetPasswordNotification(
userId: string,
type: SendHumanResetPasswordNotificationRequest_Type,
): Promise<SendHumanResetPasswordNotificationResponse> {
return this.grpcService.mgmtNew.sendHumanResetPasswordNotification(
create(SendHumanResetPasswordNotificationRequestSchema, { userId, type }),
);
}
public resendHumanInitialization(userId: string, email: string = ''): Promise<ResendHumanInitializationResponse> {
return this.grpcService.mgmtNew.resendHumanInitialization(
create(ResendHumanInitializationRequestSchema, { userId, email }),
);
}
public listUserMetadata(id: string): Promise<ListUserMetadataResponse> {
return this.grpcService.mgmtNew.listUserMetadata(create(ListUserMetadataRequestSchema, { id }));
}
public setUserMetadata(req: MessageInitShape<typeof SetUserMetadataRequestSchema>): Promise<SetUserMetadataResponse> {
return this.grpcService.mgmtNew.setUserMetadata(create(SetUserMetadataRequestSchema, req));
}
public removeUserMetadata(
req: MessageInitShape<typeof RemoveUserMetadataRequestSchema>,
): Promise<RemoveUserMetadataResponse> {
return this.grpcService.mgmtNew.removeUserMetadata(create(RemoveUserMetadataRequestSchema, req));
}
}

View File

@@ -0,0 +1,302 @@
import { Injectable } from '@angular/core';
import { GrpcService } from './grpc.service';
import {
AddHumanUserRequestSchema,
AddHumanUserResponse,
CreateInviteCodeRequestSchema,
CreateInviteCodeResponse,
CreatePasskeyRegistrationLinkRequestSchema,
CreatePasskeyRegistrationLinkResponse,
DeactivateUserRequestSchema,
DeactivateUserResponse,
DeleteUserRequestSchema,
DeleteUserResponse,
GetUserByIDRequestSchema,
GetUserByIDResponse,
ListAuthenticationFactorsRequestSchema,
ListAuthenticationFactorsResponse,
ListPasskeysRequestSchema,
ListPasskeysResponse,
ListUsersRequestSchema,
ListUsersResponse,
LockUserRequestSchema,
LockUserResponse,
PasswordResetRequestSchema,
ReactivateUserRequestSchema,
ReactivateUserResponse,
RemoveOTPEmailRequestSchema,
RemoveOTPEmailResponse,
RemoveOTPSMSRequestSchema,
RemoveOTPSMSResponse,
RemovePasskeyRequestSchema,
RemovePasskeyResponse,
RemovePhoneRequestSchema,
RemovePhoneResponse,
RemoveTOTPRequestSchema,
RemoveTOTPResponse,
RemoveU2FRequestSchema,
RemoveU2FResponse,
ResendInviteCodeRequestSchema,
ResendInviteCodeResponse,
SetEmailRequestSchema,
SetEmailResponse,
SetPasswordRequestSchema,
SetPasswordResponse,
SetPhoneRequestSchema,
SetPhoneResponse,
UnlockUserRequestSchema,
UnlockUserResponse,
UpdateHumanUserRequestSchema,
UpdateHumanUserResponse,
} from '@zitadel/proto/zitadel/user/v2/user_service_pb';
import type { MessageInitShape } from '@bufbuild/protobuf';
import {
AccessTokenType,
Gender,
HumanProfile,
HumanProfileSchema,
HumanUser,
HumanUserSchema,
MachineUser,
MachineUserSchema,
User as UserV2,
UserSchema,
UserState,
} from '@zitadel/proto/zitadel/user/v2/user_pb';
import { create } from '@bufbuild/protobuf';
import { Timestamp as TimestampV2, TimestampSchema } from '@bufbuild/protobuf/wkt';
import { Details, DetailsSchema, ListQuerySchema } from '@zitadel/proto/zitadel/object/v2/object_pb';
import { SearchQuery, UserFieldName } from '@zitadel/proto/zitadel/user/v2/query_pb';
import { SortDirection } from '@angular/material/sort';
import { Human, Machine, Phone, Profile, User } from '../proto/generated/zitadel/user_pb';
import { ObjectDetails } from '../proto/generated/zitadel/object_pb';
import { Timestamp } from '../proto/generated/google/protobuf/timestamp_pb';
import { HumanPhone, HumanPhoneSchema } from '@zitadel/proto/zitadel/user/v2/phone_pb';
@Injectable({
providedIn: 'root',
})
export class UserService {
constructor(private readonly grpcService: GrpcService) {}
public addHumanUser(req: MessageInitShape<typeof AddHumanUserRequestSchema>): Promise<AddHumanUserResponse> {
return this.grpcService.userNew.addHumanUser(create(AddHumanUserRequestSchema, req));
}
public listUsers(
limit: number,
offset: number,
queriesList?: SearchQuery[],
sortingColumn?: UserFieldName,
sortingDirection?: SortDirection,
): Promise<ListUsersResponse> {
const query = create(ListQuerySchema);
if (limit) {
query.limit = limit;
}
if (offset) {
query.offset = BigInt(offset);
}
if (sortingDirection) {
query.asc = sortingDirection === 'asc';
}
const req = create(ListUsersRequestSchema, {
query,
});
if (sortingColumn) {
req.sortingColumn = sortingColumn;
}
if (queriesList) {
req.queries = queriesList;
}
return this.grpcService.userNew.listUsers(req);
}
public getUserById(userId: string): Promise<GetUserByIDResponse> {
return this.grpcService.userNew.getUserByID(create(GetUserByIDRequestSchema, { userId }));
}
public deactivateUser(userId: string): Promise<DeactivateUserResponse> {
return this.grpcService.userNew.deactivateUser(create(DeactivateUserRequestSchema, { userId }));
}
public reactivateUser(userId: string): Promise<ReactivateUserResponse> {
return this.grpcService.userNew.reactivateUser(create(ReactivateUserRequestSchema, { userId }));
}
public deleteUser(userId: string): Promise<DeleteUserResponse> {
return this.grpcService.userNew.deleteUser(create(DeleteUserRequestSchema, { userId }));
}
public updateUser(req: MessageInitShape<typeof UpdateHumanUserRequestSchema>): Promise<UpdateHumanUserResponse> {
return this.grpcService.userNew.updateHumanUser(create(UpdateHumanUserRequestSchema, req));
}
public lockUser(userId: string): Promise<LockUserResponse> {
return this.grpcService.userNew.lockUser(create(LockUserRequestSchema, { userId }));
}
public unlockUser(userId: string): Promise<UnlockUserResponse> {
return this.grpcService.userNew.unlockUser(create(UnlockUserRequestSchema, { userId }));
}
public listAuthenticationFactors(
req: MessageInitShape<typeof ListAuthenticationFactorsRequestSchema>,
): Promise<ListAuthenticationFactorsResponse> {
return this.grpcService.userNew.listAuthenticationFactors(create(ListAuthenticationFactorsRequestSchema, req));
}
public listPasskeys(req: MessageInitShape<typeof ListPasskeysRequestSchema>): Promise<ListPasskeysResponse> {
return this.grpcService.userNew.listPasskeys(create(ListPasskeysRequestSchema, req));
}
public removePasskeys(req: MessageInitShape<typeof RemovePasskeyRequestSchema>): Promise<RemovePasskeyResponse> {
return this.grpcService.userNew.removePasskey(create(RemovePasskeyRequestSchema, req));
}
public createPasskeyRegistrationLink(
req: MessageInitShape<typeof CreatePasskeyRegistrationLinkRequestSchema>,
): Promise<CreatePasskeyRegistrationLinkResponse> {
return this.grpcService.userNew.createPasskeyRegistrationLink(create(CreatePasskeyRegistrationLinkRequestSchema, req));
}
public removePhone(userId: string): Promise<RemovePhoneResponse> {
return this.grpcService.userNew.removePhone(create(RemovePhoneRequestSchema, { userId }));
}
public setPhone(req: MessageInitShape<typeof SetPhoneRequestSchema>): Promise<SetPhoneResponse> {
return this.grpcService.userNew.setPhone(create(SetPhoneRequestSchema, req));
}
public setEmail(req: MessageInitShape<typeof SetEmailRequestSchema>): Promise<SetEmailResponse> {
return this.grpcService.userNew.setEmail(create(SetEmailRequestSchema, req));
}
public removeTOTP(userId: string): Promise<RemoveTOTPResponse> {
return this.grpcService.userNew.removeTOTP(create(RemoveTOTPRequestSchema, { userId }));
}
public removeU2F(userId: string, u2fId: string): Promise<RemoveU2FResponse> {
return this.grpcService.userNew.removeU2F(create(RemoveU2FRequestSchema, { userId, u2fId }));
}
public removeOTPSMS(userId: string): Promise<RemoveOTPSMSResponse> {
return this.grpcService.userNew.removeOTPSMS(create(RemoveOTPSMSRequestSchema, { userId }));
}
public removeOTPEmail(userId: string): Promise<RemoveOTPEmailResponse> {
return this.grpcService.userNew.removeOTPEmail(create(RemoveOTPEmailRequestSchema, { userId }));
}
public resendInviteCode(userId: string): Promise<ResendInviteCodeResponse> {
return this.grpcService.userNew.resendInviteCode(create(ResendInviteCodeRequestSchema, { userId }));
}
public createInviteCode(req: MessageInitShape<typeof CreateInviteCodeRequestSchema>): Promise<CreateInviteCodeResponse> {
return this.grpcService.userNew.createInviteCode(create(CreateInviteCodeRequestSchema, req));
}
public passwordReset(req: MessageInitShape<typeof PasswordResetRequestSchema>) {
return this.grpcService.userNew.passwordReset(create(PasswordResetRequestSchema, req));
}
public setPassword(req: MessageInitShape<typeof SetPasswordRequestSchema>): Promise<SetPasswordResponse> {
return this.grpcService.userNew.setPassword(create(SetPasswordRequestSchema, req));
}
}
function userToV2(user: User): UserV2 {
const details = user.getDetails();
return create(UserSchema, {
userId: user.getId(),
details: details && detailsToV2(details),
state: user.getState() as number as UserState,
username: user.getUserName(),
loginNames: user.getLoginNamesList(),
preferredLoginName: user.getPreferredLoginName(),
type: typeToV2(user),
});
}
function detailsToV2(details: ObjectDetails): Details {
const changeDate = details.getChangeDate();
return create(DetailsSchema, {
sequence: BigInt(details.getSequence()),
changeDate: changeDate && timestampToV2(changeDate),
resourceOwner: details.getResourceOwner(),
});
}
function timestampToV2(timestamp: Timestamp): TimestampV2 {
return create(TimestampSchema, {
seconds: BigInt(timestamp.getSeconds()),
nanos: timestamp.getNanos(),
});
}
function typeToV2(user: User): UserV2['type'] {
const human = user.getHuman();
if (human) {
return { case: 'human', value: humanToV2(user, human) };
}
const machine = user.getMachine();
if (machine) {
return { case: 'machine', value: machineToV2(machine) };
}
return { case: undefined };
}
function humanToV2(user: User, human: Human): HumanUser {
const profile = human.getProfile();
const email = human.getEmail()?.getEmail();
const phone = human.getPhone();
const passwordChanged = human.getPasswordChanged();
return create(HumanUserSchema, {
userId: user.getId(),
state: user.getState() as number as UserState,
username: user.getUserName(),
loginNames: user.getLoginNamesList(),
preferredLoginName: user.getPreferredLoginName(),
profile: profile && humanProfileToV2(profile),
email: { email },
phone: phone && humanPhoneToV2(phone),
passwordChangeRequired: false,
passwordChanged: passwordChanged && timestampToV2(passwordChanged),
});
}
function humanProfileToV2(profile: Profile): HumanProfile {
return create(HumanProfileSchema, {
givenName: profile.getFirstName(),
familyName: profile.getLastName(),
nickName: profile.getNickName(),
displayName: profile.getDisplayName(),
preferredLanguage: profile.getPreferredLanguage(),
gender: profile.getGender() as number as Gender,
avatarUrl: profile.getAvatarUrl(),
});
}
function humanPhoneToV2(phone: Phone): HumanPhone {
return create(HumanPhoneSchema, {
phone: phone.getPhone(),
isVerified: phone.getIsPhoneVerified(),
});
}
function machineToV2(machine: Machine): MachineUser {
return create(MachineUserSchema, {
name: machine.getName(),
description: machine.getDescription(),
hasSecret: machine.getHasSecret(),
accessTokenType: machine.getAccessTokenType() as number as AccessTokenType,
});
}