Files
zitadel/console/src/app/services/user.service.ts
T
f963ea9f86 feat(console): rehauled navigation for instances including breadcrumb (#10283)
This PR significantly improves user navigation by introducing a new
instance-level navigation bar. This new bar, positioned above the
existing organization navigation, provides quick access to key sections:
Home, Organizations, Actions, and Settings.

Additionally, the breadcrumb component has been refined for more
consistent behavior, reintroducing intuitive breadcrumb buttons to
easily navigate up the hierarchy. These changes also include design
improvements for a cleaner and more streamlined appearance across the
interface.

<img width="423" height="138" alt="Screenshot 2025-07-17 at 14 55 46"
src="https://github.com/user-attachments/assets/ba9e40a1-1077-4cb6-8735-ac7ab637abe7"
/>
<img width="562" height="132" alt="Screenshot 2025-07-17 at 14 56 41"
src="https://github.com/user-attachments/assets/d85dc673-0df8-4677-9d2b-dc031dde42c3"
/>
<img width="545" height="254" alt="Screenshot 2025-07-17 at 14 56 10"
src="https://github.com/user-attachments/assets/eaf10117-079e-4181-8dbb-60c89b24556a"
/>
<img width="689" height="261" alt="Screenshot 2025-07-17 at 14 56 20"
src="https://github.com/user-attachments/assets/510ad550-1d9a-4c6a-8af1-66cb0b23619c"
/>

---------

Co-authored-by: conblem <[email protected]>
Co-authored-by: Stefan Benz <[email protected]>
Co-authored-by: Florian Forster <[email protected]>
2025-08-28 11:23:14 +02:00

222 lines
8.2 KiB
TypeScript

import { computed, Injectable, Signal } from '@angular/core';
import { GrpcService } from './grpc.service';
import {
AddHumanUserRequestSchema,
AddHumanUserResponse,
CreateInviteCodeRequestSchema,
CreateInviteCodeResponse,
CreatePasskeyRegistrationLinkRequestSchema,
CreatePasskeyRegistrationLinkResponse,
DeactivateUserRequestSchema,
DeactivateUserResponse,
DeleteUserRequestSchema,
DeleteUserResponse,
GetUserByIDResponse,
ListAuthenticationFactorsRequestSchema,
ListAuthenticationFactorsResponse,
ListPasskeysRequestSchema,
ListPasskeysResponse,
ListUsersRequestSchema,
ListUsersResponse,
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 { create } from '@bufbuild/protobuf';
import { OAuthService } from 'angular-oauth2-oidc';
import { filter, map } from 'rxjs/operators';
import { toSignal } from '@angular/core/rxjs-interop';
import { injectQuery, queryOptions, skipToken } from '@tanstack/angular-query-experimental';
@Injectable({
providedIn: 'root',
})
export class UserService {
private readonly payload: Signal<unknown | undefined>;
public readonly userId: Signal<string | undefined>;
public readonly isExpired: Signal<boolean>;
public userQuery() {
return injectQuery(() => this.userQueryOptions());
}
public userQueryOptions() {
const userId = this.userId();
return queryOptions({
queryKey: [userId, 'user'],
queryFn: userId ? () => this.getUserById(userId).then((resp) => resp.user) : skipToken,
});
}
constructor(
private readonly grpcService: GrpcService,
private readonly oauthService: OAuthService,
) {
this.payload = this.getPayload();
this.userId = this.getUserId(this.payload);
this.isExpired = this.getIsExpired(this.payload);
}
private getPayload() {
const idToken$ = this.oauthService.events.pipe(
filter((event) => event.type === 'token_received'),
// can actually return null
// https://github.com/manfredsteyer/angular-oauth2-oidc/blob/c724ad73eadbb28338b084e3afa5ed49a0ea058c/projects/lib/src/oauth-service.ts#L2365
map(() => this.oauthService.getIdToken() as string | null),
);
const idToken = toSignal(idToken$, { initialValue: this.oauthService.getIdToken() as string | null });
return computed(() => {
try {
// split jwt and get base64 encoded payload
const unparsedPayload = atob((idToken() ?? '').split('.')[1]);
// parse payload
return JSON.parse(unparsedPayload) as unknown;
} catch {
return undefined;
}
});
}
private getUserId(payloadSignal: Signal<unknown | undefined>) {
return computed(() => {
const payload = payloadSignal();
if (payload && typeof payload === 'object' && 'sub' in payload && typeof payload.sub === 'string') {
return payload.sub;
}
return undefined;
});
}
private getIsExpired(payloadSignal: Signal<unknown | undefined>) {
const expSignal = computed(() => {
const payload = payloadSignal();
if (payload && typeof payload === 'object' && 'exp' in payload && typeof payload.exp === 'number') {
return new Date(payload.exp * 1000);
}
return undefined;
});
return computed(() => {
const exp = expSignal();
return exp ? exp <= new Date() : true;
});
}
public addHumanUser(req: MessageInitShape<typeof AddHumanUserRequestSchema>): Promise<AddHumanUserResponse> {
return this.grpcService.userNew.addHumanUser(create(AddHumanUserRequestSchema, req));
}
public listUsers(req: MessageInitShape<typeof ListUsersRequestSchema>): Promise<ListUsersResponse> {
return this.grpcService.userNew.listUsers(req);
}
public getUserById(userId: string): Promise<GetUserByIDResponse> {
return this.grpcService.userNew.getUserByID({ 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 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 setPassword(req: MessageInitShape<typeof SetPasswordRequestSchema>): Promise<SetPasswordResponse> {
return this.grpcService.userNew.setPassword(create(SetPasswordRequestSchema, req));
}
}