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,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PATCH/DELETE /_dineway/api/auth/passkey/[id]
|
|
3
|
+
*
|
|
4
|
+
* Rename or delete a passkey
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
|
|
9
|
+
export const prerender = false;
|
|
10
|
+
|
|
11
|
+
import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
12
|
+
|
|
13
|
+
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
14
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
15
|
+
import { passkeyRenameBody } from "#api/schemas.js";
|
|
16
|
+
|
|
17
|
+
interface PasskeyResponse {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string | null;
|
|
20
|
+
deviceType: "singleDevice" | "multiDevice";
|
|
21
|
+
backedUp: boolean;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
lastUsedAt: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* PATCH - Rename a passkey
|
|
28
|
+
*/
|
|
29
|
+
export const PATCH: APIRoute = async ({ params, request, locals }) => {
|
|
30
|
+
const { dineway, user } = locals;
|
|
31
|
+
const { id } = params;
|
|
32
|
+
|
|
33
|
+
if (!dineway?.db) {
|
|
34
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Require authentication
|
|
38
|
+
if (!user) {
|
|
39
|
+
return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!id) {
|
|
43
|
+
return apiError("MISSING_PARAM", "Passkey ID is required", 400);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
48
|
+
|
|
49
|
+
// Get the credential and verify ownership
|
|
50
|
+
const credential = await adapter.getCredentialById(id);
|
|
51
|
+
|
|
52
|
+
if (!credential || credential.userId !== user.id) {
|
|
53
|
+
return apiError("NOT_FOUND", "Passkey not found", 404);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Parse request body
|
|
57
|
+
const body = await parseBody(request, passkeyRenameBody);
|
|
58
|
+
if (isParseError(body)) return body;
|
|
59
|
+
|
|
60
|
+
// Update the name
|
|
61
|
+
const trimmedName = body.name.trim() || null;
|
|
62
|
+
await adapter.updateCredentialName(id, trimmedName);
|
|
63
|
+
|
|
64
|
+
// Return updated passkey info
|
|
65
|
+
const passkey: PasskeyResponse = {
|
|
66
|
+
id: credential.id,
|
|
67
|
+
name: trimmedName,
|
|
68
|
+
deviceType: credential.deviceType,
|
|
69
|
+
backedUp: credential.backedUp,
|
|
70
|
+
createdAt: credential.createdAt.toISOString(),
|
|
71
|
+
lastUsedAt: credential.lastUsedAt.toISOString(),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return apiSuccess({ passkey });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return handleError(error, "Failed to rename passkey", "PASSKEY_RENAME_ERROR");
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* DELETE - Remove a passkey
|
|
82
|
+
*/
|
|
83
|
+
export const DELETE: APIRoute = async ({ params, locals }) => {
|
|
84
|
+
const { dineway, user } = locals;
|
|
85
|
+
const { id } = params;
|
|
86
|
+
|
|
87
|
+
if (!dineway?.db) {
|
|
88
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Require authentication
|
|
92
|
+
if (!user) {
|
|
93
|
+
return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!id) {
|
|
97
|
+
return apiError("MISSING_PARAM", "Passkey ID is required", 400);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
102
|
+
|
|
103
|
+
// Get the credential and verify ownership
|
|
104
|
+
const credential = await adapter.getCredentialById(id);
|
|
105
|
+
|
|
106
|
+
if (!credential || credential.userId !== user.id) {
|
|
107
|
+
return apiError("NOT_FOUND", "Passkey not found", 404);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check that this isn't the last passkey
|
|
111
|
+
const count = await adapter.countCredentialsByUserId(user.id);
|
|
112
|
+
|
|
113
|
+
if (count <= 1) {
|
|
114
|
+
return apiError("LAST_PASSKEY", "Cannot remove your last passkey", 400);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Delete the passkey
|
|
118
|
+
await adapter.deleteCredential(id);
|
|
119
|
+
|
|
120
|
+
return apiSuccess({ success: true });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return handleError(error, "Failed to delete passkey", "PASSKEY_DELETE_ERROR");
|
|
123
|
+
}
|
|
124
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /_dineway/api/auth/passkey
|
|
3
|
+
*
|
|
4
|
+
* List all passkeys for the authenticated user
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
|
|
9
|
+
export const prerender = false;
|
|
10
|
+
|
|
11
|
+
import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
12
|
+
|
|
13
|
+
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
14
|
+
|
|
15
|
+
interface PasskeyResponse {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string | null;
|
|
18
|
+
deviceType: "singleDevice" | "multiDevice";
|
|
19
|
+
backedUp: boolean;
|
|
20
|
+
createdAt: string;
|
|
21
|
+
lastUsedAt: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
25
|
+
const { dineway, user } = locals;
|
|
26
|
+
|
|
27
|
+
if (!dineway?.db) {
|
|
28
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Require authentication
|
|
32
|
+
if (!user) {
|
|
33
|
+
return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
38
|
+
const credentials = await adapter.getCredentialsByUserId(user.id);
|
|
39
|
+
|
|
40
|
+
// Map to public response format (exclude sensitive fields)
|
|
41
|
+
const passkeys: PasskeyResponse[] = credentials.map((cred) => ({
|
|
42
|
+
id: cred.id,
|
|
43
|
+
name: cred.name,
|
|
44
|
+
deviceType: cred.deviceType,
|
|
45
|
+
backedUp: cred.backedUp,
|
|
46
|
+
createdAt: cred.createdAt.toISOString(),
|
|
47
|
+
lastUsedAt: cred.lastUsedAt.toISOString(),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
return apiSuccess({ items: passkeys });
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return handleError(error, "Failed to list passkeys", "PASSKEY_LIST_ERROR");
|
|
53
|
+
}
|
|
54
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/auth/passkey/options
|
|
3
|
+
*
|
|
4
|
+
* Get authentication options for passkey login.
|
|
5
|
+
*
|
|
6
|
+
* Rate limited: 10 requests per minute per IP.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { APIRoute } from "astro";
|
|
10
|
+
|
|
11
|
+
export const prerender = false;
|
|
12
|
+
|
|
13
|
+
import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
14
|
+
import { generateAuthenticationOptions } from "@dineway-ai/auth/passkey";
|
|
15
|
+
|
|
16
|
+
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
17
|
+
import { isParseError, parseOptionalBody } from "#api/parse.js";
|
|
18
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
19
|
+
import { passkeyOptionsBody } from "#api/schemas.js";
|
|
20
|
+
import { createChallengeStore, cleanupExpiredChallenges } from "#auth/challenge-store.js";
|
|
21
|
+
import { getPasskeyConfig } from "#auth/passkey-config.js";
|
|
22
|
+
import { checkRateLimit, getClientIp, rateLimitResponse } from "#auth/rate-limit.js";
|
|
23
|
+
import { OptionsRepository } from "#db/repositories/options.js";
|
|
24
|
+
|
|
25
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
26
|
+
const { dineway } = locals;
|
|
27
|
+
|
|
28
|
+
if (!dineway?.db) {
|
|
29
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Fire-and-forget cleanup of expired challenges -- prevents accumulation
|
|
34
|
+
void cleanupExpiredChallenges(dineway.db).catch(() => {});
|
|
35
|
+
|
|
36
|
+
// Parse body before rate limiting so malformed requests don't consume slots
|
|
37
|
+
const body = await parseOptionalBody(request, passkeyOptionsBody, {});
|
|
38
|
+
if (isParseError(body)) return body;
|
|
39
|
+
|
|
40
|
+
// Rate limit: 10 requests per 60 seconds per IP
|
|
41
|
+
const ip = getClientIp(request);
|
|
42
|
+
const rateLimit = await checkRateLimit(dineway.db, ip, "passkey/options", 10, 60);
|
|
43
|
+
if (!rateLimit.allowed) {
|
|
44
|
+
return rateLimitResponse(60);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
48
|
+
|
|
49
|
+
// Get credentials to allow
|
|
50
|
+
let credentials: Awaited<ReturnType<typeof adapter.getCredentialsByUserId>> = [];
|
|
51
|
+
|
|
52
|
+
if (body.email) {
|
|
53
|
+
// Get credentials for specific user
|
|
54
|
+
const user = await adapter.getUserByEmail(body.email);
|
|
55
|
+
if (user) {
|
|
56
|
+
credentials = await adapter.getCredentialsByUserId(user.id);
|
|
57
|
+
}
|
|
58
|
+
// Don't reveal if user exists - just return empty allowCredentials
|
|
59
|
+
}
|
|
60
|
+
// If no email provided, allowCredentials will be undefined (allow any discoverable credential)
|
|
61
|
+
|
|
62
|
+
// Get passkey config
|
|
63
|
+
const url = new URL(request.url);
|
|
64
|
+
const options = new OptionsRepository(dineway.db);
|
|
65
|
+
const siteName = (await options.get<string>("dineway:site_title")) ?? undefined;
|
|
66
|
+
const siteUrl = getPublicOrigin(url, dineway?.config);
|
|
67
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
|
|
68
|
+
|
|
69
|
+
// Generate authentication options
|
|
70
|
+
const challengeStore = createChallengeStore(dineway.db);
|
|
71
|
+
const authOptions = await generateAuthenticationOptions(
|
|
72
|
+
passkeyConfig,
|
|
73
|
+
credentials,
|
|
74
|
+
challengeStore,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return apiSuccess({
|
|
78
|
+
success: true,
|
|
79
|
+
options: authOptions,
|
|
80
|
+
});
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return handleError(error, "Failed to generate passkey options", "PASSKEY_OPTIONS_ERROR");
|
|
83
|
+
}
|
|
84
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/auth/passkey/register/options
|
|
3
|
+
*
|
|
4
|
+
* Get WebAuthn registration options for adding a new passkey (authenticated user)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
|
|
9
|
+
export const prerender = false;
|
|
10
|
+
|
|
11
|
+
import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
12
|
+
import { generateRegistrationOptions } from "@dineway-ai/auth/passkey";
|
|
13
|
+
|
|
14
|
+
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
15
|
+
import { isParseError, parseOptionalBody } from "#api/parse.js";
|
|
16
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
17
|
+
import { passkeyRegisterOptionsBody } from "#api/schemas.js";
|
|
18
|
+
import { createChallengeStore } from "#auth/challenge-store.js";
|
|
19
|
+
import { getPasskeyConfig } from "#auth/passkey-config.js";
|
|
20
|
+
import { OptionsRepository } from "#db/repositories/options.js";
|
|
21
|
+
|
|
22
|
+
const MAX_PASSKEYS = 10;
|
|
23
|
+
|
|
24
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
25
|
+
const { dineway, user } = locals;
|
|
26
|
+
|
|
27
|
+
if (!dineway?.db) {
|
|
28
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Require authentication
|
|
32
|
+
if (!user) {
|
|
33
|
+
return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
38
|
+
|
|
39
|
+
// Check passkey limit
|
|
40
|
+
const count = await adapter.countCredentialsByUserId(user.id);
|
|
41
|
+
if (count >= MAX_PASSKEYS) {
|
|
42
|
+
return apiError("PASSKEY_LIMIT", `Maximum of ${MAX_PASSKEYS} passkeys allowed`, 400);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Parse optional name from request
|
|
46
|
+
const body = await parseOptionalBody(request, passkeyRegisterOptionsBody, {});
|
|
47
|
+
if (isParseError(body)) return body;
|
|
48
|
+
|
|
49
|
+
// Get existing credentials for excludeCredentials
|
|
50
|
+
const existingCredentials = await adapter.getCredentialsByUserId(user.id);
|
|
51
|
+
|
|
52
|
+
// Get passkey config
|
|
53
|
+
const url = new URL(request.url);
|
|
54
|
+
const optionsRepo = new OptionsRepository(dineway.db);
|
|
55
|
+
const siteName = (await optionsRepo.get<string>("dineway:site_title")) ?? undefined;
|
|
56
|
+
const siteUrl = getPublicOrigin(url, dineway?.config);
|
|
57
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
|
|
58
|
+
|
|
59
|
+
// Generate registration options
|
|
60
|
+
const challengeStore = createChallengeStore(dineway.db);
|
|
61
|
+
const registrationOptions = await generateRegistrationOptions(
|
|
62
|
+
passkeyConfig,
|
|
63
|
+
{ id: user.id, email: user.email, name: user.name },
|
|
64
|
+
existingCredentials,
|
|
65
|
+
challengeStore,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Store the passkey name in the challenge metadata if provided
|
|
69
|
+
// We'll retrieve it during verification
|
|
70
|
+
if (body.name) {
|
|
71
|
+
// Store name with challenge for later retrieval
|
|
72
|
+
// The challenge store will need this when verifying
|
|
73
|
+
await optionsRepo.set(`dineway:passkey_pending:${user.id}`, {
|
|
74
|
+
name: body.name,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return apiSuccess({
|
|
79
|
+
options: registrationOptions,
|
|
80
|
+
});
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return handleError(
|
|
83
|
+
error,
|
|
84
|
+
"Failed to generate registration options",
|
|
85
|
+
"PASSKEY_REGISTER_OPTIONS_ERROR",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/auth/passkey/register/verify
|
|
3
|
+
*
|
|
4
|
+
* Verify and store a new passkey credential (authenticated user)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
|
|
9
|
+
export const prerender = false;
|
|
10
|
+
|
|
11
|
+
import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
12
|
+
import { verifyRegistrationResponse, registerPasskey } from "@dineway-ai/auth/passkey";
|
|
13
|
+
|
|
14
|
+
import { apiError, apiSuccess } from "#api/error.js";
|
|
15
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
16
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
17
|
+
import { passkeyRegisterVerifyBody } from "#api/schemas.js";
|
|
18
|
+
import { createChallengeStore } from "#auth/challenge-store.js";
|
|
19
|
+
import { getPasskeyConfig } from "#auth/passkey-config.js";
|
|
20
|
+
import { OptionsRepository } from "#db/repositories/options.js";
|
|
21
|
+
|
|
22
|
+
const MAX_PASSKEYS = 10;
|
|
23
|
+
|
|
24
|
+
interface PasskeyResponse {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string | null;
|
|
27
|
+
deviceType: "singleDevice" | "multiDevice";
|
|
28
|
+
backedUp: boolean;
|
|
29
|
+
createdAt: string;
|
|
30
|
+
lastUsedAt: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
34
|
+
const { dineway, user } = locals;
|
|
35
|
+
|
|
36
|
+
if (!dineway?.db) {
|
|
37
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Require authentication
|
|
41
|
+
if (!user) {
|
|
42
|
+
return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
47
|
+
|
|
48
|
+
// Check passkey limit again (in case of concurrent requests)
|
|
49
|
+
const count = await adapter.countCredentialsByUserId(user.id);
|
|
50
|
+
if (count >= MAX_PASSKEYS) {
|
|
51
|
+
return apiError("PASSKEY_LIMIT", `Maximum of ${MAX_PASSKEYS} passkeys allowed`, 400);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Parse request body
|
|
55
|
+
const body = await parseBody(request, passkeyRegisterVerifyBody);
|
|
56
|
+
if (isParseError(body)) return body;
|
|
57
|
+
|
|
58
|
+
// Get passkey config
|
|
59
|
+
const url = new URL(request.url);
|
|
60
|
+
const optionsRepo = new OptionsRepository(dineway.db);
|
|
61
|
+
const siteName = (await optionsRepo.get<string>("dineway:site_title")) ?? undefined;
|
|
62
|
+
const siteUrl = getPublicOrigin(url, dineway?.config);
|
|
63
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
|
|
64
|
+
|
|
65
|
+
// Verify the registration response
|
|
66
|
+
const challengeStore = createChallengeStore(dineway.db);
|
|
67
|
+
const verified = await verifyRegistrationResponse(
|
|
68
|
+
passkeyConfig,
|
|
69
|
+
body.credential,
|
|
70
|
+
challengeStore,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Get passkey name - prefer body.name, then check stored pending name
|
|
74
|
+
let passKeyName: string | undefined = body.name ?? undefined;
|
|
75
|
+
if (!passKeyName) {
|
|
76
|
+
const pending = await optionsRepo.get<{ name?: string }>(
|
|
77
|
+
`dineway:passkey_pending:${user.id}`,
|
|
78
|
+
);
|
|
79
|
+
if (pending?.name) {
|
|
80
|
+
passKeyName = pending.name;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Clean up pending state
|
|
85
|
+
await optionsRepo.delete(`dineway:passkey_pending:${user.id}`);
|
|
86
|
+
|
|
87
|
+
// Register the passkey
|
|
88
|
+
const credential = await registerPasskey(adapter, user.id, verified, passKeyName);
|
|
89
|
+
|
|
90
|
+
// Return the new passkey info
|
|
91
|
+
const passkey: PasskeyResponse = {
|
|
92
|
+
id: credential.id,
|
|
93
|
+
name: credential.name,
|
|
94
|
+
deviceType: credential.deviceType,
|
|
95
|
+
backedUp: credential.backedUp,
|
|
96
|
+
createdAt: credential.createdAt.toISOString(),
|
|
97
|
+
lastUsedAt: credential.lastUsedAt.toISOString(),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return apiSuccess({ passkey });
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("Passkey registration verify error:", error);
|
|
103
|
+
|
|
104
|
+
// Handle specific errors
|
|
105
|
+
const message = error instanceof Error ? error.message : "";
|
|
106
|
+
|
|
107
|
+
// Check for duplicate credential error
|
|
108
|
+
if (message.includes("credential_exists") || message.includes("already")) {
|
|
109
|
+
return apiError("CREDENTIAL_EXISTS", "This passkey is already registered", 400);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check for challenge errors
|
|
113
|
+
if (message.includes("challenge") || message.includes("expired")) {
|
|
114
|
+
return apiError("CHALLENGE_EXPIRED", "Registration expired. Please try again.", 400);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return apiError("PASSKEY_REGISTER_ERROR", "Registration failed", 500);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/auth/passkey/verify
|
|
3
|
+
*
|
|
4
|
+
* Verify a passkey authentication and create a session
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
|
|
9
|
+
export const prerender = false;
|
|
10
|
+
|
|
11
|
+
import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
12
|
+
import { authenticateWithPasskey } from "@dineway-ai/auth/passkey";
|
|
13
|
+
|
|
14
|
+
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
15
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
16
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
17
|
+
import { passkeyVerifyBody } from "#api/schemas.js";
|
|
18
|
+
import { createChallengeStore } from "#auth/challenge-store.js";
|
|
19
|
+
import { getPasskeyConfig } from "#auth/passkey-config.js";
|
|
20
|
+
import { OptionsRepository } from "#db/repositories/options.js";
|
|
21
|
+
|
|
22
|
+
export const POST: APIRoute = async ({ request, locals, session }) => {
|
|
23
|
+
const { dineway } = locals;
|
|
24
|
+
|
|
25
|
+
if (!dineway?.db) {
|
|
26
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const body = await parseBody(request, passkeyVerifyBody);
|
|
31
|
+
if (isParseError(body)) return body;
|
|
32
|
+
|
|
33
|
+
// Get passkey config
|
|
34
|
+
const url = new URL(request.url);
|
|
35
|
+
const options = new OptionsRepository(dineway.db);
|
|
36
|
+
const siteName = (await options.get<string>("dineway:site_title")) ?? undefined;
|
|
37
|
+
const siteUrl = getPublicOrigin(url, dineway?.config);
|
|
38
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
|
|
39
|
+
|
|
40
|
+
// Authenticate with passkey
|
|
41
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
42
|
+
const challengeStore = createChallengeStore(dineway.db);
|
|
43
|
+
|
|
44
|
+
const user = await authenticateWithPasskey(
|
|
45
|
+
passkeyConfig,
|
|
46
|
+
adapter,
|
|
47
|
+
body.credential,
|
|
48
|
+
challengeStore,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Create session
|
|
52
|
+
if (session) {
|
|
53
|
+
session.set("user", { id: user.id });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return apiSuccess({
|
|
57
|
+
success: true,
|
|
58
|
+
user: {
|
|
59
|
+
id: user.id,
|
|
60
|
+
email: user.email,
|
|
61
|
+
name: user.name,
|
|
62
|
+
role: user.role,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return handleError(error, "Authentication failed", "PASSKEY_VERIFY_ERROR");
|
|
67
|
+
}
|
|
68
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/auth/signup/complete
|
|
3
|
+
*
|
|
4
|
+
* Complete self-signup by registering a passkey for the new user.
|
|
5
|
+
* This creates the user account and establishes a session.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { APIRoute } from "astro";
|
|
9
|
+
|
|
10
|
+
export const prerender = false;
|
|
11
|
+
|
|
12
|
+
import { completeSignup, SignupError } from "@dineway-ai/auth";
|
|
13
|
+
import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
14
|
+
import { verifyRegistrationResponse, registerPasskey } from "@dineway-ai/auth/passkey";
|
|
15
|
+
|
|
16
|
+
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
17
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
18
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
19
|
+
import { signupCompleteBody } from "#api/schemas.js";
|
|
20
|
+
import { createChallengeStore } from "#auth/challenge-store.js";
|
|
21
|
+
import { getPasskeyConfig } from "#auth/passkey-config.js";
|
|
22
|
+
import { OptionsRepository } from "#db/repositories/options.js";
|
|
23
|
+
|
|
24
|
+
export const POST: APIRoute = async ({ request, locals, session }) => {
|
|
25
|
+
const { dineway } = locals;
|
|
26
|
+
|
|
27
|
+
if (!dineway?.db) {
|
|
28
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const body = await parseBody(request, signupCompleteBody);
|
|
33
|
+
if (isParseError(body)) return body;
|
|
34
|
+
|
|
35
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
36
|
+
|
|
37
|
+
// Get passkey config
|
|
38
|
+
const url = new URL(request.url);
|
|
39
|
+
const options = new OptionsRepository(dineway.db);
|
|
40
|
+
const siteName = (await options.get<string>("dineway:site_title")) ?? undefined;
|
|
41
|
+
const siteUrl = getPublicOrigin(url, dineway?.config);
|
|
42
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
|
|
43
|
+
|
|
44
|
+
// Verify the passkey registration response
|
|
45
|
+
const challengeStore = createChallengeStore(dineway.db);
|
|
46
|
+
const verified = await verifyRegistrationResponse(
|
|
47
|
+
passkeyConfig,
|
|
48
|
+
body.credential,
|
|
49
|
+
challengeStore,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Complete the signup - creates the user
|
|
53
|
+
const user = await completeSignup(adapter, body.token, {
|
|
54
|
+
name: body.name,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Register the passkey for the new user
|
|
58
|
+
await registerPasskey(adapter, user.id, verified, "Initial passkey");
|
|
59
|
+
|
|
60
|
+
// Create session
|
|
61
|
+
if (session) {
|
|
62
|
+
session.set("user", { id: user.id });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return apiSuccess({
|
|
66
|
+
success: true,
|
|
67
|
+
user: {
|
|
68
|
+
id: user.id,
|
|
69
|
+
email: user.email,
|
|
70
|
+
name: user.name,
|
|
71
|
+
role: user.role,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error instanceof SignupError) {
|
|
76
|
+
const statusMap: Record<string, number> = {
|
|
77
|
+
invalid_token: 404,
|
|
78
|
+
token_expired: 410,
|
|
79
|
+
user_exists: 409,
|
|
80
|
+
domain_not_allowed: 403,
|
|
81
|
+
};
|
|
82
|
+
return apiError(error.code.toUpperCase(), error.message, statusMap[error.code] ?? 400);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return handleError(error, "Failed to complete signup", "SIGNUP_COMPLETE_ERROR");
|
|
86
|
+
}
|
|
87
|
+
};
|