dineway 0.1.3 → 0.1.4
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/package.json +6 -3
- package/src/astro/routes/PluginRegistry.tsx +21 -0
- package/src/astro/routes/admin.astro +83 -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 +40 -0
- package/src/astro/routes/api/admin/api-tokens/index.ts +68 -0
- package/src/astro/routes/api/admin/bylines/[id]/index.ts +87 -0
- package/src/astro/routes/api/admin/bylines/index.ts +72 -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/hooks/exclusive/[hookName].ts +91 -0
- package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
- package/src/astro/routes/api/admin/oauth-clients/[id].ts +110 -0
- package/src/astro/routes/api/admin/oauth-clients/index.ts +71 -0
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +39 -0
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +39 -0
- package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +48 -0
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +59 -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 +64 -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/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 +69 -0
- package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
- package/src/astro/routes/api/admin/users/[id]/index.ts +146 -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/logout.ts +40 -0
- package/src/astro/routes/api/auth/magic-link/send.ts +89 -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 +84 -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 +68 -0
- package/src/astro/routes/api/auth/signup/complete.ts +87 -0
- package/src/astro/routes/api/auth/signup/request.ts +77 -0
- package/src/astro/routes/api/auth/signup/verify.ts +53 -0
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +311 -0
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +54 -0
- package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +61 -0
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +33 -0
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +56 -0
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +54 -0
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
- package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +105 -0
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +140 -0
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +30 -0
- package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +56 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +137 -0
- package/src/astro/routes/api/content/[collection]/index.ts +59 -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/import/probe.ts +47 -0
- package/src/astro/routes/api/import/wordpress/analyze.ts +531 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +296 -0
- package/src/astro/routes/api/import/wordpress/media.ts +338 -0
- package/src/astro/routes/api/import/wordpress/prepare.ts +181 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +393 -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 +357 -0
- package/src/astro/routes/api/manifest.ts +63 -0
- package/src/astro/routes/api/mcp.ts +124 -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 +86 -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 +137 -0
- package/src/astro/routes/api/media.ts +202 -0
- package/src/astro/routes/api/menus/[name]/items.ts +87 -0
- package/src/astro/routes/api/menus/[name]/reorder.ts +33 -0
- package/src/astro/routes/api/menus/[name].ts +65 -0
- package/src/astro/routes/api/menus/index.ts +47 -0
- package/src/astro/routes/api/oauth/authorize.ts +417 -0
- package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
- package/src/astro/routes/api/oauth/device/code.ts +55 -0
- package/src/astro/routes/api/oauth/device/token.ts +69 -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 +184 -0
- package/src/astro/routes/api/openapi.json.ts +32 -0
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +92 -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 +84 -0
- package/src/astro/routes/api/redirects/index.ts +52 -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 +76 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +52 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +32 -0
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +80 -0
- package/src/astro/routes/api/schema/collections/index.ts +47 -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 +51 -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 +49 -0
- package/src/astro/routes/api/sections/[slug].ts +84 -0
- package/src/astro/routes/api/sections/index.ts +52 -0
- package/src/astro/routes/api/settings/email.ts +150 -0
- package/src/astro/routes/api/settings.ts +67 -0
- package/src/astro/routes/api/setup/admin-verify.ts +102 -0
- package/src/astro/routes/api/setup/admin.ts +96 -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 +127 -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 +95 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +69 -0
- package/src/astro/routes/api/taxonomies/index.ts +59 -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 +69 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +45 -0
- package/src/astro/routes/api/well-known/oauth-protected-resource.ts +38 -0
- package/src/astro/routes/api/widget-areas/[name]/reorder.ts +72 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +127 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +80 -0
- package/src/astro/routes/api/widget-areas/[name].ts +87 -0
- package/src/astro/routes/api/widget-areas/index.ts +99 -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 +53 -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 +108 -0
- package/src/components/WidgetArea.astro +22 -0
- package/src/components/WidgetRenderer.astro +72 -0
- package/src/components/index.ts +116 -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/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
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taxonomy terms list and create endpoint
|
|
3
|
+
*
|
|
4
|
+
* GET /_dineway/api/taxonomies/:name/terms - List all terms (tree for hierarchical)
|
|
5
|
+
* POST /_dineway/api/taxonomies/:name/terms - Create a new term
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { APIRoute } from "astro";
|
|
9
|
+
|
|
10
|
+
import { requirePerm } from "#api/authorize.js";
|
|
11
|
+
import { apiError, handleError, requireDb, unwrapResult } from "#api/error.js";
|
|
12
|
+
import { handleTermCreate, handleTermList } from "#api/handlers/taxonomies.js";
|
|
13
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
14
|
+
import { createTermBody } from "#api/schemas.js";
|
|
15
|
+
|
|
16
|
+
export const prerender = false;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* List all terms for a taxonomy
|
|
20
|
+
*/
|
|
21
|
+
export const GET: APIRoute = async ({ params, locals }) => {
|
|
22
|
+
const { dineway, user } = locals;
|
|
23
|
+
const { name } = params;
|
|
24
|
+
|
|
25
|
+
if (!name) {
|
|
26
|
+
return apiError("VALIDATION_ERROR", "Taxonomy name required", 400);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const dbErr = requireDb(dineway?.db);
|
|
30
|
+
if (dbErr) return dbErr;
|
|
31
|
+
|
|
32
|
+
const denied = requirePerm(user, "taxonomies:read");
|
|
33
|
+
if (denied) return denied;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const result = await handleTermList(dineway.db, name);
|
|
37
|
+
return unwrapResult(result);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return handleError(error, "Failed to list terms", "TERM_LIST_ERROR");
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a new term
|
|
45
|
+
*/
|
|
46
|
+
export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
47
|
+
const { dineway, user } = locals;
|
|
48
|
+
const { name } = params;
|
|
49
|
+
|
|
50
|
+
if (!name) {
|
|
51
|
+
return apiError("VALIDATION_ERROR", "Taxonomy name required", 400);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const dbErr = requireDb(dineway?.db);
|
|
55
|
+
if (dbErr) return dbErr;
|
|
56
|
+
|
|
57
|
+
const denied = requirePerm(user, "taxonomies:manage");
|
|
58
|
+
if (denied) return denied;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const body = await parseBody(request, createTermBody);
|
|
62
|
+
if (isParseError(body)) return body;
|
|
63
|
+
|
|
64
|
+
const result = await handleTermCreate(dineway.db, name, body);
|
|
65
|
+
return unwrapResult(result, 201);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return handleError(error, "Failed to create term", "TERM_CREATE_ERROR");
|
|
68
|
+
}
|
|
69
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taxonomy definitions endpoint
|
|
3
|
+
*
|
|
4
|
+
* GET /_dineway/api/taxonomies - List all taxonomy definitions
|
|
5
|
+
* POST /_dineway/api/taxonomies - Create a custom taxonomy definition
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { APIRoute } from "astro";
|
|
9
|
+
|
|
10
|
+
import { requirePerm } from "#api/authorize.js";
|
|
11
|
+
import { handleError, requireDb, unwrapResult } from "#api/error.js";
|
|
12
|
+
import { handleTaxonomyCreate, handleTaxonomyList } from "#api/handlers/taxonomies.js";
|
|
13
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
14
|
+
import { createTaxonomyDefBody } from "#api/schemas.js";
|
|
15
|
+
|
|
16
|
+
export const prerender = false;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* List taxonomy definitions
|
|
20
|
+
*/
|
|
21
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
22
|
+
const { dineway, user } = locals;
|
|
23
|
+
|
|
24
|
+
const dbErr = requireDb(dineway?.db);
|
|
25
|
+
if (dbErr) return dbErr;
|
|
26
|
+
|
|
27
|
+
const denied = requirePerm(user, "taxonomies:read");
|
|
28
|
+
if (denied) return denied;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const result = await handleTaxonomyList(dineway.db);
|
|
32
|
+
return unwrapResult(result);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return handleError(error, "Failed to list taxonomies", "TAXONOMY_LIST_ERROR");
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a custom taxonomy definition
|
|
40
|
+
*/
|
|
41
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
42
|
+
const { dineway, user } = locals;
|
|
43
|
+
|
|
44
|
+
const dbErr = requireDb(dineway?.db);
|
|
45
|
+
if (dbErr) return dbErr;
|
|
46
|
+
|
|
47
|
+
const denied = requirePerm(user, "taxonomies:manage");
|
|
48
|
+
if (denied) return denied;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const body = await parseBody(request, createTaxonomyDefBody);
|
|
52
|
+
if (isParseError(body)) return body;
|
|
53
|
+
|
|
54
|
+
const result = await handleTaxonomyCreate(dineway.db, body);
|
|
55
|
+
return unwrapResult(result, 201);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return handleError(error, "Failed to create taxonomy", "TAXONOMY_CREATE_ERROR");
|
|
58
|
+
}
|
|
59
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme preview signing endpoint
|
|
3
|
+
*
|
|
4
|
+
* POST /_dineway/api/themes/preview
|
|
5
|
+
*
|
|
6
|
+
* Generates a signed preview URL for the "Try with my data" feature.
|
|
7
|
+
* The PREVIEW_SECRET must be set in the environment (shared with the preview sidecar).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { APIRoute } from "astro";
|
|
11
|
+
|
|
12
|
+
import { requirePerm } from "#api/authorize.js";
|
|
13
|
+
import { apiError, apiSuccess } from "#api/error.js";
|
|
14
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
15
|
+
|
|
16
|
+
export const prerender = false;
|
|
17
|
+
|
|
18
|
+
export const POST: APIRoute = async ({ request, url, locals }) => {
|
|
19
|
+
const { dineway, user } = locals;
|
|
20
|
+
|
|
21
|
+
if (!dineway?.db) {
|
|
22
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const denied = requirePerm(user, "plugins:read");
|
|
26
|
+
if (denied) return denied;
|
|
27
|
+
|
|
28
|
+
const secret = import.meta.env.DINEWAY_PREVIEW_SECRET || import.meta.env.PREVIEW_SECRET || "";
|
|
29
|
+
if (!secret) {
|
|
30
|
+
return apiError("NOT_CONFIGURED", "PREVIEW_SECRET is not configured", 500);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let body: { previewUrl: string };
|
|
34
|
+
try {
|
|
35
|
+
body = await request.json();
|
|
36
|
+
} catch {
|
|
37
|
+
return apiError("INVALID_REQUEST", "Invalid JSON body", 400);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!body.previewUrl || typeof body.previewUrl !== "string") {
|
|
41
|
+
return apiError("INVALID_REQUEST", "previewUrl is required", 400);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Validate previewUrl is a valid HTTPS URL
|
|
45
|
+
let parsedPreviewUrl: URL;
|
|
46
|
+
try {
|
|
47
|
+
parsedPreviewUrl = new URL(body.previewUrl);
|
|
48
|
+
} catch {
|
|
49
|
+
return apiError("INVALID_REQUEST", "previewUrl must be a valid URL", 400);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (parsedPreviewUrl.protocol !== "https:") {
|
|
53
|
+
return apiError("INVALID_REQUEST", "previewUrl must use HTTPS", 400);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const source = getPublicOrigin(url, dineway?.config);
|
|
57
|
+
const ttl = 3600; // 1 hour
|
|
58
|
+
const exp = Math.floor(Date.now() / 1000) + ttl;
|
|
59
|
+
|
|
60
|
+
// HMAC-SHA256 sign: message = "source:exp"
|
|
61
|
+
const encoder = new TextEncoder();
|
|
62
|
+
const key = await crypto.subtle.importKey(
|
|
63
|
+
"raw",
|
|
64
|
+
encoder.encode(secret),
|
|
65
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
66
|
+
false,
|
|
67
|
+
["sign"],
|
|
68
|
+
);
|
|
69
|
+
const buffer = await crypto.subtle.sign("HMAC", key, encoder.encode(`${source}:${exp}`));
|
|
70
|
+
const sig = Array.from(new Uint8Array(buffer), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
71
|
+
|
|
72
|
+
const previewUrl = new URL(body.previewUrl);
|
|
73
|
+
previewUrl.searchParams.set("source", source);
|
|
74
|
+
previewUrl.searchParams.set("exp", String(exp));
|
|
75
|
+
previewUrl.searchParams.set("sig", sig);
|
|
76
|
+
|
|
77
|
+
return apiSuccess({ url: previewUrl.toString() });
|
|
78
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typegen endpoint - generates dineway-env.d.ts content
|
|
3
|
+
*
|
|
4
|
+
* POST /_dineway/api/typegen - Generate types and return as JSON
|
|
5
|
+
* GET /_dineway/api/typegen - Return types as text (for preview/debugging)
|
|
6
|
+
*
|
|
7
|
+
* The caller (integration or CLI) is responsible for writing the file to disk.
|
|
8
|
+
* This endpoint only generates the content — it has no filesystem access, so it
|
|
9
|
+
* stays portable across runtimes where the project root or Node filesystem APIs
|
|
10
|
+
* are not available here.
|
|
11
|
+
*
|
|
12
|
+
* Dev-only endpoint - disabled in production.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { APIRoute } from "astro";
|
|
16
|
+
|
|
17
|
+
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
18
|
+
import type { SchemaRegistry } from "#schema/registry.js";
|
|
19
|
+
|
|
20
|
+
export const prerender = false;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Safely list collections, returning empty array if tables don't exist yet
|
|
24
|
+
*/
|
|
25
|
+
async function safeListCollections(registry: SchemaRegistry) {
|
|
26
|
+
try {
|
|
27
|
+
return await registry.listCollections();
|
|
28
|
+
} catch (error) {
|
|
29
|
+
// Handle missing tables for new sites that haven't run setup yet
|
|
30
|
+
if (error instanceof Error && error.message.includes("no such table")) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate types content and metadata from the current schema.
|
|
39
|
+
*/
|
|
40
|
+
async function generateTypes(registry: SchemaRegistry) {
|
|
41
|
+
const { generateTypesFile, generateSchemaHash } = await import("#schema/zod-generator.js");
|
|
42
|
+
|
|
43
|
+
const collections = await safeListCollections(registry);
|
|
44
|
+
const collectionsWithFields = await Promise.all(
|
|
45
|
+
collections.map(async (c) => {
|
|
46
|
+
const fields = await registry.listFields(c.id);
|
|
47
|
+
return { ...c, fields };
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const types = generateTypesFile(collectionsWithFields);
|
|
52
|
+
const hash: string = await generateSchemaHash(collectionsWithFields);
|
|
53
|
+
|
|
54
|
+
return { types, hash, collections: collections.length };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* GET - Return types as plain text (for preview/debugging)
|
|
59
|
+
*/
|
|
60
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
61
|
+
if (!import.meta.env.DEV) {
|
|
62
|
+
return apiError("FORBIDDEN", "Typegen is only available in development", 403);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { dineway } = locals;
|
|
66
|
+
|
|
67
|
+
if (!dineway?.db) {
|
|
68
|
+
return apiError("NOT_CONFIGURED", "Dineway is not configured", 500);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const { SchemaRegistry } = await import("#schema/registry.js");
|
|
73
|
+
const registry = new SchemaRegistry(dineway.db);
|
|
74
|
+
const { types } = await generateTypes(registry);
|
|
75
|
+
|
|
76
|
+
return new Response(types, {
|
|
77
|
+
status: 200,
|
|
78
|
+
headers: {
|
|
79
|
+
"Content-Type": "text/typescript",
|
|
80
|
+
"Cache-Control": "private, no-store",
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return handleError(error, "Typegen failed", "TYPEGEN_ERROR");
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* POST - Generate types and return as JSON
|
|
90
|
+
*
|
|
91
|
+
* The caller writes the file to disk. Response shape:
|
|
92
|
+
* { types: string, hash: string, collections: number }
|
|
93
|
+
*/
|
|
94
|
+
export const POST: APIRoute = async ({ locals }) => {
|
|
95
|
+
if (!import.meta.env.DEV) {
|
|
96
|
+
return apiError("FORBIDDEN", "Typegen is only available in development", 403);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const { dineway } = locals;
|
|
100
|
+
|
|
101
|
+
if (!dineway?.db) {
|
|
102
|
+
return apiError("NOT_CONFIGURED", "Dineway is not configured", 500);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const { SchemaRegistry } = await import("#schema/registry.js");
|
|
107
|
+
const registry = new SchemaRegistry(dineway.db);
|
|
108
|
+
const result = await generateTypes(registry);
|
|
109
|
+
|
|
110
|
+
return apiSuccess(result);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return handleError(error, "Typegen failed", "TYPEGEN_ERROR");
|
|
113
|
+
}
|
|
114
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /_dineway/.well-known/auth
|
|
3
|
+
*
|
|
4
|
+
* Auth discovery endpoint. Returns available auth mechanisms.
|
|
5
|
+
* Public, unauthenticated. Used by CLI to determine how to authenticate.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { APIRoute } from "astro";
|
|
9
|
+
|
|
10
|
+
import { getAuthMode } from "#auth/mode.js";
|
|
11
|
+
import { OptionsRepository } from "#db/repositories/options.js";
|
|
12
|
+
|
|
13
|
+
export const prerender = false;
|
|
14
|
+
|
|
15
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
16
|
+
const { dineway } = locals;
|
|
17
|
+
|
|
18
|
+
// Build discovery response
|
|
19
|
+
const config = dineway?.config;
|
|
20
|
+
const authMode = config ? getAuthMode(config) : null;
|
|
21
|
+
|
|
22
|
+
const isExternal = authMode?.type === "external";
|
|
23
|
+
|
|
24
|
+
// Try to read site name from DB options
|
|
25
|
+
let siteName = "Dineway";
|
|
26
|
+
if (dineway?.db) {
|
|
27
|
+
try {
|
|
28
|
+
const options = new OptionsRepository(dineway.db);
|
|
29
|
+
siteName = (await options.get<string>("dineway:site_title")) || "Dineway";
|
|
30
|
+
} catch {
|
|
31
|
+
// DB may not be initialized yet
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const response: Record<string, unknown> = {
|
|
36
|
+
instance: {
|
|
37
|
+
name: siteName,
|
|
38
|
+
version: "0.1.0",
|
|
39
|
+
},
|
|
40
|
+
auth: {
|
|
41
|
+
mode: isExternal ? "external" : "passkey",
|
|
42
|
+
...(isExternal && authMode.type === "external"
|
|
43
|
+
? { external_provider: authMode.entrypoint }
|
|
44
|
+
: {}),
|
|
45
|
+
methods: {
|
|
46
|
+
device_flow: !isExternal
|
|
47
|
+
? {
|
|
48
|
+
client_id: "dineway-cli",
|
|
49
|
+
device_authorization_endpoint: "/_dineway/api/oauth/device/code",
|
|
50
|
+
token_endpoint: "/_dineway/api/oauth/device/token",
|
|
51
|
+
}
|
|
52
|
+
: undefined,
|
|
53
|
+
authorization_code: !isExternal
|
|
54
|
+
? {
|
|
55
|
+
authorization_endpoint: "/_dineway/oauth/authorize",
|
|
56
|
+
token_endpoint: "/_dineway/api/oauth/token",
|
|
57
|
+
}
|
|
58
|
+
: undefined,
|
|
59
|
+
api_tokens: true,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return Response.json(response, {
|
|
65
|
+
headers: {
|
|
66
|
+
"Cache-Control": "no-store",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /_dineway/.well-known/oauth-authorization-server
|
|
3
|
+
*
|
|
4
|
+
* RFC 8414 Authorization Server Metadata. Tells MCP clients which
|
|
5
|
+
* endpoints to use for OAuth authorization, token exchange, etc.
|
|
6
|
+
*
|
|
7
|
+
* Public, unauthenticated.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { APIRoute } from "astro";
|
|
11
|
+
|
|
12
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
13
|
+
import { VALID_SCOPES } from "#auth/api-tokens.js";
|
|
14
|
+
|
|
15
|
+
export const prerender = false;
|
|
16
|
+
|
|
17
|
+
export const GET: APIRoute = async ({ url, locals }) => {
|
|
18
|
+
const origin = getPublicOrigin(url, locals.dineway?.config);
|
|
19
|
+
const issuer = `${origin}/_dineway`;
|
|
20
|
+
|
|
21
|
+
return Response.json(
|
|
22
|
+
{
|
|
23
|
+
issuer,
|
|
24
|
+
authorization_endpoint: `${origin}/_dineway/oauth/authorize`,
|
|
25
|
+
token_endpoint: `${origin}/_dineway/api/oauth/token`,
|
|
26
|
+
scopes_supported: [...VALID_SCOPES],
|
|
27
|
+
response_types_supported: ["code"],
|
|
28
|
+
grant_types_supported: [
|
|
29
|
+
"authorization_code",
|
|
30
|
+
"refresh_token",
|
|
31
|
+
"urn:ietf:params:oauth:grant-type:device_code",
|
|
32
|
+
],
|
|
33
|
+
code_challenge_methods_supported: ["S256"],
|
|
34
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
35
|
+
client_id_metadata_document_supported: true,
|
|
36
|
+
device_authorization_endpoint: `${origin}/_dineway/api/oauth/device/code`,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
headers: {
|
|
40
|
+
"Cache-Control": "public, max-age=3600",
|
|
41
|
+
"Access-Control-Allow-Origin": "*",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /.well-known/oauth-protected-resource
|
|
3
|
+
*
|
|
4
|
+
* RFC 9728 Protected Resource Metadata. Tells MCP clients where to find
|
|
5
|
+
* the authorization server. Injected at the site root (not under /_dineway/)
|
|
6
|
+
* because RFC 9728 requires it at the well-known URI of the resource's origin.
|
|
7
|
+
*
|
|
8
|
+
* Also serves as `/.well-known/oauth-protected-resource/_dineway/api/mcp`
|
|
9
|
+
* (path-scoped variant) when Astro's routing allows.
|
|
10
|
+
*
|
|
11
|
+
* Public, unauthenticated.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { APIRoute } from "astro";
|
|
15
|
+
|
|
16
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
17
|
+
import { VALID_SCOPES } from "#auth/api-tokens.js";
|
|
18
|
+
|
|
19
|
+
export const prerender = false;
|
|
20
|
+
|
|
21
|
+
export const GET: APIRoute = async ({ url, locals }) => {
|
|
22
|
+
const origin = getPublicOrigin(url, locals.dineway?.config);
|
|
23
|
+
|
|
24
|
+
return Response.json(
|
|
25
|
+
{
|
|
26
|
+
resource: `${origin}/_dineway/api/mcp`,
|
|
27
|
+
authorization_servers: [`${origin}/_dineway`],
|
|
28
|
+
scopes_supported: [...VALID_SCOPES],
|
|
29
|
+
bearer_methods_supported: ["header"],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
headers: {
|
|
33
|
+
"Cache-Control": "public, max-age=3600",
|
|
34
|
+
"Access-Control-Allow-Origin": "*",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reorder widgets endpoint
|
|
3
|
+
*
|
|
4
|
+
* POST /_dineway/api/widget-areas/:name/reorder
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
|
|
9
|
+
import { requirePerm } from "#api/authorize.js";
|
|
10
|
+
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
11
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
12
|
+
import { reorderWidgetsBody } from "#api/schemas.js";
|
|
13
|
+
|
|
14
|
+
export const prerender = false;
|
|
15
|
+
|
|
16
|
+
export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
17
|
+
const { dineway, user } = locals;
|
|
18
|
+
const db = dineway.db;
|
|
19
|
+
const { name } = params;
|
|
20
|
+
|
|
21
|
+
const denied = requirePerm(user, "widgets:manage");
|
|
22
|
+
if (denied) return denied;
|
|
23
|
+
|
|
24
|
+
if (!name) {
|
|
25
|
+
return apiError("VALIDATION_ERROR", "name is required", 400);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
// Get the area
|
|
30
|
+
const area = await db
|
|
31
|
+
.selectFrom("_dineway_widget_areas")
|
|
32
|
+
.select("id")
|
|
33
|
+
.where("name", "=", name)
|
|
34
|
+
.executeTakeFirst();
|
|
35
|
+
|
|
36
|
+
if (!area) {
|
|
37
|
+
return apiError("NOT_FOUND", `Widget area "${name}" not found`, 404);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const body = await parseBody(request, reorderWidgetsBody);
|
|
41
|
+
if (isParseError(body)) return body;
|
|
42
|
+
|
|
43
|
+
// Verify all widget IDs belong to this area
|
|
44
|
+
const existingWidgets = await db
|
|
45
|
+
.selectFrom("_dineway_widgets")
|
|
46
|
+
.select("id")
|
|
47
|
+
.where("area_id", "=", area.id)
|
|
48
|
+
.execute();
|
|
49
|
+
|
|
50
|
+
const existingIds = new Set(existingWidgets.map((w) => w.id));
|
|
51
|
+
for (const id of body.widgetIds) {
|
|
52
|
+
if (!existingIds.has(id)) {
|
|
53
|
+
return apiError("VALIDATION_ERROR", `Widget "${id}" not found in area "${name}"`, 400);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Update sort_order for each widget
|
|
58
|
+
await Promise.all(
|
|
59
|
+
body.widgetIds.map((id, index) =>
|
|
60
|
+
db
|
|
61
|
+
.updateTable("_dineway_widgets")
|
|
62
|
+
.set({ sort_order: index })
|
|
63
|
+
.where("id", "=", id)
|
|
64
|
+
.execute(),
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return apiSuccess({ success: true });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return handleError(error, "Failed to reorder widgets", "WIDGET_REORDER_ERROR");
|
|
71
|
+
}
|
|
72
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single widget endpoints
|
|
3
|
+
*
|
|
4
|
+
* PUT /_dineway/api/widget-areas/:name/widgets/:id - Update widget
|
|
5
|
+
* DELETE /_dineway/api/widget-areas/:name/widgets/:id - Delete widget
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { APIRoute } from "astro";
|
|
9
|
+
|
|
10
|
+
import { requirePerm } from "#api/authorize.js";
|
|
11
|
+
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
12
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
13
|
+
import { updateWidgetBody } from "#api/schemas.js";
|
|
14
|
+
|
|
15
|
+
export const prerender = false;
|
|
16
|
+
|
|
17
|
+
export const PUT: APIRoute = async ({ params, request, locals }) => {
|
|
18
|
+
const { dineway, user } = locals;
|
|
19
|
+
const db = dineway.db;
|
|
20
|
+
const { name, id } = params;
|
|
21
|
+
|
|
22
|
+
const denied = requirePerm(user, "widgets:manage");
|
|
23
|
+
if (denied) return denied;
|
|
24
|
+
|
|
25
|
+
if (!name || !id) {
|
|
26
|
+
return apiError("VALIDATION_ERROR", "name and id are required", 400);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Get the area
|
|
31
|
+
const area = await db
|
|
32
|
+
.selectFrom("_dineway_widget_areas")
|
|
33
|
+
.select("id")
|
|
34
|
+
.where("name", "=", name)
|
|
35
|
+
.executeTakeFirst();
|
|
36
|
+
|
|
37
|
+
if (!area) {
|
|
38
|
+
return apiError("NOT_FOUND", `Widget area "${name}" not found`, 404);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check widget exists and belongs to this area
|
|
42
|
+
const existingWidget = await db
|
|
43
|
+
.selectFrom("_dineway_widgets")
|
|
44
|
+
.select("id")
|
|
45
|
+
.where("id", "=", id)
|
|
46
|
+
.where("area_id", "=", area.id)
|
|
47
|
+
.executeTakeFirst();
|
|
48
|
+
|
|
49
|
+
if (!existingWidget) {
|
|
50
|
+
return apiError("NOT_FOUND", `Widget "${id}" not found in area "${name}"`, 404);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const body = await parseBody(request, updateWidgetBody);
|
|
54
|
+
if (isParseError(body)) return body;
|
|
55
|
+
|
|
56
|
+
// Build update object (only update provided fields)
|
|
57
|
+
const updates: Record<string, unknown> = {};
|
|
58
|
+
if (body.title !== undefined) updates.title = body.title || null;
|
|
59
|
+
if (body.type !== undefined) updates.type = body.type;
|
|
60
|
+
if (body.content !== undefined)
|
|
61
|
+
updates.content = body.content ? JSON.stringify(body.content) : null;
|
|
62
|
+
if (body.menuName !== undefined) updates.menu_name = body.menuName || null;
|
|
63
|
+
if (body.componentId !== undefined) updates.component_id = body.componentId || null;
|
|
64
|
+
if (body.componentProps !== undefined)
|
|
65
|
+
updates.component_props = body.componentProps ? JSON.stringify(body.componentProps) : null;
|
|
66
|
+
|
|
67
|
+
if (Object.keys(updates).length === 0) {
|
|
68
|
+
return apiError("VALIDATION_ERROR", "No fields to update", 400);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await db.updateTable("_dineway_widgets").set(updates).where("id", "=", id).execute();
|
|
72
|
+
|
|
73
|
+
const widget = await db
|
|
74
|
+
.selectFrom("_dineway_widgets")
|
|
75
|
+
.selectAll()
|
|
76
|
+
.where("id", "=", id)
|
|
77
|
+
.executeTakeFirstOrThrow();
|
|
78
|
+
|
|
79
|
+
return apiSuccess(widget);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return handleError(error, "Failed to update widget", "WIDGET_UPDATE_ERROR");
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const DELETE: APIRoute = async ({ params, locals }) => {
|
|
86
|
+
const { dineway, user } = locals;
|
|
87
|
+
const db = dineway.db;
|
|
88
|
+
const { name, id } = params;
|
|
89
|
+
|
|
90
|
+
const denied = requirePerm(user, "widgets:manage");
|
|
91
|
+
if (denied) return denied;
|
|
92
|
+
|
|
93
|
+
if (!name || !id) {
|
|
94
|
+
return apiError("VALIDATION_ERROR", "name and id are required", 400);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Get the area
|
|
99
|
+
const area = await db
|
|
100
|
+
.selectFrom("_dineway_widget_areas")
|
|
101
|
+
.select("id")
|
|
102
|
+
.where("name", "=", name)
|
|
103
|
+
.executeTakeFirst();
|
|
104
|
+
|
|
105
|
+
if (!area) {
|
|
106
|
+
return apiError("NOT_FOUND", `Widget area "${name}" not found`, 404);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check widget exists and belongs to this area
|
|
110
|
+
const existingWidget = await db
|
|
111
|
+
.selectFrom("_dineway_widgets")
|
|
112
|
+
.select("id")
|
|
113
|
+
.where("id", "=", id)
|
|
114
|
+
.where("area_id", "=", area.id)
|
|
115
|
+
.executeTakeFirst();
|
|
116
|
+
|
|
117
|
+
if (!existingWidget) {
|
|
118
|
+
return apiError("NOT_FOUND", `Widget "${id}" not found in area "${name}"`, 404);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await db.deleteFrom("_dineway_widgets").where("id", "=", id).execute();
|
|
122
|
+
|
|
123
|
+
return apiSuccess({ deleted: true });
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return handleError(error, "Failed to delete widget", "WIDGET_DELETE_ERROR");
|
|
126
|
+
}
|
|
127
|
+
};
|