@sdcorejs/angular 19.1.6 → 19.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.
Files changed (374) hide show
  1. package/README.md +10 -0
  2. package/assets/scss/core/menu.scss +18 -0
  3. package/assets/scss/core/utilities/_misc.scss +15 -0
  4. package/assets/scss/sd-core.scss +1 -0
  5. package/components/anchor/src/components/anchor/anchor.component.d.ts +3 -1
  6. package/components/api-contract-builder/index.d.ts +8 -0
  7. package/components/api-contract-builder/src/api-contract-builder.component.d.ts +123 -0
  8. package/components/api-contract-builder/src/api-contract.configuration.d.ts +41 -0
  9. package/components/api-contract-builder/src/api-contract.expression.d.ts +43 -0
  10. package/components/api-contract-builder/src/api-contract.model.d.ts +167 -0
  11. package/components/api-contract-builder/src/api-contract.samples.d.ts +25 -0
  12. package/components/api-contract-builder/src/api-contract.schema.d.ts +126 -0
  13. package/components/api-contract-builder/src/api-contract.serializer.d.ts +17 -0
  14. package/components/api-contract-builder/src/api-contract.validation.d.ts +12 -0
  15. package/components/api-contract-builder/src/components/api-contract-diagnostic-list.component.d.ts +18 -0
  16. package/components/api-contract-builder/src/components/api-contract-node-drawer.component.d.ts +130 -0
  17. package/components/api-contract-builder/src/components/api-contract-node-editor.component.d.ts +35 -0
  18. package/components/api-contract-builder/src/components/api-contract-node-summary.component.d.ts +41 -0
  19. package/components/api-contract-builder/src/components/api-contract-record-editor.component.d.ts +36 -0
  20. package/components/api-contract-builder/src/components/api-contract-source-editor.component.d.ts +86 -0
  21. package/components/api-contract-builder/src/components/api-contract-suggestion.model.d.ts +17 -0
  22. package/components/badge/src/badge.component.d.ts +2 -0
  23. package/components/ckeditor-styles/src/ckeditor-styles.component.d.ts +1 -1
  24. package/components/form-generic/src/components/form-builder/components/expression-builder/expression-builder.component.d.ts +2 -1
  25. package/components/form-generic/src/components/form-builder/form-builder.component.d.ts +3 -1
  26. package/components/form-generic/src/components/form-render/components/item/components/checkbox/checkbox.component.d.ts +34 -0
  27. package/components/form-generic/src/components/form-render/components/item/components/index.d.ts +1 -0
  28. package/components/form-generic/src/components/form-render/components/item/components/table/table.component.d.ts +6 -1
  29. package/components/form-generic/src/components/sd-feel-expression/sd-feel-expression.component.d.ts +1 -1
  30. package/components/form-generic/src/configurations/form.configuration.d.ts +1 -1
  31. package/components/form-generic/src/models/form-generic-component.model.d.ts +8 -8
  32. package/components/form-generic/src/models/form-generic-definition-selection.model.d.ts +5 -5
  33. package/components/form-generic/src/models/form-generic-expression.model.d.ts +12 -12
  34. package/components/form-generic/src/models/index.d.ts +1 -0
  35. package/components/form-generic/src/services/form-generic.service.d.ts +2 -2
  36. package/components/import-excel/src/import-excel.component.d.ts +1 -0
  37. package/components/index.d.ts +7 -1
  38. package/components/mini-editor/src/mini-editor.model.d.ts +1 -1
  39. package/components/modal/src/modal.component.d.ts +3 -2
  40. package/components/modal-resizable/src/modal-resizable.component.d.ts +92 -0
  41. package/components/modal-resizable/src/modal-resizable.registry.d.ts +22 -0
  42. package/components/org-chart/src/org-chart.component.d.ts +6 -0
  43. package/components/preview/src/preview-image/preview-image.component.d.ts +1 -0
  44. package/components/query-bar/src/components/actions-bar/actions-bar.component.d.ts +6 -2
  45. package/components/query-bar/src/components/build-chip/build-chip.component.d.ts +4 -1
  46. package/components/query-bar/src/components/chip-popover/chip-popover.component.d.ts +8 -0
  47. package/components/query-bar/src/components/field-picker/field-picker.component.d.ts +2 -0
  48. package/components/query-bar/src/components/inline-chip/inline-chip.component.d.ts +2 -0
  49. package/components/query-bar/src/components/inline-value-chip/inline-value-chip.component.d.ts +3 -3
  50. package/components/query-bar/src/components/saved-filters-menu/saved-filters-menu.component.d.ts +9 -4
  51. package/components/query-bar/src/query-bar.component.d.ts +32 -5
  52. package/components/query-bar/src/query-bar.model.d.ts +15 -12
  53. package/components/query-builder/src/query-builder.component.d.ts +76 -39
  54. package/components/query-builder/src/query-builder.model.d.ts +59 -52
  55. package/components/query-builder/src/query-builder.serializer.d.ts +15 -6
  56. package/components/section/src/section.component.d.ts +5 -0
  57. package/components/side-drawer/src/body-scroll-lock.service.d.ts +10 -0
  58. package/components/splitter/src/splitter-handle/splitter-handle.component.d.ts +1 -0
  59. package/components/tab-router/index.d.ts +2 -1
  60. package/components/tab-router/src/actions/tab-router.action.d.ts +2 -2
  61. package/components/tab-router/src/components/tab-router-item/tab-router-item.component.d.ts +2 -2
  62. package/components/tab-router/src/components/tab-router-nav/tab-router-nav.component.d.ts +3 -3
  63. package/components/tab-router/src/components/tab-router-outlet/tab-router-outlet.component.d.ts +3 -3
  64. package/components/tab-router/src/decorators/tab.decorator.d.ts +13 -1
  65. package/components/tab-router/src/events/tab-router.event.d.ts +3 -3
  66. package/components/tab-router/src/models/tab-router.model.d.ts +2 -2
  67. package/components/tab-router/src/pipes/tab-info.pipe.d.ts +2 -2
  68. package/components/tab-router/src/services/tab-router.service.d.ts +9 -9
  69. package/components/table/index.d.ts +1 -0
  70. package/components/table/src/components/config/config.component.d.ts +6 -1
  71. package/components/table/src/components/desktop-cell/view/view.component.d.ts +1 -0
  72. package/components/table/src/components/filter/external-filter/external-filter.component.d.ts +1 -1
  73. package/components/table/src/components/filter/mobile-filter/mobile-filter.component.d.ts +1 -1
  74. package/components/table/src/components/selector-action/action-filter.pipe.d.ts +8 -1
  75. package/components/table/src/components/selector-action/selector-action.component.d.ts +2 -0
  76. package/components/table/src/directives/index.d.ts +1 -0
  77. package/components/table/src/directives/sd-table-command-header-def.directive.d.ts +24 -0
  78. package/components/table/src/models/table-column.model.d.ts +30 -13
  79. package/components/table/src/models/table-item.model.d.ts +30 -1
  80. package/components/table/src/models/table-option-selector.model.d.ts +5 -4
  81. package/components/table/src/models/table-option.model.d.ts +12 -0
  82. package/components/table/src/pipes/sd-group.pipe.d.ts +6 -2
  83. package/components/table/src/services/table-filter/table-filter.model.d.ts +15 -15
  84. package/components/table/src/services/table-filter/table-filter.service.d.ts +2 -2
  85. package/components/table/src/services/table-format/table-format.service.d.ts +1 -1
  86. package/components/table/src/services/table-selection/selection-action.util.d.ts +24 -0
  87. package/components/table/src/services/table-selection/table-selection.util.d.ts +31 -0
  88. package/components/table/src/services/tree/tree.util.d.ts +3 -2
  89. package/components/table/src/table.component.d.ts +51 -5
  90. package/components/tree/src/tree-item-def.directive.d.ts +1 -1
  91. package/components/tree/src/tree.component.d.ts +16 -4
  92. package/components/tree/src/tree.model.d.ts +31 -25
  93. package/components/upload-file/src/configurations/upload-file.configuration.d.ts +2 -2
  94. package/components/upload-file/src/upload-file.component.d.ts +4 -1
  95. package/configurations/src/sd-core.configuration.d.ts +1 -2
  96. package/fesm2022/sdcorejs-angular-components-anchor.mjs +36 -12
  97. package/fesm2022/sdcorejs-angular-components-anchor.mjs.map +1 -1
  98. package/fesm2022/sdcorejs-angular-components-api-contract-builder.mjs +3411 -0
  99. package/fesm2022/sdcorejs-angular-components-api-contract-builder.mjs.map +1 -0
  100. package/fesm2022/sdcorejs-angular-components-autoid-inspector.mjs +2 -2
  101. package/fesm2022/sdcorejs-angular-components-autoid-inspector.mjs.map +1 -1
  102. package/fesm2022/sdcorejs-angular-components-badge.mjs +27 -2
  103. package/fesm2022/sdcorejs-angular-components-badge.mjs.map +1 -1
  104. package/fesm2022/sdcorejs-angular-components-button.mjs +2 -2
  105. package/fesm2022/sdcorejs-angular-components-button.mjs.map +1 -1
  106. package/fesm2022/sdcorejs-angular-components-ckeditor-styles.mjs +1 -1
  107. package/fesm2022/sdcorejs-angular-components-ckeditor-styles.mjs.map +1 -1
  108. package/fesm2022/sdcorejs-angular-components-code-editor.mjs +3 -3
  109. package/fesm2022/sdcorejs-angular-components-code-editor.mjs.map +1 -1
  110. package/fesm2022/sdcorejs-angular-components-data-state.mjs +3 -3
  111. package/fesm2022/sdcorejs-angular-components-data-state.mjs.map +1 -1
  112. package/fesm2022/sdcorejs-angular-components-editor.mjs +2 -2
  113. package/fesm2022/sdcorejs-angular-components-editor.mjs.map +1 -1
  114. package/fesm2022/sdcorejs-angular-components-form-generic.mjs +324 -192
  115. package/fesm2022/sdcorejs-angular-components-form-generic.mjs.map +1 -1
  116. package/fesm2022/sdcorejs-angular-components-history.mjs +4 -4
  117. package/fesm2022/sdcorejs-angular-components-history.mjs.map +1 -1
  118. package/fesm2022/sdcorejs-angular-components-import-excel.mjs +13 -5
  119. package/fesm2022/sdcorejs-angular-components-import-excel.mjs.map +1 -1
  120. package/fesm2022/sdcorejs-angular-components-inform.mjs +3 -3
  121. package/fesm2022/sdcorejs-angular-components-inform.mjs.map +1 -1
  122. package/fesm2022/sdcorejs-angular-components-modal-resizable.mjs +164 -8
  123. package/fesm2022/sdcorejs-angular-components-modal-resizable.mjs.map +1 -1
  124. package/fesm2022/sdcorejs-angular-components-modal.mjs +30 -18
  125. package/fesm2022/sdcorejs-angular-components-modal.mjs.map +1 -1
  126. package/fesm2022/sdcorejs-angular-components-operator.mjs +2 -2
  127. package/fesm2022/sdcorejs-angular-components-operator.mjs.map +1 -1
  128. package/fesm2022/sdcorejs-angular-components-org-chart.mjs +60 -13
  129. package/fesm2022/sdcorejs-angular-components-org-chart.mjs.map +1 -1
  130. package/fesm2022/sdcorejs-angular-components-preview.mjs +27 -17
  131. package/fesm2022/sdcorejs-angular-components-preview.mjs.map +1 -1
  132. package/fesm2022/sdcorejs-angular-components-query-bar.mjs +192 -30
  133. package/fesm2022/sdcorejs-angular-components-query-bar.mjs.map +1 -1
  134. package/fesm2022/sdcorejs-angular-components-query-builder.mjs +187 -127
  135. package/fesm2022/sdcorejs-angular-components-query-builder.mjs.map +1 -1
  136. package/fesm2022/sdcorejs-angular-components-section.mjs +33 -2
  137. package/fesm2022/sdcorejs-angular-components-section.mjs.map +1 -1
  138. package/fesm2022/sdcorejs-angular-components-side-drawer.mjs +83 -24
  139. package/fesm2022/sdcorejs-angular-components-side-drawer.mjs.map +1 -1
  140. package/fesm2022/sdcorejs-angular-components-splitter.mjs +57 -21
  141. package/fesm2022/sdcorejs-angular-components-splitter.mjs.map +1 -1
  142. package/fesm2022/sdcorejs-angular-components-stepper.mjs +2 -2
  143. package/fesm2022/sdcorejs-angular-components-stepper.mjs.map +1 -1
  144. package/fesm2022/sdcorejs-angular-components-tab-router.mjs +68 -20
  145. package/fesm2022/sdcorejs-angular-components-tab-router.mjs.map +1 -1
  146. package/fesm2022/sdcorejs-angular-components-tab.mjs +3 -2
  147. package/fesm2022/sdcorejs-angular-components-tab.mjs.map +1 -1
  148. package/fesm2022/sdcorejs-angular-components-table.mjs +555 -267
  149. package/fesm2022/sdcorejs-angular-components-table.mjs.map +1 -1
  150. package/fesm2022/sdcorejs-angular-components-tree.mjs +80 -17
  151. package/fesm2022/sdcorejs-angular-components-tree.mjs.map +1 -1
  152. package/fesm2022/sdcorejs-angular-components-upload-file.mjs +48 -13
  153. package/fesm2022/sdcorejs-angular-components-upload-file.mjs.map +1 -1
  154. package/fesm2022/sdcorejs-angular-components.mjs +7 -1
  155. package/fesm2022/sdcorejs-angular-components.mjs.map +1 -1
  156. package/fesm2022/sdcorejs-angular-configurations.mjs.map +1 -1
  157. package/fesm2022/sdcorejs-angular-directives.mjs +45 -8
  158. package/fesm2022/sdcorejs-angular-directives.mjs.map +1 -1
  159. package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs +40 -29
  160. package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs.map +1 -1
  161. package/fesm2022/sdcorejs-angular-forms-checkbox.mjs +5 -5
  162. package/fesm2022/sdcorejs-angular-forms-checkbox.mjs.map +1 -1
  163. package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs +67 -50
  164. package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs.map +1 -1
  165. package/fesm2022/sdcorejs-angular-forms-chip.mjs +57 -43
  166. package/fesm2022/sdcorejs-angular-forms-chip.mjs.map +1 -1
  167. package/fesm2022/sdcorejs-angular-forms-date-range.mjs +231 -60
  168. package/fesm2022/sdcorejs-angular-forms-date-range.mjs.map +1 -1
  169. package/fesm2022/sdcorejs-angular-forms-date.mjs +95 -23
  170. package/fesm2022/sdcorejs-angular-forms-date.mjs.map +1 -1
  171. package/fesm2022/sdcorejs-angular-forms-datetime.mjs +130 -26
  172. package/fesm2022/sdcorejs-angular-forms-datetime.mjs.map +1 -1
  173. package/fesm2022/sdcorejs-angular-forms-entity-picker.mjs +39 -15
  174. package/fesm2022/sdcorejs-angular-forms-entity-picker.mjs.map +1 -1
  175. package/fesm2022/sdcorejs-angular-forms-input-color.mjs +16 -5
  176. package/fesm2022/sdcorejs-angular-forms-input-color.mjs.map +1 -1
  177. package/fesm2022/sdcorejs-angular-forms-input-number.mjs +24 -9
  178. package/fesm2022/sdcorejs-angular-forms-input-number.mjs.map +1 -1
  179. package/fesm2022/sdcorejs-angular-forms-input.mjs +58 -10
  180. package/fesm2022/sdcorejs-angular-forms-input.mjs.map +1 -1
  181. package/fesm2022/sdcorejs-angular-forms-models.mjs +227 -6
  182. package/fesm2022/sdcorejs-angular-forms-models.mjs.map +1 -1
  183. package/fesm2022/sdcorejs-angular-forms-radio.mjs +4 -4
  184. package/fesm2022/sdcorejs-angular-forms-radio.mjs.map +1 -1
  185. package/fesm2022/sdcorejs-angular-forms-select.mjs +135 -37
  186. package/fesm2022/sdcorejs-angular-forms-select.mjs.map +1 -1
  187. package/fesm2022/sdcorejs-angular-forms-switch.mjs +18 -3
  188. package/fesm2022/sdcorejs-angular-forms-switch.mjs.map +1 -1
  189. package/fesm2022/sdcorejs-angular-forms-textarea.mjs +51 -62
  190. package/fesm2022/sdcorejs-angular-forms-textarea.mjs.map +1 -1
  191. package/fesm2022/sdcorejs-angular-forms-time-range.mjs +57 -12
  192. package/fesm2022/sdcorejs-angular-forms-time-range.mjs.map +1 -1
  193. package/fesm2022/sdcorejs-angular-forms-time.mjs +16 -5
  194. package/fesm2022/sdcorejs-angular-forms-time.mjs.map +1 -1
  195. package/fesm2022/sdcorejs-angular-forms-tree-select.mjs +100 -9
  196. package/fesm2022/sdcorejs-angular-forms-tree-select.mjs.map +1 -1
  197. package/fesm2022/sdcorejs-angular-forms.mjs +91 -46
  198. package/fesm2022/sdcorejs-angular-forms.mjs.map +1 -1
  199. package/fesm2022/sdcorejs-angular-handlers.mjs +18 -3
  200. package/fesm2022/sdcorejs-angular-handlers.mjs.map +1 -1
  201. package/fesm2022/sdcorejs-angular-i18n.mjs +895 -68
  202. package/fesm2022/sdcorejs-angular-i18n.mjs.map +1 -1
  203. package/fesm2022/sdcorejs-angular-interceptors.mjs +87 -29
  204. package/fesm2022/sdcorejs-angular-interceptors.mjs.map +1 -1
  205. package/fesm2022/sdcorejs-angular-modules-auth.mjs +25 -15
  206. package/fesm2022/sdcorejs-angular-modules-auth.mjs.map +1 -1
  207. package/fesm2022/sdcorejs-angular-modules-icon.mjs +2 -2
  208. package/fesm2022/sdcorejs-angular-modules-icon.mjs.map +1 -1
  209. package/fesm2022/sdcorejs-angular-modules-keycloak.mjs +32 -3
  210. package/fesm2022/sdcorejs-angular-modules-keycloak.mjs.map +1 -1
  211. package/fesm2022/sdcorejs-angular-modules-layout.mjs +342 -111
  212. package/fesm2022/sdcorejs-angular-modules-layout.mjs.map +1 -1
  213. package/fesm2022/sdcorejs-angular-modules-permission.mjs +177 -40
  214. package/fesm2022/sdcorejs-angular-modules-permission.mjs.map +1 -1
  215. package/fesm2022/sdcorejs-angular-pipes.mjs +62 -28
  216. package/fesm2022/sdcorejs-angular-pipes.mjs.map +1 -1
  217. package/fesm2022/sdcorejs-angular-services-api.mjs +123 -13
  218. package/fesm2022/sdcorejs-angular-services-api.mjs.map +1 -1
  219. package/fesm2022/sdcorejs-angular-services-cache.mjs +53 -3
  220. package/fesm2022/sdcorejs-angular-services-cache.mjs.map +1 -1
  221. package/fesm2022/sdcorejs-angular-services-confirm.mjs +25 -73
  222. package/fesm2022/sdcorejs-angular-services-confirm.mjs.map +1 -1
  223. package/fesm2022/sdcorejs-angular-services-excel.mjs +12 -10
  224. package/fesm2022/sdcorejs-angular-services-excel.mjs.map +1 -1
  225. package/fesm2022/sdcorejs-angular-services-loading.mjs +75 -84
  226. package/fesm2022/sdcorejs-angular-services-loading.mjs.map +1 -1
  227. package/fesm2022/sdcorejs-angular-services-notify.mjs +46 -5
  228. package/fesm2022/sdcorejs-angular-services-notify.mjs.map +1 -1
  229. package/fesm2022/sdcorejs-angular-services-storage.mjs +4 -0
  230. package/fesm2022/sdcorejs-angular-services-storage.mjs.map +1 -1
  231. package/fesm2022/sdcorejs-angular-services.mjs +0 -1
  232. package/fesm2022/sdcorejs-angular-services.mjs.map +1 -1
  233. package/fesm2022/sdcorejs-angular-utilities-extensions.mjs +225 -80
  234. package/fesm2022/sdcorejs-angular-utilities-extensions.mjs.map +1 -1
  235. package/fesm2022/sdcorejs-angular-utilities-models.mjs +1 -63
  236. package/fesm2022/sdcorejs-angular-utilities-models.mjs.map +1 -1
  237. package/fesm2022/sdcorejs-angular.mjs +17 -10
  238. package/fesm2022/sdcorejs-angular.mjs.map +1 -1
  239. package/forms/autocomplete/src/autocomplete.component.d.ts +5 -2
  240. package/forms/chip-calendar/src/chip-calendar.component.d.ts +10 -0
  241. package/forms/date/src/date.component.d.ts +13 -2
  242. package/forms/date-range/src/date-range.component.d.ts +13 -2
  243. package/forms/datetime/src/datetime.component.d.ts +13 -2
  244. package/forms/entity-picker/src/entity-picker.component.d.ts +9 -3
  245. package/forms/input/src/input.component.d.ts +3 -0
  246. package/forms/input-color/src/input-color.component.d.ts +4 -0
  247. package/forms/input-number/src/input-number.component.d.ts +3 -0
  248. package/forms/models/index.d.ts +3 -0
  249. package/forms/models/src/sd-form-control-connector.d.ts +12 -0
  250. package/forms/models/src/sd-model-facing-control.d.ts +43 -0
  251. package/forms/models/src/sd-search.model.d.ts +1 -1
  252. package/forms/models/src/sd-selection-data.model.d.ts +1 -1
  253. package/forms/models/src/sd-temporal-transform.d.ts +31 -0
  254. package/forms/models/src/sd-timer-scope.d.ts +29 -0
  255. package/forms/select/src/select.component.d.ts +27 -4
  256. package/forms/src/forms.module.d.ts +22 -13
  257. package/forms/switch/src/switch.component.d.ts +5 -0
  258. package/forms/textarea/src/textarea.component.d.ts +5 -3
  259. package/forms/time-range/src/time-range.component.d.ts +22 -1
  260. package/forms/tree-select/src/tree-select.component.d.ts +36 -5
  261. package/i18n/src/en.d.ts +170 -10
  262. package/i18n/src/i18n.messages.d.ts +1 -1
  263. package/i18n/src/i18n.pipe.d.ts +3 -3
  264. package/i18n/src/i18n.service.d.ts +2 -1
  265. package/i18n/src/i18n.types.d.ts +0 -3
  266. package/interceptors/no-internet/no-internet.interceptor.d.ts +11 -1
  267. package/modules/auth/services/auth.model.d.ts +1 -1
  268. package/modules/keycloak/keycloak.service.d.ts +1 -0
  269. package/modules/layout/components/shared/menu-tree/menu-tree.component.d.ts +2 -0
  270. package/modules/layout/components/sidebar-mobile-v1/components/sidebar/sidebar.component.d.ts +3 -3
  271. package/modules/layout/components/sidebar-mobile-v1/main.component.d.ts +3 -3
  272. package/modules/layout/components/sidebar-mobile-v2/main.component.d.ts +3 -3
  273. package/modules/layout/components/sidebar-mobile-v3/main.component.d.ts +3 -3
  274. package/modules/layout/components/sidebar-v1/components/sidebar/sidebar.component.d.ts +16 -3
  275. package/modules/layout/components/sidebar-v1/main.component.d.ts +3 -3
  276. package/modules/layout/components/sidebar-v2/main.component.d.ts +3 -3
  277. package/modules/layout/components/sidebar-v3/main.component.d.ts +3 -3
  278. package/modules/layout/modules/forbidden/pages/root/root.component.d.ts +5 -0
  279. package/modules/layout/modules/not-found/pages/root/root.component.d.ts +5 -0
  280. package/modules/layout/services/layout.service.d.ts +21 -0
  281. package/modules/layout/services/storage/storage.service.d.ts +52 -7
  282. package/modules/permission/src/configurations/permission.configuration.d.ts +11 -0
  283. package/modules/permission/src/directives/permission.directive.d.ts +2 -1
  284. package/modules/permission/src/services/permission.service.d.ts +52 -1
  285. package/package.json +40 -29
  286. package/pipes/src/safe-html.pipe.d.ts +26 -4
  287. package/pipes/src/time-different.pipe.d.ts +1 -0
  288. package/public-api.d.ts +11 -10
  289. package/services/api/src/api-host.d.ts +15 -0
  290. package/services/api/src/api.model.d.ts +14 -0
  291. package/services/api/src/api.module.d.ts +6 -0
  292. package/services/cache/src/cache.model.d.ts +10 -0
  293. package/services/index.d.ts +0 -1
  294. package/services/notify/index.d.ts +1 -1
  295. package/utilities/extensions/index.d.ts +1 -5
  296. package/utilities/extensions/src/url-safety.d.ts +72 -0
  297. package/utilities/extensions/src/utility.extension.d.ts +9 -5
  298. package/utilities/models/index.d.ts +0 -10
  299. package/components/base/index.d.ts +0 -1
  300. package/components/base/src/base-secure.component.d.ts +0 -8
  301. package/components/document-builder/index.d.ts +0 -4
  302. package/components/document-builder/src/components/header-footer-builder/header-footer-builder.component.d.ts +0 -15
  303. package/components/document-builder/src/components/header-footer-builder/plugins/page-number.plugin.d.ts +0 -4
  304. package/components/document-builder/src/components/index.d.ts +0 -1
  305. package/components/document-builder/src/document-builder.component.d.ts +0 -53
  306. package/components/document-builder/src/document-builder.config.d.ts +0 -21
  307. package/components/document-builder/src/document-builder.model.d.ts +0 -41
  308. package/components/document-builder/src/document-builder.utils.d.ts +0 -10
  309. package/components/document-builder/src/plugins/block-space/block-space.plugin.d.ts +0 -9
  310. package/components/document-builder/src/plugins/ck-comment/ck-comment.plugin.d.ts +0 -44
  311. package/components/document-builder/src/plugins/ck-comment/ck-comment.plugin.model.d.ts +0 -57
  312. package/components/document-builder/src/plugins/heading/heading.plugin.d.ts +0 -5
  313. package/components/document-builder/src/plugins/highlight-range/highlight-range.plugin.d.ts +0 -4
  314. package/components/document-builder/src/plugins/image-custom/image-custom.plugin.d.ts +0 -31
  315. package/components/document-builder/src/plugins/image-upload/image-upload.plugin.d.ts +0 -5
  316. package/components/document-builder/src/plugins/index.d.ts +0 -11
  317. package/components/document-builder/src/plugins/page-orientation/page-orientation.plugin.d.ts +0 -24
  318. package/components/document-builder/src/plugins/paste-handler/filters/bookmark.d.ts +0 -14
  319. package/components/document-builder/src/plugins/paste-handler/filters/br.d.ts +0 -15
  320. package/components/document-builder/src/plugins/paste-handler/filters/image.d.ts +0 -25
  321. package/components/document-builder/src/plugins/paste-handler/filters/list.d.ts +0 -29
  322. package/components/document-builder/src/plugins/paste-handler/filters/parse.d.ts +0 -35
  323. package/components/document-builder/src/plugins/paste-handler/filters/removeboldwrapper.d.ts +0 -15
  324. package/components/document-builder/src/plugins/paste-handler/filters/removegooglesheetstag.d.ts +0 -15
  325. package/components/document-builder/src/plugins/paste-handler/filters/removeinvalidtablewidth.d.ts +0 -15
  326. package/components/document-builder/src/plugins/paste-handler/filters/removemsattributes.d.ts +0 -15
  327. package/components/document-builder/src/plugins/paste-handler/filters/removestyleblock.d.ts +0 -15
  328. package/components/document-builder/src/plugins/paste-handler/filters/removexmlns.d.ts +0 -15
  329. package/components/document-builder/src/plugins/paste-handler/filters/replacemsfootnotes.d.ts +0 -54
  330. package/components/document-builder/src/plugins/paste-handler/filters/space.d.ts +0 -27
  331. package/components/document-builder/src/plugins/paste-handler/filters/table.d.ts +0 -16
  332. package/components/document-builder/src/plugins/paste-handler/filters/utils.d.ts +0 -25
  333. package/components/document-builder/src/plugins/paste-handler/index.d.ts +0 -35
  334. package/components/document-builder/src/plugins/paste-handler/normalizers/googledocsnormalizer.d.ts +0 -31
  335. package/components/document-builder/src/plugins/paste-handler/normalizers/googlesheetsnormalizer.d.ts +0 -31
  336. package/components/document-builder/src/plugins/paste-handler/normalizers/mswordnormalizer.d.ts +0 -29
  337. package/components/document-builder/src/plugins/paste-handler/types.d.ts +0 -30
  338. package/components/document-builder/src/plugins/table-custom/index.d.ts +0 -34
  339. package/components/document-builder/src/plugins/variable/variable.plugin.d.ts +0 -51
  340. package/components/table/src/pipes/selection-visible-select-all.pipe.d.ts +0 -9
  341. package/fesm2022/sdcorejs-angular-components-base.mjs +0 -23
  342. package/fesm2022/sdcorejs-angular-components-base.mjs.map +0 -1
  343. package/fesm2022/sdcorejs-angular-components-document-builder.mjs +0 -5038
  344. package/fesm2022/sdcorejs-angular-components-document-builder.mjs.map +0 -1
  345. package/fesm2022/sdcorejs-angular-models.mjs +0 -6
  346. package/fesm2022/sdcorejs-angular-models.mjs.map +0 -1
  347. package/fesm2022/sdcorejs-angular-services-docx.mjs +0 -291
  348. package/fesm2022/sdcorejs-angular-services-docx.mjs.map +0 -1
  349. package/fesm2022/sdcorejs-angular-services-license.mjs +0 -97
  350. package/fesm2022/sdcorejs-angular-services-license.mjs.map +0 -1
  351. package/models/index.d.ts +0 -1
  352. package/models/src/language.model.d.ts +0 -2
  353. package/services/docx/index.d.ts +0 -1
  354. package/services/docx/src/lib/docx.model.d.ts +0 -9
  355. package/services/docx/src/lib/docx.service.d.ts +0 -14
  356. package/services/docx/src/lib/pandoc-core.d.ts +0 -11
  357. package/services/docx/src/public-api.d.ts +0 -2
  358. package/services/license/index.d.ts +0 -1
  359. package/services/license/src/license.service.d.ts +0 -9
  360. package/utilities/extensions/src/array.extension.d.ts +0 -1
  361. package/utilities/extensions/src/color.extension.d.ts +0 -5
  362. package/utilities/extensions/src/date.extension.d.ts +0 -1
  363. package/utilities/extensions/src/number.extension.d.ts +0 -1
  364. package/utilities/extensions/src/string.extension.d.ts +0 -50
  365. package/utilities/models/src/color.model.d.ts +0 -4
  366. package/utilities/models/src/empty.model.d.ts +0 -4
  367. package/utilities/models/src/filter.model.d.ts +0 -12
  368. package/utilities/models/src/maybe-async.model.d.ts +0 -10
  369. package/utilities/models/src/nested-key-of.model.d.ts +0 -4
  370. package/utilities/models/src/operator.model.d.ts +0 -13
  371. package/utilities/models/src/order.model.d.ts +0 -4
  372. package/utilities/models/src/paging.model.d.ts +0 -8
  373. package/utilities/models/src/pattern.model.d.ts +0 -21
  374. package/utilities/models/src/size.model.d.ts +0 -4
