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,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,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,89 @@
|
|
|
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 { 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
|
+
// Parse request body first — avoids consuming rate limit slots on
|
|
33
|
+
// malformed requests and normalizes timing between rate-limited
|
|
34
|
+
// and real paths (parse cost evens out the response time).
|
|
35
|
+
const body = await parseBody(request, magicLinkSendBody);
|
|
36
|
+
if (isParseError(body)) return body;
|
|
37
|
+
|
|
38
|
+
// Rate limit: 3 requests per 300 seconds (5 minutes) per IP
|
|
39
|
+
const ip = getClientIp(request);
|
|
40
|
+
const rateLimit = await checkRateLimit(dineway.db, ip, "magic-link/send", 3, 300);
|
|
41
|
+
if (!rateLimit.allowed) {
|
|
42
|
+
// Return success-shaped response to avoid revealing rate limit
|
|
43
|
+
// (which could leak information about email enumeration attempts)
|
|
44
|
+
return apiSuccess({
|
|
45
|
+
success: true,
|
|
46
|
+
message: "If an account exists for this email, a magic link has been sent.",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check if email pipeline is available
|
|
51
|
+
if (!dineway.email?.isAvailable()) {
|
|
52
|
+
return apiError(
|
|
53
|
+
"EMAIL_NOT_CONFIGURED",
|
|
54
|
+
"Email is not configured. Magic link authentication requires an email provider.",
|
|
55
|
+
503,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Build magic link config using stored site URL (not request Host header)
|
|
60
|
+
const options = new OptionsRepository(dineway.db);
|
|
61
|
+
const baseUrl = await getSiteBaseUrl(dineway.db, request);
|
|
62
|
+
const siteName = (await options.get<string>("dineway:site_title")) ?? "Dineway";
|
|
63
|
+
|
|
64
|
+
const config: MagicLinkConfig = {
|
|
65
|
+
baseUrl,
|
|
66
|
+
siteName,
|
|
67
|
+
email: (message) => dineway.email!.send(message, "system"),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Send magic link (silently fails if user doesn't exist)
|
|
71
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
72
|
+
await sendMagicLink(config, adapter, body.email.toLowerCase());
|
|
73
|
+
|
|
74
|
+
// Always return success to avoid revealing if email exists
|
|
75
|
+
return apiSuccess({
|
|
76
|
+
success: true,
|
|
77
|
+
message: "If an account exists for this email, a magic link has been sent.",
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error("Magic link send error:", error);
|
|
81
|
+
|
|
82
|
+
// Still return success to avoid revealing information
|
|
83
|
+
// Log the error but don't expose it to the client
|
|
84
|
+
return apiSuccess({
|
|
85
|
+
success: true,
|
|
86
|
+
message: "If an account exists for this email, a magic link has been sent.",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /_dineway/api/auth/me
|
|
3
|
+
*
|
|
4
|
+
* Returns the current authenticated user's info.
|
|
5
|
+
* Used by the admin UI to display user info in the header.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { APIRoute } from "astro";
|
|
9
|
+
|
|
10
|
+
export const prerender = false;
|
|
11
|
+
|
|
12
|
+
import { apiError, apiSuccess } from "#api/error.js";
|
|
13
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
14
|
+
import { authMeActionBody } from "#api/schemas.js";
|
|
15
|
+
|
|
16
|
+
export const GET: APIRoute = async ({ locals, session }) => {
|
|
17
|
+
const { user } = locals;
|
|
18
|
+
|
|
19
|
+
if (!user) {
|
|
20
|
+
return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check if this is the user's first login (for welcome modal)
|
|
24
|
+
// We track this in the session to show the modal only once
|
|
25
|
+
const hasSeenWelcome = await session?.get("hasSeenWelcome");
|
|
26
|
+
const isFirstLogin = !hasSeenWelcome;
|
|
27
|
+
|
|
28
|
+
// Return safe user info (no sensitive data)
|
|
29
|
+
return apiSuccess({
|
|
30
|
+
id: user.id,
|
|
31
|
+
email: user.email,
|
|
32
|
+
name: user.name,
|
|
33
|
+
role: user.role,
|
|
34
|
+
avatarUrl: user.avatarUrl,
|
|
35
|
+
isFirstLogin,
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* POST /_dineway/api/auth/me
|
|
41
|
+
*
|
|
42
|
+
* Mark that the user has seen the welcome modal.
|
|
43
|
+
*/
|
|
44
|
+
export const POST: APIRoute = async ({ request, locals, session }) => {
|
|
45
|
+
const { user } = locals;
|
|
46
|
+
|
|
47
|
+
if (!user) {
|
|
48
|
+
return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const body = await parseBody(request, authMeActionBody);
|
|
52
|
+
if (isParseError(body)) return body;
|
|
53
|
+
|
|
54
|
+
if (body.action === "dismissWelcome") {
|
|
55
|
+
session?.set("hasSeenWelcome", true);
|
|
56
|
+
return apiSuccess({ success: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return apiError("UNKNOWN_ACTION", "Unknown action", 400);
|
|
60
|
+
};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /_dineway/api/auth/oauth/[provider]/callback
|
|
3
|
+
*
|
|
4
|
+
* Handle OAuth callback from provider
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
|
|
9
|
+
export const prerender = false;
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
handleOAuthCallback,
|
|
13
|
+
OAuthError,
|
|
14
|
+
Role,
|
|
15
|
+
type OAuthConsumerConfig,
|
|
16
|
+
type RoleLevel,
|
|
17
|
+
} from "@dineway-ai/auth";
|
|
18
|
+
import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
19
|
+
|
|
20
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
21
|
+
import { createOAuthStateStore } from "#auth/oauth-state-store.js";
|
|
22
|
+
|
|
23
|
+
type ProviderName = "github" | "google";
|
|
24
|
+
|
|
25
|
+
const VALID_PROVIDERS = new Set<string>(["github", "google"]);
|
|
26
|
+
|
|
27
|
+
function isValidProvider(provider: string): provider is ProviderName {
|
|
28
|
+
return VALID_PROVIDERS.has(provider);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Safely extract a string value from an env-like record */
|
|
32
|
+
function envString(env: Record<string, unknown>, ...keys: string[]): string | undefined {
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
const val = env[key];
|
|
35
|
+
if (typeof val === "string" && val) return val;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get OAuth config from environment variables
|
|
42
|
+
*/
|
|
43
|
+
function getOAuthConfig(env: Record<string, unknown>): OAuthConsumerConfig["providers"] {
|
|
44
|
+
const providers: OAuthConsumerConfig["providers"] = {};
|
|
45
|
+
|
|
46
|
+
// GitHub
|
|
47
|
+
const githubClientId = envString(env, "DINEWAY_OAUTH_GITHUB_CLIENT_ID", "GITHUB_CLIENT_ID");
|
|
48
|
+
const githubClientSecret = envString(
|
|
49
|
+
env,
|
|
50
|
+
"DINEWAY_OAUTH_GITHUB_CLIENT_SECRET",
|
|
51
|
+
"GITHUB_CLIENT_SECRET",
|
|
52
|
+
);
|
|
53
|
+
if (githubClientId && githubClientSecret) {
|
|
54
|
+
providers.github = {
|
|
55
|
+
clientId: githubClientId,
|
|
56
|
+
clientSecret: githubClientSecret,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Google
|
|
61
|
+
const googleClientId = envString(env, "DINEWAY_OAUTH_GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_ID");
|
|
62
|
+
const googleClientSecret = envString(
|
|
63
|
+
env,
|
|
64
|
+
"DINEWAY_OAUTH_GOOGLE_CLIENT_SECRET",
|
|
65
|
+
"GOOGLE_CLIENT_SECRET",
|
|
66
|
+
);
|
|
67
|
+
if (googleClientId && googleClientSecret) {
|
|
68
|
+
providers.google = {
|
|
69
|
+
clientId: googleClientId,
|
|
70
|
+
clientSecret: googleClientSecret,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return providers;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const GET: APIRoute = async ({ params, request, locals, session, redirect }) => {
|
|
78
|
+
const { dineway } = locals;
|
|
79
|
+
const provider = params.provider;
|
|
80
|
+
|
|
81
|
+
// Validate provider
|
|
82
|
+
if (!provider || !isValidProvider(provider)) {
|
|
83
|
+
return redirect(
|
|
84
|
+
`/_dineway/admin/login?error=invalid_provider&message=${encodeURIComponent("Invalid OAuth provider")}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!dineway?.db) {
|
|
89
|
+
return redirect(
|
|
90
|
+
`/_dineway/admin/login?error=server_error&message=${encodeURIComponent("Database not configured")}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const url = new URL(request.url);
|
|
95
|
+
const code = url.searchParams.get("code");
|
|
96
|
+
const state = url.searchParams.get("state");
|
|
97
|
+
const error = url.searchParams.get("error");
|
|
98
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
99
|
+
|
|
100
|
+
// Handle OAuth errors from provider
|
|
101
|
+
if (error) {
|
|
102
|
+
const message = errorDescription || error;
|
|
103
|
+
return redirect(
|
|
104
|
+
`/_dineway/admin/login?error=oauth_denied&message=${encodeURIComponent(message)}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Validate required params
|
|
109
|
+
if (!code || !state) {
|
|
110
|
+
return redirect(
|
|
111
|
+
`/_dineway/admin/login?error=invalid_callback&message=${encodeURIComponent("Missing code or state parameter")}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Get OAuth providers from the adapter/runtime env when available,
|
|
117
|
+
// otherwise fall back to the Node build env.
|
|
118
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- some adapters inject locals.runtime.env at runtime; App.Locals intentionally stays generic
|
|
119
|
+
const runtimeLocals = locals as unknown as { runtime?: { env?: Record<string, unknown> } };
|
|
120
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- import.meta.env is typed as ImportMetaEnv but we need Record<string, unknown> for getOAuthConfig
|
|
121
|
+
const env = runtimeLocals.runtime?.env ?? (import.meta.env as Record<string, unknown>);
|
|
122
|
+
const providers = getOAuthConfig(env);
|
|
123
|
+
|
|
124
|
+
if (!providers[provider]) {
|
|
125
|
+
return redirect(
|
|
126
|
+
`/_dineway/admin/login?error=provider_not_configured&message=${encodeURIComponent(`OAuth provider ${provider} is not configured`)}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const config: OAuthConsumerConfig = {
|
|
131
|
+
baseUrl: `${getPublicOrigin(url, dineway?.config)}/_dineway`,
|
|
132
|
+
providers,
|
|
133
|
+
canSelfSignup: async (email: string) => {
|
|
134
|
+
// Extract domain from email
|
|
135
|
+
const domain = email.split("@")[1]?.toLowerCase();
|
|
136
|
+
if (!domain) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check allowed_domains table for a matching, enabled entry
|
|
141
|
+
const entry = await dineway.db
|
|
142
|
+
.selectFrom("allowed_domains")
|
|
143
|
+
.selectAll()
|
|
144
|
+
.where("domain", "=", domain)
|
|
145
|
+
.where("enabled", "=", 1)
|
|
146
|
+
.executeTakeFirst();
|
|
147
|
+
|
|
148
|
+
if (!entry) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Map the stored role level to the Role enum
|
|
153
|
+
const roleLevel = entry.default_role;
|
|
154
|
+
const roleMap: Record<number, RoleLevel> = {
|
|
155
|
+
50: Role.ADMIN,
|
|
156
|
+
40: Role.EDITOR,
|
|
157
|
+
30: Role.AUTHOR,
|
|
158
|
+
20: Role.CONTRIBUTOR,
|
|
159
|
+
10: Role.SUBSCRIBER,
|
|
160
|
+
};
|
|
161
|
+
const role = roleMap[roleLevel] ?? Role.CONTRIBUTOR;
|
|
162
|
+
if (!roleMap[roleLevel]) {
|
|
163
|
+
console.warn(
|
|
164
|
+
`[oauth] Unknown role level ${roleLevel} for domain ${domain}, defaulting to CONTRIBUTOR`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { allowed: true, role };
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
173
|
+
const stateStore = createOAuthStateStore(dineway.db);
|
|
174
|
+
|
|
175
|
+
const user = await handleOAuthCallback(config, adapter, provider, code, state, stateStore);
|
|
176
|
+
|
|
177
|
+
// Create session
|
|
178
|
+
if (session) {
|
|
179
|
+
session.set("user", { id: user.id });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Redirect to admin dashboard
|
|
183
|
+
return redirect("/_dineway/admin");
|
|
184
|
+
} catch (callbackError) {
|
|
185
|
+
console.error("OAuth callback error:", callbackError);
|
|
186
|
+
|
|
187
|
+
let message = "Authentication failed";
|
|
188
|
+
let errorCode = "oauth_error";
|
|
189
|
+
|
|
190
|
+
if (callbackError instanceof OAuthError) {
|
|
191
|
+
errorCode = callbackError.code;
|
|
192
|
+
|
|
193
|
+
// Map all error codes to user-friendly messages (never expose raw error.message)
|
|
194
|
+
switch (callbackError.code) {
|
|
195
|
+
case "invalid_state":
|
|
196
|
+
message = "OAuth session expired or invalid. Please try again.";
|
|
197
|
+
break;
|
|
198
|
+
case "signup_not_allowed":
|
|
199
|
+
message = "Self-signup is not allowed for your email. Please contact an administrator.";
|
|
200
|
+
break;
|
|
201
|
+
case "user_not_found":
|
|
202
|
+
message = "Your account was not found. It may have been deleted.";
|
|
203
|
+
break;
|
|
204
|
+
case "token_exchange_failed":
|
|
205
|
+
message = "Failed to complete authentication. Please try again.";
|
|
206
|
+
break;
|
|
207
|
+
case "profile_fetch_failed":
|
|
208
|
+
message = "Failed to retrieve your profile. Please try again.";
|
|
209
|
+
break;
|
|
210
|
+
default:
|
|
211
|
+
message = "Authentication failed. Please try again.";
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// For generic errors, keep the default "Authentication failed" message
|
|
216
|
+
|
|
217
|
+
return redirect(
|
|
218
|
+
`/_dineway/admin/login?error=${errorCode}&message=${encodeURIComponent(message)}`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /_dineway/api/auth/oauth/[provider]
|
|
3
|
+
*
|
|
4
|
+
* Start OAuth flow - redirects to provider authorization URL
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { APIRoute } from "astro";
|
|
8
|
+
|
|
9
|
+
export const prerender = false;
|
|
10
|
+
|
|
11
|
+
import { createAuthorizationUrl, type OAuthConsumerConfig } from "@dineway-ai/auth";
|
|
12
|
+
|
|
13
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
14
|
+
import { createOAuthStateStore } from "#auth/oauth-state-store.js";
|
|
15
|
+
|
|
16
|
+
type ProviderName = "github" | "google";
|
|
17
|
+
|
|
18
|
+
const VALID_PROVIDERS = new Set<string>(["github", "google"]);
|
|
19
|
+
|
|
20
|
+
function isValidProvider(provider: string): provider is ProviderName {
|
|
21
|
+
return VALID_PROVIDERS.has(provider);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Safely extract a string value from an env-like record */
|
|
25
|
+
function envString(env: Record<string, unknown>, ...keys: string[]): string | undefined {
|
|
26
|
+
for (const key of keys) {
|
|
27
|
+
const val = env[key];
|
|
28
|
+
if (typeof val === "string" && val) return val;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get OAuth config from environment variables
|
|
35
|
+
*/
|
|
36
|
+
function getOAuthConfig(env: Record<string, unknown>): OAuthConsumerConfig["providers"] {
|
|
37
|
+
const providers: OAuthConsumerConfig["providers"] = {};
|
|
38
|
+
|
|
39
|
+
// GitHub
|
|
40
|
+
const githubClientId = envString(env, "DINEWAY_OAUTH_GITHUB_CLIENT_ID", "GITHUB_CLIENT_ID");
|
|
41
|
+
const githubClientSecret = envString(
|
|
42
|
+
env,
|
|
43
|
+
"DINEWAY_OAUTH_GITHUB_CLIENT_SECRET",
|
|
44
|
+
"GITHUB_CLIENT_SECRET",
|
|
45
|
+
);
|
|
46
|
+
if (githubClientId && githubClientSecret) {
|
|
47
|
+
providers.github = {
|
|
48
|
+
clientId: githubClientId,
|
|
49
|
+
clientSecret: githubClientSecret,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Google
|
|
54
|
+
const googleClientId = envString(env, "DINEWAY_OAUTH_GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_ID");
|
|
55
|
+
const googleClientSecret = envString(
|
|
56
|
+
env,
|
|
57
|
+
"DINEWAY_OAUTH_GOOGLE_CLIENT_SECRET",
|
|
58
|
+
"GOOGLE_CLIENT_SECRET",
|
|
59
|
+
);
|
|
60
|
+
if (googleClientId && googleClientSecret) {
|
|
61
|
+
providers.google = {
|
|
62
|
+
clientId: googleClientId,
|
|
63
|
+
clientSecret: googleClientSecret,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return providers;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const GET: APIRoute = async ({ params, request, locals, redirect }) => {
|
|
71
|
+
const { dineway } = locals;
|
|
72
|
+
const provider = params.provider;
|
|
73
|
+
|
|
74
|
+
// Validate provider
|
|
75
|
+
if (!provider || !isValidProvider(provider)) {
|
|
76
|
+
return redirect(
|
|
77
|
+
`/_dineway/admin/login?error=invalid_provider&message=${encodeURIComponent("Invalid OAuth provider")}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!dineway?.db) {
|
|
82
|
+
return redirect(
|
|
83
|
+
`/_dineway/admin/login?error=server_error&message=${encodeURIComponent("Database not configured")}`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const url = new URL(request.url);
|
|
89
|
+
|
|
90
|
+
// Get OAuth providers from the adapter/runtime env when available,
|
|
91
|
+
// otherwise fall back to the Node build env.
|
|
92
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- some adapters inject locals.runtime.env at runtime; App.Locals intentionally stays generic
|
|
93
|
+
const runtimeLocals = locals as unknown as { runtime?: { env?: Record<string, unknown> } };
|
|
94
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- import.meta.env is typed as ImportMetaEnv but we need Record<string, unknown> for getOAuthConfig
|
|
95
|
+
const env = runtimeLocals.runtime?.env ?? (import.meta.env as Record<string, unknown>);
|
|
96
|
+
const providers = getOAuthConfig(env);
|
|
97
|
+
|
|
98
|
+
if (!providers[provider]) {
|
|
99
|
+
return redirect(
|
|
100
|
+
`/_dineway/admin/login?error=provider_not_configured&message=${encodeURIComponent(`OAuth provider ${provider} is not configured`)}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const config: OAuthConsumerConfig = {
|
|
105
|
+
baseUrl: `${getPublicOrigin(url, dineway?.config)}/_dineway`,
|
|
106
|
+
providers,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const stateStore = createOAuthStateStore(dineway.db);
|
|
110
|
+
|
|
111
|
+
const { url: authUrl } = await createAuthorizationUrl(config, provider, stateStore);
|
|
112
|
+
|
|
113
|
+
return redirect(authUrl);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error("OAuth initiation error:", error);
|
|
116
|
+
return redirect(
|
|
117
|
+
`/_dineway/admin/login?error=oauth_error&message=${encodeURIComponent("Failed to start OAuth flow. Please try again.")}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
};
|