@tinya-code/gas-admin-ui 0.2.0-alpha.12 → 0.2.0-alpha.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (369) hide show
  1. package/README.md +222 -0
  2. package/ng-package.json +7 -0
  3. package/package.json +3 -15
  4. package/src/lib/actions/action-registry.spec.ts +52 -0
  5. package/src/lib/actions/action-registry.ts +126 -0
  6. package/src/lib/actions/action-service.spec.ts +215 -0
  7. package/src/lib/actions/action-service.ts +87 -0
  8. package/src/lib/actions/descriptors.spec.ts +155 -0
  9. package/src/lib/actions/descriptors.ts +154 -0
  10. package/src/lib/columns/column-cell.ts +33 -0
  11. package/src/lib/columns/column-defaults.spec.ts +99 -0
  12. package/src/lib/columns/column-defaults.ts +84 -0
  13. package/src/lib/columns/column-format.spec.ts +274 -0
  14. package/src/lib/columns/descriptors.ts +113 -0
  15. package/src/lib/columns/format-badge.ts +51 -0
  16. package/src/lib/columns/format-boolean.ts +58 -0
  17. package/src/lib/columns/format-currency.ts +30 -0
  18. package/src/lib/columns/format-date.ts +35 -0
  19. package/src/lib/columns/format-number.ts +32 -0
  20. package/src/lib/columns/format-text.ts +23 -0
  21. package/src/lib/columns/format-thumbnail.ts +63 -0
  22. package/src/lib/core/adapters/firebase-auth.adapter.ts +6 -0
  23. package/src/lib/core/guards/auth.guard.spec.ts +88 -0
  24. package/src/lib/core/guards/auth.guard.ts +48 -0
  25. package/src/lib/core/guards/unsaved-changes.guard.ts +16 -0
  26. package/src/lib/core/interceptors/api-error.interceptor.spec.ts +182 -0
  27. package/src/lib/core/interceptors/api-error.interceptor.ts +87 -0
  28. package/src/lib/core/models/api.model.ts +104 -0
  29. package/src/lib/core/models/schema-descriptions.ts +118 -0
  30. package/src/lib/core/models/schema.model.ts +1097 -0
  31. package/src/lib/core/registry/composition-validator.spec.ts +358 -0
  32. package/src/lib/core/registry/composition-validator.ts +270 -0
  33. package/src/lib/core/registry/field-validators.spec.ts +199 -0
  34. package/src/lib/core/registry/field-validators.ts +440 -0
  35. package/src/lib/core/registry/render-host.spec.ts +208 -0
  36. package/src/lib/core/registry/render-host.ts +646 -0
  37. package/src/lib/core/registry/render-registry.spec.ts +96 -0
  38. package/src/lib/core/registry/render-registry.ts +312 -0
  39. package/src/lib/core/registry/schema-catalog.ts +156 -0
  40. package/src/lib/core/services/api.service.spec.ts +1142 -0
  41. package/src/lib/core/services/api.service.ts +371 -0
  42. package/src/lib/core/services/auth.service.spec.ts +653 -0
  43. package/src/lib/core/services/auth.service.ts +503 -0
  44. package/src/lib/core/services/config.service.spec.ts +121 -0
  45. package/src/lib/core/services/config.service.ts +119 -0
  46. package/src/lib/core/services/data-source.service.spec.ts +179 -0
  47. package/src/lib/core/services/data-source.service.ts +142 -0
  48. package/src/lib/core/services/error-mapper.service.spec.ts +50 -0
  49. package/src/lib/core/services/error-mapper.service.ts +49 -0
  50. package/src/lib/core/services/notification.service.ts +73 -0
  51. package/src/lib/core/services/relation-drawer.service.ts +41 -0
  52. package/src/lib/core/services/theme.service.spec.ts +106 -0
  53. package/src/lib/core/services/theme.service.ts +86 -0
  54. package/src/lib/core/services/transport.spec.ts +198 -0
  55. package/src/lib/core/services/transport.ts +164 -0
  56. package/src/lib/core/services/upload.service.ts +81 -0
  57. package/src/lib/core/testing/categories.schema.ts +125 -0
  58. package/src/lib/core/testing/orders.schema.ts +372 -0
  59. package/src/lib/core/testing/products.schema.ts +258 -0
  60. package/src/lib/core/testing/site.schema.ts +195 -0
  61. package/src/lib/core/testing/test-catalog.ts +21 -0
  62. package/src/lib/core/tokens.ts +112 -0
  63. package/src/lib/core/utils/clipboard.spec.ts +55 -0
  64. package/src/lib/core/utils/clipboard.ts +90 -0
  65. package/src/lib/core/utils/context-resolver.spec.ts +95 -0
  66. package/src/lib/core/utils/context-resolver.ts +101 -0
  67. package/src/lib/core/utils/response-unwrapper.spec.ts +70 -0
  68. package/src/lib/core/utils/response-unwrapper.ts +103 -0
  69. package/src/lib/core/utils/source-options.spec.ts +96 -0
  70. package/src/lib/core/utils/source-options.ts +54 -0
  71. package/src/lib/core/utils/template-interpolator.spec.ts +59 -0
  72. package/src/lib/core/utils/template-interpolator.ts +56 -0
  73. package/src/lib/core/view-navigation/view-navigation.service.spec.ts +207 -0
  74. package/src/lib/core/view-navigation/view-navigation.service.ts +132 -0
  75. package/src/lib/fields/boolean/boolean.descriptor.ts +11 -0
  76. package/src/lib/fields/boolean/boolean.ts +40 -0
  77. package/src/lib/fields/color/color.descriptor.ts +12 -0
  78. package/src/lib/fields/color/color.ts +34 -0
  79. package/src/lib/fields/currency/currency.descriptor.ts +12 -0
  80. package/src/lib/fields/currency/currency.ts +67 -0
  81. package/src/lib/fields/date/date.descriptor.ts +12 -0
  82. package/src/lib/fields/date/date.ts +36 -0
  83. package/src/lib/fields/descriptors.ts +122 -0
  84. package/src/lib/fields/display/display-badge.ts +65 -0
  85. package/src/lib/fields/display/display-date.ts +39 -0
  86. package/src/lib/fields/display/display-defaults.ts +84 -0
  87. package/src/lib/fields/display/display-file.ts +41 -0
  88. package/src/lib/fields/display/display-image.ts +54 -0
  89. package/src/lib/fields/display/display-list.ts +107 -0
  90. package/src/lib/fields/display/display-number.ts +41 -0
  91. package/src/lib/fields/display/display-text.ts +42 -0
  92. package/src/lib/fields/display/display.spec.ts +242 -0
  93. package/src/lib/fields/dropdown/dropdown.descriptor.ts +12 -0
  94. package/src/lib/fields/dropdown/dropdown.ts +45 -0
  95. package/src/lib/fields/email/email.descriptor.ts +12 -0
  96. package/src/lib/fields/email/email.ts +28 -0
  97. package/src/lib/fields/field-aria.ts +60 -0
  98. package/src/lib/fields/field-host/field-host.spec.ts +118 -0
  99. package/src/lib/fields/field-host/field-host.ts +47 -0
  100. package/src/lib/fields/field-node.ts +88 -0
  101. package/src/lib/fields/file/file.descriptor.ts +14 -0
  102. package/src/lib/fields/file/file.spec.ts +134 -0
  103. package/src/lib/fields/file/file.ts +281 -0
  104. package/src/lib/fields/group/group.descriptor.ts +21 -0
  105. package/src/lib/fields/group/group.ts +110 -0
  106. package/src/lib/fields/image/image.descriptor.ts +12 -0
  107. package/src/lib/fields/image/image.spec.ts +117 -0
  108. package/src/lib/fields/image/image.ts +281 -0
  109. package/src/lib/fields/key-value/key-value.descriptor.ts +11 -0
  110. package/src/lib/fields/key-value/key-value.ts +283 -0
  111. package/src/lib/fields/list/list.descriptor.ts +21 -0
  112. package/src/lib/fields/list/list.ts +376 -0
  113. package/src/lib/fields/multiselect/multiselect.descriptor.ts +11 -0
  114. package/src/lib/fields/multiselect/multiselect.spec.ts +142 -0
  115. package/src/lib/fields/multiselect/multiselect.ts +152 -0
  116. package/src/lib/fields/number/number.descriptor.ts +12 -0
  117. package/src/lib/fields/number/number.ts +40 -0
  118. package/src/lib/fields/phone/phone.descriptor.ts +12 -0
  119. package/src/lib/fields/phone/phone.ts +46 -0
  120. package/src/lib/fields/readonly-text/readonly-text.descriptor.ts +12 -0
  121. package/src/lib/fields/readonly-text/readonly-text.ts +58 -0
  122. package/src/lib/fields/relation/relation.descriptor.ts +12 -0
  123. package/src/lib/fields/relation/relation.spec.ts +193 -0
  124. package/src/lib/fields/relation/relation.ts +224 -0
  125. package/src/lib/fields/rich-select/rich-select.descriptor.ts +12 -0
  126. package/src/lib/fields/rich-select/rich-select.ts +175 -0
  127. package/src/lib/fields/select/radio-mode.ts +25 -0
  128. package/src/lib/fields/select/select.descriptor.ts +12 -0
  129. package/src/lib/fields/select/select.spec.ts +162 -0
  130. package/src/lib/fields/select/select.ts +210 -0
  131. package/src/lib/fields/slug/slug.descriptor.ts +19 -0
  132. package/src/lib/fields/slug/slug.ts +67 -0
  133. package/src/lib/fields/string-list/string-list.descriptor.ts +11 -0
  134. package/src/lib/fields/string-list/string-list.ts +222 -0
  135. package/src/lib/fields/text/text.descriptor.ts +12 -0
  136. package/src/lib/fields/text/text.ts +35 -0
  137. package/src/lib/fields/textarea/textarea.descriptor.ts +12 -0
  138. package/src/lib/fields/textarea/textarea.spec.ts +117 -0
  139. package/src/lib/fields/textarea/textarea.ts +126 -0
  140. package/src/lib/fields/time/time.descriptor.ts +12 -0
  141. package/src/lib/fields/time/time.ts +28 -0
  142. package/src/lib/fields/unknown-render/unknown-render.ts +49 -0
  143. package/src/lib/fields/url/url.descriptor.ts +12 -0
  144. package/src/lib/fields/url/url.ts +53 -0
  145. package/src/lib/gas-admin-ui.spec.ts +21 -0
  146. package/src/lib/gas-admin-ui.ts +9 -0
  147. package/src/lib/layouts/descriptors.ts +9 -0
  148. package/src/lib/pages/dashboard/dashboard.ts +167 -0
  149. package/src/lib/pages/detail-view/detail-view.spec.ts +333 -0
  150. package/src/lib/pages/detail-view/detail-view.ts +570 -0
  151. package/src/lib/pages/drawer-form/drawer-form.ts +228 -0
  152. package/src/lib/pages/form-view/form-model.ts +88 -0
  153. package/src/lib/pages/form-view/form-schema.ts +255 -0
  154. package/src/lib/pages/form-view/form-view.ts +1622 -0
  155. package/src/lib/pages/list-view/list-view.formats.spec.ts +214 -0
  156. package/src/lib/pages/list-view/list-view.ts +882 -0
  157. package/src/lib/pages/login/login.spec.ts +109 -0
  158. package/src/lib/pages/login/login.ts +227 -0
  159. package/src/lib/pages/resource-page/resource-page.ts +234 -0
  160. package/src/lib/shared/components/badge/badge.ts +35 -0
  161. package/src/lib/shared/components/button/button.ts +65 -0
  162. package/src/lib/shared/components/confirm-dialog/confirm-dialog.ts +48 -0
  163. package/src/lib/shared/components/copy-button/copy-button.ts +56 -0
  164. package/src/lib/shared/components/copy-share/copy-share.spec.ts +122 -0
  165. package/src/lib/shared/components/copy-share/copy-share.ts +107 -0
  166. package/src/lib/shared/components/drawer/drawer.spec.ts +127 -0
  167. package/src/lib/shared/components/drawer/drawer.ts +172 -0
  168. package/src/lib/shared/components/empty-state/empty-state.ts +30 -0
  169. package/src/lib/shared/components/modal/modal.spec.ts +149 -0
  170. package/src/lib/shared/components/modal/modal.ts +145 -0
  171. package/src/lib/shared/components/post-create-modal/post-create-modal.spec.ts +165 -0
  172. package/src/lib/shared/components/post-create-modal/post-create-modal.ts +159 -0
  173. package/src/lib/shared/components/skeleton/skeleton.ts +31 -0
  174. package/src/lib/shared/components/toast/toast.ts +58 -0
  175. package/src/lib/shared/utils/col-span.ts +25 -0
  176. package/src/lib/shared/utils/condition-evaluator.ts +53 -0
  177. package/src/lib/shared/utils/depends-on.ts +48 -0
  178. package/src/lib/shared/utils/dialog-focus.ts +28 -0
  179. package/src/lib/shared/utils/field-path.ts +49 -0
  180. package/src/lib/shared/utils/gap-sorting.ts +144 -0
  181. package/src/lib/shared/utils/seed.spec.ts +64 -0
  182. package/src/lib/shared/utils/seed.ts +149 -0
  183. package/src/lib/shared/utils/slugify.ts +13 -0
  184. package/src/lib/shared/utils/validators.ts +41 -0
  185. package/src/lib/shell/layout/layout.ts +63 -0
  186. package/src/lib/shell/shell.routes.spec.ts +59 -0
  187. package/src/lib/shell/shell.routes.ts +122 -0
  188. package/src/lib/shell/sidebar/sidebar.ts +139 -0
  189. package/src/lib/shell/theme-toggle/theme-toggle.ts +74 -0
  190. package/src/lib/views/calendar-view/calendar-model.ts +426 -0
  191. package/src/lib/views/calendar-view/calendar-view.spec.ts +557 -0
  192. package/src/lib/views/calendar-view/calendar-view.ts +568 -0
  193. package/src/lib/views/composer/composer-host.spec.ts +301 -0
  194. package/src/lib/views/composer/composer-host.ts +248 -0
  195. package/src/lib/views/composer/composer-imports.spec.ts +54 -0
  196. package/src/lib/views/composer/view-context.spec.ts +117 -0
  197. package/src/lib/views/composer/view-context.ts +109 -0
  198. package/src/lib/views/composer/view-slot.spec.ts +130 -0
  199. package/src/lib/views/composer/view-slot.ts +178 -0
  200. package/src/lib/views/descriptors.ts +46 -0
  201. package/src/lib/views/kanban-view/kanban-model.ts +173 -0
  202. package/src/lib/views/kanban-view/kanban-view.spec.ts +540 -0
  203. package/src/lib/views/kanban-view/kanban-view.ts +327 -0
  204. package/src/lib/widgets/bar-chart/bar-chart.descriptor.ts +17 -0
  205. package/src/lib/widgets/bar-chart/bar-chart.spec.ts +66 -0
  206. package/src/lib/widgets/bar-chart/bar-chart.ts +76 -0
  207. package/src/lib/widgets/chart-line/chart-line.descriptor.ts +17 -0
  208. package/src/lib/widgets/chart-line/chart-line.spec.ts +113 -0
  209. package/src/lib/widgets/chart-line/chart-line.ts +219 -0
  210. package/src/lib/widgets/descriptors.ts +29 -0
  211. package/src/lib/widgets/metric-card/metric-card.descriptor.ts +41 -0
  212. package/src/lib/widgets/metric-card/metric-card.spec.ts +52 -0
  213. package/src/lib/widgets/metric-card/metric-card.ts +48 -0
  214. package/src/lib/widgets/quick-actions/quick-actions.descriptor.ts +13 -0
  215. package/src/lib/widgets/quick-actions/quick-actions.spec.ts +61 -0
  216. package/src/lib/widgets/quick-actions/quick-actions.ts +100 -0
  217. package/src/lib/widgets/record-list/record-list.descriptor.ts +16 -0
  218. package/src/lib/widgets/record-list/record-list.spec.ts +79 -0
  219. package/src/lib/widgets/record-list/record-list.ts +115 -0
  220. package/src/lib/widgets/series.spec.ts +42 -0
  221. package/src/lib/widgets/series.ts +56 -0
  222. package/src/lib/widgets/status-progress/status-progress.descriptor.ts +19 -0
  223. package/src/lib/widgets/status-progress/status-progress.spec.ts +63 -0
  224. package/src/lib/widgets/status-progress/status-progress.ts +104 -0
  225. package/src/lib/widgets/widget-host/widget-host.spec.ts +179 -0
  226. package/src/lib/widgets/widget-host/widget-host.ts +99 -0
  227. package/src/public-api.ts +104 -0
  228. package/tinya-code-gas-admin-ui-0.2.0-alpha.15.tgz +0 -0
  229. package/tsconfig.lib.json +13 -0
  230. package/tsconfig.lib.prod.json +11 -0
  231. package/tsconfig.spec.json +10 -0
  232. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs +0 -84
  233. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs.map +0 -1
  234. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs +0 -140
  235. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs.map +0 -1
  236. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs +0 -113
  237. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs.map +0 -1
  238. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs +0 -163
  239. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs.map +0 -1
  240. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs +0 -1027
  241. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs.map +0 -1
  242. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs +0 -278
  243. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs.map +0 -1
  244. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs +0 -107
  245. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs.map +0 -1
  246. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs +0 -101
  247. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs.map +0 -1
  248. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs +0 -141
  249. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs.map +0 -1
  250. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs +0 -88
  251. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs.map +0 -1
  252. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs +0 -107
  253. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs.map +0 -1
  254. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs +0 -140
  255. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs.map +0 -1
  256. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs +0 -119
  257. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs.map +0 -1
  258. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs +0 -125
  259. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs.map +0 -1
  260. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs +0 -152
  261. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs.map +0 -1
  262. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs +0 -211
  263. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs.map +0 -1
  264. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs +0 -116
  265. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs.map +0 -1
  266. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs +0 -111
  267. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs.map +0 -1
  268. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs +0 -122
  269. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs.map +0 -1
  270. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs +0 -107
  271. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs.map +0 -1
  272. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs +0 -109
  273. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs.map +0 -1
  274. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs +0 -45
  275. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs.map +0 -1
  276. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs +0 -50
  277. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs.map +0 -1
  278. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs +0 -24
  279. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs.map +0 -1
  280. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs +0 -27
  281. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs.map +0 -1
  282. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs +0 -408
  283. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs.map +0 -1
  284. package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs +0 -4516
  285. package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs.map +0 -1
  286. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs +0 -82
  287. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs.map +0 -1
  288. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs +0 -105
  289. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs.map +0 -1
  290. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs +0 -65
  291. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs.map +0 -1
  292. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs +0 -65
  293. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs.map +0 -1
  294. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs +0 -65
  295. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs.map +0 -1
  296. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs +0 -59
  297. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs.map +0 -1
  298. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs +0 -94
  299. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs.map +0 -1
  300. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs +0 -72
  301. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs.map +0 -1
  302. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs +0 -238
  303. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs.map +0 -1
  304. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs +0 -416
  305. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs.map +0 -1
  306. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs +0 -439
  307. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs.map +0 -1
  308. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs +0 -513
  309. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs.map +0 -1
  310. package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs +0 -637
  311. package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs.map +0 -1
  312. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs +0 -127
  313. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs.map +0 -1
  314. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs +0 -251
  315. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs.map +0 -1
  316. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs +0 -66
  317. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs.map +0 -1
  318. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs +0 -114
  319. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs.map +0 -1
  320. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs +0 -116
  321. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs.map +0 -1
  322. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs +0 -235
  323. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs.map +0 -1
  324. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs +0 -8
  325. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs.map +0 -1
  326. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs +0 -124
  327. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs.map +0 -1
  328. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs +0 -249
  329. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs.map +0 -1
  330. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs +0 -39
  331. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs.map +0 -1
  332. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs +0 -309
  333. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs.map +0 -1
  334. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs +0 -576
  335. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs.map +0 -1
  336. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs +0 -161
  337. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs.map +0 -1
  338. package/fesm2022/tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs +0 -11
  339. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs +0 -685
  340. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs.map +0 -1
  341. package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs +0 -80
  342. package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs.map +0 -1
  343. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs +0 -296
  344. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs.map +0 -1
  345. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs +0 -119
  346. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs.map +0 -1
  347. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs +0 -158
  348. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs.map +0 -1
  349. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs +0 -26
  350. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs.map +0 -1
  351. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs +0 -205
  352. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs.map +0 -1
  353. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs +0 -432
  354. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs.map +0 -1
  355. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs +0 -36
  356. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs.map +0 -1
  357. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs +0 -107
  358. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs.map +0 -1
  359. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs +0 -223
  360. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs.map +0 -1
  361. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs +0 -107
  362. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs.map +0 -1
  363. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs +0 -60
  364. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs.map +0 -1
  365. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs +0 -141
  366. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs.map +0 -1
  367. package/fesm2022/tinya-code-gas-admin-ui.mjs +0 -4599
  368. package/fesm2022/tinya-code-gas-admin-ui.mjs.map +0 -1
  369. package/types/tinya-code-gas-admin-ui.d.ts +0 -3632
