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);
+ }
+}