@michaelthielemann/kestrel 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +234 -0
- package/kestrel.config.ts +28 -0
- package/layers/access/nuxt.config.ts +5 -0
- package/layers/access/server/middleware/00.ip-allowlist.ts +67 -0
- package/layers/access/server/middleware/access-guard.ts +30 -0
- package/layers/access/server/types.d.ts +8 -0
- package/layers/access/server/utils/csrf.ts +39 -0
- package/layers/access/server/utils/grant-registry.ts +36 -0
- package/layers/access/server/utils/guard.ts +44 -0
- package/layers/access/server/utils/ip-allowlist.ts +76 -0
- package/layers/access/server/utils/policy.ts +98 -0
- package/layers/access/server/utils/public-resources.ts +11 -0
- package/layers/access/server/utils/render-context.ts +41 -0
- package/layers/access/server/utils/require-admin.ts +15 -0
- package/layers/admin/app/components/AdminAccount.vue +212 -0
- package/layers/admin/app/components/AdminNav.vue +44 -0
- package/layers/admin/app/components/BlockFields.vue +68 -0
- package/layers/admin/app/components/BlockPreview.vue +401 -0
- package/layers/admin/app/components/BlockTree.vue +433 -0
- package/layers/admin/app/components/BlocksBody.vue +162 -0
- package/layers/admin/app/components/CollectionDeleteDialog.vue +38 -0
- package/layers/admin/app/components/CollectionEditor.vue +203 -0
- package/layers/admin/app/components/CollectionList.vue +1091 -0
- package/layers/admin/app/components/EditorStatus.vue +199 -0
- package/layers/admin/app/components/EditorUnsupported.vue +28 -0
- package/layers/admin/app/components/FieldsBody.vue +23 -0
- package/layers/admin/app/components/LocaleBar.vue +105 -0
- package/layers/admin/app/components/PageFields.vue +101 -0
- package/layers/admin/app/components/PageFieldsPane.vue +39 -0
- package/layers/admin/app/components/SeoFields.vue +169 -0
- package/layers/admin/app/components/SingletonEditor.vue +92 -0
- package/layers/admin/app/composables/useAuth.ts +68 -0
- package/layers/admin/app/composables/useBlockTree.ts +95 -0
- package/layers/admin/app/composables/useBlocks.ts +7 -0
- package/layers/admin/app/composables/useCachedResource.ts +35 -0
- package/layers/admin/app/composables/useCollectionOps.ts +53 -0
- package/layers/admin/app/composables/useCollections.ts +7 -0
- package/layers/admin/app/composables/useContentLocales.ts +23 -0
- package/layers/admin/app/composables/useEditForm.ts +370 -0
- package/layers/admin/app/composables/useLinkResolver.ts +40 -0
- package/layers/admin/app/composables/useListColumns.ts +53 -0
- package/layers/admin/app/composables/useListUrlState.ts +53 -0
- package/layers/admin/app/composables/usePendingPublishPoll.ts +15 -0
- package/layers/admin/app/composables/usePreviewViewport.ts +30 -0
- package/layers/admin/app/composables/usePublishStatus.ts +98 -0
- package/layers/admin/app/composables/useRailCollapsed.ts +14 -0
- package/layers/admin/app/composables/useTheme.ts +36 -0
- package/layers/admin/app/composables/useUnsavedGuard.ts +22 -0
- package/layers/admin/app/layouts/admin.vue +267 -0
- package/layers/admin/app/middleware/admin-auth.ts +4 -0
- package/layers/admin/app/pages/admin/[collection]/[id].vue +202 -0
- package/layers/admin/app/pages/admin/[collection]/index.vue +62 -0
- package/layers/admin/app/pages/admin/index.vue +118 -0
- package/layers/admin/app/pages/admin/login.vue +68 -0
- package/layers/admin/app/pages/admin/media.vue +28 -0
- package/layers/admin/app/pages/admin/references.vue +146 -0
- package/layers/admin/app/plugins/admin-style-guard.client.ts +76 -0
- package/layers/admin/app/plugins/reauth.client.ts +20 -0
- package/layers/admin/app/utils/admin-style-guard.ts +20 -0
- package/layers/admin/app/utils/block-tree.ts +214 -0
- package/layers/admin/app/utils/collection-ops.ts +44 -0
- package/layers/admin/app/utils/dead-refs.ts +30 -0
- package/layers/admin/app/utils/edit-form.ts +223 -0
- package/layers/admin/app/utils/editor-expose.ts +26 -0
- package/layers/admin/app/utils/editor-form-context.ts +83 -0
- package/layers/admin/app/utils/editor-registry.ts +20 -0
- package/layers/admin/app/utils/field-keys.ts +11 -0
- package/layers/admin/app/utils/list-columns.ts +141 -0
- package/layers/admin/app/utils/list-query.ts +96 -0
- package/layers/admin/app/utils/nav-active.ts +9 -0
- package/layers/admin/app/utils/populate-blocks.ts +288 -0
- package/layers/admin/app/utils/preview-channel.ts +62 -0
- package/layers/admin/app/utils/preview-viewport.ts +80 -0
- package/layers/admin/app/utils/reauth.ts +68 -0
- package/layers/admin/app/utils/record-title.ts +15 -0
- package/layers/admin/app/utils/register-builtin-editors.ts +9 -0
- package/layers/admin/app/utils/safe-redirect.ts +12 -0
- package/layers/admin/nuxt.config.ts +17 -0
- package/layers/auth/nuxt.config.ts +1 -0
- package/layers/auth/server/api/auth/login.post.ts +38 -0
- package/layers/auth/server/api/auth/logout.post.ts +9 -0
- package/layers/auth/server/api/auth/session.get.ts +1 -0
- package/layers/auth/server/utils/admin-credential.ts +42 -0
- package/layers/auth/server/utils/client-ip.ts +88 -0
- package/layers/auth/server/utils/login-throttle.ts +126 -0
- package/layers/auth/server/utils/password.ts +52 -0
- package/layers/auth/server/utils/session-cookie.ts +44 -0
- package/layers/auth/server/utils/session-epoch.ts +96 -0
- package/layers/auth/server/utils/session.ts +85 -0
- package/layers/collections/nuxt.config.ts +1 -0
- package/layers/collections/server/collections/pages.ts +19 -0
- package/layers/collections/server/plugins/01.register.ts +32 -0
- package/layers/collections/server/plugins/02.register-relation-populate.ts +17 -0
- package/layers/collections/server/utils/populate-relations.ts +66 -0
- package/layers/core/app/utils/bulk-actions.ts +9 -0
- package/layers/core/app/utils/filter-ops.ts +98 -0
- package/layers/core/app/utils/list-limits.ts +27 -0
- package/layers/core/app/utils/locale-path.ts +12 -0
- package/layers/core/app/utils/slugify.ts +16 -0
- package/layers/core/modules/auto-discovery/extract-block.ts +176 -0
- package/layers/core/modules/auto-discovery/index.ts +44 -0
- package/layers/core/modules/auto-discovery/scan.ts +78 -0
- package/layers/core/modules/auto-discovery/virtual.d.ts +12 -0
- package/layers/core/modules/kestrel/index.ts +86 -0
- package/layers/core/nuxt.config.ts +8 -0
- package/layers/core/server/api/[collection]/[id]/dead-refs.get.ts +8 -0
- package/layers/core/server/api/[collection]/[id]/translations.get.ts +4 -0
- package/layers/core/server/api/[collection]/[id].delete.ts +5 -0
- package/layers/core/server/api/[collection]/[id].get.ts +8 -0
- package/layers/core/server/api/[collection]/[id].patch.ts +8 -0
- package/layers/core/server/api/[collection]/bulk.post.ts +37 -0
- package/layers/core/server/api/[collection]/index.get.ts +19 -0
- package/layers/core/server/api/[collection]/index.post.ts +8 -0
- package/layers/core/server/api/[collection]/index.put.ts +7 -0
- package/layers/core/server/api/[collection]/options.get.ts +18 -0
- package/layers/core/server/api/[collection]/translations.get.ts +25 -0
- package/layers/core/server/api/collections/[name].get.ts +4 -0
- package/layers/core/server/api/collections/index.get.ts +3 -0
- package/layers/core/server/api/references/broken.get.ts +3 -0
- package/layers/core/server/api/references/referrers.get.ts +24 -0
- package/layers/core/server/database/record-refs.ts +20 -0
- package/layers/core/server/plugins/00.migrate.ts +11 -0
- package/layers/core/server/plugins/02.schema-sync.ts +63 -0
- package/layers/core/server/plugins/03.record-refs.ts +12 -0
- package/layers/core/server/schema/bootstrap.ts +41 -0
- package/layers/core/server/schema/desired.ts +99 -0
- package/layers/core/server/schema/dev-sync.ts +23 -0
- package/layers/core/server/schema/dialect.ts +59 -0
- package/layers/core/server/schema/diff.ts +110 -0
- package/layers/core/server/schema/introspect.ts +67 -0
- package/layers/core/server/schema/model.ts +61 -0
- package/layers/core/server/schema/render-sqlite.ts +89 -0
- package/layers/core/server/schema/sync.ts +201 -0
- package/layers/core/server/tasks/db/migrate.ts +54 -0
- package/layers/core/server/utils/blocks.ts +6 -0
- package/layers/core/server/utils/collection-types.ts +27 -0
- package/layers/core/server/utils/crud.ts +577 -0
- package/layers/core/server/utils/db.ts +26 -0
- package/layers/core/server/utils/defineCollection.ts +207 -0
- package/layers/core/server/utils/duplicate.ts +162 -0
- package/layers/core/server/utils/field-layout.ts +125 -0
- package/layers/core/server/utils/filter-predicate.ts +101 -0
- package/layers/core/server/utils/http.ts +50 -0
- package/layers/core/server/utils/if-unmodified.ts +13 -0
- package/layers/core/server/utils/kestrel-config.ts +410 -0
- package/layers/core/server/utils/key-lock.ts +33 -0
- package/layers/core/server/utils/locale.ts +53 -0
- package/layers/core/server/utils/page-route.ts +58 -0
- package/layers/core/server/utils/page-slug.ts +127 -0
- package/layers/core/server/utils/picker.ts +82 -0
- package/layers/core/server/utils/populate.ts +48 -0
- package/layers/core/server/utils/prerender-memo.ts +23 -0
- package/layers/core/server/utils/read-capture.ts +37 -0
- package/layers/core/server/utils/record-ref-index.ts +200 -0
- package/layers/core/server/utils/registry.ts +24 -0
- package/layers/core/server/utils/resolve-scope.ts +79 -0
- package/layers/core/server/utils/seo.ts +24 -0
- package/layers/core/server/utils/serialize-collection.ts +149 -0
- package/layers/core/server/utils/server-config.ts +18 -0
- package/layers/core/server/utils/sql.ts +2 -0
- package/layers/core/server/utils/storage.local.ts +117 -0
- package/layers/core/server/utils/storage.s3.ts +197 -0
- package/layers/core/server/utils/storage.ts +90 -0
- package/layers/core/server/utils/write-events.ts +31 -0
- package/layers/fields/app/utils/condition.ts +71 -0
- package/layers/fields/app/utils/define-block.ts +13 -0
- package/layers/fields/app/utils/field-constraints.ts +17 -0
- package/layers/fields/app/utils/field-factories.ts +129 -0
- package/layers/fields/app/utils/field-meta.ts +33 -0
- package/layers/fields/app/utils/field-validate.ts +75 -0
- package/layers/fields/app/utils/field-value.ts +7 -0
- package/layers/fields/app/utils/richtext-links.ts +48 -0
- package/layers/fields/nuxt.config.ts +1 -0
- package/layers/fields/server/api/blocks/index.get.ts +9 -0
- package/layers/fields/server/field-registry/index.ts +248 -0
- package/layers/fields/server/field-registry/naming.ts +10 -0
- package/layers/fields/server/field-registry/sanitize.ts +40 -0
- package/layers/fields/server/field-registry/types.ts +19 -0
- package/layers/fields/server/plugins/01.register-field-populate.ts +11 -0
- package/layers/fields/server/utils/block-ids.ts +43 -0
- package/layers/fields/server/utils/block-populate.ts +41 -0
- package/layers/fields/server/utils/buildCollection.ts +88 -0
- package/layers/fields/server/utils/buildFieldSchema.ts +22 -0
- package/layers/fields/server/utils/buildTable.ts +107 -0
- package/layers/fields/server/utils/conditional-required.ts +25 -0
- package/layers/fields/server/utils/defineBlock.ts +86 -0
- package/layers/fields/server/utils/defineFieldType.ts +33 -0
- package/layers/fields/server/utils/extract-refs.ts +152 -0
- package/layers/fields/server/utils/field-populate.ts +68 -0
- package/layers/media/app/components/KestrelImg.vue +65 -0
- package/layers/media/app/components/MediaConflictDialog.vue +35 -0
- package/layers/media/app/components/MediaDeleteDialog.vue +52 -0
- package/layers/media/app/components/MediaGrid.vue +113 -0
- package/layers/media/app/components/MediaImage.vue +70 -0
- package/layers/media/app/components/MediaLibrary.vue +451 -0
- package/layers/media/app/components/MediaNewFolderDialog.vue +32 -0
- package/layers/media/app/components/MediaPathBar.vue +163 -0
- package/layers/media/app/components/MediaPicker.vue +19 -0
- package/layers/media/app/components/MediaRenameDialog.vue +32 -0
- package/layers/media/app/components/MediaTable.vue +75 -0
- package/layers/media/app/components/MediaThumb.vue +25 -0
- package/layers/media/app/components/MediaToolbar.vue +90 -0
- package/layers/media/app/components/MediaUploadDialog.vue +87 -0
- package/layers/media/app/components/MediaViewer.vue +88 -0
- package/layers/media/app/components/field/Media.vue +261 -0
- package/layers/media/app/composables/useBlurUp.ts +55 -0
- package/layers/media/app/composables/useMediaClipboard.ts +14 -0
- package/layers/media/app/composables/useMediaContextMenu.ts +56 -0
- package/layers/media/app/composables/useMediaDnd.ts +56 -0
- package/layers/media/app/composables/useMediaLibrary.ts +151 -0
- package/layers/media/app/composables/useMediaOps.ts +50 -0
- package/layers/media/app/composables/useMediaRelocate.ts +39 -0
- package/layers/media/app/composables/useMediaResolver.ts +50 -0
- package/layers/media/app/composables/useMediaUpload.ts +93 -0
- package/layers/media/app/composables/useMediaVariant.ts +34 -0
- package/layers/media/app/plugins/field-media.client.ts +6 -0
- package/layers/media/app/utils/dnd.ts +69 -0
- package/layers/media/app/utils/library.ts +79 -0
- package/layers/media/app/utils/ops.ts +62 -0
- package/layers/media/app/utils/picture-model.ts +125 -0
- package/layers/media/app/utils/thumbhash.ts +19 -0
- package/layers/media/app/utils/variant-name.ts +20 -0
- package/layers/media/nuxt.config.ts +3 -0
- package/layers/media/server/api/media/[id]/usages.get.ts +8 -0
- package/layers/media/server/api/media/[id].delete.ts +18 -0
- package/layers/media/server/api/media/[id].get.ts +10 -0
- package/layers/media/server/api/media/[id].patch.ts +41 -0
- package/layers/media/server/api/media/backfill.post.ts +18 -0
- package/layers/media/server/api/media/copy.post.ts +7 -0
- package/layers/media/server/api/media/delete.post.ts +13 -0
- package/layers/media/server/api/media/folders.post.ts +18 -0
- package/layers/media/server/api/media/index.get.ts +15 -0
- package/layers/media/server/api/media/index.post.ts +157 -0
- package/layers/media/server/api/media/library.get.ts +17 -0
- package/layers/media/server/api/media/move.post.ts +7 -0
- package/layers/media/server/api/media/rename.post.ts +7 -0
- package/layers/media/server/api/media/resolve.get.ts +13 -0
- package/layers/media/server/collections/media-settings.ts +25 -0
- package/layers/media/server/collections/media.ts +29 -0
- package/layers/media/server/database/folders.ts +10 -0
- package/layers/media/server/middleware/ondemand-variants.ts +37 -0
- package/layers/media/server/plugins/02.register-media.ts +19 -0
- package/layers/media/server/plugins/03.media-cleanup.ts +20 -0
- package/layers/media/server/plugins/04.variant-capture.ts +23 -0
- package/layers/media/server/schema-tables/folders.ts +4 -0
- package/layers/media/server/tasks/media/backfill.ts +18 -0
- package/layers/media/server/utils/backfill.ts +132 -0
- package/layers/media/server/utils/derive.ts +59 -0
- package/layers/media/server/utils/folder-paths.ts +42 -0
- package/layers/media/server/utils/folders.ts +12 -0
- package/layers/media/server/utils/library.ts +90 -0
- package/layers/media/server/utils/media-cleanup.ts +27 -0
- package/layers/media/server/utils/media-enabled.ts +23 -0
- package/layers/media/server/utils/media-ops.ts +118 -0
- package/layers/media/server/utils/media-write.ts +13 -0
- package/layers/media/server/utils/naming.ts +52 -0
- package/layers/media/server/utils/ondemand.ts +69 -0
- package/layers/media/server/utils/persist-upload.ts +62 -0
- package/layers/media/server/utils/populate.ts +47 -0
- package/layers/media/server/utils/record.ts +63 -0
- package/layers/media/server/utils/relocate-ops.ts +341 -0
- package/layers/media/server/utils/relocate-plan.ts +63 -0
- package/layers/media/server/utils/resolve.ts +86 -0
- package/layers/media/server/utils/sanitize-svg.ts +12 -0
- package/layers/media/server/utils/sniff.ts +68 -0
- package/layers/media/server/utils/storage-relocate.ts +139 -0
- package/layers/media/server/utils/translations.ts +19 -0
- package/layers/media/server/utils/usages.ts +66 -0
- package/layers/media/server/utils/variant-capture.ts +70 -0
- package/layers/media/server/utils/variants.ts +108 -0
- package/layers/public/app/app.vue +5 -0
- package/layers/public/app/components/BlockRenderer.vue +56 -0
- package/layers/public/app/components/KestrelLink.vue +26 -0
- package/layers/public/app/components/KestrelPreviewBridge.vue +133 -0
- package/layers/public/app/composables/public-page.ts +24 -0
- package/layers/public/app/error.vue +12 -0
- package/layers/public/app/layouts/default.vue +5 -0
- package/layers/public/app/pages/[...slug].vue +157 -0
- package/layers/public/app/pages/__kestrel/preview.vue +34 -0
- package/layers/public/app/utils/block-edit-context.ts +16 -0
- package/layers/public/app/utils/link-href.ts +46 -0
- package/layers/public/app/utils/page-head.ts +84 -0
- package/layers/public/app/utils/preview-protocol.ts +81 -0
- package/layers/public/app/utils/public-route.ts +46 -0
- package/layers/public/modules/deploy-output/deploy-output.ts +370 -0
- package/layers/public/modules/deploy-output/index.ts +82 -0
- package/layers/public/modules/prerender-routes/discover.ts +37 -0
- package/layers/public/modules/prerender-routes/index.ts +85 -0
- package/layers/public/modules/prerender-routes/routes.ts +16 -0
- package/layers/public/modules/prune-media/index.ts +48 -0
- package/layers/public/modules/prune-media/prune-media.ts +81 -0
- package/layers/public/nuxt.config.ts +11 -0
- package/layers/public/server/api/links/resolve.get.ts +19 -0
- package/layers/public/server/api/publish-status.get.ts +44 -0
- package/layers/public/server/api/route.get.ts +18 -0
- package/layers/public/server/database/publish-deps.ts +12 -0
- package/layers/public/server/database/publish-status.ts +19 -0
- package/layers/public/server/plugins/02.register-links.ts +17 -0
- package/layers/public/server/plugins/zz.publish.ts +63 -0
- package/layers/public/server/routes/llms.txt.get.ts +72 -0
- package/layers/public/server/routes/robots.txt.get.ts +8 -0
- package/layers/public/server/routes/sitemap.xml.get.ts +66 -0
- package/layers/public/server/schema-tables/publish-deps.ts +4 -0
- package/layers/public/server/schema-tables/publish-status.ts +4 -0
- package/layers/public/server/tasks/publish/run.ts +26 -0
- package/layers/public/server/utils/link-resolve.ts +35 -0
- package/layers/public/server/utils/llms.ts +39 -0
- package/layers/public/server/utils/page-resolve.ts +86 -0
- package/layers/public/server/utils/populate-links.ts +66 -0
- package/layers/public/server/utils/publish/coalesce.ts +22 -0
- package/layers/public/server/utils/publish/deps-persistence.ts +43 -0
- package/layers/public/server/utils/publish/deps.ts +87 -0
- package/layers/public/server/utils/publish/invalidation.ts +141 -0
- package/layers/public/server/utils/publish/publish-status.ts +54 -0
- package/layers/public/server/utils/publish/publisher.ts +291 -0
- package/layers/public/server/utils/publish/queue.ts +67 -0
- package/layers/public/server/utils/publish/route-for-record.ts +16 -0
- package/layers/public/server/utils/publish/route-keys.ts +13 -0
- package/layers/public/server/utils/site-url.ts +32 -0
- package/layers/public/server/utils/sitemap.ts +97 -0
- package/layers/ui/app/assets/scss/_base.scss +16 -0
- package/layers/ui/app/assets/scss/_datepicker.scss +82 -0
- package/layers/ui/app/assets/scss/_mixins.scss +50 -0
- package/layers/ui/app/assets/scss/_reset.scss +65 -0
- package/layers/ui/app/assets/scss/_tokens.scss +183 -0
- package/layers/ui/app/assets/scss/main.scss +4 -0
- package/layers/ui/app/components/field/Boolean.vue +30 -0
- package/layers/ui/app/components/field/Choice.vue +68 -0
- package/layers/ui/app/components/field/Datetime.vue +96 -0
- package/layers/ui/app/components/field/DeadRefNote.vue +28 -0
- package/layers/ui/app/components/field/Json.vue +63 -0
- package/layers/ui/app/components/field/Layout.vue +140 -0
- package/layers/ui/app/components/field/Link.vue +112 -0
- package/layers/ui/app/components/field/LinkInternalPicker.vue +95 -0
- package/layers/ui/app/components/field/Number.vue +30 -0
- package/layers/ui/app/components/field/Relation.vue +49 -0
- package/layers/ui/app/components/field/Renderer.vue +30 -0
- package/layers/ui/app/components/field/Repeater.vue +460 -0
- package/layers/ui/app/components/field/Richtext.vue +26 -0
- package/layers/ui/app/components/field/Slug.vue +66 -0
- package/layers/ui/app/components/field/Text.vue +35 -0
- package/layers/ui/app/components/field/Unsupported.vue +34 -0
- package/layers/ui/app/components/ui/Alert.vue +50 -0
- package/layers/ui/app/components/ui/Brand.vue +22 -0
- package/layers/ui/app/components/ui/Button.vue +116 -0
- package/layers/ui/app/components/ui/ButtonGroup.vue +96 -0
- package/layers/ui/app/components/ui/Checkbox.vue +52 -0
- package/layers/ui/app/components/ui/CheckboxGroup.vue +50 -0
- package/layers/ui/app/components/ui/Combobox.vue +343 -0
- package/layers/ui/app/components/ui/DatePicker.vue +92 -0
- package/layers/ui/app/components/ui/DateRangePicker.vue +100 -0
- package/layers/ui/app/components/ui/Dialog.vue +107 -0
- package/layers/ui/app/components/ui/EmptyState.vue +46 -0
- package/layers/ui/app/components/ui/Field.vue +57 -0
- package/layers/ui/app/components/ui/Fieldset.vue +64 -0
- package/layers/ui/app/components/ui/Icon.vue +44 -0
- package/layers/ui/app/components/ui/Menu.vue +54 -0
- package/layers/ui/app/components/ui/NumberInput.vue +173 -0
- package/layers/ui/app/components/ui/Richtext.vue +119 -0
- package/layers/ui/app/components/ui/RichtextToolbar.vue +253 -0
- package/layers/ui/app/components/ui/Select.vue +51 -0
- package/layers/ui/app/components/ui/TextInput.vue +110 -0
- package/layers/ui/app/components/ui/Textarea.vue +38 -0
- package/layers/ui/app/components/ui/TimeInput.vue +17 -0
- package/layers/ui/app/components/ui/Toasts.vue +103 -0
- package/layers/ui/app/components/ui/Tooltip.vue +45 -0
- package/layers/ui/app/composables/useAdminLang.ts +8 -0
- package/layers/ui/app/composables/useDragReorder.ts +41 -0
- package/layers/ui/app/composables/useEchoGuard.ts +28 -0
- package/layers/ui/app/composables/useLinkField.ts +70 -0
- package/layers/ui/app/composables/useRecordOptions.ts +84 -0
- package/layers/ui/app/composables/useRepeater.ts +99 -0
- package/layers/ui/app/composables/useT.ts +32 -0
- package/layers/ui/app/composables/useToast.ts +46 -0
- package/layers/ui/app/i18n/de.ts +406 -0
- package/layers/ui/app/i18n/en.ts +440 -0
- package/layers/ui/app/utils/boolean-field.ts +10 -0
- package/layers/ui/app/utils/field-component.ts +22 -0
- package/layers/ui/app/utils/field-empty.ts +21 -0
- package/layers/ui/app/utils/field-registry.ts +36 -0
- package/layers/ui/app/utils/humanize.ts +14 -0
- package/layers/ui/app/utils/icons.ts +147 -0
- package/layers/ui/app/utils/iso-date.ts +18 -0
- package/layers/ui/app/utils/localized.ts +13 -0
- package/layers/ui/app/utils/record-context.ts +15 -0
- package/layers/ui/app/utils/reorder.ts +8 -0
- package/layers/ui/app/utils/richtext-preserve-class.ts +31 -0
- package/layers/ui/app/utils/useFieldA11y.ts +40 -0
- package/layers/ui/nuxt.config.ts +5 -0
- package/nuxt.config.ts +105 -0
- package/package.json +122 -0
- package/scripts/hash-password.mjs +26 -0
- package/scripts/typecheck.mjs +51 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { resolveLocalized } from '../../../../ui/app/utils/localized'
|
|
3
|
+
|
|
4
|
+
definePageMeta({ layout: 'admin', middleware: 'admin-auth' })
|
|
5
|
+
|
|
6
|
+
const { t, lang } = useT()
|
|
7
|
+
const { load } = useCollections()
|
|
8
|
+
const collections = ref<Awaited<ReturnType<typeof load>>>([])
|
|
9
|
+
try {
|
|
10
|
+
// Match AdminNav: hide nav:false system collections (e.g. media_settings, the machine-managed variant
|
|
11
|
+
// registry) — they are config stores, not content, and must not appear as clickable dashboard cards.
|
|
12
|
+
collections.value = (await load()).filter((c) => c.nav !== false)
|
|
13
|
+
} catch {
|
|
14
|
+
// a transient /api/collections failure degrades to an empty dashboard, not an error page
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Broken-reference count for the alert banner — fire-and-forget so it never delays the dashboard, and a
|
|
18
|
+
// failure (or a not-yet-built index) simply shows no banner.
|
|
19
|
+
const brokenCount = ref(0)
|
|
20
|
+
$fetch<unknown[]>('/api/references/broken')
|
|
21
|
+
.then((r) => { brokenCount.value = r.length })
|
|
22
|
+
.catch(() => { brokenCount.value = 0 })
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<section class="dash">
|
|
27
|
+
<h1 class="dash__title">{{ t('dash.title') }}</h1>
|
|
28
|
+
<p class="dash__lede">{{ t('dash.lede') }}</p>
|
|
29
|
+
<NuxtLink v-if="brokenCount" to="/admin/references" class="dash__alert">
|
|
30
|
+
<UiIcon name="triangle-alert" :size="18" />
|
|
31
|
+
<span>{{ t('refs.dashAlert', { n: brokenCount }) }}</span>
|
|
32
|
+
</NuxtLink>
|
|
33
|
+
<UiEmptyState
|
|
34
|
+
v-if="!collections.length"
|
|
35
|
+
icon="file-text"
|
|
36
|
+
:title="t('dash.empty.title')"
|
|
37
|
+
:description="t('dash.empty.desc')"
|
|
38
|
+
/>
|
|
39
|
+
<ul v-else class="dash__grid">
|
|
40
|
+
<li v-for="c in collections" :key="c.name">
|
|
41
|
+
<NuxtLink :to="`/admin/${c.name}`" class="dash__card">
|
|
42
|
+
<UiIcon :name="c.icon ?? 'file-text'" class="dash__icon" size="1.5rem" />
|
|
43
|
+
<span class="dash__name">{{ resolveLocalized(c.label?.plural, lang) ?? c.name }}</span>
|
|
44
|
+
<span class="dash__mode">{{ c.mode === 'single' ? t('dash.mode.singleton') : t('dash.mode.collection') }}</span>
|
|
45
|
+
</NuxtLink>
|
|
46
|
+
</li>
|
|
47
|
+
</ul>
|
|
48
|
+
</section>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<style lang="scss">
|
|
52
|
+
.dash {
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-direction: column;
|
|
55
|
+
gap: var(--space-3);
|
|
56
|
+
overflow-y: auto;
|
|
57
|
+
|
|
58
|
+
&__title {
|
|
59
|
+
font-size: var(--text-xl);
|
|
60
|
+
font-weight: var(--weight-bold);
|
|
61
|
+
}
|
|
62
|
+
&__lede {
|
|
63
|
+
color: var(--color-text-muted);
|
|
64
|
+
}
|
|
65
|
+
// Stale-reference alert: a warning banner linking to the broken-references report (only when non-zero).
|
|
66
|
+
&__alert {
|
|
67
|
+
display: inline-flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: var(--space-2);
|
|
70
|
+
align-self: flex-start;
|
|
71
|
+
padding: var(--space-2) var(--space-3);
|
|
72
|
+
border: 1px solid var(--color-warning);
|
|
73
|
+
border-radius: var(--radius-md);
|
|
74
|
+
color: var(--color-warning-text); /* banner TEXT → 4.5:1 (the border keeps the 3:1 amber accent) */
|
|
75
|
+
font-size: var(--text-sm);
|
|
76
|
+
font-weight: var(--weight-medium);
|
|
77
|
+
text-decoration: none;
|
|
78
|
+
}
|
|
79
|
+
&__grid {
|
|
80
|
+
list-style: none;
|
|
81
|
+
margin: var(--space-3) 0 0;
|
|
82
|
+
padding: 0;
|
|
83
|
+
display: grid;
|
|
84
|
+
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
|
|
85
|
+
gap: var(--space-4);
|
|
86
|
+
}
|
|
87
|
+
&__card {
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
gap: var(--space-1);
|
|
91
|
+
padding: var(--space-4);
|
|
92
|
+
border: 1px solid var(--color-border);
|
|
93
|
+
border-radius: var(--radius-lg);
|
|
94
|
+
background: var(--color-surface);
|
|
95
|
+
text-decoration: none;
|
|
96
|
+
color: var(--color-text);
|
|
97
|
+
transition: border-color var(--motion-fast) var(--ease-standard);
|
|
98
|
+
|
|
99
|
+
&:hover {
|
|
100
|
+
border-color: var(--color-border-strong);
|
|
101
|
+
}
|
|
102
|
+
&:focus-visible {
|
|
103
|
+
border-color: var(--color-primary);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
&__icon {
|
|
107
|
+
color: var(--color-primary);
|
|
108
|
+
}
|
|
109
|
+
&__name {
|
|
110
|
+
font-weight: var(--weight-medium);
|
|
111
|
+
text-transform: capitalize;
|
|
112
|
+
}
|
|
113
|
+
&__mode {
|
|
114
|
+
font-size: var(--text-sm);
|
|
115
|
+
color: var(--color-text-muted);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
</style>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
definePageMeta({ layout: 'admin' })
|
|
3
|
+
|
|
4
|
+
const { t } = useT()
|
|
5
|
+
const route = useRoute()
|
|
6
|
+
const password = ref('')
|
|
7
|
+
const error = ref<string | null>(null)
|
|
8
|
+
const loading = ref(false)
|
|
9
|
+
|
|
10
|
+
async function submit() {
|
|
11
|
+
loading.value = true
|
|
12
|
+
error.value = null
|
|
13
|
+
try {
|
|
14
|
+
await useAuth().login(password.value)
|
|
15
|
+
await navigateTo(safeRedirect(route.query.redirect) ?? '/admin')
|
|
16
|
+
} catch (e: unknown) {
|
|
17
|
+
const status = (e as { statusCode?: number })?.statusCode
|
|
18
|
+
error.value = status === 401 ? t('login.invalid') : t('login.error')
|
|
19
|
+
} finally {
|
|
20
|
+
loading.value = false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<form class="login" @submit.prevent="submit">
|
|
27
|
+
<div class="login__brand">
|
|
28
|
+
<UiBrand />
|
|
29
|
+
<span class="login__brand-word">kestrel</span>
|
|
30
|
+
</div>
|
|
31
|
+
<h1 class="login__title">{{ t('login.signIn') }}</h1>
|
|
32
|
+
<UiAlert v-if="error" variant="error">{{ error }}</UiAlert>
|
|
33
|
+
<UiField :label="t('login.password')">
|
|
34
|
+
<template #default="f">
|
|
35
|
+
<UiTextInput
|
|
36
|
+
v-model="password"
|
|
37
|
+
type="password"
|
|
38
|
+
autocomplete="current-password"
|
|
39
|
+
v-bind="f"
|
|
40
|
+
/>
|
|
41
|
+
</template>
|
|
42
|
+
</UiField>
|
|
43
|
+
<UiButton type="submit" variant="primary" :loading="loading">{{ t('login.signIn') }}</UiButton>
|
|
44
|
+
</form>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<style lang="scss">
|
|
48
|
+
.login {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: var(--space-4);
|
|
52
|
+
max-width: 22rem;
|
|
53
|
+
margin-inline: auto;
|
|
54
|
+
overflow-y: auto;
|
|
55
|
+
|
|
56
|
+
&__brand {
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: var(--space-2);
|
|
60
|
+
font-size: var(--text-lg);
|
|
61
|
+
font-weight: var(--weight-bold);
|
|
62
|
+
}
|
|
63
|
+
&__title {
|
|
64
|
+
font-size: var(--text-xl);
|
|
65
|
+
font-weight: var(--weight-bold);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
definePageMeta({ layout: 'admin', middleware: 'admin-auth' })
|
|
3
|
+
const { t } = useT()
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<section class="media-page">
|
|
8
|
+
<h1 class="media-page__title">{{ t('media.title') }}</h1>
|
|
9
|
+
<MediaLibrary />
|
|
10
|
+
</section>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<style lang="scss">
|
|
14
|
+
// Non-scrolling page section: the title stays fixed and MediaLibrary's grid owns the scroll.
|
|
15
|
+
.media-page {
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
gap: var(--space-4);
|
|
19
|
+
min-height: 0;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
|
|
22
|
+
&__title {
|
|
23
|
+
flex: 0 0 auto;
|
|
24
|
+
font-size: var(--text-xl);
|
|
25
|
+
font-weight: var(--weight-bold);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
</style>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
definePageMeta({ layout: 'admin', middleware: 'admin-auth' })
|
|
3
|
+
|
|
4
|
+
// The global broken-references report: every record that links to a deleted or unpublished target. The
|
|
5
|
+
// referrer keeps its last published output (the stale link stays live) until it is re-edited — this is
|
|
6
|
+
// the editor's queue for fixing those. Derived on read, so an entry clears once the link is fixed or the
|
|
7
|
+
// target restored. Admin-only (the `references` API resource is outside the public set).
|
|
8
|
+
interface BrokenRow {
|
|
9
|
+
source: { collection: string; id: number }
|
|
10
|
+
target: { collection: string; id: number }
|
|
11
|
+
reason: 'missing' | 'unpublished'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { t } = useT()
|
|
15
|
+
const broken = ref<BrokenRow[]>([])
|
|
16
|
+
// Distinguish "the check ran and found nothing" from "the check failed": a failed fetch must NOT render
|
|
17
|
+
// the green all-clear (that would imply a verified-clean site when nothing was actually verified).
|
|
18
|
+
const loadError = ref(false)
|
|
19
|
+
try {
|
|
20
|
+
broken.value = await $fetch<BrokenRow[]>('/api/references/broken')
|
|
21
|
+
} catch {
|
|
22
|
+
loadError.value = true
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<section class="refs">
|
|
28
|
+
<NuxtLink to="/admin" class="refs__back">
|
|
29
|
+
<UiIcon name="arrow-left" :size="16" />
|
|
30
|
+
<span>{{ t('nav.dashboard') }}</span>
|
|
31
|
+
</NuxtLink>
|
|
32
|
+
<h1 class="refs__title">{{ t('refs.title') }}</h1>
|
|
33
|
+
<p class="refs__lede">{{ t('refs.lede') }}</p>
|
|
34
|
+
|
|
35
|
+
<UiAlert v-if="loadError" variant="error" class="refs__error">{{ t('refs.loadError') }}</UiAlert>
|
|
36
|
+
<UiEmptyState
|
|
37
|
+
v-else-if="!broken.length"
|
|
38
|
+
icon="check"
|
|
39
|
+
:title="t('refs.empty.title')"
|
|
40
|
+
:description="t('refs.empty.desc')"
|
|
41
|
+
/>
|
|
42
|
+
<!-- The report table is the one scroll region; the back link, title and lede stay fixed above it. -->
|
|
43
|
+
<div v-else class="refs__scroll">
|
|
44
|
+
<table class="refs__table">
|
|
45
|
+
<thead>
|
|
46
|
+
<tr>
|
|
47
|
+
<th>{{ t('refs.colReferrer') }}</th>
|
|
48
|
+
<th>{{ t('refs.colTarget') }}</th>
|
|
49
|
+
<th>{{ t('refs.colReason') }}</th>
|
|
50
|
+
</tr>
|
|
51
|
+
</thead>
|
|
52
|
+
<tbody>
|
|
53
|
+
<tr v-for="(b, i) in broken" :key="i">
|
|
54
|
+
<td>
|
|
55
|
+
<NuxtLink :to="`/admin/${b.source.collection}/${b.source.id}`" class="refs__link">{{ b.source.collection }} #{{ b.source.id }}</NuxtLink>
|
|
56
|
+
</td>
|
|
57
|
+
<td class="refs__target">{{ b.target.collection }} #{{ b.target.id }}</td>
|
|
58
|
+
<td>
|
|
59
|
+
<span class="refs__reason" :class="`refs__reason--${b.reason}`">{{ t(`refs.reason.${b.reason}`) }}</span>
|
|
60
|
+
</td>
|
|
61
|
+
</tr>
|
|
62
|
+
</tbody>
|
|
63
|
+
</table>
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<style lang="scss">
|
|
69
|
+
// Non-scrolling section: the back link, title and lede stay fixed; the report table scrolls in .refs__scroll.
|
|
70
|
+
.refs {
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
gap: var(--space-3);
|
|
74
|
+
min-height: 0;
|
|
75
|
+
overflow: hidden;
|
|
76
|
+
|
|
77
|
+
&__back,
|
|
78
|
+
&__title,
|
|
79
|
+
&__lede {
|
|
80
|
+
flex: 0 0 auto;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
&__scroll {
|
|
84
|
+
flex: 1 1 auto;
|
|
85
|
+
min-height: 0;
|
|
86
|
+
overflow: auto;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
&__back {
|
|
90
|
+
display: inline-flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
gap: var(--space-1);
|
|
93
|
+
align-self: flex-start;
|
|
94
|
+
font-size: var(--text-sm);
|
|
95
|
+
color: var(--color-text-muted);
|
|
96
|
+
text-decoration: none;
|
|
97
|
+
|
|
98
|
+
&:hover {
|
|
99
|
+
color: var(--color-text);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
&__title {
|
|
103
|
+
font-size: var(--text-xl);
|
|
104
|
+
font-weight: var(--weight-bold);
|
|
105
|
+
}
|
|
106
|
+
&__lede {
|
|
107
|
+
color: var(--color-text-muted);
|
|
108
|
+
}
|
|
109
|
+
&__table {
|
|
110
|
+
width: 100%;
|
|
111
|
+
border-collapse: collapse;
|
|
112
|
+
font-size: var(--text-sm);
|
|
113
|
+
|
|
114
|
+
th,
|
|
115
|
+
td {
|
|
116
|
+
text-align: start;
|
|
117
|
+
padding: var(--space-2) var(--space-3);
|
|
118
|
+
border-bottom: 1px solid var(--color-border);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
&__link {
|
|
122
|
+
color: var(--color-primary);
|
|
123
|
+
text-decoration: none;
|
|
124
|
+
text-transform: capitalize;
|
|
125
|
+
}
|
|
126
|
+
&__target {
|
|
127
|
+
color: var(--color-text-muted);
|
|
128
|
+
text-transform: capitalize;
|
|
129
|
+
}
|
|
130
|
+
&__reason {
|
|
131
|
+
display: inline-flex;
|
|
132
|
+
padding: 1px var(--space-2);
|
|
133
|
+
border-radius: var(--radius-sm);
|
|
134
|
+
font-size: var(--text-xs);
|
|
135
|
+
font-weight: var(--weight-medium);
|
|
136
|
+
background: transparent;
|
|
137
|
+
border: 1px solid var(--color-warning);
|
|
138
|
+
color: var(--color-warning-text); /* warning conveyed as TEXT → needs 4.5:1, not the 3:1 icon accent */
|
|
139
|
+
|
|
140
|
+
&--missing {
|
|
141
|
+
background: var(--color-danger-solid);
|
|
142
|
+
color: var(--color-on-danger);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
</style>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isProtectedStyle } from '../utils/admin-style-guard'
|
|
2
|
+
|
|
3
|
+
function isKestrelOwned(node: Element): boolean {
|
|
4
|
+
const el = node as HTMLElement
|
|
5
|
+
const devId = el.dataset?.viteDevId ?? '' // Vite stamps the source path here in dev
|
|
6
|
+
const href = node.getAttribute?.('href') ?? ''
|
|
7
|
+
return isProtectedStyle(devId, href, el.dataset?.kestrel !== undefined)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Dev-only admin style guard — keeps a consumer's public CSS out of /admin.
|
|
12
|
+
*
|
|
13
|
+
* In `pnpm dev` the whole app (the consumer's public site AND /admin) is ONE Vite SPA sharing one
|
|
14
|
+
* `<head>`. A consumer's public global CSS — typically a reset with bare `html { font-size: … }`, `*`,
|
|
15
|
+
* `#__nuxt > div`, `body` — is injected as a persistent `<style>` the moment a public route renders, and
|
|
16
|
+
* Vite/Nuxt never unload it on client-side navigation. Soft-navigating to /admin therefore leaves that
|
|
17
|
+
* sheet live, and because it is globally-selectored it restyles the admin shell (rescaled rems from a
|
|
18
|
+
* changed root font-size, a collapsed rail, wrong fonts). Layout-chunk code-splitting only governs WHEN a
|
|
19
|
+
* sheet is loaded, never WHICH elements it matches once it is present in a shared document — so it is not
|
|
20
|
+
* isolation. This puts the defense upstream so consumers never have to scope their own styles defensively.
|
|
21
|
+
*
|
|
22
|
+
* While the active route is under /admin, this guard disables every stylesheet that is NOT Kestrel's own
|
|
23
|
+
* by flipping its `media` to `not all` (remembering the previous value so it can be restored exactly on
|
|
24
|
+
* leave — reversible, so returning to a public route never leaves it unstyled), and it watches `<head>` so
|
|
25
|
+
* any sheet injected later (HMR, another layer) is disabled too. On leaving /admin it restores everything.
|
|
26
|
+
*
|
|
27
|
+
* Gated to development: in production the public site ships as a separate static `nuxt generate` build with
|
|
28
|
+
* no /admin routes, and the origin serves /admin as its own document — there is no shared `<head>` to guard.
|
|
29
|
+
* Client-only (the static renderer never runs client plugins). Note: Kestrel's admin also re-asserts the
|
|
30
|
+
* root font-size inline while its layout is mounted (see admin.vue), which defends the rem base even in a
|
|
31
|
+
* single-origin production deployment where this dev guard is inactive.
|
|
32
|
+
*/
|
|
33
|
+
export default defineNuxtPlugin(() => {
|
|
34
|
+
if (!import.meta.dev) return
|
|
35
|
+
|
|
36
|
+
// Foreign sheets we disabled → their previous `media` value (null = attribute was absent).
|
|
37
|
+
const disabled = new Map<Element, string | null>()
|
|
38
|
+
let observer: MutationObserver | undefined
|
|
39
|
+
|
|
40
|
+
const disableForeign = () => {
|
|
41
|
+
for (const node of document.querySelectorAll('style, link[rel~="stylesheet"]')) {
|
|
42
|
+
if (isKestrelOwned(node) || disabled.has(node)) continue
|
|
43
|
+
disabled.set(node, node.getAttribute('media'))
|
|
44
|
+
node.setAttribute('media', 'not all')
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const restoreAll = () => {
|
|
49
|
+
for (const [node, media] of disabled) {
|
|
50
|
+
if (media === null) node.removeAttribute('media')
|
|
51
|
+
else node.setAttribute('media', media)
|
|
52
|
+
}
|
|
53
|
+
disabled.clear()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Runs on the initial client navigation and every subsequent one. A named global middleware — the
|
|
57
|
+
// anonymous form cannot take `{ global: true }`.
|
|
58
|
+
addRouteMiddleware(
|
|
59
|
+
'kestrel-admin-style-guard',
|
|
60
|
+
(to) => {
|
|
61
|
+
const onAdmin = to.path === '/admin' || to.path.startsWith('/admin/')
|
|
62
|
+
if (!onAdmin) {
|
|
63
|
+
observer?.disconnect()
|
|
64
|
+
observer = undefined
|
|
65
|
+
restoreAll()
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
disableForeign()
|
|
69
|
+
if (!observer) {
|
|
70
|
+
observer = new MutationObserver(disableForeign)
|
|
71
|
+
observer.observe(document.head, { childList: true, subtree: true })
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{ global: true },
|
|
75
|
+
)
|
|
76
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { makeReauthInterceptor } from '../utils/reauth'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Global re-auth interceptor. When a guarded `/api` call 401s mid-session (the cookie expired or was
|
|
5
|
+
* rotated server-side), an ofetch `onResponseError` hook clears the stale local auth and bounces to the
|
|
6
|
+
* login screen with a redirect back to the current page. Client-only: the static renderer never runs
|
|
7
|
+
* client plugins, so the prerender principal is unaffected; non-admin/public locations are filtered out
|
|
8
|
+
* in `reauthTarget`.
|
|
9
|
+
*/
|
|
10
|
+
export default defineNuxtPlugin(() => {
|
|
11
|
+
const auth = useAuth()
|
|
12
|
+
const router = useRouter()
|
|
13
|
+
const onResponseError = makeReauthInterceptor({
|
|
14
|
+
currentPath: () => router.currentRoute.value.fullPath,
|
|
15
|
+
reset: () => auth.reset(),
|
|
16
|
+
navigate: (to) => { navigateTo(to) },
|
|
17
|
+
})
|
|
18
|
+
// Wrap the auto-imported `$fetch` so every call (incl. useFetch/useAsyncData) runs through the hook.
|
|
19
|
+
globalThis.$fetch = globalThis.$fetch.create({ onResponseError }) as typeof globalThis.$fetch
|
|
20
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Kestrel's own stylesheets live under `…/kestrel/layers/…`; its extensions ship as `kestrel-*` packages
|
|
2
|
+
// (or a monorepo `extensions/` dir → still resolved through a `kestrel-*` name). A Vue `<style scoped>`
|
|
3
|
+
// block is component-scoped (its rules carry a `data-v-<hash>` attribute), so it can NEVER be the global
|
|
4
|
+
// public CSS this guard targets — keep it wherever it comes from, which is what extension AND consumer
|
|
5
|
+
// admin widgets (custom field types, registered editor bodies) rely on.
|
|
6
|
+
const KESTREL_OWNED = /[/\\]kestrel[/\\]layers[/\\]/
|
|
7
|
+
const KESTREL_EXTENSION = /[/\\]kestrel-[^/\\]+[/\\]/
|
|
8
|
+
const SCOPED = /[?&]scoped[=&]/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Whether a stylesheet must be PROTECTED from the dev admin style-guard (i.e. NOT disabled on /admin). True
|
|
12
|
+
* for Kestrel core/extension sheets, any component-scoped Vue style, or a node explicitly tagged
|
|
13
|
+
* `data-kestrel`. Only genuinely foreign global CSS (a consumer's unscoped public reset) falls through to
|
|
14
|
+
* false and gets disabled. Pure → unit-tested without a DOM.
|
|
15
|
+
*/
|
|
16
|
+
export function isProtectedStyle(devId: string, href: string, hasDataKestrel: boolean): boolean {
|
|
17
|
+
if (hasDataKestrel) return true
|
|
18
|
+
if (SCOPED.test(devId)) return true
|
|
19
|
+
return KESTREL_OWNED.test(devId) || KESTREL_OWNED.test(href) || KESTREL_EXTENSION.test(devId) || KESTREL_EXTENSION.test(href)
|
|
20
|
+
}
|