@@ -0,0 +1,503 @@
1
+ import { HttpClient, HttpHeaders } from '@angular/common/http';
2
+ import { computed, inject, Service, signal } from '@angular/core';
3
+ import { Router } from '@angular/router';
4
+ import { catchError, from, map, Observable, of, switchMap, tap, throwError } from 'rxjs';
5
+
6
+ import type { FieldSchema, ResourceSchema } from '../models/schema.model';
7
+ import { renewedTokenOf } from '../models/api.model';
8
+ import { API_DRIVER, API_URL, FIREBASE_AUTH_ADAPTER } from '../tokens';
9
+ import { getAuth } from '../registry/schema-catalog';
10
+
11
+
12
+ /**
13
+ * Sesión del admin (auth.md §5.2) — **única dueña** del token y del usuario.
14
+ *
15
+ * Qué vive acá y por qué:
16
+ *
17
+ * - **Señales** `user` / `token` / `isAuthenticated` / `role`: el único
18
+ * estado que el resto de la app lee (guards, menú, interceptor).
19
+ * - **`login()` / `logout()` / `logoutAll()` / `restore()`**: operaciones de sesión.
20
+ * El `AuthService` NO inyecta `ApiService` — sólo `HttpClient` — porque
21
+ * `ApiService` inyecta a éste para leer el token, y un ciclo de DI en
22
+ * Angular es un error en tiempo de arranque, no un warning.
23
+ * - **`applyAuthCode()` (R2.9): EL único intérprete de la tabla de §4.5.**
24
+ * Ningún otro archivo decide qué pasa ante `AUTH_*` o `FORBIDDEN`.
25
+ * - **Almacenamiento (R2.12): `sessionStorage`, siempre.** `rememberMe`
26
+ * no puede usar `localStorage` porque D10-3 lo negó — ver más abajo.
27
+ *
28
+ * La sesión arranca desde el storage y `restore()` la revalida contra
29
+ * `/auth/me` en el initializer: un usuario desactivado mientras la pestaña
30
+ * estaba abierta cae al primer reload, no en mitad de una edición.
31
+ */
32
+
33
+ /** Usuario tal y como lo sirve `publicUser_` (lista blanca, R2.17). */
34
+ export interface SessionUser {
35
+ id?: string;
36
+ email?: string;
37
+ name?: string;
38
+ role?: string;
39
+ active?: boolean;
40
+ last_login_at?: string;
41
+ created_at?: string;
42
+ }
43
+
44
+ /** Los cinco códigos de §4.5. El resto de errores no son de sesión. */
45
+ export type AuthErrorCode =
46
+ 'AUTH_REQUIRED' | 'AUTH_INVALID' | 'AUTH_EXPIRED' | 'FORBIDDEN' | 'RATE_LIMITED';
47
+
48
+ /** Ruta de login: fuera del shell para que ningún guard la proteja. */
49
+ export const LOGIN_PATH = '/login';
50
+
51
+ /**
52
+ * Claves del `sessionStorage`. Exportadas porque **la sesión se restaura
53
+ * desde ahí en el constructor**: un test que quiera una sesión arrancando
54
+ * tiene que sembrarlas antes de inyectar el servicio, y el que las use con
55
+ * otro nombre quebraría el restore en silencio.
56
+ */
57
+ export const SESSION_TOKEN_KEY = 'admin.session.token';
58
+ export const SESSION_USER_KEY = 'admin.session.user';
59
+
60
+ interface LoginResponse {
61
+ token: string;
62
+ user: SessionUser;
63
+ }
64
+ interface MeResponse {
65
+ user: SessionUser;
66
+ }
67
+
68
+ @Service()
69
+ export class AuthService {
70
+ private readonly http = inject(HttpClient);
71
+ private readonly apiUrl = inject(API_URL);
72
+ private readonly router = inject(Router);
73
+ private readonly firebaseAdapter = inject(FIREBASE_AUTH_ADAPTER, { optional: true });
74
+ private readonly driver = inject(API_DRIVER, { optional: true }) ?? 'gas';
75
+
76
+ /**
77
+ * D10-3: **sólo `sessionStorage`.** La opción «recordarme» con
78
+ * `localStorage` quedó denegada en §9.3 — muere con la pestaña ⇒ cero
79
+ * token legible por un XSS futuro. `authSchema.rememberMe` es `false`
80
+ * por la misma razón: declarar `true` sería prometer un storage que
81
+ * este servicio no usa.
82
+ *
83
+ * Nunca en la URL ni en el cuerpo de un GET (§5.4): el token sólo viaja
84
+ * dentro del envelope `{ token, method, path, payload }`.
85
+ */
86
+ private readonly storage: Storage = sessionStorage;
87
+
88
+ private readonly _user = signal<SessionUser | null>(null);
89
+ private readonly _token = signal<string | null>(null);
90
+ /** Ruta previa a un `AUTH_EXPIRED`, para volver tras reloguear (§4.5). */
91
+ private readonly _returnUrl = signal<string | null>(null);
92
+
93
+ readonly user = this._user.asReadonly();
94
+ readonly token = this._token.asReadonly();
95
+ readonly returnUrl = this._returnUrl.asReadonly();
96
+ /** Sólo hay sesión con token Y usuario: uno sin el otro es estado corrupto. */
97
+ readonly isAuthenticated = computed(() => this._token() !== null && this._user() !== null);
98
+ readonly role = computed(() => this._user()?.role ?? '');
99
+
100
+ constructor() {
101
+ const token = this.storage.getItem(SESSION_TOKEN_KEY);
102
+ if (token !== null && token !== '') this._token.set(token);
103
+ const raw = this.storage.getItem(SESSION_USER_KEY);
104
+ if (raw !== null && raw !== '') {
105
+ try {
106
+ this._user.set(JSON.parse(raw) as SessionUser);
107
+ } catch {
108
+ // JSON corrupto ⇒ no hay sesión que restaurar; storage sucio.
109
+ this.storage.removeItem(SESSION_USER_KEY);
110
+ this.storage.removeItem(SESSION_TOKEN_KEY);
111
+ this._token.set(null);
112
+ }
113
+ }
114
+ }
115
+
116
+ // ── Sesión ───────────────────────────────────────────────────────────────
117
+
118
+ login(email: string, password: string): Observable<SessionUser> {
119
+ return this.post<LoginResponse>('/auth/login', { email, password }).pipe(
120
+ map((res) => {
121
+ this.open(res.token, res.user);
122
+ const renewed = renewedTokenOf(res);
123
+ if (renewed !== null) this.applyRenewedToken(renewed);
124
+ return res.user;
125
+ }),
126
+ );
127
+ }
128
+
129
+ /**
130
+ * Inicia sesión con Google mediante el adaptador de Firebase y canjea
131
+ * el ID token contra el backend.
132
+ */
133
+ loginWithGoogle(): Observable<SessionUser> {
134
+ if (!this.firebaseAdapter) {
135
+ return throwError(() => new Error('[gas-admin-ui] FIREBASE_AUTH_ADAPTER is not provided.'));
136
+ }
137
+ return from(this.firebaseAdapter.signInWithGoogle()).pipe(
138
+ switchMap((idToken) => this.exchangeFirebaseToken(idToken)),
139
+ );
140
+ }
141
+
142
+ /**
143
+ * Inicia sesión con correo y contraseña en Firebase y canjea el ID token
144
+ * resultante contra el backend.
145
+ */
146
+ loginWithFirebasePassword(email: string, password: string): Observable<SessionUser> {
147
+ if (!this.firebaseAdapter) {
148
+ return throwError(() => new Error('[gas-admin-ui] FIREBASE_AUTH_ADAPTER is not provided.'));
149
+ }
150
+ return from(this.firebaseAdapter.signInWithEmailAndPassword(email, password)).pipe(
151
+ switchMap((idToken) => this.exchangeFirebaseToken(idToken)),
152
+ );
153
+ }
154
+
155
+ /**
156
+ * Canjea un token de identidad de Firebase por la sesión propia de la app,
157
+ * manteniendo idéntico el contrato del envelope y almacenamiento en sessionStorage.
158
+ */
159
+ private exchangeFirebaseToken(idToken: string): Observable<SessionUser> {
160
+ const endpoint = getAuth()?.firebase?.exchangeEndpoint ?? '/auth/firebase';
161
+ return this.post<LoginResponse>(endpoint, { idToken }).pipe(
162
+ map((res) => {
163
+ this.open(res.token, res.user);
164
+ const renewed = renewedTokenOf(res);
165
+ if (renewed !== null) this.applyRenewedToken(renewed);
166
+ return res.user;
167
+ }),
168
+ );
169
+ }
170
+
171
+ /**
172
+ * Revocación best-effort (§9.7): se pide al back que ponga el `sid` en la
173
+ * lista de revocados, pero **la sesión local se ciere haya o no respuesta**.
174
+ * Un logout que «falla» por red y deja al usuario dentro es peor que uno
175
+ * que deja una sesión muerta en el servidor.
176
+ */
177
+ logout(): Observable<void> {
178
+ return this.post<{ loggedOut?: boolean }>('/auth/logout').pipe(
179
+ tap({
180
+ next: () => {
181
+ this.clear();
182
+ void this.firebaseAdapter?.signOut();
183
+ this.goLogin();
184
+ },
185
+ error: () => {
186
+ // La sesión local se cierra haya o no respuesta del back (§9.7).
187
+ // Firebase también se cierra best-effort y se va al login igual.
188
+ this.clear();
189
+ void this.firebaseAdapter?.signOut();
190
+ this.goLogin();
191
+ },
192
+ }),
193
+ map(() => undefined),
194
+ );
195
+ }
196
+
197
+
198
+ /** Revalida la sesión restaurada contra `/auth/me`; sin token ⇒ sin viaje. */
199
+ restore(): Observable<void> {
200
+ if (this._token() === null) return of(undefined);
201
+ return this.post<MeResponse>('/auth/me').pipe(
202
+ tap((res) => {
203
+ this.open(this._token(), res.user);
204
+ }),
205
+ // El interceptor ya interpretó el `AUTH_*` (R2.9) antes de llegar
206
+ // acá: esto sólo evita que un fallo rompa el arranque de la app.
207
+ catchError(() => of(undefined)),
208
+ map(() => undefined),
209
+ );
210
+ }
211
+
212
+ /**
213
+ * Renovación deslizante (§3.2): el back manda `meta.renewedToken` cuando
214
+ * la vida del token bajó del umbral. Se persiste el MISMO día que el
215
+ * token original — el `sid` no cambia, sólo se emite una firma nueva.
216
+ */
217
+ applyRenewedToken(token: string): void {
218
+ if (token === '' || this._token() === null) return;
219
+ this._token.set(token);
220
+ this.storage.setItem(SESSION_TOKEN_KEY, token);
221
+ }
222
+
223
+ // ── R2.9 · la tabla de §4.5, en un solo sitio ───────────────────────────
224
+
225
+ /**
226
+ * ÚNICO intérprete de los códigos de sesión (auth.md §4.5 / §5.2).
227
+ * El interceptor `apiErrorInterceptor` lo llama y nada más: cualquier
228
+ * archivo que decida «si ve AUTH_EXPIRED hago X» está fuera de contrato.
229
+ *
230
+ * | Código | Qué hace |
231
+ * | --------------- | ------------------------------------------- |
232
+ * | `AUTH_REQUIRED` | limpia sesión y va a login |
233
+ * | `AUTH_INVALID` | limpia sesión y va a login |
234
+ * | `AUTH_EXPIRED` | guarda la ruta previa, limpia, va a login |
235
+ * | `FORBIDDEN` | **no** cierra sesión (el usuario sigue dentro) |
236
+ * | `RATE_LIMITED` | no hace nada: lo espera el formulario |
237
+ */
238
+ applyAuthCode(code: string | undefined): void {
239
+ switch (code) {
240
+ case 'AUTH_EXPIRED':
241
+ // La ruta previa se guarda ANTES de borrar: después de limpiar no
242
+ // queda desde dónde navegar (y `router.url` ya cambió al llegar).
243
+ this._returnUrl.set(this.currentUrl());
244
+ this.clear();
245
+ this.goLogin();
246
+ break;
247
+ case 'AUTH_REQUIRED':
248
+ case 'AUTH_INVALID':
249
+ // «La sesión terminó» — no hay vuelta atrás que conservar.
250
+ this.clear();
251
+ this.goLogin();
252
+ break;
253
+ case 'FORBIDDEN':
254
+ case 'RATE_LIMITED':
255
+ // Decisión explícita de NO tocar la sesión: §4.5 exige que el
256
+ // usuario vea «sin permiso» o «espera» **siguiendo dentro**.
257
+ break;
258
+ default:
259
+ // No es un código de sesión: nada que interpretar.
260
+ break;
261
+ }
262
+ }
263
+
264
+ // ── Helpers ──────────────────────────────────────────────────────────────
265
+
266
+ /** ¿El rol de la sesión cubre `required`? (mínimo: admin ≥ staff, §4.4) */
267
+ hasRole(required: string): boolean {
268
+ if (required === '' || required === 'session') return this.isAuthenticated();
269
+ const role = this.role();
270
+ if (role === 'admin') return true;
271
+ return role !== '' && role === required;
272
+ }
273
+
274
+ /**
275
+ * §5.3 — el ÚNICO criterio de visibilidad del front (menú, rutas y
276
+ * botones). `requires` viene del vocabulario de §4.4: `public` no pide
277
+ * sesión; todo lo demás sí; `roles` estrecha.
278
+ *
279
+ * Es **pura decisión de UI**. El back re-verifica `accessLevel` en cada
280
+ * petición (R2.6), así que filtrar acá no protege nada: sólo evita
281
+ * enseñarle a alguien un botón que le va a devolver 403.
282
+ */
283
+ canAccess(access?: { requires?: string; roles?: string[] }): boolean {
284
+ const requires = access?.requires ?? 'session';
285
+ if (requires === 'public') return true;
286
+ if (!this.isAuthenticated()) return false;
287
+
288
+ // §4.4 escribe el requerimiento como `role:admin`; §5.3 como
289
+ // `roles: ['admin']`. Se aceptan las dos formas en lugar de elegir una:
290
+ // son el mismo dato visto desde el nivel de acceso y desde el recurso.
291
+ const role = requires.startsWith('role:') ? requires.slice('role:'.length) : '';
292
+ const required = role === '' ? (access?.roles ?? []) : [role];
293
+ if (required.length === 0) return true;
294
+ return required.some((one) => this.hasRole(one));
295
+ }
296
+
297
+ // ── R7.5 · Consumo de políticas en UI (F10) ───────────────────────────────
298
+
299
+ /**
300
+ * Evalúa si el usuario puede ejecutar una operación específica según
301
+ * `policies.access.operations` del recurso (B8 · R7.4a).
302
+ *
303
+ * El override por operación tiene prioridad sobre el umbral del grupo.
304
+ * Si no hay override, se usa el umbral `policies.access.read` o
305
+ * `policies.access.write` según la operación.
306
+ *
307
+ * Fallback a `permissions` (legacy) si `policies` no existe: mientras
308
+ * los schemas locales no tengan `policies`, se mantiene compatibilidad.
309
+ *
310
+ * Es **pura decisión de UI**. El back vuelve a verificar en cada petición
311
+ * (24-crud), así que esto sólo evita mostrar botones que darían 403.
312
+ */
313
+ canDoOperation(
314
+ schema: ResourceSchema,
315
+ operation: 'list' | 'get' | 'create' | 'update' | 'remove',
316
+ ): boolean {
317
+ // Un recurso sin endpoint declarado para la operación no puede ejecutarla,
318
+ // aunque el rol alcance: no hay a dónde llamar. Evita botones/formularios
319
+ // de creación/edición/borrado en recursos de sólo lectura (R10).
320
+ // Sólo se aplica cuando el schema DECLARA un `endpoint` (si no lo declara,
321
+ // se mantiene el comportamiento legacy optimista que los tests esperan).
322
+ if (operation === 'create' || operation === 'update' || operation === 'remove') {
323
+ const ep = schema.endpoint;
324
+ const declaresEndpoints = !!ep && Object.keys(ep).length > 0;
325
+ if (declaresEndpoints && !ep![operation]) {
326
+ return false;
327
+ }
328
+ }
329
+
330
+ // R7.5: intentar policies primero
331
+ const ops = schema.policies?.access?.operations;
332
+ if (ops && ops[operation]) {
333
+ return this.meetsLevel(ops[operation]);
334
+ }
335
+
336
+ // Sin override en policies: usar el umbral del grupo
337
+ if (schema.policies?.access) {
338
+ const isWrite = operation === 'create' || operation === 'update' || operation === 'remove';
339
+ const groupLevel = isWrite ? schema.policies.access.write : schema.policies.access.read;
340
+ return this.meetsLevel(groupLevel ?? 'admin');
341
+ }
342
+
343
+ // Fallback a permissions (legacy, R7.5 transición)
344
+ const perms = schema.permissions;
345
+ if (perms) {
346
+ const permMap: Record<string, boolean | undefined> = {
347
+ list: true, // list no tiene en permissions, siempre true si no hay policies
348
+ get: true,
349
+ create: perms.create,
350
+ update: perms.update,
351
+ remove: perms.remove,
352
+ };
353
+ return permMap[operation] !== false;
354
+ }
355
+
356
+ // Sin policies ni permissions: UX optimista (§5.3 legacy) — el back es
357
+ // la autoridad y responde 403 si el rol no alcanza.
358
+ return true;
359
+ }
360
+
361
+ /**
362
+ * Evalúa si el usuario puede leer un campo según `field.access.read`
363
+ * (B8 · R7.4b).
364
+ *
365
+ * Es **pura decisión de UI**. El back vuelve a filtrar la respuesta en
366
+ * cada petición (24-crud), así que esto sólo evita mostrar datos que el
367
+ * usuario no debería ver.
368
+ */
369
+ canReadField(field: FieldSchema): boolean {
370
+ if (!field.access || !field.access.read) return true; // sin restricción ⇒ visible
371
+ return this.meetsLevel(field.access.read);
372
+ }
373
+
374
+ /**
375
+ * Evalúa si el usuario puede escribir un campo según `field.access.write`
376
+ * (B8 · R7.4b).
377
+ *
378
+ * Es **pura decisión de UI**. El back rechaza la escritura con 422 si el
379
+ * usuario no tiene permiso, así que esto sólo pone el campo en
380
+ * solo-lectura en el frontend.
381
+ */
382
+ canWriteField(field: FieldSchema): boolean {
383
+ if (!field.access || !field.access.write) return true; // sin restricción ⇒ editable
384
+ return this.meetsLevel(field.access.write);
385
+ }
386
+
387
+ /**
388
+ * Verifica si el rol actual cumple un nivel de acceso dado.
389
+ * Usa la misma jerarquía que `ACCESS_ROLE_RANK` del back (11-auth):
390
+ * admin ≥ staff.
391
+ */
392
+ private meetsLevel(level: string): boolean {
393
+ if (level === 'public') return true;
394
+ if (level === 'session') return this.isAuthenticated();
395
+ if (level.startsWith('role:')) {
396
+ const requiredRole = level.slice('role:'.length);
397
+ return this.hasRole(requiredRole);
398
+ }
399
+ // fallback conservador: nivel desconocido ⇒ sólo admin
400
+ return this.hasRole('admin');
401
+ }
402
+
403
+ private open(token: string | null, user: SessionUser | null): void {
404
+ if (token !== null && token !== '') this._token.set(token);
405
+ this._user.set(user);
406
+ if (token !== null && token !== '') this.storage.setItem(SESSION_TOKEN_KEY, token);
407
+ if (user !== null) this.storage.setItem(SESSION_USER_KEY, JSON.stringify(user));
408
+ }
409
+
410
+ /**
411
+ * Borra token **y** usuario (§5.4) — en ese orden de importancia: un token
412
+ * sin usuario no autentica, pero un usuario sin token engaña al menú.
413
+ */
414
+ /**
415
+ * Ruta previa guardada por un `AUTH_EXPIRED` (§4.5), **consumida**: se
416
+ * devuelve una sola vez y se borra. Sin el consume, cada login futuro
417
+ * volvería a la misma URL vieja aunque el usuario hubiera llegado al
418
+ * login desde el menú.
419
+ */
420
+ consumeReturnUrl(): string | null {
421
+ const url = this._returnUrl();
422
+ this._returnUrl.set(null);
423
+ return url;
424
+ }
425
+
426
+ private clear(): void {
427
+ this._token.set(null);
428
+ this._user.set(null);
429
+ this.storage.removeItem(SESSION_TOKEN_KEY);
430
+ this.storage.removeItem(SESSION_USER_KEY);
431
+ }
432
+
433
+ private goLogin(): void {
434
+ if (this.currentUrl().startsWith(LOGIN_PATH)) return;
435
+ // Angular rechaza la promesa cuando no hay ruta que empate (NG04002).
436
+ // Navegar es un efecto, no una operación cuyo resultado alguien espere:
437
+ // el catch evita una rejection sin handler en arranques y en tests.
438
+ void this.router.navigate([LOGIN_PATH]).catch(() => undefined);
439
+ }
440
+
441
+ private currentUrl(): string {
442
+ return this.router.url;
443
+ }
444
+
445
+ private buildUrl(path: string): string {
446
+ const base = this.apiUrl ? this.apiUrl.replace(/\/+$/, '') : '';
447
+ const cleanPath = path.startsWith('/') ? path : `/${path}`;
448
+ return `${base}${cleanPath}`;
449
+ }
450
+
451
+ private envelope(path: string, payload: Record<string, unknown> = {}) {
452
+ return { token: this._token(), method: 'POST' as const, path, payload };
453
+ }
454
+
455
+ /**
456
+ * Despacho dual-driver para peticiones de autenticación:
457
+ *
458
+ * - **`gas` (default)**: `POST` a `API_URL` con cuerpo `text/plain` y envelope
459
+ * `{ token, method: 'POST', path, payload }`.
460
+ * - **`rest`**: llamadas REST reales:
461
+ * - `/auth/me`: `GET` a `${apiUrl}/auth/me` con `Authorization: Bearer ${token}`.
462
+ * - resto (`/auth/login`, `/auth/firebase`, `/auth/logout`, etc.): `POST` con
463
+ * `Content-Type: application/json` y payload directo como body JSON. Si existe
464
+ * token activo, se adjunta en cabecera `Authorization`.
465
+ */
466
+ private buildAuthRequest<T>(
467
+ path: string,
468
+ payload: Record<string, unknown> = {},
469
+ ): Observable<T> {
470
+ if (this.driver === 'rest') {
471
+ const url = this.buildUrl(path);
472
+ let headers = new HttpHeaders();
473
+ const token = this._token();
474
+ if (token) {
475
+ headers = headers.set('Authorization', `Bearer ${token}`);
476
+ }
477
+
478
+ if (path === '/auth/me') {
479
+ return this.http.get<T>(url, { headers });
480
+ }
481
+
482
+ headers = headers.set('Content-Type', 'application/json');
483
+ return this.http.post<T>(url, payload, { headers });
484
+ }
485
+
486
+ return this.http.post<T>(this.apiUrl, this.envelope(path, payload), {
487
+ headers: { 'Content-Type': 'text/plain' },
488
+ });
489
+ }
490
+
491
+ private post<T>(path: string, payload: Record<string, unknown> = {}): Observable<T> {
492
+ return this.buildAuthRequest<T>(path, payload).pipe(
493
+ // Loyalty API (y otros backends) envuelven toda respuesta en `{ data }`;
494
+ // el interceptor de la app hacía este desenvolvimiento. AuthService no pasa
495
+ // por ApiService, así que lo hace acá para no perder token/usuario/meta.
496
+ map((res: unknown) =>
497
+ res && typeof res === 'object' && !Array.isArray(res) && 'data' in (res as Record<string, unknown>)
498
+ ? ((res as Record<string, unknown>)['data'] as T)
499
+ : (res as T),
500
+ ),
501
+ );
502
+ }
503
+ }
@@ -0,0 +1,121 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { Subject, map, of, throwError } from 'rxjs';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import type { ResourceEndpoints } from '../models/schema.model';
6
+ import { ApiService } from './api.service';
7
+ import { ConfigService } from './config.service';
8
+
9
+ const SITE: ResourceEndpoints = { get: '/admin/site' };
10
+ const PICKPASS: ResourceEndpoints = { get: '/admin/pickpass' };
11
+
12
+ describe('ConfigService', () => {
13
+ const getSpy = vi.fn();
14
+
15
+ const svc = (): ConfigService => TestBed.inject(ConfigService);
16
+
17
+ /** Resuelve el último intento de `ensureLoaded` (o el `load` disparado). */
18
+ const settle = () => new Promise<void>((resolve) => setTimeout(resolve));
19
+
20
+ beforeEach(() => {
21
+ getSpy.mockReset();
22
+ TestBed.configureTestingModule({
23
+ providers: [{ provide: ApiService, useValue: { get: getSpy } }],
24
+ });
25
+ });
26
+
27
+ afterEach(() => {
28
+ vi.useRealTimers();
29
+ });
30
+
31
+ it('load() mezcla los singletons en globalConfig', async () => {
32
+ getSpy.mockImplementation((ep: ResourceEndpoints) =>
33
+ ep.get === '/admin/site'
34
+ ? of({ public_base_url: 'https://pasteleria.example' })
35
+ : of({ share_message_template: 'Hola {customer_name}' }),
36
+ );
37
+
38
+ svc().load([SITE, PICKPASS]);
39
+ await settle();
40
+
41
+ expect(svc().globalConfig()['public_base_url']).toBe('https://pasteleria.example');
42
+ expect(svc().globalConfig()['share_message_template']).toBe('Hola {customer_name}');
43
+ });
44
+
45
+ it('un singleton que falla no rompe la carga y deja la config parcial', async () => {
46
+ getSpy.mockImplementation((ep: ResourceEndpoints) =>
47
+ ep.get === '/admin/site'
48
+ ? of({ public_base_url: 'https://pasteleria.example' })
49
+ : throwError(() => new Error('401')),
50
+ );
51
+
52
+ svc().load([SITE, PICKPASS]);
53
+ await settle();
54
+
55
+ // El singleton vivo aporta sus claves…
56
+ expect(svc().globalConfig()['public_base_url']).toBe('https://pasteleria.example');
57
+ // …y el caído no tumba nada (comportamiento histórico intacto).
58
+ expect(svc().globalConfig()['share_message_template']).toBeUndefined();
59
+ });
60
+
61
+ it('regresión R8.7: si el load del bootstrap falló, ensureLoaded reintenta y trae el template', async () => {
62
+ // Escenario real: la app arrancó SIN sesión → los singletons responden
63
+ // 401 → el catch silencia → sin `share_message_template` el mensaje de
64
+ // `copy-share` salía «sólo la URL». Con sesión ya iniciada, el share
65
+ // pide la config de nuevo.
66
+ getSpy.mockImplementation(() => throwError(() => new Error('401')));
67
+
68
+ const config = svc();
69
+ config.load([SITE, PICKPASS]);
70
+ await settle();
71
+ expect(config.globalConfig()['share_message_template']).toBeUndefined();
72
+
73
+ // …llega el login y `copy-share` llama a `ensureLoaded()`:
74
+ getSpy.mockImplementation((ep: ResourceEndpoints) =>
75
+ ep.get === '/admin/pickpass'
76
+ ? of({ share_message_template: 'Hola {customer_name}, tu pedido {ref}' })
77
+ : of({ public_base_url: 'https://pasteleria.example' }),
78
+ );
79
+ await config.ensureLoaded();
80
+
81
+ expect(config.globalConfig()['share_message_template']).toBe(
82
+ 'Hola {customer_name}, tu pedido {ref}',
83
+ );
84
+ });
85
+
86
+ it('tras una carga completa, ensureLoaded no vuelve a viajar', async () => {
87
+ getSpy.mockImplementation(() => of({ marca: 'ok' }));
88
+
89
+ const config = svc();
90
+ config.load([SITE]);
91
+ await settle();
92
+ expect(getSpy).toHaveBeenCalledTimes(1);
93
+
94
+ await config.ensureLoaded();
95
+ expect(getSpy).toHaveBeenCalledTimes(1);
96
+ });
97
+
98
+ it('ensureLoaded concurrente comparte UN solo viaje', async () => {
99
+ const gate = new Subject<void>();
100
+ getSpy.mockImplementation(() => gate.pipe(map(() => ({ marca: 'ok' }))));
101
+
102
+ const config = svc();
103
+ // `load` dispara la primera carga y registra los endpoints; la llamada
104
+ // concurrente debe apilarse en ese MISMO vuelo, no abrir otro.
105
+ config.load([SITE]);
106
+ const segundo = config.ensureLoaded();
107
+ gate.next(undefined);
108
+ gate.complete();
109
+ await segundo;
110
+
111
+ expect(getSpy).toHaveBeenCalledTimes(1);
112
+ expect(config.globalConfig()['marca']).toBe('ok');
113
+ });
114
+
115
+ it('sin endpoints o sin api resolve sin viajar', async () => {
116
+ const config = svc();
117
+ config.load([]);
118
+ await config.ensureLoaded();
119
+ expect(getSpy).not.toHaveBeenCalled();
120
+ });
121
+ });