mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-23 05:07:35 +00:00
feat(console): list v2 sessions (#9539)
# Which Problems Are Solved With the new login comes a new API. Sessions must be listed using the new sessions api. This PR checks for the new login feature flag to be enabled and lists the users sessions using the new API. --------- Co-authored-by: conblem <mail@conblem.me>
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
<div class="accounts-card">
|
<div class="accounts-card" *ngIf="user$ | async as user">
|
||||||
<cnsl-avatar
|
<cnsl-avatar
|
||||||
(click)="editUserProfile()"
|
(click)="editUserProfile()"
|
||||||
*ngIf="user"
|
|
||||||
class="avatar"
|
class="avatar"
|
||||||
[ngClass]="{ 'iam-user': iamuser }"
|
[ngClass]="{ 'iam-user': iamuser }"
|
||||||
[forColor]="user.preferredLoginName"
|
[forColor]="user.preferredLoginName"
|
||||||
@@ -16,8 +15,8 @@
|
|||||||
|
|
||||||
<button (click)="editUserProfile()" mat-stroked-button>{{ 'USER.EDITACCOUNT' | translate }}</button>
|
<button (click)="editUserProfile()" mat-stroked-button>{{ 'USER.EDITACCOUNT' | translate }}</button>
|
||||||
<div class="l-accounts">
|
<div class="l-accounts">
|
||||||
<mat-progress-bar *ngIf="loadingUsers" color="primary" mode="indeterminate"></mat-progress-bar>
|
<mat-progress-bar *ngIf="(sessions$ | async) === null" color="primary" mode="indeterminate"></mat-progress-bar>
|
||||||
<a class="row" *ngFor="let session of sessions" (click)="selectAccount(session.loginName)">
|
<a class="row" *ngFor="let session of sessions$ | async" (click)="selectAccount(session.loginName)">
|
||||||
<cnsl-avatar
|
<cnsl-avatar
|
||||||
*ngIf="session && session.displayName"
|
*ngIf="session && session.displayName"
|
||||||
class="small-avatar"
|
class="small-avatar"
|
||||||
@@ -31,7 +30,7 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<span class="user-title">{{ session.displayName ? session.displayName : session.userName }} </span>
|
<span class="user-title">{{ session.displayName ? session.displayName : session.userName }} </span>
|
||||||
<span class="loginname">{{ session.loginName }}</span>
|
<span class="loginname">{{ session.loginName }}</span>
|
||||||
<span class="state inactive" *ngIf="session.authState === UserState.USER_STATE_INACTIVE">{{
|
<span class="state inactive" *ngIf="$any(session.authState) === UserState.USER_STATE_INACTIVE">{{
|
||||||
'USER.STATE.' + session.authState | translate
|
'USER.STATE.' + session.authState | translate
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,64 +1,156 @@
|
|||||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { AuthConfig } from 'angular-oauth2-oidc';
|
import { AuthConfig } from 'angular-oauth2-oidc';
|
||||||
import { Session, User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
|
import { SessionState as V1SessionState, User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
|
||||||
import { AuthenticationService } from 'src/app/services/authentication.service';
|
import { AuthenticationService } from 'src/app/services/authentication.service';
|
||||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||||
import { toSignal } from '@angular/core/rxjs-interop';
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
|
import { SessionService } from 'src/app/services/session.service';
|
||||||
|
import {
|
||||||
|
catchError,
|
||||||
|
defer,
|
||||||
|
from,
|
||||||
|
map,
|
||||||
|
mergeMap,
|
||||||
|
Observable,
|
||||||
|
of,
|
||||||
|
ReplaySubject,
|
||||||
|
shareReplay,
|
||||||
|
switchMap,
|
||||||
|
timeout,
|
||||||
|
TimeoutError,
|
||||||
|
toArray,
|
||||||
|
} from 'rxjs';
|
||||||
|
import { NewFeatureService } from 'src/app/services/new-feature.service';
|
||||||
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
|
import { SessionState as V2SessionState } from '@zitadel/proto/zitadel/user_pb';
|
||||||
|
import { filter, withLatestFrom } from 'rxjs/operators';
|
||||||
|
|
||||||
|
interface V1AndV2Session {
|
||||||
|
displayName: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
loginName: string;
|
||||||
|
userName: string;
|
||||||
|
authState: V1SessionState | V2SessionState;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cnsl-accounts-card',
|
selector: 'cnsl-accounts-card',
|
||||||
templateUrl: './accounts-card.component.html',
|
templateUrl: './accounts-card.component.html',
|
||||||
styleUrls: ['./accounts-card.component.scss'],
|
styleUrls: ['./accounts-card.component.scss'],
|
||||||
})
|
})
|
||||||
export class AccountsCardComponent implements OnInit {
|
export class AccountsCardComponent {
|
||||||
@Input() public user?: User.AsObject;
|
@Input({ required: true })
|
||||||
@Input() public iamuser: boolean | null = false;
|
public set user(user: User.AsObject) {
|
||||||
|
this.user$.next(user);
|
||||||
@Output() public closedCard: EventEmitter<void> = new EventEmitter();
|
|
||||||
public sessions: Session.AsObject[] = [];
|
|
||||||
public loadingUsers: boolean = false;
|
|
||||||
public UserState: any = UserState;
|
|
||||||
private labelpolicy = toSignal(this.userService.labelpolicy$, { initialValue: undefined });
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public authService: AuthenticationService,
|
|
||||||
private router: Router,
|
|
||||||
private userService: GrpcAuthService,
|
|
||||||
) {
|
|
||||||
this.userService
|
|
||||||
.listMyUserSessions()
|
|
||||||
.then((sessions) => {
|
|
||||||
this.sessions = sessions.resultList.filter((user) => user.loginName !== this.user?.preferredLoginName);
|
|
||||||
this.loadingUsers = false;
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
this.loadingUsers = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
@Input() public iamuser: boolean | null = false;
|
||||||
this.loadingUsers = true;
|
|
||||||
|
@Output() public closedCard = new EventEmitter<void>();
|
||||||
|
|
||||||
|
protected readonly user$ = new ReplaySubject<User.AsObject>(1);
|
||||||
|
protected readonly UserState = UserState;
|
||||||
|
private readonly labelpolicy = toSignal(this.userService.labelpolicy$, { initialValue: undefined });
|
||||||
|
protected readonly sessions$: Observable<V1AndV2Session[] | undefined>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly authService: AuthenticationService,
|
||||||
|
private readonly router: Router,
|
||||||
|
private readonly userService: GrpcAuthService,
|
||||||
|
private readonly sessionService: SessionService,
|
||||||
|
private readonly featureService: NewFeatureService,
|
||||||
|
private readonly toast: ToastService,
|
||||||
|
) {
|
||||||
|
this.sessions$ = this.getSessions().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUseLoginV2() {
|
||||||
|
return defer(() => this.featureService.getInstanceFeatures()).pipe(
|
||||||
|
map(({ loginV2 }) => loginV2?.required ?? false),
|
||||||
|
timeout(1000),
|
||||||
|
catchError((err) => {
|
||||||
|
if (!(err instanceof TimeoutError)) {
|
||||||
|
this.toast.showError(err);
|
||||||
|
}
|
||||||
|
return of(false);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSessions(): Observable<V1AndV2Session[]> {
|
||||||
|
const useLoginV2$ = this.getUseLoginV2();
|
||||||
|
|
||||||
|
return useLoginV2$.pipe(
|
||||||
|
switchMap((useLoginV2) => {
|
||||||
|
if (useLoginV2) {
|
||||||
|
return this.getV2Sessions();
|
||||||
|
} else {
|
||||||
|
return this.getV1Sessions();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
catchError((err) => {
|
||||||
|
this.toast.showError(err);
|
||||||
|
return of([]);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getV1Sessions(): Observable<V1AndV2Session[]> {
|
||||||
|
return defer(() => this.userService.listMyUserSessions()).pipe(
|
||||||
|
mergeMap(({ resultList }) => from(resultList)),
|
||||||
|
withLatestFrom(this.user$),
|
||||||
|
filter(([{ loginName }, user]) => loginName !== user.preferredLoginName),
|
||||||
|
map(([s]) => ({
|
||||||
|
displayName: s.displayName,
|
||||||
|
avatarUrl: s.avatarUrl,
|
||||||
|
loginName: s.loginName,
|
||||||
|
authState: s.authState,
|
||||||
|
userName: s.userName,
|
||||||
|
})),
|
||||||
|
toArray(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getV2Sessions(): Observable<V1AndV2Session[]> {
|
||||||
|
return defer(() =>
|
||||||
|
this.sessionService.listSessions({
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
case: 'userAgentQuery',
|
||||||
|
value: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).pipe(
|
||||||
|
mergeMap(({ sessions }) => from(sessions)),
|
||||||
|
withLatestFrom(this.user$),
|
||||||
|
filter(([s, user]) => s.factors?.user?.loginName !== user.preferredLoginName),
|
||||||
|
map(([s]) => ({
|
||||||
|
displayName: s.factors?.user?.displayName ?? '',
|
||||||
|
avatarUrl: '',
|
||||||
|
loginName: s.factors?.user?.loginName ?? '',
|
||||||
|
authState: V2SessionState.ACTIVE,
|
||||||
|
userName: s.factors?.user?.loginName ?? '',
|
||||||
|
})),
|
||||||
|
toArray(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public editUserProfile(): void {
|
public editUserProfile(): void {
|
||||||
this.router.navigate(['users/me']);
|
this.router.navigate(['users/me']).then();
|
||||||
this.closedCard.emit();
|
this.closedCard.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeCard(element: HTMLElement): void {
|
|
||||||
if (!element.classList.contains('dontcloseonclick')) {
|
|
||||||
this.closedCard.emit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public selectAccount(loginHint: string): void {
|
public selectAccount(loginHint: string): void {
|
||||||
const configWithPrompt: Partial<AuthConfig> = {
|
const configWithPrompt: Partial<AuthConfig> = {
|
||||||
customQueryParams: {
|
customQueryParams: {
|
||||||
login_hint: loginHint,
|
login_hint: loginHint,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.authService.authenticate(configWithPrompt);
|
this.authService.authenticate(configWithPrompt).then();
|
||||||
}
|
}
|
||||||
|
|
||||||
public selectNewAccount(): void {
|
public selectNewAccount(): void {
|
||||||
@@ -67,7 +159,7 @@ export class AccountsCardComponent implements OnInit {
|
|||||||
prompt: 'login',
|
prompt: 'login',
|
||||||
} as any,
|
} as any,
|
||||||
};
|
};
|
||||||
this.authService.authenticate(configWithPrompt);
|
this.authService.authenticate(configWithPrompt).then();
|
||||||
}
|
}
|
||||||
|
|
||||||
public logout(): void {
|
public logout(): void {
|
||||||
|
@@ -15,7 +15,7 @@ import { ActionKeysType } from '../action-keys/action-keys.component';
|
|||||||
})
|
})
|
||||||
export class HeaderComponent {
|
export class HeaderComponent {
|
||||||
@Input() public isDarkTheme: boolean = true;
|
@Input() public isDarkTheme: boolean = true;
|
||||||
@Input() public user?: User.AsObject;
|
@Input({ required: true }) public user!: User.AsObject;
|
||||||
public showOrgContext: boolean = false;
|
public showOrgContext: boolean = false;
|
||||||
|
|
||||||
@Input() public org!: Org.AsObject;
|
@Input() public org!: Org.AsObject;
|
||||||
|
@@ -18,7 +18,7 @@ import { NewConnectWebOrgInterceptor, OrgInterceptor, OrgInterceptorProvider } f
|
|||||||
import { StorageService } from './storage.service';
|
import { StorageService } from './storage.service';
|
||||||
import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb';
|
import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb';
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
import { createFeatureServiceClient, createUserServiceClient } from '@zitadel/client/v2';
|
import { createFeatureServiceClient, createUserServiceClient, createSessionServiceClient } from '@zitadel/client/v2';
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
import { createAuthServiceClient, createManagementServiceClient } from '@zitadel/client/v1';
|
import { createAuthServiceClient, createManagementServiceClient } from '@zitadel/client/v1';
|
||||||
import { createGrpcWebTransport } from '@connectrpc/connect-web';
|
import { createGrpcWebTransport } from '@connectrpc/connect-web';
|
||||||
@@ -38,6 +38,7 @@ export class GrpcService {
|
|||||||
public admin!: AdminServiceClient;
|
public admin!: AdminServiceClient;
|
||||||
public user!: UserServiceClient;
|
public user!: UserServiceClient;
|
||||||
public userNew!: ReturnType<typeof createUserServiceClient>;
|
public userNew!: ReturnType<typeof createUserServiceClient>;
|
||||||
|
public session!: ReturnType<typeof createSessionServiceClient>;
|
||||||
public mgmtNew!: ReturnType<typeof createManagementServiceClient>;
|
public mgmtNew!: ReturnType<typeof createManagementServiceClient>;
|
||||||
public authNew!: ReturnType<typeof createAuthServiceClient>;
|
public authNew!: ReturnType<typeof createAuthServiceClient>;
|
||||||
public featureNew!: ReturnType<typeof createFeatureServiceClient>;
|
public featureNew!: ReturnType<typeof createFeatureServiceClient>;
|
||||||
@@ -47,7 +48,6 @@ export class GrpcService {
|
|||||||
private readonly envService: EnvironmentService,
|
private readonly envService: EnvironmentService,
|
||||||
private readonly platformLocation: PlatformLocation,
|
private readonly platformLocation: PlatformLocation,
|
||||||
private readonly authenticationService: AuthenticationService,
|
private readonly authenticationService: AuthenticationService,
|
||||||
private readonly storageService: StorageService,
|
|
||||||
private readonly translate: TranslateService,
|
private readonly translate: TranslateService,
|
||||||
private readonly exhaustedService: ExhaustedService,
|
private readonly exhaustedService: ExhaustedService,
|
||||||
private readonly authInterceptor: AuthInterceptor,
|
private readonly authInterceptor: AuthInterceptor,
|
||||||
@@ -112,6 +112,7 @@ export class GrpcService {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
this.userNew = createUserServiceClient(transport);
|
this.userNew = createUserServiceClient(transport);
|
||||||
|
this.session = createSessionServiceClient(transport);
|
||||||
this.mgmtNew = createManagementServiceClient(transportOldAPIs);
|
this.mgmtNew = createManagementServiceClient(transportOldAPIs);
|
||||||
this.authNew = createAuthServiceClient(transport);
|
this.authNew = createAuthServiceClient(transport);
|
||||||
this.featureNew = createFeatureServiceClient(transport);
|
this.featureNew = createFeatureServiceClient(transport);
|
||||||
|
15
console/src/app/services/session.service.ts
Normal file
15
console/src/app/services/session.service.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { GrpcService } from './grpc.service';
|
||||||
|
import type { MessageInitShape } from '@bufbuild/protobuf';
|
||||||
|
import { ListSessionsRequestSchema, ListSessionsResponse } from '@zitadel/proto/zitadel/session/v2/session_service_pb';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class SessionService {
|
||||||
|
constructor(private readonly grpcService: GrpcService) {}
|
||||||
|
|
||||||
|
public listSessions(req: MessageInitShape<typeof ListSessionsRequestSchema>): Promise<ListSessionsResponse> {
|
||||||
|
return this.grpcService.session.listSessions(req);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user