@acorex-platform/module-auth-client 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/acorex-platform-module-auth-client-acorex-platform-module-auth-client-B7tIKOw6.mjs +3174 -0
- package/fesm2022/acorex-platform-module-auth-client-acorex-platform-module-auth-client-B7tIKOw6.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-app-chooser.component-Cm2ZagaI.mjs +67 -0
- package/fesm2022/acorex-platform-module-auth-client-app-chooser.component-Cm2ZagaI.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-blank.layout-BRGgntib.mjs +18 -0
- package/fesm2022/acorex-platform-module-auth-client-blank.layout-BRGgntib.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-change-password.command-ZyXAjUKw.mjs +168 -0
- package/fesm2022/acorex-platform-module-auth-client-change-password.command-ZyXAjUKw.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-change-photo.command-BC8lw--F.mjs +192 -0
- package/fesm2022/acorex-platform-module-auth-client-change-photo.command-BC8lw--F.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-contracts.mjs +80 -0
- package/fesm2022/acorex-platform-module-auth-client-contracts.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-edit-account-info.command-B-mVlXxg.mjs +129 -0
- package/fesm2022/acorex-platform-module-auth-client-edit-account-info.command-B-mVlXxg.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-edit-signature.command-CcTJJwAX.mjs +32 -0
- package/fesm2022/acorex-platform-module-auth-client-edit-signature.command-CcTJJwAX.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-lock-screen.command-41QnG1TQ.mjs +30 -0
- package/fesm2022/acorex-platform-module-auth-client-lock-screen.command-41QnG1TQ.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-login.module-DYtNYzF6.mjs +57 -0
- package/fesm2022/acorex-platform-module-auth-client-login.module-DYtNYzF6.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-master.layout-DpdD9kgk.mjs +24 -0
- package/fesm2022/acorex-platform-module-auth-client-master.layout-DpdD9kgk.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-menu.provider-Bz-kRip4.mjs +19 -0
- package/fesm2022/acorex-platform-module-auth-client-menu.provider-Bz-kRip4.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-oauth-callback.component-BspBH4Ul.mjs +105 -0
- package/fesm2022/acorex-platform-module-auth-client-oauth-callback.component-BspBH4Ul.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-open-sessions.command-CXSGuqYp.mjs +196 -0
- package/fesm2022/acorex-platform-module-auth-client-open-sessions.command-CXSGuqYp.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-password.component-BuqUhET7.mjs +59 -0
- package/fesm2022/acorex-platform-module-auth-client-password.component-BuqUhET7.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-password.component-Cihg82vM.mjs +529 -0
- package/fesm2022/acorex-platform-module-auth-client-password.component-Cihg82vM.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-pin-code.constants-CRsFG7fg.mjs +7 -0
- package/fesm2022/acorex-platform-module-auth-client-pin-code.constants-CRsFG7fg.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-profile-activity-chart.component-B8lBvZsA.mjs +158 -0
- package/fesm2022/acorex-platform-module-auth-client-profile-activity-chart.component-B8lBvZsA.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-profile-coming-soon.command-DBbFbzAx.mjs +31 -0
- package/fesm2022/acorex-platform-module-auth-client-profile-coming-soon.command-DBbFbzAx.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-profile-delete-account-danger-zone.component-CtJho4ab.mjs +79 -0
- package/fesm2022/acorex-platform-module-auth-client-profile-delete-account-danger-zone.component-CtJho4ab.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-profile-page-component.provider-B-Qtd4_j.mjs +27 -0
- package/fesm2022/acorex-platform-module-auth-client-profile-page-component.provider-B-Qtd4_j.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-profile-page.component-tbhf7K0V.mjs +446 -0
- package/fesm2022/acorex-platform-module-auth-client-profile-page.component-tbhf7K0V.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-register-password.component-hSJyEA6Q.mjs +145 -0
- package/fesm2022/acorex-platform-module-auth-client-register-password.component-hSJyEA6Q.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-register.module-DXm6AQpo.mjs +62 -0
- package/fesm2022/acorex-platform-module-auth-client-register.module-DXm6AQpo.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-remove-pin-code.command-zUZooOtv.mjs +91 -0
- package/fesm2022/acorex-platform-module-auth-client-remove-pin-code.command-zUZooOtv.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-result.component-DYJaB-FD.mjs +36 -0
- package/fesm2022/acorex-platform-module-auth-client-result.component-DYJaB-FD.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-routes-REjaWcqY.mjs +9 -0
- package/fesm2022/acorex-platform-module-auth-client-routes-REjaWcqY.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-session-lock-overlay.component-DqXGuIgN.mjs +346 -0
- package/fesm2022/acorex-platform-module-auth-client-session-lock-overlay.component-DqXGuIgN.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-set-pin-code.command-DXTVbrS0.mjs +33 -0
- package/fesm2022/acorex-platform-module-auth-client-set-pin-code.command-DXTVbrS0.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-set-pin-code.component-5TxB6aFi.mjs +188 -0
- package/fesm2022/acorex-platform-module-auth-client-set-pin-code.component-5TxB6aFi.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-settings.provider-n1qbs6e1.mjs +177 -0
- package/fesm2022/acorex-platform-module-auth-client-settings.provider-n1qbs6e1.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-show-profile-command-CxMz1yy-.mjs +70 -0
- package/fesm2022/acorex-platform-module-auth-client-show-profile-command-CxMz1yy-.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-signout-command-DqqrP5pk.mjs +24 -0
- package/fesm2022/acorex-platform-module-auth-client-signout-command-DqqrP5pk.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-tenant-chooser.component-ihsqCJKN.mjs +59 -0
- package/fesm2022/acorex-platform-module-auth-client-tenant-chooser.component-ihsqCJKN.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-two-factor-code.component-CiENsIEh.mjs +40 -0
- package/fesm2022/acorex-platform-module-auth-client-two-factor-code.component-CiENsIEh.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-two-factor.module-BJqdeuAz.mjs +45 -0
- package/fesm2022/acorex-platform-module-auth-client-two-factor.module-BJqdeuAz.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client-view-login-history.command-Dl0os5lD.mjs +108 -0
- package/fesm2022/acorex-platform-module-auth-client-view-login-history.command-Dl0os5lD.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-auth-client.mjs +2 -0
- package/fesm2022/acorex-platform-module-auth-client.mjs.map +1 -0
- package/package.json +28 -0
- package/types/acorex-platform-module-auth-client-contracts.d.ts +67 -0
- package/types/acorex-platform-module-auth-client.d.ts +390 -0
package/fesm2022/acorex-platform-module-auth-client-acorex-platform-module-auth-client-B7tIKOw6.mjs
ADDED
|
@@ -0,0 +1,3174 @@
|
|
|
1
|
+
import * as i1$4 from '@acorex-platform/framework-client/common';
|
|
2
|
+
import { AXPFileStorageService, AXPSettingsService, AXPHomePageService, AXPDataProvider, AXPCommonSettings, AXP_PROTECTED_ROUTE_GUARDS, AXPCommonModule, AXP_MENU_PROVIDER, AXP_SETTING_DEFINITION_PROVIDER } from '@acorex-platform/framework-client/common';
|
|
3
|
+
import { AXPSessionService, AXPUserProfileIdentityService, AXPUnauthorizedError, AXPUnauthenticatedError, AXPAuthStrategyRegistryService, AXPAuthGuard, AXPSessionStatus } from '@acorex-platform/framework-client/auth';
|
|
4
|
+
import { AXPUserAvatarService, AXPUserAvatarComponent, AXP_PAGE_COMPONENT_PROVIDER } from '@acorex-platform/framework-client/layout/components';
|
|
5
|
+
import { AXPFileStorageStatus, AXPWidgetsCatalog, coerceUnknownToTrimmedString, AXPPlatformScope, AXPKeyboardShortcutPriority, verifyPassword } from '@acorex-platform/framework-shared/core';
|
|
6
|
+
import { RootConfig } from '@acorex-platform/module-person-core-client/contracts';
|
|
7
|
+
import { RootConfig as RootConfig$1 } from '@acorex-platform/module-security-management-client/contracts';
|
|
8
|
+
import { AXPEntityService, AXP_ENTITY_STORAGE_MIDDLEWARE } from '@acorex-platform/framework-client/layout/entity';
|
|
9
|
+
import * as i0 from '@angular/core';
|
|
10
|
+
import { inject, Injectable, isDevMode, InjectionToken, NgModule, input, output, viewChild, viewChildren, signal, computed, effect, ChangeDetectionStrategy, Component, DestroyRef, ViewEncapsulation, ViewChild, Injector, afterNextRender, provideAppInitializer } from '@angular/core';
|
|
11
|
+
import { first, isEmpty, isNil, isString, orderBy, get, defaultTo } from 'lodash';
|
|
12
|
+
import { AXDataSource } from '@acorex/cdk/common';
|
|
13
|
+
import { AXMAuthDataSourceKeys, AXPAuthSettings, AXMAuthCommandKeys } from '@acorex-platform/module-auth-client/contracts';
|
|
14
|
+
import { AXPLayoutThemeService, AXPMenuVerticalMode } from '@acorex-platform/framework-client/themes/shared';
|
|
15
|
+
import { HttpErrorResponse } from '@angular/common/http';
|
|
16
|
+
import * as i1$5 from '@angular/router';
|
|
17
|
+
import { Router, RouterModule, ROUTES } from '@angular/router';
|
|
18
|
+
import * as i1 from '@acorex/core/validation';
|
|
19
|
+
import { AXValidationModule } from '@acorex/core/validation';
|
|
20
|
+
import * as i1$3 from '@acorex-platform/framework-client/core';
|
|
21
|
+
import { axpApplicationLogoKey, AXPEntityLogoRegistrationService, AXPLogoComponent, axpTenantLogoKey, AXPHookService, AXPKeyboardShortcutRegistry, chordToKbdItemKeys, getPrimaryKeyboardShortcutChord, normalizeKeyboardShortcuts, AXPDeviceService, AXPComponentSlotModule, AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER, provideLazyProvider, AXP_DATASOURCE_DEFINITION_PROVIDER, AXPComponentSlotDirective } from '@acorex-platform/framework-client/core';
|
|
22
|
+
import { AXPCommandService, provideCommandSetups } from '@acorex-platform/framework-client/runtime';
|
|
23
|
+
import { AXPRootLayoutComponent } from '@acorex-platform/framework-client/themes/default';
|
|
24
|
+
import * as i4$1 from '@angular/common';
|
|
25
|
+
import { CommonModule, AsyncPipe } from '@angular/common';
|
|
26
|
+
import { first as first$1, firstValueFrom, filter, combineLatest, switchMap, from } from 'rxjs';
|
|
27
|
+
import * as i1$1 from '@acorex/components/button';
|
|
28
|
+
import { AXButtonModule } from '@acorex/components/button';
|
|
29
|
+
import * as i3$1 from '@acorex/components/decorators';
|
|
30
|
+
import { AXDecoratorModule } from '@acorex/components/decorators';
|
|
31
|
+
import * as i2$2 from '@acorex/components/popover';
|
|
32
|
+
import { AXPopoverModule } from '@acorex/components/popover';
|
|
33
|
+
import { RootConfig as RootConfig$2 } from '@acorex-platform/module-application-management-client/contracts';
|
|
34
|
+
import * as i5 from '@acorex/cdk/list-navigation';
|
|
35
|
+
import { AXListNavigationDirective, AXListNavigationItemDirective, AXListNavigationModule } from '@acorex/cdk/list-navigation';
|
|
36
|
+
import * as i4 from '@acorex/components/search-box';
|
|
37
|
+
import { AXSearchBoxModule } from '@acorex/components/search-box';
|
|
38
|
+
import * as i3 from '@acorex/components/select-box';
|
|
39
|
+
import { AXSelectBoxModule } from '@acorex/components/select-box';
|
|
40
|
+
import * as i2 from '@acorex/components/skeleton';
|
|
41
|
+
import { AXSkeletonModule } from '@acorex/components/skeleton';
|
|
42
|
+
import * as i2$1 from '@acorex/core/translation';
|
|
43
|
+
import { AXTranslationModule, AXTranslationService } from '@acorex/core/translation';
|
|
44
|
+
import { AXAvatarModule } from '@acorex/components/avatar';
|
|
45
|
+
import { AXImageModule } from '@acorex/components/image';
|
|
46
|
+
import { AXLoadingModule } from '@acorex/components/loading';
|
|
47
|
+
import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
48
|
+
import * as i1$2 from '@angular/forms';
|
|
49
|
+
import { FormsModule } from '@angular/forms';
|
|
50
|
+
import { AXToastService } from '@acorex/components/toast';
|
|
51
|
+
import { AXMDeviceSessionsService } from '@acorex-platform/module-security-management-client';
|
|
52
|
+
import { AXOverlayService } from '@acorex/cdk/overlay';
|
|
53
|
+
import { AXPlatform } from '@acorex/core/platform';
|
|
54
|
+
|
|
55
|
+
//#region ---- Hook keys ----
|
|
56
|
+
const PROFILE_PAGE_GROUPS_HOOK = 'profile-page.groups';
|
|
57
|
+
const PROFILE_PAGE_SECTIONS_HOOK = 'profile-page.sections';
|
|
58
|
+
const PROFILE_PAGE_HEADER_META_HOOK = 'profile-page.header-meta';
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region ---- Helpers ----
|
|
61
|
+
/** Resolves section content; top-level `actions` imply card mode. */
|
|
62
|
+
function resolveUserProfileSectionContent(section) {
|
|
63
|
+
if (section.content) {
|
|
64
|
+
return section.content;
|
|
65
|
+
}
|
|
66
|
+
return { mode: 'card', actions: section.actions ?? [] };
|
|
67
|
+
}
|
|
68
|
+
/** Whether the group renders a full-tab component instead of sections. */
|
|
69
|
+
function isUserProfileGroupComponent(group) {
|
|
70
|
+
return group.content?.mode === 'component';
|
|
71
|
+
}
|
|
72
|
+
/** Visible actions on a section card. */
|
|
73
|
+
function visibleUserProfileSectionActions(actions) {
|
|
74
|
+
return actions.filter((action) => action.visible !== false);
|
|
75
|
+
}
|
|
76
|
+
/** Default card action: explicit `default`, otherwise the first visible action. */
|
|
77
|
+
function resolveDefaultUserProfileSectionAction(actions) {
|
|
78
|
+
const visibleActions = visibleUserProfileSectionActions(actions);
|
|
79
|
+
if (!visibleActions.length) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return visibleActions.find((action) => action.default) ?? visibleActions[0];
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
|
|
86
|
+
//#region ---- Avatar storage constants ----
|
|
87
|
+
const AXP_PROFILE_PHOTO_REF_TYPE = 'PersonCore.Person';
|
|
88
|
+
const AXP_PROFILE_PHOTO_USER_REF_TYPE = 'SecurityManagement.User';
|
|
89
|
+
const AXP_PROFILE_PHOTO_CATEGORY = 'profile-photo';
|
|
90
|
+
const FILE_STORAGE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region ---- Avatar storage helpers ----
|
|
93
|
+
function isFileStorageId(value) {
|
|
94
|
+
if (!value) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return FILE_STORAGE_ID_PATTERN.test(value.trim());
|
|
98
|
+
}
|
|
99
|
+
function isExternalAvatarUrl(value) {
|
|
100
|
+
if (!value) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return /^https?:\/\//i.test(value.trim());
|
|
104
|
+
}
|
|
105
|
+
function isDataImageUrl(value) {
|
|
106
|
+
if (!value) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return value.trim().startsWith('data:image/');
|
|
110
|
+
}
|
|
111
|
+
function normalizeAvatarRef(value) {
|
|
112
|
+
if (value === null || value === undefined) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
if (typeof value !== 'string') {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const trimmed = value.trim();
|
|
119
|
+
if (!trimmed || trimmed === 'null') {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return trimmed;
|
|
123
|
+
}
|
|
124
|
+
function dataUrlToBlob(dataUrl) {
|
|
125
|
+
const [header, base64] = dataUrl.split(',');
|
|
126
|
+
const mimeMatch = /data:([^;]+)/.exec(header);
|
|
127
|
+
const mimeType = mimeMatch?.[1] ?? 'image/png';
|
|
128
|
+
const binary = atob(base64);
|
|
129
|
+
const bytes = new Uint8Array(binary.length);
|
|
130
|
+
for (let i = 0; i < binary.length; i++) {
|
|
131
|
+
bytes[i] = binary.charCodeAt(i);
|
|
132
|
+
}
|
|
133
|
+
return new Blob([bytes], { type: mimeType });
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region ---- User avatar sync service (client) ----
|
|
139
|
+
/**
|
|
140
|
+
* Client-side profile photo operations for self-service flows.
|
|
141
|
+
* Bidirectional Person ↔ User sync runs on the server via HCM avatar middleware.
|
|
142
|
+
*/
|
|
143
|
+
class AXPUserAvatarSyncService {
|
|
144
|
+
constructor() {
|
|
145
|
+
//#region ---- Services & Dependencies ----
|
|
146
|
+
this.entityService = inject(AXPEntityService);
|
|
147
|
+
this.fileStorage = inject(AXPFileStorageService);
|
|
148
|
+
this.sessionService = inject(AXPSessionService);
|
|
149
|
+
this.identityService = inject(AXPUserProfileIdentityService);
|
|
150
|
+
this.userAvatarService = inject(AXPUserAvatarService);
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region ---- Public API ----
|
|
154
|
+
async resolveStorageTarget(userId) {
|
|
155
|
+
const person = await this.findPersonByUserId(userId);
|
|
156
|
+
if (person?.id) {
|
|
157
|
+
return { kind: 'person', personId: person.id, userId };
|
|
158
|
+
}
|
|
159
|
+
return { kind: 'user', userId };
|
|
160
|
+
}
|
|
161
|
+
async getCurrentAvatarRef(userId) {
|
|
162
|
+
const sessionUser = this.sessionService.user;
|
|
163
|
+
if (sessionUser?.id === userId) {
|
|
164
|
+
const sessionRef = normalizeAvatarRef(sessionUser.avatar);
|
|
165
|
+
if (sessionRef) {
|
|
166
|
+
return sessionRef;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const target = await this.resolveStorageTarget(userId);
|
|
170
|
+
if (target.kind === 'person') {
|
|
171
|
+
const person = await this.personData().byKey(target.personId);
|
|
172
|
+
return normalizeAvatarRef(person?.image);
|
|
173
|
+
}
|
|
174
|
+
const user = await this.userData().byKey(userId);
|
|
175
|
+
return normalizeAvatarRef(user?.avatar);
|
|
176
|
+
}
|
|
177
|
+
async getCurrentAvatarBlob(userId) {
|
|
178
|
+
const ref = await this.getCurrentAvatarRef(userId);
|
|
179
|
+
if (!ref) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (isDataImageUrl(ref)) {
|
|
183
|
+
return dataUrlToBlob(ref);
|
|
184
|
+
}
|
|
185
|
+
if (isExternalAvatarUrl(ref)) {
|
|
186
|
+
const response = await fetch(ref);
|
|
187
|
+
return response.blob();
|
|
188
|
+
}
|
|
189
|
+
if (isFileStorageId(ref)) {
|
|
190
|
+
return this.fileStorage.download(ref);
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
async uploadAndSetAvatar(userId, file) {
|
|
195
|
+
const target = await this.resolveStorageTarget(userId);
|
|
196
|
+
const previousRef = await this.getCurrentAvatarRef(userId);
|
|
197
|
+
const refId = target.kind === 'person' ? target.personId : userId;
|
|
198
|
+
const refType = target.kind === 'person' ? AXP_PROFILE_PHOTO_REF_TYPE : AXP_PROFILE_PHOTO_USER_REF_TYPE;
|
|
199
|
+
const info = await this.fileStorage.save({
|
|
200
|
+
file,
|
|
201
|
+
name: file.name || 'profile-photo.png',
|
|
202
|
+
refId,
|
|
203
|
+
refType,
|
|
204
|
+
category: AXP_PROFILE_PHOTO_CATEGORY,
|
|
205
|
+
status: AXPFileStorageStatus.Temporary,
|
|
206
|
+
});
|
|
207
|
+
const fileId = info.fileId?.trim();
|
|
208
|
+
if (!fileId) {
|
|
209
|
+
throw new Error('File storage did not return a fileId.');
|
|
210
|
+
}
|
|
211
|
+
await this.fileStorage.commit(fileId);
|
|
212
|
+
if (target.kind === 'person') {
|
|
213
|
+
await this.personData().update(target.personId, { image: fileId });
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
await this.userData().update(userId, { avatar: fileId });
|
|
217
|
+
}
|
|
218
|
+
await this.notifyAvatarChanged(userId, fileId);
|
|
219
|
+
if (previousRef && isFileStorageId(previousRef) && previousRef !== fileId) {
|
|
220
|
+
await this.safeRemoveFile(previousRef);
|
|
221
|
+
}
|
|
222
|
+
return fileId;
|
|
223
|
+
}
|
|
224
|
+
async removeAvatar(userId) {
|
|
225
|
+
const target = await this.resolveStorageTarget(userId);
|
|
226
|
+
const previousRef = await this.getCurrentAvatarRef(userId);
|
|
227
|
+
if (target.kind === 'person') {
|
|
228
|
+
await this.personData().update(target.personId, { image: undefined });
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
await this.userData().update(userId, { avatar: null });
|
|
232
|
+
}
|
|
233
|
+
await this.notifyAvatarChanged(userId, null);
|
|
234
|
+
if (previousRef && isFileStorageId(previousRef)) {
|
|
235
|
+
await this.safeRemoveFile(previousRef);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** Keeps session, avatar cache, and shell avatars in sync after any avatar change. */
|
|
239
|
+
async notifyAvatarChanged(userId, avatar) {
|
|
240
|
+
await this.refreshClientDisplay(userId, avatar);
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region ---- Private helpers ----
|
|
244
|
+
async refreshClientDisplay(userId, avatar) {
|
|
245
|
+
const isCurrentUser = this.sessionService.user?.id === userId;
|
|
246
|
+
if (isCurrentUser) {
|
|
247
|
+
await this.identityService.applySessionPatch({
|
|
248
|
+
avatar: avatar === null ? '' : avatar,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
this.userAvatarService.clearCache(userId);
|
|
252
|
+
await this.userAvatarService.refreshUserInfo(userId);
|
|
253
|
+
}
|
|
254
|
+
personData() {
|
|
255
|
+
return this.entityService
|
|
256
|
+
.withEntity(RootConfig.module.name, RootConfig.entities.person.name)
|
|
257
|
+
.data();
|
|
258
|
+
}
|
|
259
|
+
userData() {
|
|
260
|
+
return this.entityService
|
|
261
|
+
.withEntity(RootConfig$1.module.name, RootConfig$1.entities.users.name)
|
|
262
|
+
.data();
|
|
263
|
+
}
|
|
264
|
+
async findPersonByUserId(userId) {
|
|
265
|
+
const result = await this.personData().query({
|
|
266
|
+
filter: { field: 'userId', operator: { type: 'equal' }, value: userId },
|
|
267
|
+
take: 1,
|
|
268
|
+
skip: 0,
|
|
269
|
+
});
|
|
270
|
+
return first(result.items) ?? null;
|
|
271
|
+
}
|
|
272
|
+
async safeRemoveFile(fileId) {
|
|
273
|
+
try {
|
|
274
|
+
await this.fileStorage.remove(fileId);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
try {
|
|
278
|
+
await this.fileStorage.markForDeletion(fileId);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
/* ignore cleanup failures */
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPUserAvatarSyncService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
286
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPUserAvatarSyncService, providedIn: 'root' }); }
|
|
287
|
+
}
|
|
288
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPUserAvatarSyncService, decorators: [{
|
|
289
|
+
type: Injectable,
|
|
290
|
+
args: [{ providedIn: 'root' }]
|
|
291
|
+
}] });
|
|
292
|
+
|
|
293
|
+
//#region ---- User activity data source types ----
|
|
294
|
+
//#endregion
|
|
295
|
+
|
|
296
|
+
//#region ---- User activity helpers ----
|
|
297
|
+
//#region ---- Constants ----
|
|
298
|
+
const AUTH_USER_ACTIVITY_DEFAULT_WEEKS = 53;
|
|
299
|
+
const LEVEL_THRESHOLDS = [0, 3, 8, 15, 30];
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region ---- Date range ----
|
|
302
|
+
function resolveDefaultUserActivityDateRange(weeksToShow = AUTH_USER_ACTIVITY_DEFAULT_WEEKS, referenceDate = new Date()) {
|
|
303
|
+
const today = new Date(referenceDate);
|
|
304
|
+
today.setHours(0, 0, 0, 0);
|
|
305
|
+
const endSunday = new Date(today);
|
|
306
|
+
endSunday.setDate(today.getDate() + (6 - today.getDay()));
|
|
307
|
+
const startDate = new Date(endSunday);
|
|
308
|
+
startDate.setDate(endSunday.getDate() - weeksToShow * 7 + 1);
|
|
309
|
+
startDate.setHours(0, 0, 0, 0);
|
|
310
|
+
return { startDate, endDate: endSunday };
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region ---- Session aggregation ----
|
|
314
|
+
function normalizeDateRange(startDate, endDate) {
|
|
315
|
+
const start = new Date(startDate);
|
|
316
|
+
const end = new Date(endDate);
|
|
317
|
+
start.setHours(0, 0, 0, 0);
|
|
318
|
+
end.setHours(0, 0, 0, 0);
|
|
319
|
+
return start <= end ? { rangeStart: start, rangeEnd: end } : { rangeStart: end, rangeEnd: start };
|
|
320
|
+
}
|
|
321
|
+
function toIsoDate(date) {
|
|
322
|
+
return date.toISOString().slice(0, 10);
|
|
323
|
+
}
|
|
324
|
+
function toDayKey(value) {
|
|
325
|
+
const date = new Date(value);
|
|
326
|
+
if (Number.isNaN(date.getTime())) {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
date.setHours(0, 0, 0, 0);
|
|
330
|
+
return toIsoDate(date);
|
|
331
|
+
}
|
|
332
|
+
/** Counts sign-in events per calendar day from device session rows. */
|
|
333
|
+
function aggregateDailySessionCounts(sessions, params) {
|
|
334
|
+
const { rangeStart, rangeEnd } = normalizeDateRange(params.startDate, params.endDate);
|
|
335
|
+
const counts = new Map();
|
|
336
|
+
for (const session of sessions) {
|
|
337
|
+
if (session.userId !== params.userId || session.lastLoginDate == null) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const dayKey = toDayKey(session.lastLoginDate);
|
|
341
|
+
if (!dayKey) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const day = new Date(dayKey);
|
|
345
|
+
if (day < rangeStart || day > rangeEnd) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
counts.set(dayKey, (counts.get(dayKey) ?? 0) + 1);
|
|
349
|
+
}
|
|
350
|
+
return counts;
|
|
351
|
+
}
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region ---- Week grid ----
|
|
354
|
+
function countToActivityLevel(count) {
|
|
355
|
+
if (count <= LEVEL_THRESHOLDS[1]) {
|
|
356
|
+
return count === 0 ? 0 : 1;
|
|
357
|
+
}
|
|
358
|
+
if (count <= LEVEL_THRESHOLDS[2]) {
|
|
359
|
+
return 2;
|
|
360
|
+
}
|
|
361
|
+
if (count <= LEVEL_THRESHOLDS[3]) {
|
|
362
|
+
return 3;
|
|
363
|
+
}
|
|
364
|
+
return 4;
|
|
365
|
+
}
|
|
366
|
+
function startOfWeekSunday(date) {
|
|
367
|
+
const value = new Date(date);
|
|
368
|
+
value.setHours(0, 0, 0, 0);
|
|
369
|
+
value.setDate(value.getDate() - value.getDay());
|
|
370
|
+
return value;
|
|
371
|
+
}
|
|
372
|
+
function buildUserActivityWeeks(params, dailyCounts) {
|
|
373
|
+
const { rangeStart, rangeEnd } = normalizeDateRange(params.startDate, params.endDate);
|
|
374
|
+
const today = new Date();
|
|
375
|
+
today.setHours(0, 0, 0, 0);
|
|
376
|
+
const gridStart = startOfWeekSunday(rangeStart);
|
|
377
|
+
const gridEnd = startOfWeekSunday(rangeEnd);
|
|
378
|
+
gridEnd.setDate(gridEnd.getDate() + 6);
|
|
379
|
+
const weeks = [];
|
|
380
|
+
const cursor = new Date(gridStart);
|
|
381
|
+
while (cursor <= gridEnd) {
|
|
382
|
+
const days = [];
|
|
383
|
+
for (let dayIndex = 0; dayIndex < 7; dayIndex += 1) {
|
|
384
|
+
const date = new Date(cursor);
|
|
385
|
+
const inRange = date >= rangeStart && date <= rangeEnd;
|
|
386
|
+
const dayKey = toIsoDate(date);
|
|
387
|
+
const count = inRange && date <= today ? (dailyCounts.get(dayKey) ?? 0) : 0;
|
|
388
|
+
days.push({
|
|
389
|
+
date: dayKey,
|
|
390
|
+
count,
|
|
391
|
+
level: countToActivityLevel(count),
|
|
392
|
+
});
|
|
393
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
394
|
+
}
|
|
395
|
+
weeks.push({ days });
|
|
396
|
+
}
|
|
397
|
+
return weeks;
|
|
398
|
+
}
|
|
399
|
+
function computeUserActivityStats(weeks) {
|
|
400
|
+
const days = weeks.flatMap((week) => week.days).filter((day) => day.count > 0);
|
|
401
|
+
const total = days.reduce((sum, day) => sum + day.count, 0);
|
|
402
|
+
const activeDayCount = days.length || 1;
|
|
403
|
+
let longestStreak = 0;
|
|
404
|
+
let currentStreak = 0;
|
|
405
|
+
for (const week of weeks) {
|
|
406
|
+
for (const day of week.days) {
|
|
407
|
+
if (day.count > 0) {
|
|
408
|
+
currentStreak += 1;
|
|
409
|
+
longestStreak = Math.max(longestStreak, currentStreak);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
currentStreak = 0;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const weekCount = weeks.length || 1;
|
|
417
|
+
return {
|
|
418
|
+
longestStreak,
|
|
419
|
+
avgPerDay: Math.round(total / activeDayCount),
|
|
420
|
+
avgPerWeek: Math.round(total / weekCount),
|
|
421
|
+
total,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function buildUserActivityDataSourceItem(params, sessions) {
|
|
425
|
+
const dailyCounts = aggregateDailySessionCounts(sessions, params);
|
|
426
|
+
const weeks = buildUserActivityWeeks(params, dailyCounts);
|
|
427
|
+
return {
|
|
428
|
+
id: `${params.userId}:${toIsoDate(params.startDate)}:${toIsoDate(params.endDate)}`,
|
|
429
|
+
userId: params.userId,
|
|
430
|
+
startDate: toIsoDate(params.startDate),
|
|
431
|
+
endDate: toIsoDate(params.endDate),
|
|
432
|
+
weeks,
|
|
433
|
+
stats: computeUserActivityStats(weeks),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function formatCompactActivityCount(value) {
|
|
437
|
+
if (value >= 1_000_000) {
|
|
438
|
+
return `${(value / 1_000_000).toFixed(2)}M`;
|
|
439
|
+
}
|
|
440
|
+
if (value >= 1_000) {
|
|
441
|
+
return `${Math.round(value / 1000)}K`;
|
|
442
|
+
}
|
|
443
|
+
return String(value);
|
|
444
|
+
}
|
|
445
|
+
//#endregion
|
|
446
|
+
|
|
447
|
+
//#region ---- User activity filter helpers ----
|
|
448
|
+
function isResolvedFilterScalar(value) {
|
|
449
|
+
if (value == null) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
if (value instanceof Date) {
|
|
453
|
+
return !Number.isNaN(value.getTime());
|
|
454
|
+
}
|
|
455
|
+
const text = String(value).trim();
|
|
456
|
+
return text.length > 0 && !text.includes('{{');
|
|
457
|
+
}
|
|
458
|
+
function parseFilterDate(value) {
|
|
459
|
+
if (!isResolvedFilterScalar(value)) {
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
if (value instanceof Date) {
|
|
463
|
+
return value;
|
|
464
|
+
}
|
|
465
|
+
const parsed = new Date(String(value));
|
|
466
|
+
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
467
|
+
}
|
|
468
|
+
function extractFilterFieldValue(filter, field, operators) {
|
|
469
|
+
if (!filter) {
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
if (filter.filters?.length) {
|
|
473
|
+
for (const child of filter.filters) {
|
|
474
|
+
const value = extractFilterFieldValue(child, field, operators);
|
|
475
|
+
if (value !== undefined) {
|
|
476
|
+
return value;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
const operator = String(filter.operator?.type ?? '');
|
|
482
|
+
if (filter.field === field && operators.includes(operator)) {
|
|
483
|
+
return isResolvedFilterScalar(filter.value) ? filter.value : undefined;
|
|
484
|
+
}
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
function extractUserActivityUserId(filter) {
|
|
488
|
+
const value = extractFilterFieldValue(filter, 'userId', ['equal', 'eq']);
|
|
489
|
+
if (value == null) {
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
const text = String(value).trim();
|
|
493
|
+
return text.length > 0 ? text : undefined;
|
|
494
|
+
}
|
|
495
|
+
function extractUserActivityStartDate(filter) {
|
|
496
|
+
const gte = parseFilterDate(extractFilterFieldValue(filter, 'startDate', ['gte', 'greaterThanOrEqual', 'equal', 'eq']));
|
|
497
|
+
if (gte) {
|
|
498
|
+
return gte;
|
|
499
|
+
}
|
|
500
|
+
return parseFilterDate(extractFilterFieldValue(filter, 'dateFrom', ['gte', 'greaterThanOrEqual', 'equal', 'eq']));
|
|
501
|
+
}
|
|
502
|
+
function extractUserActivityEndDate(filter) {
|
|
503
|
+
const lte = parseFilterDate(extractFilterFieldValue(filter, 'endDate', ['lte', 'lessThanOrEqual', 'equal', 'eq']));
|
|
504
|
+
if (lte) {
|
|
505
|
+
return lte;
|
|
506
|
+
}
|
|
507
|
+
return parseFilterDate(extractFilterFieldValue(filter, 'dateTo', ['lte', 'lessThanOrEqual', 'equal', 'eq']));
|
|
508
|
+
}
|
|
509
|
+
//#endregion
|
|
510
|
+
|
|
511
|
+
//#region ---- User activity data source ----
|
|
512
|
+
function resolveLoadParams(request) {
|
|
513
|
+
const userId = extractUserActivityUserId(request?.filter);
|
|
514
|
+
if (isEmpty(userId)) {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
const defaultRange = resolveDefaultUserActivityDateRange();
|
|
518
|
+
const startDate = extractUserActivityStartDate(request?.filter) ?? defaultRange.startDate;
|
|
519
|
+
const endDate = extractUserActivityEndDate(request?.filter) ?? defaultRange.endDate;
|
|
520
|
+
return {
|
|
521
|
+
userId: userId,
|
|
522
|
+
startDate,
|
|
523
|
+
endDate,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
/** Contribution-style user activity heatmap derived from device session sign-in dates. */
|
|
527
|
+
class AXMUserActivityDataSourceDefinition {
|
|
528
|
+
constructor() {
|
|
529
|
+
//#region ---- Services & Dependencies ----
|
|
530
|
+
this.entityService = inject(AXPEntityService);
|
|
531
|
+
//#endregion
|
|
532
|
+
}
|
|
533
|
+
//#endregion
|
|
534
|
+
//#region ---- Public API ----
|
|
535
|
+
items() {
|
|
536
|
+
const sampleParams = resolveDefaultUserActivityDateRange();
|
|
537
|
+
const sampleItem = buildUserActivityDataSourceItem({ userId: 'sample-user', ...sampleParams }, []);
|
|
538
|
+
return Promise.resolve([
|
|
539
|
+
{
|
|
540
|
+
name: AXMAuthDataSourceKeys.UserActivity,
|
|
541
|
+
title: 'User Activity',
|
|
542
|
+
columns: [
|
|
543
|
+
{
|
|
544
|
+
name: 'id',
|
|
545
|
+
title: 'ID',
|
|
546
|
+
datatype: 'string',
|
|
547
|
+
type: AXPWidgetsCatalog.text,
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: 'userId',
|
|
551
|
+
title: 'User ID',
|
|
552
|
+
datatype: 'string',
|
|
553
|
+
type: AXPWidgetsCatalog.text,
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
name: 'startDate',
|
|
557
|
+
title: 'Start Date',
|
|
558
|
+
datatype: 'date',
|
|
559
|
+
type: AXPWidgetsCatalog.dateTime,
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
name: 'endDate',
|
|
563
|
+
title: 'End Date',
|
|
564
|
+
datatype: 'date',
|
|
565
|
+
type: AXPWidgetsCatalog.dateTime,
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
filters: [
|
|
569
|
+
{
|
|
570
|
+
field: 'userId',
|
|
571
|
+
title: 'User',
|
|
572
|
+
operator: { type: 'equal' },
|
|
573
|
+
widget: { type: AXPWidgetsCatalog.text },
|
|
574
|
+
filterType: { advance: true, inline: true },
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
field: 'startDate',
|
|
578
|
+
title: 'Start Date',
|
|
579
|
+
operator: { type: 'gte' },
|
|
580
|
+
widget: { type: AXPWidgetsCatalog.dateTime, options: { format: 'date' } },
|
|
581
|
+
filterType: { advance: true, inline: true },
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
field: 'endDate',
|
|
585
|
+
title: 'End Date',
|
|
586
|
+
operator: { type: 'lte' },
|
|
587
|
+
widget: { type: AXPWidgetsCatalog.dateTime, options: { format: 'date' } },
|
|
588
|
+
filterType: { advance: true, inline: true },
|
|
589
|
+
},
|
|
590
|
+
],
|
|
591
|
+
textField: { name: 'id', title: 'ID' },
|
|
592
|
+
valueField: { name: 'id', title: 'ID' },
|
|
593
|
+
samples: [sampleItem],
|
|
594
|
+
source: () => new AXDataSource({
|
|
595
|
+
key: 'id',
|
|
596
|
+
pageSize: 1,
|
|
597
|
+
load: async (request) => {
|
|
598
|
+
const params = resolveLoadParams(request);
|
|
599
|
+
if (!params) {
|
|
600
|
+
return { items: [], total: 0 };
|
|
601
|
+
}
|
|
602
|
+
const sessions = await this.loadUserSessions(params.userId);
|
|
603
|
+
const item = buildUserActivityDataSourceItem(params, sessions);
|
|
604
|
+
return { items: [item], total: 1 };
|
|
605
|
+
},
|
|
606
|
+
}),
|
|
607
|
+
},
|
|
608
|
+
]);
|
|
609
|
+
}
|
|
610
|
+
//#endregion
|
|
611
|
+
//#region ---- Private Methods ----
|
|
612
|
+
async loadUserSessions(userId) {
|
|
613
|
+
const response = await this.entityService
|
|
614
|
+
.withEntity(RootConfig$1.module.name, RootConfig$1.entities.deviceSessions.name)
|
|
615
|
+
.data()
|
|
616
|
+
.query({
|
|
617
|
+
skip: 0,
|
|
618
|
+
take: 1000,
|
|
619
|
+
filter: {
|
|
620
|
+
field: 'userId',
|
|
621
|
+
operator: { type: 'equal' },
|
|
622
|
+
value: userId,
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
return response.items ?? [];
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
//#endregion
|
|
629
|
+
|
|
630
|
+
//#region ---- Auth data source provider ----
|
|
631
|
+
/**
|
|
632
|
+
* Registers auth module data source definitions.
|
|
633
|
+
*/
|
|
634
|
+
class AXMAuthDataSourceProvider {
|
|
635
|
+
constructor() {
|
|
636
|
+
this.userActivity = new AXMUserActivityDataSourceDefinition();
|
|
637
|
+
}
|
|
638
|
+
async items() {
|
|
639
|
+
return [...(await this.userActivity.items())];
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
//#endregion
|
|
643
|
+
|
|
644
|
+
var datasource_provider = /*#__PURE__*/Object.freeze({
|
|
645
|
+
__proto__: null,
|
|
646
|
+
AXMAuthDataSourceProvider: AXMAuthDataSourceProvider
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
class AXMAuthErrorHandler {
|
|
650
|
+
constructor() {
|
|
651
|
+
this.router = inject(Router);
|
|
652
|
+
this.layoutService = inject(AXPLayoutThemeService);
|
|
653
|
+
}
|
|
654
|
+
handleError(error, next) {
|
|
655
|
+
if (error instanceof HttpErrorResponse) {
|
|
656
|
+
// if (error.status == 401) {
|
|
657
|
+
// this.router.navigate(['/auth/login']);
|
|
658
|
+
// }
|
|
659
|
+
// else if (error.status == 404) {
|
|
660
|
+
// this.router.navigate(['/404']);
|
|
661
|
+
// }
|
|
662
|
+
}
|
|
663
|
+
else if (error instanceof AXPUnauthorizedError) {
|
|
664
|
+
const redirectUrl = error.data?.redirectUrl;
|
|
665
|
+
console.error(error.message);
|
|
666
|
+
this.router.navigate(['/401'], { queryParams: redirectUrl ? { redirectUrl: redirectUrl } : null });
|
|
667
|
+
}
|
|
668
|
+
else if (error instanceof AXPUnauthenticatedError) {
|
|
669
|
+
console.error(error.message);
|
|
670
|
+
const redirectUrl = error.data?.redirectUrl;
|
|
671
|
+
this.router.navigate(['/auth/login'], { queryParams: redirectUrl ? { redirectUrl: redirectUrl } : null });
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
if (isDevMode()) {
|
|
675
|
+
next(error); // Pass the error to the next handler
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
next(error); // maybe we need a error page
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
this.layoutService.setNavigationLoading(false);
|
|
682
|
+
}
|
|
683
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMAuthErrorHandler, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
684
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMAuthErrorHandler }); }
|
|
685
|
+
}
|
|
686
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMAuthErrorHandler, decorators: [{
|
|
687
|
+
type: Injectable
|
|
688
|
+
}] });
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Guard for login page. Clears incomplete sessions so a prior OAuth attempt
|
|
692
|
+
* does not leak into the next username/password sign-in.
|
|
693
|
+
*/
|
|
694
|
+
const canActivateLogin = async (route, state) => {
|
|
695
|
+
const sessionService = inject(AXPSessionService);
|
|
696
|
+
const session = sessionService.getSessionData();
|
|
697
|
+
if (session?.user?.id && (!session.tenant?.id || !session.application?.id)) {
|
|
698
|
+
await sessionService.signout();
|
|
699
|
+
}
|
|
700
|
+
return true;
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
var AXMAuthenticationTypes;
|
|
704
|
+
(function (AXMAuthenticationTypes) {
|
|
705
|
+
AXMAuthenticationTypes[AXMAuthenticationTypes["UsernamePassword"] = 0] = "UsernamePassword";
|
|
706
|
+
AXMAuthenticationTypes[AXMAuthenticationTypes["UsernameEmailPassword"] = 1] = "UsernameEmailPassword";
|
|
707
|
+
AXMAuthenticationTypes[AXMAuthenticationTypes["EmailPassword"] = 2] = "EmailPassword";
|
|
708
|
+
AXMAuthenticationTypes[AXMAuthenticationTypes["MobilePassword"] = 3] = "MobilePassword";
|
|
709
|
+
AXMAuthenticationTypes[AXMAuthenticationTypes["EmailCode"] = 4] = "EmailCode";
|
|
710
|
+
AXMAuthenticationTypes[AXMAuthenticationTypes["MobileCode"] = 5] = "MobileCode";
|
|
711
|
+
})(AXMAuthenticationTypes || (AXMAuthenticationTypes = {}));
|
|
712
|
+
const AXM_AUTH_CONFIG_TOKEN = new InjectionToken('app-module-auth-config', {
|
|
713
|
+
providedIn: 'root',
|
|
714
|
+
factory: () => {
|
|
715
|
+
return AXMDefaultAuthConfigs;
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
const AXMDefaultAuthConfigs = {
|
|
719
|
+
type: AXMAuthenticationTypes.UsernamePassword,
|
|
720
|
+
// defaultStrategyName:'oidc',
|
|
721
|
+
// حذف authConfig از اینجا چون از environment خوانده میشود
|
|
722
|
+
defaultStrategyName: 'user-pass',
|
|
723
|
+
};
|
|
724
|
+
function configAuthModule(config = AXMDefaultAuthConfigs) {
|
|
725
|
+
return { ...AXMDefaultAuthConfigs, ...config };
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Injection token for login page guard.
|
|
729
|
+
* Apps can provide their own custom guard by providing this token.
|
|
730
|
+
* If not provided, the default guard (canActivateLogin) will be used.
|
|
731
|
+
*/
|
|
732
|
+
const AXM_LOGIN_GUARD_TOKEN = new InjectionToken('AXM_LOGIN_GUARD_TOKEN', {
|
|
733
|
+
providedIn: 'root',
|
|
734
|
+
factory: () => canActivateLogin,
|
|
735
|
+
});
|
|
736
|
+
//#endregion
|
|
737
|
+
|
|
738
|
+
//#region ---- Types ----
|
|
739
|
+
const AXPAuthPasswordComplexityLevels = {
|
|
740
|
+
Low: 'Low',
|
|
741
|
+
Medium: 'Medium',
|
|
742
|
+
High: 'High',
|
|
743
|
+
};
|
|
744
|
+
//#endregion
|
|
745
|
+
//#region ---- Policy ----
|
|
746
|
+
const PASSWORD_COMPLEXITY_POLICIES = {
|
|
747
|
+
[AXPAuthPasswordComplexityLevels.Low]: {
|
|
748
|
+
minLength: 6,
|
|
749
|
+
requireUppercase: false,
|
|
750
|
+
requireLowercase: false,
|
|
751
|
+
requireDigit: false,
|
|
752
|
+
requireSpecialCharacter: false,
|
|
753
|
+
},
|
|
754
|
+
[AXPAuthPasswordComplexityLevels.Medium]: {
|
|
755
|
+
minLength: 8,
|
|
756
|
+
requireUppercase: true,
|
|
757
|
+
requireLowercase: true,
|
|
758
|
+
requireDigit: true,
|
|
759
|
+
requireSpecialCharacter: false,
|
|
760
|
+
},
|
|
761
|
+
[AXPAuthPasswordComplexityLevels.High]: {
|
|
762
|
+
minLength: 12,
|
|
763
|
+
requireUppercase: true,
|
|
764
|
+
requireLowercase: true,
|
|
765
|
+
requireDigit: true,
|
|
766
|
+
requireSpecialCharacter: true,
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
const SPECIAL_CHARACTER_PATTERN = /[^a-zA-Z0-9]/;
|
|
770
|
+
//#endregion
|
|
771
|
+
//#region ---- Utility Methods ----
|
|
772
|
+
/**
|
|
773
|
+
* Normalizes tenant password complexity setting values to a supported level.
|
|
774
|
+
*/
|
|
775
|
+
function normalizePasswordComplexityLevel(value) {
|
|
776
|
+
if (isNil(value)) {
|
|
777
|
+
return AXPAuthPasswordComplexityLevels.Medium;
|
|
778
|
+
}
|
|
779
|
+
const normalized = coerceUnknownToTrimmedString(value).toLowerCase();
|
|
780
|
+
if (normalized === 'low') {
|
|
781
|
+
return AXPAuthPasswordComplexityLevels.Low;
|
|
782
|
+
}
|
|
783
|
+
if (normalized === 'medium') {
|
|
784
|
+
return AXPAuthPasswordComplexityLevels.Medium;
|
|
785
|
+
}
|
|
786
|
+
if (normalized === 'high') {
|
|
787
|
+
return AXPAuthPasswordComplexityLevels.High;
|
|
788
|
+
}
|
|
789
|
+
return AXPAuthPasswordComplexityLevels.Medium;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Returns the validation policy for the given complexity level.
|
|
793
|
+
*/
|
|
794
|
+
function getPasswordComplexityPolicy(level) {
|
|
795
|
+
return PASSWORD_COMPLEXITY_POLICIES[level];
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Validates a password against the configured complexity policy.
|
|
799
|
+
*/
|
|
800
|
+
function validatePasswordAgainstComplexity(password, level) {
|
|
801
|
+
const policy = getPasswordComplexityPolicy(level);
|
|
802
|
+
if (password.length < policy.minLength) {
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
if (policy.requireUppercase && !/[A-Z]/.test(password)) {
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
808
|
+
if (policy.requireLowercase && !/[a-z]/.test(password)) {
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
if (policy.requireDigit && !/\d/.test(password)) {
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
if (policy.requireSpecialCharacter && !SPECIAL_CHARACTER_PATTERN.test(password)) {
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Resolves the i18n key for the password complexity hint shown under the new password field.
|
|
821
|
+
*/
|
|
822
|
+
function getPasswordComplexityHintKey(level) {
|
|
823
|
+
return `@auth:profile-page.commands.change-password.complexity.hint.${level.toLowerCase()}`;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Resolves the i18n key for the password complexity validation error message.
|
|
827
|
+
*/
|
|
828
|
+
function getPasswordComplexityValidationMessageKey(level) {
|
|
829
|
+
return `@auth:profile-page.commands.change-password.complexity.validation.${level.toLowerCase()}`;
|
|
830
|
+
}
|
|
831
|
+
//#endregion
|
|
832
|
+
|
|
833
|
+
//#region ---- Password Complexity Service ----
|
|
834
|
+
/**
|
|
835
|
+
* Resolves tenant password complexity policy for auth flows (change password, registration, etc.).
|
|
836
|
+
* Uses tenant scope explicitly so inherited user-scope snapshots do not override tenant policy.
|
|
837
|
+
*/
|
|
838
|
+
class AXPAuthPasswordComplexityService {
|
|
839
|
+
constructor() {
|
|
840
|
+
//#region ---- Services & Dependencies ----
|
|
841
|
+
this.settingsService = inject(AXPSettingsService);
|
|
842
|
+
}
|
|
843
|
+
//#endregion
|
|
844
|
+
//#region ---- Public Methods ----
|
|
845
|
+
async getLevel() {
|
|
846
|
+
const tenantValue = await this.settingsService
|
|
847
|
+
.scope(AXPPlatformScope.Tenant)
|
|
848
|
+
.get(AXPAuthSettings.PasswordComplexity);
|
|
849
|
+
if (tenantValue !== undefined && tenantValue !== null) {
|
|
850
|
+
return normalizePasswordComplexityLevel(tenantValue);
|
|
851
|
+
}
|
|
852
|
+
const effectiveValue = await this.settingsService.get(AXPAuthSettings.PasswordComplexity);
|
|
853
|
+
return normalizePasswordComplexityLevel(effectiveValue);
|
|
854
|
+
}
|
|
855
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPAuthPasswordComplexityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
856
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPAuthPasswordComplexityService, providedIn: 'root' }); }
|
|
857
|
+
}
|
|
858
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPAuthPasswordComplexityService, decorators: [{
|
|
859
|
+
type: Injectable,
|
|
860
|
+
args: [{ providedIn: 'root' }]
|
|
861
|
+
}] });
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Validates passwords against tenant-configured complexity levels (Low, Medium, High).
|
|
865
|
+
*/
|
|
866
|
+
class PasswordComplexityValidationRule {
|
|
867
|
+
constructor() {
|
|
868
|
+
//#region ---- Services & Dependencies ----
|
|
869
|
+
this.passwordComplexityService = inject(AXPAuthPasswordComplexityService);
|
|
870
|
+
}
|
|
871
|
+
//#endregion
|
|
872
|
+
get name() {
|
|
873
|
+
return 'password-complexity';
|
|
874
|
+
}
|
|
875
|
+
async validate(value, options) {
|
|
876
|
+
const complexity = await this.resolveComplexity(options?.complexity);
|
|
877
|
+
const message = options?.message ?? '';
|
|
878
|
+
if (!isString(value) || value.length === 0) {
|
|
879
|
+
return {
|
|
880
|
+
rule: this.name,
|
|
881
|
+
result: false,
|
|
882
|
+
message,
|
|
883
|
+
value,
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
const isValid = validatePasswordAgainstComplexity(value, complexity);
|
|
887
|
+
return {
|
|
888
|
+
rule: this.name,
|
|
889
|
+
result: isValid,
|
|
890
|
+
message: isValid ? '' : message,
|
|
891
|
+
value,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
//#endregion
|
|
895
|
+
//#region ---- Utility Methods ----
|
|
896
|
+
async resolveComplexity(override) {
|
|
897
|
+
if (override !== undefined && override !== null && String(override).trim() !== '') {
|
|
898
|
+
return normalizePasswordComplexityLevel(override);
|
|
899
|
+
}
|
|
900
|
+
return this.passwordComplexityService.getLevel();
|
|
901
|
+
}
|
|
902
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PasswordComplexityValidationRule, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
903
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PasswordComplexityValidationRule }); }
|
|
904
|
+
}
|
|
905
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PasswordComplexityValidationRule, decorators: [{
|
|
906
|
+
type: Injectable
|
|
907
|
+
}] });
|
|
908
|
+
|
|
909
|
+
//#region ---- Auth Validation Rules Module ----
|
|
910
|
+
class AXMAuthValidationRulesModule {
|
|
911
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMAuthValidationRulesModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
912
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: AXMAuthValidationRulesModule, imports: [i1.AXValidationModule] }); }
|
|
913
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMAuthValidationRulesModule, imports: [AXValidationModule.forChild({
|
|
914
|
+
rules: [PasswordComplexityValidationRule],
|
|
915
|
+
})] }); }
|
|
916
|
+
}
|
|
917
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMAuthValidationRulesModule, decorators: [{
|
|
918
|
+
type: NgModule,
|
|
919
|
+
args: [{
|
|
920
|
+
imports: [
|
|
921
|
+
AXValidationModule.forChild({
|
|
922
|
+
rules: [PasswordComplexityValidationRule],
|
|
923
|
+
}),
|
|
924
|
+
],
|
|
925
|
+
}]
|
|
926
|
+
}] });
|
|
927
|
+
|
|
928
|
+
const routes = [
|
|
929
|
+
{
|
|
930
|
+
path: 'auth',
|
|
931
|
+
children: [
|
|
932
|
+
{
|
|
933
|
+
path: '',
|
|
934
|
+
pathMatch: 'full',
|
|
935
|
+
redirectTo: 'login',
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
path: '',
|
|
939
|
+
loadComponent: () => import('./acorex-platform-module-auth-client-master.layout-DpdD9kgk.mjs').then((c) => c.AXPAuthMasterLayoutComponent),
|
|
940
|
+
loadChildren: () => import('./acorex-platform-module-auth-client-login.module-DYtNYzF6.mjs').then((c) => c.AXPSignInModule),
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
path: '',
|
|
944
|
+
loadComponent: () => import('./acorex-platform-module-auth-client-master.layout-DpdD9kgk.mjs').then((c) => c.AXPAuthMasterLayoutComponent),
|
|
945
|
+
loadChildren: () => import('./acorex-platform-module-auth-client-two-factor.module-BJqdeuAz.mjs').then((c) => c.AXPTwoFactorModule),
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
path: '',
|
|
949
|
+
loadComponent: () => import('./acorex-platform-module-auth-client-master.layout-DpdD9kgk.mjs').then((c) => c.AXPAuthMasterLayoutComponent),
|
|
950
|
+
loadChildren: () => import('./acorex-platform-module-auth-client-routes-REjaWcqY.mjs').then((c) => c),
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
path: 'axp-oauth-callback',
|
|
954
|
+
loadComponent: () => import('./acorex-platform-module-auth-client-oauth-callback.component-BspBH4Ul.mjs').then((c) => c.AXPOAuthCallbackComponent),
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
path: 'account',
|
|
958
|
+
loadComponent: () => import('./acorex-platform-module-auth-client-blank.layout-BRGgntib.mjs').then((c) => c.AXPAuthBlankLayoutComponent),
|
|
959
|
+
loadChildren: () => Promise.resolve().then(function () { return account_module; }).then((c) => c.AXPAccountModule),
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
path: '',
|
|
963
|
+
loadComponent: () => import('./acorex-platform-module-auth-client-master.layout-DpdD9kgk.mjs').then((c) => c.AXPAuthMasterLayoutComponent),
|
|
964
|
+
loadChildren: () => import('./acorex-platform-module-auth-client-register.module-DXm6AQpo.mjs').then((c) => c.AXPRegisterModule),
|
|
965
|
+
},
|
|
966
|
+
],
|
|
967
|
+
},
|
|
968
|
+
];
|
|
969
|
+
|
|
970
|
+
class AXPAppChooserListComponent {
|
|
971
|
+
//#endregion
|
|
972
|
+
//#region ---- Computed Properties ----
|
|
973
|
+
isApplicationSelected(application) {
|
|
974
|
+
return application.id === this.sessionService.application?.id;
|
|
975
|
+
}
|
|
976
|
+
applicationLogoKey(application) {
|
|
977
|
+
return axpApplicationLogoKey(application.id);
|
|
978
|
+
}
|
|
979
|
+
//#endregion
|
|
980
|
+
//#region ---- Constructor ----
|
|
981
|
+
constructor() {
|
|
982
|
+
this.entityService = inject(AXPEntityService);
|
|
983
|
+
//#region ---- Services & Dependencies ----
|
|
984
|
+
this.sessionService = inject(AXPSessionService);
|
|
985
|
+
this.homePageService = inject(AXPHomePageService);
|
|
986
|
+
this.authStrategyRegistry = inject(AXPAuthStrategyRegistryService);
|
|
987
|
+
this.logoRegistration = inject(AXPEntityLogoRegistrationService);
|
|
988
|
+
this.applicationService = this.entityService.withEntity(RootConfig$2.module.name, RootConfig$2.entities.application.name).data();
|
|
989
|
+
this.strategyName = this.sessionService.getSessionData()?.strategy;
|
|
990
|
+
this.strategy = this.authStrategyRegistry.get(this.strategyName);
|
|
991
|
+
//#endregion
|
|
992
|
+
//#region ---- Inputs & Outputs ----
|
|
993
|
+
this.simple = input(false, ...(ngDevMode ? [{ debugName: "simple" }] : /* istanbul ignore next */ []));
|
|
994
|
+
this.applicationChosen = output();
|
|
995
|
+
//#endregion
|
|
996
|
+
//#region ---- View References ----
|
|
997
|
+
this.listNav = viewChild(AXListNavigationDirective, ...(ngDevMode ? [{ debugName: "listNav" }] : /* istanbul ignore next */ []));
|
|
998
|
+
this.listItems = viewChildren(AXListNavigationItemDirective, ...(ngDevMode ? [{ debugName: "listItems" }] : /* istanbul ignore next */ []));
|
|
999
|
+
//#endregion
|
|
1000
|
+
//#region ---- State ----
|
|
1001
|
+
this.isLoading = signal(true, ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
|
|
1002
|
+
this.allApplications = signal([], ...(ngDevMode ? [{ debugName: "allApplications" }] : /* istanbul ignore next */ []));
|
|
1003
|
+
this.useSelectBox = computed(() => this.allApplications().length > 3, ...(ngDevMode ? [{ debugName: "useSelectBox" }] : /* istanbul ignore next */ []));
|
|
1004
|
+
this.selectedApplication = signal(null, ...(ngDevMode ? [{ debugName: "selectedApplication" }] : /* istanbul ignore next */ []));
|
|
1005
|
+
this.isSelectBoxOpen = signal(false, ...(ngDevMode ? [{ debugName: "isSelectBoxOpen" }] : /* istanbul ignore next */ []));
|
|
1006
|
+
this.canApply = computed(() => !!this.selectedApplication(), ...(ngDevMode ? [{ debugName: "canApply" }] : /* istanbul ignore next */ []));
|
|
1007
|
+
this.skeletonItems = [1, 2, 3];
|
|
1008
|
+
effect(() => {
|
|
1009
|
+
if (!this.isLoading() && !this.useSelectBox() && this.allApplications().length > 0) {
|
|
1010
|
+
queueMicrotask(() => this.focusFirstListItem());
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
//#endregion
|
|
1015
|
+
//#region ---- Lifecycle Methods ----
|
|
1016
|
+
ngOnInit() {
|
|
1017
|
+
this.sessionService.applications$.pipe(first$1()).subscribe((applications) => {
|
|
1018
|
+
this.logoRegistration.registerApplications(applications);
|
|
1019
|
+
this.allApplications.set(applications);
|
|
1020
|
+
if (applications.length > 0) {
|
|
1021
|
+
this.selectedApplication.set(applications[0]);
|
|
1022
|
+
}
|
|
1023
|
+
this.isLoading.set(false);
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
//#endregion
|
|
1027
|
+
//#region ---- UI Handlers ----
|
|
1028
|
+
handleApplicationSelect(event) {
|
|
1029
|
+
if (!event.isUserInteraction) {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
this.selectedApplication.set(event.component.selectedItems[0] ?? null);
|
|
1033
|
+
}
|
|
1034
|
+
handleListItemKeyDown(event, application) {
|
|
1035
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1036
|
+
event.preventDefault();
|
|
1037
|
+
void this.chooseApplication(application);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
handleSelectBoxKeyDown(event) {
|
|
1041
|
+
if (!(event instanceof KeyboardEvent)) {
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
if (event.key !== 'Enter' && event.key !== 'NumpadEnter') {
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
if (this.isSelectBoxOpen()) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
event.preventDefault();
|
|
1051
|
+
event.stopPropagation();
|
|
1052
|
+
if (this.canApply()) {
|
|
1053
|
+
this.applySelection();
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
applySelection() {
|
|
1057
|
+
const application = this.selectedApplication();
|
|
1058
|
+
if (application) {
|
|
1059
|
+
void this.chooseApplication(application);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
async chooseApplication(item) {
|
|
1063
|
+
const tenantId = this.sessionService.tenant?.id || '';
|
|
1064
|
+
if (!tenantId) {
|
|
1065
|
+
throw new Error('Tenant ID is missing. Cannot choose application.');
|
|
1066
|
+
}
|
|
1067
|
+
const fullApplication = await this.applicationService.byKey(item.id);
|
|
1068
|
+
if (!fullApplication) {
|
|
1069
|
+
throw new Error(`Application not found: ${item.id}`);
|
|
1070
|
+
}
|
|
1071
|
+
let selectedEdition = item.edition;
|
|
1072
|
+
if (!selectedEdition && fullApplication.editions && fullApplication.editions.length > 0) {
|
|
1073
|
+
selectedEdition = {
|
|
1074
|
+
id: fullApplication.editions[0].id,
|
|
1075
|
+
title: fullApplication.editions[0].title,
|
|
1076
|
+
description: fullApplication.editions[0].description,
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
const applicationWithEdition = {
|
|
1080
|
+
...item,
|
|
1081
|
+
version: fullApplication.version || item.version,
|
|
1082
|
+
edition: selectedEdition,
|
|
1083
|
+
};
|
|
1084
|
+
if (this.strategy && typeof this.strategy.updateToken === 'function') {
|
|
1085
|
+
const updateContext = {
|
|
1086
|
+
application: applicationWithEdition,
|
|
1087
|
+
user: this.sessionService.user,
|
|
1088
|
+
tenant: this.sessionService.tenant,
|
|
1089
|
+
};
|
|
1090
|
+
await this.sessionService.updateToken(updateContext);
|
|
1091
|
+
}
|
|
1092
|
+
this.applicationChosen.emit();
|
|
1093
|
+
await this.homePageService.navigateTo();
|
|
1094
|
+
}
|
|
1095
|
+
//#endregion
|
|
1096
|
+
//#region ---- Utility Methods ----
|
|
1097
|
+
focusFirstListItem() {
|
|
1098
|
+
const nav = this.listNav();
|
|
1099
|
+
const items = this.listItems();
|
|
1100
|
+
const applications = this.allApplications();
|
|
1101
|
+
const currentIndex = applications.findIndex((application) => this.isApplicationSelected(application));
|
|
1102
|
+
if (nav && items.length > 0) {
|
|
1103
|
+
nav.navigateTo(items[currentIndex >= 0 ? currentIndex : 0]);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPAppChooserListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1107
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPAppChooserListComponent, isStandalone: true, selector: "axp-app-chooser-list", inputs: { simple: { classPropertyName: "simple", publicName: "simple", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { applicationChosen: "applicationChosen" }, viewQueries: [{ propertyName: "listNav", first: true, predicate: AXListNavigationDirective, descendants: true, isSignal: true }, { propertyName: "listItems", predicate: AXListNavigationItemDirective, descendants: true, isSignal: true }], ngImport: i0, template: "@if (isLoading()) {\n <div class=\"w-full flex flex-col gap-3\">\n @for (item of skeletonItems; track item) {\n <div class=\"flex gap-2 items-center p-2\">\n <ax-skeleton [animated]=\"true\" class=\"w-12 h-12 rounded-full shrink-0\"></ax-skeleton>\n <ax-skeleton [animated]=\"true\" class=\"flex-1 h-6 rounded-md\"></ax-skeleton>\n </div>\n }\n </div>\n} @else if (useSelectBox()) {\n <div class=\"w-full flex flex-col gap-4\" (keydown.capture)=\"handleSelectBoxKeyDown($event)\">\n <ax-select-box\n class=\"w-full\"\n [dataSource]=\"allApplications()\"\n textField=\"title\"\n valueField=\"id\"\n [itemHeight]=\"56\"\n [value]=\"selectedApplication()\"\n [placeholder]=\"('@auth:application-chooser.select-placeholder' | translate | async) ?? ''\"\n [itemTemplate]=\"applicationItemTemplate\"\n (onOpened)=\"isSelectBoxOpen.set(true)\"\n (onClosed)=\"isSelectBoxOpen.set(false)\"\n (onValueChanged)=\"handleApplicationSelect($event)\"\n >\n <ax-search-box look=\"fill\"></ax-search-box>\n </ax-select-box>\n <ax-button\n class=\"w-full\"\n color=\"primary\"\n [disabled]=\"!canApply()\"\n [text]=\"('@general:actions.apply.title' | translate | async) ?? ''\"\n (onClick)=\"applySelection()\"\n ></ax-button>\n </div>\n} @else {\n <div\n axListNavigation\n #listNav=\"axListNavigation\"\n class=\"w-full overflow-auto max-h-[300px] flex flex-col gap-1\"\n role=\"listbox\"\n >\n @for (application of allApplications(); track application.id) {\n <div\n axListNavigationItem\n #navItem=\"axListNavigationItem\"\n tabindex=\"0\"\n role=\"option\"\n [attr.aria-selected]=\"isApplicationSelected(application)\"\n class=\"flex gap-2 items-center p-2 cursor-pointer rounded-md transition-colors outline-none hover:bg-primary-lightest hover:dark:bg-primary-dark\"\n [class.bg-primary-lighter]=\"isApplicationSelected(application)\"\n [class.dark:bg-primary-darker]=\"isApplicationSelected(application)\"\n [class.text-primary-on-lighter]=\"isApplicationSelected(application)\"\n [class.dark:text-primary-on-darker]=\"isApplicationSelected(application)\"\n [class.ring-2]=\"navItem.isActive()\"\n [class.ring-primary]=\"navItem.isActive()\"\n (click)=\"chooseApplication(application)\"\n (keydown)=\"handleListItemKeyDown($event, application)\"\n >\n <div class=\"flex flex-1 gap-2 items-center min-w-0\">\n <div\n class=\"shrink-0 flex justify-center items-center rounded-full bg-neutral-100\"\n [class.w-10]=\"simple()\"\n [class.h-10]=\"simple()\"\n [class.w-12]=\"!simple()\"\n [class.h-12]=\"!simple()\"\n >\n <axp-logo\n [logoKey]=\"applicationLogoKey(application)\"\n appearance=\"colored\"\n [size]=\"40\"\n [attr.alt]=\"application.title\"\n class=\"flex! items-center justify-center text-sm! bg-neutral-200 dark:bg-neutral-800 rounded-full\"\n [class.w-8]=\"simple()\"\n [class.h-8]=\"simple()\"\n [class.w-10]=\"!simple()\"\n [class.h-10]=\"!simple()\"\n ></axp-logo>\n </div>\n <span class=\"flex-1 truncate text-start md:text-base text-sm\">{{ application.title }}</span>\n @if (application.edition?.title) {\n <span class=\"text-muted text-xs shrink-0\">{{ application.edition?.title }}</span>\n }\n </div>\n </div>\n }\n </div>\n}\n\n<ng-template #applicationItemTemplate let-item>\n <div class=\"flex gap-2 items-center p-2\">\n <div\n class=\"w-10 h-10 flex justify-center items-center rounded-full bg-neutral-100 shrink-0\"\n >\n <axp-logo\n [logoKey]=\"applicationLogoKey(item.data)\"\n appearance=\"colored\"\n [size]=\"32\"\n [attr.alt]=\"item.data.title\"\n class=\"flex! items-center justify-center text-sm! w-8 h-8 bg-neutral-200 rounded-full\"\n ></axp-logo>\n </div>\n <span class=\"flex-1 text-start md:text-base text-sm\">{{ item.data.title }}</span>\n @if (item.data.edition?.title) {\n <span class=\"text-muted text-xs\">{{ item.data.edition.title }}</span>\n }\n </div>\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i1$1.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "ngmodule", type: AXSkeletonModule }, { kind: "component", type: i2.AXSkeletonComponent, selector: "ax-skeleton", inputs: ["animated"] }, { kind: "ngmodule", type: AXSelectBoxModule }, { kind: "component", type: i3.AXSelectBoxComponent, selector: "ax-select-box", inputs: ["disabled", "readonly", "tabIndex", "placeholder", "minValue", "maxValue", "value", "state", "name", "id", "type", "look", "multiple", "valueField", "textField", "disabledField", "textTemplate", "selectedItems", "isItemTruncated", "showItemTooltip", "itemHeight", "maxVisibleItems", "dataSource", "minRecordsForSearch", "caption", "itemTemplate", "selectedTemplate", "emptyTemplate", "loadingTemplate", "dropdownWidth", "searchBoxAutoFocus"], outputs: ["valueChange", "stateChange", "onValueChanged", "onBlur", "onFocus", "readonlyChange", "disabledChange", "onOpened", "onClosed", "onItemSelected", "onItemClick"] }, { kind: "ngmodule", type: AXSearchBoxModule }, { kind: "component", type: i4.AXSearchBoxComponent, selector: "ax-search-box", inputs: ["disabled", "readonly", "tabIndex", "placeholder", "value", "state", "name", "id", "look", "class", "delayTime", "type", "autoSearch"], outputs: ["valueChange", "stateChange", "onValueChanged", "onBlur", "onFocus", "readonlyChange", "disabledChange", "onKeyDown", "onKeyUp", "onKeyPress"] }, { kind: "ngmodule", type: AXListNavigationModule }, { kind: "directive", type: i5.AXListNavigationDirective, selector: "[axListNavigation]", inputs: ["orientation"], outputs: ["onNavigationChanged", "onKeypress"], exportAs: ["axListNavigation"] }, { kind: "directive", type: i5.AXListNavigationItemDirective, selector: "[axListNavigationItem]", outputs: ["onKeypress"], exportAs: ["axListNavigationItem"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "component", type: AXPLogoComponent, selector: "axp-logo", inputs: ["logoKey", "appearance", "size", "format"] }, { kind: "pipe", type: i4$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i2$1.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1108
|
+
}
|
|
1109
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPAppChooserListComponent, decorators: [{
|
|
1110
|
+
type: Component,
|
|
1111
|
+
args: [{ selector: 'axp-app-chooser-list', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
1112
|
+
CommonModule,
|
|
1113
|
+
AXButtonModule,
|
|
1114
|
+
AXDecoratorModule,
|
|
1115
|
+
AXSkeletonModule,
|
|
1116
|
+
AXSelectBoxModule,
|
|
1117
|
+
AXSearchBoxModule,
|
|
1118
|
+
AXListNavigationModule,
|
|
1119
|
+
AXTranslationModule,
|
|
1120
|
+
AXPLogoComponent,
|
|
1121
|
+
], template: "@if (isLoading()) {\n <div class=\"w-full flex flex-col gap-3\">\n @for (item of skeletonItems; track item) {\n <div class=\"flex gap-2 items-center p-2\">\n <ax-skeleton [animated]=\"true\" class=\"w-12 h-12 rounded-full shrink-0\"></ax-skeleton>\n <ax-skeleton [animated]=\"true\" class=\"flex-1 h-6 rounded-md\"></ax-skeleton>\n </div>\n }\n </div>\n} @else if (useSelectBox()) {\n <div class=\"w-full flex flex-col gap-4\" (keydown.capture)=\"handleSelectBoxKeyDown($event)\">\n <ax-select-box\n class=\"w-full\"\n [dataSource]=\"allApplications()\"\n textField=\"title\"\n valueField=\"id\"\n [itemHeight]=\"56\"\n [value]=\"selectedApplication()\"\n [placeholder]=\"('@auth:application-chooser.select-placeholder' | translate | async) ?? ''\"\n [itemTemplate]=\"applicationItemTemplate\"\n (onOpened)=\"isSelectBoxOpen.set(true)\"\n (onClosed)=\"isSelectBoxOpen.set(false)\"\n (onValueChanged)=\"handleApplicationSelect($event)\"\n >\n <ax-search-box look=\"fill\"></ax-search-box>\n </ax-select-box>\n <ax-button\n class=\"w-full\"\n color=\"primary\"\n [disabled]=\"!canApply()\"\n [text]=\"('@general:actions.apply.title' | translate | async) ?? ''\"\n (onClick)=\"applySelection()\"\n ></ax-button>\n </div>\n} @else {\n <div\n axListNavigation\n #listNav=\"axListNavigation\"\n class=\"w-full overflow-auto max-h-[300px] flex flex-col gap-1\"\n role=\"listbox\"\n >\n @for (application of allApplications(); track application.id) {\n <div\n axListNavigationItem\n #navItem=\"axListNavigationItem\"\n tabindex=\"0\"\n role=\"option\"\n [attr.aria-selected]=\"isApplicationSelected(application)\"\n class=\"flex gap-2 items-center p-2 cursor-pointer rounded-md transition-colors outline-none hover:bg-primary-lightest hover:dark:bg-primary-dark\"\n [class.bg-primary-lighter]=\"isApplicationSelected(application)\"\n [class.dark:bg-primary-darker]=\"isApplicationSelected(application)\"\n [class.text-primary-on-lighter]=\"isApplicationSelected(application)\"\n [class.dark:text-primary-on-darker]=\"isApplicationSelected(application)\"\n [class.ring-2]=\"navItem.isActive()\"\n [class.ring-primary]=\"navItem.isActive()\"\n (click)=\"chooseApplication(application)\"\n (keydown)=\"handleListItemKeyDown($event, application)\"\n >\n <div class=\"flex flex-1 gap-2 items-center min-w-0\">\n <div\n class=\"shrink-0 flex justify-center items-center rounded-full bg-neutral-100\"\n [class.w-10]=\"simple()\"\n [class.h-10]=\"simple()\"\n [class.w-12]=\"!simple()\"\n [class.h-12]=\"!simple()\"\n >\n <axp-logo\n [logoKey]=\"applicationLogoKey(application)\"\n appearance=\"colored\"\n [size]=\"40\"\n [attr.alt]=\"application.title\"\n class=\"flex! items-center justify-center text-sm! bg-neutral-200 dark:bg-neutral-800 rounded-full\"\n [class.w-8]=\"simple()\"\n [class.h-8]=\"simple()\"\n [class.w-10]=\"!simple()\"\n [class.h-10]=\"!simple()\"\n ></axp-logo>\n </div>\n <span class=\"flex-1 truncate text-start md:text-base text-sm\">{{ application.title }}</span>\n @if (application.edition?.title) {\n <span class=\"text-muted text-xs shrink-0\">{{ application.edition?.title }}</span>\n }\n </div>\n </div>\n }\n </div>\n}\n\n<ng-template #applicationItemTemplate let-item>\n <div class=\"flex gap-2 items-center p-2\">\n <div\n class=\"w-10 h-10 flex justify-center items-center rounded-full bg-neutral-100 shrink-0\"\n >\n <axp-logo\n [logoKey]=\"applicationLogoKey(item.data)\"\n appearance=\"colored\"\n [size]=\"32\"\n [attr.alt]=\"item.data.title\"\n class=\"flex! items-center justify-center text-sm! w-8 h-8 bg-neutral-200 rounded-full\"\n ></axp-logo>\n </div>\n <span class=\"flex-1 text-start md:text-base text-sm\">{{ item.data.title }}</span>\n @if (item.data.edition?.title) {\n <span class=\"text-muted text-xs\">{{ item.data.edition.title }}</span>\n }\n </div>\n</ng-template>\n" }]
|
|
1122
|
+
}], ctorParameters: () => [], propDecorators: { simple: [{ type: i0.Input, args: [{ isSignal: true, alias: "simple", required: false }] }], applicationChosen: [{ type: i0.Output, args: ["applicationChosen"] }], listNav: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AXListNavigationDirective), { isSignal: true }] }], listItems: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => AXListNavigationItemDirective), { isSignal: true }] }] } });
|
|
1123
|
+
|
|
1124
|
+
class AXMAppChooserSlotComponent {
|
|
1125
|
+
constructor() {
|
|
1126
|
+
this.sessionService = inject(AXPSessionService);
|
|
1127
|
+
this.applications$ = this.sessionService.applications$;
|
|
1128
|
+
this.appPopover = viewChild('appPopover', ...(ngDevMode ? [{ debugName: "appPopover" }] : /* istanbul ignore next */ []));
|
|
1129
|
+
}
|
|
1130
|
+
closePopover() {
|
|
1131
|
+
this.appPopover()?.close();
|
|
1132
|
+
}
|
|
1133
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMAppChooserSlotComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1134
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXMAppChooserSlotComponent, isStandalone: true, selector: "ng-component", viewQueries: [{ propertyName: "appPopover", first: true, predicate: ["appPopover"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (((applications$ | async)?.length ?? 0) > 1) {\n <ax-button color=\"primary\" #appChooser>\n <ax-icon class=\"fa-regular fa-grid-2\"> </ax-icon>\n </ax-button>\n <ax-popover [target]=\"appChooser\" [openOn]=\"'toggle'\" [closeOn]=\"'clickOut'\" #appPopover>\n <div class=\"bg-lightest border overflow-hidden rounded-md shadow-md w-full min-w-64 p-1\">\n <axp-app-chooser-list [simple]=\"true\" (applicationChosen)=\"closePopover()\"></axp-app-chooser-list>\n </div>\n </ax-popover>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i1$1.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "ngmodule", type: AXPopoverModule }, { kind: "component", type: i2$2.AXPopoverComponent, selector: "ax-popover", inputs: ["width", "disablePanelClass", "disabled", "offsetX", "offsetY", "target", "placement", "content", "openOn", "closeOn", "hasBackdrop", "openAfter", "closeAfter", "closeOnScroll", "backdropClass", "panelClass", "adaptivityEnabled"], outputs: ["onOpened", "onClosed"] }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i3$1.AXDecoratorIconComponent, selector: "ax-icon", inputs: ["icon"] }, { kind: "component", type: AXPAppChooserListComponent, selector: "axp-app-chooser-list", inputs: ["simple"], outputs: ["applicationChosen"] }, { kind: "pipe", type: i4$1.AsyncPipe, name: "async" }] }); }
|
|
1135
|
+
}
|
|
1136
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMAppChooserSlotComponent, decorators: [{
|
|
1137
|
+
type: Component,
|
|
1138
|
+
args: [{ imports: [CommonModule, AXButtonModule, AXPopoverModule, AXDecoratorModule, AXPAppChooserListComponent], template: "@if (((applications$ | async)?.length ?? 0) > 1) {\n <ax-button color=\"primary\" #appChooser>\n <ax-icon class=\"fa-regular fa-grid-2\"> </ax-icon>\n </ax-button>\n <ax-popover [target]=\"appChooser\" [openOn]=\"'toggle'\" [closeOn]=\"'clickOut'\" #appPopover>\n <div class=\"bg-lightest border overflow-hidden rounded-md shadow-md w-full min-w-64 p-1\">\n <axp-app-chooser-list [simple]=\"true\" (applicationChosen)=\"closePopover()\"></axp-app-chooser-list>\n </div>\n </ax-popover>\n}\n" }]
|
|
1139
|
+
}], propDecorators: { appPopover: [{ type: i0.ViewChild, args: ['appPopover', { isSignal: true }] }] } });
|
|
1140
|
+
|
|
1141
|
+
class AXPTenantChooserListComponent {
|
|
1142
|
+
tenantLogoKey(tenant) {
|
|
1143
|
+
return axpTenantLogoKey(tenant.id);
|
|
1144
|
+
}
|
|
1145
|
+
//#endregion
|
|
1146
|
+
//#region ---- Computed Properties ----
|
|
1147
|
+
isTenantSelected(tenant) {
|
|
1148
|
+
return tenant.id === this.sessionService.tenant?.id;
|
|
1149
|
+
}
|
|
1150
|
+
//#endregion
|
|
1151
|
+
//#region ---- Constructor ----
|
|
1152
|
+
constructor() {
|
|
1153
|
+
//#region ---- Services & Dependencies ----
|
|
1154
|
+
this.sessionService = inject(AXPSessionService);
|
|
1155
|
+
this.authStrategyRegistry = inject(AXPAuthStrategyRegistryService);
|
|
1156
|
+
this.logoRegistration = inject(AXPEntityLogoRegistrationService);
|
|
1157
|
+
this.router = inject(Router);
|
|
1158
|
+
this.strategyName = this.sessionService.getSessionData()?.strategy;
|
|
1159
|
+
this.strategy = this.authStrategyRegistry.get(this.strategyName);
|
|
1160
|
+
//#endregion
|
|
1161
|
+
//#region ---- Inputs & Outputs ----
|
|
1162
|
+
this.simple = input(false, ...(ngDevMode ? [{ debugName: "simple" }] : /* istanbul ignore next */ []));
|
|
1163
|
+
this.tenantChosen = output();
|
|
1164
|
+
//#endregion
|
|
1165
|
+
//#region ---- View References ----
|
|
1166
|
+
this.listNav = viewChild(AXListNavigationDirective, ...(ngDevMode ? [{ debugName: "listNav" }] : /* istanbul ignore next */ []));
|
|
1167
|
+
this.listItems = viewChildren(AXListNavigationItemDirective, ...(ngDevMode ? [{ debugName: "listItems" }] : /* istanbul ignore next */ []));
|
|
1168
|
+
//#endregion
|
|
1169
|
+
//#region ---- State ----
|
|
1170
|
+
this.isLoading = signal(true, ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
|
|
1171
|
+
this.allTenants = signal([], ...(ngDevMode ? [{ debugName: "allTenants" }] : /* istanbul ignore next */ []));
|
|
1172
|
+
this.searchValue = signal('', ...(ngDevMode ? [{ debugName: "searchValue" }] : /* istanbul ignore next */ []));
|
|
1173
|
+
this.showAll = signal(false, ...(ngDevMode ? [{ debugName: "showAll" }] : /* istanbul ignore next */ []));
|
|
1174
|
+
this.useSelectBox = computed(() => this.allTenants().length > 3, ...(ngDevMode ? [{ debugName: "useSelectBox" }] : /* istanbul ignore next */ []));
|
|
1175
|
+
this.selectedTenant = signal(null, ...(ngDevMode ? [{ debugName: "selectedTenant" }] : /* istanbul ignore next */ []));
|
|
1176
|
+
this.isSelectBoxOpen = signal(false, ...(ngDevMode ? [{ debugName: "isSelectBoxOpen" }] : /* istanbul ignore next */ []));
|
|
1177
|
+
this.canApply = computed(() => !!this.selectedTenant(), ...(ngDevMode ? [{ debugName: "canApply" }] : /* istanbul ignore next */ []));
|
|
1178
|
+
this.skeletonItems = [1, 2, 3];
|
|
1179
|
+
/** Tenants shown in the list (respects search and view-all toggle). */
|
|
1180
|
+
this.displayedTenants = computed(() => {
|
|
1181
|
+
const tenants = this.filterTenants(this.allTenants());
|
|
1182
|
+
if (this.showAll() || tenants.length <= 4) {
|
|
1183
|
+
return tenants;
|
|
1184
|
+
}
|
|
1185
|
+
return tenants.slice(0, 4);
|
|
1186
|
+
}, ...(ngDevMode ? [{ debugName: "displayedTenants" }] : /* istanbul ignore next */ []));
|
|
1187
|
+
this.showSearchAndViewAll = computed(() => this.allTenants().length > 4, ...(ngDevMode ? [{ debugName: "showSearchAndViewAll" }] : /* istanbul ignore next */ []));
|
|
1188
|
+
effect(() => {
|
|
1189
|
+
if (!this.isLoading() && !this.useSelectBox() && !this.simple() && this.allTenants().length > 0) {
|
|
1190
|
+
queueMicrotask(() => this.focusFirstListItem());
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
//#endregion
|
|
1195
|
+
//#region ---- Lifecycle Methods ----
|
|
1196
|
+
ngOnInit() {
|
|
1197
|
+
this.sessionService.tenants$.pipe(first$1()).subscribe((tenants) => {
|
|
1198
|
+
this.logoRegistration.registerTenants(tenants);
|
|
1199
|
+
this.allTenants.set(tenants);
|
|
1200
|
+
if (tenants.length > 0) {
|
|
1201
|
+
this.selectedTenant.set(tenants[0]);
|
|
1202
|
+
}
|
|
1203
|
+
this.isLoading.set(false);
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
//#endregion
|
|
1207
|
+
//#region ---- UI Handlers ----
|
|
1208
|
+
filterTenants(tenants) {
|
|
1209
|
+
const query = this.searchValue().toLowerCase();
|
|
1210
|
+
return tenants.filter((tenant) => tenant.title.toLowerCase().includes(query));
|
|
1211
|
+
}
|
|
1212
|
+
handleSearch(event) {
|
|
1213
|
+
if (event !== undefined) {
|
|
1214
|
+
this.searchValue.set(event);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
showAllTenants() {
|
|
1218
|
+
this.showAll.update((value) => !value);
|
|
1219
|
+
}
|
|
1220
|
+
handleTenantSelect(event) {
|
|
1221
|
+
if (!event.isUserInteraction) {
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
this.selectedTenant.set(event.component.selectedItems[0] ?? null);
|
|
1225
|
+
}
|
|
1226
|
+
handleListItemKeyDown(event, tenant) {
|
|
1227
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1228
|
+
event.preventDefault();
|
|
1229
|
+
void this.chooseTenant(tenant);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
handleSelectBoxKeyDown(event) {
|
|
1233
|
+
if (!(event instanceof KeyboardEvent)) {
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
if (event.key !== 'Enter' && event.key !== 'NumpadEnter') {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
if (this.isSelectBoxOpen()) {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
event.preventDefault();
|
|
1243
|
+
event.stopPropagation();
|
|
1244
|
+
if (this.canApply()) {
|
|
1245
|
+
this.applySelection();
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
applySelection() {
|
|
1249
|
+
const tenant = this.selectedTenant();
|
|
1250
|
+
if (tenant) {
|
|
1251
|
+
void this.chooseTenant(tenant);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
async chooseTenant(item) {
|
|
1255
|
+
if (this.strategy) {
|
|
1256
|
+
const updateContext = {
|
|
1257
|
+
tenant: item,
|
|
1258
|
+
user: this.sessionService.user,
|
|
1259
|
+
};
|
|
1260
|
+
await this.sessionService.updateToken(updateContext);
|
|
1261
|
+
}
|
|
1262
|
+
this.tenantChosen.emit();
|
|
1263
|
+
return this.router.navigate(['/auth/account/app-chooser']);
|
|
1264
|
+
}
|
|
1265
|
+
//#endregion
|
|
1266
|
+
//#region ---- Utility Methods ----
|
|
1267
|
+
focusFirstListItem() {
|
|
1268
|
+
const nav = this.listNav();
|
|
1269
|
+
const items = this.listItems();
|
|
1270
|
+
const tenants = this.allTenants();
|
|
1271
|
+
const currentIndex = tenants.findIndex((tenant) => this.isTenantSelected(tenant));
|
|
1272
|
+
if (nav && items.length > 0) {
|
|
1273
|
+
nav.navigateTo(items[currentIndex >= 0 ? currentIndex : 0]);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPTenantChooserListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1277
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPTenantChooserListComponent, isStandalone: true, selector: "axp-tenant-chooser-list", inputs: { simple: { classPropertyName: "simple", publicName: "simple", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { tenantChosen: "tenantChosen" }, viewQueries: [{ propertyName: "listNav", first: true, predicate: AXListNavigationDirective, descendants: true, isSignal: true }, { propertyName: "listItems", predicate: AXListNavigationItemDirective, descendants: true, isSignal: true }], ngImport: i0, template: "@if (isLoading()) {\n <div class=\"w-full flex flex-col gap-3\">\n @for (item of skeletonItems; track item) {\n <div class=\"flex gap-2 items-center p-2\">\n <ax-skeleton [animated]=\"true\" class=\"w-12 h-12 rounded-full shrink-0\"></ax-skeleton>\n <ax-skeleton [animated]=\"true\" class=\"flex-1 h-6 rounded-md\"></ax-skeleton>\n </div>\n }\n </div>\n} @else if (simple()) {\n <div class=\"w-full overflow-auto max-h-[300px] pe-2\">\n @for (tenant of allTenants(); track tenant.id) {\n <div\n class=\"flex gap-2 justify-between border-b last:border-b-0 py-3 items-start cursor-pointer\"\n (click)=\"chooseTenant(tenant)\"\n >\n <div class=\"flex flex-1 gap-2 items-center font-medium\">\n <div\n class=\"w-12 h-12 flex justify-center items-center rounded-full bg-neutral-100 m-auto\"\n >\n <axp-logo\n [logoKey]=\"tenantLogoKey(tenant)\"\n appearance=\"colored\"\n [size]=\"40\"\n [attr.alt]=\"tenant.title\"\n class=\"flex! items-center justify-center text-sm! mx-auto w-10 h-10 bg-neutral-200 rounded-full\"\n ></axp-logo>\n </div>\n <span class=\"flex-1 text-start md:text-base text-sm\">{{ tenant.title }}</span>\n </div>\n </div>\n }\n </div>\n} @else if (useSelectBox()) {\n <div class=\"w-full flex flex-col gap-4\" (keydown.capture)=\"handleSelectBoxKeyDown($event)\">\n <ax-select-box\n class=\"w-full\"\n [dataSource]=\"allTenants()\"\n textField=\"title\"\n valueField=\"id\"\n [value]=\"selectedTenant()\"\n [itemHeight]=\"56\"\n [placeholder]=\"('@auth:tenant-chooser.select-placeholder' | translate | async) ?? ''\"\n [itemTemplate]=\"tenantItemTemplate\"\n (onOpened)=\"isSelectBoxOpen.set(true)\"\n (onClosed)=\"isSelectBoxOpen.set(false)\"\n (onValueChanged)=\"handleTenantSelect($event)\"\n >\n <ax-search-box look=\"fill\"></ax-search-box>\n </ax-select-box>\n <ax-button\n class=\"w-full\"\n color=\"primary\"\n [disabled]=\"!canApply()\"\n [text]=\"('@general:actions.apply.title' | translate | async) ?? ''\"\n (onClick)=\"applySelection()\"\n ></ax-button>\n </div>\n} @else {\n @if (showSearchAndViewAll()) {\n <ax-search-box\n [placeholder]=\"'@general:search.title' | translate | async\"\n [ngModel]=\"searchValue()\"\n (ngModelChange)=\"handleSearch($event)\"\n class=\"w-full\"\n >\n <ax-clear-button></ax-clear-button>\n </ax-search-box>\n }\n <div\n axListNavigation\n #listNav=\"axListNavigation\"\n class=\"w-full overflow-auto max-h-[300px] flex flex-col gap-1\"\n role=\"listbox\"\n >\n @for (tenant of displayedTenants(); track tenant.id) {\n <div\n axListNavigationItem\n #navItem=\"axListNavigationItem\"\n tabindex=\"0\"\n role=\"option\"\n [attr.aria-selected]=\"isTenantSelected(tenant)\"\n class=\"flex gap-2 items-center p-2 cursor-pointer rounded-md transition-colors outline-none hover:bg-primary-lightest hover:dark:bg-primary-dark\"\n [class.bg-primary-lighter]=\"isTenantSelected(tenant)\"\n [class.dark:bg-primary-darker]=\"isTenantSelected(tenant)\"\n [class.text-primary-on-lighter]=\"isTenantSelected(tenant)\"\n [class.dark:text-primary-on-darker]=\"isTenantSelected(tenant)\"\n [class.ring-2]=\"navItem.isActive()\"\n [class.ring-primary]=\"navItem.isActive()\"\n (click)=\"chooseTenant(tenant)\"\n (keydown)=\"handleListItemKeyDown($event, tenant)\"\n >\n <div class=\"flex flex-1 gap-2 items-center min-w-0\">\n <div\n class=\"shrink-0 flex justify-center items-center rounded-full bg-neutral-100\"\n [class.w-10]=\"simple()\"\n [class.h-10]=\"simple()\"\n [class.w-12]=\"!simple()\"\n [class.h-12]=\"!simple()\"\n >\n <axp-logo\n [logoKey]=\"tenantLogoKey(tenant)\"\n appearance=\"colored\"\n [size]=\"40\"\n [attr.alt]=\"tenant.title\"\n class=\"flex! items-center justify-center text-sm! bg-neutral-200 dark:bg-neutral-800 rounded-full\"\n [class.w-8]=\"simple()\"\n [class.h-8]=\"simple()\"\n [class.w-10]=\"!simple()\"\n [class.h-10]=\"!simple()\"\n ></axp-logo>\n </div>\n <span class=\"flex-1 truncate text-start md:text-base text-sm\">{{ tenant.title }}</span>\n </div>\n </div>\n }\n </div>\n @if (showSearchAndViewAll()) {\n <div class=\"mt-6 w-full flex justify-center\">\n <ax-button\n (onClick)=\"showAllTenants()\"\n class=\"w-full\"\n look=\"outline\"\n [text]=\"\n showAll()\n ? ('@general:actions.view-less.title' | translate | async)\n : ('@general:actions.view-all.title' | translate | async)\n \"\n ></ax-button>\n </div>\n }\n}\n\n<ng-template #tenantItemTemplate let-item>\n <div class=\"flex gap-2 items-center p-2\">\n <div\n class=\"w-10 h-10 flex justify-center items-center rounded-full bg-neutral-100 shrink-0\"\n >\n <axp-logo\n [logoKey]=\"tenantLogoKey(item.data)\"\n appearance=\"colored\"\n [size]=\"32\"\n [attr.alt]=\"item.data.title\"\n class=\"flex! items-center justify-center text-sm! w-8 h-8 bg-neutral-200 rounded-full\"\n ></axp-logo>\n </div>\n <span class=\"text-start md:text-base text-sm\">{{ item.data.title }}</span>\n </div>\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i1$1.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i3$1.AXDecoratorClearButtonComponent, selector: "ax-clear-button", inputs: ["icon"] }, { kind: "ngmodule", type: AXSkeletonModule }, { kind: "component", type: i2.AXSkeletonComponent, selector: "ax-skeleton", inputs: ["animated"] }, { kind: "ngmodule", type: AXSelectBoxModule }, { kind: "component", type: i3.AXSelectBoxComponent, selector: "ax-select-box", inputs: ["disabled", "readonly", "tabIndex", "placeholder", "minValue", "maxValue", "value", "state", "name", "id", "type", "look", "multiple", "valueField", "textField", "disabledField", "textTemplate", "selectedItems", "isItemTruncated", "showItemTooltip", "itemHeight", "maxVisibleItems", "dataSource", "minRecordsForSearch", "caption", "itemTemplate", "selectedTemplate", "emptyTemplate", "loadingTemplate", "dropdownWidth", "searchBoxAutoFocus"], outputs: ["valueChange", "stateChange", "onValueChanged", "onBlur", "onFocus", "readonlyChange", "disabledChange", "onOpened", "onClosed", "onItemSelected", "onItemClick"] }, { kind: "ngmodule", type: AXSearchBoxModule }, { kind: "component", type: i4.AXSearchBoxComponent, selector: "ax-search-box", inputs: ["disabled", "readonly", "tabIndex", "placeholder", "value", "state", "name", "id", "look", "class", "delayTime", "type", "autoSearch"], outputs: ["valueChange", "stateChange", "onValueChanged", "onBlur", "onFocus", "readonlyChange", "disabledChange", "onKeyDown", "onKeyUp", "onKeyPress"] }, { kind: "ngmodule", type: AXListNavigationModule }, { kind: "directive", type: i5.AXListNavigationDirective, selector: "[axListNavigation]", inputs: ["orientation"], outputs: ["onNavigationChanged", "onKeypress"], exportAs: ["axListNavigation"] }, { kind: "directive", type: i5.AXListNavigationItemDirective, selector: "[axListNavigationItem]", outputs: ["onKeypress"], exportAs: ["axListNavigationItem"] }, { kind: "component", type: AXPLogoComponent, selector: "axp-logo", inputs: ["logoKey", "appearance", "size", "format"] }, { kind: "pipe", type: i4$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i2$1.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1278
|
+
}
|
|
1279
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPTenantChooserListComponent, decorators: [{
|
|
1280
|
+
type: Component,
|
|
1281
|
+
args: [{ selector: 'axp-tenant-chooser-list', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
1282
|
+
CommonModule,
|
|
1283
|
+
FormsModule,
|
|
1284
|
+
AXTranslationModule,
|
|
1285
|
+
AXButtonModule,
|
|
1286
|
+
AXDecoratorModule,
|
|
1287
|
+
AXSkeletonModule,
|
|
1288
|
+
AXSelectBoxModule,
|
|
1289
|
+
AXSearchBoxModule,
|
|
1290
|
+
AXListNavigationModule,
|
|
1291
|
+
AXPLogoComponent,
|
|
1292
|
+
], template: "@if (isLoading()) {\n <div class=\"w-full flex flex-col gap-3\">\n @for (item of skeletonItems; track item) {\n <div class=\"flex gap-2 items-center p-2\">\n <ax-skeleton [animated]=\"true\" class=\"w-12 h-12 rounded-full shrink-0\"></ax-skeleton>\n <ax-skeleton [animated]=\"true\" class=\"flex-1 h-6 rounded-md\"></ax-skeleton>\n </div>\n }\n </div>\n} @else if (simple()) {\n <div class=\"w-full overflow-auto max-h-[300px] pe-2\">\n @for (tenant of allTenants(); track tenant.id) {\n <div\n class=\"flex gap-2 justify-between border-b last:border-b-0 py-3 items-start cursor-pointer\"\n (click)=\"chooseTenant(tenant)\"\n >\n <div class=\"flex flex-1 gap-2 items-center font-medium\">\n <div\n class=\"w-12 h-12 flex justify-center items-center rounded-full bg-neutral-100 m-auto\"\n >\n <axp-logo\n [logoKey]=\"tenantLogoKey(tenant)\"\n appearance=\"colored\"\n [size]=\"40\"\n [attr.alt]=\"tenant.title\"\n class=\"flex! items-center justify-center text-sm! mx-auto w-10 h-10 bg-neutral-200 rounded-full\"\n ></axp-logo>\n </div>\n <span class=\"flex-1 text-start md:text-base text-sm\">{{ tenant.title }}</span>\n </div>\n </div>\n }\n </div>\n} @else if (useSelectBox()) {\n <div class=\"w-full flex flex-col gap-4\" (keydown.capture)=\"handleSelectBoxKeyDown($event)\">\n <ax-select-box\n class=\"w-full\"\n [dataSource]=\"allTenants()\"\n textField=\"title\"\n valueField=\"id\"\n [value]=\"selectedTenant()\"\n [itemHeight]=\"56\"\n [placeholder]=\"('@auth:tenant-chooser.select-placeholder' | translate | async) ?? ''\"\n [itemTemplate]=\"tenantItemTemplate\"\n (onOpened)=\"isSelectBoxOpen.set(true)\"\n (onClosed)=\"isSelectBoxOpen.set(false)\"\n (onValueChanged)=\"handleTenantSelect($event)\"\n >\n <ax-search-box look=\"fill\"></ax-search-box>\n </ax-select-box>\n <ax-button\n class=\"w-full\"\n color=\"primary\"\n [disabled]=\"!canApply()\"\n [text]=\"('@general:actions.apply.title' | translate | async) ?? ''\"\n (onClick)=\"applySelection()\"\n ></ax-button>\n </div>\n} @else {\n @if (showSearchAndViewAll()) {\n <ax-search-box\n [placeholder]=\"'@general:search.title' | translate | async\"\n [ngModel]=\"searchValue()\"\n (ngModelChange)=\"handleSearch($event)\"\n class=\"w-full\"\n >\n <ax-clear-button></ax-clear-button>\n </ax-search-box>\n }\n <div\n axListNavigation\n #listNav=\"axListNavigation\"\n class=\"w-full overflow-auto max-h-[300px] flex flex-col gap-1\"\n role=\"listbox\"\n >\n @for (tenant of displayedTenants(); track tenant.id) {\n <div\n axListNavigationItem\n #navItem=\"axListNavigationItem\"\n tabindex=\"0\"\n role=\"option\"\n [attr.aria-selected]=\"isTenantSelected(tenant)\"\n class=\"flex gap-2 items-center p-2 cursor-pointer rounded-md transition-colors outline-none hover:bg-primary-lightest hover:dark:bg-primary-dark\"\n [class.bg-primary-lighter]=\"isTenantSelected(tenant)\"\n [class.dark:bg-primary-darker]=\"isTenantSelected(tenant)\"\n [class.text-primary-on-lighter]=\"isTenantSelected(tenant)\"\n [class.dark:text-primary-on-darker]=\"isTenantSelected(tenant)\"\n [class.ring-2]=\"navItem.isActive()\"\n [class.ring-primary]=\"navItem.isActive()\"\n (click)=\"chooseTenant(tenant)\"\n (keydown)=\"handleListItemKeyDown($event, tenant)\"\n >\n <div class=\"flex flex-1 gap-2 items-center min-w-0\">\n <div\n class=\"shrink-0 flex justify-center items-center rounded-full bg-neutral-100\"\n [class.w-10]=\"simple()\"\n [class.h-10]=\"simple()\"\n [class.w-12]=\"!simple()\"\n [class.h-12]=\"!simple()\"\n >\n <axp-logo\n [logoKey]=\"tenantLogoKey(tenant)\"\n appearance=\"colored\"\n [size]=\"40\"\n [attr.alt]=\"tenant.title\"\n class=\"flex! items-center justify-center text-sm! bg-neutral-200 dark:bg-neutral-800 rounded-full\"\n [class.w-8]=\"simple()\"\n [class.h-8]=\"simple()\"\n [class.w-10]=\"!simple()\"\n [class.h-10]=\"!simple()\"\n ></axp-logo>\n </div>\n <span class=\"flex-1 truncate text-start md:text-base text-sm\">{{ tenant.title }}</span>\n </div>\n </div>\n }\n </div>\n @if (showSearchAndViewAll()) {\n <div class=\"mt-6 w-full flex justify-center\">\n <ax-button\n (onClick)=\"showAllTenants()\"\n class=\"w-full\"\n look=\"outline\"\n [text]=\"\n showAll()\n ? ('@general:actions.view-less.title' | translate | async)\n : ('@general:actions.view-all.title' | translate | async)\n \"\n ></ax-button>\n </div>\n }\n}\n\n<ng-template #tenantItemTemplate let-item>\n <div class=\"flex gap-2 items-center p-2\">\n <div\n class=\"w-10 h-10 flex justify-center items-center rounded-full bg-neutral-100 shrink-0\"\n >\n <axp-logo\n [logoKey]=\"tenantLogoKey(item.data)\"\n appearance=\"colored\"\n [size]=\"32\"\n [attr.alt]=\"item.data.title\"\n class=\"flex! items-center justify-center text-sm! w-8 h-8 bg-neutral-200 rounded-full\"\n ></axp-logo>\n </div>\n <span class=\"text-start md:text-base text-sm\">{{ item.data.title }}</span>\n </div>\n</ng-template>\n" }]
|
|
1293
|
+
}], ctorParameters: () => [], propDecorators: { simple: [{ type: i0.Input, args: [{ isSignal: true, alias: "simple", required: false }] }], tenantChosen: [{ type: i0.Output, args: ["tenantChosen"] }], listNav: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AXListNavigationDirective), { isSignal: true }] }], listItems: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => AXListNavigationItemDirective), { isSignal: true }] }] } });
|
|
1294
|
+
|
|
1295
|
+
class AXPTenantChooserDropdownComponent {
|
|
1296
|
+
constructor() {
|
|
1297
|
+
this.sessionService = inject(AXPSessionService);
|
|
1298
|
+
this.tenants$ = this.sessionService.tenants$;
|
|
1299
|
+
this.tenant = this.sessionService.tenant;
|
|
1300
|
+
this.tenantPopover = viewChild('tenantPopover', ...(ngDevMode ? [{ debugName: "tenantPopover" }] : /* istanbul ignore next */ []));
|
|
1301
|
+
}
|
|
1302
|
+
closePopover() {
|
|
1303
|
+
this.tenantPopover()?.close();
|
|
1304
|
+
}
|
|
1305
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPTenantChooserDropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1306
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPTenantChooserDropdownComponent, isStandalone: true, selector: "axp-tenant-chooser-dropdown", viewQueries: [{ propertyName: "tenantPopover", first: true, predicate: ["tenantPopover"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (tenants$ | async; as tenants) {\n @if (tenants.length > 1) {\n <ax-button\n [text]=\"tenant?.title ?? ''\"\n class=\"ax-sm\"\n look=\"blank\"\n #tenantChooser\n >\n <ax-suffix>\n <ax-icon class=\"fa-solid fa-caret-down\"></ax-icon>\n </ax-suffix>\n </ax-button>\n <ax-popover\n [target]=\"tenantChooser\"\n [openOn]=\"'toggle'\"\n [closeOn]=\"'clickOut'\"\n [adaptivityEnabled]=\"true\"\n #tenantPopover\n >\n <div\n class=\"bg-lightest border overflow-hidden rounded-md shadow-md w-full min-w-64 p-1\"\n >\n <axp-tenant-chooser-list\n [simple]=\"true\"\n (tenantChosen)=\"closePopover()\"\n ></axp-tenant-chooser-list>\n </div>\n </ax-popover>\n } @else {\n <ax-button\n [text]=\"tenant?.title ?? ''\"\n class=\"ax-sm\"\n look=\"blank\"\n ></ax-button>\n }\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i1$1.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "ngmodule", type: AXPopoverModule }, { kind: "component", type: i2$2.AXPopoverComponent, selector: "ax-popover", inputs: ["width", "disablePanelClass", "disabled", "offsetX", "offsetY", "target", "placement", "content", "openOn", "closeOn", "hasBackdrop", "openAfter", "closeAfter", "closeOnScroll", "backdropClass", "panelClass", "adaptivityEnabled"], outputs: ["onOpened", "onClosed"] }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i3$1.AXDecoratorIconComponent, selector: "ax-icon", inputs: ["icon"] }, { kind: "component", type: i3$1.AXDecoratorGenericComponent, selector: "ax-footer, ax-header, ax-content, ax-divider, ax-form-hint, ax-prefix, ax-suffix, ax-text, ax-title, ax-subtitle, ax-placeholder, ax-overlay" }, { kind: "component", type: AXPTenantChooserListComponent, selector: "axp-tenant-chooser-list", inputs: ["simple"], outputs: ["tenantChosen"] }, { kind: "pipe", type: i4$1.AsyncPipe, name: "async" }] }); }
|
|
1307
|
+
}
|
|
1308
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPTenantChooserDropdownComponent, decorators: [{
|
|
1309
|
+
type: Component,
|
|
1310
|
+
args: [{ selector: 'axp-tenant-chooser-dropdown', imports: [CommonModule, AXButtonModule, AXPopoverModule, AXDecoratorModule, AXPTenantChooserListComponent], template: "@if (tenants$ | async; as tenants) {\n @if (tenants.length > 1) {\n <ax-button\n [text]=\"tenant?.title ?? ''\"\n class=\"ax-sm\"\n look=\"blank\"\n #tenantChooser\n >\n <ax-suffix>\n <ax-icon class=\"fa-solid fa-caret-down\"></ax-icon>\n </ax-suffix>\n </ax-button>\n <ax-popover\n [target]=\"tenantChooser\"\n [openOn]=\"'toggle'\"\n [closeOn]=\"'clickOut'\"\n [adaptivityEnabled]=\"true\"\n #tenantPopover\n >\n <div\n class=\"bg-lightest border overflow-hidden rounded-md shadow-md w-full min-w-64 p-1\"\n >\n <axp-tenant-chooser-list\n [simple]=\"true\"\n (tenantChosen)=\"closePopover()\"\n ></axp-tenant-chooser-list>\n </div>\n </ax-popover>\n } @else {\n <ax-button\n [text]=\"tenant?.title ?? ''\"\n class=\"ax-sm\"\n look=\"blank\"\n ></ax-button>\n }\n}\n" }]
|
|
1311
|
+
}], propDecorators: { tenantPopover: [{ type: i0.ViewChild, args: ['tenantPopover', { isSignal: true }] }] } });
|
|
1312
|
+
|
|
1313
|
+
//#region ---- Profile slot menu hook ----
|
|
1314
|
+
const PROFILE_SLOT_MENU_ITEMS_HOOK = 'profile-slot.menu-items';
|
|
1315
|
+
function hasProfileSlotMenuBreakBefore(breakOptions) {
|
|
1316
|
+
return breakOptions?.before === true;
|
|
1317
|
+
}
|
|
1318
|
+
function hasProfileSlotMenuBreakAfter(breakOptions) {
|
|
1319
|
+
return breakOptions?.after === true;
|
|
1320
|
+
}
|
|
1321
|
+
//#endregion
|
|
1322
|
+
|
|
1323
|
+
class AXMAuthProfileSlotComponent {
|
|
1324
|
+
constructor() {
|
|
1325
|
+
//#region ---- Services & Dependencies ----
|
|
1326
|
+
this.router = inject(Router);
|
|
1327
|
+
this.sessionService = inject(AXPSessionService);
|
|
1328
|
+
this.themeService = inject(AXPLayoutThemeService);
|
|
1329
|
+
this.hooks = inject(AXPHookService);
|
|
1330
|
+
this.commandService = inject(AXPCommandService);
|
|
1331
|
+
this.shortcutRegistry = inject(AXPKeyboardShortcutRegistry);
|
|
1332
|
+
this.settingsService = inject(AXPSettingsService);
|
|
1333
|
+
this.destroyRef = inject(DestroyRef);
|
|
1334
|
+
//#endregion
|
|
1335
|
+
//#region ---- State ----
|
|
1336
|
+
this.isDarkMode = this.themeService.isDarkMode;
|
|
1337
|
+
this.user$ = this.sessionService.user$;
|
|
1338
|
+
this.tenant$ = this.sessionService.tenant$;
|
|
1339
|
+
this.isAuthenticated$ = this.sessionService.isAuthenticated$;
|
|
1340
|
+
this.isAuthenticated = toSignal(this.sessionService.isAuthenticated$, { initialValue: false });
|
|
1341
|
+
this.menuItems = signal([], ...(ngDevMode ? [{ debugName: "menuItems" }] : /* istanbul ignore next */ []));
|
|
1342
|
+
//#endregion
|
|
1343
|
+
//#region ---- UI Handlers ----
|
|
1344
|
+
this.appName = () => this.sessionService.application?.name;
|
|
1345
|
+
this.hasProfileSlotMenuBreakBefore = hasProfileSlotMenuBreakBefore;
|
|
1346
|
+
this.hasProfileSlotMenuBreakAfter = hasProfileSlotMenuBreakAfter;
|
|
1347
|
+
}
|
|
1348
|
+
//#endregion
|
|
1349
|
+
//#region ---- Lifecycle Methods ----
|
|
1350
|
+
async ngOnInit() {
|
|
1351
|
+
await this.loadMenuItems();
|
|
1352
|
+
this.bindLockMenuSettingsReload();
|
|
1353
|
+
}
|
|
1354
|
+
menuItemClass(item) {
|
|
1355
|
+
return {
|
|
1356
|
+
'text-danger-500': item.color === 'danger',
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
shortcutKeys(chord) {
|
|
1360
|
+
return chordToKbdItemKeys(chord);
|
|
1361
|
+
}
|
|
1362
|
+
primaryShortcut(item) {
|
|
1363
|
+
return getPrimaryKeyboardShortcutChord(item.shortcuts);
|
|
1364
|
+
}
|
|
1365
|
+
async handleMenuItemClick(item, popover) {
|
|
1366
|
+
popover.close();
|
|
1367
|
+
if (item.command) {
|
|
1368
|
+
await this.executeCommand(item.command);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
handleSignIn() {
|
|
1372
|
+
this.router.navigate(['/auth/login']);
|
|
1373
|
+
}
|
|
1374
|
+
//#endregion
|
|
1375
|
+
//#region ---- Private Methods ----
|
|
1376
|
+
bindLockMenuSettingsReload() {
|
|
1377
|
+
this.settingsService.onChanged.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => {
|
|
1378
|
+
if (!event.keys.includes(AXPAuthSettings.LockWhenIdleEnabled)) {
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
void this.loadMenuItems();
|
|
1382
|
+
});
|
|
1383
|
+
this.settingsService.onLoaded.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
|
1384
|
+
void this.loadMenuItems();
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
async loadMenuItems() {
|
|
1388
|
+
const payload = await this.hooks.runAsync(PROFILE_SLOT_MENU_ITEMS_HOOK, {
|
|
1389
|
+
items: [],
|
|
1390
|
+
});
|
|
1391
|
+
const items = payload.items ?? [];
|
|
1392
|
+
this.menuItems.set(items);
|
|
1393
|
+
this.registerMenuShortcuts(items);
|
|
1394
|
+
}
|
|
1395
|
+
registerMenuShortcuts(items) {
|
|
1396
|
+
const shortcuts = [];
|
|
1397
|
+
for (const item of items) {
|
|
1398
|
+
if (item.visible === false || !item.command || !item.shortcuts?.length) {
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
const command = item.command;
|
|
1402
|
+
for (const shortcut of normalizeKeyboardShortcuts(item.shortcuts)) {
|
|
1403
|
+
for (const chord of shortcut.keys) {
|
|
1404
|
+
shortcuts.push({
|
|
1405
|
+
keys: [chord],
|
|
1406
|
+
title: item.title,
|
|
1407
|
+
group: '@general:keyboard-shortcuts.groups.global',
|
|
1408
|
+
allowInEditableFields: shortcut.allowInEditableFields,
|
|
1409
|
+
directionBehavior: shortcut.directionBehavior,
|
|
1410
|
+
when: () => this.isAuthenticated() && (shortcut.when?.() ?? true),
|
|
1411
|
+
handler: () => {
|
|
1412
|
+
void this.executeCommand(command);
|
|
1413
|
+
},
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
if (shortcuts.length === 0) {
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
this.shortcutRegistry.register({
|
|
1422
|
+
owner: this.destroyRef,
|
|
1423
|
+
id: 'profile-slot-menu',
|
|
1424
|
+
priority: AXPKeyboardShortcutPriority.Global,
|
|
1425
|
+
group: '@general:keyboard-shortcuts.groups.global',
|
|
1426
|
+
when: () => this.isAuthenticated(),
|
|
1427
|
+
shortcuts,
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
async executeCommand(command) {
|
|
1431
|
+
if (this.commandService.exists(command.name)) {
|
|
1432
|
+
await this.commandService.execute(command.name, command.options ?? {});
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMAuthProfileSlotComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1436
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXMAuthProfileSlotComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "@if (isAuthenticated$ | async) {\n <div class=\"size-fit\">\n <div #avatar>\n <axp-user-avatar\n [size]=\"36\"\n [userId]=\"(user$ | async)?.id!\"\n ></axp-user-avatar>\n </div>\n\n <ax-popover [target]=\"avatar\" [openOn]=\"'toggle'\" [closeOn]=\"'clickOut'\" #p>\n <div\n class=\"bg-lightest border border-lightest rounded-md shadow-md w-full min-w-64\"\n >\n <div class=\"flex flex-col items-center p-4 border-b\">\n <axp-user-avatar\n [size]=\"64\"\n [userId]=\"(user$ | async)?.id!\"\n ></axp-user-avatar>\n\n <div class=\"flex flex-col items-center justify-center mt-2\">\n <div class=\"text-base font-bold\">{{ (user$ | async)?.title }}</div>\n <axp-tenant-chooser-dropdown></axp-tenant-chooser-dropdown>\n </div>\n </div>\n <div class=\"axp-profile-slot-menus\">\n <ul>\n @for (item of menuItems(); track item.name ?? item.title) {\n @if (item.visible != false) {\n @if (hasProfileSlotMenuBreakBefore(item.break)) {\n <li class=\"axp-profile-slot-menu-break\" role=\"separator\"></li>\n }\n <li [class]=\"menuItemClass(item)\">\n <a (click)=\"handleMenuItemClick(item, p)\">\n <i [class]=\"item.icon\"></i>\n <span class=\"flex-1\">{{\n item.title | translate | async\n }}</span>\n </a>\n </li>\n @if (hasProfileSlotMenuBreakAfter(item.break)) {\n <li class=\"axp-profile-slot-menu-break\" role=\"separator\"></li>\n }\n }\n }\n </ul>\n </div>\n </div>\n </ax-popover>\n </div>\n} @else {\n <ax-button\n color=\"default\"\n look=\"blank\"\n text=\"Sign Up / Sign In\"\n (click)=\"handleSignIn()\"\n >\n <ax-icon>\n <i class=\"fa-solid fa-user text-slate-400 dark:text-slate-200\"></i>\n </ax-icon>\n </ax-button>\n}\n", styles: ["@layer properties;.axp-profile-slot-menus ul{margin-top:calc(var(--spacing, .25rem) * 2);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-border-default);padding-inline:calc(var(--spacing, .25rem) * 2)}.axp-profile-slot-menus ul:last-child{border-style:var(--tw-border-style);border-width:0px}.axp-profile-slot-menus ul li{display:flex;cursor:pointer;border-radius:var(--radius-md, .375rem)}.axp-profile-slot-menus ul li:last-child{margin-bottom:calc(var(--spacing, .25rem) * 2)}.axp-profile-slot-menus ul li:hover{background-color:rgba(var(--ax-sys-color-dark-surface))}.axp-profile-slot-menus ul li.axp-profile-slot-menu-break{pointer-events:none;margin-inline:calc(var(--spacing, .25rem) * 2);margin-block:var(--spacing, .25rem);height:0;min-height:0;cursor:default;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-border-default);padding:0}.axp-profile-slot-menus ul li.axp-profile-slot-menu-break:hover{background-color:transparent}.axp-profile-slot-menus ul li i{margin-inline-end:calc(var(--spacing, .25rem) * 3);display:grid;height:calc(var(--spacing, .25rem) * 6);width:calc(var(--spacing, .25rem) * 6);place-items:center;font-size:var(--text-xl, 1.25rem);line-height:var(--tw-leading, var(--text-xl--line-height, calc(1.75 / 1.25)))}.axp-profile-slot-menus ul li a{display:flex;height:100%;width:100%;align-items:center;gap:calc(var(--spacing, .25rem) * 2);padding:calc(var(--spacing, .25rem) * 2)}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid}}}\n/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: RouterModule }, { kind: "ngmodule", type:
|
|
1437
|
+
//
|
|
1438
|
+
AXImageModule }, { kind: "ngmodule", type: AXAvatarModule }, { kind: "ngmodule", type: AXPopoverModule }, { kind: "component", type: i2$2.AXPopoverComponent, selector: "ax-popover", inputs: ["width", "disablePanelClass", "disabled", "offsetX", "offsetY", "target", "placement", "content", "openOn", "closeOn", "hasBackdrop", "openAfter", "closeAfter", "closeOnScroll", "backdropClass", "panelClass", "adaptivityEnabled"], outputs: ["onOpened", "onClosed"] }, { kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i1$1.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i3$1.AXDecoratorIconComponent, selector: "ax-icon", inputs: ["icon"] }, { kind: "ngmodule", type:
|
|
1439
|
+
// AXKBDComponent,
|
|
1440
|
+
// AXKBDItemComponent,
|
|
1441
|
+
AXLoadingModule }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "component", type: AXPUserAvatarComponent, selector: "axp-user-avatar", inputs: ["size", "userId"] }, { kind: "component", type:
|
|
1442
|
+
//
|
|
1443
|
+
AXPTenantChooserDropdownComponent, selector: "axp-tenant-chooser-dropdown" }, { kind: "pipe", type: i4$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i2$1.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
1444
|
+
}
|
|
1445
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMAuthProfileSlotComponent, decorators: [{
|
|
1446
|
+
type: Component,
|
|
1447
|
+
args: [{ imports: [
|
|
1448
|
+
CommonModule,
|
|
1449
|
+
RouterModule,
|
|
1450
|
+
//
|
|
1451
|
+
AXImageModule,
|
|
1452
|
+
AXAvatarModule,
|
|
1453
|
+
AXPopoverModule,
|
|
1454
|
+
AXButtonModule,
|
|
1455
|
+
AXDecoratorModule,
|
|
1456
|
+
// AXKBDComponent,
|
|
1457
|
+
// AXKBDItemComponent,
|
|
1458
|
+
AXLoadingModule,
|
|
1459
|
+
AXTranslationModule,
|
|
1460
|
+
AXPUserAvatarComponent,
|
|
1461
|
+
//
|
|
1462
|
+
AXPTenantChooserDropdownComponent,
|
|
1463
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "@if (isAuthenticated$ | async) {\n <div class=\"size-fit\">\n <div #avatar>\n <axp-user-avatar\n [size]=\"36\"\n [userId]=\"(user$ | async)?.id!\"\n ></axp-user-avatar>\n </div>\n\n <ax-popover [target]=\"avatar\" [openOn]=\"'toggle'\" [closeOn]=\"'clickOut'\" #p>\n <div\n class=\"bg-lightest border border-lightest rounded-md shadow-md w-full min-w-64\"\n >\n <div class=\"flex flex-col items-center p-4 border-b\">\n <axp-user-avatar\n [size]=\"64\"\n [userId]=\"(user$ | async)?.id!\"\n ></axp-user-avatar>\n\n <div class=\"flex flex-col items-center justify-center mt-2\">\n <div class=\"text-base font-bold\">{{ (user$ | async)?.title }}</div>\n <axp-tenant-chooser-dropdown></axp-tenant-chooser-dropdown>\n </div>\n </div>\n <div class=\"axp-profile-slot-menus\">\n <ul>\n @for (item of menuItems(); track item.name ?? item.title) {\n @if (item.visible != false) {\n @if (hasProfileSlotMenuBreakBefore(item.break)) {\n <li class=\"axp-profile-slot-menu-break\" role=\"separator\"></li>\n }\n <li [class]=\"menuItemClass(item)\">\n <a (click)=\"handleMenuItemClick(item, p)\">\n <i [class]=\"item.icon\"></i>\n <span class=\"flex-1\">{{\n item.title | translate | async\n }}</span>\n </a>\n </li>\n @if (hasProfileSlotMenuBreakAfter(item.break)) {\n <li class=\"axp-profile-slot-menu-break\" role=\"separator\"></li>\n }\n }\n }\n </ul>\n </div>\n </div>\n </ax-popover>\n </div>\n} @else {\n <ax-button\n color=\"default\"\n look=\"blank\"\n text=\"Sign Up / Sign In\"\n (click)=\"handleSignIn()\"\n >\n <ax-icon>\n <i class=\"fa-solid fa-user text-slate-400 dark:text-slate-200\"></i>\n </ax-icon>\n </ax-button>\n}\n", styles: ["@layer properties;.axp-profile-slot-menus ul{margin-top:calc(var(--spacing, .25rem) * 2);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-border-default);padding-inline:calc(var(--spacing, .25rem) * 2)}.axp-profile-slot-menus ul:last-child{border-style:var(--tw-border-style);border-width:0px}.axp-profile-slot-menus ul li{display:flex;cursor:pointer;border-radius:var(--radius-md, .375rem)}.axp-profile-slot-menus ul li:last-child{margin-bottom:calc(var(--spacing, .25rem) * 2)}.axp-profile-slot-menus ul li:hover{background-color:rgba(var(--ax-sys-color-dark-surface))}.axp-profile-slot-menus ul li.axp-profile-slot-menu-break{pointer-events:none;margin-inline:calc(var(--spacing, .25rem) * 2);margin-block:var(--spacing, .25rem);height:0;min-height:0;cursor:default;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-border-default);padding:0}.axp-profile-slot-menus ul li.axp-profile-slot-menu-break:hover{background-color:transparent}.axp-profile-slot-menus ul li i{margin-inline-end:calc(var(--spacing, .25rem) * 3);display:grid;height:calc(var(--spacing, .25rem) * 6);width:calc(var(--spacing, .25rem) * 6);place-items:center;font-size:var(--text-xl, 1.25rem);line-height:var(--tw-leading, var(--text-xl--line-height, calc(1.75 / 1.25)))}.axp-profile-slot-menus ul li a{display:flex;height:100%;width:100%;align-items:center;gap:calc(var(--spacing, .25rem) * 2);padding:calc(var(--spacing, .25rem) * 2)}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid}}}\n/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */\n"] }]
|
|
1464
|
+
}] });
|
|
1465
|
+
|
|
1466
|
+
class AXMUserProfileCardSlotComponent {
|
|
1467
|
+
constructor() {
|
|
1468
|
+
this.router = inject(Router);
|
|
1469
|
+
this.sessionService = inject(AXPSessionService);
|
|
1470
|
+
this.dataService = inject(AXPDataProvider, { optional: true });
|
|
1471
|
+
this.themeStore = inject(AXPLayoutThemeService);
|
|
1472
|
+
this.deviceService = inject(AXPDeviceService);
|
|
1473
|
+
this.isCompactMode = computed(() => {
|
|
1474
|
+
this.deviceService.screenSize();
|
|
1475
|
+
return this.themeStore.effectiveMenuVerticalMode() === AXPMenuVerticalMode.Compact;
|
|
1476
|
+
}, ...(ngDevMode ? [{ debugName: "isCompactMode" }] : /* istanbul ignore next */ []));
|
|
1477
|
+
this.hasPicture = signal(true, ...(ngDevMode ? [{ debugName: "hasPicture" }] : /* istanbul ignore next */ []));
|
|
1478
|
+
this.avatarText = signal('', ...(ngDevMode ? [{ debugName: "avatarText" }] : /* istanbul ignore next */ []));
|
|
1479
|
+
this.user$ = this.sessionService.user$;
|
|
1480
|
+
this.tenant$ = this.sessionService.tenant$;
|
|
1481
|
+
this.isAuthenticated$ = this.sessionService.isAuthenticated$;
|
|
1482
|
+
this.appName = () => this.sessionService.application?.name;
|
|
1483
|
+
this.getAvatarText = () => {
|
|
1484
|
+
this.user$.pipe(first$1()).subscribe((user) => {
|
|
1485
|
+
const names = user?.title.split(' ');
|
|
1486
|
+
if (names?.length && names?.length > 1) {
|
|
1487
|
+
this.avatarText.set(`${names[0][0].toUpperCase()}${names[names.length - 1][0].toUpperCase()}`);
|
|
1488
|
+
}
|
|
1489
|
+
else
|
|
1490
|
+
return this.avatarText.set('AC');
|
|
1491
|
+
});
|
|
1492
|
+
};
|
|
1493
|
+
this.loadImage();
|
|
1494
|
+
this.getAvatarText();
|
|
1495
|
+
}
|
|
1496
|
+
editProfile() {
|
|
1497
|
+
this.router.navigate([`/${this.appName()}/m/securityManagement/e/profile/0/view`]);
|
|
1498
|
+
}
|
|
1499
|
+
async logOut() {
|
|
1500
|
+
await this.sessionService.signout();
|
|
1501
|
+
}
|
|
1502
|
+
toggleProfileMenu() {
|
|
1503
|
+
this.profilePopover.open();
|
|
1504
|
+
}
|
|
1505
|
+
handleSignIn() {
|
|
1506
|
+
this.router.navigate(['/auth/login']);
|
|
1507
|
+
}
|
|
1508
|
+
onImageError(event) {
|
|
1509
|
+
this.hasPicture.set(false);
|
|
1510
|
+
}
|
|
1511
|
+
async loadImage() {
|
|
1512
|
+
try {
|
|
1513
|
+
const url = this.sessionService.user?.avatar;
|
|
1514
|
+
if (url?.toLowerCase().startsWith('http')) {
|
|
1515
|
+
this.src = url;
|
|
1516
|
+
}
|
|
1517
|
+
else if (this.sessionService.user?.avatar != 'null' && this.dataService) {
|
|
1518
|
+
const blob = await this.dataService.viewFile(url, '');
|
|
1519
|
+
this.src = URL.createObjectURL(blob);
|
|
1520
|
+
}
|
|
1521
|
+
else {
|
|
1522
|
+
//this.src = this.mockerService.avatar();
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
catch (error) {
|
|
1526
|
+
this.hasPicture.set(false);
|
|
1527
|
+
console.error('Failed to load image by ID:', error);
|
|
1528
|
+
this.src = '';
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMUserProfileCardSlotComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1532
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXMUserProfileCardSlotComponent, isStandalone: true, selector: "ng-component", viewQueries: [{ propertyName: "profilePopover", first: true, predicate: ["profilePopover"], descendants: true }], ngImport: i0, template: "@if((isAuthenticated$ | async)) {\n<div class=\"p-3 flex items-center gap-1 border-b border-lighter\" [class.justify-center]=\"isCompactMode()\">\n <div #avatar>\n <axp-user-avatar [size]=\"48\" [userId]=\"(user$ | async)?.id!\"></axp-user-avatar>\n </div>\n @if (!isCompactMode()) {\n <div class=\"flex flex-col\">\n <ax-title class=\"text-sm font-medium px-3\">{{ (user$ | async)?.title }}</ax-title>\n <axp-tenant-chooser-dropdown />\n </div>\n }\n</div>\n}\n", dependencies: [{ kind: "ngmodule", type: RouterModule }, { kind: "ngmodule", type:
|
|
1533
|
+
//
|
|
1534
|
+
AXImageModule }, { kind: "ngmodule", type: AXAvatarModule }, { kind: "ngmodule", type: AXPopoverModule }, { kind: "ngmodule", type: AXButtonModule }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i3$1.AXDecoratorGenericComponent, selector: "ax-footer, ax-header, ax-content, ax-divider, ax-form-hint, ax-prefix, ax-suffix, ax-text, ax-title, ax-subtitle, ax-placeholder, ax-overlay" }, { kind: "ngmodule", type: AXLoadingModule }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "component", type:
|
|
1535
|
+
//
|
|
1536
|
+
AXPTenantChooserDropdownComponent, selector: "axp-tenant-chooser-dropdown" }, { kind: "component", type: AXPUserAvatarComponent, selector: "axp-user-avatar", inputs: ["size", "userId"] }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); }
|
|
1537
|
+
}
|
|
1538
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMUserProfileCardSlotComponent, decorators: [{
|
|
1539
|
+
type: Component,
|
|
1540
|
+
args: [{ imports: [
|
|
1541
|
+
RouterModule,
|
|
1542
|
+
//
|
|
1543
|
+
AXImageModule,
|
|
1544
|
+
AXAvatarModule,
|
|
1545
|
+
AXPopoverModule,
|
|
1546
|
+
AXButtonModule,
|
|
1547
|
+
AXDecoratorModule,
|
|
1548
|
+
AXLoadingModule,
|
|
1549
|
+
AXTranslationModule,
|
|
1550
|
+
//
|
|
1551
|
+
AXPTenantChooserDropdownComponent,
|
|
1552
|
+
AsyncPipe,
|
|
1553
|
+
AXPUserAvatarComponent,
|
|
1554
|
+
], template: "@if((isAuthenticated$ | async)) {\n<div class=\"p-3 flex items-center gap-1 border-b border-lighter\" [class.justify-center]=\"isCompactMode()\">\n <div #avatar>\n <axp-user-avatar [size]=\"48\" [userId]=\"(user$ | async)?.id!\"></axp-user-avatar>\n </div>\n @if (!isCompactMode()) {\n <div class=\"flex flex-col\">\n <ax-title class=\"text-sm font-medium px-3\">{{ (user$ | async)?.title }}</ax-title>\n <axp-tenant-chooser-dropdown />\n </div>\n }\n</div>\n}\n" }]
|
|
1555
|
+
}], ctorParameters: () => [], propDecorators: { profilePopover: [{
|
|
1556
|
+
type: ViewChild,
|
|
1557
|
+
args: ['profilePopover']
|
|
1558
|
+
}] } });
|
|
1559
|
+
|
|
1560
|
+
class AXMUserDeviceSessionsService {
|
|
1561
|
+
}
|
|
1562
|
+
class AXMUserDeviceSessionsServiceImp extends AXMUserDeviceSessionsService {
|
|
1563
|
+
constructor() {
|
|
1564
|
+
super(...arguments);
|
|
1565
|
+
//#region ---- Services & Dependencies ----
|
|
1566
|
+
this.sessionService = inject(AXPSessionService);
|
|
1567
|
+
this.entityService = inject(AXPEntityService);
|
|
1568
|
+
this.deviceSessionsDataAccessor = this.entityService
|
|
1569
|
+
.withEntity(RootConfig$1.module.name, RootConfig$1.entities.deviceSessions.name)
|
|
1570
|
+
.data();
|
|
1571
|
+
this.deviceSessionsService = inject(AXMDeviceSessionsService);
|
|
1572
|
+
this.settingsService = inject(AXPSettingsService);
|
|
1573
|
+
this.toastService = inject(AXToastService);
|
|
1574
|
+
this.translationService = inject(AXTranslationService);
|
|
1575
|
+
}
|
|
1576
|
+
//#endregion
|
|
1577
|
+
async getSessions() {
|
|
1578
|
+
const userId = this.sessionService.user?.id;
|
|
1579
|
+
if (!userId) {
|
|
1580
|
+
return [];
|
|
1581
|
+
}
|
|
1582
|
+
const response = await this.deviceSessionsDataAccessor.query();
|
|
1583
|
+
return response.items.filter((item) => item.userId === userId &&
|
|
1584
|
+
String(item.status).toLowerCase() === 'active' &&
|
|
1585
|
+
!item.isActiveSession);
|
|
1586
|
+
}
|
|
1587
|
+
async getCurrentActiveSessions() {
|
|
1588
|
+
const userId = this.sessionService.user?.id;
|
|
1589
|
+
if (!userId) {
|
|
1590
|
+
throw new Error('No user found in session');
|
|
1591
|
+
}
|
|
1592
|
+
const response = await this.deviceSessionsDataAccessor.query();
|
|
1593
|
+
const userSessions = response.items.filter((item) => item.userId === userId && String(item.status).toLowerCase() === 'active');
|
|
1594
|
+
const activeSession = userSessions.find((item) => item.isActiveSession);
|
|
1595
|
+
if (activeSession) {
|
|
1596
|
+
return activeSession;
|
|
1597
|
+
}
|
|
1598
|
+
const mostRecentSession = orderBy(userSessions, [(session) => new Date(session.lastLoginDate).getTime()], ['desc'])[0];
|
|
1599
|
+
if (!mostRecentSession) {
|
|
1600
|
+
throw new Error('No active session found for current user');
|
|
1601
|
+
}
|
|
1602
|
+
return mostRecentSession;
|
|
1603
|
+
}
|
|
1604
|
+
async terminateSession(sessionId) {
|
|
1605
|
+
await this.deviceSessionsDataAccessor.update(sessionId, { status: 'revoked' });
|
|
1606
|
+
await this.showTerminateSuccessToast();
|
|
1607
|
+
}
|
|
1608
|
+
async terminateAllSession() {
|
|
1609
|
+
await this.deviceSessionsService.terminateAllCurrentUserSessions();
|
|
1610
|
+
await this.showTerminateSuccessToast();
|
|
1611
|
+
}
|
|
1612
|
+
async setInactiveSessionTermination(value) {
|
|
1613
|
+
await this.settingsService
|
|
1614
|
+
.scope(AXPPlatformScope.User)
|
|
1615
|
+
.set(AXPAuthSettings.InactiveSessionTermination, value);
|
|
1616
|
+
}
|
|
1617
|
+
async showTerminateSuccessToast() {
|
|
1618
|
+
const showResult = await this.settingsService.get(AXPCommonSettings.EnableOperationToasts);
|
|
1619
|
+
if (!showResult) {
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
this.toastService.show({
|
|
1623
|
+
color: 'success',
|
|
1624
|
+
title: await this.translationService.translateAsync('workflow.session-terminated'),
|
|
1625
|
+
content: await this.translationService.translateAsync('workflow.session-terminated-body'),
|
|
1626
|
+
closeButton: true,
|
|
1627
|
+
timeOut: 3000,
|
|
1628
|
+
timeOutProgress: true,
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMUserDeviceSessionsServiceImp, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1632
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMUserDeviceSessionsServiceImp }); }
|
|
1633
|
+
}
|
|
1634
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMUserDeviceSessionsServiceImp, decorators: [{
|
|
1635
|
+
type: Injectable
|
|
1636
|
+
}] });
|
|
1637
|
+
|
|
1638
|
+
const canActivateTenantChooser = async (route, state) => {
|
|
1639
|
+
const sessionService = inject(AXPSessionService);
|
|
1640
|
+
const authStrategyRegistry = inject(AXPAuthStrategyRegistryService);
|
|
1641
|
+
const router = inject(Router);
|
|
1642
|
+
const strategyName = sessionService.getSessionData()?.strategy;
|
|
1643
|
+
const strategy = authStrategyRegistry.get(strategyName);
|
|
1644
|
+
if (!sessionService.user?.id) {
|
|
1645
|
+
return router.createUrlTree(['/auth/login']);
|
|
1646
|
+
}
|
|
1647
|
+
const tenants = await firstValueFrom(sessionService.tenants$);
|
|
1648
|
+
if (tenants.length === 0) {
|
|
1649
|
+
await sessionService.signout();
|
|
1650
|
+
window.alert('You do not have any tenant assigned to your account and cannot use the site. Please contact your administrator.');
|
|
1651
|
+
return router.createUrlTree(['/auth/login']);
|
|
1652
|
+
}
|
|
1653
|
+
if (tenants.length === 1) {
|
|
1654
|
+
if (strategy && typeof strategy.updateToken === 'function') {
|
|
1655
|
+
const updateContext = {
|
|
1656
|
+
user: sessionService.user,
|
|
1657
|
+
tenant: tenants[0],
|
|
1658
|
+
};
|
|
1659
|
+
await sessionService.updateToken(updateContext);
|
|
1660
|
+
return router.createUrlTree(['/auth/account/app-chooser']);
|
|
1661
|
+
}
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1664
|
+
return true;
|
|
1665
|
+
};
|
|
1666
|
+
|
|
1667
|
+
const canActivateAppChooser = async (route, state) => {
|
|
1668
|
+
const sessionService = inject(AXPSessionService);
|
|
1669
|
+
const authStrategyRegistry = inject(AXPAuthStrategyRegistryService);
|
|
1670
|
+
const homePageService = inject(AXPHomePageService);
|
|
1671
|
+
const router = inject(Router);
|
|
1672
|
+
const strategyName = sessionService.getSessionData()?.strategy;
|
|
1673
|
+
const strategy = authStrategyRegistry.get(strategyName);
|
|
1674
|
+
const apps = await firstValueFrom(sessionService.applications$);
|
|
1675
|
+
if (apps.length === 0) {
|
|
1676
|
+
await router.navigate(['/auth/login']);
|
|
1677
|
+
return false;
|
|
1678
|
+
}
|
|
1679
|
+
if (apps.length === 1) {
|
|
1680
|
+
const tenantId = sessionService.tenant?.id;
|
|
1681
|
+
if (!tenantId) {
|
|
1682
|
+
throw new Error('Tenant ID is missing. Please contact support.');
|
|
1683
|
+
}
|
|
1684
|
+
if (strategy && typeof strategy.updateToken === 'function') {
|
|
1685
|
+
const updateContext = {
|
|
1686
|
+
user: sessionService.user,
|
|
1687
|
+
tenant: sessionService.tenant,
|
|
1688
|
+
application: apps[0],
|
|
1689
|
+
};
|
|
1690
|
+
await sessionService.updateToken(updateContext);
|
|
1691
|
+
return router.createUrlTree([homePageService.getCurrent().path]);
|
|
1692
|
+
}
|
|
1693
|
+
return false;
|
|
1694
|
+
}
|
|
1695
|
+
return true;
|
|
1696
|
+
};
|
|
1697
|
+
|
|
1698
|
+
function routesFacory() {
|
|
1699
|
+
let routes = [];
|
|
1700
|
+
routes = [
|
|
1701
|
+
{
|
|
1702
|
+
path: 'tenant-chooser',
|
|
1703
|
+
loadComponent: () => import('./acorex-platform-module-auth-client-tenant-chooser.component-ihsqCJKN.mjs').then((c) => c.AXPAuthTenantChooserComponent),
|
|
1704
|
+
canActivate: [AXPAuthGuard, canActivateTenantChooser],
|
|
1705
|
+
},
|
|
1706
|
+
{
|
|
1707
|
+
path: 'app-chooser',
|
|
1708
|
+
loadComponent: () => import('./acorex-platform-module-auth-client-app-chooser.component-Cm2ZagaI.mjs').then((c) => c.AXPAuthAppChooserComponent),
|
|
1709
|
+
canActivate: [AXPAuthGuard, canActivateAppChooser],
|
|
1710
|
+
},
|
|
1711
|
+
];
|
|
1712
|
+
return routes;
|
|
1713
|
+
}
|
|
1714
|
+
class AXPAccountModule {
|
|
1715
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPAccountModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
1716
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: AXPAccountModule, imports: [RouterModule, i1$3.AXPComponentSlotModule] }); }
|
|
1717
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPAccountModule, providers: [
|
|
1718
|
+
{
|
|
1719
|
+
provide: ROUTES,
|
|
1720
|
+
multi: true,
|
|
1721
|
+
useFactory: routesFacory,
|
|
1722
|
+
},
|
|
1723
|
+
{
|
|
1724
|
+
provide: AXMUserDeviceSessionsService,
|
|
1725
|
+
useClass: AXMUserDeviceSessionsServiceImp,
|
|
1726
|
+
},
|
|
1727
|
+
], imports: [RouterModule,
|
|
1728
|
+
//
|
|
1729
|
+
AXPComponentSlotModule.forChild({
|
|
1730
|
+
'root-side-menu-header': [
|
|
1731
|
+
{
|
|
1732
|
+
priority: 1,
|
|
1733
|
+
name: 'user-card',
|
|
1734
|
+
component: AXMUserProfileCardSlotComponent,
|
|
1735
|
+
},
|
|
1736
|
+
],
|
|
1737
|
+
'root-header-end': [
|
|
1738
|
+
{
|
|
1739
|
+
priority: 98,
|
|
1740
|
+
name: 'app-choser',
|
|
1741
|
+
component: AXMAppChooserSlotComponent,
|
|
1742
|
+
},
|
|
1743
|
+
{
|
|
1744
|
+
priority: 99,
|
|
1745
|
+
name: 'profile',
|
|
1746
|
+
component: AXMAuthProfileSlotComponent,
|
|
1747
|
+
},
|
|
1748
|
+
],
|
|
1749
|
+
})] }); }
|
|
1750
|
+
}
|
|
1751
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPAccountModule, decorators: [{
|
|
1752
|
+
type: NgModule,
|
|
1753
|
+
args: [{
|
|
1754
|
+
declarations: [],
|
|
1755
|
+
imports: [
|
|
1756
|
+
RouterModule,
|
|
1757
|
+
//
|
|
1758
|
+
AXPComponentSlotModule.forChild({
|
|
1759
|
+
'root-side-menu-header': [
|
|
1760
|
+
{
|
|
1761
|
+
priority: 1,
|
|
1762
|
+
name: 'user-card',
|
|
1763
|
+
component: AXMUserProfileCardSlotComponent,
|
|
1764
|
+
},
|
|
1765
|
+
],
|
|
1766
|
+
'root-header-end': [
|
|
1767
|
+
{
|
|
1768
|
+
priority: 98,
|
|
1769
|
+
name: 'app-choser',
|
|
1770
|
+
component: AXMAppChooserSlotComponent,
|
|
1771
|
+
},
|
|
1772
|
+
{
|
|
1773
|
+
priority: 99,
|
|
1774
|
+
name: 'profile',
|
|
1775
|
+
component: AXMAuthProfileSlotComponent,
|
|
1776
|
+
},
|
|
1777
|
+
],
|
|
1778
|
+
}),
|
|
1779
|
+
],
|
|
1780
|
+
exports: [],
|
|
1781
|
+
providers: [
|
|
1782
|
+
{
|
|
1783
|
+
provide: ROUTES,
|
|
1784
|
+
multi: true,
|
|
1785
|
+
useFactory: routesFacory,
|
|
1786
|
+
},
|
|
1787
|
+
{
|
|
1788
|
+
provide: AXMUserDeviceSessionsService,
|
|
1789
|
+
useClass: AXMUserDeviceSessionsServiceImp,
|
|
1790
|
+
},
|
|
1791
|
+
],
|
|
1792
|
+
}]
|
|
1793
|
+
}] });
|
|
1794
|
+
|
|
1795
|
+
var account_module = /*#__PURE__*/Object.freeze({
|
|
1796
|
+
__proto__: null,
|
|
1797
|
+
AXPAccountModule: AXPAccountModule
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
//#region ---- Idle lock service ----
|
|
1801
|
+
const IDLE_CHECK_INTERVAL_MS = 1_000;
|
|
1802
|
+
const MOUSE_MOVE_THROTTLE_MS = 1_000;
|
|
1803
|
+
const DEFAULT_LOCK_WHEN_IDLE_MINUTES = 15;
|
|
1804
|
+
const SECURITY_MANAGEMENT_MODULE = 'SecurityManagement';
|
|
1805
|
+
const SECURITY_MANAGEMENT_USER_ENTITY = 'User';
|
|
1806
|
+
const LOCK_WHEN_IDLE_SETTING_KEYS = [AXPAuthSettings.LockWhenIdleEnabled, AXPAuthSettings.LockWhenIdleMinutes];
|
|
1807
|
+
/**
|
|
1808
|
+
* Tracks user inactivity and locks the session overlay when idle threshold is exceeded.
|
|
1809
|
+
* Requires the Lock When Idle setting to be enabled; unlock uses PIN when configured, otherwise password.
|
|
1810
|
+
* Locked state is stored on {@link AXPSessionService.SESSION_KEY} and survives page refresh and browser restart.
|
|
1811
|
+
*/
|
|
1812
|
+
class AXPIdleLockService {
|
|
1813
|
+
//#endregion
|
|
1814
|
+
//#region ---- Constructor ----
|
|
1815
|
+
constructor() {
|
|
1816
|
+
//#region ---- State ----
|
|
1817
|
+
this.locked = signal(false, ...(ngDevMode ? [{ debugName: "locked" }] : /* istanbul ignore next */ []));
|
|
1818
|
+
this.idleTimeoutMs = signal(DEFAULT_LOCK_WHEN_IDLE_MINUTES * 60_000, ...(ngDevMode ? [{ debugName: "idleTimeoutMs" }] : /* istanbul ignore next */ []));
|
|
1819
|
+
this.enabled = signal(false, ...(ngDevMode ? [{ debugName: "enabled" }] : /* istanbul ignore next */ []));
|
|
1820
|
+
/** Whether manual or idle lock is available (Lock When Idle setting is enabled). */
|
|
1821
|
+
this.canLock = this.enabled.asReadonly();
|
|
1822
|
+
this.storedPinCode = signal('', ...(ngDevMode ? [{ debugName: "storedPinCode" }] : /* istanbul ignore next */ []));
|
|
1823
|
+
this.lastActivityAt = Date.now();
|
|
1824
|
+
this.lastMouseMoveAt = 0;
|
|
1825
|
+
this.idleCheckTimerId = null;
|
|
1826
|
+
this.overlayRef = null;
|
|
1827
|
+
this.overlayOpening = false;
|
|
1828
|
+
this.enableTask = Promise.resolve();
|
|
1829
|
+
//#endregion
|
|
1830
|
+
//#region ---- Services & Dependencies ----
|
|
1831
|
+
this.sessionService = inject(AXPSessionService);
|
|
1832
|
+
this.settingsService = inject(AXPSettingsService);
|
|
1833
|
+
this.entityService = inject(AXPEntityService);
|
|
1834
|
+
this.overlayService = inject(AXOverlayService);
|
|
1835
|
+
this.platform = inject(AXPlatform);
|
|
1836
|
+
this.destroyRef = inject(DestroyRef);
|
|
1837
|
+
this.injector = inject(Injector);
|
|
1838
|
+
//#region ---- Activity listeners ----
|
|
1839
|
+
this.onActivity = () => {
|
|
1840
|
+
this.recordActivity();
|
|
1841
|
+
};
|
|
1842
|
+
this.onMouseMove = () => {
|
|
1843
|
+
const now = Date.now();
|
|
1844
|
+
if (now - this.lastMouseMoveAt < MOUSE_MOVE_THROTTLE_MS) {
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
this.lastMouseMoveAt = now;
|
|
1848
|
+
this.recordActivity();
|
|
1849
|
+
};
|
|
1850
|
+
this.sessionService.status$
|
|
1851
|
+
.pipe(filter((status) => status === AXPSessionStatus.Authenticated ||
|
|
1852
|
+
status === AXPSessionStatus.Authorized ||
|
|
1853
|
+
status === AXPSessionStatus.SignedOut ||
|
|
1854
|
+
status === AXPSessionStatus.Expired), takeUntilDestroyed(this.destroyRef))
|
|
1855
|
+
.subscribe((status) => {
|
|
1856
|
+
const isActiveSession = status === AXPSessionStatus.Authenticated || status === AXPSessionStatus.Authorized;
|
|
1857
|
+
if (isActiveSession) {
|
|
1858
|
+
void this.scheduleEnable();
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
void this.handleInactiveSession(status);
|
|
1862
|
+
});
|
|
1863
|
+
afterNextRender(() => {
|
|
1864
|
+
if (this.sessionService.isLocked()) {
|
|
1865
|
+
void this.scheduleEnable();
|
|
1866
|
+
}
|
|
1867
|
+
}, { injector: this.injector });
|
|
1868
|
+
this.settingsService.onChanged.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => {
|
|
1869
|
+
if (!LOCK_WHEN_IDLE_SETTING_KEYS.some((key) => event.keys.includes(key))) {
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
void this.applyLockWhenIdleSettingChange();
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
//#endregion
|
|
1876
|
+
//#region ---- Public Methods ----
|
|
1877
|
+
hasPinCode() {
|
|
1878
|
+
return !isEmpty(this.storedPinCode());
|
|
1879
|
+
}
|
|
1880
|
+
verifyPin(pin) {
|
|
1881
|
+
const expected = this.storedPinCode();
|
|
1882
|
+
return !isEmpty(expected) && pin === expected;
|
|
1883
|
+
}
|
|
1884
|
+
async verifyPassword(password) {
|
|
1885
|
+
const userId = this.sessionService.user?.id;
|
|
1886
|
+
if (isNil(userId) || isEmpty(password)) {
|
|
1887
|
+
return false;
|
|
1888
|
+
}
|
|
1889
|
+
try {
|
|
1890
|
+
const user = await this.entityService
|
|
1891
|
+
.withEntity(SECURITY_MANAGEMENT_MODULE, SECURITY_MANAGEMENT_USER_ENTITY)
|
|
1892
|
+
.data()
|
|
1893
|
+
.byKey(userId);
|
|
1894
|
+
return await verifyPassword(password, get(user, 'password'));
|
|
1895
|
+
}
|
|
1896
|
+
catch (error) {
|
|
1897
|
+
console.error('Failed to verify password for session unlock', error);
|
|
1898
|
+
return false;
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
unlock() {
|
|
1902
|
+
this.locked.set(false);
|
|
1903
|
+
this.sessionService.setLocked(false);
|
|
1904
|
+
this.closeLockOverlay();
|
|
1905
|
+
this.lastActivityAt = Date.now();
|
|
1906
|
+
}
|
|
1907
|
+
/** Locks the session overlay when Lock When Idle is enabled. */
|
|
1908
|
+
lockScreen() {
|
|
1909
|
+
this.lock();
|
|
1910
|
+
}
|
|
1911
|
+
async refreshPinState() {
|
|
1912
|
+
if (!this.isOperational()) {
|
|
1913
|
+
this.storedPinCode.set('');
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
const userId = this.sessionService.user?.id;
|
|
1917
|
+
if (isNil(userId)) {
|
|
1918
|
+
this.storedPinCode.set('');
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
try {
|
|
1922
|
+
const user = await this.entityService
|
|
1923
|
+
.withEntity(SECURITY_MANAGEMENT_MODULE, SECURITY_MANAGEMENT_USER_ENTITY)
|
|
1924
|
+
.data()
|
|
1925
|
+
.byKey(userId);
|
|
1926
|
+
const pinCode = defaultTo(get(user, 'pinCode'), '');
|
|
1927
|
+
this.storedPinCode.set(pinCode);
|
|
1928
|
+
}
|
|
1929
|
+
catch (error) {
|
|
1930
|
+
console.error('Failed to load security PIN for idle lock', error);
|
|
1931
|
+
this.storedPinCode.set('');
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
/** Refreshes lock settings and PIN state for menu/commands. */
|
|
1935
|
+
async refreshLockState() {
|
|
1936
|
+
await this.refreshConfiguration();
|
|
1937
|
+
if (!this.isOperational()) {
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
await this.refreshPinState();
|
|
1941
|
+
}
|
|
1942
|
+
/** Restores the lock overlay when the persisted session is locked. */
|
|
1943
|
+
restoreFromPersistedSession() {
|
|
1944
|
+
if (!this.sessionService.isLocked()) {
|
|
1945
|
+
return Promise.resolve();
|
|
1946
|
+
}
|
|
1947
|
+
return this.scheduleEnable();
|
|
1948
|
+
}
|
|
1949
|
+
/** Re-evaluates idle lock after the user sets or changes their security PIN. */
|
|
1950
|
+
async notifyPinChanged() {
|
|
1951
|
+
await this.refreshConfiguration();
|
|
1952
|
+
if (!this.isOperational()) {
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
const status = await firstValueFrom(this.sessionService.status$);
|
|
1956
|
+
const isActiveSession = status === AXPSessionStatus.Authenticated || status === AXPSessionStatus.Authorized;
|
|
1957
|
+
if (isActiveSession) {
|
|
1958
|
+
await this.scheduleEnable();
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
//#endregion
|
|
1962
|
+
//#region ---- Private Methods ----
|
|
1963
|
+
scheduleEnable() {
|
|
1964
|
+
this.enableTask = this.enableTask.then(() => this.runEnable());
|
|
1965
|
+
return this.enableTask;
|
|
1966
|
+
}
|
|
1967
|
+
async runEnable() {
|
|
1968
|
+
await this.refreshConfiguration();
|
|
1969
|
+
if (this.sessionService.isLocked()) {
|
|
1970
|
+
await this.refreshPinState();
|
|
1971
|
+
this.restoreLockedSession();
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
if (!this.enabled()) {
|
|
1975
|
+
this.disable(false);
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
await this.refreshPinState();
|
|
1979
|
+
this.resetUnlockedRuntimeState();
|
|
1980
|
+
this.attachActivityListeners();
|
|
1981
|
+
this.startIdleCheck();
|
|
1982
|
+
}
|
|
1983
|
+
/** Idle lock is active when enabled, or when a persisted lock must be restored/unlocked. */
|
|
1984
|
+
isOperational() {
|
|
1985
|
+
return this.enabled() || this.sessionService.isLocked();
|
|
1986
|
+
}
|
|
1987
|
+
restoreLockedSession() {
|
|
1988
|
+
this.locked.set(true);
|
|
1989
|
+
if (!this.overlayRef && !this.overlayOpening) {
|
|
1990
|
+
void this.openLockOverlay();
|
|
1991
|
+
}
|
|
1992
|
+
if (this.enabled()) {
|
|
1993
|
+
this.attachActivityListeners();
|
|
1994
|
+
this.startIdleCheck();
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
async handleInactiveSession(status) {
|
|
1998
|
+
if (status !== AXPSessionStatus.SignedOut && status !== AXPSessionStatus.Expired) {
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
const isLoading = await firstValueFrom(this.sessionService.isLoading$);
|
|
2002
|
+
if (isLoading) {
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
this.disable(true);
|
|
2006
|
+
}
|
|
2007
|
+
disable(clearPersistedLock = false) {
|
|
2008
|
+
const preserveLockedSession = this.sessionService.isLocked() && !clearPersistedLock;
|
|
2009
|
+
this.enabled.set(false);
|
|
2010
|
+
if (!preserveLockedSession) {
|
|
2011
|
+
this.locked.set(false);
|
|
2012
|
+
this.closeLockOverlay();
|
|
2013
|
+
}
|
|
2014
|
+
if (clearPersistedLock) {
|
|
2015
|
+
this.sessionService.setLocked(false);
|
|
2016
|
+
}
|
|
2017
|
+
this.detachActivityListeners();
|
|
2018
|
+
this.stopIdleCheck();
|
|
2019
|
+
}
|
|
2020
|
+
async refreshConfiguration() {
|
|
2021
|
+
const lockWhenIdleEnabled = await this.settingsService.get(AXPAuthSettings.LockWhenIdleEnabled);
|
|
2022
|
+
const minutes = await this.settingsService.get(AXPAuthSettings.LockWhenIdleMinutes);
|
|
2023
|
+
const resolvedMinutes = typeof minutes === 'number' && minutes > 0 ? minutes : DEFAULT_LOCK_WHEN_IDLE_MINUTES;
|
|
2024
|
+
this.enabled.set(lockWhenIdleEnabled === true);
|
|
2025
|
+
this.idleTimeoutMs.set(resolvedMinutes * 60_000);
|
|
2026
|
+
}
|
|
2027
|
+
async applyLockWhenIdleSettingChange() {
|
|
2028
|
+
const status = await firstValueFrom(this.sessionService.status$);
|
|
2029
|
+
const isActiveSession = status === AXPSessionStatus.Authenticated || status === AXPSessionStatus.Authorized;
|
|
2030
|
+
if (!isActiveSession) {
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
await this.scheduleEnable();
|
|
2034
|
+
}
|
|
2035
|
+
recordActivity() {
|
|
2036
|
+
if (!this.enabled() || this.locked()) {
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
this.lastActivityAt = Date.now();
|
|
2040
|
+
}
|
|
2041
|
+
lock() {
|
|
2042
|
+
if (!this.enabled() || this.locked()) {
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
this.locked.set(true);
|
|
2046
|
+
this.sessionService.setLocked(true);
|
|
2047
|
+
void this.openLockOverlay();
|
|
2048
|
+
}
|
|
2049
|
+
openLockOverlay() {
|
|
2050
|
+
if (this.overlayRef || this.overlayOpening || !this.shouldShowLockOverlay()) {
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
afterNextRender(() => {
|
|
2054
|
+
void this.createLockOverlay();
|
|
2055
|
+
}, { injector: this.injector });
|
|
2056
|
+
}
|
|
2057
|
+
shouldShowLockOverlay() {
|
|
2058
|
+
return this.locked() || this.sessionService.isLocked();
|
|
2059
|
+
}
|
|
2060
|
+
async createLockOverlay() {
|
|
2061
|
+
if (this.overlayRef || this.overlayOpening || !this.shouldShowLockOverlay()) {
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
this.overlayOpening = true;
|
|
2065
|
+
this.locked.set(true);
|
|
2066
|
+
try {
|
|
2067
|
+
const { AXMSessionLockOverlayComponent: overlayComponent } = await import('./acorex-platform-module-auth-client-session-lock-overlay.component-DqXGuIgN.mjs');
|
|
2068
|
+
const panelClasses = ['axm-session-lock-overlay-shell'];
|
|
2069
|
+
if (this.platform.isDark()) {
|
|
2070
|
+
panelClasses.push('ax-dark');
|
|
2071
|
+
}
|
|
2072
|
+
const ref = await this.overlayService.create(overlayComponent, {
|
|
2073
|
+
centered: false,
|
|
2074
|
+
width: '100%',
|
|
2075
|
+
panelClass: panelClasses,
|
|
2076
|
+
backdrop: {
|
|
2077
|
+
enabled: false,
|
|
2078
|
+
background: false,
|
|
2079
|
+
closeOnClick: false,
|
|
2080
|
+
},
|
|
2081
|
+
onDispose: () => {
|
|
2082
|
+
this.overlayRef = null;
|
|
2083
|
+
},
|
|
2084
|
+
});
|
|
2085
|
+
if (!this.shouldShowLockOverlay()) {
|
|
2086
|
+
ref.dispose();
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
this.locked.set(true);
|
|
2090
|
+
this.overlayRef = ref;
|
|
2091
|
+
}
|
|
2092
|
+
catch (error) {
|
|
2093
|
+
console.error('Failed to open session lock overlay', error);
|
|
2094
|
+
if (this.shouldShowLockOverlay()) {
|
|
2095
|
+
window.setTimeout(() => this.openLockOverlay(), 250);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
finally {
|
|
2099
|
+
this.overlayOpening = false;
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
closeLockOverlay() {
|
|
2103
|
+
if (this.overlayRef) {
|
|
2104
|
+
this.overlayRef.dispose();
|
|
2105
|
+
this.overlayRef = null;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
startIdleCheck() {
|
|
2109
|
+
this.stopIdleCheck();
|
|
2110
|
+
this.idleCheckTimerId = setInterval(() => {
|
|
2111
|
+
if (!this.enabled() || this.locked()) {
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
if (Date.now() - this.lastActivityAt >= this.idleTimeoutMs()) {
|
|
2115
|
+
this.lock();
|
|
2116
|
+
}
|
|
2117
|
+
}, IDLE_CHECK_INTERVAL_MS);
|
|
2118
|
+
}
|
|
2119
|
+
stopIdleCheck() {
|
|
2120
|
+
if (!isNil(this.idleCheckTimerId)) {
|
|
2121
|
+
clearInterval(this.idleCheckTimerId);
|
|
2122
|
+
this.idleCheckTimerId = null;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
resetUnlockedRuntimeState() {
|
|
2126
|
+
this.locked.set(false);
|
|
2127
|
+
this.closeLockOverlay();
|
|
2128
|
+
this.lastActivityAt = Date.now();
|
|
2129
|
+
}
|
|
2130
|
+
attachActivityListeners() {
|
|
2131
|
+
this.detachActivityListeners();
|
|
2132
|
+
const events = ['mousedown', 'keydown', 'wheel', 'touchstart', 'scroll', 'click'];
|
|
2133
|
+
events.forEach((eventName) => document.addEventListener(eventName, this.onActivity, true));
|
|
2134
|
+
document.addEventListener('mousemove', this.onMouseMove, true);
|
|
2135
|
+
}
|
|
2136
|
+
detachActivityListeners() {
|
|
2137
|
+
const events = ['mousedown', 'keydown', 'wheel', 'touchstart', 'scroll', 'click'];
|
|
2138
|
+
events.forEach((eventName) => document.removeEventListener(eventName, this.onActivity, true));
|
|
2139
|
+
document.removeEventListener('mousemove', this.onMouseMove, true);
|
|
2140
|
+
}
|
|
2141
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPIdleLockService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2142
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPIdleLockService, providedIn: 'root' }); }
|
|
2143
|
+
}
|
|
2144
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPIdleLockService, decorators: [{
|
|
2145
|
+
type: Injectable,
|
|
2146
|
+
args: [{ providedIn: 'root' }]
|
|
2147
|
+
}], ctorParameters: () => [] });
|
|
2148
|
+
|
|
2149
|
+
//#region ---- Entity logo sync ----
|
|
2150
|
+
/** Registers tenant/application logo providers when the session is authorized. */
|
|
2151
|
+
class AXPLogoEntitySyncService {
|
|
2152
|
+
constructor() {
|
|
2153
|
+
this.session = inject(AXPSessionService);
|
|
2154
|
+
this.logoRegistration = inject(AXPEntityLogoRegistrationService);
|
|
2155
|
+
combineLatest([this.session.isAuthorized$, this.session.tenant$])
|
|
2156
|
+
.pipe(filter(([authorized]) => authorized), switchMap(() => from(this.syncEntityLogos())))
|
|
2157
|
+
.subscribe();
|
|
2158
|
+
}
|
|
2159
|
+
async syncEntityLogos() {
|
|
2160
|
+
const tenants = await firstValueFrom(this.session.tenants$);
|
|
2161
|
+
this.logoRegistration.registerTenants(tenants.map((tenant) => ({
|
|
2162
|
+
id: tenant.id,
|
|
2163
|
+
title: tenant.title,
|
|
2164
|
+
logo: tenant.logo,
|
|
2165
|
+
})));
|
|
2166
|
+
if (!this.session.tenant?.id) {
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
const applications = await firstValueFrom(this.session.applications$);
|
|
2170
|
+
this.logoRegistration.registerApplications(applications.map((application) => ({
|
|
2171
|
+
id: application.id,
|
|
2172
|
+
title: application.title,
|
|
2173
|
+
logoUrl: application.logoUrl,
|
|
2174
|
+
})));
|
|
2175
|
+
}
|
|
2176
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLogoEntitySyncService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2177
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLogoEntitySyncService, providedIn: 'root' }); }
|
|
2178
|
+
}
|
|
2179
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLogoEntitySyncService, decorators: [{
|
|
2180
|
+
type: Injectable,
|
|
2181
|
+
args: [{ providedIn: 'root' }]
|
|
2182
|
+
}], ctorParameters: () => [] });
|
|
2183
|
+
|
|
2184
|
+
//#region ---- Profile page component keys ----
|
|
2185
|
+
/** Full-tab activity heatmap on the profile Activity group. */
|
|
2186
|
+
const PROFILE_ACTIVITY_CHART_COMPONENT_KEY = 'auth.profile-activity-chart';
|
|
2187
|
+
/** Danger-zone block for the delete-account profile section. */
|
|
2188
|
+
const PROFILE_DELETE_ACCOUNT_DANGER_ZONE_COMPONENT_KEY = 'auth.profile-delete-account-danger-zone';
|
|
2189
|
+
//#endregion
|
|
2190
|
+
|
|
2191
|
+
//#region ---- Helpers ----
|
|
2192
|
+
const comingSoon = (messageKey) => ({
|
|
2193
|
+
name: AXMAuthCommandKeys.Profile.ComingSoon,
|
|
2194
|
+
options: { messageKey },
|
|
2195
|
+
});
|
|
2196
|
+
//#endregion
|
|
2197
|
+
//#region ---- Core profile groups ----
|
|
2198
|
+
/** Registers built-in profile page tab groups. */
|
|
2199
|
+
const CoreProfileGroupsHookProvider = {
|
|
2200
|
+
key: PROFILE_PAGE_GROUPS_HOOK,
|
|
2201
|
+
priority: 0,
|
|
2202
|
+
execute: (payload) => ({
|
|
2203
|
+
...payload,
|
|
2204
|
+
groups: [
|
|
2205
|
+
...payload.groups,
|
|
2206
|
+
{
|
|
2207
|
+
id: 'account',
|
|
2208
|
+
title: '@auth:profile-page.groups.account.title',
|
|
2209
|
+
icon: 'far fa-user',
|
|
2210
|
+
priority: 10,
|
|
2211
|
+
},
|
|
2212
|
+
{
|
|
2213
|
+
id: 'security',
|
|
2214
|
+
title: '@auth:profile-page.groups.security.title',
|
|
2215
|
+
icon: 'far fa-shield-halved',
|
|
2216
|
+
priority: 20,
|
|
2217
|
+
},
|
|
2218
|
+
{
|
|
2219
|
+
id: 'requests',
|
|
2220
|
+
title: '@auth:profile-page.groups.requests.title',
|
|
2221
|
+
icon: 'far fa-inbox',
|
|
2222
|
+
priority: 30,
|
|
2223
|
+
},
|
|
2224
|
+
{
|
|
2225
|
+
id: 'activity',
|
|
2226
|
+
title: '@auth:profile-page.groups.activity.title',
|
|
2227
|
+
icon: 'far fa-clock-rotate-left',
|
|
2228
|
+
priority: 40,
|
|
2229
|
+
content: {
|
|
2230
|
+
mode: 'component',
|
|
2231
|
+
componentKey: PROFILE_ACTIVITY_CHART_COMPONENT_KEY,
|
|
2232
|
+
},
|
|
2233
|
+
},
|
|
2234
|
+
],
|
|
2235
|
+
}),
|
|
2236
|
+
};
|
|
2237
|
+
//#endregion
|
|
2238
|
+
//#region ---- Core profile sections ----
|
|
2239
|
+
/** Registers built-in profile hub section cards and actions. */
|
|
2240
|
+
const CoreProfileSectionsHookProvider = {
|
|
2241
|
+
key: PROFILE_PAGE_SECTIONS_HOOK,
|
|
2242
|
+
priority: 0,
|
|
2243
|
+
execute: (payload) => ({
|
|
2244
|
+
...payload,
|
|
2245
|
+
sections: [
|
|
2246
|
+
...payload.sections,
|
|
2247
|
+
{
|
|
2248
|
+
id: 'basic-info',
|
|
2249
|
+
groupId: 'account',
|
|
2250
|
+
title: '@auth:profile-page.sections.basic-info.title',
|
|
2251
|
+
description: '@auth:profile-page.sections.basic-info.description',
|
|
2252
|
+
icon: 'far fa-address-card',
|
|
2253
|
+
priority: 10,
|
|
2254
|
+
layout: { colSpan: 6 },
|
|
2255
|
+
actions: [
|
|
2256
|
+
{
|
|
2257
|
+
name: 'edit-basic-info',
|
|
2258
|
+
title: '@auth:profile-page.sections.basic-info.actions.edit',
|
|
2259
|
+
icon: 'far fa-pen',
|
|
2260
|
+
command: { name: AXMAuthCommandKeys.Profile.EditAccountInfo },
|
|
2261
|
+
},
|
|
2262
|
+
],
|
|
2263
|
+
},
|
|
2264
|
+
{
|
|
2265
|
+
id: 'profile-photo',
|
|
2266
|
+
groupId: 'account',
|
|
2267
|
+
title: '@auth:profile-page.sections.profile-photo.title',
|
|
2268
|
+
description: '@auth:profile-page.sections.profile-photo.description',
|
|
2269
|
+
icon: 'far fa-camera',
|
|
2270
|
+
priority: 15,
|
|
2271
|
+
layout: { colSpan: 6 },
|
|
2272
|
+
actions: [
|
|
2273
|
+
{
|
|
2274
|
+
name: 'change-profile-photo',
|
|
2275
|
+
title: '@auth:profile-page.sections.profile-photo.actions.manage',
|
|
2276
|
+
icon: 'far fa-pen',
|
|
2277
|
+
command: { name: AXMAuthCommandKeys.Profile.ChangePhoto, options: { mode: 'manage' } },
|
|
2278
|
+
},
|
|
2279
|
+
],
|
|
2280
|
+
},
|
|
2281
|
+
{
|
|
2282
|
+
id: 'change-password',
|
|
2283
|
+
groupId: 'security',
|
|
2284
|
+
title: '@auth:profile-page.sections.change-password.title',
|
|
2285
|
+
description: '@auth:profile-page.sections.change-password.description',
|
|
2286
|
+
icon: 'far fa-key',
|
|
2287
|
+
priority: 10,
|
|
2288
|
+
layout: { colSpan: 6 },
|
|
2289
|
+
actions: [
|
|
2290
|
+
{
|
|
2291
|
+
name: 'change-password',
|
|
2292
|
+
title: '@auth:profile-page.sections.change-password.actions.manage',
|
|
2293
|
+
icon: 'far fa-pen',
|
|
2294
|
+
command: { name: AXMAuthCommandKeys.Security.ChangePassword },
|
|
2295
|
+
},
|
|
2296
|
+
],
|
|
2297
|
+
},
|
|
2298
|
+
{
|
|
2299
|
+
id: 'security-pin',
|
|
2300
|
+
groupId: 'security',
|
|
2301
|
+
title: '@auth:profile-page.sections.security-pin.title',
|
|
2302
|
+
description: '@auth:profile-page.sections.security-pin.description',
|
|
2303
|
+
icon: 'far fa-lock',
|
|
2304
|
+
priority: 15,
|
|
2305
|
+
layout: { colSpan: 6 },
|
|
2306
|
+
},
|
|
2307
|
+
{
|
|
2308
|
+
id: 'active-sessions',
|
|
2309
|
+
groupId: 'security',
|
|
2310
|
+
title: '@auth:profile-page.sections.active-sessions.title',
|
|
2311
|
+
description: '@auth:profile-page.sections.active-sessions.description',
|
|
2312
|
+
icon: 'far fa-laptop-mobile',
|
|
2313
|
+
priority: 20,
|
|
2314
|
+
layout: { colSpan: 6 },
|
|
2315
|
+
actions: [
|
|
2316
|
+
{
|
|
2317
|
+
name: 'active-sessions',
|
|
2318
|
+
title: '@auth:profile-page.sections.active-sessions.actions.manage',
|
|
2319
|
+
icon: 'far fa-arrow-right',
|
|
2320
|
+
command: { name: AXMAuthCommandKeys.Session.OpenSessions },
|
|
2321
|
+
},
|
|
2322
|
+
],
|
|
2323
|
+
},
|
|
2324
|
+
{
|
|
2325
|
+
id: 'two-factor',
|
|
2326
|
+
groupId: 'security',
|
|
2327
|
+
title: '@auth:profile-page.sections.two-factor.title',
|
|
2328
|
+
description: '@auth:profile-page.sections.two-factor.description',
|
|
2329
|
+
icon: 'far fa-shield-keyhole',
|
|
2330
|
+
priority: 30,
|
|
2331
|
+
layout: { colSpan: 6 },
|
|
2332
|
+
actions: [
|
|
2333
|
+
{
|
|
2334
|
+
name: 'manage-two-factor',
|
|
2335
|
+
title: '@auth:profile-page.sections.two-factor.actions.manage',
|
|
2336
|
+
icon: 'far fa-pen',
|
|
2337
|
+
command: comingSoon('@auth:profile-page.commands.manage-two-factor.coming-soon'),
|
|
2338
|
+
},
|
|
2339
|
+
],
|
|
2340
|
+
},
|
|
2341
|
+
{
|
|
2342
|
+
id: 'login-history',
|
|
2343
|
+
groupId: 'security',
|
|
2344
|
+
title: '@auth:profile-page.sections.login-history.title',
|
|
2345
|
+
description: '@auth:profile-page.sections.login-history.description',
|
|
2346
|
+
icon: 'far fa-right-to-bracket',
|
|
2347
|
+
priority: 40,
|
|
2348
|
+
layout: { colSpan: 6 },
|
|
2349
|
+
actions: [
|
|
2350
|
+
{
|
|
2351
|
+
name: 'view-login-history',
|
|
2352
|
+
title: '@auth:profile-page.sections.login-history.actions.manage',
|
|
2353
|
+
icon: 'far fa-arrow-right',
|
|
2354
|
+
command: { name: AXMAuthCommandKeys.Security.ViewLoginHistory },
|
|
2355
|
+
},
|
|
2356
|
+
],
|
|
2357
|
+
},
|
|
2358
|
+
{
|
|
2359
|
+
id: 'delete-account',
|
|
2360
|
+
groupId: 'account',
|
|
2361
|
+
priority: 100,
|
|
2362
|
+
content: {
|
|
2363
|
+
mode: 'component',
|
|
2364
|
+
componentKey: PROFILE_DELETE_ACCOUNT_DANGER_ZONE_COMPONENT_KEY,
|
|
2365
|
+
},
|
|
2366
|
+
layout: { colSpan: 12 },
|
|
2367
|
+
},
|
|
2368
|
+
],
|
|
2369
|
+
}),
|
|
2370
|
+
};
|
|
2371
|
+
//#endregion
|
|
2372
|
+
|
|
2373
|
+
//#region ---- Security PIN section hook ----
|
|
2374
|
+
const SECURITY_PIN_SECTION_ID = 'security-pin';
|
|
2375
|
+
/** Enriches the security PIN section with change/remove actions when a PIN exists. */
|
|
2376
|
+
const SecurityPinSectionHookProvider = {
|
|
2377
|
+
key: PROFILE_PAGE_SECTIONS_HOOK,
|
|
2378
|
+
priority: 10,
|
|
2379
|
+
execute: async (payload) => {
|
|
2380
|
+
const hasPinCode = await loadHasPinCode();
|
|
2381
|
+
return {
|
|
2382
|
+
...payload,
|
|
2383
|
+
sections: payload.sections.map((section) => {
|
|
2384
|
+
if (section.id !== SECURITY_PIN_SECTION_ID) {
|
|
2385
|
+
return section;
|
|
2386
|
+
}
|
|
2387
|
+
return {
|
|
2388
|
+
...section,
|
|
2389
|
+
actions: [
|
|
2390
|
+
{
|
|
2391
|
+
name: 'set-pin-code',
|
|
2392
|
+
title: hasPinCode
|
|
2393
|
+
? '@auth:profile-page.sections.security-pin.actions.change'
|
|
2394
|
+
: '@auth:profile-page.sections.security-pin.actions.manage',
|
|
2395
|
+
icon: 'far fa-pen',
|
|
2396
|
+
default: true,
|
|
2397
|
+
command: { name: AXMAuthCommandKeys.Security.SetPinCode },
|
|
2398
|
+
},
|
|
2399
|
+
...(hasPinCode
|
|
2400
|
+
? [
|
|
2401
|
+
{
|
|
2402
|
+
name: 'remove-pin-code',
|
|
2403
|
+
title: '@auth:profile-page.sections.security-pin.actions.remove',
|
|
2404
|
+
icon: 'far fa-trash-can',
|
|
2405
|
+
color: 'danger',
|
|
2406
|
+
command: { name: AXMAuthCommandKeys.Security.RemovePinCode },
|
|
2407
|
+
},
|
|
2408
|
+
]
|
|
2409
|
+
: []),
|
|
2410
|
+
],
|
|
2411
|
+
};
|
|
2412
|
+
}),
|
|
2413
|
+
};
|
|
2414
|
+
},
|
|
2415
|
+
};
|
|
2416
|
+
async function loadHasPinCode() {
|
|
2417
|
+
const sessionService = inject(AXPSessionService);
|
|
2418
|
+
const entityService = inject(AXPEntityService);
|
|
2419
|
+
const userId = sessionService.user?.id;
|
|
2420
|
+
if (isNil(userId)) {
|
|
2421
|
+
return false;
|
|
2422
|
+
}
|
|
2423
|
+
try {
|
|
2424
|
+
const user = await entityService
|
|
2425
|
+
.withEntity(RootConfig$1.module.name, RootConfig$1.entities.users.name)
|
|
2426
|
+
.data()
|
|
2427
|
+
.byKey(userId);
|
|
2428
|
+
return !isEmpty(defaultTo(get(user, 'pinCode'), ''));
|
|
2429
|
+
}
|
|
2430
|
+
catch (error) {
|
|
2431
|
+
console.error('Failed to load security PIN for profile section', error);
|
|
2432
|
+
return false;
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
//#endregion
|
|
2436
|
+
|
|
2437
|
+
//#region ---- Password change tracking service ----
|
|
2438
|
+
const STORAGE_KEY_PREFIX = 'axp-auth:last-password-change-at:';
|
|
2439
|
+
/** Persists the last self-service password change timestamp per user (client-side). */
|
|
2440
|
+
class AXPAuthPasswordChangeTrackingService {
|
|
2441
|
+
constructor() {
|
|
2442
|
+
//#region ---- Services & Dependencies ----
|
|
2443
|
+
this.sessionService = inject(AXPSessionService);
|
|
2444
|
+
}
|
|
2445
|
+
//#endregion
|
|
2446
|
+
//#region ---- Public API ----
|
|
2447
|
+
getLastChangedAt(userId) {
|
|
2448
|
+
const id = userId ?? this.sessionService.user?.id;
|
|
2449
|
+
if (isNil(id)) {
|
|
2450
|
+
return undefined;
|
|
2451
|
+
}
|
|
2452
|
+
const raw = localStorage.getItem(this.buildStorageKey(id));
|
|
2453
|
+
if (!raw) {
|
|
2454
|
+
return undefined;
|
|
2455
|
+
}
|
|
2456
|
+
const parsed = new Date(raw);
|
|
2457
|
+
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
2458
|
+
}
|
|
2459
|
+
recordChange(userId, changedAt = new Date()) {
|
|
2460
|
+
const id = userId ?? this.sessionService.user?.id;
|
|
2461
|
+
if (isNil(id)) {
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
localStorage.setItem(this.buildStorageKey(id), changedAt.toISOString());
|
|
2465
|
+
}
|
|
2466
|
+
//#endregion
|
|
2467
|
+
//#region ---- Private Methods ----
|
|
2468
|
+
buildStorageKey(userId) {
|
|
2469
|
+
return `${STORAGE_KEY_PREFIX}${userId}`;
|
|
2470
|
+
}
|
|
2471
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPAuthPasswordChangeTrackingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2472
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPAuthPasswordChangeTrackingService, providedIn: 'root' }); }
|
|
2473
|
+
}
|
|
2474
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPAuthPasswordChangeTrackingService, decorators: [{
|
|
2475
|
+
type: Injectable,
|
|
2476
|
+
args: [{ providedIn: 'root' }]
|
|
2477
|
+
}] });
|
|
2478
|
+
|
|
2479
|
+
//#region ---- Section ids ----
|
|
2480
|
+
const CHANGE_PASSWORD_SECTION_ID = 'change-password';
|
|
2481
|
+
const ACTIVE_SESSIONS_SECTION_ID = 'active-sessions';
|
|
2482
|
+
const LOGIN_HISTORY_SECTION_ID = 'login-history';
|
|
2483
|
+
//#endregion
|
|
2484
|
+
//#region ---- Profile page security insights service ----
|
|
2485
|
+
/** Loads contextual security insights for profile section cards. */
|
|
2486
|
+
class AXPUserProfileSecurityInsightsService {
|
|
2487
|
+
constructor() {
|
|
2488
|
+
//#region ---- Services & Dependencies ----
|
|
2489
|
+
this.sessionService = inject(AXPSessionService);
|
|
2490
|
+
this.entityService = inject(AXPEntityService);
|
|
2491
|
+
this.translateService = inject(AXTranslationService);
|
|
2492
|
+
this.passwordChangeTrackingService = inject(AXPAuthPasswordChangeTrackingService);
|
|
2493
|
+
}
|
|
2494
|
+
//#endregion
|
|
2495
|
+
//#region ---- Public API ----
|
|
2496
|
+
async loadBySectionId() {
|
|
2497
|
+
const userId = this.sessionService.user?.id;
|
|
2498
|
+
if (!userId) {
|
|
2499
|
+
return {};
|
|
2500
|
+
}
|
|
2501
|
+
const sessions = await this.loadUserSessions(userId);
|
|
2502
|
+
const insightsBySectionId = {};
|
|
2503
|
+
const passwordInsights = this.buildPasswordInsights(userId);
|
|
2504
|
+
if (!isEmpty(passwordInsights)) {
|
|
2505
|
+
insightsBySectionId[CHANGE_PASSWORD_SECTION_ID] = passwordInsights;
|
|
2506
|
+
}
|
|
2507
|
+
const activeSessionInsights = await this.buildActiveSessionInsights(sessions);
|
|
2508
|
+
if (!isEmpty(activeSessionInsights)) {
|
|
2509
|
+
insightsBySectionId[ACTIVE_SESSIONS_SECTION_ID] = activeSessionInsights;
|
|
2510
|
+
}
|
|
2511
|
+
const loginHistoryInsights = this.buildLoginHistoryInsights(sessions);
|
|
2512
|
+
if (!isEmpty(loginHistoryInsights)) {
|
|
2513
|
+
insightsBySectionId[LOGIN_HISTORY_SECTION_ID] = loginHistoryInsights;
|
|
2514
|
+
}
|
|
2515
|
+
return insightsBySectionId;
|
|
2516
|
+
}
|
|
2517
|
+
//#endregion
|
|
2518
|
+
//#region ---- Insight builders ----
|
|
2519
|
+
buildPasswordInsights(userId) {
|
|
2520
|
+
const lastChangedAt = this.passwordChangeTrackingService.getLastChangedAt(userId);
|
|
2521
|
+
if (isNil(lastChangedAt)) {
|
|
2522
|
+
return [];
|
|
2523
|
+
}
|
|
2524
|
+
return [
|
|
2525
|
+
{
|
|
2526
|
+
id: 'last-password-change',
|
|
2527
|
+
icon: 'fa-light fa-fw fa-clock',
|
|
2528
|
+
date: lastChangedAt,
|
|
2529
|
+
datePrefix: '@auth:profile-page.sections.change-password.insights.last-changed',
|
|
2530
|
+
},
|
|
2531
|
+
];
|
|
2532
|
+
}
|
|
2533
|
+
async buildActiveSessionInsights(sessions) {
|
|
2534
|
+
const activeSessions = sessions.filter((session) => String(session.status).toLowerCase() === 'active');
|
|
2535
|
+
const currentSession = activeSessions.find((session) => session.isActiveSession) ??
|
|
2536
|
+
orderBy(activeSessions, [(session) => this.parseValidDate(session.lastLoginDate)?.getTime() ?? 0], ['desc'])[0];
|
|
2537
|
+
if (isNil(currentSession)) {
|
|
2538
|
+
return [];
|
|
2539
|
+
}
|
|
2540
|
+
const deviceSummary = this.formatSessionSummary(currentSession);
|
|
2541
|
+
if (isEmpty(deviceSummary)) {
|
|
2542
|
+
return [];
|
|
2543
|
+
}
|
|
2544
|
+
const otherActiveCount = activeSessions.filter((session) => session.id !== currentSession.id).length;
|
|
2545
|
+
const insights = [
|
|
2546
|
+
{
|
|
2547
|
+
id: 'current-session',
|
|
2548
|
+
icon: 'fa-light fa-fw fa-laptop',
|
|
2549
|
+
text: deviceSummary,
|
|
2550
|
+
},
|
|
2551
|
+
];
|
|
2552
|
+
if (otherActiveCount > 0) {
|
|
2553
|
+
insights.push({
|
|
2554
|
+
id: 'other-sessions',
|
|
2555
|
+
icon: 'fa-light fa-fw fa-mobile-screen',
|
|
2556
|
+
text: await this.translateService.translateAsync('@auth:profile-page.sections.active-sessions.insights.other-devices-value', { params: { count: otherActiveCount } }),
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
return insights;
|
|
2560
|
+
}
|
|
2561
|
+
buildLoginHistoryInsights(sessions) {
|
|
2562
|
+
const latestSession = orderBy(sessions, [(session) => this.parseValidDate(session.lastLoginDate)?.getTime() ?? 0], ['desc'])[0];
|
|
2563
|
+
if (isNil(latestSession)) {
|
|
2564
|
+
return [];
|
|
2565
|
+
}
|
|
2566
|
+
const insights = [];
|
|
2567
|
+
const lastLoginDate = this.parseValidDate(latestSession.lastLoginDate);
|
|
2568
|
+
if (lastLoginDate) {
|
|
2569
|
+
insights.push({
|
|
2570
|
+
id: 'last-login',
|
|
2571
|
+
icon: 'fa-light fa-fw fa-clock-rotate-left',
|
|
2572
|
+
date: lastLoginDate,
|
|
2573
|
+
datePrefix: '@auth:profile-page.sections.login-history.insights.last-sign-in',
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
const location = this.formatLoginLocation(latestSession);
|
|
2577
|
+
if (!isEmpty(location)) {
|
|
2578
|
+
insights.push({
|
|
2579
|
+
id: 'last-login-location',
|
|
2580
|
+
icon: 'fa-light fa-fw fa-location-dot',
|
|
2581
|
+
text: location,
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
return insights;
|
|
2585
|
+
}
|
|
2586
|
+
//#endregion
|
|
2587
|
+
//#region ---- Data loading ----
|
|
2588
|
+
async loadUserSessions(userId) {
|
|
2589
|
+
const response = await this.entityService
|
|
2590
|
+
.withEntity(RootConfig$1.module.name, RootConfig$1.entities.deviceSessions.name)
|
|
2591
|
+
.data()
|
|
2592
|
+
.query({ skip: 0, take: 1000 });
|
|
2593
|
+
return (response.items ?? []).filter((item) => item.userId === userId);
|
|
2594
|
+
}
|
|
2595
|
+
//#endregion
|
|
2596
|
+
//#region ---- Formatting ----
|
|
2597
|
+
parseValidDate(value) {
|
|
2598
|
+
if (value instanceof Date) {
|
|
2599
|
+
return Number.isNaN(value.getTime()) ? undefined : value;
|
|
2600
|
+
}
|
|
2601
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
2602
|
+
const parsed = new Date(value);
|
|
2603
|
+
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
2604
|
+
}
|
|
2605
|
+
return undefined;
|
|
2606
|
+
}
|
|
2607
|
+
formatSessionSummary(session) {
|
|
2608
|
+
const parts = [session.deviceName, session.browser, session.location]
|
|
2609
|
+
.map((part) => defaultTo(part, '').trim())
|
|
2610
|
+
.filter(Boolean);
|
|
2611
|
+
return parts.join(' · ');
|
|
2612
|
+
}
|
|
2613
|
+
formatLoginLocation(session) {
|
|
2614
|
+
const parts = [session.location, session.ipAddress]
|
|
2615
|
+
.map((part) => defaultTo(part, '').trim())
|
|
2616
|
+
.filter(Boolean);
|
|
2617
|
+
return parts.join(' · ');
|
|
2618
|
+
}
|
|
2619
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPUserProfileSecurityInsightsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2620
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPUserProfileSecurityInsightsService, providedIn: 'root' }); }
|
|
2621
|
+
}
|
|
2622
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPUserProfileSecurityInsightsService, decorators: [{
|
|
2623
|
+
type: Injectable,
|
|
2624
|
+
args: [{ providedIn: 'root' }]
|
|
2625
|
+
}] });
|
|
2626
|
+
|
|
2627
|
+
//#region ---- Security sections insights hook ----
|
|
2628
|
+
/** Adds live security context (sessions, login, password) to profile section cards. */
|
|
2629
|
+
const SecuritySectionsInsightsHookProvider = {
|
|
2630
|
+
key: PROFILE_PAGE_SECTIONS_HOOK,
|
|
2631
|
+
priority: 15,
|
|
2632
|
+
execute: async (payload) => {
|
|
2633
|
+
const insightsService = inject(AXPUserProfileSecurityInsightsService);
|
|
2634
|
+
const insightsBySectionId = await insightsService.loadBySectionId();
|
|
2635
|
+
return {
|
|
2636
|
+
...payload,
|
|
2637
|
+
sections: payload.sections.map((section) => {
|
|
2638
|
+
const insights = insightsBySectionId[section.id];
|
|
2639
|
+
if (isEmpty(insights)) {
|
|
2640
|
+
return section;
|
|
2641
|
+
}
|
|
2642
|
+
return {
|
|
2643
|
+
...section,
|
|
2644
|
+
insights,
|
|
2645
|
+
};
|
|
2646
|
+
}),
|
|
2647
|
+
};
|
|
2648
|
+
},
|
|
2649
|
+
};
|
|
2650
|
+
//#endregion
|
|
2651
|
+
|
|
2652
|
+
//#region ---- Profile slot logout menu hook ----
|
|
2653
|
+
/** Registers the sign-out entry in the profile dropdown. */
|
|
2654
|
+
const ProfileSlotLogoutMenuHookProvider = {
|
|
2655
|
+
key: PROFILE_SLOT_MENU_ITEMS_HOOK,
|
|
2656
|
+
priority: 100,
|
|
2657
|
+
execute: (payload) => ({
|
|
2658
|
+
...payload,
|
|
2659
|
+
items: [
|
|
2660
|
+
...payload.items,
|
|
2661
|
+
{
|
|
2662
|
+
name: 'logout',
|
|
2663
|
+
title: '@auth:profile.logout',
|
|
2664
|
+
icon: 'fa-light fa-arrow-right-from-bracket',
|
|
2665
|
+
color: 'danger',
|
|
2666
|
+
shortcuts: [{ keys: 'ctrl+shift+q', allowInEditableFields: true }],
|
|
2667
|
+
command: { name: AXMAuthCommandKeys.Session.Signout },
|
|
2668
|
+
},
|
|
2669
|
+
],
|
|
2670
|
+
}),
|
|
2671
|
+
};
|
|
2672
|
+
//#endregion
|
|
2673
|
+
|
|
2674
|
+
//#region ---- Profile slot my profile menu hook ----
|
|
2675
|
+
/** Registers the My Profile entry in the profile dropdown. */
|
|
2676
|
+
const ProfileSlotMyProfileMenuHookProvider = {
|
|
2677
|
+
key: PROFILE_SLOT_MENU_ITEMS_HOOK,
|
|
2678
|
+
priority: -10,
|
|
2679
|
+
execute: (payload) => {
|
|
2680
|
+
const sessionService = inject(AXPSessionService);
|
|
2681
|
+
const appName = sessionService.application?.name;
|
|
2682
|
+
if (!appName) {
|
|
2683
|
+
return payload;
|
|
2684
|
+
}
|
|
2685
|
+
return {
|
|
2686
|
+
...payload,
|
|
2687
|
+
items: [
|
|
2688
|
+
...payload.items,
|
|
2689
|
+
{
|
|
2690
|
+
name: 'my-profile',
|
|
2691
|
+
title: '@auth:profile-page.profile-slot.my-profile',
|
|
2692
|
+
icon: 'fa-light fa-user',
|
|
2693
|
+
shortcuts: [{ keys: 'ctrl+shift+p', allowInEditableFields: true }],
|
|
2694
|
+
command: {
|
|
2695
|
+
name: 'navigate',
|
|
2696
|
+
options: {
|
|
2697
|
+
path: `/${appName}/profile/account`,
|
|
2698
|
+
},
|
|
2699
|
+
},
|
|
2700
|
+
},
|
|
2701
|
+
],
|
|
2702
|
+
};
|
|
2703
|
+
},
|
|
2704
|
+
};
|
|
2705
|
+
//#endregion
|
|
2706
|
+
|
|
2707
|
+
//#region ---- Profile slot lock menu hook ----
|
|
2708
|
+
/** Registers the manual lock entry when Lock When Idle is enabled. Profile menu reloads on {@link AXPAuthSettings.LockWhenIdleEnabled} changes. */
|
|
2709
|
+
const ProfileSlotLockMenuHookProvider = {
|
|
2710
|
+
key: PROFILE_SLOT_MENU_ITEMS_HOOK,
|
|
2711
|
+
priority: 50,
|
|
2712
|
+
execute: async (payload) => {
|
|
2713
|
+
const idleLockService = inject(AXPIdleLockService);
|
|
2714
|
+
await idleLockService.refreshLockState();
|
|
2715
|
+
if (!idleLockService.canLock()) {
|
|
2716
|
+
return payload;
|
|
2717
|
+
}
|
|
2718
|
+
return {
|
|
2719
|
+
...payload,
|
|
2720
|
+
items: [
|
|
2721
|
+
...payload.items,
|
|
2722
|
+
{
|
|
2723
|
+
name: 'lock-screen',
|
|
2724
|
+
title: '@auth:profile.lock',
|
|
2725
|
+
icon: 'fa-light fa-lock',
|
|
2726
|
+
break: { before: true },
|
|
2727
|
+
shortcuts: [{ keys: 'ctrl+alt+l', allowInEditableFields: true }],
|
|
2728
|
+
command: { name: AXMAuthCommandKeys.Session.LockScreen },
|
|
2729
|
+
},
|
|
2730
|
+
],
|
|
2731
|
+
};
|
|
2732
|
+
},
|
|
2733
|
+
};
|
|
2734
|
+
//#endregion
|
|
2735
|
+
|
|
2736
|
+
//#region ---- User avatar client middleware ----
|
|
2737
|
+
const USER_ENTITY = `${RootConfig$1.module.name}.${RootConfig$1.entities.users.name}`;
|
|
2738
|
+
const PERSON_ENTITY = `${RootConfig.module.name}.${RootConfig.entities.person.name}`;
|
|
2739
|
+
function collectUserRows(result) {
|
|
2740
|
+
if (Array.isArray(result)) {
|
|
2741
|
+
return result;
|
|
2742
|
+
}
|
|
2743
|
+
if (result && typeof result === 'object' && Array.isArray(result.items)) {
|
|
2744
|
+
return result.items ?? [];
|
|
2745
|
+
}
|
|
2746
|
+
if (result && typeof result === 'object') {
|
|
2747
|
+
return [result];
|
|
2748
|
+
}
|
|
2749
|
+
return [];
|
|
2750
|
+
}
|
|
2751
|
+
async function loadPersonImagesByUserIds(ctx, userIds) {
|
|
2752
|
+
const imageByUserId = new Map();
|
|
2753
|
+
if (!userIds.length) {
|
|
2754
|
+
return imageByUserId;
|
|
2755
|
+
}
|
|
2756
|
+
try {
|
|
2757
|
+
const response = await ctx.backend.query(PERSON_ENTITY, {
|
|
2758
|
+
skip: 0,
|
|
2759
|
+
take: userIds.length,
|
|
2760
|
+
filter: { field: 'userId', operator: { type: 'in' }, value: userIds },
|
|
2761
|
+
});
|
|
2762
|
+
for (const item of response?.items ?? []) {
|
|
2763
|
+
const person = item;
|
|
2764
|
+
const userId = get(person, 'userId');
|
|
2765
|
+
const image = normalizeAvatarRef(person['image']);
|
|
2766
|
+
if (userId != null && image) {
|
|
2767
|
+
imageByUserId.set(String(userId), image);
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
catch {
|
|
2772
|
+
/* ignore hydration failures */
|
|
2773
|
+
}
|
|
2774
|
+
return imageByUserId;
|
|
2775
|
+
}
|
|
2776
|
+
/** Hydrates User.avatar from linked Person.image on read. */
|
|
2777
|
+
const userAvatarHydrateMiddleware = {
|
|
2778
|
+
target: { ops: ['getOne', 'getAll', 'query'], order: 13 },
|
|
2779
|
+
execute: async (ctx, next) => {
|
|
2780
|
+
await next();
|
|
2781
|
+
if (ctx.entityName !== USER_ENTITY) {
|
|
2782
|
+
return;
|
|
2783
|
+
}
|
|
2784
|
+
const rows = collectUserRows(ctx.result);
|
|
2785
|
+
const userIds = Array.from(new Set(rows.map((row) => get(row, 'id')).filter((id) => id != null).map(String)));
|
|
2786
|
+
const imageByUserId = await loadPersonImagesByUserIds(ctx, userIds);
|
|
2787
|
+
for (const row of rows) {
|
|
2788
|
+
const userId = get(row, 'id');
|
|
2789
|
+
if (userId == null) {
|
|
2790
|
+
continue;
|
|
2791
|
+
}
|
|
2792
|
+
const personImage = imageByUserId.get(String(userId));
|
|
2793
|
+
if (personImage) {
|
|
2794
|
+
row['avatar'] = personImage;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
},
|
|
2798
|
+
};
|
|
2799
|
+
/** Refreshes avatar cache/session after user detail saves avatar. */
|
|
2800
|
+
const userAvatarCacheRefreshMiddleware = {
|
|
2801
|
+
target: { ops: ['update'], order: 95 },
|
|
2802
|
+
execute: async (ctx, next) => {
|
|
2803
|
+
const avatarSync = inject(AXPUserAvatarSyncService);
|
|
2804
|
+
if (ctx.entityName !== USER_ENTITY) {
|
|
2805
|
+
await next();
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
const data = (ctx.data ?? {});
|
|
2809
|
+
const avatarTouched = Object.prototype.hasOwnProperty.call(data, 'avatar');
|
|
2810
|
+
await next();
|
|
2811
|
+
if (!avatarTouched) {
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
const userId = (ctx.id ?? get(ctx.result, 'id'));
|
|
2815
|
+
if (!userId) {
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
const avatar = normalizeAvatarRef(get(ctx.result, 'avatar') ?? data['avatar']);
|
|
2819
|
+
await avatarSync.notifyAvatarChanged(userId, avatar);
|
|
2820
|
+
},
|
|
2821
|
+
};
|
|
2822
|
+
//#endregion
|
|
2823
|
+
|
|
2824
|
+
function profileRoutesFactory() {
|
|
2825
|
+
return [
|
|
2826
|
+
{
|
|
2827
|
+
path: ':app/profile',
|
|
2828
|
+
canActivate: [...AXP_PROTECTED_ROUTE_GUARDS],
|
|
2829
|
+
data: { tab: true, rootLayout: true },
|
|
2830
|
+
loadComponent: () => AXPRootLayoutComponent,
|
|
2831
|
+
children: [
|
|
2832
|
+
{
|
|
2833
|
+
path: '',
|
|
2834
|
+
redirectTo: 'account',
|
|
2835
|
+
pathMatch: 'full',
|
|
2836
|
+
},
|
|
2837
|
+
{
|
|
2838
|
+
path: ':groupId',
|
|
2839
|
+
loadComponent: () => import('./acorex-platform-module-auth-client-profile-page.component-tbhf7K0V.mjs').then((c) => c.AXMUserProfilePageComponent),
|
|
2840
|
+
data: { reuse: true },
|
|
2841
|
+
},
|
|
2842
|
+
],
|
|
2843
|
+
},
|
|
2844
|
+
];
|
|
2845
|
+
}
|
|
2846
|
+
class AXMAuthModule {
|
|
2847
|
+
constructor() {
|
|
2848
|
+
inject(AXPIdleLockService);
|
|
2849
|
+
inject(AXPLogoEntitySyncService);
|
|
2850
|
+
}
|
|
2851
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMAuthModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
2852
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: AXMAuthModule, imports: [CommonModule, i1$4.AXPCommonModule, i1$5.RouterModule, AXPAccountModule,
|
|
2853
|
+
AXMAuthValidationRulesModule] }); }
|
|
2854
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMAuthModule, providers: [
|
|
2855
|
+
provideAppInitializer(() => {
|
|
2856
|
+
const sessionService = inject(AXPSessionService);
|
|
2857
|
+
const idleLockService = inject(AXPIdleLockService);
|
|
2858
|
+
return (async () => {
|
|
2859
|
+
await firstValueFrom(sessionService.isLoading$.pipe(filter((loading) => !loading)));
|
|
2860
|
+
if (sessionService.isLocked()) {
|
|
2861
|
+
await idleLockService.restoreFromPersistedSession();
|
|
2862
|
+
}
|
|
2863
|
+
})();
|
|
2864
|
+
}),
|
|
2865
|
+
AXMAuthErrorHandler,
|
|
2866
|
+
provideLazyProvider(AXP_MENU_PROVIDER, () => import('./acorex-platform-module-auth-client-menu.provider-Bz-kRip4.mjs').then(m => m.AXMSecurityManagementModuleMenuProvider)),
|
|
2867
|
+
provideLazyProvider(AXP_SETTING_DEFINITION_PROVIDER, () => import('./acorex-platform-module-auth-client-settings.provider-n1qbs6e1.mjs').then(m => m.AXMSettingProvider)),
|
|
2868
|
+
provideLazyProvider(AXP_PAGE_COMPONENT_PROVIDER, () => import('./acorex-platform-module-auth-client-profile-page-component.provider-B-Qtd4_j.mjs').then((module) => module.AXMProfilePageComponentProvider)),
|
|
2869
|
+
provideLazyProvider(AXP_DATASOURCE_DEFINITION_PROVIDER, () => Promise.resolve().then(function () { return datasource_provider; }).then((m) => m.AXMAuthDataSourceProvider)),
|
|
2870
|
+
{
|
|
2871
|
+
provide: ROUTES,
|
|
2872
|
+
multi: true,
|
|
2873
|
+
useFactory: profileRoutesFactory,
|
|
2874
|
+
},
|
|
2875
|
+
{
|
|
2876
|
+
provide: AXP_ENTITY_STORAGE_MIDDLEWARE,
|
|
2877
|
+
multi: true,
|
|
2878
|
+
useValue: userAvatarHydrateMiddleware,
|
|
2879
|
+
},
|
|
2880
|
+
{
|
|
2881
|
+
provide: AXP_ENTITY_STORAGE_MIDDLEWARE,
|
|
2882
|
+
multi: true,
|
|
2883
|
+
useValue: userAvatarCacheRefreshMiddleware,
|
|
2884
|
+
},
|
|
2885
|
+
provideCommandSetups([
|
|
2886
|
+
{
|
|
2887
|
+
key: AXMAuthCommandKeys.Profile.Show,
|
|
2888
|
+
command: () => import('./acorex-platform-module-auth-client-show-profile-command-CxMz1yy-.mjs').then((c) => c.AXPShowProfileCommand),
|
|
2889
|
+
},
|
|
2890
|
+
{
|
|
2891
|
+
key: AXMAuthCommandKeys.Session.Signout,
|
|
2892
|
+
command: () => import('./acorex-platform-module-auth-client-signout-command-DqqrP5pk.mjs').then((c) => c.AXPSignoutCommand),
|
|
2893
|
+
},
|
|
2894
|
+
{
|
|
2895
|
+
key: AXMAuthCommandKeys.Session.OpenSessions,
|
|
2896
|
+
command: () => import('./acorex-platform-module-auth-client-open-sessions.command-CXSGuqYp.mjs').then((c) => c.AXPOpenSessionsCommand),
|
|
2897
|
+
},
|
|
2898
|
+
{
|
|
2899
|
+
key: AXMAuthCommandKeys.Session.LockScreen,
|
|
2900
|
+
command: () => import('./acorex-platform-module-auth-client-lock-screen.command-41QnG1TQ.mjs').then((c) => c.AXPLockScreenCommand),
|
|
2901
|
+
},
|
|
2902
|
+
{
|
|
2903
|
+
key: AXMAuthCommandKeys.Profile.ChangePhoto,
|
|
2904
|
+
command: () => import('./acorex-platform-module-auth-client-change-photo.command-BC8lw--F.mjs').then((c) => c.AXPChangePhotoCommand),
|
|
2905
|
+
},
|
|
2906
|
+
{
|
|
2907
|
+
key: AXMAuthCommandKeys.Profile.EditAccountInfo,
|
|
2908
|
+
command: () => import('./acorex-platform-module-auth-client-edit-account-info.command-B-mVlXxg.mjs').then((c) => c.AXPEditAccountInfoCommand),
|
|
2909
|
+
},
|
|
2910
|
+
{
|
|
2911
|
+
key: AXMAuthCommandKeys.Security.ChangePassword,
|
|
2912
|
+
command: () => import('./acorex-platform-module-auth-client-change-password.command-ZyXAjUKw.mjs').then((c) => c.AXPChangePasswordCommand),
|
|
2913
|
+
},
|
|
2914
|
+
{
|
|
2915
|
+
key: AXMAuthCommandKeys.Security.SetPinCode,
|
|
2916
|
+
command: () => import('./acorex-platform-module-auth-client-set-pin-code.command-DXTVbrS0.mjs').then((c) => c.AXPSetPinCodeCommand),
|
|
2917
|
+
},
|
|
2918
|
+
{
|
|
2919
|
+
key: AXMAuthCommandKeys.Security.RemovePinCode,
|
|
2920
|
+
command: () => import('./acorex-platform-module-auth-client-remove-pin-code.command-zUZooOtv.mjs').then((c) => c.AXPRemovePinCodeCommand),
|
|
2921
|
+
},
|
|
2922
|
+
{
|
|
2923
|
+
key: AXMAuthCommandKeys.Security.ViewLoginHistory,
|
|
2924
|
+
command: () => import('./acorex-platform-module-auth-client-view-login-history.command-Dl0os5lD.mjs').then((c) => c.AXPViewLoginHistoryCommand),
|
|
2925
|
+
},
|
|
2926
|
+
{
|
|
2927
|
+
key: AXMAuthCommandKeys.Security.EditSignature,
|
|
2928
|
+
command: () => import('./acorex-platform-module-auth-client-edit-signature.command-CcTJJwAX.mjs').then((c) => c.AXPEditSignatureCommand),
|
|
2929
|
+
},
|
|
2930
|
+
{
|
|
2931
|
+
key: AXMAuthCommandKeys.Profile.ComingSoon,
|
|
2932
|
+
command: () => import('./acorex-platform-module-auth-client-profile-coming-soon.command-DBbFbzAx.mjs').then((c) => c.AXPProfileComingSoonCommand),
|
|
2933
|
+
},
|
|
2934
|
+
]),
|
|
2935
|
+
{
|
|
2936
|
+
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
2937
|
+
useValue: ProfileSlotLogoutMenuHookProvider,
|
|
2938
|
+
multi: true,
|
|
2939
|
+
},
|
|
2940
|
+
{
|
|
2941
|
+
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
2942
|
+
useValue: ProfileSlotMyProfileMenuHookProvider,
|
|
2943
|
+
multi: true,
|
|
2944
|
+
},
|
|
2945
|
+
{
|
|
2946
|
+
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
2947
|
+
useValue: ProfileSlotLockMenuHookProvider,
|
|
2948
|
+
multi: true,
|
|
2949
|
+
},
|
|
2950
|
+
{
|
|
2951
|
+
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
2952
|
+
useValue: CoreProfileGroupsHookProvider,
|
|
2953
|
+
multi: true,
|
|
2954
|
+
},
|
|
2955
|
+
{
|
|
2956
|
+
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
2957
|
+
useValue: CoreProfileSectionsHookProvider,
|
|
2958
|
+
multi: true,
|
|
2959
|
+
},
|
|
2960
|
+
{
|
|
2961
|
+
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
2962
|
+
useValue: SecurityPinSectionHookProvider,
|
|
2963
|
+
multi: true,
|
|
2964
|
+
},
|
|
2965
|
+
{
|
|
2966
|
+
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
2967
|
+
useValue: SecuritySectionsInsightsHookProvider,
|
|
2968
|
+
multi: true,
|
|
2969
|
+
},
|
|
2970
|
+
], imports: [CommonModule,
|
|
2971
|
+
AXPCommonModule.forChild({ errorHandlers: [AXMAuthErrorHandler] }),
|
|
2972
|
+
RouterModule.forChild(routes),
|
|
2973
|
+
AXPAccountModule,
|
|
2974
|
+
AXMAuthValidationRulesModule] }); }
|
|
2975
|
+
}
|
|
2976
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXMAuthModule, decorators: [{
|
|
2977
|
+
type: NgModule,
|
|
2978
|
+
args: [{
|
|
2979
|
+
imports: [
|
|
2980
|
+
CommonModule,
|
|
2981
|
+
AXPCommonModule.forChild({ errorHandlers: [AXMAuthErrorHandler] }),
|
|
2982
|
+
RouterModule.forChild(routes),
|
|
2983
|
+
AXPAccountModule,
|
|
2984
|
+
AXMAuthValidationRulesModule,
|
|
2985
|
+
],
|
|
2986
|
+
providers: [
|
|
2987
|
+
provideAppInitializer(() => {
|
|
2988
|
+
const sessionService = inject(AXPSessionService);
|
|
2989
|
+
const idleLockService = inject(AXPIdleLockService);
|
|
2990
|
+
return (async () => {
|
|
2991
|
+
await firstValueFrom(sessionService.isLoading$.pipe(filter((loading) => !loading)));
|
|
2992
|
+
if (sessionService.isLocked()) {
|
|
2993
|
+
await idleLockService.restoreFromPersistedSession();
|
|
2994
|
+
}
|
|
2995
|
+
})();
|
|
2996
|
+
}),
|
|
2997
|
+
AXMAuthErrorHandler,
|
|
2998
|
+
provideLazyProvider(AXP_MENU_PROVIDER, () => import('./acorex-platform-module-auth-client-menu.provider-Bz-kRip4.mjs').then(m => m.AXMSecurityManagementModuleMenuProvider)),
|
|
2999
|
+
provideLazyProvider(AXP_SETTING_DEFINITION_PROVIDER, () => import('./acorex-platform-module-auth-client-settings.provider-n1qbs6e1.mjs').then(m => m.AXMSettingProvider)),
|
|
3000
|
+
provideLazyProvider(AXP_PAGE_COMPONENT_PROVIDER, () => import('./acorex-platform-module-auth-client-profile-page-component.provider-B-Qtd4_j.mjs').then((module) => module.AXMProfilePageComponentProvider)),
|
|
3001
|
+
provideLazyProvider(AXP_DATASOURCE_DEFINITION_PROVIDER, () => Promise.resolve().then(function () { return datasource_provider; }).then((m) => m.AXMAuthDataSourceProvider)),
|
|
3002
|
+
{
|
|
3003
|
+
provide: ROUTES,
|
|
3004
|
+
multi: true,
|
|
3005
|
+
useFactory: profileRoutesFactory,
|
|
3006
|
+
},
|
|
3007
|
+
{
|
|
3008
|
+
provide: AXP_ENTITY_STORAGE_MIDDLEWARE,
|
|
3009
|
+
multi: true,
|
|
3010
|
+
useValue: userAvatarHydrateMiddleware,
|
|
3011
|
+
},
|
|
3012
|
+
{
|
|
3013
|
+
provide: AXP_ENTITY_STORAGE_MIDDLEWARE,
|
|
3014
|
+
multi: true,
|
|
3015
|
+
useValue: userAvatarCacheRefreshMiddleware,
|
|
3016
|
+
},
|
|
3017
|
+
provideCommandSetups([
|
|
3018
|
+
{
|
|
3019
|
+
key: AXMAuthCommandKeys.Profile.Show,
|
|
3020
|
+
command: () => import('./acorex-platform-module-auth-client-show-profile-command-CxMz1yy-.mjs').then((c) => c.AXPShowProfileCommand),
|
|
3021
|
+
},
|
|
3022
|
+
{
|
|
3023
|
+
key: AXMAuthCommandKeys.Session.Signout,
|
|
3024
|
+
command: () => import('./acorex-platform-module-auth-client-signout-command-DqqrP5pk.mjs').then((c) => c.AXPSignoutCommand),
|
|
3025
|
+
},
|
|
3026
|
+
{
|
|
3027
|
+
key: AXMAuthCommandKeys.Session.OpenSessions,
|
|
3028
|
+
command: () => import('./acorex-platform-module-auth-client-open-sessions.command-CXSGuqYp.mjs').then((c) => c.AXPOpenSessionsCommand),
|
|
3029
|
+
},
|
|
3030
|
+
{
|
|
3031
|
+
key: AXMAuthCommandKeys.Session.LockScreen,
|
|
3032
|
+
command: () => import('./acorex-platform-module-auth-client-lock-screen.command-41QnG1TQ.mjs').then((c) => c.AXPLockScreenCommand),
|
|
3033
|
+
},
|
|
3034
|
+
{
|
|
3035
|
+
key: AXMAuthCommandKeys.Profile.ChangePhoto,
|
|
3036
|
+
command: () => import('./acorex-platform-module-auth-client-change-photo.command-BC8lw--F.mjs').then((c) => c.AXPChangePhotoCommand),
|
|
3037
|
+
},
|
|
3038
|
+
{
|
|
3039
|
+
key: AXMAuthCommandKeys.Profile.EditAccountInfo,
|
|
3040
|
+
command: () => import('./acorex-platform-module-auth-client-edit-account-info.command-B-mVlXxg.mjs').then((c) => c.AXPEditAccountInfoCommand),
|
|
3041
|
+
},
|
|
3042
|
+
{
|
|
3043
|
+
key: AXMAuthCommandKeys.Security.ChangePassword,
|
|
3044
|
+
command: () => import('./acorex-platform-module-auth-client-change-password.command-ZyXAjUKw.mjs').then((c) => c.AXPChangePasswordCommand),
|
|
3045
|
+
},
|
|
3046
|
+
{
|
|
3047
|
+
key: AXMAuthCommandKeys.Security.SetPinCode,
|
|
3048
|
+
command: () => import('./acorex-platform-module-auth-client-set-pin-code.command-DXTVbrS0.mjs').then((c) => c.AXPSetPinCodeCommand),
|
|
3049
|
+
},
|
|
3050
|
+
{
|
|
3051
|
+
key: AXMAuthCommandKeys.Security.RemovePinCode,
|
|
3052
|
+
command: () => import('./acorex-platform-module-auth-client-remove-pin-code.command-zUZooOtv.mjs').then((c) => c.AXPRemovePinCodeCommand),
|
|
3053
|
+
},
|
|
3054
|
+
{
|
|
3055
|
+
key: AXMAuthCommandKeys.Security.ViewLoginHistory,
|
|
3056
|
+
command: () => import('./acorex-platform-module-auth-client-view-login-history.command-Dl0os5lD.mjs').then((c) => c.AXPViewLoginHistoryCommand),
|
|
3057
|
+
},
|
|
3058
|
+
{
|
|
3059
|
+
key: AXMAuthCommandKeys.Security.EditSignature,
|
|
3060
|
+
command: () => import('./acorex-platform-module-auth-client-edit-signature.command-CcTJJwAX.mjs').then((c) => c.AXPEditSignatureCommand),
|
|
3061
|
+
},
|
|
3062
|
+
{
|
|
3063
|
+
key: AXMAuthCommandKeys.Profile.ComingSoon,
|
|
3064
|
+
command: () => import('./acorex-platform-module-auth-client-profile-coming-soon.command-DBbFbzAx.mjs').then((c) => c.AXPProfileComingSoonCommand),
|
|
3065
|
+
},
|
|
3066
|
+
]),
|
|
3067
|
+
{
|
|
3068
|
+
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
3069
|
+
useValue: ProfileSlotLogoutMenuHookProvider,
|
|
3070
|
+
multi: true,
|
|
3071
|
+
},
|
|
3072
|
+
{
|
|
3073
|
+
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
3074
|
+
useValue: ProfileSlotMyProfileMenuHookProvider,
|
|
3075
|
+
multi: true,
|
|
3076
|
+
},
|
|
3077
|
+
{
|
|
3078
|
+
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
3079
|
+
useValue: ProfileSlotLockMenuHookProvider,
|
|
3080
|
+
multi: true,
|
|
3081
|
+
},
|
|
3082
|
+
{
|
|
3083
|
+
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
3084
|
+
useValue: CoreProfileGroupsHookProvider,
|
|
3085
|
+
multi: true,
|
|
3086
|
+
},
|
|
3087
|
+
{
|
|
3088
|
+
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
3089
|
+
useValue: CoreProfileSectionsHookProvider,
|
|
3090
|
+
multi: true,
|
|
3091
|
+
},
|
|
3092
|
+
{
|
|
3093
|
+
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
3094
|
+
useValue: SecurityPinSectionHookProvider,
|
|
3095
|
+
multi: true,
|
|
3096
|
+
},
|
|
3097
|
+
{
|
|
3098
|
+
provide: AXP_DISTRIBUTED_EVENT_LISTENER_PROVIDER,
|
|
3099
|
+
useValue: SecuritySectionsInsightsHookProvider,
|
|
3100
|
+
multi: true,
|
|
3101
|
+
},
|
|
3102
|
+
],
|
|
3103
|
+
}]
|
|
3104
|
+
}], ctorParameters: () => [] });
|
|
3105
|
+
|
|
3106
|
+
//#region ---- Login external provider slot ----
|
|
3107
|
+
/** Slot name for optional extension-provided login buttons (e.g. Google platform login). */
|
|
3108
|
+
const AUTH_LOGIN_EXTERNAL_PROVIDERS_SLOT = 'auth-login-external-providers';
|
|
3109
|
+
//#endregion
|
|
3110
|
+
|
|
3111
|
+
class AXPLoginExternalProvidersComponent {
|
|
3112
|
+
constructor() {
|
|
3113
|
+
this.slotName = AUTH_LOGIN_EXTERNAL_PROVIDERS_SLOT;
|
|
3114
|
+
this.slot = viewChild(AXPComponentSlotDirective, ...(ngDevMode ? [{ debugName: "slot" }] : /* istanbul ignore next */ []));
|
|
3115
|
+
}
|
|
3116
|
+
hasContent() {
|
|
3117
|
+
const slot = this.slot();
|
|
3118
|
+
return !!slot && !slot.isEmpty();
|
|
3119
|
+
}
|
|
3120
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLoginExternalProvidersComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3121
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.9", type: AXPLoginExternalProvidersComponent, isStandalone: true, selector: "axm-login-external-providers", viewQueries: [{ propertyName: "slot", first: true, predicate: AXPComponentSlotDirective, descendants: true, isSignal: true }], ngImport: i0, template: "<axp-component-slot\n class=\"flex flex-col gap-2 w-full mt-6\"\n #slot=\"slot\"\n [name]=\"slotName\"\n [host]=\"this\"\n></axp-component-slot>\n", dependencies: [{ kind: "ngmodule", type: AXPComponentSlotModule }, { kind: "directive", type: i1$3.AXPComponentSlotDirective, selector: "axp-component-slot", inputs: ["name", "host", "context"], exportAs: ["slot"] }] }); }
|
|
3122
|
+
}
|
|
3123
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLoginExternalProvidersComponent, decorators: [{
|
|
3124
|
+
type: Component,
|
|
3125
|
+
args: [{ selector: 'axm-login-external-providers', imports: [AXPComponentSlotModule], template: "<axp-component-slot\n class=\"flex flex-col gap-2 w-full mt-6\"\n #slot=\"slot\"\n [name]=\"slotName\"\n [host]=\"this\"\n></axp-component-slot>\n" }]
|
|
3126
|
+
}], propDecorators: { slot: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AXPComponentSlotDirective), { isSignal: true }] }] } });
|
|
3127
|
+
|
|
3128
|
+
//#endregion
|
|
3129
|
+
//#region ---- Login history service ----
|
|
3130
|
+
/** Loads sign-in records for the current user from device session storage. */
|
|
3131
|
+
class AXPLoginHistoryService {
|
|
3132
|
+
constructor() {
|
|
3133
|
+
//#region ---- Services & Dependencies ----
|
|
3134
|
+
this.sessionService = inject(AXPSessionService);
|
|
3135
|
+
this.entityService = inject(AXPEntityService);
|
|
3136
|
+
}
|
|
3137
|
+
//#endregion
|
|
3138
|
+
//#region ---- Public API ----
|
|
3139
|
+
async getEntries() {
|
|
3140
|
+
const userId = this.sessionService.user?.id;
|
|
3141
|
+
if (!userId) {
|
|
3142
|
+
return [];
|
|
3143
|
+
}
|
|
3144
|
+
const response = await this.entityService
|
|
3145
|
+
.withEntity(RootConfig$1.module.name, RootConfig$1.entities.deviceSessions.name)
|
|
3146
|
+
.data()
|
|
3147
|
+
.query({ skip: 0, take: 1000 });
|
|
3148
|
+
const items = response.items ?? [];
|
|
3149
|
+
return orderBy(items
|
|
3150
|
+
.filter((item) => item.userId === userId)
|
|
3151
|
+
.map((item) => ({
|
|
3152
|
+
id: item.id,
|
|
3153
|
+
device: item.deviceName ?? item.operatingSystem ?? '—',
|
|
3154
|
+
location: item.location ?? '—',
|
|
3155
|
+
ipAddress: item.ipAddress ?? '—',
|
|
3156
|
+
browser: item.browser ?? '—',
|
|
3157
|
+
status: String(item.status ?? 'unknown').toLowerCase(),
|
|
3158
|
+
lastLoginDate: item.lastLoginDate,
|
|
3159
|
+
})), [(entry) => new Date(entry.lastLoginDate).getTime()], ['desc']);
|
|
3160
|
+
}
|
|
3161
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLoginHistoryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
3162
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLoginHistoryService, providedIn: 'root' }); }
|
|
3163
|
+
}
|
|
3164
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLoginHistoryService, decorators: [{
|
|
3165
|
+
type: Injectable,
|
|
3166
|
+
args: [{ providedIn: 'root' }]
|
|
3167
|
+
}] });
|
|
3168
|
+
|
|
3169
|
+
/**
|
|
3170
|
+
* Generated bundle index. Do not edit.
|
|
3171
|
+
*/
|
|
3172
|
+
|
|
3173
|
+
export { AXMUserDeviceSessionsService as A, AXMAuthDataSourceProvider as B, AXMAuthErrorHandler as C, AXMAuthModule as D, AXMDefaultAuthConfigs as E, AXMUserActivityDataSourceDefinition as F, aggregateDailySessionCounts as G, buildUserActivityDataSourceItem as H, buildUserActivityWeeks as I, canActivateLogin as J, computeUserActivityStats as K, configAuthModule as L, countToActivityLevel as M, extractUserActivityEndDate as N, extractUserActivityStartDate as O, PROFILE_PAGE_GROUPS_HOOK as P, extractUserActivityUserId as Q, isUserProfileGroupComponent as R, PROFILE_PAGE_SECTIONS_HOOK as a, PROFILE_PAGE_HEADER_META_HOOK as b, resolveUserProfileSectionContent as c, PROFILE_ACTIVITY_CHART_COMPONENT_KEY as d, PROFILE_DELETE_ACCOUNT_DANGER_ZONE_COMPONENT_KEY as e, AXPIdleLockService as f, AXPUserAvatarSyncService as g, AXPAuthPasswordComplexityService as h, AXPAuthPasswordChangeTrackingService as i, getPasswordComplexityHintKey as j, getPasswordComplexityValidationMessageKey as k, validatePasswordAgainstComplexity as l, AXPLoginHistoryService as m, AXM_AUTH_CONFIG_TOKEN as n, AXM_LOGIN_GUARD_TOKEN as o, AXMAuthenticationTypes as p, AXPTenantChooserListComponent as q, resolveDefaultUserProfileSectionAction as r, AXPTenantChooserDropdownComponent as s, AXPAppChooserListComponent as t, resolveDefaultUserActivityDateRange as u, visibleUserProfileSectionActions as v, formatCompactActivityCount as w, AXPLoginExternalProvidersComponent as x, AUTH_LOGIN_EXTERNAL_PROVIDERS_SLOT as y, AUTH_USER_ACTIVITY_DEFAULT_WEEKS as z };
|
|
3174
|
+
//# sourceMappingURL=acorex-platform-module-auth-client-acorex-platform-module-auth-client-B7tIKOw6.mjs.map
|