@@ -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/angular/utilities/extensions';
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
- #permissionsByKey = this.#cacheService.create('212a51fa-38d5-43b2-bd46-922d85950ba3', {
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
- #setPermissionsForKey = (normalizedKey, permissions) => {
69
- const distinctPermissions = ArrayUtilities.distinct(permissions || []);
70
- const current = this.#permissionsByKey.get() || {};
71
- this.#permissionsByKey.set({
72
- ...current,
73
- [normalizedKey]: distinctPermissions,
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.get()?.[normalizedKey] || [];
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.get()?.[normalizedKey] || [];
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
- hasPermission = (permission, key) => {
115
- if (!permission?.toString()) {
116
- return true;
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
- const permissions = Array.isArray(permission) ? permission : [permission];
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
- decodeToken = async (key) => {
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: "19.2.21", ngImport: i0, type: SdPermissionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
160
279
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.21", 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
- // Nếu không gắn permission thì render
180
- if (!permission?.toString()) {
181
- this.#viewContainerRef.createEmbeddedView(this.#templateRef);
182
- return;
183
- }
184
- // Kiểm tra permission theo key (nếu có)
298
+ // why: code 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 permission = activatedRoute.data['permission'];
217
- const permissionKey = activatedRoute.data?.['permissionKey'];
218
- if (this.permissionService.hasPermission(permission, permissionKey)) {
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
- const configurations = this.#getConfigurations();
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: "19.2.21", ngImport: i0, type: SdPermissionGuard, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
230
367
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdPermissionGuard, providedIn: 'root' });
@@ -238,5 +375,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", 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":"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"}
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/angular/utilities/extensions';
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: "19.2.21", 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
- constructor() { }
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
- return this.sanitizer.bypassSecurityTrustHtml(html);
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: "19.2.21", ngImport: i0, type: SdSafeHtmlPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
92
120
  static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.2.21", ngImport: i0, type: SdSafeHtmlPipe, isStandalone: true, name: "sdSafeHtml" });
@@ -103,7 +131,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImpo
103
131
  args: [{
104
132
  providedIn: 'root',
105
133
  }]
106
- }], ctorParameters: () => [] });
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
- if (Math.round((new Date().getTime() - new Date(value).getTime()) / 1000) < 0) {
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
- return interval(1000).pipe(map(() => {
125
- const timeDifferent = Math.round((new Date().getTime() - new Date(value).getTime()) / 1000);
126
- if (different === 'month' && timeDifferent < this.maxMonth) {
127
- return DateUtilities.timeDifference(value);
128
- }
129
- if (different === 'day' && timeDifferent < this.maxDay) {
130
- return DateUtilities.timeDifference(value);
131
- }
132
- if (different === 'hour' && timeDifferent < this.maxHour) {
133
- return DateUtilities.timeDifference(value);
134
- }
135
- if (different === 'minute' && timeDifferent < this.maxMinute) {
136
- return DateUtilities.timeDifference(value);
137
- }
138
- if (different === 'second' && timeDifferent < this.maxSecond) {
139
- return DateUtilities.timeDifference(value);
140
- }
141
- return DateUtilities.toFormat(value, format);
142
- }));
153
+ const threshold = this.#threshold(different);
154
+ // why: quá ngưỡng thì output 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: "19.2.21", ngImport: i0, type: SdTimeDifferentPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
145
179
  static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.2.21", ngImport: i0, type: SdTimeDifferentPipe, isStandalone: true, name: "sdTimeDifferent" });