@tinya-code/gas-admin-ui 0.2.0-alpha.13 → 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.
- package/ng-package.json +7 -0
- package/package.json +3 -15
- package/src/lib/actions/action-registry.spec.ts +52 -0
- package/src/lib/actions/action-registry.ts +126 -0
- package/src/lib/actions/action-service.spec.ts +215 -0
- package/src/lib/actions/action-service.ts +87 -0
- package/src/lib/actions/descriptors.spec.ts +155 -0
- package/src/lib/actions/descriptors.ts +154 -0
- package/src/lib/columns/column-cell.ts +33 -0
- package/src/lib/columns/column-defaults.spec.ts +99 -0
- package/src/lib/columns/column-defaults.ts +84 -0
- package/src/lib/columns/column-format.spec.ts +274 -0
- package/src/lib/columns/descriptors.ts +113 -0
- package/src/lib/columns/format-badge.ts +51 -0
- package/src/lib/columns/format-boolean.ts +58 -0
- package/src/lib/columns/format-currency.ts +30 -0
- package/src/lib/columns/format-date.ts +35 -0
- package/src/lib/columns/format-number.ts +32 -0
- package/src/lib/columns/format-text.ts +23 -0
- package/src/lib/columns/format-thumbnail.ts +63 -0
- package/src/lib/core/adapters/firebase-auth.adapter.ts +6 -0
- package/src/lib/core/guards/auth.guard.spec.ts +88 -0
- package/src/lib/core/guards/auth.guard.ts +48 -0
- package/src/lib/core/guards/unsaved-changes.guard.ts +16 -0
- package/src/lib/core/interceptors/api-error.interceptor.spec.ts +182 -0
- package/src/lib/core/interceptors/api-error.interceptor.ts +87 -0
- package/src/lib/core/models/api.model.ts +104 -0
- package/src/lib/core/models/schema-descriptions.ts +118 -0
- package/src/lib/core/models/schema.model.ts +1097 -0
- package/src/lib/core/registry/composition-validator.spec.ts +358 -0
- package/src/lib/core/registry/composition-validator.ts +270 -0
- package/src/lib/core/registry/field-validators.spec.ts +199 -0
- package/src/lib/core/registry/field-validators.ts +440 -0
- package/src/lib/core/registry/render-host.spec.ts +208 -0
- package/src/lib/core/registry/render-host.ts +646 -0
- package/src/lib/core/registry/render-registry.spec.ts +96 -0
- package/src/lib/core/registry/render-registry.ts +312 -0
- package/src/lib/core/registry/schema-catalog.ts +156 -0
- package/src/lib/core/services/api.service.spec.ts +1142 -0
- package/src/lib/core/services/api.service.ts +371 -0
- package/src/lib/core/services/auth.service.spec.ts +653 -0
- package/src/lib/core/services/auth.service.ts +503 -0
- package/src/lib/core/services/config.service.spec.ts +121 -0
- package/src/lib/core/services/config.service.ts +119 -0
- package/src/lib/core/services/data-source.service.spec.ts +179 -0
- package/src/lib/core/services/data-source.service.ts +142 -0
- package/src/lib/core/services/error-mapper.service.spec.ts +50 -0
- package/src/lib/core/services/error-mapper.service.ts +49 -0
- package/src/lib/core/services/notification.service.ts +73 -0
- package/src/lib/core/services/relation-drawer.service.ts +41 -0
- package/src/lib/core/services/theme.service.spec.ts +106 -0
- package/src/lib/core/services/theme.service.ts +86 -0
- package/src/lib/core/services/transport.spec.ts +198 -0
- package/src/lib/core/services/transport.ts +164 -0
- package/src/lib/core/services/upload.service.ts +81 -0
- package/src/lib/core/testing/categories.schema.ts +125 -0
- package/src/lib/core/testing/orders.schema.ts +372 -0
- package/src/lib/core/testing/products.schema.ts +258 -0
- package/src/lib/core/testing/site.schema.ts +195 -0
- package/src/lib/core/testing/test-catalog.ts +21 -0
- package/src/lib/core/tokens.ts +112 -0
- package/src/lib/core/utils/clipboard.spec.ts +55 -0
- package/src/lib/core/utils/clipboard.ts +90 -0
- package/src/lib/core/utils/context-resolver.spec.ts +95 -0
- package/src/lib/core/utils/context-resolver.ts +101 -0
- package/src/lib/core/utils/response-unwrapper.spec.ts +70 -0
- package/src/lib/core/utils/response-unwrapper.ts +103 -0
- package/src/lib/core/utils/source-options.spec.ts +96 -0
- package/src/lib/core/utils/source-options.ts +54 -0
- package/src/lib/core/utils/template-interpolator.spec.ts +59 -0
- package/src/lib/core/utils/template-interpolator.ts +56 -0
- package/src/lib/core/view-navigation/view-navigation.service.spec.ts +207 -0
- package/src/lib/core/view-navigation/view-navigation.service.ts +132 -0
- package/src/lib/fields/boolean/boolean.descriptor.ts +11 -0
- package/src/lib/fields/boolean/boolean.ts +40 -0
- package/src/lib/fields/color/color.descriptor.ts +12 -0
- package/src/lib/fields/color/color.ts +34 -0
- package/src/lib/fields/currency/currency.descriptor.ts +12 -0
- package/src/lib/fields/currency/currency.ts +67 -0
- package/src/lib/fields/date/date.descriptor.ts +12 -0
- package/src/lib/fields/date/date.ts +36 -0
- package/src/lib/fields/descriptors.ts +122 -0
- package/src/lib/fields/display/display-badge.ts +65 -0
- package/src/lib/fields/display/display-date.ts +39 -0
- package/src/lib/fields/display/display-defaults.ts +84 -0
- package/src/lib/fields/display/display-file.ts +41 -0
- package/src/lib/fields/display/display-image.ts +54 -0
- package/src/lib/fields/display/display-list.ts +107 -0
- package/src/lib/fields/display/display-number.ts +41 -0
- package/src/lib/fields/display/display-text.ts +42 -0
- package/src/lib/fields/display/display.spec.ts +242 -0
- package/src/lib/fields/dropdown/dropdown.descriptor.ts +12 -0
- package/src/lib/fields/dropdown/dropdown.ts +45 -0
- package/src/lib/fields/email/email.descriptor.ts +12 -0
- package/src/lib/fields/email/email.ts +28 -0
- package/src/lib/fields/field-aria.ts +60 -0
- package/src/lib/fields/field-host/field-host.spec.ts +118 -0
- package/src/lib/fields/field-host/field-host.ts +47 -0
- package/src/lib/fields/field-node.ts +88 -0
- package/src/lib/fields/file/file.descriptor.ts +14 -0
- package/src/lib/fields/file/file.spec.ts +134 -0
- package/src/lib/fields/file/file.ts +281 -0
- package/src/lib/fields/group/group.descriptor.ts +21 -0
- package/src/lib/fields/group/group.ts +110 -0
- package/src/lib/fields/image/image.descriptor.ts +12 -0
- package/src/lib/fields/image/image.spec.ts +117 -0
- package/src/lib/fields/image/image.ts +281 -0
- package/src/lib/fields/key-value/key-value.descriptor.ts +11 -0
- package/src/lib/fields/key-value/key-value.ts +283 -0
- package/src/lib/fields/list/list.descriptor.ts +21 -0
- package/src/lib/fields/list/list.ts +376 -0
- package/src/lib/fields/multiselect/multiselect.descriptor.ts +11 -0
- package/src/lib/fields/multiselect/multiselect.spec.ts +142 -0
- package/src/lib/fields/multiselect/multiselect.ts +152 -0
- package/src/lib/fields/number/number.descriptor.ts +12 -0
- package/src/lib/fields/number/number.ts +40 -0
- package/src/lib/fields/phone/phone.descriptor.ts +12 -0
- package/src/lib/fields/phone/phone.ts +46 -0
- package/src/lib/fields/readonly-text/readonly-text.descriptor.ts +12 -0
- package/src/lib/fields/readonly-text/readonly-text.ts +58 -0
- package/src/lib/fields/relation/relation.descriptor.ts +12 -0
- package/src/lib/fields/relation/relation.spec.ts +193 -0
- package/src/lib/fields/relation/relation.ts +224 -0
- package/src/lib/fields/rich-select/rich-select.descriptor.ts +12 -0
- package/src/lib/fields/rich-select/rich-select.ts +175 -0
- package/src/lib/fields/select/radio-mode.ts +25 -0
- package/src/lib/fields/select/select.descriptor.ts +12 -0
- package/src/lib/fields/select/select.spec.ts +162 -0
- package/src/lib/fields/select/select.ts +210 -0
- package/src/lib/fields/slug/slug.descriptor.ts +19 -0
- package/src/lib/fields/slug/slug.ts +67 -0
- package/src/lib/fields/string-list/string-list.descriptor.ts +11 -0
- package/src/lib/fields/string-list/string-list.ts +222 -0
- package/src/lib/fields/text/text.descriptor.ts +12 -0
- package/src/lib/fields/text/text.ts +35 -0
- package/src/lib/fields/textarea/textarea.descriptor.ts +12 -0
- package/src/lib/fields/textarea/textarea.spec.ts +117 -0
- package/src/lib/fields/textarea/textarea.ts +126 -0
- package/src/lib/fields/time/time.descriptor.ts +12 -0
- package/src/lib/fields/time/time.ts +28 -0
- package/src/lib/fields/unknown-render/unknown-render.ts +49 -0
- package/src/lib/fields/url/url.descriptor.ts +12 -0
- package/src/lib/fields/url/url.ts +53 -0
- package/src/lib/gas-admin-ui.spec.ts +21 -0
- package/src/lib/gas-admin-ui.ts +9 -0
- package/src/lib/layouts/descriptors.ts +9 -0
- package/src/lib/pages/dashboard/dashboard.ts +167 -0
- package/src/lib/pages/detail-view/detail-view.spec.ts +333 -0
- package/src/lib/pages/detail-view/detail-view.ts +570 -0
- package/src/lib/pages/drawer-form/drawer-form.ts +228 -0
- package/src/lib/pages/form-view/form-model.ts +88 -0
- package/src/lib/pages/form-view/form-schema.ts +255 -0
- package/src/lib/pages/form-view/form-view.ts +1622 -0
- package/src/lib/pages/list-view/list-view.formats.spec.ts +214 -0
- package/src/lib/pages/list-view/list-view.ts +882 -0
- package/src/lib/pages/login/login.spec.ts +109 -0
- package/src/lib/pages/login/login.ts +227 -0
- package/src/lib/pages/resource-page/resource-page.ts +234 -0
- package/src/lib/shared/components/badge/badge.ts +35 -0
- package/src/lib/shared/components/button/button.ts +65 -0
- package/src/lib/shared/components/confirm-dialog/confirm-dialog.ts +48 -0
- package/src/lib/shared/components/copy-button/copy-button.ts +56 -0
- package/src/lib/shared/components/copy-share/copy-share.spec.ts +122 -0
- package/src/lib/shared/components/copy-share/copy-share.ts +107 -0
- package/src/lib/shared/components/drawer/drawer.spec.ts +127 -0
- package/src/lib/shared/components/drawer/drawer.ts +172 -0
- package/src/lib/shared/components/empty-state/empty-state.ts +30 -0
- package/src/lib/shared/components/modal/modal.spec.ts +149 -0
- package/src/lib/shared/components/modal/modal.ts +145 -0
- package/src/lib/shared/components/post-create-modal/post-create-modal.spec.ts +165 -0
- package/src/lib/shared/components/post-create-modal/post-create-modal.ts +159 -0
- package/src/lib/shared/components/skeleton/skeleton.ts +31 -0
- package/src/lib/shared/components/toast/toast.ts +58 -0
- package/src/lib/shared/utils/col-span.ts +25 -0
- package/src/lib/shared/utils/condition-evaluator.ts +53 -0
- package/src/lib/shared/utils/depends-on.ts +48 -0
- package/src/lib/shared/utils/dialog-focus.ts +28 -0
- package/src/lib/shared/utils/field-path.ts +49 -0
- package/src/lib/shared/utils/gap-sorting.ts +144 -0
- package/src/lib/shared/utils/seed.spec.ts +64 -0
- package/src/lib/shared/utils/seed.ts +149 -0
- package/src/lib/shared/utils/slugify.ts +13 -0
- package/src/lib/shared/utils/validators.ts +41 -0
- package/src/lib/shell/layout/layout.ts +63 -0
- package/src/lib/shell/shell.routes.spec.ts +59 -0
- package/src/lib/shell/shell.routes.ts +122 -0
- package/src/lib/shell/sidebar/sidebar.ts +139 -0
- package/src/lib/shell/theme-toggle/theme-toggle.ts +74 -0
- package/src/lib/views/calendar-view/calendar-model.ts +426 -0
- package/src/lib/views/calendar-view/calendar-view.spec.ts +557 -0
- package/src/lib/views/calendar-view/calendar-view.ts +568 -0
- package/src/lib/views/composer/composer-host.spec.ts +301 -0
- package/src/lib/views/composer/composer-host.ts +248 -0
- package/src/lib/views/composer/composer-imports.spec.ts +54 -0
- package/src/lib/views/composer/view-context.spec.ts +117 -0
- package/src/lib/views/composer/view-context.ts +109 -0
- package/src/lib/views/composer/view-slot.spec.ts +130 -0
- package/src/lib/views/composer/view-slot.ts +178 -0
- package/src/lib/views/descriptors.ts +46 -0
- package/src/lib/views/kanban-view/kanban-model.ts +173 -0
- package/src/lib/views/kanban-view/kanban-view.spec.ts +540 -0
- package/src/lib/views/kanban-view/kanban-view.ts +327 -0
- package/src/lib/widgets/bar-chart/bar-chart.descriptor.ts +17 -0
- package/src/lib/widgets/bar-chart/bar-chart.spec.ts +66 -0
- package/src/lib/widgets/bar-chart/bar-chart.ts +76 -0
- package/src/lib/widgets/chart-line/chart-line.descriptor.ts +17 -0
- package/src/lib/widgets/chart-line/chart-line.spec.ts +113 -0
- package/src/lib/widgets/chart-line/chart-line.ts +219 -0
- package/src/lib/widgets/descriptors.ts +29 -0
- package/src/lib/widgets/metric-card/metric-card.descriptor.ts +41 -0
- package/src/lib/widgets/metric-card/metric-card.spec.ts +52 -0
- package/src/lib/widgets/metric-card/metric-card.ts +48 -0
- package/src/lib/widgets/quick-actions/quick-actions.descriptor.ts +13 -0
- package/src/lib/widgets/quick-actions/quick-actions.spec.ts +61 -0
- package/src/lib/widgets/quick-actions/quick-actions.ts +100 -0
- package/src/lib/widgets/record-list/record-list.descriptor.ts +16 -0
- package/src/lib/widgets/record-list/record-list.spec.ts +79 -0
- package/src/lib/widgets/record-list/record-list.ts +115 -0
- package/src/lib/widgets/series.spec.ts +42 -0
- package/src/lib/widgets/series.ts +56 -0
- package/src/lib/widgets/status-progress/status-progress.descriptor.ts +19 -0
- package/src/lib/widgets/status-progress/status-progress.spec.ts +63 -0
- package/src/lib/widgets/status-progress/status-progress.ts +104 -0
- package/src/lib/widgets/widget-host/widget-host.spec.ts +179 -0
- package/src/lib/widgets/widget-host/widget-host.ts +99 -0
- package/src/public-api.ts +104 -0
- package/tinya-code-gas-admin-ui-0.2.0-alpha.15.tgz +0 -0
- package/tsconfig.lib.json +13 -0
- package/tsconfig.lib.prod.json +11 -0
- package/tsconfig.spec.json +10 -0
- package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs +0 -84
- package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs +0 -140
- package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs +0 -113
- package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs +0 -163
- package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs +0 -1027
- package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs +0 -278
- package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs +0 -107
- package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs +0 -101
- package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs +0 -141
- package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs +0 -88
- package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs +0 -107
- package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs +0 -140
- package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs +0 -119
- package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs +0 -125
- package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs +0 -152
- package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs +0 -211
- package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs +0 -116
- package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs +0 -111
- package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs +0 -122
- package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs +0 -107
- package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs +0 -109
- package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs +0 -45
- package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs +0 -50
- package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs +0 -24
- package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs +0 -27
- package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs +0 -408
- package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs +0 -4516
- package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs +0 -82
- package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs +0 -105
- package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs +0 -65
- package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs +0 -65
- package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs +0 -65
- package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs +0 -59
- package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs +0 -94
- package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs +0 -72
- package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs +0 -238
- package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs +0 -416
- package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs +0 -439
- package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs +0 -513
- package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs +0 -637
- package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs +0 -127
- package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs +0 -251
- package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs +0 -66
- package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs +0 -114
- package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs +0 -116
- package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs +0 -235
- package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs +0 -8
- package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs +0 -124
- package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs +0 -249
- package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs +0 -39
- package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs +0 -309
- package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs +0 -576
- package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs +0 -161
- package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs +0 -11
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs +0 -685
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs +0 -80
- package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs +0 -296
- package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs +0 -119
- package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs +0 -158
- package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs +0 -26
- package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs +0 -205
- package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs +0 -432
- package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs +0 -36
- package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs +0 -107
- package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs +0 -223
- package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs +0 -107
- package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs +0 -60
- package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs +0 -141
- package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui.mjs +0 -4599
- package/fesm2022/tinya-code-gas-admin-ui.mjs.map +0 -1
- 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
|
+
});
|