mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-07 14:52:15 +00:00
# Which Problems Are Solved The new breadcrumbs were causing problems with some more advanced Zitadel setups. This pr removes the new breadcrumbs to revert to the old navigation behaviour. # How the Problems Are Solved Most of the changes could be kept but those specific to the navigation where mostly reverted. # Additional Changes Updated some dependencies. # Additional Context - Closes #10863
This commit is contained in:
@@ -1,8 +1,19 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SortDirection } from '@angular/material/sort';
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
import { BehaviorSubject, combineLatestWith, EMPTY, identity, mergeWith, NEVER, Observable, of, shareReplay } from 'rxjs';
|
||||
import { catchError, distinctUntilChanged, filter, finalize, map, startWith, switchMap, tap, timeout } from 'rxjs/operators';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatestWith,
|
||||
EMPTY,
|
||||
identity,
|
||||
mergeWith,
|
||||
NEVER,
|
||||
Observable,
|
||||
of,
|
||||
shareReplay,
|
||||
Subject,
|
||||
} from 'rxjs';
|
||||
import { catchError, distinctUntilChanged, filter, finalize, map, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
AddMyAuthFactorOTPEmailRequest,
|
||||
@@ -85,18 +96,21 @@ import {
|
||||
import { ChangeQuery } from '../proto/generated/zitadel/change_pb';
|
||||
import { MetadataQuery } from '../proto/generated/zitadel/metadata_pb';
|
||||
import { ListQuery } from '../proto/generated/zitadel/object_pb';
|
||||
import { OrgFieldName, OrgQuery } from '../proto/generated/zitadel/org_pb';
|
||||
import { Org, OrgFieldName, OrgIDQuery, OrgQuery } from '../proto/generated/zitadel/org_pb';
|
||||
import { LabelPolicy, PrivacyPolicy } from '../proto/generated/zitadel/policy_pb';
|
||||
import { Gender, MembershipQuery, User, WebAuthNVerification } from '../proto/generated/zitadel/user_pb';
|
||||
import { GrpcService } from './grpc.service';
|
||||
import { NewOrganizationService } from './new-organization.service';
|
||||
import { toObservable } from '@angular/core/rxjs-interop';
|
||||
import { StorageKey, StorageLocation, StorageService } from './storage.service';
|
||||
|
||||
const ORG_LIMIT = 10;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class GrpcAuthService {
|
||||
private _activeOrgChanged: Subject<Org.AsObject | undefined> = new Subject();
|
||||
public user: Observable<User.AsObject | undefined>;
|
||||
private triggerPermissionsRefresh: Subject<void> = new Subject();
|
||||
public zitadelPermissions: Observable<string[]>;
|
||||
|
||||
public labelpolicy$!: Observable<LabelPolicy.AsObject>;
|
||||
@@ -107,27 +121,26 @@ export class GrpcAuthService {
|
||||
PrivacyPolicy.AsObject | undefined
|
||||
>(undefined);
|
||||
|
||||
public cachedOrgs: BehaviorSubject<Org.AsObject[]> = new BehaviorSubject<Org.AsObject[]>([]);
|
||||
private cachedLabelPolicies: { [orgId: string]: LabelPolicy.AsObject } = {};
|
||||
private cachedPrivacyPolicies: { [orgId: string]: PrivacyPolicy.AsObject } = {};
|
||||
|
||||
constructor(
|
||||
private readonly grpcService: GrpcService,
|
||||
private oauthService: OAuthService,
|
||||
newOrganizationService: NewOrganizationService,
|
||||
private storage: StorageService,
|
||||
) {
|
||||
const activeOrg = toObservable(newOrganizationService.orgId);
|
||||
|
||||
this.labelpolicy$ = activeOrg.pipe(
|
||||
this.labelpolicy$ = this.activeOrgChanged.pipe(
|
||||
tap(() => this.labelPolicyLoading$.next(true)),
|
||||
switchMap((org) => this.getMyLabelPolicy(org ?? '')),
|
||||
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.privacypolicy$ = activeOrg.pipe(
|
||||
switchMap((org) => this.getMyPrivacyPolicy(org ?? '')),
|
||||
this.privacypolicy$ = this.activeOrgChanged.pipe(
|
||||
switchMap((org) => this.getMyPrivacyPolicy(org ? org.id : '')),
|
||||
filter((policy) => !!policy),
|
||||
catchError((err) => {
|
||||
console.error(err);
|
||||
@@ -148,7 +161,7 @@ export class GrpcAuthService {
|
||||
);
|
||||
|
||||
this.zitadelPermissions = this.user.pipe(
|
||||
combineLatestWith(activeOrg),
|
||||
combineLatestWith(this.activeOrgChanged),
|
||||
// ignore errors from observables
|
||||
catchError(() => of(true)),
|
||||
// make sure observable never completes
|
||||
@@ -184,6 +197,87 @@ export class GrpcAuthService {
|
||||
return this.grpcService.auth.listMyMetadata(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public async getActiveOrg(id?: string): Promise<Org.AsObject> {
|
||||
if (id) {
|
||||
const find = this.cachedOrgs.getValue().find((tmp) => tmp.id === id);
|
||||
if (find) {
|
||||
this.setActiveOrg(find);
|
||||
return Promise.resolve(find);
|
||||
} else {
|
||||
const orgQuery = new OrgQuery();
|
||||
const orgIdQuery = new OrgIDQuery();
|
||||
orgIdQuery.setId(id);
|
||||
orgQuery.setIdQuery(orgIdQuery);
|
||||
|
||||
const orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0, [orgQuery])).resultList;
|
||||
if (orgs.length === 1) {
|
||||
this.setActiveOrg(orgs[0]);
|
||||
return Promise.resolve(orgs[0]);
|
||||
} else {
|
||||
// throw error if the org was specifically requested but not found
|
||||
return Promise.reject(new Error('requested organization not found'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let orgs: Org.AsObject[];
|
||||
const org = this.storage.getItem<Org.AsObject>(StorageKey.organization, StorageLocation.local);
|
||||
|
||||
if (org) {
|
||||
orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0)).resultList;
|
||||
this.cachedOrgs.next(orgs);
|
||||
|
||||
const find = this.cachedOrgs.getValue().find((tmp) => tmp.id === id);
|
||||
if (find) {
|
||||
this.setActiveOrg(find);
|
||||
return Promise.resolve(find);
|
||||
} else {
|
||||
const orgQuery = new OrgQuery();
|
||||
const orgIdQuery = new OrgIDQuery();
|
||||
orgIdQuery.setId(org.id);
|
||||
orgQuery.setIdQuery(orgIdQuery);
|
||||
|
||||
const specificOrg = (await this.listMyProjectOrgs(ORG_LIMIT, 0, [orgQuery])).resultList;
|
||||
if (specificOrg.length === 1) {
|
||||
this.setActiveOrg(specificOrg[0]);
|
||||
return Promise.resolve(specificOrg[0]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0)).resultList;
|
||||
this.cachedOrgs.next(orgs);
|
||||
}
|
||||
|
||||
if (orgs.length === 0) {
|
||||
this._activeOrgChanged.next(undefined);
|
||||
return Promise.reject(new Error('No organizations found!'));
|
||||
}
|
||||
|
||||
const orgToSet = orgs.find((element) => element.id !== '0' && element.name !== '');
|
||||
if (orgToSet) {
|
||||
this.setActiveOrg(orgToSet);
|
||||
return Promise.resolve(orgToSet);
|
||||
}
|
||||
return Promise.resolve(orgs[0]);
|
||||
}
|
||||
}
|
||||
|
||||
public get activeOrgChanged(): Observable<Org.AsObject | undefined> {
|
||||
return this._activeOrgChanged.asObservable();
|
||||
}
|
||||
|
||||
public setActiveOrg(org: Org.AsObject): void {
|
||||
// Set organization in localstorage to get the last used organization in a new tab
|
||||
this.storage.setItem(StorageKey.organization, org, StorageLocation.local);
|
||||
this.storage.setItem(StorageKey.organizationId, org.id, StorageLocation.local);
|
||||
this.storage.setItem(StorageKey.organization, org, StorageLocation.session);
|
||||
this.storage.setItem(StorageKey.organizationId, org.id, StorageLocation.session);
|
||||
this._activeOrgChanged.next(org);
|
||||
}
|
||||
|
||||
private loadPermissions(): void {
|
||||
this.triggerPermissionsRefresh.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if user has one of the provided roles
|
||||
* @param roles roles of the user
|
||||
@@ -253,6 +347,11 @@ export class GrpcAuthService {
|
||||
return this.grpcService.auth.getMyUser(new GetMyUserRequest(), null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public async revalidateOrgs() {
|
||||
const orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0)).resultList;
|
||||
this.cachedOrgs.next(orgs);
|
||||
}
|
||||
|
||||
public listMyProjectOrgs(
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import { computed, Injectable, signal } from '@angular/core';
|
||||
import { computed, Injectable } from '@angular/core';
|
||||
import { GrpcService } from './grpc.service';
|
||||
import { injectQuery, mutationOptions, QueryClient, queryOptions, skipToken } from '@tanstack/angular-query-experimental';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { ListOrganizationsRequestSchema, ListOrganizationsResponse } from '@zitadel/proto/zitadel/org/v2/org_service_pb';
|
||||
import { create, DescMessage, MessageInitShape, toBinary } from '@bufbuild/protobuf';
|
||||
import {
|
||||
ListOrganizationsRequestSchema,
|
||||
ListOrganizationsResponse,
|
||||
OrganizationService,
|
||||
} from '@zitadel/proto/zitadel/org/v2/org_service_pb';
|
||||
import { NewMgmtService } from './new-mgmt.service';
|
||||
import { OrgInterceptorProvider } from './interceptors/org.interceptor';
|
||||
import { NewAdminService } from './new-admin.service';
|
||||
import { SetUpOrgRequestSchema } from '@zitadel/proto/zitadel/admin_pb';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { StorageKey, StorageLocation, StorageService } from './storage.service';
|
||||
import { UserService } from './user.service';
|
||||
import { GrpcAuthService } from './grpc-auth.service';
|
||||
import { concatWith, defer, map } from 'rxjs';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { Buffer } from 'buffer';
|
||||
import { AuthService, ListMyProjectOrgsRequestSchema } from '@zitadel/proto/zitadel/auth_pb';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -19,44 +25,14 @@ import { UserService } from './user.service';
|
||||
export class NewOrganizationService {
|
||||
constructor(
|
||||
private readonly grpcService: GrpcService,
|
||||
private readonly authService: GrpcAuthService,
|
||||
private readonly newMgtmService: NewMgmtService,
|
||||
private readonly newAdminService: NewAdminService,
|
||||
private readonly orgInterceptorProvider: OrgInterceptorProvider,
|
||||
private readonly queryClient: QueryClient,
|
||||
private readonly translate: TranslateService,
|
||||
private readonly storage: StorageService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
private readonly orgIdSignal = signal<string | undefined>(
|
||||
this.storage.getItem(StorageKey.organizationId, StorageLocation.session) ??
|
||||
this.storage.getItem(StorageKey.organizationId, StorageLocation.local) ??
|
||||
undefined,
|
||||
);
|
||||
public readonly orgId = this.orgIdSignal.asReadonly();
|
||||
|
||||
public getOrgId() {
|
||||
return computed(() => {
|
||||
const orgId = this.orgIdSignal();
|
||||
if (orgId === undefined) {
|
||||
throw new Error('No organization ID set');
|
||||
}
|
||||
return orgId;
|
||||
});
|
||||
}
|
||||
|
||||
public async setOrgId(orgId?: string) {
|
||||
const organization = await this.queryClient.fetchQuery(this.organizationByIdQueryOptions(orgId ?? this.getOrgId()()));
|
||||
if (organization) {
|
||||
this.storage.setItem(StorageKey.organizationId, orgId, StorageLocation.session);
|
||||
this.storage.setItem(StorageKey.organizationId, orgId, StorageLocation.local);
|
||||
this.orgIdSignal.set(orgId);
|
||||
} else {
|
||||
throw new Error('request organization not found');
|
||||
}
|
||||
return organization;
|
||||
}
|
||||
|
||||
public organizationByIdQueryOptions(organizationId?: string) {
|
||||
const req = {
|
||||
query: {
|
||||
@@ -74,62 +50,82 @@ export class NewOrganizationService {
|
||||
],
|
||||
};
|
||||
|
||||
const { queryFn, ...listOrganizationsQueryOptions } = this.listOrganizationsQueryOptions(req);
|
||||
|
||||
return queryOptions({
|
||||
queryKey: [this.userService.userId(), 'organization', 'listOrganizations', req],
|
||||
queryFn: organizationId
|
||||
? () => this.listOrganizations(req).then((resp) => resp.result.find(Boolean) ?? null)
|
||||
: skipToken,
|
||||
...listOrganizationsQueryOptions,
|
||||
queryFn: organizationId ? queryFn : skipToken,
|
||||
select: (data) => data.result.find(Boolean) ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
public activeOrganizationQuery() {
|
||||
return injectQuery(() => this.organizationByIdQueryOptions(this.orgId()));
|
||||
}
|
||||
const activeOrg$ = defer(() => this.authService.getActiveOrg()).pipe(
|
||||
concatWith(this.authService.activeOrgChanged),
|
||||
filter(Boolean),
|
||||
map((org) => org.id),
|
||||
);
|
||||
|
||||
public listOrganizationsQueryOptions(req?: MessageInitShape<typeof ListOrganizationsRequestSchema>) {
|
||||
return queryOptions({
|
||||
queryKey: this.listOrganizationsQueryKey(req),
|
||||
queryFn: () => this.listOrganizations(req ?? {}),
|
||||
const activeOrg = toSignal(activeOrg$);
|
||||
|
||||
const req = computed(
|
||||
() =>
|
||||
({
|
||||
query: {
|
||||
limit: 1,
|
||||
},
|
||||
queries: [
|
||||
{
|
||||
query: {
|
||||
case: 'idQuery' as const,
|
||||
value: {
|
||||
id: activeOrg(),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}) satisfies MessageInitShape<typeof ListMyProjectOrgsRequestSchema>,
|
||||
);
|
||||
|
||||
return injectQuery(() => {
|
||||
const { queryFn, ...listMyProjectOrgsQueryOptions } = this.listMyProjectOrgsQueryOptions(req());
|
||||
|
||||
return queryOptions({
|
||||
...listMyProjectOrgsQueryOptions,
|
||||
queryFn: activeOrg() ? queryFn : skipToken,
|
||||
select: (data) => data.result.find(Boolean) ?? null,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public listOrganizationsQueryKey(req?: MessageInitShape<typeof ListOrganizationsRequestSchema>) {
|
||||
if (!req) {
|
||||
return [this.userService.userId(), 'organization', 'listOrganizations'];
|
||||
}
|
||||
|
||||
// needed because angular query isn't able to serialize a bigint key
|
||||
const query = req.query ? { ...req.query, offset: req.query.offset ? Number(req.query.offset) : undefined } : undefined;
|
||||
const queryKey = {
|
||||
...req,
|
||||
...(query ? { query } : {}),
|
||||
};
|
||||
|
||||
return [this.userService.userId(), 'organization', 'listOrganizations', queryKey];
|
||||
public listOrganizationsQueryOptions(req?: MessageInitShape<typeof ListOrganizationsRequestSchema>) {
|
||||
const queryKeyHashFn = tanstackQueryKeyHashFn(ListOrganizationsRequestSchema);
|
||||
return queryOptions({
|
||||
queryKey: [
|
||||
this.userService.userId(),
|
||||
OrganizationService.name,
|
||||
OrganizationService.method.listOrganizations.name,
|
||||
req,
|
||||
] as const,
|
||||
queryKeyHashFn: (key) => queryKeyHashFn(...key),
|
||||
queryFn: ({ signal }) => this.listOrganizations(req ?? {}, signal),
|
||||
});
|
||||
}
|
||||
|
||||
public listOrganizations(
|
||||
private listOrganizations(
|
||||
req: MessageInitShape<typeof ListOrganizationsRequestSchema>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ListOrganizationsResponse> {
|
||||
return this.grpcService.organizationNew.listOrganizations(req, { signal });
|
||||
}
|
||||
|
||||
private async getDefaultOrganization() {
|
||||
let resp = await this.listOrganizations({
|
||||
query: {
|
||||
limit: 1,
|
||||
},
|
||||
queries: [
|
||||
{
|
||||
query: {
|
||||
case: 'defaultQuery',
|
||||
value: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
private listMyProjectOrgsQueryOptions(req?: MessageInitShape<typeof ListMyProjectOrgsRequestSchema>) {
|
||||
const queryKeyHashFn = tanstackQueryKeyHashFn(ListMyProjectOrgsRequestSchema);
|
||||
return queryOptions({
|
||||
queryKey: [this.userService.userId(), AuthService.name, AuthService.method.listMyProjectOrgs.name, req] as const,
|
||||
queryKeyHashFn: (key) => queryKeyHashFn(...key),
|
||||
queryFn: ({ signal }) => this.grpcService.authNew.listMyProjectOrgs(req ?? {}, { signal }),
|
||||
});
|
||||
return resp.result.find(Boolean) ?? null;
|
||||
}
|
||||
|
||||
private invalidateAllOrganizationQueries() {
|
||||
@@ -149,20 +145,11 @@ export class NewOrganizationService {
|
||||
mutationOptions({
|
||||
mutationKey: ['deleteOrg'],
|
||||
mutationFn: async () => {
|
||||
// Before we remove the org we get the current default org
|
||||
// we have to query before the current org is removed
|
||||
const defaultOrg = await this.getDefaultOrganization();
|
||||
if (!defaultOrg) {
|
||||
const error$ = this.translate.get('ORG.TOAST.DEFAULTORGOTFOUND').pipe(first());
|
||||
throw { message: await lastValueFrom(error$) };
|
||||
}
|
||||
|
||||
const resp = await this.newMgtmService.removeOrg();
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
// We change active org to default org as
|
||||
// current org was deleted to avoid Organization doesn't exist
|
||||
await this.setOrgId(defaultOrg.id);
|
||||
await this.authService.getActiveOrg();
|
||||
|
||||
return resp;
|
||||
},
|
||||
@@ -210,3 +197,16 @@ export class NewOrganizationService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function tanstackQueryKeyHashFn<T extends DescMessage>(schema: T) {
|
||||
return (userId: string | undefined, serviceName: string, methodName: string, req?: MessageInitShape<T>) => {
|
||||
if (!req) {
|
||||
return JSON.stringify([userId, serviceName, methodName]);
|
||||
}
|
||||
|
||||
const serializedReq = toBinary(schema, create(schema, req));
|
||||
const serializedReqAsString = Buffer.from(serializedReq).toString('base64');
|
||||
|
||||
return JSON.stringify([userId, serviceName, methodName, serializedReqAsString]);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ export class StorageConfig {
|
||||
}
|
||||
|
||||
export enum StorageKey {
|
||||
organization = 'organization',
|
||||
organizationId = 'organizationId',
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user