@sdcorejs/angular 21.1.6 → 21.2.1
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/README.md +10 -0
- package/assets/scss/core/menu.scss +18 -0
- package/assets/scss/core/utilities/_misc.scss +15 -0
- package/assets/scss/sd-core.scss +1 -0
- package/fesm2022/sdcorejs-angular-components-anchor.mjs +37 -13
- package/fesm2022/sdcorejs-angular-components-anchor.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-api-contract-builder.mjs +3409 -0
- package/fesm2022/sdcorejs-angular-components-api-contract-builder.mjs.map +1 -0
- package/fesm2022/sdcorejs-angular-components-autoid-inspector.mjs +2 -2
- package/fesm2022/sdcorejs-angular-components-autoid-inspector.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-badge.mjs +27 -2
- package/fesm2022/sdcorejs-angular-components-badge.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-button.mjs +2 -2
- package/fesm2022/sdcorejs-angular-components-button.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-ckeditor-styles.mjs +1 -1
- package/fesm2022/sdcorejs-angular-components-ckeditor-styles.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-code-editor.mjs +3 -3
- package/fesm2022/sdcorejs-angular-components-code-editor.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-data-state.mjs +3 -3
- package/fesm2022/sdcorejs-angular-components-data-state.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-editor.mjs +2 -2
- package/fesm2022/sdcorejs-angular-components-editor.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-form-generic.mjs +324 -192
- package/fesm2022/sdcorejs-angular-components-form-generic.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-history.mjs +4 -4
- package/fesm2022/sdcorejs-angular-components-history.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-import-excel.mjs +13 -5
- package/fesm2022/sdcorejs-angular-components-import-excel.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-inform.mjs +3 -3
- package/fesm2022/sdcorejs-angular-components-inform.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-modal-resizable.mjs +164 -8
- package/fesm2022/sdcorejs-angular-components-modal-resizable.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-modal.mjs +30 -18
- package/fesm2022/sdcorejs-angular-components-modal.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-operator.mjs +2 -2
- package/fesm2022/sdcorejs-angular-components-operator.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-org-chart.mjs +60 -13
- package/fesm2022/sdcorejs-angular-components-org-chart.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-preview.mjs +27 -17
- package/fesm2022/sdcorejs-angular-components-preview.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-query-bar.mjs +192 -30
- package/fesm2022/sdcorejs-angular-components-query-bar.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-query-builder.mjs +187 -127
- package/fesm2022/sdcorejs-angular-components-query-builder.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-section.mjs +33 -2
- package/fesm2022/sdcorejs-angular-components-section.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-side-drawer.mjs +83 -24
- package/fesm2022/sdcorejs-angular-components-side-drawer.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-splitter.mjs +57 -21
- package/fesm2022/sdcorejs-angular-components-splitter.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-stepper.mjs +2 -2
- package/fesm2022/sdcorejs-angular-components-stepper.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-tab-router.mjs +68 -20
- package/fesm2022/sdcorejs-angular-components-tab-router.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-tab.mjs +3 -2
- package/fesm2022/sdcorejs-angular-components-tab.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-table.mjs +556 -268
- package/fesm2022/sdcorejs-angular-components-table.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-tree.mjs +80 -17
- package/fesm2022/sdcorejs-angular-components-tree.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components-upload-file.mjs +48 -13
- package/fesm2022/sdcorejs-angular-components-upload-file.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-components.mjs +7 -1
- package/fesm2022/sdcorejs-angular-components.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-configurations.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-directives.mjs +45 -8
- package/fesm2022/sdcorejs-angular-directives.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs +40 -29
- package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-checkbox.mjs +5 -5
- package/fesm2022/sdcorejs-angular-forms-checkbox.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs +67 -50
- package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-chip.mjs +57 -43
- package/fesm2022/sdcorejs-angular-forms-chip.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-date-range.mjs +232 -61
- package/fesm2022/sdcorejs-angular-forms-date-range.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-date.mjs +96 -24
- package/fesm2022/sdcorejs-angular-forms-date.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-datetime.mjs +131 -27
- package/fesm2022/sdcorejs-angular-forms-datetime.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-entity-picker.mjs +40 -16
- package/fesm2022/sdcorejs-angular-forms-entity-picker.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-input-color.mjs +16 -5
- package/fesm2022/sdcorejs-angular-forms-input-color.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-input-number.mjs +24 -9
- package/fesm2022/sdcorejs-angular-forms-input-number.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-input.mjs +58 -10
- package/fesm2022/sdcorejs-angular-forms-input.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-models.mjs +227 -6
- package/fesm2022/sdcorejs-angular-forms-models.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-radio.mjs +4 -4
- package/fesm2022/sdcorejs-angular-forms-radio.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-select.mjs +136 -38
- package/fesm2022/sdcorejs-angular-forms-select.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-switch.mjs +18 -3
- package/fesm2022/sdcorejs-angular-forms-switch.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-textarea.mjs +51 -62
- package/fesm2022/sdcorejs-angular-forms-textarea.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-time-range.mjs +57 -12
- package/fesm2022/sdcorejs-angular-forms-time-range.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-time.mjs +16 -5
- package/fesm2022/sdcorejs-angular-forms-time.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms-tree-select.mjs +100 -9
- package/fesm2022/sdcorejs-angular-forms-tree-select.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-forms.mjs +91 -46
- package/fesm2022/sdcorejs-angular-forms.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-handlers.mjs +18 -3
- package/fesm2022/sdcorejs-angular-handlers.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-i18n.mjs +895 -68
- package/fesm2022/sdcorejs-angular-i18n.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-interceptors.mjs +87 -29
- package/fesm2022/sdcorejs-angular-interceptors.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-modules-auth.mjs +25 -15
- package/fesm2022/sdcorejs-angular-modules-auth.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-modules-icon.mjs +2 -2
- package/fesm2022/sdcorejs-angular-modules-icon.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-modules-keycloak.mjs +32 -3
- package/fesm2022/sdcorejs-angular-modules-keycloak.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-modules-layout.mjs +342 -111
- package/fesm2022/sdcorejs-angular-modules-layout.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-modules-permission.mjs +177 -40
- package/fesm2022/sdcorejs-angular-modules-permission.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-pipes.mjs +62 -28
- package/fesm2022/sdcorejs-angular-pipes.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-api.mjs +123 -13
- package/fesm2022/sdcorejs-angular-services-api.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-cache.mjs +53 -3
- package/fesm2022/sdcorejs-angular-services-cache.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-confirm.mjs +25 -73
- package/fesm2022/sdcorejs-angular-services-confirm.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-excel.mjs +12 -10
- package/fesm2022/sdcorejs-angular-services-excel.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-loading.mjs +75 -84
- package/fesm2022/sdcorejs-angular-services-loading.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-notify.mjs +46 -5
- package/fesm2022/sdcorejs-angular-services-notify.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services-storage.mjs +4 -0
- package/fesm2022/sdcorejs-angular-services-storage.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-services.mjs +0 -1
- package/fesm2022/sdcorejs-angular-services.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-utilities-extensions.mjs +225 -80
- package/fesm2022/sdcorejs-angular-utilities-extensions.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular-utilities-models.mjs +1 -63
- package/fesm2022/sdcorejs-angular-utilities-models.mjs.map +1 -1
- package/fesm2022/sdcorejs-angular.mjs +17 -10
- package/fesm2022/sdcorejs-angular.mjs.map +1 -1
- package/package.json +36 -25
- package/types/sdcorejs-angular-components-anchor.d.ts +3 -1
- package/types/sdcorejs-angular-components-api-contract-builder.d.ts +694 -0
- package/types/sdcorejs-angular-components-badge.d.ts +2 -0
- package/types/sdcorejs-angular-components-ckeditor-styles.d.ts +1 -1
- package/types/sdcorejs-angular-components-form-generic.d.ts +44 -33
- package/types/sdcorejs-angular-components-import-excel.d.ts +1 -0
- package/types/sdcorejs-angular-components-mini-editor.d.ts +1 -1
- package/types/sdcorejs-angular-components-modal-resizable.d.ts +92 -0
- package/types/sdcorejs-angular-components-modal.d.ts +3 -2
- package/types/sdcorejs-angular-components-org-chart.d.ts +6 -0
- package/types/sdcorejs-angular-components-preview.d.ts +1 -0
- package/types/sdcorejs-angular-components-query-bar.d.ts +55 -17
- package/types/sdcorejs-angular-components-query-builder.d.ts +150 -98
- package/types/sdcorejs-angular-components-section.d.ts +5 -0
- package/types/sdcorejs-angular-components-tab-router.d.ts +19 -19
- package/types/sdcorejs-angular-components-table.d.ts +251 -109
- package/types/sdcorejs-angular-components-tree.d.ts +48 -30
- package/types/sdcorejs-angular-components-upload-file.d.ts +6 -3
- package/types/sdcorejs-angular-components.d.ts +7 -1
- package/types/sdcorejs-angular-configurations.d.ts +1 -2
- package/types/sdcorejs-angular-forms-autocomplete.d.ts +5 -2
- package/types/sdcorejs-angular-forms-chip-calendar.d.ts +10 -0
- package/types/sdcorejs-angular-forms-date-range.d.ts +14 -2
- package/types/sdcorejs-angular-forms-date.d.ts +13 -2
- package/types/sdcorejs-angular-forms-datetime.d.ts +13 -2
- package/types/sdcorejs-angular-forms-entity-picker.d.ts +9 -3
- package/types/sdcorejs-angular-forms-input-color.d.ts +4 -0
- package/types/sdcorejs-angular-forms-input-number.d.ts +3 -0
- package/types/sdcorejs-angular-forms-input.d.ts +3 -0
- package/types/sdcorejs-angular-forms-models.d.ts +119 -4
- package/types/sdcorejs-angular-forms-select.d.ts +27 -4
- package/types/sdcorejs-angular-forms-switch.d.ts +7 -1
- package/types/sdcorejs-angular-forms-textarea.d.ts +5 -3
- package/types/sdcorejs-angular-forms-time-range.d.ts +22 -1
- package/types/sdcorejs-angular-forms-tree-select.d.ts +36 -5
- package/types/sdcorejs-angular-forms.d.ts +22 -13
- package/types/sdcorejs-angular-i18n.d.ts +175 -16
- package/types/sdcorejs-angular-interceptors.d.ts +13 -3
- package/types/sdcorejs-angular-modules-auth.d.ts +1 -1
- package/types/sdcorejs-angular-modules-keycloak.d.ts +1 -0
- package/types/sdcorejs-angular-modules-layout.d.ts +94 -28
- package/types/sdcorejs-angular-modules-permission.d.ts +66 -4
- package/types/sdcorejs-angular-pipes.d.ts +27 -5
- package/types/sdcorejs-angular-services-api.d.ts +21 -1
- package/types/sdcorejs-angular-services-cache.d.ts +11 -1
- package/types/sdcorejs-angular-services-notify.d.ts +1 -1
- package/types/sdcorejs-angular-services.d.ts +0 -1
- package/types/sdcorejs-angular-utilities-extensions.d.ts +83 -61
- package/types/sdcorejs-angular-utilities-models.d.ts +2 -76
- package/types/sdcorejs-angular.d.ts +11 -2
- package/fesm2022/sdcorejs-angular-components-base.mjs +0 -23
- package/fesm2022/sdcorejs-angular-components-base.mjs.map +0 -1
- package/fesm2022/sdcorejs-angular-components-document-builder.mjs +0 -5038
- package/fesm2022/sdcorejs-angular-components-document-builder.mjs.map +0 -1
- package/fesm2022/sdcorejs-angular-models.mjs +0 -6
- package/fesm2022/sdcorejs-angular-models.mjs.map +0 -1
- package/fesm2022/sdcorejs-angular-services-docx.mjs +0 -291
- package/fesm2022/sdcorejs-angular-services-docx.mjs.map +0 -1
- package/fesm2022/sdcorejs-angular-services-license.mjs +0 -97
- package/fesm2022/sdcorejs-angular-services-license.mjs.map +0 -1
- package/types/sdcorejs-angular-components-base.d.ts +0 -11
- package/types/sdcorejs-angular-components-document-builder.d.ts +0 -252
- package/types/sdcorejs-angular-models.d.ts +0 -2
- package/types/sdcorejs-angular-services-docx.d.ts +0 -27
- package/types/sdcorejs-angular-services-license.d.ts +0 -12
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject, Injectable, TemplateRef, ViewContainerRef, input, effect, Directive } from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, isDevMode, Injectable, TemplateRef, ViewContainerRef, input, effect, Directive } from '@angular/core';
|
|
3
3
|
import { SdCacheService } from '@sdcorejs/angular/services/cache';
|
|
4
|
-
import { ArrayUtilities } from '@sdcorejs/
|
|
4
|
+
import { ArrayUtilities } from '@sdcorejs/utils/fns';
|
|
5
5
|
import { resolveMaybeAsync } from '@sdcorejs/utils/models';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -19,14 +19,31 @@ import { resolveMaybeAsync } from '@sdcorejs/utils/models';
|
|
|
19
19
|
*/
|
|
20
20
|
const SD_PERMISSION_CONFIGURATION = new InjectionToken('sd-permission.configuration');
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Opt-out TƯỜNG MINH: "chỗ này cố ý không yêu cầu quyền nào".
|
|
24
|
+
*
|
|
25
|
+
* Dùng cho `route.data.permission`, `[sdPermission]` hoặc `hasPermission(...)` khi một route/phần tử
|
|
26
|
+
* thật sự công khai. Mọi giá trị rỗng khác (`undefined`, `null`, `''`, `[]`) đều bị coi là CẤU HÌNH
|
|
27
|
+
* THIẾU và bị từ chối — xem `SdPermissionService.hasPermission`.
|
|
28
|
+
*/
|
|
29
|
+
const SD_PERMISSION_PUBLIC = '__SD_PERMISSION_PUBLIC__';
|
|
30
|
+
/**
|
|
31
|
+
* why: khoá cache cũ là một UUID cố định (`212a51fa-…`) dùng chung cho MỌI key, nên một entry
|
|
32
|
+
* sessionStorage duy nhất chứa quyền của tất cả profile. Đổi sang prefix có namespace + tách theo key
|
|
33
|
+
* để `invalidate(key)` xoá đúng phần của nó và không đụng key khác.
|
|
34
|
+
*/
|
|
35
|
+
const PERSISTED_CACHE_PREFIX = 'sd-permission.codes.';
|
|
22
36
|
class SdPermissionService {
|
|
23
37
|
#permissionMapByKey = {};
|
|
38
|
+
// why: danh sách quyền đã resolve giờ CHỈ sống trong bộ nhớ. Trước đây nó luôn được mirror xuống
|
|
39
|
+
// sessionStorage, tức mọi script trên cùng origin đọc/ghi được — chỉ cần chạy được một dòng JS là
|
|
40
|
+
// tự cấp cho mình quyền trên UI. Tệ hơn: `#loadedKeys` vốn nằm trong bộ nhớ nên sau reload service
|
|
41
|
+
// vẫn gọi lại `loadPermissions()`, entry sessionStorage kia gần như không mang lại lợi ích gì mà
|
|
42
|
+
// chỉ là bề mặt tấn công. Ai thật sự cần cache qua reload thì bật `persistCache` cho từng key.
|
|
43
|
+
#permissionsByKey = {};
|
|
24
44
|
#configuration = inject(SD_PERMISSION_CONFIGURATION);
|
|
25
45
|
#cacheService = inject(SdCacheService);
|
|
26
|
-
#
|
|
27
|
-
type: 'session',
|
|
28
|
-
default: {},
|
|
29
|
-
});
|
|
46
|
+
#persistedCaches = new Map();
|
|
30
47
|
#loadedKeys = new Set();
|
|
31
48
|
constructor() {
|
|
32
49
|
this.#validateDuplicateConfigKeys();
|
|
@@ -65,43 +82,68 @@ class SdPermissionService {
|
|
|
65
82
|
}
|
|
66
83
|
return key;
|
|
67
84
|
};
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Chỉ tạo handle cache khi key đó bật `persistCache`. Handle được memo hoá để `reset()` /
|
|
87
|
+
* `invalidate()` gọi được `remove()` lên đúng entry đã ghi.
|
|
88
|
+
*/
|
|
89
|
+
#getPersistedCache = (normalizedKey) => {
|
|
90
|
+
const existing = this.#persistedCaches.get(normalizedKey);
|
|
91
|
+
if (existing) {
|
|
92
|
+
return existing;
|
|
93
|
+
}
|
|
94
|
+
const handle = this.#cacheService.create(`${PERSISTED_CACHE_PREFIX}${normalizedKey}`, {
|
|
95
|
+
type: 'session',
|
|
96
|
+
default: [],
|
|
74
97
|
});
|
|
98
|
+
this.#persistedCaches.set(normalizedKey, handle);
|
|
99
|
+
return handle;
|
|
100
|
+
};
|
|
101
|
+
#setPermissionsForKey = (normalizedKey, permissions, persist) => {
|
|
102
|
+
const distinctPermissions = ArrayUtilities.distinct(permissions || []);
|
|
103
|
+
this.#permissionsByKey[normalizedKey] = distinctPermissions;
|
|
75
104
|
const permissionMap = {};
|
|
76
105
|
distinctPermissions.forEach(permission => {
|
|
77
106
|
permissionMap[permission] = true;
|
|
78
107
|
});
|
|
79
108
|
this.#permissionMapByKey[normalizedKey] = permissionMap;
|
|
109
|
+
if (persist) {
|
|
110
|
+
this.#getPersistedCache(normalizedKey).set(distinctPermissions);
|
|
111
|
+
}
|
|
80
112
|
};
|
|
81
113
|
loadPermissions = async (key) => {
|
|
82
114
|
const effectiveKey = this.#getEffectivePermissionKey(key);
|
|
83
115
|
const normalizedKey = this.#normalizeKey(effectiveKey);
|
|
84
116
|
if (this.#loadedKeys.has(normalizedKey)) {
|
|
85
|
-
return this.#permissionsByKey
|
|
117
|
+
return this.#permissionsByKey[normalizedKey] ?? [];
|
|
86
118
|
}
|
|
87
119
|
const configuration = this.#getConfigurationByKey(effectiveKey);
|
|
88
120
|
if (!configuration) {
|
|
89
|
-
this.#setPermissionsForKey(normalizedKey, []);
|
|
121
|
+
this.#setPermissionsForKey(normalizedKey, [], false);
|
|
90
122
|
this.#loadedKeys.add(normalizedKey);
|
|
91
123
|
return [];
|
|
92
124
|
}
|
|
125
|
+
const persist = configuration.persistCache === true;
|
|
126
|
+
if (persist) {
|
|
127
|
+
// why: chỉ khi consumer bật `persistCache` mới đọc lại bản mirror — mặc định không chạm storage.
|
|
128
|
+
const cached = this.#getPersistedCache(normalizedKey).get();
|
|
129
|
+
if (cached?.length) {
|
|
130
|
+
this.#setPermissionsForKey(normalizedKey, cached, false);
|
|
131
|
+
this.#loadedKeys.add(normalizedKey);
|
|
132
|
+
return this.#permissionsByKey[normalizedKey] ?? [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
93
135
|
try {
|
|
94
136
|
const permissions = await resolveMaybeAsync(configuration.loadPermissions());
|
|
95
|
-
this.#setPermissionsForKey(normalizedKey, permissions || []);
|
|
137
|
+
this.#setPermissionsForKey(normalizedKey, permissions || [], persist);
|
|
96
138
|
}
|
|
97
139
|
catch (err) {
|
|
98
140
|
console.error(err);
|
|
99
|
-
this.#setPermissionsForKey(normalizedKey, []);
|
|
141
|
+
this.#setPermissionsForKey(normalizedKey, [], false);
|
|
100
142
|
}
|
|
101
143
|
finally {
|
|
102
144
|
this.#loadedKeys.add(normalizedKey);
|
|
103
145
|
}
|
|
104
|
-
return this.#permissionsByKey
|
|
146
|
+
return this.#permissionsByKey[normalizedKey] ?? [];
|
|
105
147
|
};
|
|
106
148
|
loadAllPermissions = async () => {
|
|
107
149
|
const configurations = this.#getConfigurations();
|
|
@@ -111,19 +153,78 @@ class SdPermissionService {
|
|
|
111
153
|
}
|
|
112
154
|
await Promise.all(configurations.map(config => this.loadPermissions(config.key)));
|
|
113
155
|
};
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Xoá TOÀN BỘ quyền đã nạp (mọi key) — cả map trong bộ nhớ, cờ "đã nạp" lẫn bản mirror
|
|
158
|
+
* `sessionStorage` của các key bật `persistCache`.
|
|
159
|
+
*
|
|
160
|
+
* ⚠️ BẮT BUỘC gọi khi signout. Service là singleton `providedIn: 'root'`, nên trong một SPA
|
|
161
|
+
* signout → signin (không reload trang) mà không reset thì `loadPermissions()` sẽ short-circuit
|
|
162
|
+
* theo key đã cache và user MỚI thừa hưởng nguyên bộ quyền của user CŨ.
|
|
163
|
+
*
|
|
164
|
+
* ```ts
|
|
165
|
+
* auth.signout$?.subscribe(() => permissionService.reset());
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
reset = () => {
|
|
169
|
+
// why: sau reload, service chưa tạo handle cho key nào cả nhưng entry sessionStorage của phiên
|
|
170
|
+
// trước vẫn còn. Tạo handle cho mọi key có `persistCache` trước khi remove để reset thật sự sạch.
|
|
171
|
+
this.#getConfigurations()
|
|
172
|
+
.filter(config => config.persistCache === true)
|
|
173
|
+
.forEach(config => this.#getPersistedCache(this.#normalizeKey(config.key)));
|
|
174
|
+
this.#persistedCaches.forEach(cache => cache.remove());
|
|
175
|
+
this.#persistedCaches.clear();
|
|
176
|
+
this.#loadedKeys.clear();
|
|
177
|
+
this.#permissionMapByKey = {};
|
|
178
|
+
this.#permissionsByKey = {};
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Xoá quyền đã nạp của MỘT key (mặc định là key portal `undefined`), buộc lần
|
|
182
|
+
* `loadPermissions(key)` kế tiếp gọi lại loader. Dùng khi chỉ một profile/tenant đổi quyền;
|
|
183
|
+
* signout thì dùng {@link reset}.
|
|
184
|
+
*/
|
|
185
|
+
invalidate = (key) => {
|
|
186
|
+
const effectiveKey = this.#getEffectivePermissionKey(key);
|
|
187
|
+
const normalizedKey = this.#normalizeKey(effectiveKey);
|
|
188
|
+
this.#loadedKeys.delete(normalizedKey);
|
|
189
|
+
delete this.#permissionMapByKey[normalizedKey];
|
|
190
|
+
delete this.#permissionsByKey[normalizedKey];
|
|
191
|
+
// why: tạo handle nếu chưa có, để xoá được cả entry còn sót lại từ phiên trước khi reload.
|
|
192
|
+
if (this.#getConfigurationByKey(effectiveKey)?.persistCache === true) {
|
|
193
|
+
this.#getPersistedCache(normalizedKey).remove();
|
|
117
194
|
}
|
|
195
|
+
this.#persistedCaches.delete(normalizedKey);
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Kiểm tra quyền (OR cho mảng).
|
|
199
|
+
*
|
|
200
|
+
* ⚠️ BREAKING: đầu vào rỗng (`undefined` / `null` / `''` / `[]`) giờ trả `false`.
|
|
201
|
+
* Muốn "không yêu cầu quyền" thì phải nói ra bằng {@link SD_PERMISSION_PUBLIC}.
|
|
202
|
+
*/
|
|
203
|
+
hasPermission = (permission, key) => {
|
|
118
204
|
const effectiveKey = this.#getEffectivePermissionKey(key);
|
|
119
205
|
const configuration = this.#getConfigurationByKey(effectiveKey);
|
|
120
206
|
if (configuration?.disabled) {
|
|
121
207
|
return true;
|
|
122
208
|
}
|
|
209
|
+
const requested = (Array.isArray(permission) ? permission : [permission]).filter((value) => typeof value === 'string' && value.trim() !== '');
|
|
210
|
+
// why: opt-out phải TƯỜNG MINH. Chỉ sentinel này mới nghĩa là "cố ý công khai".
|
|
211
|
+
if (requested.includes(SD_PERMISSION_PUBLIC)) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
if (!requested.length) {
|
|
215
|
+
// why: code cũ trả `true` cho mọi đầu vào rỗng, nên một route gõ sai key `data` (`permision`),
|
|
216
|
+
// một `[sdPermission]` bind nhầm sang biến undefined, hay một mã quyền rỗng do API trả về đều
|
|
217
|
+
// được cấp quyền âm thầm — không phân biệt được "cố ý không giới hạn" với "cấu hình hỏng".
|
|
218
|
+
// Giờ mặc định là TỪ CHỐI; ca công khai thật sự phải dùng `SD_PERMISSION_PUBLIC`.
|
|
219
|
+
if (isDevMode()) {
|
|
220
|
+
console.error('[Permission] Access denied: an empty permission was checked. ' +
|
|
221
|
+
'Pass a real permission code, or SD_PERMISSION_PUBLIC if this route/element is intentionally public.', { permission, key });
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
123
225
|
const normalizedKey = this.#normalizeKey(effectiveKey);
|
|
124
226
|
const permissionMap = this.#permissionMapByKey[normalizedKey] || {};
|
|
125
|
-
|
|
126
|
-
return permissions.some(val => permissionMap[val]);
|
|
227
|
+
return requested.some(value => permissionMap[value] === true);
|
|
127
228
|
};
|
|
128
229
|
getToken = async (key) => {
|
|
129
230
|
const effectiveKey = this.#getEffectivePermissionKey(key);
|
|
@@ -137,7 +238,17 @@ class SdPermissionService {
|
|
|
137
238
|
}
|
|
138
239
|
return token;
|
|
139
240
|
};
|
|
140
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Giải mã phần payload của JWT hiện tại và trả về claim **CHƯA ĐƯỢC XÁC THỰC**.
|
|
243
|
+
*
|
|
244
|
+
* ⚠️ KHÔNG kiểm chữ ký, KHÔNG kiểm `exp`/`nbf`/`aud`/`iss`. Đây thuần tuý là base64url-decode: bất
|
|
245
|
+
* kỳ ai cũng tự tạo được một token có claim tuỳ ý và hàm này sẽ vui vẻ trả về. Vì vậy kết quả CHỈ
|
|
246
|
+
* dùng cho mục đích hiển thị (tên, avatar, tenant đang chọn…) và TUYỆT ĐỐI không được dùng để ra
|
|
247
|
+
* quyết định phân quyền — quyền phải do backend xác thực chữ ký rồi trả về.
|
|
248
|
+
*
|
|
249
|
+
* Trả `null` khi không có token hoặc payload hỏng.
|
|
250
|
+
*/
|
|
251
|
+
readUnverifiedTokenClaims = async (key) => {
|
|
141
252
|
const token = await this.getToken(key);
|
|
142
253
|
if (!token) {
|
|
143
254
|
return null;
|
|
@@ -156,6 +267,14 @@ class SdPermissionService {
|
|
|
156
267
|
return null;
|
|
157
268
|
}
|
|
158
269
|
};
|
|
270
|
+
/**
|
|
271
|
+
* @deprecated Tên `decodeToken` gợi ý một thao tác đã được kiểm chứng, trong khi thực tế payload
|
|
272
|
+
* hoàn toàn KHÔNG được xác thực. Dùng {@link readUnverifiedTokenClaims} — alias này chỉ còn để
|
|
273
|
+
* consumer cũ không vỡ và sẽ bị gỡ ở release sau.
|
|
274
|
+
*/
|
|
275
|
+
decodeToken = async (key) => {
|
|
276
|
+
return this.readUnverifiedTokenClaims(key);
|
|
277
|
+
};
|
|
159
278
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SdPermissionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
160
279
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SdPermissionService, providedIn: 'root' });
|
|
161
280
|
}
|
|
@@ -176,12 +295,10 @@ class SdPermissionDirective {
|
|
|
176
295
|
const permission = this.sdPermission();
|
|
177
296
|
const permissionKey = this.sdPermissionKey();
|
|
178
297
|
this.#viewContainerRef.clear();
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
// Kiểm tra permission theo key (nếu có)
|
|
298
|
+
// why: code cũ render vô điều kiện khi `permission` rỗng, nên `*sdPermission="perm"` với `perm`
|
|
299
|
+
// undefined (typo tên biến, dữ liệu chưa về, mã quyền rỗng từ API) hiện nút cho tất cả mọi
|
|
300
|
+
// người. Giờ mọi quyết định đều đi qua `hasPermission` — nó fail closed và chỉ chấp nhận
|
|
301
|
+
// `SD_PERMISSION_PUBLIC` như opt-out tường minh, nên không còn nhánh render ngầm nào nữa.
|
|
185
302
|
if (this.#permissionService.hasPermission(permission, permissionKey)) {
|
|
186
303
|
this.#viewContainerRef.createEmbeddedView(this.#templateRef);
|
|
187
304
|
}
|
|
@@ -207,24 +324,44 @@ class SdPermissionGuard {
|
|
|
207
324
|
}
|
|
208
325
|
return Array.isArray(config) ? config : [config];
|
|
209
326
|
};
|
|
327
|
+
/** Gọi `onForbiden` của config khớp key (hoặc config portal) rồi chặn điều hướng. */
|
|
328
|
+
#deny = (permissionKey) => {
|
|
329
|
+
const configurations = this.#getConfigurations();
|
|
330
|
+
const onForbiden = configurations
|
|
331
|
+
.filter(config => config.key === permissionKey || (permissionKey !== undefined && config.key === undefined))
|
|
332
|
+
.map(config => config.onForbiden)
|
|
333
|
+
.find(val => !!val);
|
|
334
|
+
onForbiden?.();
|
|
335
|
+
return false;
|
|
336
|
+
};
|
|
210
337
|
canActivate = async (next, state) => {
|
|
211
338
|
// Guard ở layer portal: preload toàn bộ permission theo tất cả key đã cấu hình
|
|
212
339
|
await this.permissionService.loadAllPermissions().catch(console.error);
|
|
213
340
|
return true;
|
|
214
341
|
};
|
|
215
342
|
canActivateChild = async (activatedRoute, state) => {
|
|
216
|
-
const
|
|
217
|
-
const permissionKey =
|
|
218
|
-
|
|
343
|
+
const data = activatedRoute.data ?? {};
|
|
344
|
+
const permissionKey = data['permissionKey'];
|
|
345
|
+
// why: `canActivateChild` chạy cho MỌI route con, nên một route quên khai báo `data.permission`
|
|
346
|
+
// hoặc gõ sai key (`permision`, `permissions`) trước đây rơi thẳng vào nhánh "rỗng ⇒ cho qua" của
|
|
347
|
+
// `hasPermission` và được cấp quyền âm thầm. Không khai báo giờ = TỪ CHỐI; route thật sự công khai
|
|
348
|
+
// phải nói ra bằng `data: { permission: SD_PERMISSION_PUBLIC }`.
|
|
349
|
+
if (!Object.prototype.hasOwnProperty.call(data, 'permission')) {
|
|
350
|
+
if (isDevMode()) {
|
|
351
|
+
console.error(`[Permission] Access denied for "${state?.url ?? ''}": route data has no "permission" entry. ` +
|
|
352
|
+
'Declare data.permission with a permission code, or SD_PERMISSION_PUBLIC if the route is intentionally public. ' +
|
|
353
|
+
'Check for a typo in the data key.');
|
|
354
|
+
}
|
|
355
|
+
// why: KHÔNG gọi `onForbiden()` ở nhánh này. `onForbiden` thường điều hướng sang trang forbidden;
|
|
356
|
+
// nếu chính trang đó lại quên khai báo `data.permission` thì nó cũng bị từ chối và gọi tiếp
|
|
357
|
+
// `onForbiden` → vòng lặp redirect vô tận, treo app. Thiếu khai báo là LỖI CẤU HÌNH, không phải
|
|
358
|
+
// "hết quyền": chặn im lặng + log dev là đủ, và một cấu hình sai không bao giờ tự đệ quy được.
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
if (this.permissionService.hasPermission(data['permission'], permissionKey)) {
|
|
219
362
|
return true;
|
|
220
363
|
}
|
|
221
|
-
|
|
222
|
-
const onForbiden = configurations
|
|
223
|
-
.filter(config => config.key === permissionKey || (permissionKey !== undefined && config.key === undefined))
|
|
224
|
-
.map(config => config.onForbiden)
|
|
225
|
-
.find(val => !!val);
|
|
226
|
-
onForbiden?.();
|
|
227
|
-
return false;
|
|
364
|
+
return this.#deny(permissionKey);
|
|
228
365
|
};
|
|
229
366
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SdPermissionGuard, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
230
367
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SdPermissionGuard, providedIn: 'root' });
|
|
@@ -238,5 +375,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
|
|
|
238
375
|
* Generated bundle index. Do not edit.
|
|
239
376
|
*/
|
|
240
377
|
|
|
241
|
-
export { SD_PERMISSION_CONFIGURATION, SdPermissionDirective, SdPermissionGuard, SdPermissionService };
|
|
378
|
+
export { SD_PERMISSION_CONFIGURATION, SD_PERMISSION_PUBLIC, SdPermissionDirective, SdPermissionGuard, SdPermissionService };
|
|
242
379
|
//# sourceMappingURL=sdcorejs-angular-modules-permission.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdcorejs-angular-modules-permission.mjs","sources":["../../../projects/sdcorejs-angular/modules/permission/src/configurations/permission.configuration.ts","../../../projects/sdcorejs-angular/modules/permission/src/services/permission.service.ts","../../../projects/sdcorejs-angular/modules/permission/src/directives/permission.directive.ts","../../../projects/sdcorejs-angular/modules/permission/src/guards/permission.guard.ts","../../../projects/sdcorejs-angular/modules/permission/sdcorejs-angular-modules-permission.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\nimport { MaybeAsync } from '@sdcorejs/utils/models';\n\n/**\n * Cấu hình trung tâm cho module permission.\n *\n * Luồng hoạt động chính:\n * 1. `loadPermissions(key?)` ở service được gọi để lấy danh sách mã quyền theo từng `key` cấu hình.\n * 2. Guard/service đối chiếu mã quyền theo route metadata.\n * 3. Khi không đủ quyền, callback `onForbiden()` sẽ được gọi (nếu có).\n *\n * Lưu ý:\n * - `SD_PERMISSION_CONFIGURATION` hỗ trợ cả cấu hình đơn và mảng cấu hình (`multi: true`).\n * - Nếu cần tạm bỏ qua kiểm tra quyền (POC/UAT cục bộ), đặt `disabled = true`.\n */\nexport interface ISdPermissionConfiguration {\n /**\n * Khóa định danh cấu hình.\n * Dùng để phân biệt khi hệ thống mở rộng theo nhiều profile permission.\n *\n * Lưu ý: `undefined` cũng được xem là một key hợp lệ (cấu hình mặc định).\n */\n key?: string;\n\n /**\n * Bật/tắt kiểm tra permission toàn cục.\n * - `true`: bỏ qua kiểm tra quyền.\n * - `false | undefined`: kiểm tra quyền theo cấu hình route.\n */\n disabled?: boolean;\n\n /**\n * Trả về danh sách mã quyền của user hiện tại.\n * Có thể đồng bộ hoặc bất đồng bộ.\n *\n * Ví dụ giá trị trả về:\n * - `['PRODUCT_C_EMPLOYEE_VIEW', 'PRODUCT_C_EMPLOYEE_UPDATE']`\n */\n loadPermissions: () => MaybeAsync<string[]>;\n\n /**\n * Callback xử lý khi user không có quyền truy cập URL hiện tại.\n * Thường dùng để điều hướng sang trang forbidden hoặc hiển thị thông báo.\n *\n * Giữ nguyên tên `onForbiden` để tương thích API hiện tại.\n */\n onForbiden?: () => void;\n\n /**\n * Cung cấp access token hiện tại cho các tác vụ liên quan permission.\n * Hỗ trợ trả về đồng bộ, Promise hoặc Observable.\n */\n getToken?: () => MaybeAsync<string | undefined | null>;\n}\n\n/**\n * InjectionToken cho cấu hình permission.\n *\n * Ví dụ provider:\n * {\n * provide: SD_PERMISSION_CONFIGURATION,\n * useValue: {\n * disabled: false,\n * loadPermissions: () => ['SAMPLE_C_EMPLOYEE_VIEW'],\n * onForbiden: () => router.navigateByUrl('/layout/forbidden')\n * }\n * }\n */\nexport const SD_PERMISSION_CONFIGURATION = new InjectionToken<ISdPermissionConfiguration | ISdPermissionConfiguration[]>(\n 'sd-permission.configuration'\n);\n","import { inject, Injectable } from '@angular/core';\nimport { SdCache, SdCacheService } from '@sdcorejs/angular/services/cache';\nimport { ArrayUtilities } from '@sdcorejs/angular/utilities/extensions';\nimport { ISdPermissionConfiguration, SD_PERMISSION_CONFIGURATION } from '../configurations';\nimport { MaybeAsync, resolveMaybeAsync } from '@sdcorejs/utils/models';\n\n@Injectable({ providedIn: 'root' })\nexport class SdPermissionService {\n #permissionMapByKey: Record<string, Record<string, boolean>> = {};\n readonly #configuration = inject<ISdPermissionConfiguration | ISdPermissionConfiguration[]>(SD_PERMISSION_CONFIGURATION);\n readonly #cacheService = inject(SdCacheService);\n #permissionsByKey: SdCache<Record<string, string[]>> = this.#cacheService.create<Record<string, string[]>>(\n '212a51fa-38d5-43b2-bd46-922d85950ba3',\n {\n type: 'session',\n default: {},\n }\n );\n readonly #loadedKeys = new Set<string>();\n\n constructor() {\n this.#validateDuplicateConfigKeys();\n }\n\n #getConfigurations = (): ISdPermissionConfiguration[] => {\n const config = this.#configuration;\n if (!config) {\n return [];\n }\n return Array.isArray(config) ? config : [config];\n };\n\n #normalizeKey = (key?: string): string => {\n return key === undefined ? '__undefined__' : key;\n };\n\n #validateDuplicateConfigKeys = (): void => {\n const seen = new Set<string>();\n for (const config of this.#getConfigurations()) {\n const normalizedKey = this.#normalizeKey(config.key);\n if (seen.has(normalizedKey)) {\n const keyLabel = config.key === undefined ? 'undefined' : config.key;\n throw new Error(`[Permission] Duplicate permission configuration key: ${keyLabel}`);\n }\n seen.add(normalizedKey);\n }\n };\n\n #getConfigurationByKey = (key?: string): ISdPermissionConfiguration | undefined => {\n return this.#getConfigurations().find(config => config.key === key);\n };\n\n #getEffectivePermissionKey = (key?: string): string | undefined => {\n if (this.#getConfigurationByKey(key)) {\n return key;\n }\n\n // Portal-level config uses key = undefined and acts as default fallback\n if (key !== undefined && this.#getConfigurationByKey(undefined)) {\n return undefined;\n }\n\n return key;\n };\n\n #setPermissionsForKey = (normalizedKey: string, permissions: string[]): void => {\n const distinctPermissions = ArrayUtilities.distinct(permissions || []);\n const current = this.#permissionsByKey.get() || {};\n this.#permissionsByKey.set({\n ...current,\n [normalizedKey]: distinctPermissions,\n });\n\n const permissionMap: Record<string, boolean> = {};\n distinctPermissions.forEach(permission => {\n permissionMap[permission] = true;\n });\n this.#permissionMapByKey[normalizedKey] = permissionMap;\n };\n\n loadPermissions = async (key?: string): Promise<string[]> => {\n const effectiveKey = this.#getEffectivePermissionKey(key);\n const normalizedKey = this.#normalizeKey(effectiveKey);\n if (this.#loadedKeys.has(normalizedKey)) {\n return this.#permissionsByKey.get()?.[normalizedKey] || [];\n }\n\n const configuration = this.#getConfigurationByKey(effectiveKey);\n if (!configuration) {\n this.#setPermissionsForKey(normalizedKey, []);\n this.#loadedKeys.add(normalizedKey);\n return [];\n }\n\n try {\n const permissions: string[] = await resolveMaybeAsync(configuration.loadPermissions());\n this.#setPermissionsForKey(normalizedKey, permissions || []);\n } catch (err) {\n console.error(err);\n this.#setPermissionsForKey(normalizedKey, []);\n } finally {\n this.#loadedKeys.add(normalizedKey);\n }\n\n return this.#permissionsByKey.get()?.[normalizedKey] || [];\n };\n\n loadAllPermissions = async (): Promise<void> => {\n const configurations = this.#getConfigurations();\n if (!configurations.length) {\n await this.loadPermissions(undefined);\n return;\n }\n\n await Promise.all(configurations.map(config => this.loadPermissions(config.key)));\n };\n\n hasPermission = (permission: string | string[], key?: string) => {\n if (!permission?.toString()) {\n return true;\n }\n\n const effectiveKey = this.#getEffectivePermissionKey(key);\n const configuration = this.#getConfigurationByKey(effectiveKey);\n if (configuration?.disabled) {\n return true;\n }\n\n const normalizedKey = this.#normalizeKey(effectiveKey);\n const permissionMap = this.#permissionMapByKey[normalizedKey] || {};\n const permissions = Array.isArray(permission) ? permission : [permission];\n return permissions.some(val => permissionMap[val]);\n };\n\n getToken = async (key?: string) => {\n const effectiveKey = this.#getEffectivePermissionKey(key);\n const getToken = this.#getConfigurationByKey(effectiveKey)?.getToken as (() => MaybeAsync<string | undefined | null>) | undefined;\n if (!getToken) {\n throw new Error('[Permission] Method getToken');\n }\n\n const token = await resolveMaybeAsync(getToken());\n if (token === '') {\n return undefined;\n }\n return token;\n };\n\n decodeToken = async <T>(key?: string): Promise<T | null> => {\n const token = await this.getToken(key);\n if (!token) {\n return null;\n }\n try {\n const payload = token.split('.')[1];\n const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');\n const jsonPayload = decodeURIComponent(\n atob(base64)\n .split('')\n .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))\n .join('')\n );\n return JSON.parse(jsonPayload);\n } catch (error) {\n console.error('Invalid token', error);\n return null;\n }\n };\n}\n","import { Directive, effect, inject, input, TemplateRef, ViewContainerRef } from '@angular/core';\nimport { SdPermissionService } from '../services';\n\n@Directive({\n selector: '[sdPermission]',\n})\nexport class SdPermissionDirective {\n readonly #templateRef: TemplateRef<Record<string, never>> = inject(TemplateRef);\n readonly #viewContainerRef = inject(ViewContainerRef);\n readonly #permissionService = inject(SdPermissionService);\n\n // Nếu là mảng thì chỉ cần có 1 permission trong mảng đó xem như có quyền\n readonly sdPermission = input<string | string[] | undefined | null>(undefined);\n readonly sdPermissionKey = input<string | undefined>(undefined);\n\n constructor() {\n effect(() => {\n const permission = this.sdPermission();\n const permissionKey = this.sdPermissionKey();\n\n this.#viewContainerRef.clear();\n\n // Nếu không gắn permission thì render\n if (!permission?.toString()) {\n this.#viewContainerRef.createEmbeddedView(this.#templateRef);\n return;\n }\n\n // Kiểm tra permission theo key (nếu có)\n if (this.#permissionService.hasPermission(permission, permissionKey)) {\n this.#viewContainerRef.createEmbeddedView(this.#templateRef);\n }\n });\n }\n}\n","import { inject, Injectable } from '@angular/core';\nimport { ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot } from '@angular/router';\nimport { ISdPermissionConfiguration, SD_PERMISSION_CONFIGURATION } from '../configurations';\nimport { SdPermissionService } from '../services';\n\n@Injectable({ providedIn: 'root' })\nexport class SdPermissionGuard implements CanActivate, CanActivateChild {\n private readonly configuration: ISdPermissionConfiguration | ISdPermissionConfiguration[] = inject(SD_PERMISSION_CONFIGURATION);\n private readonly permissionService = inject(SdPermissionService);\n\n #getConfigurations = (): ISdPermissionConfiguration[] => {\n const config = this.configuration;\n if (!config) {\n return [];\n }\n return Array.isArray(config) ? config : [config];\n };\n\n canActivate = async (next: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {\n // Guard ở layer portal: preload toàn bộ permission theo tất cả key đã cấu hình\n await this.permissionService.loadAllPermissions().catch(console.error);\n return true;\n };\n\n canActivateChild = async (activatedRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {\n const permission = activatedRoute.data['permission'];\n const permissionKey = activatedRoute.data?.['permissionKey'] as string | undefined;\n if (this.permissionService.hasPermission(permission, permissionKey)) {\n return true;\n }\n\n const configurations = this.#getConfigurations();\n const onForbiden = configurations\n .filter(config => config.key === permissionKey || (permissionKey !== undefined && config.key === undefined))\n .map(config => config.onForbiden)\n .find(val => !!val);\n onForbiden?.();\n return false;\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;;AAuDA;;;;;;;;;;;;AAYG;MACU,2BAA2B,GAAG,IAAI,cAAc,CAC3D,6BAA6B;;MC9DlB,mBAAmB,CAAA;IAC9B,mBAAmB,GAA4C,EAAE;AACxD,IAAA,cAAc,GAAG,MAAM,CAA4D,2BAA2B,CAAC;AAC/G,IAAA,aAAa,GAAG,MAAM,CAAC,cAAc,CAAC;IAC/C,iBAAiB,GAAsC,IAAI,CAAC,aAAa,CAAC,MAAM,CAC9E,sCAAsC,EACtC;AACE,QAAA,IAAI,EAAE,SAAS;AACf,QAAA,OAAO,EAAE,EAAE;AACZ,KAAA,CACF;AACQ,IAAA,WAAW,GAAG,IAAI,GAAG,EAAU;AAExC,IAAA,WAAA,GAAA;QACE,IAAI,CAAC,4BAA4B,EAAE;IACrC;IAEA,kBAAkB,GAAG,MAAmC;AACtD,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc;QAClC,IAAI,CAAC,MAAM,EAAE;AACX,YAAA,OAAO,EAAE;QACX;AACA,QAAA,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC;AAClD,IAAA,CAAC;AAED,IAAA,aAAa,GAAG,CAAC,GAAY,KAAY;QACvC,OAAO,GAAG,KAAK,SAAS,GAAG,eAAe,GAAG,GAAG;AAClD,IAAA,CAAC;IAED,4BAA4B,GAAG,MAAW;AACxC,QAAA,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU;QAC9B,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,kBAAkB,EAAE,EAAE;YAC9C,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC;AACpD,YAAA,IAAI,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE;AAC3B,gBAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,KAAK,SAAS,GAAG,WAAW,GAAG,MAAM,CAAC,GAAG;AACpE,gBAAA,MAAM,IAAI,KAAK,CAAC,wDAAwD,QAAQ,CAAA,CAAE,CAAC;YACrF;AACA,YAAA,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC;QACzB;AACF,IAAA,CAAC;AAED,IAAA,sBAAsB,GAAG,CAAC,GAAY,KAA4C;AAChF,QAAA,OAAO,IAAI,CAAC,kBAAkB,EAAE,CAAC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,GAAG,KAAK,GAAG,CAAC;AACrE,IAAA,CAAC;AAED,IAAA,0BAA0B,GAAG,CAAC,GAAY,KAAwB;AAChE,QAAA,IAAI,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,EAAE;AACpC,YAAA,OAAO,GAAG;QACZ;;QAGA,IAAI,GAAG,KAAK,SAAS,IAAI,IAAI,CAAC,sBAAsB,CAAC,SAAS,CAAC,EAAE;AAC/D,YAAA,OAAO,SAAS;QAClB;AAEA,QAAA,OAAO,GAAG;AACZ,IAAA,CAAC;AAED,IAAA,qBAAqB,GAAG,CAAC,aAAqB,EAAE,WAAqB,KAAU;QAC7E,MAAM,mBAAmB,GAAG,cAAc,CAAC,QAAQ,CAAC,WAAW,IAAI,EAAE,CAAC;QACtE,MAAM,OAAO,GAAG,IAAI,CAAC,iBAAiB,CAAC,GAAG,EAAE,IAAI,EAAE;AAClD,QAAA,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC;AACzB,YAAA,GAAG,OAAO;YACV,CAAC,aAAa,GAAG,mBAAmB;AACrC,SAAA,CAAC;QAEF,MAAM,aAAa,GAA4B,EAAE;AACjD,QAAA,mBAAmB,CAAC,OAAO,CAAC,UAAU,IAAG;AACvC,YAAA,aAAa,CAAC,UAAU,CAAC,GAAG,IAAI;AAClC,QAAA,CAAC,CAAC;AACF,QAAA,IAAI,CAAC,mBAAmB,CAAC,aAAa,CAAC,GAAG,aAAa;AACzD,IAAA,CAAC;AAED,IAAA,eAAe,GAAG,OAAO,GAAY,KAAuB;QAC1D,MAAM,YAAY,GAAG,IAAI,CAAC,0BAA0B,CAAC,GAAG,CAAC;QACzD,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC;QACtD,IAAI,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE;AACvC,YAAA,OAAO,IAAI,CAAC,iBAAiB,CAAC,GAAG,EAAE,GAAG,aAAa,CAAC,IAAI,EAAE;QAC5D;QAEA,MAAM,aAAa,GAAG,IAAI,CAAC,sBAAsB,CAAC,YAAY,CAAC;QAC/D,IAAI,CAAC,aAAa,EAAE;AAClB,YAAA,IAAI,CAAC,qBAAqB,CAAC,aAAa,EAAE,EAAE,CAAC;AAC7C,YAAA,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,aAAa,CAAC;AACnC,YAAA,OAAO,EAAE;QACX;AAEA,QAAA,IAAI;YACF,MAAM,WAAW,GAAa,MAAM,iBAAiB,CAAC,aAAa,CAAC,eAAe,EAAE,CAAC;YACtF,IAAI,CAAC,qBAAqB,CAAC,aAAa,EAAE,WAAW,IAAI,EAAE,CAAC;QAC9D;QAAE,OAAO,GAAG,EAAE;AACZ,YAAA,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC;AAClB,YAAA,IAAI,CAAC,qBAAqB,CAAC,aAAa,EAAE,EAAE,CAAC;QAC/C;gBAAU;AACR,YAAA,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,aAAa,CAAC;QACrC;AAEA,QAAA,OAAO,IAAI,CAAC,iBAAiB,CAAC,GAAG,EAAE,GAAG,aAAa,CAAC,IAAI,EAAE;AAC5D,IAAA,CAAC;IAED,kBAAkB,GAAG,YAA0B;AAC7C,QAAA,MAAM,cAAc,GAAG,IAAI,CAAC,kBAAkB,EAAE;AAChD,QAAA,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE;AAC1B,YAAA,MAAM,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC;YACrC;QACF;QAEA,MAAM,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,IAAI,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;AACnF,IAAA,CAAC;AAED,IAAA,aAAa,GAAG,CAAC,UAA6B,EAAE,GAAY,KAAI;AAC9D,QAAA,IAAI,CAAC,UAAU,EAAE,QAAQ,EAAE,EAAE;AAC3B,YAAA,OAAO,IAAI;QACb;QAEA,MAAM,YAAY,GAAG,IAAI,CAAC,0BAA0B,CAAC,GAAG,CAAC;QACzD,MAAM,aAAa,GAAG,IAAI,CAAC,sBAAsB,CAAC,YAAY,CAAC;AAC/D,QAAA,IAAI,aAAa,EAAE,QAAQ,EAAE;AAC3B,YAAA,OAAO,IAAI;QACb;QAEA,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC;QACtD,MAAM,aAAa,GAAG,IAAI,CAAC,mBAAmB,CAAC,aAAa,CAAC,IAAI,EAAE;AACnE,QAAA,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,UAAU,GAAG,CAAC,UAAU,CAAC;AACzE,QAAA,OAAO,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,aAAa,CAAC,GAAG,CAAC,CAAC;AACpD,IAAA,CAAC;AAED,IAAA,QAAQ,GAAG,OAAO,GAAY,KAAI;QAChC,MAAM,YAAY,GAAG,IAAI,CAAC,0BAA0B,CAAC,GAAG,CAAC;QACzD,MAAM,QAAQ,GAAG,IAAI,CAAC,sBAAsB,CAAC,YAAY,CAAC,EAAE,QAAqE;QACjI,IAAI,CAAC,QAAQ,EAAE;AACb,YAAA,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC;QACjD;QAEA,MAAM,KAAK,GAAG,MAAM,iBAAiB,CAAC,QAAQ,EAAE,CAAC;AACjD,QAAA,IAAI,KAAK,KAAK,EAAE,EAAE;AAChB,YAAA,OAAO,SAAS;QAClB;AACA,QAAA,OAAO,KAAK;AACd,IAAA,CAAC;AAED,IAAA,WAAW,GAAG,OAAU,GAAY,KAAuB;QACzD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QACtC,IAAI,CAAC,KAAK,EAAE;AACV,YAAA,OAAO,IAAI;QACb;AACA,QAAA,IAAI;YACF,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AACnC,YAAA,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC;AAC5D,YAAA,MAAM,WAAW,GAAG,kBAAkB,CACpC,IAAI,CAAC,MAAM;iBACR,KAAK,CAAC,EAAE;AACR,iBAAA,GAAG,CAAC,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;AAC9D,iBAAA,IAAI,CAAC,EAAE,CAAC,CACZ;AACD,YAAA,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC;QAChC;QAAE,OAAO,KAAK,EAAE;AACd,YAAA,OAAO,CAAC,KAAK,CAAC,eAAe,EAAE,KAAK,CAAC;AACrC,YAAA,OAAO,IAAI;QACb;AACF,IAAA,CAAC;wGAhKU,mBAAmB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,UAAA,EAAA,CAAA;AAAnB,IAAA,OAAA,KAAA,GAAA,EAAA,CAAA,qBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,SAAA,EAAA,QAAA,EAAA,EAAA,EAAA,IAAA,EAAA,mBAAmB,cADN,MAAM,EAAA,CAAA;;4FACnB,mBAAmB,EAAA,UAAA,EAAA,CAAA;kBAD/B,UAAU;mBAAC,EAAE,UAAU,EAAE,MAAM,EAAE;;;MCArB,qBAAqB,CAAA;AACvB,IAAA,YAAY,GAAuC,MAAM,CAAC,WAAW,CAAC;AACtE,IAAA,iBAAiB,GAAG,MAAM,CAAC,gBAAgB,CAAC;AAC5C,IAAA,kBAAkB,GAAG,MAAM,CAAC,mBAAmB,CAAC;;AAGhD,IAAA,YAAY,GAAG,KAAK,CAAuC,SAAS,mFAAC;AACrE,IAAA,eAAe,GAAG,KAAK,CAAqB,SAAS,sFAAC;AAE/D,IAAA,WAAA,GAAA;QACE,MAAM,CAAC,MAAK;AACV,YAAA,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,EAAE;AACtC,YAAA,MAAM,aAAa,GAAG,IAAI,CAAC,eAAe,EAAE;AAE5C,YAAA,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE;;AAG9B,YAAA,IAAI,CAAC,UAAU,EAAE,QAAQ,EAAE,EAAE;gBAC3B,IAAI,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,IAAI,CAAC,YAAY,CAAC;gBAC5D;YACF;;YAGA,IAAI,IAAI,CAAC,kBAAkB,CAAC,aAAa,CAAC,UAAU,EAAE,aAAa,CAAC,EAAE;gBACpE,IAAI,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,IAAI,CAAC,YAAY,CAAC;YAC9D;AACF,QAAA,CAAC,CAAC;IACJ;wGA3BW,qBAAqB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;4FAArB,qBAAqB,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,gBAAA,EAAA,MAAA,EAAA,EAAA,YAAA,EAAA,EAAA,iBAAA,EAAA,cAAA,EAAA,UAAA,EAAA,cAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,eAAA,EAAA,EAAA,iBAAA,EAAA,iBAAA,EAAA,UAAA,EAAA,iBAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA;;4FAArB,qBAAqB,EAAA,UAAA,EAAA,CAAA;kBAHjC,SAAS;AAAC,YAAA,IAAA,EAAA,CAAA;AACT,oBAAA,QAAQ,EAAE,gBAAgB;AAC3B,iBAAA;;;MCCY,iBAAiB,CAAA;AACX,IAAA,aAAa,GAA8D,MAAM,CAAC,2BAA2B,CAAC;AAC9G,IAAA,iBAAiB,GAAG,MAAM,CAAC,mBAAmB,CAAC;IAEhE,kBAAkB,GAAG,MAAmC;AACtD,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa;QACjC,IAAI,CAAC,MAAM,EAAE;AACX,YAAA,OAAO,EAAE;QACX;AACA,QAAA,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC;AAClD,IAAA,CAAC;AAED,IAAA,WAAW,GAAG,OAAO,IAA4B,EAAE,KAA0B,KAAI;;AAE/E,QAAA,MAAM,IAAI,CAAC,iBAAiB,CAAC,kBAAkB,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;AACtE,QAAA,OAAO,IAAI;AACb,IAAA,CAAC;AAED,IAAA,gBAAgB,GAAG,OAAO,cAAsC,EAAE,KAA0B,KAAI;QAC9F,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,YAAY,CAAC;QACpD,MAAM,aAAa,GAAG,cAAc,CAAC,IAAI,GAAG,eAAe,CAAuB;QAClF,IAAI,IAAI,CAAC,iBAAiB,CAAC,aAAa,CAAC,UAAU,EAAE,aAAa,CAAC,EAAE;AACnE,YAAA,OAAO,IAAI;QACb;AAEA,QAAA,MAAM,cAAc,GAAG,IAAI,CAAC,kBAAkB,EAAE;QAChD,MAAM,UAAU,GAAG;aAChB,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,GAAG,KAAK,aAAa,KAAK,aAAa,KAAK,SAAS,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS,CAAC;aAC1G,GAAG,CAAC,MAAM,IAAI,MAAM,CAAC,UAAU;aAC/B,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC;QACrB,UAAU,IAAI;AACd,QAAA,OAAO,KAAK;AACd,IAAA,CAAC;wGAhCU,iBAAiB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,UAAA,EAAA,CAAA;AAAjB,IAAA,OAAA,KAAA,GAAA,EAAA,CAAA,qBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,SAAA,EAAA,QAAA,EAAA,EAAA,EAAA,IAAA,EAAA,iBAAiB,cADJ,MAAM,EAAA,CAAA;;4FACnB,iBAAiB,EAAA,UAAA,EAAA,CAAA;kBAD7B,UAAU;mBAAC,EAAE,UAAU,EAAE,MAAM,EAAE;;;ACLlC;;AAEG;;;;"}
|
|
1
|
+
{"version":3,"file":"sdcorejs-angular-modules-permission.mjs","sources":["../../../projects/sdcorejs-angular/modules/permission/src/configurations/permission.configuration.ts","../../../projects/sdcorejs-angular/modules/permission/src/services/permission.service.ts","../../../projects/sdcorejs-angular/modules/permission/src/directives/permission.directive.ts","../../../projects/sdcorejs-angular/modules/permission/src/guards/permission.guard.ts","../../../projects/sdcorejs-angular/modules/permission/sdcorejs-angular-modules-permission.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\nimport { MaybeAsync } from '@sdcorejs/utils/models';\n\n/**\n * Cấu hình trung tâm cho module permission.\n *\n * Luồng hoạt động chính:\n * 1. `loadPermissions(key?)` ở service được gọi để lấy danh sách mã quyền theo từng `key` cấu hình.\n * 2. Guard/service đối chiếu mã quyền theo route metadata.\n * 3. Khi không đủ quyền, callback `onForbiden()` sẽ được gọi (nếu có).\n *\n * Lưu ý:\n * - `SD_PERMISSION_CONFIGURATION` hỗ trợ cả cấu hình đơn và mảng cấu hình (`multi: true`).\n * - Nếu cần tạm bỏ qua kiểm tra quyền (POC/UAT cục bộ), đặt `disabled = true`.\n */\nexport interface ISdPermissionConfiguration {\n /**\n * Khóa định danh cấu hình.\n * Dùng để phân biệt khi hệ thống mở rộng theo nhiều profile permission.\n *\n * Lưu ý: `undefined` cũng được xem là một key hợp lệ (cấu hình mặc định).\n */\n key?: string;\n\n /**\n * Bật/tắt kiểm tra permission toàn cục.\n * - `true`: bỏ qua kiểm tra quyền.\n * - `false | undefined`: kiểm tra quyền theo cấu hình route.\n */\n disabled?: boolean;\n\n /**\n * Trả về danh sách mã quyền của user hiện tại.\n * Có thể đồng bộ hoặc bất đồng bộ.\n *\n * Ví dụ giá trị trả về:\n * - `['PRODUCT_C_EMPLOYEE_VIEW', 'PRODUCT_C_EMPLOYEE_UPDATE']`\n */\n loadPermissions: () => MaybeAsync<string[]>;\n\n /**\n * Callback xử lý khi user không có quyền truy cập URL hiện tại.\n * Thường dùng để điều hướng sang trang forbidden hoặc hiển thị thông báo.\n *\n * Giữ nguyên tên `onForbiden` để tương thích API hiện tại.\n */\n onForbiden?: () => void;\n\n /**\n * Cung cấp access token hiện tại cho các tác vụ liên quan permission.\n * Hỗ trợ trả về đồng bộ, Promise hoặc Observable.\n */\n getToken?: () => MaybeAsync<string | undefined | null>;\n\n /**\n * Cho phép mirror danh sách mã quyền của key này xuống `sessionStorage` để sống qua reload.\n *\n * ⚠️ Mặc định `false` — danh sách quyền CHỈ nằm trong bộ nhớ.\n *\n * Bật cờ này nghĩa là chấp nhận: mọi script chạy trên cùng origin đều ĐỌC và GHI được danh sách đó.\n * Cache KHÔNG phải một ranh giới phân quyền — nó chỉ tiết kiệm một lần gọi API khi reload; backend\n * vẫn phải kiểm tra quyền cho từng request. Khi bật, nhớ gọi `SdPermissionService.reset()` lúc\n * signout để xoá cả bản mirror.\n */\n persistCache?: boolean;\n}\n\n/**\n * InjectionToken cho cấu hình permission.\n *\n * Ví dụ provider:\n * {\n * provide: SD_PERMISSION_CONFIGURATION,\n * useValue: {\n * disabled: false,\n * loadPermissions: () => ['SAMPLE_C_EMPLOYEE_VIEW'],\n * onForbiden: () => router.navigateByUrl('/layout/forbidden')\n * }\n * }\n */\nexport const SD_PERMISSION_CONFIGURATION = new InjectionToken<ISdPermissionConfiguration | ISdPermissionConfiguration[]>(\n 'sd-permission.configuration'\n);\n","import { inject, Injectable, isDevMode } from '@angular/core';\nimport { SdCache, SdCacheService } from '@sdcorejs/angular/services/cache';\nimport { ArrayUtilities } from '@sdcorejs/utils/fns';\nimport { ISdPermissionConfiguration, SD_PERMISSION_CONFIGURATION } from '../configurations';\nimport { MaybeAsync, resolveMaybeAsync } from '@sdcorejs/utils/models';\n\n/**\n * Opt-out TƯỜNG MINH: \"chỗ này cố ý không yêu cầu quyền nào\".\n *\n * Dùng cho `route.data.permission`, `[sdPermission]` hoặc `hasPermission(...)` khi một route/phần tử\n * thật sự công khai. Mọi giá trị rỗng khác (`undefined`, `null`, `''`, `[]`) đều bị coi là CẤU HÌNH\n * THIẾU và bị từ chối — xem `SdPermissionService.hasPermission`.\n */\nexport const SD_PERMISSION_PUBLIC = '__SD_PERMISSION_PUBLIC__';\n\n/** Kiểu đầu vào cho `hasPermission` / `[sdPermission]`. */\nexport type SdPermissionInput = string | string[] | null | undefined;\n\n/**\n * why: khoá cache cũ là một UUID cố định (`212a51fa-…`) dùng chung cho MỌI key, nên một entry\n * sessionStorage duy nhất chứa quyền của tất cả profile. Đổi sang prefix có namespace + tách theo key\n * để `invalidate(key)` xoá đúng phần của nó và không đụng key khác.\n */\nconst PERSISTED_CACHE_PREFIX = 'sd-permission.codes.';\n\n@Injectable({ providedIn: 'root' })\nexport class SdPermissionService {\n #permissionMapByKey: Record<string, Record<string, boolean>> = {};\n // why: danh sách quyền đã resolve giờ CHỈ sống trong bộ nhớ. Trước đây nó luôn được mirror xuống\n // sessionStorage, tức mọi script trên cùng origin đọc/ghi được — chỉ cần chạy được một dòng JS là\n // tự cấp cho mình quyền trên UI. Tệ hơn: `#loadedKeys` vốn nằm trong bộ nhớ nên sau reload service\n // vẫn gọi lại `loadPermissions()`, entry sessionStorage kia gần như không mang lại lợi ích gì mà\n // chỉ là bề mặt tấn công. Ai thật sự cần cache qua reload thì bật `persistCache` cho từng key.\n #permissionsByKey: Record<string, string[]> = {};\n readonly #configuration = inject<ISdPermissionConfiguration | ISdPermissionConfiguration[]>(SD_PERMISSION_CONFIGURATION);\n readonly #cacheService = inject(SdCacheService);\n readonly #persistedCaches = new Map<string, SdCache<string[]>>();\n readonly #loadedKeys = new Set<string>();\n\n constructor() {\n this.#validateDuplicateConfigKeys();\n }\n\n #getConfigurations = (): ISdPermissionConfiguration[] => {\n const config = this.#configuration;\n if (!config) {\n return [];\n }\n return Array.isArray(config) ? config : [config];\n };\n\n #normalizeKey = (key?: string): string => {\n return key === undefined ? '__undefined__' : key;\n };\n\n #validateDuplicateConfigKeys = (): void => {\n const seen = new Set<string>();\n for (const config of this.#getConfigurations()) {\n const normalizedKey = this.#normalizeKey(config.key);\n if (seen.has(normalizedKey)) {\n const keyLabel = config.key === undefined ? 'undefined' : config.key;\n throw new Error(`[Permission] Duplicate permission configuration key: ${keyLabel}`);\n }\n seen.add(normalizedKey);\n }\n };\n\n #getConfigurationByKey = (key?: string): ISdPermissionConfiguration | undefined => {\n return this.#getConfigurations().find(config => config.key === key);\n };\n\n #getEffectivePermissionKey = (key?: string): string | undefined => {\n if (this.#getConfigurationByKey(key)) {\n return key;\n }\n\n // Portal-level config uses key = undefined and acts as default fallback\n if (key !== undefined && this.#getConfigurationByKey(undefined)) {\n return undefined;\n }\n\n return key;\n };\n\n /**\n * Chỉ tạo handle cache khi key đó bật `persistCache`. Handle được memo hoá để `reset()` /\n * `invalidate()` gọi được `remove()` lên đúng entry đã ghi.\n */\n #getPersistedCache = (normalizedKey: string): SdCache<string[]> => {\n const existing = this.#persistedCaches.get(normalizedKey);\n if (existing) {\n return existing;\n }\n\n const handle = this.#cacheService.create<string[]>(`${PERSISTED_CACHE_PREFIX}${normalizedKey}`, {\n type: 'session',\n default: [],\n });\n this.#persistedCaches.set(normalizedKey, handle);\n return handle;\n };\n\n #setPermissionsForKey = (normalizedKey: string, permissions: string[], persist: boolean): void => {\n const distinctPermissions = ArrayUtilities.distinct(permissions || []);\n this.#permissionsByKey[normalizedKey] = distinctPermissions;\n\n const permissionMap: Record<string, boolean> = {};\n distinctPermissions.forEach(permission => {\n permissionMap[permission] = true;\n });\n this.#permissionMapByKey[normalizedKey] = permissionMap;\n\n if (persist) {\n this.#getPersistedCache(normalizedKey).set(distinctPermissions);\n }\n };\n\n loadPermissions = async (key?: string): Promise<string[]> => {\n const effectiveKey = this.#getEffectivePermissionKey(key);\n const normalizedKey = this.#normalizeKey(effectiveKey);\n if (this.#loadedKeys.has(normalizedKey)) {\n return this.#permissionsByKey[normalizedKey] ?? [];\n }\n\n const configuration = this.#getConfigurationByKey(effectiveKey);\n if (!configuration) {\n this.#setPermissionsForKey(normalizedKey, [], false);\n this.#loadedKeys.add(normalizedKey);\n return [];\n }\n\n const persist = configuration.persistCache === true;\n if (persist) {\n // why: chỉ khi consumer bật `persistCache` mới đọc lại bản mirror — mặc định không chạm storage.\n const cached = this.#getPersistedCache(normalizedKey).get();\n if (cached?.length) {\n this.#setPermissionsForKey(normalizedKey, cached, false);\n this.#loadedKeys.add(normalizedKey);\n return this.#permissionsByKey[normalizedKey] ?? [];\n }\n }\n\n try {\n const permissions: string[] = await resolveMaybeAsync(configuration.loadPermissions());\n this.#setPermissionsForKey(normalizedKey, permissions || [], persist);\n } catch (err) {\n console.error(err);\n this.#setPermissionsForKey(normalizedKey, [], false);\n } finally {\n this.#loadedKeys.add(normalizedKey);\n }\n\n return this.#permissionsByKey[normalizedKey] ?? [];\n };\n\n loadAllPermissions = async (): Promise<void> => {\n const configurations = this.#getConfigurations();\n if (!configurations.length) {\n await this.loadPermissions(undefined);\n return;\n }\n\n await Promise.all(configurations.map(config => this.loadPermissions(config.key)));\n };\n\n /**\n * Xoá TOÀN BỘ quyền đã nạp (mọi key) — cả map trong bộ nhớ, cờ \"đã nạp\" lẫn bản mirror\n * `sessionStorage` của các key bật `persistCache`.\n *\n * ⚠️ BẮT BUỘC gọi khi signout. Service là singleton `providedIn: 'root'`, nên trong một SPA\n * signout → signin (không reload trang) mà không reset thì `loadPermissions()` sẽ short-circuit\n * theo key đã cache và user MỚI thừa hưởng nguyên bộ quyền của user CŨ.\n *\n * ```ts\n * auth.signout$?.subscribe(() => permissionService.reset());\n * ```\n */\n reset = (): void => {\n // why: sau reload, service chưa tạo handle cho key nào cả nhưng entry sessionStorage của phiên\n // trước vẫn còn. Tạo handle cho mọi key có `persistCache` trước khi remove để reset thật sự sạch.\n this.#getConfigurations()\n .filter(config => config.persistCache === true)\n .forEach(config => this.#getPersistedCache(this.#normalizeKey(config.key)));\n\n this.#persistedCaches.forEach(cache => cache.remove());\n this.#persistedCaches.clear();\n this.#loadedKeys.clear();\n this.#permissionMapByKey = {};\n this.#permissionsByKey = {};\n };\n\n /**\n * Xoá quyền đã nạp của MỘT key (mặc định là key portal `undefined`), buộc lần\n * `loadPermissions(key)` kế tiếp gọi lại loader. Dùng khi chỉ một profile/tenant đổi quyền;\n * signout thì dùng {@link reset}.\n */\n invalidate = (key?: string): void => {\n const effectiveKey = this.#getEffectivePermissionKey(key);\n const normalizedKey = this.#normalizeKey(effectiveKey);\n\n this.#loadedKeys.delete(normalizedKey);\n delete this.#permissionMapByKey[normalizedKey];\n delete this.#permissionsByKey[normalizedKey];\n\n // why: tạo handle nếu chưa có, để xoá được cả entry còn sót lại từ phiên trước khi reload.\n if (this.#getConfigurationByKey(effectiveKey)?.persistCache === true) {\n this.#getPersistedCache(normalizedKey).remove();\n }\n this.#persistedCaches.delete(normalizedKey);\n };\n\n /**\n * Kiểm tra quyền (OR cho mảng).\n *\n * ⚠️ BREAKING: đầu vào rỗng (`undefined` / `null` / `''` / `[]`) giờ trả `false`.\n * Muốn \"không yêu cầu quyền\" thì phải nói ra bằng {@link SD_PERMISSION_PUBLIC}.\n */\n hasPermission = (permission: SdPermissionInput, key?: string): boolean => {\n const effectiveKey = this.#getEffectivePermissionKey(key);\n const configuration = this.#getConfigurationByKey(effectiveKey);\n if (configuration?.disabled) {\n return true;\n }\n\n const requested = (Array.isArray(permission) ? permission : [permission]).filter(\n (value): value is string => typeof value === 'string' && value.trim() !== ''\n );\n\n // why: opt-out phải TƯỜNG MINH. Chỉ sentinel này mới nghĩa là \"cố ý công khai\".\n if (requested.includes(SD_PERMISSION_PUBLIC)) {\n return true;\n }\n\n if (!requested.length) {\n // why: code cũ trả `true` cho mọi đầu vào rỗng, nên một route gõ sai key `data` (`permision`),\n // một `[sdPermission]` bind nhầm sang biến undefined, hay một mã quyền rỗng do API trả về đều\n // được cấp quyền âm thầm — không phân biệt được \"cố ý không giới hạn\" với \"cấu hình hỏng\".\n // Giờ mặc định là TỪ CHỐI; ca công khai thật sự phải dùng `SD_PERMISSION_PUBLIC`.\n if (isDevMode()) {\n console.error(\n '[Permission] Access denied: an empty permission was checked. ' +\n 'Pass a real permission code, or SD_PERMISSION_PUBLIC if this route/element is intentionally public.',\n { permission, key }\n );\n }\n return false;\n }\n\n const normalizedKey = this.#normalizeKey(effectiveKey);\n const permissionMap = this.#permissionMapByKey[normalizedKey] || {};\n return requested.some(value => permissionMap[value] === true);\n };\n\n getToken = async (key?: string) => {\n const effectiveKey = this.#getEffectivePermissionKey(key);\n const getToken = this.#getConfigurationByKey(effectiveKey)?.getToken as (() => MaybeAsync<string | undefined | null>) | undefined;\n if (!getToken) {\n throw new Error('[Permission] Method getToken');\n }\n\n const token = await resolveMaybeAsync(getToken());\n if (token === '') {\n return undefined;\n }\n return token;\n };\n\n /**\n * Giải mã phần payload của JWT hiện tại và trả về claim **CHƯA ĐƯỢC XÁC THỰC**.\n *\n * ⚠️ KHÔNG kiểm chữ ký, KHÔNG kiểm `exp`/`nbf`/`aud`/`iss`. Đây thuần tuý là base64url-decode: bất\n * kỳ ai cũng tự tạo được một token có claim tuỳ ý và hàm này sẽ vui vẻ trả về. Vì vậy kết quả CHỈ\n * dùng cho mục đích hiển thị (tên, avatar, tenant đang chọn…) và TUYỆT ĐỐI không được dùng để ra\n * quyết định phân quyền — quyền phải do backend xác thực chữ ký rồi trả về.\n *\n * Trả `null` khi không có token hoặc payload hỏng.\n */\n readUnverifiedTokenClaims = async <T>(key?: string): Promise<T | null> => {\n const token = await this.getToken(key);\n if (!token) {\n return null;\n }\n try {\n const payload = token.split('.')[1];\n const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');\n const jsonPayload = decodeURIComponent(\n atob(base64)\n .split('')\n .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))\n .join('')\n );\n return JSON.parse(jsonPayload);\n } catch (error) {\n console.error('Invalid token', error);\n return null;\n }\n };\n\n /**\n * @deprecated Tên `decodeToken` gợi ý một thao tác đã được kiểm chứng, trong khi thực tế payload\n * hoàn toàn KHÔNG được xác thực. Dùng {@link readUnverifiedTokenClaims} — alias này chỉ còn để\n * consumer cũ không vỡ và sẽ bị gỡ ở release sau.\n */\n decodeToken = async <T>(key?: string): Promise<T | null> => {\n return this.readUnverifiedTokenClaims<T>(key);\n };\n}\n","import { Directive, effect, inject, input, TemplateRef, ViewContainerRef } from '@angular/core';\nimport { SdPermissionInput, SdPermissionService } from '../services';\n\n@Directive({\n selector: '[sdPermission]',\n})\nexport class SdPermissionDirective {\n readonly #templateRef: TemplateRef<Record<string, never>> = inject(TemplateRef);\n readonly #viewContainerRef = inject(ViewContainerRef);\n readonly #permissionService = inject(SdPermissionService);\n\n // Nếu là mảng thì chỉ cần có 1 permission trong mảng đó xem như có quyền\n readonly sdPermission = input<SdPermissionInput>(undefined);\n readonly sdPermissionKey = input<string | undefined>(undefined);\n\n constructor() {\n effect(() => {\n const permission = this.sdPermission();\n const permissionKey = this.sdPermissionKey();\n\n this.#viewContainerRef.clear();\n\n // why: code cũ render vô điều kiện khi `permission` rỗng, nên `*sdPermission=\"perm\"` với `perm`\n // undefined (typo tên biến, dữ liệu chưa về, mã quyền rỗng từ API) hiện nút cho tất cả mọi\n // người. Giờ mọi quyết định đều đi qua `hasPermission` — nó fail closed và chỉ chấp nhận\n // `SD_PERMISSION_PUBLIC` như opt-out tường minh, nên không còn nhánh render ngầm nào nữa.\n if (this.#permissionService.hasPermission(permission, permissionKey)) {\n this.#viewContainerRef.createEmbeddedView(this.#templateRef);\n }\n });\n }\n}\n","import { inject, Injectable, isDevMode } from '@angular/core';\nimport { ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot } from '@angular/router';\nimport { ISdPermissionConfiguration, SD_PERMISSION_CONFIGURATION } from '../configurations';\nimport { SdPermissionService } from '../services';\n\n@Injectable({ providedIn: 'root' })\nexport class SdPermissionGuard implements CanActivate, CanActivateChild {\n private readonly configuration: ISdPermissionConfiguration | ISdPermissionConfiguration[] = inject(SD_PERMISSION_CONFIGURATION);\n private readonly permissionService = inject(SdPermissionService);\n\n #getConfigurations = (): ISdPermissionConfiguration[] => {\n const config = this.configuration;\n if (!config) {\n return [];\n }\n return Array.isArray(config) ? config : [config];\n };\n\n /** Gọi `onForbiden` của config khớp key (hoặc config portal) rồi chặn điều hướng. */\n #deny = (permissionKey?: string): false => {\n const configurations = this.#getConfigurations();\n const onForbiden = configurations\n .filter(config => config.key === permissionKey || (permissionKey !== undefined && config.key === undefined))\n .map(config => config.onForbiden)\n .find(val => !!val);\n onForbiden?.();\n return false;\n };\n\n canActivate = async (next: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {\n // Guard ở layer portal: preload toàn bộ permission theo tất cả key đã cấu hình\n await this.permissionService.loadAllPermissions().catch(console.error);\n return true;\n };\n\n canActivateChild = async (activatedRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {\n const data = activatedRoute.data ?? {};\n const permissionKey = data['permissionKey'] as string | undefined;\n\n // why: `canActivateChild` chạy cho MỌI route con, nên một route quên khai báo `data.permission`\n // hoặc gõ sai key (`permision`, `permissions`) trước đây rơi thẳng vào nhánh \"rỗng ⇒ cho qua\" của\n // `hasPermission` và được cấp quyền âm thầm. Không khai báo giờ = TỪ CHỐI; route thật sự công khai\n // phải nói ra bằng `data: { permission: SD_PERMISSION_PUBLIC }`.\n if (!Object.prototype.hasOwnProperty.call(data, 'permission')) {\n if (isDevMode()) {\n console.error(\n `[Permission] Access denied for \"${state?.url ?? ''}\": route data has no \"permission\" entry. ` +\n 'Declare data.permission with a permission code, or SD_PERMISSION_PUBLIC if the route is intentionally public. ' +\n 'Check for a typo in the data key.'\n );\n }\n // why: KHÔNG gọi `onForbiden()` ở nhánh này. `onForbiden` thường điều hướng sang trang forbidden;\n // nếu chính trang đó lại quên khai báo `data.permission` thì nó cũng bị từ chối và gọi tiếp\n // `onForbiden` → vòng lặp redirect vô tận, treo app. Thiếu khai báo là LỖI CẤU HÌNH, không phải\n // \"hết quyền\": chặn im lặng + log dev là đủ, và một cấu hình sai không bao giờ tự đệ quy được.\n return false;\n }\n\n if (this.permissionService.hasPermission(data['permission'], permissionKey)) {\n return true;\n }\n\n return this.#deny(permissionKey);\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Pipe, Injectable, inject } from '@angular/core';
|
|
3
|
-
import { DateUtilities, NumberUtilities } from '@sdcorejs/
|
|
2
|
+
import { Pipe, Injectable, inject, SecurityContext } from '@angular/core';
|
|
3
|
+
import { DateUtilities, NumberUtilities } from '@sdcorejs/utils/fns';
|
|
4
4
|
import { SD_CORE_CONFIGURATION } from '@sdcorejs/angular/configurations';
|
|
5
5
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
6
6
|
import { of, interval } from 'rxjs';
|
|
7
|
-
import { map } from 'rxjs/operators';
|
|
7
|
+
import { map, takeWhile } from 'rxjs/operators';
|
|
8
8
|
import { EMPTY_STR } from '@sdcorejs/utils/constants';
|
|
9
9
|
|
|
10
10
|
class SdFormatDatePipe {
|
|
@@ -76,17 +76,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
|
|
|
76
76
|
}]
|
|
77
77
|
}] });
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Render an HTML string through `[innerHTML]`.
|
|
81
|
+
*
|
|
82
|
+
* why: this pipe used to call `bypassSecurityTrustHtml()` unconditionally, on every value, with no
|
|
83
|
+
* sanitize step. That turned every call site into a stored-XSS sink — and the library itself pipes
|
|
84
|
+
* server-supplied table cell data through it
|
|
85
|
+
* (`components/table/src/components/desktop-cell/view/view.component.html`), while
|
|
86
|
+
* `components/table/sd-table.md` already described the value as "sanitized via sdSafeHtml".
|
|
87
|
+
*
|
|
88
|
+
* Now it **sanitizes by default** and the bypass is an explicit, per-call-site opt-in:
|
|
89
|
+
*
|
|
90
|
+
* ```html
|
|
91
|
+
* <!-- untrusted / server-supplied: script, on* handlers and javascript: urls are stripped -->
|
|
92
|
+
* <div [innerHTML]="row.note | sdSafeHtml"></div>
|
|
93
|
+
*
|
|
94
|
+
* <!-- app-authored markup you control, e.g. an inline SVG sprite -->
|
|
95
|
+
* <span [innerHTML]="iconSvg | sdSafeHtml: true"></span>
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* This mirrors the decision already taken for the toast in `services/notify`: text-safe by default,
|
|
99
|
+
* opt-in HTML. Auto-detecting "this looks like HTML" is deliberately NOT done — it would escalate
|
|
100
|
+
* untrusted text that merely contains `<` straight back into the unsanitized sink.
|
|
101
|
+
*/
|
|
79
102
|
class SdSafeHtmlPipe {
|
|
80
|
-
sanitizer = inject(DomSanitizer);
|
|
81
|
-
|
|
82
|
-
transform(html) {
|
|
103
|
+
#sanitizer = inject(DomSanitizer);
|
|
104
|
+
transform(html, trusted = false) {
|
|
83
105
|
if (typeof html === 'number') {
|
|
84
106
|
return html;
|
|
85
107
|
}
|
|
86
108
|
if (!html) {
|
|
87
109
|
return undefined;
|
|
88
110
|
}
|
|
89
|
-
|
|
111
|
+
if (trusted) {
|
|
112
|
+
// Caller has explicitly declared this markup as app-authored.
|
|
113
|
+
return this.#sanitizer.bypassSecurityTrustHtml(html);
|
|
114
|
+
}
|
|
115
|
+
// `sanitize` returns `null` when the value is entirely stripped; normalise to `''` so the
|
|
116
|
+
// binding clears the element instead of rendering the string "null".
|
|
117
|
+
return this.#sanitizer.sanitize(SecurityContext.HTML, html) ?? '';
|
|
90
118
|
}
|
|
91
119
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SdSafeHtmlPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
92
120
|
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.17", ngImport: i0, type: SdSafeHtmlPipe, isStandalone: true, name: "sdSafeHtml" });
|
|
@@ -103,7 +131,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
|
|
|
103
131
|
args: [{
|
|
104
132
|
providedIn: 'root',
|
|
105
133
|
}]
|
|
106
|
-
}]
|
|
134
|
+
}] });
|
|
107
135
|
|
|
108
136
|
class SdTimeDifferentPipe {
|
|
109
137
|
maxSecond = 60;
|
|
@@ -118,28 +146,34 @@ class SdTimeDifferentPipe {
|
|
|
118
146
|
if (!different) {
|
|
119
147
|
return of(DateUtilities.toFormat(value, format));
|
|
120
148
|
}
|
|
121
|
-
|
|
149
|
+
const elapsedSeconds = () => Math.round((new Date().getTime() - new Date(value).getTime()) / 1000);
|
|
150
|
+
if (elapsedSeconds() < 0) {
|
|
122
151
|
return of(DateUtilities.toFormat(value, format));
|
|
123
152
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
153
|
+
const threshold = this.#threshold(different);
|
|
154
|
+
// why: quá ngưỡng thì output là một chuỗi ngày tĩnh, tick mỗi giây không đổi được gì nữa.
|
|
155
|
+
// Trả `of()` để không tạo timer nào cho các giá trị đã cũ (list N dòng = N timer thừa).
|
|
156
|
+
if (elapsedSeconds() >= threshold) {
|
|
157
|
+
return of(DateUtilities.toFormat(value, format));
|
|
158
|
+
}
|
|
159
|
+
// why: `interval(1000)` cũ KHÔNG BAO GIỜ complete — mỗi usage giữ một timer + một lượt
|
|
160
|
+
// change-detection của async pipe mỗi giây, vĩnh viễn, kể cả khi giá trị đã vượt ngưỡng
|
|
161
|
+
// tương đối. `takeWhile(..., true)` phát nốt lần cuối (dạng ngày tuyệt đối) rồi complete.
|
|
162
|
+
return interval(1000).pipe(map(() => elapsedSeconds()), takeWhile(elapsed => elapsed < threshold, true), map(elapsed => (elapsed < threshold ? DateUtilities.timeDifference(value) : DateUtilities.toFormat(value, format))));
|
|
163
|
+
}
|
|
164
|
+
#threshold(different) {
|
|
165
|
+
switch (different) {
|
|
166
|
+
case 'month':
|
|
167
|
+
return this.maxMonth;
|
|
168
|
+
case 'day':
|
|
169
|
+
return this.maxDay;
|
|
170
|
+
case 'hour':
|
|
171
|
+
return this.maxHour;
|
|
172
|
+
case 'minute':
|
|
173
|
+
return this.maxMinute;
|
|
174
|
+
default:
|
|
175
|
+
return this.maxSecond;
|
|
176
|
+
}
|
|
143
177
|
}
|
|
144
178
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SdTimeDifferentPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
145
179
|
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.17", ngImport: i0, type: SdTimeDifferentPipe, isStandalone: true, name: "sdTimeDifferent" });
|