diff --git a/console/src/app/modules/accounts-card/accounts-card.component.html b/console/src/app/modules/accounts-card/accounts-card.component.html index 1ed1649545..24fe098331 100644 --- a/console/src/app/modules/accounts-card/accounts-card.component.html +++ b/console/src/app/modules/accounts-card/accounts-card.component.html @@ -1,7 +1,6 @@ -
+
{{ 'USER.EDITACCOUNT' | translate }} diff --git a/console/src/app/modules/accounts-card/accounts-card.component.ts b/console/src/app/modules/accounts-card/accounts-card.component.ts index 2676a5bcf5..d91148e862 100644 --- a/console/src/app/modules/accounts-card/accounts-card.component.ts +++ b/console/src/app/modules/accounts-card/accounts-card.component.ts @@ -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 = 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(); + + protected readonly user$ = new ReplaySubject(1); + protected readonly UserState = UserState; + private readonly labelpolicy = toSignal(this.userService.labelpolicy$, { initialValue: undefined }); + protected readonly sessions$: Observable; + + 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 { + 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 { + 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 { + 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 = { 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 { diff --git a/console/src/app/modules/header/header.component.ts b/console/src/app/modules/header/header.component.ts index e560237343..c12c38e43a 100644 --- a/console/src/app/modules/header/header.component.ts +++ b/console/src/app/modules/header/header.component.ts @@ -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; diff --git a/console/src/app/services/grpc.service.ts b/console/src/app/services/grpc.service.ts index baec234d6a..6ec04975f6 100644 --- a/console/src/app/services/grpc.service.ts +++ b/console/src/app/services/grpc.service.ts @@ -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; + public session!: ReturnType; public mgmtNew!: ReturnType; public authNew!: ReturnType; public featureNew!: ReturnType; @@ -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); diff --git a/console/src/app/services/session.service.ts b/console/src/app/services/session.service.ts new file mode 100644 index 0000000000..12e07049b4 --- /dev/null +++ b/console/src/app/services/session.service.ts @@ -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): Promise { + return this.grpcService.session.listSessions(req); + } +}