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,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media upload URL endpoint
|
|
3
|
+
*
|
|
4
|
+
* POST /_dineway/api/media/upload-url
|
|
5
|
+
*
|
|
6
|
+
* Returns a signed URL for direct upload to storage.
|
|
7
|
+
* Creates a pending media record that must be confirmed after upload.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
|
|
12
|
+
import type { APIRoute } from "astro";
|
|
13
|
+
import { MediaRepository } from "dineway";
|
|
14
|
+
import { ulid } from "ulidx";
|
|
15
|
+
|
|
16
|
+
import { requirePerm } from "#api/authorize.js";
|
|
17
|
+
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
18
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
19
|
+
import { DEFAULT_MAX_UPLOAD_SIZE, mediaUploadUrlBody } from "#api/schemas.js";
|
|
20
|
+
|
|
21
|
+
export const prerender = false;
|
|
22
|
+
|
|
23
|
+
interface UploadUrlResponse {
|
|
24
|
+
uploadUrl: string;
|
|
25
|
+
method: "PUT";
|
|
26
|
+
headers: Record<string, string>;
|
|
27
|
+
mediaId: string;
|
|
28
|
+
storageKey: string;
|
|
29
|
+
expiresAt: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Response when content already exists (deduplication) */
|
|
33
|
+
interface ExistingMediaResponse {
|
|
34
|
+
existing: true;
|
|
35
|
+
mediaId: string;
|
|
36
|
+
storageKey: string;
|
|
37
|
+
url: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get a signed upload URL for direct-to-storage upload
|
|
42
|
+
*/
|
|
43
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
44
|
+
const { dineway, user } = locals;
|
|
45
|
+
|
|
46
|
+
const denied = requirePerm(user, "media:upload");
|
|
47
|
+
if (denied) return denied;
|
|
48
|
+
|
|
49
|
+
if (!dineway?.storage) {
|
|
50
|
+
return apiError(
|
|
51
|
+
"NO_STORAGE",
|
|
52
|
+
"Storage not configured. Signed URL uploads require S3-compatible storage.",
|
|
53
|
+
501,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!dineway?.db) {
|
|
58
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const maxUploadSize = dineway.config.maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE;
|
|
63
|
+
if (!Number.isFinite(maxUploadSize) || maxUploadSize <= 0) {
|
|
64
|
+
return apiError(
|
|
65
|
+
"CONFIGURATION_ERROR",
|
|
66
|
+
"Invalid maxUploadSize configuration. Expected a positive finite number.",
|
|
67
|
+
500,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const body = await parseBody(request, mediaUploadUrlBody(maxUploadSize));
|
|
72
|
+
if (isParseError(body)) return body;
|
|
73
|
+
|
|
74
|
+
// Validate content type
|
|
75
|
+
const allowedTypes = ["image/", "video/", "audio/", "application/pdf"];
|
|
76
|
+
if (!allowedTypes.some((type) => body.contentType.startsWith(type))) {
|
|
77
|
+
return apiError("INVALID_TYPE", "File type not allowed", 400);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const repo = new MediaRepository(dineway.db);
|
|
81
|
+
|
|
82
|
+
// Check for existing content with same hash (deduplication)
|
|
83
|
+
if (body.contentHash) {
|
|
84
|
+
const existing = await repo.findByContentHash(body.contentHash);
|
|
85
|
+
if (existing) {
|
|
86
|
+
const response: ExistingMediaResponse = {
|
|
87
|
+
existing: true,
|
|
88
|
+
mediaId: existing.id,
|
|
89
|
+
storageKey: existing.storageKey,
|
|
90
|
+
url: `/_dineway/api/media/file/${existing.storageKey}`,
|
|
91
|
+
};
|
|
92
|
+
return apiSuccess(response);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Generate unique storage key
|
|
97
|
+
const id = ulid();
|
|
98
|
+
const ext = path.extname(body.filename) || "";
|
|
99
|
+
const storageKey = `${id}${ext}`;
|
|
100
|
+
|
|
101
|
+
// Create pending media record with content hash
|
|
102
|
+
const mediaItem = await repo.createPending({
|
|
103
|
+
filename: body.filename,
|
|
104
|
+
mimeType: body.contentType,
|
|
105
|
+
size: body.size,
|
|
106
|
+
storageKey,
|
|
107
|
+
contentHash: body.contentHash,
|
|
108
|
+
authorId: user?.id,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Get signed upload URL from storage
|
|
112
|
+
const signedUrl = await dineway.storage.getSignedUploadUrl({
|
|
113
|
+
key: storageKey,
|
|
114
|
+
contentType: body.contentType,
|
|
115
|
+
size: body.size,
|
|
116
|
+
expiresIn: 3600, // 1 hour
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const response: UploadUrlResponse = {
|
|
120
|
+
uploadUrl: signedUrl.url,
|
|
121
|
+
method: signedUrl.method,
|
|
122
|
+
headers: signedUrl.headers,
|
|
123
|
+
mediaId: mediaItem.id,
|
|
124
|
+
storageKey,
|
|
125
|
+
expiresAt: signedUrl.expiresAt,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return apiSuccess(response);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// Check if storage doesn't support signed URLs (e.g., local storage)
|
|
131
|
+
if (
|
|
132
|
+
error instanceof Error &&
|
|
133
|
+
"code" in error &&
|
|
134
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowing error to check custom code property after "code" in error guard
|
|
135
|
+
(error as { code: string }).code === "NOT_SUPPORTED"
|
|
136
|
+
) {
|
|
137
|
+
return apiError(
|
|
138
|
+
"NOT_SUPPORTED",
|
|
139
|
+
"Storage does not support signed upload URLs. Use direct upload.",
|
|
140
|
+
501,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return handleError(error, "Failed to generate upload URL", "UPLOAD_URL_ERROR");
|
|
145
|
+
}
|
|
146
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media list and upload endpoint
|
|
3
|
+
*
|
|
4
|
+
* GET /_dineway/api/media - List all media
|
|
5
|
+
* POST /_dineway/api/media - Upload new media (via configured storage adapter)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
|
|
10
|
+
import type { APIRoute } from "astro";
|
|
11
|
+
import { ulid } from "ulidx";
|
|
12
|
+
|
|
13
|
+
import { requirePerm } from "#api/authorize.js";
|
|
14
|
+
import { apiError, apiSuccess, handleError, unwrapResult } from "#api/error.js";
|
|
15
|
+
import { isParseError, parseQuery } from "#api/parse.js";
|
|
16
|
+
import { DEFAULT_MAX_UPLOAD_SIZE, formatFileSize, mediaListQuery } from "#api/schemas.js";
|
|
17
|
+
import { MediaRepository } from "#db/repositories/media.js";
|
|
18
|
+
import { generatePlaceholder } from "#media/placeholder.js";
|
|
19
|
+
import { computeContentHash } from "#utils/hash.js";
|
|
20
|
+
|
|
21
|
+
import type { MediaItem } from "../../types.js";
|
|
22
|
+
|
|
23
|
+
export const prerender = false;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Add URL to media items
|
|
27
|
+
* Uses relative URLs to ensure portability across deployments
|
|
28
|
+
*/
|
|
29
|
+
function addUrlToMedia(item: MediaItem): MediaItem & { url: string } {
|
|
30
|
+
return {
|
|
31
|
+
...item,
|
|
32
|
+
url: `/_dineway/api/media/file/${item.storageKey}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* List media items
|
|
38
|
+
*/
|
|
39
|
+
export const GET: APIRoute = async ({ request, locals }) => {
|
|
40
|
+
const { dineway, user } = locals;
|
|
41
|
+
|
|
42
|
+
const denied = requirePerm(user, "media:read");
|
|
43
|
+
if (denied) return denied;
|
|
44
|
+
|
|
45
|
+
if (!dineway?.handleMediaList) {
|
|
46
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const url = new URL(request.url);
|
|
50
|
+
const query = parseQuery(url, mediaListQuery);
|
|
51
|
+
if (isParseError(query)) return query;
|
|
52
|
+
|
|
53
|
+
const result = await dineway.handleMediaList({
|
|
54
|
+
cursor: query.cursor,
|
|
55
|
+
limit: query.limit,
|
|
56
|
+
mimeType: query.mimeType,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!result.success) {
|
|
60
|
+
return unwrapResult(result);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Add URL to each media item (relative URLs for portability)
|
|
64
|
+
const itemsWithUrl = result.data.items.map((item) => addUrlToMedia(item));
|
|
65
|
+
|
|
66
|
+
return apiSuccess({ items: itemsWithUrl, nextCursor: result.data.nextCursor });
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Upload media file
|
|
71
|
+
*
|
|
72
|
+
* Uses the configured storage adapter to store the file.
|
|
73
|
+
*/
|
|
74
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
75
|
+
const { dineway, user } = locals;
|
|
76
|
+
|
|
77
|
+
const denied = requirePerm(user, "media:upload");
|
|
78
|
+
if (denied) return denied;
|
|
79
|
+
|
|
80
|
+
if (!dineway?.handleMediaCreate) {
|
|
81
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!dineway?.storage) {
|
|
85
|
+
return apiError("NO_STORAGE", "Storage not configured", 500);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const maxUploadSize = dineway.config.maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE;
|
|
90
|
+
if (!Number.isFinite(maxUploadSize) || maxUploadSize <= 0) {
|
|
91
|
+
return apiError("CONFIGURATION_ERROR", "Invalid maxUploadSize configuration", 500);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Best-effort size check before buffering the full multipart body
|
|
95
|
+
const contentLength = request.headers.get("Content-Length");
|
|
96
|
+
if (contentLength && parseInt(contentLength, 10) > maxUploadSize) {
|
|
97
|
+
return apiError("PAYLOAD_TOO_LARGE", "Upload too large", 413);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const formData = await request.formData();
|
|
101
|
+
const fileEntry = formData.get("file");
|
|
102
|
+
const file = fileEntry instanceof File ? fileEntry : null;
|
|
103
|
+
|
|
104
|
+
if (!file) {
|
|
105
|
+
return apiError("NO_FILE", "No file provided", 400);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Validate file type
|
|
109
|
+
const allowedTypes = ["image/", "video/", "audio/", "application/pdf"];
|
|
110
|
+
if (!allowedTypes.some((type) => file.type.startsWith(type))) {
|
|
111
|
+
return apiError("INVALID_TYPE", "File type not allowed", 400);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check file size before buffering
|
|
115
|
+
if (file.size > maxUploadSize) {
|
|
116
|
+
return apiError(
|
|
117
|
+
"PAYLOAD_TOO_LARGE",
|
|
118
|
+
`File exceeds maximum size of ${formatFileSize(maxUploadSize)}`,
|
|
119
|
+
413,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Get file content and compute hash
|
|
124
|
+
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
125
|
+
const contentHash = await computeContentHash(buffer);
|
|
126
|
+
|
|
127
|
+
// Check for existing media with same content hash (deduplication)
|
|
128
|
+
const repo = new MediaRepository(dineway.db);
|
|
129
|
+
const existing = await repo.findByContentHash(contentHash);
|
|
130
|
+
if (existing) {
|
|
131
|
+
// Same content already exists - return existing item
|
|
132
|
+
const itemWithUrl = addUrlToMedia(existing);
|
|
133
|
+
return apiSuccess({ item: itemWithUrl, deduplicated: true });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Generate unique storage key
|
|
137
|
+
const id = ulid();
|
|
138
|
+
const ext = path.extname(file.name) || "";
|
|
139
|
+
const storageKey = `${id}${ext}`;
|
|
140
|
+
|
|
141
|
+
// Upload to storage using the configured adapter
|
|
142
|
+
await dineway.storage.upload({
|
|
143
|
+
key: storageKey,
|
|
144
|
+
body: buffer,
|
|
145
|
+
contentType: file.type,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Get image dimensions from form data (sent by client)
|
|
149
|
+
const widthEntry = formData.get("width");
|
|
150
|
+
const widthStr = typeof widthEntry === "string" ? widthEntry : null;
|
|
151
|
+
const heightEntry = formData.get("height");
|
|
152
|
+
const heightStr = typeof heightEntry === "string" ? heightEntry : null;
|
|
153
|
+
const width = widthStr ? parseInt(widthStr, 10) : undefined;
|
|
154
|
+
const height = heightStr ? parseInt(heightStr, 10) : undefined;
|
|
155
|
+
|
|
156
|
+
// Generate placeholder data for images.
|
|
157
|
+
// If the client sent a thumbnail (small pre-resized image), use that
|
|
158
|
+
// instead of the full buffer to avoid OOM on memory-constrained runtimes.
|
|
159
|
+
const thumbnailEntry = formData.get("thumbnail");
|
|
160
|
+
const thumbnail = thumbnailEntry instanceof File ? thumbnailEntry : null;
|
|
161
|
+
|
|
162
|
+
let placeholder: Awaited<ReturnType<typeof generatePlaceholder>> = null;
|
|
163
|
+
if (file.type.startsWith("image/")) {
|
|
164
|
+
if (thumbnail) {
|
|
165
|
+
const thumbBuffer = new Uint8Array(await thumbnail.arrayBuffer());
|
|
166
|
+
placeholder = await generatePlaceholder(thumbBuffer, thumbnail.type);
|
|
167
|
+
} else {
|
|
168
|
+
const clientDims = width && height ? { width, height } : undefined;
|
|
169
|
+
placeholder = await generatePlaceholder(buffer, file.type, clientDims);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Create media record
|
|
174
|
+
const result = await dineway.handleMediaCreate({
|
|
175
|
+
filename: file.name,
|
|
176
|
+
mimeType: file.type,
|
|
177
|
+
size: file.size,
|
|
178
|
+
width,
|
|
179
|
+
height,
|
|
180
|
+
storageKey,
|
|
181
|
+
contentHash,
|
|
182
|
+
blurhash: placeholder?.blurhash,
|
|
183
|
+
dominantColor: placeholder?.dominantColor,
|
|
184
|
+
authorId: user?.id,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (!result.success) {
|
|
188
|
+
// Clean up the uploaded file on failure
|
|
189
|
+
try {
|
|
190
|
+
await dineway.storage.delete(storageKey);
|
|
191
|
+
} catch {
|
|
192
|
+
// Ignore cleanup errors
|
|
193
|
+
}
|
|
194
|
+
return unwrapResult(result);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Add URL to the response (relative URL for portability)
|
|
198
|
+
const itemWithUrl = addUrlToMedia(result.data.item);
|
|
199
|
+
|
|
200
|
+
return apiSuccess({ item: itemWithUrl }, 201);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return handleError(error, "Upload failed", "UPLOAD_ERROR");
|
|
203
|
+
}
|
|
204
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Menu items CRUD endpoints
|
|
3
|
+
*
|
|
4
|
+
* POST /_dineway/api/menus/:name/items - Add item
|
|
5
|
+
* PUT /_dineway/api/menus/:name/items/:id - Update item
|
|
6
|
+
* DELETE /_dineway/api/menus/:name/items/:id - Delete item
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { APIRoute } from "astro";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
import { requirePerm } from "#api/authorize.js";
|
|
13
|
+
import { handleError, unwrapResult } from "#api/error.js";
|
|
14
|
+
import {
|
|
15
|
+
handleMenuItemCreate,
|
|
16
|
+
handleMenuItemDelete,
|
|
17
|
+
handleMenuItemUpdate,
|
|
18
|
+
} from "#api/handlers/menus.js";
|
|
19
|
+
import {
|
|
20
|
+
ensureWorkflowHitlRouteRequest,
|
|
21
|
+
hitlRequiredRouteError,
|
|
22
|
+
resolveHitlRouteActor,
|
|
23
|
+
} from "#api/hitl-route-helpers.js";
|
|
24
|
+
import { isParseError, parseBody, parseQuery } from "#api/parse.js";
|
|
25
|
+
import {
|
|
26
|
+
createMenuItemBody,
|
|
27
|
+
menuItemDeleteQuery,
|
|
28
|
+
menuItemUpdateQuery,
|
|
29
|
+
updateMenuItemBody,
|
|
30
|
+
} from "#api/schemas.js";
|
|
31
|
+
import {
|
|
32
|
+
logMenuActivity,
|
|
33
|
+
menuApiRouteSource,
|
|
34
|
+
MenuHitlPayloadBuilder,
|
|
35
|
+
RiskPolicyEvaluator,
|
|
36
|
+
} from "#site-context/index.js";
|
|
37
|
+
|
|
38
|
+
export const prerender = false;
|
|
39
|
+
|
|
40
|
+
const createMenuItemHitlBody = createMenuItemBody.extend({
|
|
41
|
+
hitlRequestId: z.string().min(1).optional(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const updateMenuItemHitlBody = updateMenuItemBody.extend({
|
|
45
|
+
hitlRequestId: z.string().min(1).optional(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const deleteMenuItemHitlQuery = menuItemDeleteQuery.extend({
|
|
49
|
+
hitlRequestId: z.string().min(1).optional(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
53
|
+
const { dineway, user } = locals;
|
|
54
|
+
const name = params.name!;
|
|
55
|
+
|
|
56
|
+
const denied = requirePerm(user, "menus:manage");
|
|
57
|
+
if (denied) return denied;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const body = await parseBody(request, createMenuItemHitlBody);
|
|
61
|
+
if (isParseError(body)) return body;
|
|
62
|
+
|
|
63
|
+
const { hitlRequestId, ...menuInput } = body;
|
|
64
|
+
const actor = resolveHitlRouteActor(locals);
|
|
65
|
+
const action = await new MenuHitlPayloadBuilder(dineway.db).buildCreateMenuItemRequest({
|
|
66
|
+
menuName: name,
|
|
67
|
+
...menuInput,
|
|
68
|
+
});
|
|
69
|
+
const decision = await new RiskPolicyEvaluator({
|
|
70
|
+
db: dineway.db,
|
|
71
|
+
handlers: dineway,
|
|
72
|
+
}).evaluateWorkflowHitl({
|
|
73
|
+
actor: actor.identity,
|
|
74
|
+
hitlRequestId,
|
|
75
|
+
action,
|
|
76
|
+
});
|
|
77
|
+
if (!decision.allowed) {
|
|
78
|
+
const ensured = await ensureWorkflowHitlRouteRequest(dineway.db, locals, decision.action);
|
|
79
|
+
return hitlRequiredRouteError(decision, ensured);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const result = await handleMenuItemCreate(dineway.db, name, menuInput);
|
|
83
|
+
if (!result.success) return unwrapResult(result, 201);
|
|
84
|
+
|
|
85
|
+
await logMenuActivity(dineway.db, locals, {
|
|
86
|
+
action: "item_created",
|
|
87
|
+
menuName: name,
|
|
88
|
+
itemId: result.data.id,
|
|
89
|
+
...menuApiRouteSource("item_created"),
|
|
90
|
+
summary: `Created menu item ${result.data.id} in ${name}`,
|
|
91
|
+
detail: {
|
|
92
|
+
label: result.data.label,
|
|
93
|
+
hitlRequestId: decision.required ? decision.hitlRequest.id : null,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
return unwrapResult(result, 201);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return handleError(error, "Failed to create menu item", "MENU_ITEM_CREATE_ERROR");
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const PUT: APIRoute = async ({ params, request, locals }) => {
|
|
103
|
+
const { dineway, user } = locals;
|
|
104
|
+
const name = params.name!;
|
|
105
|
+
|
|
106
|
+
const denied = requirePerm(user, "menus:manage");
|
|
107
|
+
if (denied) return denied;
|
|
108
|
+
|
|
109
|
+
const url = new URL(request.url);
|
|
110
|
+
const query = parseQuery(url, menuItemUpdateQuery);
|
|
111
|
+
if (isParseError(query)) return query;
|
|
112
|
+
const itemId = query.id;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const body = await parseBody(request, updateMenuItemHitlBody);
|
|
116
|
+
if (isParseError(body)) return body;
|
|
117
|
+
|
|
118
|
+
const { hitlRequestId, ...menuInput } = body;
|
|
119
|
+
const actor = resolveHitlRouteActor(locals);
|
|
120
|
+
const action = await new MenuHitlPayloadBuilder(dineway.db).buildUpdateMenuItemRequest({
|
|
121
|
+
menuName: name,
|
|
122
|
+
itemId,
|
|
123
|
+
...menuInput,
|
|
124
|
+
});
|
|
125
|
+
const decision = await new RiskPolicyEvaluator({
|
|
126
|
+
db: dineway.db,
|
|
127
|
+
handlers: dineway,
|
|
128
|
+
}).evaluateWorkflowHitl({
|
|
129
|
+
actor: actor.identity,
|
|
130
|
+
hitlRequestId,
|
|
131
|
+
action,
|
|
132
|
+
});
|
|
133
|
+
if (!decision.allowed) {
|
|
134
|
+
const ensured = await ensureWorkflowHitlRouteRequest(dineway.db, locals, decision.action);
|
|
135
|
+
return hitlRequiredRouteError(decision, ensured);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const result = await handleMenuItemUpdate(dineway.db, name, itemId, menuInput);
|
|
139
|
+
if (!result.success) return unwrapResult(result);
|
|
140
|
+
|
|
141
|
+
await logMenuActivity(dineway.db, locals, {
|
|
142
|
+
action: "item_updated",
|
|
143
|
+
menuName: name,
|
|
144
|
+
itemId,
|
|
145
|
+
...menuApiRouteSource("item_updated"),
|
|
146
|
+
summary: `Updated menu item ${itemId} in ${name}`,
|
|
147
|
+
detail: {
|
|
148
|
+
label: result.data.label,
|
|
149
|
+
hitlRequestId: decision.required ? decision.hitlRequest.id : null,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
return unwrapResult(result);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return handleError(error, "Failed to update menu item", "MENU_ITEM_UPDATE_ERROR");
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const DELETE: APIRoute = async ({ params, request, locals }) => {
|
|
159
|
+
const { dineway, user } = locals;
|
|
160
|
+
const name = params.name!;
|
|
161
|
+
|
|
162
|
+
const denied = requirePerm(user, "menus:manage");
|
|
163
|
+
if (denied) return denied;
|
|
164
|
+
|
|
165
|
+
const url = new URL(request.url);
|
|
166
|
+
const query = parseQuery(url, deleteMenuItemHitlQuery);
|
|
167
|
+
if (isParseError(query)) return query;
|
|
168
|
+
const itemId = query.id;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const actor = resolveHitlRouteActor(locals);
|
|
172
|
+
const action = await new MenuHitlPayloadBuilder(dineway.db).buildDeleteMenuItemRequest({
|
|
173
|
+
menuName: name,
|
|
174
|
+
itemId,
|
|
175
|
+
});
|
|
176
|
+
const decision = await new RiskPolicyEvaluator({
|
|
177
|
+
db: dineway.db,
|
|
178
|
+
handlers: dineway,
|
|
179
|
+
}).evaluateWorkflowHitl({
|
|
180
|
+
actor: actor.identity,
|
|
181
|
+
hitlRequestId: query.hitlRequestId,
|
|
182
|
+
action,
|
|
183
|
+
});
|
|
184
|
+
if (!decision.allowed) {
|
|
185
|
+
const ensured = await ensureWorkflowHitlRouteRequest(dineway.db, locals, decision.action);
|
|
186
|
+
return hitlRequiredRouteError(decision, ensured);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const result = await handleMenuItemDelete(dineway.db, name, itemId);
|
|
190
|
+
if (!result.success) return unwrapResult(result);
|
|
191
|
+
|
|
192
|
+
await logMenuActivity(dineway.db, locals, {
|
|
193
|
+
action: "item_deleted",
|
|
194
|
+
menuName: name,
|
|
195
|
+
itemId,
|
|
196
|
+
...menuApiRouteSource("item_deleted"),
|
|
197
|
+
summary: `Deleted menu item ${itemId} from ${name}`,
|
|
198
|
+
detail: {
|
|
199
|
+
hitlRequestId: decision.required ? decision.hitlRequest.id : null,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
return unwrapResult(result);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return handleError(error, "Failed to delete menu item", "MENU_ITEM_DELETE_ERROR");
|
|
205
|
+
}
|
|
206
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Menu items reorder endpoint
|
|
3
|
+
*
|
|
4
|
+
* POST /_dineway/api/menus/:name/reorder - Batch update positions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
import { requirePerm } from "#api/authorize.js";
|
|
11
|
+
import { handleError, unwrapResult } from "#api/error.js";
|
|
12
|
+
import { handleMenuItemReorder } from "#api/handlers/menus.js";
|
|
13
|
+
import {
|
|
14
|
+
ensureWorkflowHitlRouteRequest,
|
|
15
|
+
hitlRequiredRouteError,
|
|
16
|
+
resolveHitlRouteActor,
|
|
17
|
+
} from "#api/hitl-route-helpers.js";
|
|
18
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
19
|
+
import { reorderMenuItemsBody } from "#api/schemas.js";
|
|
20
|
+
import {
|
|
21
|
+
logMenuActivity,
|
|
22
|
+
menuApiRouteSource,
|
|
23
|
+
MenuHitlPayloadBuilder,
|
|
24
|
+
RiskPolicyEvaluator,
|
|
25
|
+
} from "#site-context/index.js";
|
|
26
|
+
|
|
27
|
+
export const prerender = false;
|
|
28
|
+
|
|
29
|
+
const reorderMenuItemsHitlBody = reorderMenuItemsBody.extend({
|
|
30
|
+
hitlRequestId: z.string().min(1).optional(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
34
|
+
const { dineway, user } = locals;
|
|
35
|
+
const name = params.name!;
|
|
36
|
+
|
|
37
|
+
const denied = requirePerm(user, "menus:manage");
|
|
38
|
+
if (denied) return denied;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const body = await parseBody(request, reorderMenuItemsHitlBody);
|
|
42
|
+
if (isParseError(body)) return body;
|
|
43
|
+
|
|
44
|
+
const actor = resolveHitlRouteActor(locals);
|
|
45
|
+
const action = await new MenuHitlPayloadBuilder(dineway.db).buildReorderMenuItemsRequest({
|
|
46
|
+
menuName: name,
|
|
47
|
+
items: body.items,
|
|
48
|
+
});
|
|
49
|
+
const decision = await new RiskPolicyEvaluator({
|
|
50
|
+
db: dineway.db,
|
|
51
|
+
handlers: dineway,
|
|
52
|
+
}).evaluateWorkflowHitl({
|
|
53
|
+
actor: actor.identity,
|
|
54
|
+
hitlRequestId: body.hitlRequestId,
|
|
55
|
+
action,
|
|
56
|
+
});
|
|
57
|
+
if (!decision.allowed) {
|
|
58
|
+
const ensured = await ensureWorkflowHitlRouteRequest(dineway.db, locals, decision.action);
|
|
59
|
+
return hitlRequiredRouteError(decision, ensured);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = await handleMenuItemReorder(dineway.db, name, body.items);
|
|
63
|
+
if (!result.success) return unwrapResult(result);
|
|
64
|
+
|
|
65
|
+
await logMenuActivity(dineway.db, locals, {
|
|
66
|
+
action: "reordered",
|
|
67
|
+
menuName: name,
|
|
68
|
+
...menuApiRouteSource("reordered"),
|
|
69
|
+
summary: `Reordered menu ${name}`,
|
|
70
|
+
detail: {
|
|
71
|
+
itemCount: body.items.length,
|
|
72
|
+
hitlRequestId: decision.required ? decision.hitlRequest.id : null,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
return unwrapResult(result);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return handleError(error, "Failed to reorder menu items", "MENU_REORDER_ERROR");
|
|
78
|
+
}
|
|
79
|
+
};
|