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,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/auth/dev-bypass
|
|
3
|
+
* GET /_dineway/api/auth/dev-bypass
|
|
4
|
+
*
|
|
5
|
+
* Development-only endpoint to bypass passkey authentication.
|
|
6
|
+
* Creates or uses a test admin user and establishes a session.
|
|
7
|
+
*
|
|
8
|
+
* ONLY available when import.meta.env.DEV is true.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* - GET with redirect: /_dineway/api/auth/dev-bypass?redirect=/_dineway/admin
|
|
12
|
+
* - POST for API: Returns JSON with user info
|
|
13
|
+
*
|
|
14
|
+
* For agent/browser testing, navigate to:
|
|
15
|
+
* /_dineway/api/auth/dev-bypass?redirect=/_dineway/admin
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { APIRoute } from "astro";
|
|
19
|
+
|
|
20
|
+
export const prerender = false;
|
|
21
|
+
|
|
22
|
+
import { ulid } from "ulidx";
|
|
23
|
+
|
|
24
|
+
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
25
|
+
import { escapeHtml } from "#api/escape.js";
|
|
26
|
+
import { isSafeRedirect } from "#api/redirect.js";
|
|
27
|
+
import { runMigrations } from "#db/migrations/runner.js";
|
|
28
|
+
|
|
29
|
+
const DEV_USER_EMAIL = "dev@dineway.local";
|
|
30
|
+
const DEV_USER_NAME = "Dev Admin";
|
|
31
|
+
|
|
32
|
+
// RBAC role levels (matching @dineway-ai/auth)
|
|
33
|
+
const ROLE_ADMIN = 50;
|
|
34
|
+
|
|
35
|
+
async function handleDevBypass(context: Parameters<APIRoute>[0]): Promise<Response> {
|
|
36
|
+
// CRITICAL: Only allow in development mode
|
|
37
|
+
if (!import.meta.env.DEV) {
|
|
38
|
+
return apiError("FORBIDDEN", "Dev bypass is only available in development mode", 403);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { locals, url, session } = context;
|
|
42
|
+
const { dineway } = locals;
|
|
43
|
+
|
|
44
|
+
if (!dineway?.db) {
|
|
45
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// Ensure migrations are run
|
|
50
|
+
await runMigrations(dineway.db);
|
|
51
|
+
|
|
52
|
+
// Find or create dev user (direct DB access to avoid @dineway-ai/auth import issues in dev)
|
|
53
|
+
const existingUser = await dineway.db
|
|
54
|
+
.selectFrom("users")
|
|
55
|
+
.selectAll()
|
|
56
|
+
.where("email", "=", DEV_USER_EMAIL)
|
|
57
|
+
.executeTakeFirst();
|
|
58
|
+
|
|
59
|
+
let user: { id: string; email: string; name: string; role: number };
|
|
60
|
+
|
|
61
|
+
if (!existingUser) {
|
|
62
|
+
const now = new Date().toISOString();
|
|
63
|
+
const newUser = {
|
|
64
|
+
id: ulid(),
|
|
65
|
+
email: DEV_USER_EMAIL,
|
|
66
|
+
name: DEV_USER_NAME,
|
|
67
|
+
role: ROLE_ADMIN,
|
|
68
|
+
email_verified: 1,
|
|
69
|
+
created_at: now,
|
|
70
|
+
updated_at: now,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
await dineway.db.insertInto("users").values(newUser).execute();
|
|
74
|
+
|
|
75
|
+
user = {
|
|
76
|
+
id: newUser.id,
|
|
77
|
+
email: newUser.email,
|
|
78
|
+
name: newUser.name,
|
|
79
|
+
role: newUser.role,
|
|
80
|
+
};
|
|
81
|
+
console.log("[dev-bypass] Created dev admin user:", user.email);
|
|
82
|
+
} else {
|
|
83
|
+
user = {
|
|
84
|
+
id: existingUser.id,
|
|
85
|
+
email: existingUser.email,
|
|
86
|
+
name: existingUser.name || DEV_USER_NAME,
|
|
87
|
+
role: existingUser.role,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Create session
|
|
92
|
+
if (session) {
|
|
93
|
+
session.set("user", { id: user.id });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check for redirect parameter
|
|
97
|
+
const redirect = url.searchParams.get("redirect");
|
|
98
|
+
|
|
99
|
+
if (redirect) {
|
|
100
|
+
// Validate redirect is a safe local path (prevent open redirect via //evil.com or /\evil.com)
|
|
101
|
+
if (!isSafeRedirect(redirect)) {
|
|
102
|
+
return apiError("INVALID_REDIRECT", "Redirect must be a local path", 400);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Return an HTML page with meta-refresh redirect
|
|
106
|
+
// This ensures the session is fully saved before redirect
|
|
107
|
+
const safeRedirect = escapeHtml(redirect);
|
|
108
|
+
const html = `<!DOCTYPE html>
|
|
109
|
+
<html>
|
|
110
|
+
<head>
|
|
111
|
+
<meta http-equiv="refresh" content="0;url=${safeRedirect}">
|
|
112
|
+
</head>
|
|
113
|
+
<body>Redirecting...</body>
|
|
114
|
+
</html>`;
|
|
115
|
+
return new Response(html, {
|
|
116
|
+
status: 200,
|
|
117
|
+
headers: { "Content-Type": "text/html" },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Return JSON response
|
|
122
|
+
return apiSuccess({
|
|
123
|
+
success: true,
|
|
124
|
+
message: "Dev session created",
|
|
125
|
+
user: {
|
|
126
|
+
id: user.id,
|
|
127
|
+
email: user.email,
|
|
128
|
+
name: user.name,
|
|
129
|
+
role: user.role,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return handleError(error, "Dev bypass setup failed", "DEV_BYPASS_ERROR");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Support both GET and POST
|
|
138
|
+
export const GET: APIRoute = handleDevBypass;
|
|
139
|
+
export const POST: APIRoute = handleDevBypass;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /_dineway/api/auth/invite/accept
|
|
3
|
+
*
|
|
4
|
+
* Validate an invite token and return invite data for the UI.
|
|
5
|
+
* This is called when the invitee clicks the email link.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { APIRoute } from "astro";
|
|
9
|
+
|
|
10
|
+
export const prerender = false;
|
|
11
|
+
|
|
12
|
+
import { validateInvite, InviteError, roleFromLevel } from "@dineway-ai/auth";
|
|
13
|
+
import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
14
|
+
|
|
15
|
+
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
16
|
+
|
|
17
|
+
export const GET: APIRoute = async ({ url, locals }) => {
|
|
18
|
+
const { dineway } = locals;
|
|
19
|
+
|
|
20
|
+
if (!dineway?.db) {
|
|
21
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const token = url.searchParams.get("token");
|
|
25
|
+
|
|
26
|
+
if (!token) {
|
|
27
|
+
return apiError("MISSING_PARAM", "Token is required", 400);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
32
|
+
const invite = await validateInvite(adapter, token);
|
|
33
|
+
|
|
34
|
+
return apiSuccess({
|
|
35
|
+
success: true,
|
|
36
|
+
email: invite.email,
|
|
37
|
+
role: invite.role,
|
|
38
|
+
roleName: roleFromLevel(invite.role),
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error instanceof InviteError) {
|
|
42
|
+
const statusMap: Record<string, number> = {
|
|
43
|
+
invalid_token: 404,
|
|
44
|
+
token_expired: 410,
|
|
45
|
+
user_exists: 409,
|
|
46
|
+
};
|
|
47
|
+
return apiError(error.code.toUpperCase(), error.message, statusMap[error.code] ?? 400);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return handleError(error, "Failed to validate invite", "INVITE_VALIDATE_ERROR");
|
|
51
|
+
}
|
|
52
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/auth/invite/complete
|
|
3
|
+
*
|
|
4
|
+
* Complete the invite 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 { completeInvite, InviteError } 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 { inviteCompleteBody } 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, inviteCompleteBody);
|
|
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 invite - creates the user
|
|
53
|
+
const user = await completeInvite(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 InviteError) {
|
|
76
|
+
const statusMap: Record<string, number> = {
|
|
77
|
+
invalid_token: 404,
|
|
78
|
+
token_expired: 410,
|
|
79
|
+
user_exists: 409,
|
|
80
|
+
};
|
|
81
|
+
return apiError(error.code.toUpperCase(), error.message, statusMap[error.code] ?? 400);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return handleError(error, "Failed to complete invite", "INVITE_COMPLETE_ERROR");
|
|
85
|
+
}
|
|
86
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/auth/invite
|
|
3
|
+
*
|
|
4
|
+
* Create an invite for a new user. Admin only.
|
|
5
|
+
*
|
|
6
|
+
* When an email provider is configured (via the plugin email pipeline),
|
|
7
|
+
* the invite email is sent automatically.
|
|
8
|
+
* When no provider is configured, returns the invite URL for the admin
|
|
9
|
+
* to share manually (copy-link fallback).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { APIRoute } from "astro";
|
|
13
|
+
|
|
14
|
+
export const prerender = false;
|
|
15
|
+
|
|
16
|
+
import { createInvite, InviteError, Role } from "@dineway-ai/auth";
|
|
17
|
+
import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
18
|
+
|
|
19
|
+
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
20
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
21
|
+
import { inviteCreateBody } from "#api/schemas.js";
|
|
22
|
+
import { getSiteBaseUrl } from "#api/site-url.js";
|
|
23
|
+
import { OptionsRepository } from "#db/repositories/options.js";
|
|
24
|
+
|
|
25
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
26
|
+
const { dineway, user } = locals;
|
|
27
|
+
|
|
28
|
+
if (!dineway?.db) {
|
|
29
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!user || user.role < Role.ADMIN) {
|
|
33
|
+
return apiError("FORBIDDEN", "Admin privileges required", 403);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const body = await parseBody(request, inviteCreateBody);
|
|
40
|
+
if (isParseError(body)) return body;
|
|
41
|
+
|
|
42
|
+
// Default to AUTHOR role if not specified (Zod validates the level)
|
|
43
|
+
const role = body.role ?? Role.AUTHOR;
|
|
44
|
+
|
|
45
|
+
// Get site config for invite email
|
|
46
|
+
const options = new OptionsRepository(dineway.db);
|
|
47
|
+
const siteName = (await options.get<string>("dineway:site_title")) || "Dineway";
|
|
48
|
+
|
|
49
|
+
// Use stored site URL to prevent Host header spoofing in invite emails
|
|
50
|
+
const baseUrl = await getSiteBaseUrl(dineway.db, request);
|
|
51
|
+
|
|
52
|
+
// Build email sender from the plugin pipeline (if available)
|
|
53
|
+
const emailSend = dineway.email?.isAvailable()
|
|
54
|
+
? (message: { to: string; subject: string; text: string; html?: string }) =>
|
|
55
|
+
dineway.email!.send(message, "system")
|
|
56
|
+
: undefined;
|
|
57
|
+
|
|
58
|
+
const result = await createInvite(
|
|
59
|
+
{
|
|
60
|
+
baseUrl,
|
|
61
|
+
siteName,
|
|
62
|
+
email: emailSend,
|
|
63
|
+
},
|
|
64
|
+
adapter,
|
|
65
|
+
body.email,
|
|
66
|
+
role,
|
|
67
|
+
user.id,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (emailSend) {
|
|
71
|
+
// Email was sent
|
|
72
|
+
return apiSuccess({
|
|
73
|
+
success: true,
|
|
74
|
+
message: `Invite sent to ${body.email}`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// No email provider — return the invite URL for manual sharing
|
|
79
|
+
return apiSuccess(
|
|
80
|
+
{
|
|
81
|
+
success: true,
|
|
82
|
+
message: "Invite created. No email provider configured — share the link manually.",
|
|
83
|
+
inviteUrl: result.url,
|
|
84
|
+
},
|
|
85
|
+
200,
|
|
86
|
+
);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error instanceof InviteError) {
|
|
89
|
+
const statusMap: Record<string, number> = {
|
|
90
|
+
user_exists: 409,
|
|
91
|
+
invalid_token: 400,
|
|
92
|
+
token_expired: 400,
|
|
93
|
+
};
|
|
94
|
+
return apiError(error.code.toUpperCase(), error.message, statusMap[error.code] ?? 400);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return handleError(error, "Failed to create invite", "INVITE_CREATE_ERROR");
|
|
98
|
+
}
|
|
99
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/auth/invite/register-options
|
|
3
|
+
*
|
|
4
|
+
* Generate WebAuthn registration options for an invited user.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
|
|
9
|
+
export const prerender = false;
|
|
10
|
+
|
|
11
|
+
import { validateInvite, InviteError } from "@dineway-ai/auth";
|
|
12
|
+
import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
13
|
+
import { generateRegistrationOptions } from "@dineway-ai/auth/passkey";
|
|
14
|
+
import { ulid } from "ulidx";
|
|
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 { inviteRegisterOptionsBody } 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 }) => {
|
|
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, inviteRegisterOptionsBody);
|
|
33
|
+
if (isParseError(body)) return body;
|
|
34
|
+
|
|
35
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
36
|
+
const invite = await validateInvite(adapter, body.token);
|
|
37
|
+
|
|
38
|
+
const url = new URL(request.url);
|
|
39
|
+
const optionsRepo = new OptionsRepository(dineway.db);
|
|
40
|
+
const siteName = (await optionsRepo.get<string>("dineway:site_title")) ?? undefined;
|
|
41
|
+
const siteUrl = getPublicOrigin(url, dineway?.config);
|
|
42
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
|
|
43
|
+
|
|
44
|
+
const challengeStore = createChallengeStore(dineway.db);
|
|
45
|
+
const registrationOptions = await generateRegistrationOptions(
|
|
46
|
+
passkeyConfig,
|
|
47
|
+
{
|
|
48
|
+
id: ulid(),
|
|
49
|
+
email: invite.email,
|
|
50
|
+
name: body.name || null,
|
|
51
|
+
},
|
|
52
|
+
[],
|
|
53
|
+
challengeStore,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return apiSuccess({ options: registrationOptions });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error instanceof InviteError) {
|
|
59
|
+
const statusMap: Record<string, number> = {
|
|
60
|
+
invalid_token: 404,
|
|
61
|
+
token_expired: 410,
|
|
62
|
+
user_exists: 409,
|
|
63
|
+
};
|
|
64
|
+
return apiError(error.code.toUpperCase(), error.message, statusMap[error.code] ?? 400);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return handleError(
|
|
68
|
+
error,
|
|
69
|
+
"Failed to generate registration options",
|
|
70
|
+
"INVITE_REGISTER_OPTIONS_ERROR",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/auth/logout
|
|
3
|
+
*
|
|
4
|
+
* Destroys the current session and logs the user out.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
|
|
9
|
+
export const prerender = false;
|
|
10
|
+
|
|
11
|
+
import { apiSuccess, handleError } from "#api/error.js";
|
|
12
|
+
import { isSafeRedirect } from "#api/redirect.js";
|
|
13
|
+
|
|
14
|
+
export const POST: APIRoute = async ({ session, url }) => {
|
|
15
|
+
try {
|
|
16
|
+
// Destroy session
|
|
17
|
+
if (session) {
|
|
18
|
+
session.destroy();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check for redirect parameter
|
|
22
|
+
const redirect = url.searchParams.get("redirect");
|
|
23
|
+
|
|
24
|
+
if (isSafeRedirect(redirect)) {
|
|
25
|
+
return new Response(null, {
|
|
26
|
+
status: 302,
|
|
27
|
+
headers: { Location: redirect },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return apiSuccess({
|
|
32
|
+
success: true,
|
|
33
|
+
message: "Logged out successfully",
|
|
34
|
+
});
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return handleError(error, "Logout failed", "LOGOUT_ERROR");
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// No GET handler — logout must be POST-only to prevent CSRF via link/img tags
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/auth/magic-link/send
|
|
3
|
+
*
|
|
4
|
+
* Send a magic link email for passwordless authentication.
|
|
5
|
+
* Always returns success to avoid revealing whether email exists.
|
|
6
|
+
*
|
|
7
|
+
* Rate limited: 3 requests per 5 minutes per IP.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { APIRoute } from "astro";
|
|
11
|
+
|
|
12
|
+
export const prerender = false;
|
|
13
|
+
|
|
14
|
+
import { sendMagicLink, type MagicLinkConfig } from "@dineway-ai/auth";
|
|
15
|
+
import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
16
|
+
|
|
17
|
+
import { apiError, apiSuccess } from "#api/error.js";
|
|
18
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
19
|
+
import { magicLinkSendBody } from "#api/schemas.js";
|
|
20
|
+
import { getSiteBaseUrl } from "#api/site-url.js";
|
|
21
|
+
import { checkRateLimit, getClientIp } from "#auth/rate-limit.js";
|
|
22
|
+
import { getTrustedProxyHeaders } from "#auth/trusted-proxy.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
|
+
// Parse request body first — avoids consuming rate limit slots on
|
|
34
|
+
// malformed requests and normalizes timing between rate-limited
|
|
35
|
+
// and real paths (parse cost evens out the response time).
|
|
36
|
+
const body = await parseBody(request, magicLinkSendBody);
|
|
37
|
+
if (isParseError(body)) return body;
|
|
38
|
+
|
|
39
|
+
// Rate limit: 3 requests per 300 seconds (5 minutes) per IP
|
|
40
|
+
const ip = getClientIp(request, getTrustedProxyHeaders(dineway.config));
|
|
41
|
+
const rateLimit = await checkRateLimit(dineway.db, ip, "magic-link/send", 3, 300);
|
|
42
|
+
if (!rateLimit.allowed) {
|
|
43
|
+
// Return success-shaped response to avoid revealing rate limit
|
|
44
|
+
// (which could leak information about email enumeration attempts)
|
|
45
|
+
return apiSuccess({
|
|
46
|
+
success: true,
|
|
47
|
+
message: "If an account exists for this email, a magic link has been sent.",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check if email pipeline is available
|
|
52
|
+
if (!dineway.email?.isAvailable()) {
|
|
53
|
+
return apiError(
|
|
54
|
+
"EMAIL_NOT_CONFIGURED",
|
|
55
|
+
"Email is not configured. Magic link authentication requires an email provider.",
|
|
56
|
+
503,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Build magic link config using stored site URL (not request Host header)
|
|
61
|
+
const options = new OptionsRepository(dineway.db);
|
|
62
|
+
const baseUrl = await getSiteBaseUrl(dineway.db, request);
|
|
63
|
+
const siteName = (await options.get<string>("dineway:site_title")) ?? "Dineway";
|
|
64
|
+
|
|
65
|
+
const config: MagicLinkConfig = {
|
|
66
|
+
baseUrl,
|
|
67
|
+
siteName,
|
|
68
|
+
email: (message) => dineway.email!.send(message, "system"),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Send magic link (silently fails if user doesn't exist)
|
|
72
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
73
|
+
await sendMagicLink(config, adapter, body.email.toLowerCase());
|
|
74
|
+
|
|
75
|
+
// Always return success to avoid revealing if email exists
|
|
76
|
+
return apiSuccess({
|
|
77
|
+
success: true,
|
|
78
|
+
message: "If an account exists for this email, a magic link has been sent.",
|
|
79
|
+
});
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error("Magic link send error:", error);
|
|
82
|
+
|
|
83
|
+
// Still return success to avoid revealing information
|
|
84
|
+
// Log the error but don't expose it to the client
|
|
85
|
+
return apiSuccess({
|
|
86
|
+
success: true,
|
|
87
|
+
message: "If an account exists for this email, a magic link has been sent.",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /_dineway/api/auth/magic-link/verify
|
|
3
|
+
*
|
|
4
|
+
* Verify a magic link token and create a session.
|
|
5
|
+
* Tokens are single-use and expire after 15 minutes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { APIRoute } from "astro";
|
|
9
|
+
|
|
10
|
+
export const prerender = false;
|
|
11
|
+
|
|
12
|
+
import { verifyMagicLink, MagicLinkError } from "@dineway-ai/auth";
|
|
13
|
+
import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
14
|
+
|
|
15
|
+
import { apiError } from "#api/error.js";
|
|
16
|
+
import { isSafeRedirect } from "#api/redirect.js";
|
|
17
|
+
|
|
18
|
+
export const GET: APIRoute = async ({ url, locals, session, redirect }) => {
|
|
19
|
+
const { dineway } = locals;
|
|
20
|
+
|
|
21
|
+
if (!dineway?.db) {
|
|
22
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Get token from query params
|
|
26
|
+
const token = url.searchParams.get("token");
|
|
27
|
+
|
|
28
|
+
if (!token) {
|
|
29
|
+
// Redirect to login with error
|
|
30
|
+
return redirect("/_dineway/admin/login?error=missing_token");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Verify the magic link token
|
|
35
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
36
|
+
const user = await verifyMagicLink(adapter, token);
|
|
37
|
+
|
|
38
|
+
// Fire-and-forget cleanup of expired tokens -- prevents accumulation
|
|
39
|
+
void adapter.deleteExpiredTokens().catch(() => {});
|
|
40
|
+
|
|
41
|
+
// Create session
|
|
42
|
+
if (session) {
|
|
43
|
+
session.set("user", { id: user.id });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check for a stored redirect URL (from original request)
|
|
47
|
+
// Validate redirect is a safe local path (prevent open redirect via //evil.com or /\evil.com)
|
|
48
|
+
const rawRedirect = url.searchParams.get("redirect");
|
|
49
|
+
const redirectUrl = isSafeRedirect(rawRedirect) ? rawRedirect : "/_dineway/admin";
|
|
50
|
+
|
|
51
|
+
// Redirect to admin dashboard or original URL
|
|
52
|
+
return redirect(redirectUrl);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("Magic link verify error:", error);
|
|
55
|
+
|
|
56
|
+
// Handle specific errors
|
|
57
|
+
if (error instanceof MagicLinkError) {
|
|
58
|
+
switch (error.code) {
|
|
59
|
+
case "invalid_token":
|
|
60
|
+
return redirect("/_dineway/admin/login?error=invalid_link");
|
|
61
|
+
case "token_expired":
|
|
62
|
+
return redirect("/_dineway/admin/login?error=link_expired");
|
|
63
|
+
case "user_not_found":
|
|
64
|
+
return redirect("/_dineway/admin/login?error=user_not_found");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Generic error
|
|
69
|
+
return redirect("/_dineway/admin/login?error=verification_failed");
|
|
70
|
+
}
|
|
71
|
+
};
|