dineway 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/dist/{apply-CAPvMfoU.mjs → apply-iVSqz2qs.mjs} +132 -39
- package/dist/astro/index.d.mts +18 -9
- package/dist/astro/index.mjs +238 -16
- package/dist/astro/middleware/auth.d.mts +16 -5
- package/dist/astro/middleware/auth.mjs +74 -37
- package/dist/astro/middleware/redirect.mjs +24 -8
- package/dist/astro/middleware/request-context.mjs +18 -5
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.mjs +411 -169
- package/dist/astro/types.d.mts +25 -8
- package/dist/{byline-DeWCMU_i.mjs → byline-OhH2dlRu.mjs} +6 -21
- package/dist/{bylines-DyqBV9EQ.mjs → bylines-BGpD9_hy.mjs} +16 -6
- package/dist/cache-BdSY-gQN.mjs +42 -0
- package/dist/chunks--4F8ddV4.mjs +18 -0
- package/dist/cli/index.mjs +935 -15
- package/dist/client/external-auth-headers.d.mts +1 -1
- package/dist/client/index.d.mts +11 -3
- package/dist/client/index.mjs +4 -3
- package/dist/{connection-C9pxzuag.mjs → connection-BCNICDWN.mjs} +22 -5
- package/dist/{content-zSgdNmnt.mjs → content-DWi4d0rT.mjs} +41 -2
- package/dist/database/instrumentation.d.mts +34 -0
- package/dist/database/instrumentation.mjs +53 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/libsql.mjs +11 -5
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.mjs +7 -1
- package/dist/db-errors-CEqD7qH9.mjs +23 -0
- package/dist/{default-WYlzADZL.mjs → default-VjJyuuG9.mjs} +2 -0
- package/dist/{dialect-helpers-B9uSp2GJ.mjs → dialect-helpers-DhTzaUxP.mjs} +3 -0
- package/dist/{error-DrxtnGPg.mjs → error-BmL6QipT.mjs} +7 -3
- package/dist/{index-C-jx21qs.d.mts → index-yvc6E_17.d.mts} +157 -30
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +24 -22
- package/dist/{loader-qKmo0wAY.mjs → loader-sMG4TZ-u.mjs} +9 -3
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/page/index.d.mts +10 -2
- package/dist/page/index.mjs +22 -1
- package/dist/patterns-CrCYkMBb.mjs +92 -0
- package/dist/{placeholder-bOx1xCTY.d.mts → placeholder--wOi4TbO.d.mts} +1 -1
- package/dist/{placeholder-B3knXwNc.mjs → placeholder-Cp8g5Emj.mjs} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-BiaPl_g2.mjs → query-kDmwCsHh.mjs} +118 -50
- package/dist/{redirect-JPqLAbxa.mjs → redirect-DnEWAkVg.mjs} +43 -99
- package/dist/{registry-DSd1GWB8.mjs → registry-C0zjeB9P.mjs} +191 -123
- package/dist/request-cache-Dk5qPSOx.mjs +66 -0
- package/dist/request-context.d.mts +4 -16
- package/dist/{runner-B5l1JfOj.d.mts → runner-CFI6B6J2.d.mts} +1 -1
- package/dist/{runner-BGUGywgG.mjs → runner-DWZm2KQm.mjs} +589 -137
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-BNruJHDL.mjs → search-ByRGV2pq.mjs} +570 -424
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +11 -10
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +11 -3
- package/dist/storage/s3.mjs +78 -15
- package/dist/taxonomies-1s5PaS_8.mjs +266 -0
- package/dist/transaction-Cn2rjY78.mjs +27 -0
- package/dist/{types-BgQeVaPj.d.mts → types-BuMDPy5C.d.mts} +52 -3
- package/dist/{types-DuNbGKjF.mjs → types-COeOq9nK.mjs} +6 -1
- package/dist/{types-ju-_ORz7.d.mts → types-CWbdtiux.d.mts} +13 -5
- package/dist/{types-D38djUXv.d.mts → types-Cj0KMIZV.d.mts} +16 -3
- package/dist/{types-DkvMXalq.d.mts → types-DOrVigru.d.mts} +159 -0
- package/dist/{validate-CXnRKfJK.mjs → validate-BZ5wnLLp.mjs} +2 -1
- package/dist/{validate-DVKJJ-M_.d.mts → validate-IPf8n4Fj.d.mts} +4 -51
- package/dist/{validate-CqRJb_xU.mjs → validate-VPnKoIzW.mjs} +10 -10
- package/dist/version-BKXPsfmJ.mjs +6 -0
- package/package.json +53 -39
- package/src/astro/routes/PluginRegistry.tsx +21 -0
- package/src/astro/routes/admin.astro +99 -0
- package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
- package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
- package/src/astro/routes/api/admin/api-tokens/[id].ts +44 -0
- package/src/astro/routes/api/admin/api-tokens/index.ts +90 -0
- package/src/astro/routes/api/admin/briefing.ts +76 -0
- package/src/astro/routes/api/admin/bylines/[id]/index.ts +90 -0
- package/src/astro/routes/api/admin/bylines/index.ts +74 -0
- package/src/astro/routes/api/admin/comments/[id]/status.ts +120 -0
- package/src/astro/routes/api/admin/comments/[id].ts +64 -0
- package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
- package/src/astro/routes/api/admin/comments/counts.ts +30 -0
- package/src/astro/routes/api/admin/comments/index.ts +46 -0
- package/src/astro/routes/api/admin/context/[id]/history.ts +35 -0
- package/src/astro/routes/api/admin/context/[id]/index.ts +35 -0
- package/src/astro/routes/api/admin/context/[id]/review.ts +57 -0
- package/src/astro/routes/api/admin/context/[id]/supersede.ts +58 -0
- package/src/astro/routes/api/admin/context/diff.ts +35 -0
- package/src/astro/routes/api/admin/context/index.ts +69 -0
- package/src/astro/routes/api/admin/context/stale.ts +35 -0
- package/src/astro/routes/api/admin/hitl-requests/[id]/index.ts +38 -0
- package/src/astro/routes/api/admin/hitl-requests/[id]/resolve.ts +54 -0
- package/src/astro/routes/api/admin/hitl-requests/index.ts +38 -0
- package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +132 -0
- package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
- package/src/astro/routes/api/admin/oauth-clients/[id].ts +137 -0
- package/src/astro/routes/api/admin/oauth-clients/index.ts +95 -0
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +91 -0
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +91 -0
- package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +98 -0
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +154 -0
- package/src/astro/routes/api/admin/plugins/index.ts +32 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +62 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +135 -0
- package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
- package/src/astro/routes/api/admin/review-requests/[id]/index.ts +35 -0
- package/src/astro/routes/api/admin/review-requests/[id]/resolve.ts +52 -0
- package/src/astro/routes/api/admin/review-requests/index.ts +35 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +62 -0
- package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
- package/src/astro/routes/api/admin/users/[id]/disable.ts +72 -0
- package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
- package/src/astro/routes/api/admin/users/[id]/index.ts +166 -0
- package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
- package/src/astro/routes/api/admin/users/index.ts +66 -0
- package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
- package/src/astro/routes/api/auth/invite/accept.ts +52 -0
- package/src/astro/routes/api/auth/invite/complete.ts +86 -0
- package/src/astro/routes/api/auth/invite/index.ts +99 -0
- package/src/astro/routes/api/auth/invite/register-options.ts +73 -0
- package/src/astro/routes/api/auth/logout.ts +40 -0
- package/src/astro/routes/api/auth/magic-link/send.ts +90 -0
- package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
- package/src/astro/routes/api/auth/me.ts +60 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +221 -0
- package/src/astro/routes/api/auth/oauth/[provider].ts +120 -0
- package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
- package/src/astro/routes/api/auth/passkey/index.ts +54 -0
- package/src/astro/routes/api/auth/passkey/options.ts +85 -0
- package/src/astro/routes/api/auth/passkey/register/options.ts +88 -0
- package/src/astro/routes/api/auth/passkey/register/verify.ts +119 -0
- package/src/astro/routes/api/auth/passkey/verify.ts +72 -0
- package/src/astro/routes/api/auth/signup/complete.ts +87 -0
- package/src/astro/routes/api/auth/signup/request.ts +89 -0
- package/src/astro/routes/api/auth/signup/verify.ts +53 -0
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +310 -0
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +68 -0
- package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +77 -0
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +42 -0
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +100 -0
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +64 -0
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
- package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +129 -0
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +143 -0
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +50 -0
- package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +69 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +173 -0
- package/src/astro/routes/api/content/[collection]/index.ts +103 -0
- package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
- package/src/astro/routes/api/dashboard.ts +32 -0
- package/src/astro/routes/api/dev/emails.ts +36 -0
- package/src/astro/routes/api/health.ts +54 -0
- package/src/astro/routes/api/import/probe.ts +47 -0
- package/src/astro/routes/api/import/wordpress/analyze.ts +523 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +330 -0
- package/src/astro/routes/api/import/wordpress/media.ts +338 -0
- package/src/astro/routes/api/import/wordpress/prepare.ts +212 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +425 -0
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
- package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +399 -0
- package/src/astro/routes/api/manifest.ts +75 -0
- package/src/astro/routes/api/mcp.ts +125 -0
- package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
- package/src/astro/routes/api/media/[id].ts +145 -0
- package/src/astro/routes/api/media/file/[...key].ts +79 -0
- package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +91 -0
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
- package/src/astro/routes/api/media/providers/index.ts +30 -0
- package/src/astro/routes/api/media/upload-url.ts +146 -0
- package/src/astro/routes/api/media.ts +204 -0
- package/src/astro/routes/api/menus/[name]/items.ts +206 -0
- package/src/astro/routes/api/menus/[name]/reorder.ts +79 -0
- package/src/astro/routes/api/menus/[name].ts +145 -0
- package/src/astro/routes/api/menus/index.ts +91 -0
- package/src/astro/routes/api/oauth/authorize.ts +430 -0
- package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
- package/src/astro/routes/api/oauth/device/code.ts +56 -0
- package/src/astro/routes/api/oauth/device/token.ts +70 -0
- package/src/astro/routes/api/oauth/register.ts +182 -0
- package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
- package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
- package/src/astro/routes/api/oauth/token.ts +195 -0
- package/src/astro/routes/api/openapi.json.ts +33 -0
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +109 -0
- package/src/astro/routes/api/redirects/404s/index.ts +72 -0
- package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
- package/src/astro/routes/api/redirects/[id].ts +183 -0
- package/src/astro/routes/api/redirects/index.ts +100 -0
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
- package/src/astro/routes/api/revisions/[revisionId]/restore.ts +62 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +104 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +67 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +45 -0
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +107 -0
- package/src/astro/routes/api/schema/collections/index.ts +61 -0
- package/src/astro/routes/api/schema/index.ts +109 -0
- package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
- package/src/astro/routes/api/schema/orphans/index.ts +26 -0
- package/src/astro/routes/api/search/enable.ts +64 -0
- package/src/astro/routes/api/search/index.ts +52 -0
- package/src/astro/routes/api/search/rebuild.ts +72 -0
- package/src/astro/routes/api/search/stats.ts +35 -0
- package/src/astro/routes/api/search/suggest.ts +50 -0
- package/src/astro/routes/api/sections/[slug].ts +203 -0
- package/src/astro/routes/api/sections/index.ts +107 -0
- package/src/astro/routes/api/settings/email.ts +150 -0
- package/src/astro/routes/api/settings.ts +116 -0
- package/src/astro/routes/api/setup/admin-verify.ts +122 -0
- package/src/astro/routes/api/setup/admin.ts +104 -0
- package/src/astro/routes/api/setup/dev-bypass.ts +200 -0
- package/src/astro/routes/api/setup/dev-reset.ts +40 -0
- package/src/astro/routes/api/setup/index.ts +128 -0
- package/src/astro/routes/api/setup/status.ts +122 -0
- package/src/astro/routes/api/snapshot.ts +76 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +232 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +131 -0
- package/src/astro/routes/api/taxonomies/index.ts +114 -0
- package/src/astro/routes/api/themes/preview.ts +78 -0
- package/src/astro/routes/api/typegen.ts +114 -0
- package/src/astro/routes/api/well-known/auth.ts +71 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +48 -0
- package/src/astro/routes/api/well-known/oauth-protected-resource.ts +39 -0
- package/src/astro/routes/api/widget-areas/[name]/reorder.ts +114 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +213 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +126 -0
- package/src/astro/routes/api/widget-areas/[name].ts +135 -0
- package/src/astro/routes/api/widget-areas/index.ts +149 -0
- package/src/astro/routes/api/widget-components.ts +22 -0
- package/src/astro/routes/robots.txt.ts +81 -0
- package/src/astro/routes/sitemap-[collection].xml.ts +104 -0
- package/src/astro/routes/sitemap.xml.ts +92 -0
- package/src/components/Break.astro +45 -0
- package/src/components/Button.astro +71 -0
- package/src/components/Buttons.astro +49 -0
- package/src/components/Code.astro +59 -0
- package/src/components/Columns.astro +59 -0
- package/src/components/CommentForm.astro +315 -0
- package/src/components/Comments.astro +232 -0
- package/src/components/Cover.astro +128 -0
- package/src/components/DinewayBodyEnd.astro +32 -0
- package/src/components/DinewayBodyStart.astro +32 -0
- package/src/components/DinewayHead.astro +61 -0
- package/src/components/DinewayImage.astro +178 -0
- package/src/components/DinewayMedia.astro +167 -0
- package/src/components/Embed.astro +128 -0
- package/src/components/File.astro +122 -0
- package/src/components/Gallery.astro +93 -0
- package/src/components/HtmlBlock.astro +33 -0
- package/src/components/Image.astro +178 -0
- package/src/components/InlineEditor.astro +27 -0
- package/src/components/InlinePortableTextEditor.tsx +1937 -0
- package/src/components/LiveSearch.astro +614 -0
- package/src/components/PortableText.astro +51 -0
- package/src/components/Pullquote.astro +51 -0
- package/src/components/Table.astro +135 -0
- package/src/components/WidgetArea.astro +22 -0
- package/src/components/WidgetRenderer.astro +72 -0
- package/src/components/index.ts +106 -0
- package/src/components/marks/Link.astro +31 -0
- package/src/components/marks/StrikeThrough.astro +7 -0
- package/src/components/marks/Subscript.astro +7 -0
- package/src/components/marks/Superscript.astro +7 -0
- package/src/components/marks/Underline.astro +7 -0
- package/src/components/marks.ts +19 -0
- package/src/components/widgets/Archives.astro +65 -0
- package/src/components/widgets/Categories.astro +35 -0
- package/src/components/widgets/RecentPosts.astro +51 -0
- package/src/components/widgets/Search.astro +18 -0
- package/src/components/widgets/Tags.astro +38 -0
- package/src/ui.ts +75 -0
- package/LICENSE +0 -9
- /package/dist/{adapters-BlzWJG82.d.mts → adapters-C2ypTrZZ.d.mts} +0 -0
- /package/dist/{config-Cq8H0SfX.mjs → config-BXwuX8Bx.mjs} +0 -0
- /package/dist/{load-C6FCD1FU.mjs → load-Coc9HpHH.mjs} +0 -0
- /package/dist/{manifest-schema-CTSEyIJ3.mjs → manifest-schema-D1MSVnoI.mjs} +0 -0
- /package/dist/{mode-BlyYtIFO.mjs → mode-47goXBBK.mjs} +0 -0
- /package/dist/{tokens-4vgYuXsZ.mjs → tokens-CJz9ubV6.mjs} +0 -0
- /package/dist/{transport-C5FYnid7.mjs → transport-DB5eDN4x.mjs} +0 -0
- /package/dist/{transport-gIL-e43D.d.mts → transport-Wge_IzKl.d.mts} +0 -0
- /package/dist/{types-CLLdsG3g.d.mts → types-BzcUjoqg.d.mts} +0 -0
- /package/dist/{types-DShnjzb6.mjs → types-griIBQOQ.mjs} +0 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public comment endpoints
|
|
3
|
+
*
|
|
4
|
+
* GET /_dineway/api/comments/:collection/:contentId - List approved comments
|
|
5
|
+
* POST /_dineway/api/comments/:collection/:contentId - Submit a comment
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { APIRoute } from "astro";
|
|
9
|
+
|
|
10
|
+
import { apiError, apiSuccess, handleError, requireDb, unwrapResult } from "#api/error.js";
|
|
11
|
+
import { handleCommentList, checkRateLimit, hashIp } from "#api/handlers/comments.js";
|
|
12
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
13
|
+
import { createCommentBody } from "#api/schemas.js";
|
|
14
|
+
import { getSiteBaseUrl } from "#api/site-url.js";
|
|
15
|
+
import { sendCommentNotification } from "#comments/notifications.js";
|
|
16
|
+
import { createComment, type CommentHookRunner } from "#comments/service.js";
|
|
17
|
+
import { CommentRepository } from "#db/repositories/comment.js";
|
|
18
|
+
import { validateIdentifier } from "#db/validate.js";
|
|
19
|
+
import { extractRequestMeta } from "#plugins/request-meta.js";
|
|
20
|
+
import type { CollectionCommentSettings, ModerationDecision } from "#plugins/types.js";
|
|
21
|
+
|
|
22
|
+
export const prerender = false;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* List approved comments for a content item (public, no auth required)
|
|
26
|
+
*/
|
|
27
|
+
export const GET: APIRoute = async ({ params, url, locals }) => {
|
|
28
|
+
const { dineway } = locals;
|
|
29
|
+
const { collection, contentId } = params;
|
|
30
|
+
|
|
31
|
+
if (!collection || !contentId) {
|
|
32
|
+
return apiError("VALIDATION_ERROR", "Collection and content ID required", 400);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const dbErr = requireDb(dineway?.db);
|
|
36
|
+
if (dbErr) return dbErr;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const limit = Math.min(Number(url.searchParams.get("limit") || 50), 100);
|
|
40
|
+
const cursor = url.searchParams.get("cursor") ?? undefined;
|
|
41
|
+
const threaded = url.searchParams.get("threaded") === "true";
|
|
42
|
+
|
|
43
|
+
// Check collection exists and has comments enabled
|
|
44
|
+
const collectionRow = await dineway.db
|
|
45
|
+
.selectFrom("_dineway_collections")
|
|
46
|
+
.select(["comments_enabled"])
|
|
47
|
+
.where("slug", "=", collection)
|
|
48
|
+
.executeTakeFirst();
|
|
49
|
+
|
|
50
|
+
if (!collectionRow) {
|
|
51
|
+
return apiError("NOT_FOUND", `Collection '${collection}' not found`, 404);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!collectionRow.comments_enabled) {
|
|
55
|
+
return apiError("COMMENTS_DISABLED", "Comments are not enabled for this collection", 403);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const result = await handleCommentList(dineway.db, collection, contentId, {
|
|
59
|
+
limit,
|
|
60
|
+
cursor,
|
|
61
|
+
threaded,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return unwrapResult(result);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return handleError(error, "Failed to list comments", "COMMENT_LIST_ERROR");
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Submit a comment (public, gated by anti-spam checks)
|
|
72
|
+
*/
|
|
73
|
+
export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
74
|
+
const { dineway, user } = locals;
|
|
75
|
+
const { collection, contentId } = params;
|
|
76
|
+
|
|
77
|
+
if (!collection || !contentId) {
|
|
78
|
+
return apiError("VALIDATION_ERROR", "Collection and content ID required", 400);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const dbErr = requireDb(dineway?.db);
|
|
82
|
+
if (dbErr) return dbErr;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// Parse and validate input
|
|
86
|
+
const body = await parseBody(request, createCommentBody);
|
|
87
|
+
if (isParseError(body)) return body;
|
|
88
|
+
|
|
89
|
+
// Check collection exists and has comments enabled
|
|
90
|
+
const collectionRow = await dineway.db
|
|
91
|
+
.selectFrom("_dineway_collections")
|
|
92
|
+
.select([
|
|
93
|
+
"comments_enabled",
|
|
94
|
+
"comments_moderation",
|
|
95
|
+
"comments_closed_after_days",
|
|
96
|
+
"comments_auto_approve_users",
|
|
97
|
+
])
|
|
98
|
+
.where("slug", "=", collection)
|
|
99
|
+
.executeTakeFirst();
|
|
100
|
+
|
|
101
|
+
if (!collectionRow) {
|
|
102
|
+
return apiError("NOT_FOUND", `Collection '${collection}' not found`, 404);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!collectionRow.comments_enabled) {
|
|
106
|
+
return apiError("COMMENTS_DISABLED", "Comments are not enabled for this collection", 403);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Verify the content item exists, is published, and not soft-deleted
|
|
110
|
+
validateIdentifier(collection, "collection");
|
|
111
|
+
const contentRow = await dineway.db
|
|
112
|
+
.selectFrom(`ec_${collection}` as never)
|
|
113
|
+
.select(["id" as never, "slug" as never, "author_id" as never, "published_at" as never])
|
|
114
|
+
.where("id" as never, "=", contentId as never)
|
|
115
|
+
.where("status" as never, "=", "published" as never)
|
|
116
|
+
.where("deleted_at" as never, "is", null as never)
|
|
117
|
+
.executeTakeFirst();
|
|
118
|
+
|
|
119
|
+
if (!contentRow) {
|
|
120
|
+
return apiError("NOT_FOUND", "Content not found", 404);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check if comments are closed (published_at + closed_after_days)
|
|
124
|
+
if (collectionRow.comments_closed_after_days > 0) {
|
|
125
|
+
const publishedAt = (contentRow as { published_at: string | null }).published_at;
|
|
126
|
+
if (publishedAt) {
|
|
127
|
+
const closedDate = new Date(publishedAt);
|
|
128
|
+
closedDate.setDate(closedDate.getDate() + collectionRow.comments_closed_after_days);
|
|
129
|
+
if (new Date() > closedDate) {
|
|
130
|
+
return apiError("COMMENTS_CLOSED", "Comments are closed for this content", 403);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Anti-spam: Honeypot — hidden field filled only by bots
|
|
136
|
+
if (body.website_url) {
|
|
137
|
+
// Silently accept — don't reveal the honeypot to bots
|
|
138
|
+
return apiSuccess({ status: "pending", message: "Comment submitted for review" });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Anti-spam: Rate limiting
|
|
142
|
+
const meta = extractRequestMeta(request, dineway.config);
|
|
143
|
+
const ipSalt =
|
|
144
|
+
import.meta.env.DINEWAY_AUTH_SECRET || import.meta.env.AUTH_SECRET || "dineway-ip-salt";
|
|
145
|
+
let ipHash: string;
|
|
146
|
+
if (meta.ip) {
|
|
147
|
+
ipHash = await hashIp(meta.ip, ipSalt);
|
|
148
|
+
} else {
|
|
149
|
+
// Fail closed: all unidentifiable requests share one rate-limit bucket.
|
|
150
|
+
// Use a larger limit since this bucket is shared across all anonymous users.
|
|
151
|
+
ipHash = "unknown";
|
|
152
|
+
}
|
|
153
|
+
const unknownBucketLimit = ipHash === "unknown" ? 20 : undefined;
|
|
154
|
+
const rateLimited = await checkRateLimit(dineway.db, ipHash, unknownBucketLimit);
|
|
155
|
+
if (rateLimited) {
|
|
156
|
+
return apiError("RATE_LIMITED", "Too many comments. Please try again later.", 429);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Build collection settings
|
|
160
|
+
const collectionSettings: CollectionCommentSettings = {
|
|
161
|
+
commentsEnabled: collectionRow.comments_enabled === 1,
|
|
162
|
+
commentsModeration:
|
|
163
|
+
collectionRow.comments_moderation as CollectionCommentSettings["commentsModeration"],
|
|
164
|
+
commentsClosedAfterDays: collectionRow.comments_closed_after_days,
|
|
165
|
+
commentsAutoApproveUsers: collectionRow.comments_auto_approve_users === 1,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Determine author fields — authenticated user overrides form input
|
|
169
|
+
let authorName = body.authorName;
|
|
170
|
+
let authorEmail = body.authorEmail;
|
|
171
|
+
let authorUserId: string | null = null;
|
|
172
|
+
|
|
173
|
+
if (user) {
|
|
174
|
+
authorName = user.name || authorName;
|
|
175
|
+
authorEmail = user.email;
|
|
176
|
+
authorUserId = user.id;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Validate parent exists and belongs to the same content.
|
|
180
|
+
// Enforce 1-level nesting: if the parent is itself a reply, attach to its root.
|
|
181
|
+
let resolvedParentId = body.parentId ?? null;
|
|
182
|
+
if (body.parentId) {
|
|
183
|
+
const repo = new CommentRepository(dineway.db);
|
|
184
|
+
const parent = await repo.findById(body.parentId);
|
|
185
|
+
if (!parent) {
|
|
186
|
+
return apiError("VALIDATION_ERROR", "Parent comment not found", 400);
|
|
187
|
+
}
|
|
188
|
+
if (parent.collection !== collection || parent.contentId !== contentId) {
|
|
189
|
+
return apiError("VALIDATION_ERROR", "Parent comment belongs to different content", 400);
|
|
190
|
+
}
|
|
191
|
+
// Flatten: if parent is a reply, use its parent (the root) instead
|
|
192
|
+
resolvedParentId = parent.parentId ?? parent.id;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Wire the comment service to the real hook pipeline
|
|
196
|
+
const hookRunner: CommentHookRunner = {
|
|
197
|
+
async runBeforeCreate(event) {
|
|
198
|
+
return dineway.hooks.runCommentBeforeCreate(event);
|
|
199
|
+
},
|
|
200
|
+
async runModerate(event) {
|
|
201
|
+
const result = await dineway.hooks.invokeExclusiveHook("comment:moderate", event);
|
|
202
|
+
if (!result) return { status: "pending" as const, reason: "No moderator configured" };
|
|
203
|
+
if (result.error) {
|
|
204
|
+
console.error(`[comments] Moderation error (${result.pluginId}):`, result.error.message);
|
|
205
|
+
return { status: "pending" as const, reason: "Moderation error" };
|
|
206
|
+
}
|
|
207
|
+
return result.result as ModerationDecision;
|
|
208
|
+
},
|
|
209
|
+
fireAfterCreate(event) {
|
|
210
|
+
dineway.hooks
|
|
211
|
+
.runCommentAfterCreate(event)
|
|
212
|
+
.catch((err) =>
|
|
213
|
+
console.error(
|
|
214
|
+
"[comments] afterCreate error:",
|
|
215
|
+
err instanceof Error ? err.message : err,
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
},
|
|
219
|
+
fireAfterModerate(event) {
|
|
220
|
+
dineway.hooks
|
|
221
|
+
.runCommentAfterModerate(event)
|
|
222
|
+
.catch((err) =>
|
|
223
|
+
console.error(
|
|
224
|
+
"[comments] afterModerate error:",
|
|
225
|
+
err instanceof Error ? err.message : err,
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Build content info for afterCreate hooks (e.g. email notifications)
|
|
232
|
+
const typedContent = contentRow as {
|
|
233
|
+
id: string;
|
|
234
|
+
slug: string;
|
|
235
|
+
author_id: string | null;
|
|
236
|
+
};
|
|
237
|
+
let contentAuthor: { id: string; name: string | null; email: string } | undefined;
|
|
238
|
+
if (typedContent.author_id) {
|
|
239
|
+
const authorRow = await dineway.db
|
|
240
|
+
.selectFrom("users")
|
|
241
|
+
.select(["id", "name", "email", "email_verified"])
|
|
242
|
+
.where("id", "=", typedContent.author_id)
|
|
243
|
+
.executeTakeFirst();
|
|
244
|
+
if (authorRow && authorRow.email_verified) {
|
|
245
|
+
contentAuthor = {
|
|
246
|
+
id: authorRow.id,
|
|
247
|
+
name: authorRow.name,
|
|
248
|
+
email: authorRow.email,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const result = await createComment(
|
|
254
|
+
dineway.db,
|
|
255
|
+
{
|
|
256
|
+
collection,
|
|
257
|
+
contentId,
|
|
258
|
+
parentId: resolvedParentId,
|
|
259
|
+
authorName,
|
|
260
|
+
authorEmail,
|
|
261
|
+
authorUserId,
|
|
262
|
+
body: body.body,
|
|
263
|
+
ipHash,
|
|
264
|
+
userAgent: meta.userAgent,
|
|
265
|
+
},
|
|
266
|
+
collectionSettings,
|
|
267
|
+
hookRunner,
|
|
268
|
+
{
|
|
269
|
+
id: typedContent.id,
|
|
270
|
+
collection,
|
|
271
|
+
slug: typedContent.slug,
|
|
272
|
+
author: contentAuthor,
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
if (!result) {
|
|
277
|
+
return apiError("COMMENT_REJECTED", "Comment was rejected", 403);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Send notification to content author and await completion before the
|
|
281
|
+
// response is sent so the delivery path does not outlive the request.
|
|
282
|
+
if (result.comment.status === "approved" && dineway.email && contentAuthor) {
|
|
283
|
+
try {
|
|
284
|
+
const adminBaseUrl = await getSiteBaseUrl(dineway.db, request);
|
|
285
|
+
await sendCommentNotification({
|
|
286
|
+
email: dineway.email,
|
|
287
|
+
comment: result.comment,
|
|
288
|
+
contentAuthor,
|
|
289
|
+
adminBaseUrl,
|
|
290
|
+
});
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.error("[comments] notification error:", err instanceof Error ? err.message : err);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return apiSuccess(
|
|
297
|
+
{
|
|
298
|
+
id: result.comment.id,
|
|
299
|
+
status: result.comment.status,
|
|
300
|
+
message:
|
|
301
|
+
result.comment.status === "approved"
|
|
302
|
+
? "Comment published"
|
|
303
|
+
: "Comment submitted for review",
|
|
304
|
+
},
|
|
305
|
+
201,
|
|
306
|
+
);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
return handleError(error, "Failed to submit comment", "COMMENT_CREATE_ERROR");
|
|
309
|
+
}
|
|
310
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare live and draft revisions
|
|
3
|
+
*
|
|
4
|
+
* GET /_dineway/api/content/{collection}/{id}/compare
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
|
|
9
|
+
import { requirePerm } from "#api/authorize.js";
|
|
10
|
+
import { apiError, unwrapResult } from "#api/error.js";
|
|
11
|
+
|
|
12
|
+
export const prerender = false;
|
|
13
|
+
|
|
14
|
+
export const GET: APIRoute = async ({ params, locals }) => {
|
|
15
|
+
const { dineway, user } = locals;
|
|
16
|
+
const denied = requirePerm(user, "content:read_drafts");
|
|
17
|
+
if (denied) return denied;
|
|
18
|
+
const collection = params.collection!;
|
|
19
|
+
const id = params.id!;
|
|
20
|
+
|
|
21
|
+
if (!dineway?.handleContentCompare) {
|
|
22
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result = await dineway.handleContentCompare(collection, id);
|
|
26
|
+
|
|
27
|
+
return unwrapResult(result);
|
|
28
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discard draft changes - reverts to live version
|
|
3
|
+
*
|
|
4
|
+
* POST /_dineway/api/content/{collection}/{id}/discard-draft
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
|
|
9
|
+
import { requireOwnerPerm } from "#api/authorize.js";
|
|
10
|
+
import { apiError, mapErrorStatus, unwrapResult } from "#api/error.js";
|
|
11
|
+
import {
|
|
12
|
+
contentApiRouteSource,
|
|
13
|
+
extractActivityItemId,
|
|
14
|
+
logContentActivity,
|
|
15
|
+
} from "#site-context/activity-events.js";
|
|
16
|
+
|
|
17
|
+
export const prerender = false;
|
|
18
|
+
|
|
19
|
+
export const POST: APIRoute = async ({ params, locals, cache }) => {
|
|
20
|
+
const { dineway, user } = locals;
|
|
21
|
+
const collection = params.collection!;
|
|
22
|
+
const id = params.id!;
|
|
23
|
+
|
|
24
|
+
if (!dineway?.handleContentDiscardDraft || !dineway?.handleContentGet) {
|
|
25
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Fetch item to check ownership
|
|
29
|
+
const existing = await dineway.handleContentGet(collection, id);
|
|
30
|
+
if (!existing.success) {
|
|
31
|
+
return apiError(
|
|
32
|
+
existing.error?.code ?? "UNKNOWN_ERROR",
|
|
33
|
+
existing.error?.message ?? "Unknown error",
|
|
34
|
+
mapErrorStatus(existing.error?.code),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const existingData =
|
|
38
|
+
existing.data && typeof existing.data === "object"
|
|
39
|
+
? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check above
|
|
40
|
+
(existing.data as Record<string, unknown>)
|
|
41
|
+
: undefined;
|
|
42
|
+
// Handler returns { item, _rev } — extract the item for ownership check
|
|
43
|
+
const existingItem =
|
|
44
|
+
existingData?.item && typeof existingData.item === "object"
|
|
45
|
+
? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by typeof check above
|
|
46
|
+
(existingData.item as Record<string, unknown>)
|
|
47
|
+
: existingData;
|
|
48
|
+
const authorId = typeof existingItem?.authorId === "string" ? existingItem.authorId : "";
|
|
49
|
+
const denied = requireOwnerPerm(user, authorId, "content:edit_own", "content:edit_any");
|
|
50
|
+
if (denied) return denied;
|
|
51
|
+
const resolvedId = typeof existingItem?.id === "string" ? existingItem.id : id;
|
|
52
|
+
|
|
53
|
+
const result = await dineway.handleContentDiscardDraft(collection, resolvedId);
|
|
54
|
+
|
|
55
|
+
if (!result.success) return unwrapResult(result);
|
|
56
|
+
|
|
57
|
+
if (cache.enabled) await cache.invalidate({ tags: [collection, resolvedId] });
|
|
58
|
+
|
|
59
|
+
await logContentActivity(dineway.db, locals, {
|
|
60
|
+
action: "draft_discarded",
|
|
61
|
+
collection,
|
|
62
|
+
entryId: extractActivityItemId(result.data, resolvedId),
|
|
63
|
+
...contentApiRouteSource("draft_discarded"),
|
|
64
|
+
summary: `Discarded draft for content in ${collection}`,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return unwrapResult(result);
|
|
68
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duplicate content endpoint - injected by Dineway integration
|
|
3
|
+
*
|
|
4
|
+
* POST /_dineway/api/content/{collection}/{id}/duplicate - Create a copy
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
|
|
9
|
+
import { requirePerm, requireOwnerPerm } from "#api/authorize.js";
|
|
10
|
+
import { apiError, mapErrorStatus, unwrapResult } from "#api/error.js";
|
|
11
|
+
import {
|
|
12
|
+
contentApiRouteSource,
|
|
13
|
+
extractActivityItemId,
|
|
14
|
+
logContentActivity,
|
|
15
|
+
} from "#site-context/activity-events.js";
|
|
16
|
+
|
|
17
|
+
export const prerender = false;
|
|
18
|
+
|
|
19
|
+
export const POST: APIRoute = async ({ params, locals, cache }) => {
|
|
20
|
+
const { dineway, user } = locals;
|
|
21
|
+
const collection = params.collection!;
|
|
22
|
+
const id = params.id!;
|
|
23
|
+
|
|
24
|
+
const denied = requirePerm(user, "content:create");
|
|
25
|
+
if (denied) return denied;
|
|
26
|
+
|
|
27
|
+
if (!dineway?.handleContentDuplicate || !dineway?.handleContentGet) {
|
|
28
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Fetch item to check ownership — duplicating requires read access to the source
|
|
32
|
+
const existing = await dineway.handleContentGet(collection, id);
|
|
33
|
+
if (!existing.success) {
|
|
34
|
+
return apiError(
|
|
35
|
+
existing.error?.code ?? "UNKNOWN_ERROR",
|
|
36
|
+
existing.error?.message ?? "Unknown error",
|
|
37
|
+
mapErrorStatus(existing.error?.code),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const existingData =
|
|
42
|
+
existing.data && typeof existing.data === "object"
|
|
43
|
+
? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- handler returns unknown data; narrowed by typeof check above
|
|
44
|
+
(existing.data as Record<string, unknown>)
|
|
45
|
+
: undefined;
|
|
46
|
+
// Handler returns { item, _rev } — extract the item for ownership check
|
|
47
|
+
const existingItem =
|
|
48
|
+
existingData?.item && typeof existingData.item === "object"
|
|
49
|
+
? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed by typeof check above
|
|
50
|
+
(existingData.item as Record<string, unknown>)
|
|
51
|
+
: existingData;
|
|
52
|
+
const authorId = typeof existingItem?.authorId === "string" ? existingItem.authorId : "";
|
|
53
|
+
// Duplicating requires read access to the source — check ownership-based edit permissions
|
|
54
|
+
// since content:read is flat (no own/any split). This ensures authors can only duplicate their own.
|
|
55
|
+
const readDenied = requireOwnerPerm(user, authorId, "content:edit_own", "content:edit_any");
|
|
56
|
+
if (readDenied) return readDenied;
|
|
57
|
+
|
|
58
|
+
const resolvedId = typeof existingItem?.id === "string" ? existingItem.id : id;
|
|
59
|
+
const result = await dineway.handleContentDuplicate(collection, resolvedId, user?.id);
|
|
60
|
+
|
|
61
|
+
if (!result.success) return unwrapResult(result);
|
|
62
|
+
|
|
63
|
+
if (cache.enabled) await cache.invalidate({ tags: [collection] });
|
|
64
|
+
|
|
65
|
+
await logContentActivity(dineway.db, locals, {
|
|
66
|
+
action: "duplicated",
|
|
67
|
+
collection,
|
|
68
|
+
entryId: extractActivityItemId(result.data),
|
|
69
|
+
...contentApiRouteSource("duplicated"),
|
|
70
|
+
summary: `Duplicated content in ${collection}`,
|
|
71
|
+
detail: {
|
|
72
|
+
sourceEntryId: resolvedId,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return unwrapResult(result, 201);
|
|
77
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permanent delete content endpoint - injected by Dineway integration
|
|
3
|
+
*
|
|
4
|
+
* DELETE /_dineway/api/content/{collection}/{id}/permanent - Permanently delete (no undo)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
|
|
9
|
+
import { requirePerm } from "#api/authorize.js";
|
|
10
|
+
import { apiError, unwrapResult } from "#api/error.js";
|
|
11
|
+
import { contentApiRouteSource, logContentActivity } from "#site-context/activity-events.js";
|
|
12
|
+
|
|
13
|
+
export const prerender = false;
|
|
14
|
+
|
|
15
|
+
export const DELETE: APIRoute = async ({ params, locals, cache }) => {
|
|
16
|
+
const { dineway, user } = locals;
|
|
17
|
+
const collection = params.collection!;
|
|
18
|
+
const id = params.id!;
|
|
19
|
+
|
|
20
|
+
const denied = requirePerm(user, "import:execute");
|
|
21
|
+
if (denied) return denied;
|
|
22
|
+
|
|
23
|
+
if (!dineway?.handleContentPermanentDelete) {
|
|
24
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = await dineway.handleContentPermanentDelete(collection, id);
|
|
28
|
+
|
|
29
|
+
if (!result.success) return unwrapResult(result);
|
|
30
|
+
|
|
31
|
+
if (cache.enabled) await cache.invalidate({ tags: [collection, id] });
|
|
32
|
+
|
|
33
|
+
await logContentActivity(dineway.db, locals, {
|
|
34
|
+
action: "permanently_deleted",
|
|
35
|
+
collection,
|
|
36
|
+
entryId: id,
|
|
37
|
+
...contentApiRouteSource("permanently_deleted"),
|
|
38
|
+
summary: `Permanently deleted content in ${collection}`,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return unwrapResult(result);
|
|
42
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preview URL endpoint - generates a signed preview URL for content
|
|
3
|
+
*
|
|
4
|
+
* POST /_dineway/api/content/{collection}/{id}/preview-url
|
|
5
|
+
*
|
|
6
|
+
* Request body:
|
|
7
|
+
* {
|
|
8
|
+
* expiresIn?: string | number; // Default: "1h"
|
|
9
|
+
* pathPattern?: string; // Default: "/{collection}/{id}"
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* Response:
|
|
13
|
+
* {
|
|
14
|
+
* url: string; // The preview URL with token
|
|
15
|
+
* expiresAt: number; // Unix timestamp when token expires
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { APIRoute } from "astro";
|
|
20
|
+
|
|
21
|
+
import { requirePerm } from "#api/authorize.js";
|
|
22
|
+
import { apiError, apiSuccess, handleError, unwrapResult } from "#api/error.js";
|
|
23
|
+
import { parseOptionalBody, isParseError } from "#api/parse.js";
|
|
24
|
+
import { contentPreviewUrlBody } from "#api/schemas.js";
|
|
25
|
+
import { getPreviewUrl } from "#preview/index.js";
|
|
26
|
+
|
|
27
|
+
export const prerender = false;
|
|
28
|
+
|
|
29
|
+
const DURATION_PATTERN = /^(\d+)([smhdw])$/;
|
|
30
|
+
|
|
31
|
+
export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
32
|
+
const { dineway, user } = locals;
|
|
33
|
+
const denied = requirePerm(user, "content:read_drafts");
|
|
34
|
+
if (denied) return denied;
|
|
35
|
+
const collection = params.collection!;
|
|
36
|
+
const id = params.id!;
|
|
37
|
+
|
|
38
|
+
// Get the preview secret from environment
|
|
39
|
+
const previewSecret = import.meta.env.DINEWAY_PREVIEW_SECRET || import.meta.env.PREVIEW_SECRET;
|
|
40
|
+
|
|
41
|
+
if (!previewSecret) {
|
|
42
|
+
return apiError(
|
|
43
|
+
"NOT_CONFIGURED",
|
|
44
|
+
"Preview not configured. Set DINEWAY_PREVIEW_SECRET environment variable.",
|
|
45
|
+
500,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Verify the content exists (optional, but good for UX)
|
|
50
|
+
if (dineway?.handleContentGet) {
|
|
51
|
+
const result = await dineway.handleContentGet(collection, id);
|
|
52
|
+
if (!result.success) return unwrapResult(result);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Parse request body
|
|
56
|
+
const body = await parseOptionalBody(request, contentPreviewUrlBody, {});
|
|
57
|
+
if (isParseError(body)) return body;
|
|
58
|
+
|
|
59
|
+
const expiresIn = body.expiresIn || "1h";
|
|
60
|
+
const pathPattern = body.pathPattern;
|
|
61
|
+
|
|
62
|
+
// Calculate expiry timestamp
|
|
63
|
+
const expiresInSeconds = typeof expiresIn === "number" ? expiresIn : parseExpiresIn(expiresIn);
|
|
64
|
+
const expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const url = await getPreviewUrl({
|
|
68
|
+
collection,
|
|
69
|
+
id,
|
|
70
|
+
secret: previewSecret,
|
|
71
|
+
expiresIn,
|
|
72
|
+
pathPattern,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return apiSuccess({ url, expiresAt });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return handleError(error, "Failed to generate preview URL", "TOKEN_ERROR");
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parse duration string to seconds
|
|
83
|
+
*/
|
|
84
|
+
function parseExpiresIn(duration: string): number {
|
|
85
|
+
const match = duration.match(DURATION_PATTERN);
|
|
86
|
+
if (!match) {
|
|
87
|
+
return 3600; // Default 1 hour
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const value = parseInt(match[1], 10);
|
|
91
|
+
const unit = match[2];
|
|
92
|
+
|
|
93
|
+
switch (unit) {
|
|
94
|
+
case "s":
|
|
95
|
+
return value;
|
|
96
|
+
case "m":
|
|
97
|
+
return value * 60;
|
|
98
|
+
case "h":
|
|
99
|
+
return value * 60 * 60;
|
|
100
|
+
case "d":
|
|
101
|
+
return value * 60 * 60 * 24;
|
|
102
|
+
case "w":
|
|
103
|
+
return value * 60 * 60 * 24 * 7;
|
|
104
|
+
default:
|
|
105
|
+
return 3600;
|
|
106
|
+
}
|
|
107
|
+
}
|