mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-22 06:28:24 +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
|
||||
(click)="editUserProfile()"
|
||||
*ngIf="user"
|
||||
class="avatar"
|
||||
[ngClass]="{ 'iam-user': iamuser }"
|
||||
[forColor]="user.preferredLoginName"
|
||||
@@ -16,8 +15,8 @@
|
||||
|
||||
<button (click)="editUserProfile()" mat-stroked-button>{{ 'USER.EDITACCOUNT' | translate }}</button>
|
||||
<div class="l-accounts">
|
||||
<mat-progress-bar *ngIf="loadingUsers" color="primary" mode="indeterminate"></mat-progress-bar>
|
||||
<a class="row" *ngFor="let session of sessions" (click)="selectAccount(session.loginName)">
|
||||
<mat-progress-bar *ngIf="(sessions$ | async) === null" color="primary" mode="indeterminate"></mat-progress-bar>
|
||||
<a class="row" *ngFor="let session of sessions$ | async" (click)="selectAccount(session.loginName)">
|
||||
<cnsl-avatar
|
||||
*ngIf="session && session.displayName"
|
||||
class="small-avatar"
|
||||
@@ -31,7 +30,7 @@
|
||||
<div class="col">
|
||||
<span class="user-title">{{ session.displayName ? session.displayName : session.userName }} </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
|
||||
}}</span>
|
||||
</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 { 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 { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
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({
|
||||
selector: 'cnsl-accounts-card',
|
||||
templateUrl: './accounts-card.component.html',
|
||||
styleUrls: ['./accounts-card.component.scss'],
|
||||
})
|
||||
export class AccountsCardComponent implements OnInit {
|
||||
@Input() public user?: User.AsObject;
|
||||
@Input() public iamuser: boolean | null = false;
|
||||
|
||||
@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;
|
||||
});
|
||||
export class AccountsCardComponent {
|
||||
@Input({ required: true })
|
||||
public set user(user: User.AsObject) {
|
||||
this.user$.next(user);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadingUsers = true;
|
||||
@Input() public iamuser: boolean | null = false;
|
||||
|
||||
@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 {
|
||||
this.router.navigate(['users/me']);
|
||||
this.router.navigate(['users/me']).then();
|
||||
this.closedCard.emit();
|
||||
}
|
||||
|
||||
public closeCard(element: HTMLElement): void {
|
||||
if (!element.classList.contains('dontcloseonclick')) {
|
||||
this.closedCard.emit();
|
||||
}
|
||||
}
|
||||
|
||||
public selectAccount(loginHint: string): void {
|
||||
const configWithPrompt: Partial<AuthConfig> = {
|
||||
customQueryParams: {
|
||||
login_hint: loginHint,
|
||||
},
|
||||
};
|
||||
this.authService.authenticate(configWithPrompt);
|
||||
this.authService.authenticate(configWithPrompt).then();
|
||||
}
|
||||
|
||||
public selectNewAccount(): void {
|
||||
@@ -67,7 +159,7 @@ export class AccountsCardComponent implements OnInit {
|
||||
prompt: 'login',
|
||||
} as any,
|
||||
};
|
||||
this.authService.authenticate(configWithPrompt);
|
||||
this.authService.authenticate(configWithPrompt).then();
|
||||
}
|
||||
|
||||
public logout(): void {
|
||||
|
@@ -15,7 +15,7 @@ import { ActionKeysType } from '../action-keys/action-keys.component';
|
||||
})
|
||||
export class HeaderComponent {
|
||||
@Input() public isDarkTheme: boolean = true;
|
||||
@Input() public user?: User.AsObject;
|
||||
@Input({ required: true }) public user!: User.AsObject;
|
||||
public showOrgContext: boolean = false;
|
||||
|
||||
@Input() public org!: Org.AsObject;
|
||||
|
@@ -18,7 +18,7 @@ import { NewConnectWebOrgInterceptor, OrgInterceptor, OrgInterceptorProvider } f
|
||||
import { StorageService } from './storage.service';
|
||||
import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb';
|
||||
//@ts-ignore
|
||||
import { createFeatureServiceClient, createUserServiceClient } from '@zitadel/client/v2';
|
||||
import { createFeatureServiceClient, createUserServiceClient, createSessionServiceClient } from '@zitadel/client/v2';
|
||||
//@ts-ignore
|
||||
import { createAuthServiceClient, createManagementServiceClient } from '@zitadel/client/v1';
|
||||
import { createGrpcWebTransport } from '@connectrpc/connect-web';
|
||||
@@ -38,6 +38,7 @@ export class GrpcService {
|
||||
public admin!: AdminServiceClient;
|
||||
public user!: UserServiceClient;
|
||||
public userNew!: ReturnType<typeof createUserServiceClient>;
|
||||
public session!: ReturnType<typeof createSessionServiceClient>;
|
||||
public mgmtNew!: ReturnType<typeof createManagementServiceClient>;
|
||||
public authNew!: ReturnType<typeof createAuthServiceClient>;
|
||||
public featureNew!: ReturnType<typeof createFeatureServiceClient>;
|
||||
@@ -47,7 +48,6 @@ export class GrpcService {
|
||||
private readonly envService: EnvironmentService,
|
||||
private readonly platformLocation: PlatformLocation,
|
||||
private readonly authenticationService: AuthenticationService,
|
||||
private readonly storageService: StorageService,
|
||||
private readonly translate: TranslateService,
|
||||
private readonly exhaustedService: ExhaustedService,
|
||||
private readonly authInterceptor: AuthInterceptor,
|
||||
@@ -112,6 +112,7 @@ export class GrpcService {
|
||||
],
|
||||
});
|
||||
this.userNew = createUserServiceClient(transport);
|
||||
this.session = createSessionServiceClient(transport);
|
||||
this.mgmtNew = createManagementServiceClient(transportOldAPIs);
|
||||
this.authNew = createAuthServiceClient(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