mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-26 01:02:16 +00:00
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]>
222 lines
8.2 KiB
TypeScript
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));
|
|
}
|
|
}
|