fix: v2 user console errors (#9386)

# Which Problems Are Solved

- Fixed filtering in overview
- Only get users from current organization
- Use V2 api to get auth user

# How the Problems Are Solved

Added the organization filter to the List queries
Get current User ID from ID Token to get auth user by id

# Additional Changes

Refactored the UserList

# Additional Context
- Closes #9382
This commit is contained in:
Ramon 2025-02-21 14:57:09 +01:00 committed by GitHub
parent 9aad207ee4
commit 70234289cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 500 additions and 409 deletions

View File

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, DestroyRef, OnInit } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { ActivatedRoute, Router } from '@angular/router';
import { take } from 'rxjs';
@ -27,9 +27,10 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
constructor(
router: Router,
destroyRef: DestroyRef,
protected override route: ActivatedRoute,
) {
super(router, route);
super(router, route, destroyRef);
}
ngOnInit(): void {

View File

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, DestroyRef, OnInit } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { ActivatedRoute, Router } from '@angular/router';
import { take } from 'rxjs';
@ -23,8 +23,8 @@ export class FilterProjectComponent extends FilterComponent implements OnInit {
public searchQueries: ProjectQuery[] = [];
public states: ProjectState[] = [ProjectState.PROJECT_STATE_ACTIVE, ProjectState.PROJECT_STATE_INACTIVE];
constructor(router: Router, route: ActivatedRoute) {
super(router, route);
constructor(router: Router, route: ActivatedRoute, destroyRef: DestroyRef) {
super(router, route, destroyRef);
}
ngOnInit(): void {

View File

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, DestroyRef, OnInit } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { ActivatedRoute, Router } from '@angular/router';
import { take } from 'rxjs';
@ -29,8 +29,8 @@ export class FilterUserGrantsComponent extends FilterComponent implements OnInit
public SubQuery: any = SubQuery;
public searchQueries: UserGrantQuery[] = [];
constructor(router: Router, route: ActivatedRoute) {
super(router, route);
constructor(router: Router, route: ActivatedRoute, destroyRef: DestroyRef) {
super(router, route, destroyRef);
}
ngOnInit(): void {

View File

@ -1,4 +1,4 @@
<cnsl-filter (resetted)="resetFilter()" (trigger)="emitFilter()" [queryCount]="searchQueries.length">
<cnsl-filter (resetted)="resetFilter()" (trigger)="emitFilter()" [queryCount]="(filterChanged | async)?.length ?? 0">
<div class="filter-row" id="filtercomp">
<div class="name-query">
<mat-checkbox

View File

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, DestroyRef, OnInit } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { ActivatedRoute, Router } from '@angular/router';
import { take } from 'rxjs';
@ -13,6 +13,7 @@ import {
} from 'src/app/proto/generated/zitadel/user_pb';
import { FilterComponent } from '../filter/filter.component';
import { filter, map } from 'rxjs/operators';
enum SubQuery {
STATE,
@ -28,25 +29,27 @@ enum SubQuery {
})
export class FilterUserComponent extends FilterComponent implements OnInit {
public SubQuery: any = SubQuery;
public searchQueries: UserSearchQuery[] = [];
private searchQueries: UserSearchQuery[] = [];
public states: UserState[] = [
UserState.USER_STATE_ACTIVE,
UserState.USER_STATE_INACTIVE,
UserState.USER_STATE_DELETED,
UserState.USER_STATE_INITIAL,
UserState.USER_STATE_LOCKED,
UserState.USER_STATE_SUSPEND,
UserState.USER_STATE_INITIAL,
];
constructor(router: Router, route: ActivatedRoute) {
super(router, route);
constructor(router: Router, route: ActivatedRoute, destroyRef: DestroyRef) {
super(router, route, destroyRef);
}
ngOnInit(): void {
this.route.queryParams.pipe(take(1)).subscribe((params) => {
const { filter } = params;
if (filter) {
const stringifiedFilters = filter as string;
this.route.queryParamMap
.pipe(
take(1),
map((params) => params.get('filter')),
filter(Boolean),
)
.subscribe((stringifiedFilters) => {
const filters: UserSearchQuery.AsObject[] = JSON.parse(stringifiedFilters) as UserSearchQuery.AsObject[];
const userQueries = filters.map((filter) => {
@ -94,7 +97,6 @@ export class FilterUserComponent extends FilterComponent implements OnInit {
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
// this.showFilter = true;
// this.filterOpen.emit(true);
}
});
}

View File

@ -1,7 +1,6 @@
import { ConnectedPosition, ConnectionPositionPair } from '@angular/cdk/overlay';
import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
import { Component, DestroyRef, EventEmitter, Input, Output } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable, Subject, takeUntil } from 'rxjs';
import { SearchQuery as MemberSearchQuery } from 'src/app/proto/generated/zitadel/member_pb';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import { OrgQuery } from 'src/app/proto/generated/zitadel/org_pb';
@ -9,6 +8,7 @@ import { ProjectQuery } from 'src/app/proto/generated/zitadel/project_pb';
import { SearchQuery as UserSearchQuery, UserGrantQuery } from 'src/app/proto/generated/zitadel/user_pb';
import { ActionKeysType } from '../action-keys/action-keys.component';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
type FilterSearchQuery = UserSearchQuery | MemberSearchQuery | UserGrantQuery | ProjectQuery | OrgQuery;
type FilterSearchQueryAsObject =
@ -23,7 +23,7 @@ type FilterSearchQueryAsObject =
templateUrl: './filter.component.html',
styleUrls: ['./filter.component.scss'],
})
export class FilterComponent implements OnDestroy {
export class FilterComponent {
@Output() public filterChanged: EventEmitter<FilterSearchQuery[]> = new EventEmitter();
@Output() public filterOpen: EventEmitter<boolean> = new EventEmitter<boolean>(false);
@ -32,9 +32,6 @@ export class FilterComponent implements OnDestroy {
@Input() public queryCount: number = 0;
private destroy$: Subject<void> = new Subject();
public filterChanged$: Observable<FilterSearchQuery[]> = this.filterChanged.asObservable();
public showFilter: boolean = false;
public methods: TextQueryMethod[] = [
TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE,
@ -59,17 +56,13 @@ export class FilterComponent implements OnDestroy {
this.trigger.emit();
}
public ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
constructor(
private router: Router,
protected route: ActivatedRoute,
destroyRef: DestroyRef,
) {
const changes$ = this.filterChanged.asObservable();
changes$.pipe(takeUntil(this.destroy$)).subscribe((queries) => {
changes$.pipe(takeUntilDestroyed(destroyRef)).subscribe((queries) => {
const filters: Array<FilterSearchQueryAsObject | {}> | undefined = queries
?.map((q) => q.toObject())
.map((query) =>
@ -81,7 +74,8 @@ export class FilterComponent implements OnDestroy {
);
if (filters && Object.keys(filters)) {
this.router.navigate([], {
this.router
.navigate([], {
relativeTo: this.route,
queryParams: {
['filter']: JSON.stringify(filters),
@ -89,7 +83,8 @@ export class FilterComponent implements OnDestroy {
replaceUrl: true,
queryParamsHandling: 'merge',
skipLocationChange: false,
});
})
.then();
}
});
}

View File

@ -2,12 +2,11 @@
<cnsl-top-view
title="{{ userName$ | async }}"
sub="{{ user(userQuery)?.preferredLoginName }}"
[isActive]="user(userQuery)?.state === UserState.USER_STATE_ACTIVE"
[isInactive]="user(userQuery)?.state === UserState.USER_STATE_INACTIVE"
[isActive]="user(userQuery)?.state === UserState.ACTIVE"
[isInactive]="user(userQuery)?.state === UserState.INACTIVE"
stateTooltip="{{ 'USER.STATE.' + user(userQuery)?.state | translate }}"
[hasBackButton]="['org.read'] | hasRole | async"
>
<span *ngIf="userQuery.state === 'notfound'">{{ 'USER.PAGES.NOUSER' | translate }}</span>
<cnsl-info-row
topContent
*ngIf="user(userQuery) as user"
@ -42,7 +41,7 @@
[disabled]="false"
[genders]="genders"
[languages]="(langSvc.supported$ | async) || []"
[username]="user.userName"
[username]="user.username"
[profile]="profile"
[showEditImage]="true"
(changedLanguage)="changedLanguage($event)"
@ -93,7 +92,7 @@
</ng-container>
<ng-container *ngIf="currentSetting === 'idp'">
<cnsl-external-idps [userId]="user.id" [service]="grpcAuthService"></cnsl-external-idps>
<cnsl-external-idps [userId]="user.userId" [service]="grpcAuthService"></cnsl-external-idps>
</ng-container>
<ng-container *ngIf="currentSetting === 'security'">
@ -124,15 +123,14 @@
<cnsl-auth-passwordless #mfaComponent></cnsl-auth-passwordless>
<cnsl-auth-user-mfa
[phoneVerified]="humanUser(userQuery)?.type?.value?.phone?.isPhoneVerified ?? false"
#mfaComponent
[phoneVerified]="humanUser(userQuery)?.type?.value?.phone?.isVerified ?? false"
></cnsl-auth-user-mfa>
</ng-container>
<ng-container *ngIf="currentSetting === 'grants'">
<cnsl-card title="{{ 'GRANTS.USER.TITLE' | translate }}" description="{{ 'GRANTS.USER.DESCRIPTION' | translate }}">
<cnsl-user-grants
[userId]="user.id"
[userId]="user.userId"
[context]="USERGRANTCONTEXT"
[displayedColumns]="[
'org',
@ -165,7 +163,7 @@
*ngIf="metadataQuery.state !== 'error'"
[metadata]="metadataQuery.value"
[description]="'DESCRIPTIONS.USERS.SELF.METADATA' | translate"
[disabled]="(['user.write:' + user.id, 'user.write'] | hasRole | async) === false"
[disabled]="(['user.write:' + user.userId, 'user.write'] | hasRole | async) === false"
(editClicked)="editMetadata(user, metadataQuery.value)"
(refresh)="refreshMetadata$.next(true)"
[loading]="metadataQuery.state === 'loading'"

View File

@ -36,11 +36,10 @@ import { ToastService } from 'src/app/services/toast.service';
import { formatPhone } from 'src/app/utils/formatPhone';
import { EditDialogComponent, EditDialogData, EditDialogResult, EditDialogType } from './edit-dialog/edit-dialog.component';
import { LanguagesService } from 'src/app/services/languages.service';
import { Gender, HumanProfile } from '@zitadel/proto/zitadel/user/v2/user_pb';
import { Gender, HumanProfile, HumanUser, User, UserState } from '@zitadel/proto/zitadel/user/v2/user_pb';
import { catchError, filter, map, startWith, tap, withLatestFrom } from 'rxjs/operators';
import { pairwiseStartWith } from 'src/app/utils/pairwiseStartWith';
import { NewAuthService } from 'src/app/services/new-auth.service';
import { Human, User, UserState } from '@zitadel/proto/zitadel/user_pb';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NewMgmtService } from 'src/app/services/new-mgmt.service';
import { Metadata } from '@zitadel/proto/zitadel/metadata_pb';
@ -48,18 +47,14 @@ import { UserService } from 'src/app/services/user.service';
import { LoginPolicy } from '@zitadel/proto/zitadel/policy_pb';
import { query } from '@angular/animations';
type UserQuery =
| { state: 'success'; value: User }
| { state: 'error'; value: string }
| { state: 'loading'; value?: User }
| { state: 'notfound' };
type UserQuery = { state: 'success'; value: User } | { state: 'error'; value: string } | { state: 'loading'; value?: User };
type MetadataQuery =
| { state: 'success'; value: Metadata[] }
| { state: 'loading'; value: Metadata[] }
| { state: 'error'; value: string };
type UserWithHumanType = Omit<User, 'type'> & { type: { case: 'human'; value: Human } };
type UserWithHumanType = Omit<User, 'type'> & { type: { case: 'human'; value: HumanUser } };
@Component({
selector: 'cnsl-auth-user-detail',
@ -67,17 +62,17 @@ type UserWithHumanType = Omit<User, 'type'> & { type: { case: 'human'; value: Hu
styleUrls: ['./auth-user-detail.component.scss'],
})
export class AuthUserDetailComponent implements OnInit {
public genders: Gender[] = [Gender.MALE, Gender.FEMALE, Gender.DIVERSE];
protected readonly genders: Gender[] = [Gender.MALE, Gender.FEMALE, Gender.DIVERSE];
public ChangeType: any = ChangeType;
protected readonly ChangeType = ChangeType;
public userLoginMustBeDomain: boolean = false;
public UserState: any = UserState;
protected readonly UserState = UserState;
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.AUTHUSER;
public refreshChanges$: EventEmitter<void> = new EventEmitter();
public refreshMetadata$ = new Subject<true>();
protected USERGRANTCONTEXT: UserGrantContext = UserGrantContext.AUTHUSER;
protected readonly refreshChanges$: EventEmitter<void> = new EventEmitter();
protected readonly refreshMetadata$ = new Subject<true>();
public settingsList: SidenavSetting[] = [
protected readonly settingsList: SidenavSetting[] = [
{ id: 'general', i18nKey: 'USER.SETTINGS.GENERAL' },
{ id: 'security', i18nKey: 'USER.SETTINGS.SECURITY' },
{ id: 'idp', i18nKey: 'USER.SETTINGS.IDP' },
@ -92,9 +87,9 @@ export class AuthUserDetailComponent implements OnInit {
protected readonly user$: Observable<UserQuery>;
protected readonly metadata$: Observable<MetadataQuery>;
private readonly savedLanguage$: Observable<string>;
protected currentSetting$: Observable<string | undefined>;
public loginPolicy$: Observable<LoginPolicy>;
protected userName$: Observable<string>;
protected readonly currentSetting$: Observable<string | undefined>;
protected readonly loginPolicy$: Observable<LoginPolicy>;
protected readonly userName$: Observable<string>;
constructor(
public translate: TranslateService,
@ -209,13 +204,8 @@ export class AuthUserDetailComponent implements OnInit {
}
private getMyUser(): Observable<UserQuery> {
return defer(() => this.newAuthService.getMyUser()).pipe(
map(({ user }) => {
if (user) {
return { state: 'success', value: user } as const;
}
return { state: 'notfound' } as const;
}),
return defer(() => this.userService.getMyUser()).pipe(
map((user) => ({ state: 'success' as const, value: user })),
catchError((error) => of({ state: 'error', value: error.message ?? '' } as const)),
startWith({ state: 'loading' } as const),
);
@ -232,7 +222,7 @@ export class AuthUserDetailComponent implements OnInit {
if (!user.value) {
return EMPTY;
}
return this.getMetadataById(user.value.id);
return this.getMetadataById(user.value.userId);
}),
pairwiseStartWith(undefined),
map(([prev, curr]) => {
@ -259,7 +249,7 @@ export class AuthUserDetailComponent implements OnInit {
labelKey: 'ACTIONS.NEWVALUE' as const,
titleKey: 'USER.PROFILE.CHANGEUSERNAME_TITLE' as const,
descriptionKey: 'USER.PROFILE.CHANGEUSERNAME_DESC' as const,
value: user.userName,
value: user.username,
};
const dialogRef = this.dialog.open<EditDialogComponent, typeof data, EditDialogResult>(EditDialogComponent, {
data,
@ -271,8 +261,8 @@ export class AuthUserDetailComponent implements OnInit {
.pipe(
map((value) => value?.value),
filter(Boolean),
filter((value) => user.userName != value),
switchMap((username) => this.userService.updateUser({ userId: user.id, username })),
filter((value) => user.username != value),
switchMap((username) => this.userService.updateUser({ userId: user.userId, username })),
)
.subscribe({
next: () => {
@ -288,7 +278,7 @@ export class AuthUserDetailComponent implements OnInit {
public saveProfile(user: User, profile: HumanProfile): void {
this.userService
.updateUser({
userId: user.id,
userId: user.userId,
profile: {
givenName: profile.givenName,
familyName: profile.familyName,
@ -350,7 +340,7 @@ export class AuthUserDetailComponent implements OnInit {
public resendEmailVerification(user: User): void {
this.newMgmtService
.resendHumanEmailVerification(user.id)
.resendHumanEmailVerification(user.userId)
.then(() => {
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
this.refreshChanges$.emit();
@ -362,7 +352,7 @@ export class AuthUserDetailComponent implements OnInit {
public resendPhoneVerification(user: User): void {
this.newMgmtService
.resendHumanPhoneVerification(user.id)
.resendHumanPhoneVerification(user.userId)
.then(() => {
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
this.refreshChanges$.emit();
@ -374,7 +364,7 @@ export class AuthUserDetailComponent implements OnInit {
public deletePhone(user: User): void {
this.userService
.removePhone(user.id)
.removePhone(user.userId)
.then(() => {
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
this.refreshChanges$.emit();
@ -417,7 +407,7 @@ export class AuthUserDetailComponent implements OnInit {
filter((resp): resp is Required<EditDialogResult> => !!resp?.value),
switchMap(({ value, isVerified }) =>
this.userService.setEmail({
userId: user.id,
userId: user.userId,
email: value,
verification: isVerified ? { case: 'isVerified', value: isVerified } : { case: undefined },
}),
@ -453,7 +443,7 @@ export class AuthUserDetailComponent implements OnInit {
.pipe(
map((resp) => formatPhone(resp?.value)),
filter(Boolean),
switchMap(({ phone }) => this.userService.setPhone({ userId: user.id, phone })),
switchMap(({ phone }) => this.userService.setPhone({ userId: user.userId, phone })),
)
.subscribe({
next: () => {
@ -482,7 +472,7 @@ export class AuthUserDetailComponent implements OnInit {
.afterClosed()
.pipe(
filter(Boolean),
switchMap(() => this.userService.deleteUser(user.id)),
switchMap(() => this.userService.deleteUser(user.userId)),
)
.subscribe({
next: () => {
@ -498,9 +488,9 @@ export class AuthUserDetailComponent implements OnInit {
this.newMgmtService.setUserMetadata({
key,
value: Buffer.from(value),
id: user.id,
id: user.userId,
});
const removeFcn = (key: string): Promise<any> => this.newMgmtService.removeUserMetadata({ key, id: user.id });
const removeFcn = (key: string): Promise<any> => this.newMgmtService.removeUserMetadata({ key, id: user.userId });
const dialogRef = this.dialog.open<MetadataDialogComponent, MetadataDialogData>(MetadataDialogComponent, {
data: {

View File

@ -2,7 +2,7 @@
<div class="content">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="userName" required />
<input cnslInput formControlName="username" required />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}</cnsl-label>

View File

@ -1,5 +1,5 @@
<div class="max-width-container">
<div class="enlarged-container" [ngSwitch]="type">
<div class="enlarged-container">
<div class="users-title-row">
<h1>{{ 'DESCRIPTIONS.USERS.TITLE' | translate }}</h1>
<a mat-icon-button href="https://zitadel.com/docs/concepts/structure/users" rel="noreferrer" target="_blank">
@ -7,21 +7,6 @@
</a>
</div>
<p class="user-list-sub cnsl-secondary-text">{{ 'DESCRIPTIONS.USERS.DESCRIPTION' | translate }}</p>
<ng-container *ngSwitchCase="Type.TYPE_HUMAN">
<cnsl-user-table
[type]="Type.TYPE_HUMAN"
[canWrite$]="['user.write$'] | hasRole"
[canDelete$]="['user.delete$'] | hasRole"
>
</cnsl-user-table>
</ng-container>
<ng-container *ngSwitchCase="Type.TYPE_MACHINE">
<cnsl-user-table
[type]="Type.TYPE_MACHINE"
[canWrite$]="['user.write$'] | hasRole"
[canDelete$]="['user.delete$'] | hasRole"
>
</cnsl-user-table>
</ng-container>
<cnsl-user-table [canWrite$]="['user.write$'] | hasRole" [canDelete$]="['user.delete$'] | hasRole"> </cnsl-user-table>
</div>
</div>

View File

@ -1,8 +1,5 @@
import { Component } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { take } from 'rxjs/operators';
import { Type } from 'src/app/proto/generated/zitadel/user_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
@Component({
@ -11,23 +8,10 @@ import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/
styleUrls: ['./user-list.component.scss'],
})
export class UserListComponent {
public Type: any = Type;
public type: Type = Type.TYPE_HUMAN;
constructor(
public translate: TranslateService,
activatedRoute: ActivatedRoute,
protected readonly translate: TranslateService,
breadcrumbService: BreadcrumbService,
) {
activatedRoute.queryParams.pipe(take(1)).subscribe((params: Params) => {
const { type } = params;
if (type && type === 'human') {
this.type = Type.TYPE_HUMAN;
} else if (type && type === 'machine') {
this.type = Type.TYPE_MACHINE;
}
});
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],

View File

@ -1,11 +1,11 @@
<cnsl-refresh-table
[loading]="loading$ | async"
(refreshed)="refreshPage()"
[dataSize]="totalResult"
*ngIf="type$ | async as type"
[loading]="loading()"
(refreshed)="this.refresh$.next(true)"
[dataSize]="dataSize()"
[hideRefresh]="true"
[timestamp]="viewTimestamp"
[timestamp]="(users$ | async)?.details?.timestamp"
[selection]="selection"
[emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes"
[showBorder]="true"
>
<div leftActions class="user-toggle-group">
@ -60,12 +60,12 @@
<cnsl-filter-user
actions
*ngIf="!selection.hasValue()"
(filterChanged)="applySearchQuery($any($event))"
(filterChanged)="this.searchQueries$.next($any($event))"
(filterOpen)="filterOpen = $event"
></cnsl-filter-user>
<ng-template cnslHasRole [hasRole]="['user.write']" actions>
<button
(click)="gotoRouterLink(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
(click)="router.navigate(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
color="primary"
mat-raised-button
[disabled]="(canWrite$ | async) === false"
@ -77,7 +77,7 @@
<span>{{ 'ACTIONS.NEW' | translate }}</span>
<cnsl-action-keys
*ngIf="!filterOpen"
(actionTriggered)="gotoRouterLink(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
(actionTriggered)="router.navigate(['/users', type === Type.HUMAN ? 'create' : 'create-machine'])"
>
</cnsl-action-keys>
</div>
@ -85,7 +85,7 @@
</ng-template>
<div class="table-wrapper">
<table class="table" mat-table [dataSource]="dataSource" matSort (matSortChange)="sortChange($event)">
<table class="table" mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef>
<div class="selection">
@ -133,12 +133,7 @@
</ng-container>
<ng-container matColumnDef="displayName">
<th
mat-header-cell
*matHeaderCellDef
mat-sort-header
[ngClass]="{ 'search-active': this.userSearchKey === UserListSearchKey.DISPLAY_NAME }"
>
<th mat-header-cell *matHeaderCellDef mat-sort-header>
{{ 'USER.PROFILE.DISPLAYNAME' | translate }}
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
@ -148,12 +143,7 @@
</ng-container>
<ng-container matColumnDef="preferredLoginName">
<th
mat-header-cell
*matHeaderCellDef
mat-sort-header
[ngClass]="{ 'search-active': this.userSearchKey === UserListSearchKey.DISPLAY_NAME }"
>
<th mat-header-cell *matHeaderCellDef mat-sort-header>
{{ 'USER.PROFILE.PREFERREDLOGINNAME' | translate }}
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
@ -162,12 +152,7 @@
</ng-container>
<ng-container matColumnDef="username">
<th
mat-header-cell
*matHeaderCellDef
mat-sort-header
[ngClass]="{ 'search-active': this.userSearchKey === UserListSearchKey.USER_NAME }"
>
<th mat-header-cell *matHeaderCellDef mat-sort-header>
{{ 'USER.PROFILE.USERNAME' | translate }}
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
@ -176,12 +161,7 @@
</ng-container>
<ng-container matColumnDef="email">
<th
mat-header-cell
*matHeaderCellDef
mat-sort-header
[ngClass]="{ 'search-active': this.UserListSearchKey === UserListSearchKey.EMAIL }"
>
<th mat-header-cell *matHeaderCellDef mat-sort-header>
{{ 'USER.EMAIL' | translate }}
</th>
<td mat-cell *matCellDef="let user" [routerLink]="user.userId ? ['/users', user.userId] : null">
@ -250,17 +230,16 @@
</table>
</div>
<div *ngIf="(loading$ | async) === false && !dataSource?.data?.length" class="no-content-row">
<div *ngIf="!loading() && !dataSource?.data?.length" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{ 'USER.TABLE.EMPTY' | translate }}</span>
</div>
<cnsl-paginator
#paginator
class="paginator"
[length]="totalResult || 0"
[length]="dataSize()"
[pageSize]="INITIAL_PAGE_SIZE"
[timestamp]="viewTimestamp"
[timestamp]="(users$ | async)?.details?.timestamp"
[pageSizeOptions]="[10, 20, 50, 100]"
(page)="changePage($event)"
></cnsl-paginator>
<!-- (page)="changePage($event)"-->
</cnsl-refresh-table>

View File

@ -1,34 +1,45 @@
import { LiveAnnouncer } from '@angular/cdk/a11y';
import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnInit, Output, Signal, ViewChild } from '@angular/core';
import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, signal, Signal, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSort, Sort } from '@angular/material/sort';
import { MatSort, SortDirection } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { take } from 'rxjs/operators';
import {
combineLatestWith,
defer,
delay,
distinctUntilChanged,
EMPTY,
from,
Observable,
of,
ReplaySubject,
shareReplay,
switchMap,
toArray,
} from 'rxjs';
import { catchError, filter, finalize, map, startWith, take } from 'rxjs/operators';
import { enterAnimations } from 'src/app/animations';
import { ActionKeysType } from 'src/app/modules/action-keys/action-keys.component';
import { PageEvent, PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
import { PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
import { UserService } from 'src/app/services/user.service';
import { toSignal } from '@angular/core/rxjs-interop';
import { User } from 'src/app/proto/generated/zitadel/user_pb';
import { SearchQuery, SearchQuerySchema, Type, UserFieldName } from '@zitadel/proto/zitadel/user/v2/query_pb';
import { UserState, User as UserV2 } from '@zitadel/proto/zitadel/user/v2/user_pb';
import { create } from '@bufbuild/protobuf';
import { Timestamp } from '@bufbuild/protobuf/wkt';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { SearchQuery as UserSearchQuery } from 'src/app/proto/generated/zitadel/user_pb';
import { Type, UserFieldName } from '@zitadel/proto/zitadel/user/v2/query_pb';
import { UserState, User } from '@zitadel/proto/zitadel/user/v2/user_pb';
import { MessageInitShape } from '@bufbuild/protobuf';
import { ListUsersRequestSchema, ListUsersResponse } from '@zitadel/proto/zitadel/user/v2/user_service_pb';
import { AuthenticationService } from 'src/app/services/authentication.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { UserState as UserStateV1 } from 'src/app/proto/generated/zitadel/user_pb';
enum UserListSearchKey {
FIRST_NAME,
LAST_NAME,
DISPLAY_NAME,
USER_NAME,
EMAIL,
}
type Query = Exclude<
Exclude<MessageInitShape<typeof ListUsersRequestSchema>['queries'], undefined>[number]['query'],
undefined
>;
@Component({
selector: 'cnsl-user-table',
@ -37,25 +48,33 @@ enum UserListSearchKey {
animations: [enterAnimations],
})
export class UserTableComponent implements OnInit {
public userSearchKey: UserListSearchKey | undefined = undefined;
public Type = Type;
@Input() public type: Type = Type.HUMAN;
@Input() refreshOnPreviousRoutes: string[] = [];
protected readonly Type = Type;
protected readonly refresh$ = new ReplaySubject<true>(1);
@Input() public canWrite$: Observable<boolean> = of(false);
@Input() public canDelete$: Observable<boolean> = of(false);
private user: Signal<User.AsObject | undefined> = toSignal(this.authService.user, { requireSync: true });
protected readonly dataSize: Signal<number>;
protected readonly loading = signal(false);
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
@ViewChild(MatSort) public sort!: MatSort;
public INITIAL_PAGE_SIZE: number = 20;
private readonly paginator$ = new ReplaySubject<PaginatorComponent>(1);
@ViewChild(PaginatorComponent) public set paginator(paginator: PaginatorComponent) {
this.paginator$.next(paginator);
}
private readonly sort$ = new ReplaySubject<MatSort>(1);
@ViewChild(MatSort) public set sort(sort: MatSort) {
this.sort$.next(sort);
}
protected readonly INITIAL_PAGE_SIZE = 20;
protected readonly dataSource: MatTableDataSource<User> = new MatTableDataSource<User>();
protected readonly selection: SelectionModel<User> = new SelectionModel<User>(true, []);
protected readonly users$: Observable<ListUsersResponse>;
protected readonly type$: Observable<Type>;
protected readonly searchQueries$ = new ReplaySubject<UserSearchQuery[]>(1);
protected readonly myUser: Signal<User | undefined>;
public viewTimestamp!: Timestamp;
public totalResult: number = 0;
public dataSource: MatTableDataSource<UserV2> = new MatTableDataSource<UserV2>();
public selection: SelectionModel<UserV2> = new SelectionModel<UserV2>(true, []);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
@Input() public displayedColumnsHuman: string[] = [
'select',
'displayName',
@ -76,46 +95,56 @@ export class UserTableComponent implements OnInit {
'actions',
];
@Output() public changedSelection: EventEmitter<Array<UserV2>> = new EventEmitter();
@Output() public changedSelection: EventEmitter<Array<User>> = new EventEmitter();
public UserState: any = UserState;
public UserListSearchKey: any = UserListSearchKey;
protected readonly UserState = UserState;
public ActionKeysType: any = ActionKeysType;
public filterOpen: boolean = false;
protected ActionKeysType = ActionKeysType;
protected filterOpen: boolean = false;
private searchQueries: SearchQuery[] = [];
constructor(
private router: Router,
public translate: TranslateService,
private authService: GrpcAuthService,
private userService: UserService,
private toast: ToastService,
private dialog: MatDialog,
private route: ActivatedRoute,
private _liveAnnouncer: LiveAnnouncer,
protected readonly router: Router,
public readonly translate: TranslateService,
private readonly userService: UserService,
private readonly toast: ToastService,
private readonly dialog: MatDialog,
private readonly route: ActivatedRoute,
private readonly destroyRef: DestroyRef,
private readonly authenticationService: AuthenticationService,
private readonly authService: GrpcAuthService,
) {
this.selection.changed.subscribe(() => {
this.changedSelection.emit(this.selection.selected);
});
this.type$ = this.getType$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
this.users$ = this.getUsers(this.type$).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
this.myUser = toSignal(this.getMyUser());
this.dataSize = toSignal(
this.users$.pipe(
map((users) => users.result.length),
distinctUntilChanged(),
),
{ initialValue: 0 },
);
}
ngOnInit(): void {
this.route.queryParams.pipe(take(1)).subscribe((params) => {
if (!params['filter']) {
this.getData(this.INITIAL_PAGE_SIZE, 0, this.type, this.searchQueries).then();
}
if (params['deferredReload']) {
setTimeout(() => {
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize, this.type).then();
}, 2000);
}
this.selection.changed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.changedSelection.emit(this.selection.selected);
});
this.users$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((users) => (this.dataSource.data = users.result));
this.route.queryParamMap
.pipe(
map((params) => params.get('deferredReload')),
filter(Boolean),
take(1),
delay(2000),
takeUntilDestroyed(this.destroyRef),
)
.subscribe(() => this.refresh$.next(true));
}
public setType(type: Type): void {
this.type = type;
setType(type: Type) {
this.router
.navigate([], {
relativeTo: this.route,
@ -127,12 +156,195 @@ export class UserTableComponent implements OnInit {
skipLocationChange: false,
})
.then();
this.getData(
this.paginator.pageSize,
this.paginator.pageIndex * this.paginator.pageSize,
this.type,
this.searchQueries,
).then();
}
private getMyUser() {
return defer(() => this.userService.getMyUser()).pipe(
catchError((error) => {
this.toast.showError(error);
return EMPTY;
}),
);
}
private getType$(): Observable<Type> {
return this.route.queryParamMap.pipe(
map((params) => params.get('type')),
filter(Boolean),
map((type) => (type === 'machine' ? Type.MACHINE : Type.HUMAN)),
startWith(Type.HUMAN),
distinctUntilChanged(),
);
}
private getDirection$() {
return this.sort$.pipe(
switchMap((sort) =>
sort.sortChange.pipe(
map(({ direction }) => direction),
startWith(sort.direction),
),
),
distinctUntilChanged(),
);
}
private getSortingColumn$() {
return this.sort$.pipe(
switchMap((sort) =>
sort.sortChange.pipe(
map(({ active }) => active),
startWith(sort.active),
),
),
map((active) => {
switch (active) {
case 'displayName':
return UserFieldName.DISPLAY_NAME;
case 'username':
return UserFieldName.USER_NAME;
case 'preferredLoginName':
// TODO: replace with preferred username sorting once implemented
return UserFieldName.USER_NAME;
case 'email':
return UserFieldName.EMAIL;
case 'state':
return UserFieldName.STATE;
case 'creationDate':
return UserFieldName.CREATION_DATE;
default:
return undefined;
}
}),
distinctUntilChanged(),
);
}
private getQueries(type$: Observable<Type>): Observable<Query[]> {
const activeOrgId$ = this.getActiveOrgId();
return this.searchQueries$.pipe(
startWith([]),
combineLatestWith(type$, activeOrgId$),
switchMap(([queries, type, organizationId]) =>
from(queries).pipe(
map((query) => this.searchQueryToV2(query.toObject())),
startWith({ case: 'typeQuery' as const, value: { type } }),
startWith(organizationId ? { case: 'organizationIdQuery' as const, value: { organizationId } } : undefined),
filter(Boolean),
toArray(),
),
),
);
}
private searchQueryToV2(query: UserSearchQuery.AsObject): Query | undefined {
if (query.userNameQuery) {
return {
case: 'userNameQuery' as const,
value: {
userName: query.userNameQuery.userName,
method: query.userNameQuery.method as unknown as any,
},
};
} else if (query.displayNameQuery) {
return {
case: 'displayNameQuery' as const,
value: {
displayName: query.displayNameQuery.displayName,
method: query.displayNameQuery.method as unknown as any,
},
};
} else if (query.emailQuery) {
return {
case: 'emailQuery' as const,
value: {
emailAddress: query.emailQuery.emailAddress,
method: query.emailQuery.method as unknown as any,
},
};
} else if (query.stateQuery) {
return {
case: 'stateQuery' as const,
value: {
state: this.toV2State(query.stateQuery.state),
},
};
} else {
return undefined;
}
}
private toV2State(state: UserStateV1) {
switch (state) {
case UserStateV1.USER_STATE_ACTIVE:
return UserState.ACTIVE;
case UserStateV1.USER_STATE_INACTIVE:
return UserState.INACTIVE;
case UserStateV1.USER_STATE_DELETED:
return UserState.DELETED;
case UserStateV1.USER_STATE_LOCKED:
return UserState.LOCKED;
case UserStateV1.USER_STATE_INITIAL:
return UserState.INITIAL;
default:
throw new Error(`Invalid UserState ${state}`);
}
}
private getUsers(type$: Observable<Type>) {
const queries$ = this.getQueries(type$);
const direction$ = this.getDirection$();
const sortingColumn$ = this.getSortingColumn$();
const page$ = this.paginator$.pipe(switchMap((paginator) => paginator.page));
const pageSize$ = page$.pipe(
map(({ pageSize }) => pageSize),
startWith(this.INITIAL_PAGE_SIZE),
distinctUntilChanged(),
);
const pageIndex$ = page$.pipe(
map(({ pageIndex }) => pageIndex),
startWith(0),
distinctUntilChanged(),
);
return this.refresh$.pipe(
startWith(true),
combineLatestWith(queries$, direction$, sortingColumn$, pageSize$, pageIndex$),
switchMap(([_, queries, direction, sortingColumn, pageSize, pageIndex]) => {
return this.fetchUsers(queries, direction, sortingColumn, pageSize, pageIndex);
}),
);
}
private fetchUsers(
queries: Query[],
direction: SortDirection,
sortingColumn: UserFieldName | undefined,
pageSize: number,
pageIndex: number,
) {
return defer(() => {
const req = {
query: {
limit: pageSize,
offset: BigInt(pageIndex * pageSize),
asc: direction === 'asc',
},
sortingColumn,
queries: queries.map((query) => ({ query })),
};
this.loading.set(true);
return this.userService.listUsers(req);
}).pipe(
catchError((error) => {
this.toast.showError(error);
return EMPTY;
}),
finalize(() => this.loading.set(false)),
);
}
public isAllSelected(): boolean {
@ -145,147 +357,49 @@ export class UserTableComponent implements OnInit {
this.isAllSelected() ? this.selection.clear() : this.dataSource.data.forEach((row) => this.selection.select(row));
}
public changePage(event: PageEvent): void {
this.selection.clear();
this.getData(event.pageSize, event.pageIndex * event.pageSize, this.type, this.searchQueries).then();
}
public deactivateSelectedUsers(): void {
public async deactivateSelectedUsers(): Promise<void> {
const usersToDeactivate = this.selection.selected
.filter((u) => u.state === UserState.ACTIVE)
.map((value) => {
return this.userService.deactivateUser(value.userId);
});
Promise.all(usersToDeactivate)
.then(() => {
try {
await Promise.all(usersToDeactivate);
} catch (error) {
this.toast.showError(error);
return;
}
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true);
this.selection.clear();
setTimeout(() => {
this.refreshPage();
this.refresh$.next(true);
}, 1000);
})
.catch((error) => {
this.toast.showError(error);
});
}
public reactivateSelectedUsers(): void {
public async reactivateSelectedUsers(): Promise<void> {
const usersToReactivate = this.selection.selected
.filter((u) => u.state === UserState.INACTIVE)
.map((value) => {
return this.userService.reactivateUser(value.userId);
});
Promise.all(usersToReactivate)
.then(() => {
try {
await Promise.all(usersToReactivate);
} catch (error) {
this.toast.showError(error);
return;
}
this.toast.showInfo('USER.TOAST.SELECTEDREACTIVATED', true);
this.selection.clear();
setTimeout(() => {
this.refreshPage();
this.refresh$.next(true);
}, 1000);
})
.catch((error) => {
this.toast.showError(error);
});
}
public gotoRouterLink(rL: any): Promise<boolean> {
return this.router.navigate(rL);
}
private async getData(limit: number, offset: number, type: Type, searchQueries?: SearchQuery[]): Promise<void> {
this.loadingSubject.next(true);
let queryT = create(SearchQuerySchema, {
query: {
case: 'typeQuery',
value: {
type,
},
},
});
let sortingField: UserFieldName | undefined = undefined;
if (this.sort?.active && this.sort?.direction)
switch (this.sort.active) {
case 'displayName':
sortingField = UserFieldName.DISPLAY_NAME;
break;
case 'username':
sortingField = UserFieldName.USER_NAME;
break;
case 'preferredLoginName':
// TODO: replace with preferred username sorting once implemented
sortingField = UserFieldName.USER_NAME;
break;
case 'email':
sortingField = UserFieldName.EMAIL;
break;
case 'state':
sortingField = UserFieldName.STATE;
break;
case 'creationDate':
sortingField = UserFieldName.CREATION_DATE;
break;
}
this.userService
.listUsers(
limit,
offset,
searchQueries?.length ? [queryT, ...searchQueries] : [queryT],
sortingField,
this.sort?.direction,
)
.then((resp) => {
if (resp.details?.totalResult) {
this.totalResult = Number(resp.details.totalResult);
} else {
this.totalResult = 0;
}
if (resp.details?.timestamp) {
this.viewTimestamp = resp.details?.timestamp;
}
this.dataSource.data = resp.result;
this.loadingSubject.next(false);
})
.catch((error) => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
}
public refreshPage(): void {
this.getData(
this.paginator.pageSize,
this.paginator.pageIndex * this.paginator.pageSize,
this.type,
this.searchQueries,
).then();
}
public sortChange(sortState: Sort) {
if (sortState.direction && sortState.active) {
this._liveAnnouncer.announce(`Sorted ${sortState.direction} ending`).then();
this.refreshPage();
} else {
this._liveAnnouncer.announce('Sorting cleared').then();
}
}
public applySearchQuery(searchQueries: SearchQuery[]): void {
this.selection.clear();
this.searchQueries = searchQueries;
this.getData(
this.paginator ? this.paginator.pageSize : this.INITIAL_PAGE_SIZE,
this.paginator ? this.paginator.pageIndex * this.paginator.pageSize : 0,
this.type,
searchQueries,
).then();
}
public deleteUser(user: UserV2): void {
public deleteUser(user: User): void {
const authUserData = {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
@ -309,8 +423,9 @@ export class UserTableComponent implements OnInit {
};
if (user?.userId) {
const authUser = this.user();
const isMe = authUser?.id === user.userId;
const authUser = this.myUser();
console.log('my user', authUser);
const isMe = authUser?.userId === user.userId;
let dialogRef;
@ -326,21 +441,21 @@ export class UserTableComponent implements OnInit {
});
}
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.userService
.deleteUser(user.userId)
.then(() => {
dialogRef
.afterClosed()
.pipe(
filter(Boolean),
switchMap(() => this.userService.deleteUser(user.userId)),
)
.subscribe({
next: () => {
setTimeout(() => {
this.refreshPage();
this.refresh$.next(true);
}, 1000);
this.selection.clear();
this.toast.showInfo('USER.TOAST.DELETED', true);
})
.catch((error) => {
this.toast.showError(error);
});
}
},
error: (err) => this.toast.showError(err),
});
}
}
@ -354,4 +469,20 @@ export class UserTableComponent implements OnInit {
const selected = this.selection.selected;
return selected ? selected.findIndex((user) => user.state !== UserState.INACTIVE) > -1 : false;
}
private getActiveOrgId() {
return this.authenticationService.authenticationChanged.pipe(
startWith(true),
filter(Boolean),
switchMap(() =>
from(this.authService.getActiveOrg()).pipe(
catchError((err) => {
this.toast.showError(err);
return of(undefined);
}),
),
),
map((org) => org?.id),
);
}
}

View File

@ -1,4 +1,4 @@
import { Injectable } from '@angular/core';
import { DestroyRef, Injectable } from '@angular/core';
import { GrpcService } from './grpc.service';
import {
AddHumanUserRequestSchema,
@ -11,7 +11,6 @@ import {
DeactivateUserResponse,
DeleteUserRequestSchema,
DeleteUserResponse,
GetUserByIDRequestSchema,
GetUserByIDResponse,
ListAuthenticationFactorsRequestSchema,
ListAuthenticationFactorsResponse,
@ -65,60 +64,87 @@ import {
} 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 { Details, DetailsSchema } from '@zitadel/proto/zitadel/object/v2/object_pb';
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';
import { OAuthService } from 'angular-oauth2-oidc';
import { firstValueFrom, Observable, shareReplay } from 'rxjs';
import { filter, map, startWith, tap, timeout } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Injectable({
providedIn: 'root',
})
export class UserService {
constructor(private readonly grpcService: GrpcService) {}
private readonly userId$: Observable<string>;
private user: UserV2 | undefined;
constructor(
private readonly grpcService: GrpcService,
private readonly oauthService: OAuthService,
destroyRef: DestroyRef,
) {
this.userId$ = this.getUserId().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
// this preloads the userId and deletes the cache everytime the userId changes
this.userId$.pipe(takeUntilDestroyed(destroyRef)).subscribe(async () => {
this.user = undefined;
try {
await this.getMyUser();
} catch (error) {
console.warn(error);
}
});
}
private getUserId() {
return this.oauthService.events.pipe(
filter((event) => event.type === 'token_received'),
startWith(this.oauthService.getIdToken),
map(() => this.oauthService.getIdToken()),
filter(Boolean),
// split jwt and get base64 encoded payload
map((token) => token.split('.')[1]),
// decode payload
map(atob),
// parse payload
map((payload) => JSON.parse(payload)),
map((payload: unknown) => {
// check if sub is in payload and is a string
if (payload && typeof payload === 'object' && 'sub' in payload && typeof payload.sub === 'string') {
return payload.sub;
}
throw new Error('Invalid payload');
}),
);
}
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;
}
public listUsers(req: MessageInitShape<typeof ListUsersRequestSchema>): Promise<ListUsersResponse> {
return this.grpcService.userNew.listUsers(req);
}
public async getMyUser(): Promise<UserV2> {
const userId = await firstValueFrom(this.userId$.pipe(timeout(2000)));
if (this.user) {
return this.user;
}
const resp = await this.getUserById(userId);
if (!resp.user) {
throw new Error("Couldn't find user");
}
this.user = resp.user;
return resp.user;
}
public getUserById(userId: string): Promise<GetUserByIDResponse> {
return this.grpcService.userNew.getUserByID(create(GetUserByIDRequestSchema, { userId }));
return this.grpcService.userNew.getUserByID({ userId });
}
public deactivateUser(userId: string): Promise<DeactivateUserResponse> {