emdash 0.6.0 → 1.0.0
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/dist/{adapters-Di31kZ28.d.mts → adapters-BKSf3T9R.d.mts} +1 -1
- package/dist/{adapters-Di31kZ28.d.mts.map → adapters-BKSf3T9R.d.mts.map} +1 -1
- package/dist/{apply-B4MsLM-w.mjs → apply-x0eMK1lX.mjs} +186 -28
- package/dist/apply-x0eMK1lX.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +92 -17
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +22 -2
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +2 -2
- package/dist/astro/middleware/request-context.mjs +7 -2
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +263 -74
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +25 -8
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-C4OVd8b3.mjs → byline-Chbr2GoP.mjs} +3 -3
- package/dist/byline-Chbr2GoP.mjs.map +1 -0
- package/dist/{bylines-hPTW79hw.mjs → bylines-CRNsVG88.mjs} +4 -4
- package/dist/{bylines-hPTW79hw.mjs.map → bylines-CRNsVG88.mjs.map} +1 -1
- package/dist/cli/index.mjs +17 -13
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/{content-BsBoyj8G.mjs → content-BcQPYxdV.mjs} +39 -15
- package/dist/content-BcQPYxdV.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{db-errors-D0UT85nC.mjs → db-errors-l1Qh2RPR.mjs} +1 -1
- package/dist/{db-errors-D0UT85nC.mjs.map → db-errors-l1Qh2RPR.mjs.map} +1 -1
- package/dist/{default-CME5YdZ3.mjs → default-DCVqE5ib.mjs} +1 -1
- package/dist/{default-CME5YdZ3.mjs.map → default-DCVqE5ib.mjs.map} +1 -1
- package/dist/{error-CiYn9yDu.mjs → error-zG5T1UGA.mjs} +1 -1
- package/dist/error-zG5T1UGA.mjs.map +1 -0
- package/dist/{index-BYv0mB9g.d.mts → index-DIb-CzNx.d.mts} +232 -15
- package/dist/index-DIb-CzNx.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +23 -21
- package/dist/{load-CBcmDIot.mjs → load-CyEoextb.mjs} +1 -1
- package/dist/{load-CBcmDIot.mjs.map → load-CyEoextb.mjs.map} +1 -1
- package/dist/{loader-DeiBJEMe.mjs → loader-CndGj8kM.mjs} +8 -6
- package/dist/loader-CndGj8kM.mjs.map +1 -0
- package/dist/{manifest-schema-V30qsMft.mjs → manifest-schema-DH9xhc6t.mjs} +13 -1
- package/dist/manifest-schema-DH9xhc6t.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/media/local-runtime.mjs +2 -2
- package/dist/{media-DqHVh136.mjs → media-D8FbNsl0.mjs} +4 -7
- package/dist/media-D8FbNsl0.mjs.map +1 -0
- package/dist/{mode-CpNnGkPz.mjs → mode-BnAOqItE.mjs} +1 -1
- package/dist/mode-BnAOqItE.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/placeholder-C-fk5hYI.mjs.map +1 -1
- package/dist/{placeholder-tzpqGWII.d.mts → placeholder-D29tWZ7o.d.mts} +1 -1
- package/dist/{placeholder-tzpqGWII.d.mts.map → placeholder-D29tWZ7o.d.mts.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-Bk_3vKvU.mjs → query-fqEdLFms.mjs} +9 -9
- package/dist/{query-Bk_3vKvU.mjs.map → query-fqEdLFms.mjs.map} +1 -1
- package/dist/{redirect-7lGhLBNZ.mjs → redirect-D_pshWdf.mjs} +69 -13
- package/dist/redirect-D_pshWdf.mjs.map +1 -0
- package/dist/{registry-Ci3WxVAr.mjs → registry-C3Mr0ODu.mjs} +33 -9
- package/dist/registry-C3Mr0ODu.mjs.map +1 -0
- package/dist/{request-cache-DiR961CV.mjs → request-cache-Ci7f5pBb.mjs} +1 -1
- package/dist/request-cache-Ci7f5pBb.mjs.map +1 -0
- package/dist/{runner-Fl2NcUUz.d.mts → runner-OURCaApa.d.mts} +2 -2
- package/dist/{runner-Fl2NcUUz.d.mts.map → runner-OURCaApa.d.mts.map} +1 -1
- package/dist/{runner-Cd-_WyDo.mjs → runner-tQ7BJ4T7.mjs} +211 -134
- package/dist/runner-tQ7BJ4T7.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-DI4bM2w9.mjs → search-BoZYFuUk.mjs} +339 -102
- package/dist/search-BoZYFuUk.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +12 -12
- 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 +1 -1
- package/dist/storage/s3.d.mts.map +1 -1
- package/dist/storage/s3.mjs +4 -4
- package/dist/storage/s3.mjs.map +1 -1
- package/dist/{taxonomies-DbrKzDju.mjs → taxonomies-B4IAshV8.mjs} +5 -5
- package/dist/{taxonomies-DbrKzDju.mjs.map → taxonomies-B4IAshV8.mjs.map} +1 -1
- package/dist/{tokens-BFPFx3CA.mjs → tokens-D9vnZqYS.mjs} +1 -1
- package/dist/{tokens-BFPFx3CA.mjs.map → tokens-D9vnZqYS.mjs.map} +1 -1
- package/dist/{transport-BykRfpyy.mjs → transport-C9ugt2Nr.mjs} +1 -1
- package/dist/{transport-BykRfpyy.mjs.map → transport-C9ugt2Nr.mjs.map} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts → transport-CUnEL3Vs.d.mts} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts.map → transport-CUnEL3Vs.d.mts.map} +1 -1
- package/dist/types-BIgulNsW.mjs +68 -0
- package/dist/types-BIgulNsW.mjs.map +1 -0
- package/dist/{types-DDS4MxsT.mjs → types-Bm1dn-q3.mjs} +1 -1
- package/dist/{types-DDS4MxsT.mjs.map → types-Bm1dn-q3.mjs.map} +1 -1
- package/dist/{types-CnZYHyLW.d.mts → types-BmPPSUEx.d.mts} +1 -1
- package/dist/{types-CnZYHyLW.d.mts.map → types-BmPPSUEx.d.mts.map} +1 -1
- package/dist/{types-6CUZRrZP.d.mts → types-BrA0xf5I.d.mts} +24 -2
- package/dist/{types-6CUZRrZP.d.mts.map → types-BrA0xf5I.d.mts.map} +1 -1
- package/dist/{types-8xrvl_68.d.mts → types-CS8FIX7L.d.mts} +10 -1
- package/dist/{types-8xrvl_68.d.mts.map → types-CS8FIX7L.d.mts.map} +1 -1
- package/dist/{types-BH2L167P.mjs → types-CgqmmMJB.mjs} +1 -1
- package/dist/{types-BH2L167P.mjs.map → types-CgqmmMJB.mjs.map} +1 -1
- package/dist/{types-CFWjXmus.d.mts → types-DIMwPFub.d.mts} +1 -1
- package/dist/{types-CFWjXmus.d.mts.map → types-DIMwPFub.d.mts.map} +1 -1
- package/dist/{types-DgrIP0tF.d.mts → types-i36XcA_X.d.mts} +49 -6
- package/dist/types-i36XcA_X.d.mts.map +1 -0
- package/dist/{validate-CqsNItbt.mjs → validate-CxVsLehf.mjs} +2 -2
- package/dist/{validate-CqsNItbt.mjs.map → validate-CxVsLehf.mjs.map} +1 -1
- package/dist/{validate-CaLH1Ia2.d.mts → validate-DHxmpFJt.d.mts} +4 -4
- package/dist/{validate-CaLH1Ia2.d.mts.map → validate-DHxmpFJt.d.mts.map} +1 -1
- package/dist/validation-C-ZpN2GI.mjs +144 -0
- package/dist/validation-C-ZpN2GI.mjs.map +1 -0
- package/dist/version-DJrV1K0M.mjs +7 -0
- package/dist/{version-Uaf2ynPX.mjs.map → version-DJrV1K0M.mjs.map} +1 -1
- package/dist/zod-generator-CpwccCIv.mjs +132 -0
- package/dist/zod-generator-CpwccCIv.mjs.map +1 -0
- package/package.json +19 -6
- package/src/api/auth-storage.ts +37 -0
- package/src/api/error.ts +6 -0
- package/src/api/errors.ts +8 -0
- package/src/api/handlers/comments.ts +13 -0
- package/src/api/handlers/content.ts +124 -3
- package/src/api/handlers/index.ts +2 -0
- package/src/api/handlers/media.ts +8 -1
- package/src/api/handlers/menus.ts +160 -21
- package/src/api/handlers/redirects.ts +16 -3
- package/src/api/handlers/sections.ts +8 -1
- package/src/api/handlers/taxonomies.ts +128 -16
- package/src/api/handlers/validation.ts +212 -0
- package/src/api/openapi/document.ts +4 -1
- package/src/api/public-url.ts +6 -3
- package/src/api/route-utils.ts +14 -0
- package/src/api/schemas/common.ts +1 -1
- package/src/api/schemas/content.ts +8 -0
- package/src/api/schemas/setup.ts +8 -0
- package/src/api/schemas/widgets.ts +12 -10
- package/src/api/setup-complete.ts +40 -0
- package/src/astro/integration/font-provider.ts +3 -1
- package/src/astro/integration/index.ts +15 -2
- package/src/astro/integration/routes.ts +28 -0
- package/src/astro/integration/runtime.ts +74 -2
- package/src/astro/integration/virtual-modules.ts +41 -0
- package/src/astro/integration/vite-config.ts +43 -12
- package/src/astro/middleware/auth.ts +21 -0
- package/src/astro/middleware.ts +18 -1
- package/src/astro/routes/PluginRegistry.tsx +10 -1
- package/src/astro/routes/admin.astro +14 -7
- package/src/astro/routes/api/auth/magic-link/send.ts +2 -1
- package/src/astro/routes/api/auth/mode.ts +57 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +23 -3
- package/src/astro/routes/api/auth/oauth/[provider].ts +10 -4
- package/src/astro/routes/api/auth/passkey/options.ts +2 -1
- package/src/astro/routes/api/auth/signup/request.ts +26 -8
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +10 -6
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +26 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +30 -2
- package/src/astro/routes/api/content/[collection]/index.ts +20 -10
- package/src/astro/routes/api/content/[collection]/trash.ts +1 -1
- package/src/astro/routes/api/import/wordpress/media.ts +2 -7
- package/src/astro/routes/api/import/wordpress/prepare.ts +10 -0
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +4 -3
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +4 -3
- package/src/astro/routes/api/manifest.ts +7 -0
- package/src/astro/routes/api/oauth/device/code.ts +2 -1
- package/src/astro/routes/api/oauth/device/token.ts +2 -1
- package/src/astro/routes/api/settings/email.ts +4 -9
- package/src/astro/routes/api/setup/admin-verify.ts +30 -5
- package/src/astro/routes/api/setup/admin.ts +38 -8
- package/src/astro/routes/api/setup/index.ts +7 -4
- package/src/astro/routes/api/setup/status.ts +3 -1
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +4 -1
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +4 -1
- package/src/astro/routes/api/widget-areas/[name].ts +4 -1
- package/src/astro/routes/api/widget-areas/index.ts +4 -1
- package/src/astro/types.ts +18 -0
- package/src/auth/mode.ts +15 -3
- package/src/auth/providers/github-admin.tsx +29 -0
- package/src/auth/providers/github.ts +31 -0
- package/src/auth/providers/google-admin.tsx +44 -0
- package/src/auth/providers/google.ts +31 -0
- package/src/auth/rate-limit.ts +50 -22
- package/src/auth/setup-nonce.ts +22 -0
- package/src/auth/trusted-proxy.ts +92 -0
- package/src/auth/types.ts +114 -4
- package/src/cli/commands/bundle.ts +3 -1
- package/src/components/EmDashImage.astro +7 -6
- package/src/components/Gallery.astro +5 -3
- package/src/components/Image.astro +8 -3
- package/src/components/InlinePortableTextEditor.tsx +2 -1
- package/src/components/LiveSearch.astro +5 -14
- package/src/database/migrations/035_bounded_404_log.ts +112 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/audit.ts +6 -8
- package/src/database/repositories/byline.ts +6 -8
- package/src/database/repositories/comment.ts +12 -16
- package/src/database/repositories/content.ts +79 -40
- package/src/database/repositories/index.ts +1 -1
- package/src/database/repositories/media.ts +10 -13
- package/src/database/repositories/options.ts +25 -0
- package/src/database/repositories/plugin-storage.ts +4 -6
- package/src/database/repositories/redirect.ts +123 -24
- package/src/database/repositories/taxonomy.ts +14 -3
- package/src/database/repositories/types.ts +57 -8
- package/src/database/repositories/user.ts +6 -8
- package/src/database/types.ts +9 -0
- package/src/emdash-runtime.ts +309 -91
- package/src/import/registry.ts +4 -3
- package/src/import/ssrf.ts +253 -12
- package/src/index.ts +5 -1
- package/src/loader.ts +6 -5
- package/src/mcp/server.ts +753 -107
- package/src/media/normalize.ts +1 -1
- package/src/media/url.ts +78 -0
- package/src/plugins/context.ts +15 -3
- package/src/plugins/email-console.ts +10 -3
- package/src/plugins/hooks.ts +11 -0
- package/src/plugins/manager.ts +6 -0
- package/src/plugins/manifest-schema.ts +12 -0
- package/src/plugins/request-meta.ts +66 -15
- package/src/plugins/routes.ts +3 -1
- package/src/plugins/types.ts +23 -2
- package/src/query.ts +1 -1
- package/src/request-cache.ts +3 -0
- package/src/schema/registry.ts +41 -5
- package/src/search/fts-manager.ts +0 -2
- package/src/search/query.ts +111 -26
- package/src/search/types.ts +8 -1
- package/src/sections/index.ts +7 -9
- package/src/seed/apply.ts +26 -0
- package/src/storage/s3.ts +12 -6
- package/src/virtual-modules.d.ts +21 -1
- package/src/visual-editing/toolbar.ts +6 -1
- package/src/widgets/index.ts +1 -1
- package/dist/apply-B4MsLM-w.mjs.map +0 -1
- package/dist/byline-C4OVd8b3.mjs.map +0 -1
- package/dist/content-BsBoyj8G.mjs.map +0 -1
- package/dist/error-CiYn9yDu.mjs.map +0 -1
- package/dist/index-BYv0mB9g.d.mts.map +0 -1
- package/dist/loader-DeiBJEMe.mjs.map +0 -1
- package/dist/manifest-schema-V30qsMft.mjs.map +0 -1
- package/dist/media-DqHVh136.mjs.map +0 -1
- package/dist/mode-CpNnGkPz.mjs.map +0 -1
- package/dist/redirect-7lGhLBNZ.mjs.map +0 -1
- package/dist/registry-Ci3WxVAr.mjs.map +0 -1
- package/dist/request-cache-DiR961CV.mjs.map +0 -1
- package/dist/runner-Cd-_WyDo.mjs.map +0 -1
- package/dist/search-DI4bM2w9.mjs.map +0 -1
- package/dist/types-CMMN0pNg.mjs +0 -31
- package/dist/types-CMMN0pNg.mjs.map +0 -1
- package/dist/types-DgrIP0tF.d.mts.map +0 -1
- package/dist/version-Uaf2ynPX.mjs +0 -7
|
@@ -46,6 +46,12 @@ export function injectCoreRoutes(injectRoute: InjectRoute): void {
|
|
|
46
46
|
entrypoint: resolveRoute("api/manifest.ts"),
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
// Auth mode endpoint (public — used by the login page to pick the right UI)
|
|
50
|
+
injectRoute({
|
|
51
|
+
pattern: "/_emdash/api/auth/mode",
|
|
52
|
+
entrypoint: resolveRoute("api/auth/mode.ts"),
|
|
53
|
+
});
|
|
54
|
+
|
|
49
55
|
injectRoute({
|
|
50
56
|
pattern: "/_emdash/api/dashboard",
|
|
51
57
|
entrypoint: resolveRoute("api/dashboard.ts"),
|
|
@@ -747,6 +753,28 @@ export function injectMcpRoute(injectRoute: InjectRoute): void {
|
|
|
747
753
|
});
|
|
748
754
|
}
|
|
749
755
|
|
|
756
|
+
/**
|
|
757
|
+
* Injects routes from pluggable auth providers.
|
|
758
|
+
*
|
|
759
|
+
* Each provider declares the routes it needs in its `AuthProviderDescriptor.routes` array.
|
|
760
|
+
* Routes are injected at build time so Vite can bundle them.
|
|
761
|
+
*/
|
|
762
|
+
export function injectAuthProviderRoutes(
|
|
763
|
+
injectRoute: InjectRoute,
|
|
764
|
+
providers: Array<{ routes?: Array<{ pattern: string; entrypoint: string }> }>,
|
|
765
|
+
): void {
|
|
766
|
+
for (const provider of providers) {
|
|
767
|
+
if (provider.routes) {
|
|
768
|
+
for (const route of provider.routes) {
|
|
769
|
+
injectRoute({
|
|
770
|
+
pattern: route.pattern,
|
|
771
|
+
entrypoint: route.entrypoint,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
750
778
|
/**
|
|
751
779
|
* Injects passkey/oauth/magic-link auth routes.
|
|
752
780
|
* Only used when NOT using external auth.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* DO NOT import Node.js-only modules here (fs, path, module, etc.)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { AuthDescriptor } from "../../auth/types.js";
|
|
10
|
+
import type { AuthDescriptor, AuthProviderDescriptor } from "../../auth/types.js";
|
|
11
11
|
import type { DatabaseDescriptor } from "../../db/adapters.js";
|
|
12
12
|
import type { MediaProviderDescriptor } from "../../media/types.js";
|
|
13
13
|
import type { ResolvedPlugin } from "../../plugins/types.js";
|
|
@@ -222,6 +222,24 @@ export interface EmDashConfig {
|
|
|
222
222
|
*/
|
|
223
223
|
auth?: AuthDescriptor;
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Pluggable auth providers (login methods on the login page).
|
|
227
|
+
*
|
|
228
|
+
* Auth providers appear as options alongside passkey on the login page
|
|
229
|
+
* and setup wizard. Any provider can be used to create the initial
|
|
230
|
+
* admin account. Passkey is built-in; providers listed here are additive.
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```ts
|
|
234
|
+
* import { atproto } from "@emdash-cms/auth-atproto";
|
|
235
|
+
*
|
|
236
|
+
* emdash({
|
|
237
|
+
* authProviders: [atproto()],
|
|
238
|
+
* })
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
authProviders?: AuthProviderDescriptor[];
|
|
242
|
+
|
|
225
243
|
/**
|
|
226
244
|
* MCP (Model Context Protocol) server endpoint.
|
|
227
245
|
*
|
|
@@ -284,6 +302,32 @@ export interface EmDashConfig {
|
|
|
284
302
|
*/
|
|
285
303
|
siteUrl?: string;
|
|
286
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Headers to trust for client IP resolution when running behind a reverse
|
|
307
|
+
* proxy. The first header in this list that is present on the request
|
|
308
|
+
* wins. Applies to rate limiting for auth endpoints and comment
|
|
309
|
+
* submission.
|
|
310
|
+
*
|
|
311
|
+
* Common values:
|
|
312
|
+
* - `x-real-ip` — nginx, Caddy, Traefik
|
|
313
|
+
* - `fly-client-ip` — Fly.io
|
|
314
|
+
* - `x-forwarded-for` — generic (first entry is used)
|
|
315
|
+
*
|
|
316
|
+
* Only set this when you **control the reverse proxy**. Untrusted
|
|
317
|
+
* clients can set any header they like; trusting headers from an open
|
|
318
|
+
* network is an IP-spoofing vulnerability that defeats rate limiting.
|
|
319
|
+
*
|
|
320
|
+
* On Cloudflare the `cf` object on the request is used automatically —
|
|
321
|
+
* you normally don't need to set this. Leave unset (or empty) to
|
|
322
|
+
* preserve the default: IP is resolved only when the request came
|
|
323
|
+
* through Cloudflare's edge.
|
|
324
|
+
*
|
|
325
|
+
* Falls back to `EMDASH_TRUSTED_PROXY_HEADERS` env var (comma-separated)
|
|
326
|
+
* when this option is not set, so operators can configure at deploy
|
|
327
|
+
* time without touching the Astro config.
|
|
328
|
+
*/
|
|
329
|
+
trustedProxyHeaders?: string[];
|
|
330
|
+
|
|
287
331
|
/**
|
|
288
332
|
* Enable playground mode for ephemeral "try EmDash" sites.
|
|
289
333
|
*
|
|
@@ -378,13 +422,41 @@ export interface EmDashConfig {
|
|
|
378
422
|
* Additional Noto Sans script families to include.
|
|
379
423
|
*
|
|
380
424
|
* Available scripts: arabic, armenian, bengali, chinese-simplified,
|
|
381
|
-
* chinese-traditional, chinese-hongkong, devanagari, ethiopic,
|
|
425
|
+
* chinese-traditional, chinese-hongkong, devanagari, ethiopic, farsi,
|
|
382
426
|
* georgian, gujarati, gurmukhi, hebrew, japanese, kannada, khmer,
|
|
383
427
|
* korean, lao, malayalam, myanmar, oriya, sinhala, tamil, telugu,
|
|
384
428
|
* thai, tibetan.
|
|
385
429
|
*/
|
|
386
430
|
scripts?: string[];
|
|
387
431
|
};
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Admin UI branding (white-labeling).
|
|
435
|
+
*
|
|
436
|
+
* Overrides the default EmDash logo and name in the admin panel.
|
|
437
|
+
* Use this to white-label the CMS for agency or enterprise deployments.
|
|
438
|
+
* These settings are separate from the public site settings (title, logo,
|
|
439
|
+
* favicon) which remain available for SEO and front-end use.
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* ```ts
|
|
443
|
+
* emdash({
|
|
444
|
+
* admin: {
|
|
445
|
+
* logo: "/images/agency-logo.webp",
|
|
446
|
+
* siteName: "AgencyX CMS",
|
|
447
|
+
* favicon: "/favicon.ico",
|
|
448
|
+
* },
|
|
449
|
+
* })
|
|
450
|
+
* ```
|
|
451
|
+
*/
|
|
452
|
+
admin?: {
|
|
453
|
+
/** URL or path to a custom logo image for the admin UI (login page, sidebar). */
|
|
454
|
+
logo?: string;
|
|
455
|
+
/** Custom name displayed in the admin sidebar and browser tab. */
|
|
456
|
+
siteName?: string;
|
|
457
|
+
/** URL or path to a custom favicon for the admin panel. */
|
|
458
|
+
favicon?: string;
|
|
459
|
+
};
|
|
388
460
|
}
|
|
389
461
|
|
|
390
462
|
/**
|
|
@@ -10,6 +10,7 @@ import { readFileSync } from "node:fs";
|
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
12
|
|
|
13
|
+
import type { AuthProviderDescriptor } from "../../auth/types.js";
|
|
13
14
|
import type { MediaProviderDescriptor } from "../../media/types.js";
|
|
14
15
|
import { defaultSeed } from "../../seed/default.js";
|
|
15
16
|
import type { PluginDescriptor } from "./runtime.js";
|
|
@@ -47,6 +48,9 @@ export const RESOLVED_VIRTUAL_SANDBOXED_PLUGINS_ID = "\0" + VIRTUAL_SANDBOXED_PL
|
|
|
47
48
|
export const VIRTUAL_AUTH_ID = "virtual:emdash/auth";
|
|
48
49
|
export const RESOLVED_VIRTUAL_AUTH_ID = "\0" + VIRTUAL_AUTH_ID;
|
|
49
50
|
|
|
51
|
+
export const VIRTUAL_AUTH_PROVIDERS_ID = "virtual:emdash/auth-providers";
|
|
52
|
+
export const RESOLVED_VIRTUAL_AUTH_PROVIDERS_ID = "\0" + VIRTUAL_AUTH_PROVIDERS_ID;
|
|
53
|
+
|
|
50
54
|
export const VIRTUAL_MEDIA_PROVIDERS_ID = "virtual:emdash/media-providers";
|
|
51
55
|
export const RESOLVED_VIRTUAL_MEDIA_PROVIDERS_ID = "\0" + VIRTUAL_MEDIA_PROVIDERS_ID;
|
|
52
56
|
|
|
@@ -135,6 +139,43 @@ export const authenticate = _authenticate;
|
|
|
135
139
|
`;
|
|
136
140
|
}
|
|
137
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Generates the auth providers module.
|
|
144
|
+
*
|
|
145
|
+
* Statically imports each auth provider's `adminEntry` module and exports
|
|
146
|
+
* a registry keyed by provider ID. The admin UI uses this to render
|
|
147
|
+
* provider-specific login buttons/forms and setup steps.
|
|
148
|
+
*
|
|
149
|
+
* Follows the same pattern as `generateAdminRegistryModule()` for plugins.
|
|
150
|
+
*/
|
|
151
|
+
export function generateAuthProvidersModule(descriptors: AuthProviderDescriptor[]): string {
|
|
152
|
+
const withAdmin = descriptors.filter((d) => d.adminEntry);
|
|
153
|
+
|
|
154
|
+
if (withAdmin.length === 0) {
|
|
155
|
+
return `export const authProviders = {};`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const imports: string[] = [];
|
|
159
|
+
const entries: string[] = [];
|
|
160
|
+
|
|
161
|
+
withAdmin.forEach((descriptor, index) => {
|
|
162
|
+
const varName = `authProvider${index}`;
|
|
163
|
+
imports.push(`import * as ${varName} from ${JSON.stringify(descriptor.adminEntry)};`);
|
|
164
|
+
entries.push(
|
|
165
|
+
` ${JSON.stringify(descriptor.id)}: { ...${varName}, id: ${JSON.stringify(descriptor.id)}, label: ${JSON.stringify(descriptor.label)} },`,
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return `
|
|
170
|
+
// Auto-generated auth provider registry
|
|
171
|
+
${imports.join("\n")}
|
|
172
|
+
|
|
173
|
+
export const authProviders = {
|
|
174
|
+
${entries.join("\n")}
|
|
175
|
+
};
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
|
|
138
179
|
/**
|
|
139
180
|
* Generates the plugins module.
|
|
140
181
|
* Imports and instantiates all plugins at runtime.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
|
-
import { dirname, resolve } from "node:path";
|
|
10
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
|
|
13
13
|
import type { AstroConfig } from "astro";
|
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
RESOLVED_VIRTUAL_SANDBOXED_PLUGINS_ID,
|
|
33
33
|
VIRTUAL_AUTH_ID,
|
|
34
34
|
RESOLVED_VIRTUAL_AUTH_ID,
|
|
35
|
+
VIRTUAL_AUTH_PROVIDERS_ID,
|
|
36
|
+
RESOLVED_VIRTUAL_AUTH_PROVIDERS_ID,
|
|
35
37
|
VIRTUAL_MEDIA_PROVIDERS_ID,
|
|
36
38
|
RESOLVED_VIRTUAL_MEDIA_PROVIDERS_ID,
|
|
37
39
|
VIRTUAL_BLOCK_COMPONENTS_ID,
|
|
@@ -46,6 +48,7 @@ import {
|
|
|
46
48
|
generateDialectModule,
|
|
47
49
|
generateStorageModule,
|
|
48
50
|
generateAuthModule,
|
|
51
|
+
generateAuthProvidersModule,
|
|
49
52
|
generatePluginsModule,
|
|
50
53
|
generateAdminRegistryModule,
|
|
51
54
|
generateSandboxRunnerModule,
|
|
@@ -104,24 +107,34 @@ function resolveAdminDist(): string {
|
|
|
104
107
|
return dirname(adminPath);
|
|
105
108
|
}
|
|
106
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Check whether child is inside parent without relying on simple prefix checks.
|
|
112
|
+
*/
|
|
113
|
+
function isInside(parent: string, child: string): boolean {
|
|
114
|
+
const relativePath = relative(parent, child);
|
|
115
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
|
|
116
|
+
}
|
|
117
|
+
|
|
107
118
|
/**
|
|
108
119
|
* Resolve path to the admin package source directory.
|
|
109
|
-
* In dev mode, we alias @emdash-cms/admin to the source so
|
|
110
|
-
* directly — giving instant HMR instead of requiring a
|
|
120
|
+
* In dev mode inside this repo, we alias @emdash-cms/admin to the source so
|
|
121
|
+
* Vite processes it directly — giving instant HMR instead of requiring a
|
|
122
|
+
* rebuild + restart. External apps should use the built package surface.
|
|
111
123
|
*/
|
|
112
|
-
function resolveAdminSource(): string | undefined {
|
|
124
|
+
function resolveAdminSource(projectRoot: string): string | undefined {
|
|
113
125
|
const require = createRequire(import.meta.url);
|
|
114
126
|
const adminPath = require.resolve("@emdash-cms/admin");
|
|
115
127
|
// dist/index.js -> go up to package root, then into src/
|
|
116
128
|
const packageRoot = resolve(dirname(adminPath), "..");
|
|
129
|
+
const repoRoot = resolve(packageRoot, "..", "..");
|
|
117
130
|
const srcEntry = resolve(packageRoot, "src", "index.ts");
|
|
118
131
|
|
|
119
132
|
try {
|
|
120
|
-
if (existsSync(srcEntry)) {
|
|
133
|
+
if (existsSync(srcEntry) && isInside(repoRoot, projectRoot)) {
|
|
121
134
|
return resolve(packageRoot, "src");
|
|
122
135
|
}
|
|
123
136
|
} catch {
|
|
124
|
-
// Not in
|
|
137
|
+
// Not in local repo — fall back to dist
|
|
125
138
|
}
|
|
126
139
|
return undefined;
|
|
127
140
|
}
|
|
@@ -170,6 +183,9 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
|
|
|
170
183
|
if (id === VIRTUAL_AUTH_ID) {
|
|
171
184
|
return RESOLVED_VIRTUAL_AUTH_ID;
|
|
172
185
|
}
|
|
186
|
+
if (id === VIRTUAL_AUTH_PROVIDERS_ID) {
|
|
187
|
+
return RESOLVED_VIRTUAL_AUTH_PROVIDERS_ID;
|
|
188
|
+
}
|
|
173
189
|
if (id === VIRTUAL_MEDIA_PROVIDERS_ID) {
|
|
174
190
|
return RESOLVED_VIRTUAL_MEDIA_PROVIDERS_ID;
|
|
175
191
|
}
|
|
@@ -228,6 +244,10 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
|
|
|
228
244
|
}
|
|
229
245
|
return generateAuthModule(authDescriptor.entrypoint);
|
|
230
246
|
}
|
|
247
|
+
// Generate auth providers module (pluggable login methods)
|
|
248
|
+
if (id === RESOLVED_VIRTUAL_AUTH_PROVIDERS_ID) {
|
|
249
|
+
return generateAuthProvidersModule(resolvedConfig.authProviders ?? []);
|
|
250
|
+
}
|
|
231
251
|
// Generate media providers module
|
|
232
252
|
if (id === RESOLVED_VIRTUAL_MEDIA_PROVIDERS_ID) {
|
|
233
253
|
return generateMediaProvidersModule(resolvedConfig.mediaProviders ?? []);
|
|
@@ -281,12 +301,9 @@ export function createViteConfig(
|
|
|
281
301
|
const adminDistPath = resolveAdminDist();
|
|
282
302
|
const cloudflare = isCloudflareAdapter(options.astroConfig);
|
|
283
303
|
const isDev = command === "dev";
|
|
304
|
+
const projectRoot = fileURLToPath(options.astroConfig.root);
|
|
284
305
|
|
|
285
|
-
|
|
286
|
-
// CSS always comes from dist/ (pre-compiled by @tailwindcss/cli) since Tailwind's
|
|
287
|
-
// Vite plugin has native deps that don't bundle well. Run `pnpm dev` in packages/admin
|
|
288
|
-
// alongside the demo server to get CSS watch-rebuilds too.
|
|
289
|
-
const adminSourcePath = isDev ? resolveAdminSource() : undefined;
|
|
306
|
+
const adminSourcePath = isDev ? resolveAdminSource(projectRoot) : undefined;
|
|
290
307
|
const useSource = adminSourcePath !== undefined;
|
|
291
308
|
|
|
292
309
|
return {
|
|
@@ -308,6 +325,20 @@ export function createViteConfig(
|
|
|
308
325
|
alias: [
|
|
309
326
|
{ find: "@emdash-cms/admin/styles.css", replacement: resolve(adminDistPath, "styles.css") },
|
|
310
327
|
{ find: "@emdash-cms/admin", replacement: useSource ? adminSourcePath : adminDistPath },
|
|
328
|
+
// `use-sync-external-store/shim` is a React <18 polyfill that ships
|
|
329
|
+
// only as CJS. It's pulled in transitively by `@tiptap/react`. With
|
|
330
|
+
// pnpm's virtual store the file lives under .pnpm/, where Vite's
|
|
331
|
+
// dep scanner can't reach it for pre-bundling — so the browser is
|
|
332
|
+
// served raw `module.exports` and hydration fails with
|
|
333
|
+
// `SyntaxError: ... does not provide an export named
|
|
334
|
+
// 'useSyncExternalStore'`. Redirect both shim entry points to the
|
|
335
|
+
// main `use-sync-external-store` package, which on React >=18
|
|
336
|
+
// (our peer-dep floor) delegates to React's built-in hook.
|
|
337
|
+
{
|
|
338
|
+
find: "use-sync-external-store/shim/index.js",
|
|
339
|
+
replacement: "use-sync-external-store",
|
|
340
|
+
},
|
|
341
|
+
{ find: "use-sync-external-store/shim", replacement: "use-sync-external-store" },
|
|
311
342
|
],
|
|
312
343
|
},
|
|
313
344
|
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Monorepo has both vite 6 (docs) and vite 7 (core). tsgo resolves correctly.
|
|
@@ -316,7 +347,7 @@ export function createViteConfig(
|
|
|
316
347
|
// In dev mode with source alias, compile Lingui macros on the fly
|
|
317
348
|
// and redirect locale .mjs imports to dist/.
|
|
318
349
|
// In production, macros are pre-compiled by tsdown in the admin package.
|
|
319
|
-
...(useSource ? [linguiMacroPlugin(adminSourcePath
|
|
350
|
+
...(useSource ? [linguiMacroPlugin(adminSourcePath, adminDistPath)] : []),
|
|
320
351
|
] as NonNullable<AstroConfig["vite"]>["plugins"],
|
|
321
352
|
// Handle native modules for SSR.
|
|
322
353
|
// On Node: external keeps native addons out of the SSR bundle.
|
|
@@ -17,6 +17,8 @@ import { ulid } from "ulidx";
|
|
|
17
17
|
// Import auth provider via virtual module (statically bundled)
|
|
18
18
|
// This avoids dynamic import issues in Cloudflare Workers
|
|
19
19
|
import { authenticate as virtualAuthenticate } from "virtual:emdash/auth";
|
|
20
|
+
// @ts-ignore - virtual module
|
|
21
|
+
import virtualConfig from "virtual:emdash/config";
|
|
20
22
|
|
|
21
23
|
import { checkPublicCsrf } from "../../api/csrf.js";
|
|
22
24
|
import { apiError } from "../../api/error.js";
|
|
@@ -111,6 +113,7 @@ const PUBLIC_API_PREFIXES = [
|
|
|
111
113
|
const PUBLIC_API_EXACT = new Set([
|
|
112
114
|
"/_emdash/api/auth/passkey/options",
|
|
113
115
|
"/_emdash/api/auth/passkey/verify",
|
|
116
|
+
"/_emdash/api/auth/mode",
|
|
114
117
|
"/_emdash/api/oauth/token",
|
|
115
118
|
"/_emdash/api/snapshot",
|
|
116
119
|
// Public site search — read-only. The query layer hardcodes status='published'
|
|
@@ -119,6 +122,22 @@ const PUBLIC_API_EXACT = new Set([
|
|
|
119
122
|
"/_emdash/api/search",
|
|
120
123
|
]);
|
|
121
124
|
|
|
125
|
+
// Build merged public routes at module load from auth provider descriptors.
|
|
126
|
+
// Routes ending with "/" are treated as prefixes; all others are exact matches.
|
|
127
|
+
const { exact: _providerExactRoutes, prefixes: _providerPrefixRoutes } = (() => {
|
|
128
|
+
const exact = new Set<string>();
|
|
129
|
+
const prefixes: string[] = [];
|
|
130
|
+
if (!virtualConfig?.authProviders) return { exact, prefixes };
|
|
131
|
+
for (const route of virtualConfig.authProviders.flatMap((p) => p.publicRoutes ?? [])) {
|
|
132
|
+
if (route.endsWith("/")) {
|
|
133
|
+
prefixes.push(route);
|
|
134
|
+
} else {
|
|
135
|
+
exact.add(route);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { exact, prefixes };
|
|
139
|
+
})();
|
|
140
|
+
|
|
122
141
|
/**
|
|
123
142
|
* OAuth protocol endpoints that are CSRF-exempt by design.
|
|
124
143
|
*
|
|
@@ -146,6 +165,8 @@ const CSRF_EXEMPT_PUBLIC_ROUTES = new Set([
|
|
|
146
165
|
function isPublicEmDashRoute(pathname: string): boolean {
|
|
147
166
|
if (PUBLIC_API_EXACT.has(pathname)) return true;
|
|
148
167
|
if (PUBLIC_API_PREFIXES.some((p) => pathname.startsWith(p))) return true;
|
|
168
|
+
if (_providerExactRoutes.has(pathname)) return true;
|
|
169
|
+
if (_providerPrefixRoutes.some((p) => pathname.startsWith(p))) return true;
|
|
149
170
|
if (import.meta.env.DEV && pathname === "/_emdash/api/typegen") return true;
|
|
150
171
|
return false;
|
|
151
172
|
}
|
package/src/astro/middleware.ts
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
} from "../emdash-runtime.js";
|
|
44
44
|
import { setI18nConfig } from "../i18n/config.js";
|
|
45
45
|
import type { Database, Storage } from "../index.js";
|
|
46
|
+
import { createPublicMediaUrlResolver } from "../media/url.js";
|
|
46
47
|
import type { SandboxRunner } from "../plugins/sandbox/types.js";
|
|
47
48
|
import type { ResolvedPlugin } from "../plugins/types.js";
|
|
48
49
|
import { getRequestContext, runWithContext } from "../request-context.js";
|
|
@@ -232,6 +233,20 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
232
233
|
const { request, locals, cookies } = context;
|
|
233
234
|
const url = context.url;
|
|
234
235
|
|
|
236
|
+
// Fast path: routes outside /_emdash/ that plugins inject (e.g.,
|
|
237
|
+
// /.well-known/atproto-client-metadata.json) skip the entire runtime
|
|
238
|
+
// init + middleware chain. External servers fetch these with tight
|
|
239
|
+
// timeouts (~1-2s) so they must respond quickly even on cold starts.
|
|
240
|
+
if (!url.pathname.startsWith("/_emdash") && virtualConfig?.authProviders) {
|
|
241
|
+
const isPluginFastRoute = virtualConfig.authProviders.some(
|
|
242
|
+
(p: { routes?: { pattern?: string }[] }) =>
|
|
243
|
+
p.routes?.some((r: { pattern?: string }) => r.pattern && url.pathname === r.pattern),
|
|
244
|
+
);
|
|
245
|
+
if (isPluginFastRoute) {
|
|
246
|
+
return finalizeResponse(await next());
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
235
250
|
const queryRecorder = isInstrumentationEnabled()
|
|
236
251
|
? createRecorder(url.pathname, request.method, request.headers.get("x-perf-phase") ?? "default")
|
|
237
252
|
: undefined;
|
|
@@ -301,10 +316,11 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
301
316
|
try {
|
|
302
317
|
const runtime = await getRuntime(config, initSubTimings);
|
|
303
318
|
setupVerified = true;
|
|
304
|
-
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- partial object; getPageRuntime() only checks for
|
|
319
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- partial object; getPageRuntime() only checks for the page-contribution methods
|
|
305
320
|
locals.emdash = {
|
|
306
321
|
collectPageMetadata: runtime.collectPageMetadata.bind(runtime),
|
|
307
322
|
collectPageFragments: runtime.collectPageFragments.bind(runtime),
|
|
323
|
+
getPublicMediaUrl: createPublicMediaUrlResolver(runtime.storage),
|
|
308
324
|
} as EmDashHandlers;
|
|
309
325
|
} catch {
|
|
310
326
|
// Non-fatal — EmDashHead will fall back to base SEO contributions
|
|
@@ -445,6 +461,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
445
461
|
// Direct access (for advanced use cases)
|
|
446
462
|
storage: runtime.storage,
|
|
447
463
|
db: runtime.db,
|
|
464
|
+
getPublicMediaUrl: createPublicMediaUrlResolver(runtime.storage),
|
|
448
465
|
hooks: runtime.hooks,
|
|
449
466
|
email: runtime.email,
|
|
450
467
|
configuredPlugins: runtime.configuredPlugins,
|
|
@@ -10,6 +10,8 @@ import { AdminApp } from "@emdash-cms/admin";
|
|
|
10
10
|
import type { Messages } from "@lingui/core";
|
|
11
11
|
// @ts-ignore - virtual module generated by integration
|
|
12
12
|
import { pluginAdmins } from "virtual:emdash/admin-registry";
|
|
13
|
+
// @ts-ignore - virtual module generated by integration
|
|
14
|
+
import { authProviders } from "virtual:emdash/auth-providers";
|
|
13
15
|
|
|
14
16
|
interface AdminWrapperProps {
|
|
15
17
|
locale: string;
|
|
@@ -17,5 +19,12 @@ interface AdminWrapperProps {
|
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export default function AdminWrapper({ locale, messages }: AdminWrapperProps) {
|
|
20
|
-
return
|
|
22
|
+
return (
|
|
23
|
+
<AdminApp
|
|
24
|
+
pluginAdmins={pluginAdmins}
|
|
25
|
+
authProviders={authProviders}
|
|
26
|
+
locale={locale}
|
|
27
|
+
messages={messages}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
21
30
|
}
|
|
@@ -18,6 +18,9 @@ import { resolveLocale, loadMessages, getLocaleDir } from "@emdash-cms/admin/loc
|
|
|
18
18
|
const resolvedLocale = resolveLocale(Astro.request);
|
|
19
19
|
const resolvedDir = getLocaleDir(resolvedLocale);
|
|
20
20
|
const messages = await loadMessages(resolvedLocale);
|
|
21
|
+
|
|
22
|
+
const adminConfig = Astro.locals.emdash?.config?.admin;
|
|
23
|
+
const pageTitle = adminConfig?.siteName ? `${adminConfig.siteName} Admin` : "EmDash Admin";
|
|
21
24
|
---
|
|
22
25
|
|
|
23
26
|
<!doctype html>
|
|
@@ -26,13 +29,17 @@ const messages = await loadMessages(resolvedLocale);
|
|
|
26
29
|
<meta charset="UTF-8" />
|
|
27
30
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
28
31
|
<Font cssVariable="--font-emdash" />
|
|
29
|
-
|
|
30
|
-
rel="icon"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
{adminConfig?.favicon ? (
|
|
33
|
+
<link rel="icon" href={adminConfig.favicon} />
|
|
34
|
+
) : (
|
|
35
|
+
<link
|
|
36
|
+
rel="icon"
|
|
37
|
+
href="data:image/svg+xml,<svg width='75' height='75' viewBox='0 0 75 75' fill='none' xmlns='http://www.w3.org/2000/svg'> <g clip-path='url(%23clip0_50_99)'> <rect x='3' y='3' width='69' height='69' rx='10.518' stroke='url(%23paint0_linear_50_99)' stroke-width='6'/> <rect x='18' y='34' width='39.3661' height='6.56101' fill='url(%23paint1_linear_50_99)'/> </g> <defs> <linearGradient id='paint0_linear_50_99' x1='-42.9996' y1='124' x2='92.4233' y2='-41.7456' gradientUnits='userSpaceOnUse'> <stop stop-color='%230F006B'/> <stop offset='0.0833333' stop-color='%23281A81'/> <stop offset='0.166667' stop-color='%235D0C83'/> <stop offset='0.25' stop-color='%23911475'/> <stop offset='0.333333' stop-color='%23CE2F55'/> <stop offset='0.416667' stop-color='%23FF6633'/> <stop offset='0.5' stop-color='%23F6821F'/> <stop offset='0.583333' stop-color='%23FBAD41'/> <stop offset='0.666667' stop-color='%23FFCD89'/> <stop offset='0.75' stop-color='%23FFE9CB'/> <stop offset='0.833333' stop-color='%23FFF7EC'/> <stop offset='0.916667' stop-color='%23FFF8EE'/> <stop offset='1' stop-color='white'/> </linearGradient> <linearGradient id='paint1_linear_50_99' x1='91.4992' y1='27.4982' x2='28.1217' y2='54.1775' gradientUnits='userSpaceOnUse'> <stop stop-color='white'/> <stop offset='0.129253' stop-color='%23FFF8EE'/> <stop offset='0.617058' stop-color='%23FBAD41'/> <stop offset='0.848019' stop-color='%23F6821F'/> <stop offset='1' stop-color='%23FF6633'/> </linearGradient> <clipPath id='clip0_50_99'> <rect width='75' height='75' fill='white'/> </clipPath> </defs> </svg>"
|
|
38
|
+
/>
|
|
39
|
+
)}
|
|
40
|
+
<title>{pageTitle}</title>
|
|
34
41
|
</head>
|
|
35
|
-
<body>
|
|
42
|
+
<body class="isolate">
|
|
36
43
|
<div id="admin-root" class="min-h-screen">
|
|
37
44
|
<div id="emdash-boot-loader">
|
|
38
45
|
<style>
|
|
@@ -82,7 +89,7 @@ const messages = await loadMessages(resolvedLocale);
|
|
|
82
89
|
</style>
|
|
83
90
|
<div class="loader-inner">
|
|
84
91
|
<div class="spinner"></div>
|
|
85
|
-
<p>Loading EmDash
|
|
92
|
+
<p>{adminConfig?.siteName ? `Loading ${adminConfig.siteName}...` : "Loading EmDash..."}</p>
|
|
86
93
|
</div>
|
|
87
94
|
</div>
|
|
88
95
|
<AdminWrapper client:only="react" locale={resolvedLocale} messages={messages} />
|
|
@@ -19,6 +19,7 @@ import { isParseError, parseBody } from "#api/parse.js";
|
|
|
19
19
|
import { magicLinkSendBody } from "#api/schemas.js";
|
|
20
20
|
import { getSiteBaseUrl } from "#api/site-url.js";
|
|
21
21
|
import { checkRateLimit, getClientIp } from "#auth/rate-limit.js";
|
|
22
|
+
import { getTrustedProxyHeaders } from "#auth/trusted-proxy.js";
|
|
22
23
|
import { OptionsRepository } from "#db/repositories/options.js";
|
|
23
24
|
|
|
24
25
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
@@ -36,7 +37,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
36
37
|
if (isParseError(body)) return body;
|
|
37
38
|
|
|
38
39
|
// Rate limit: 3 requests per 300 seconds (5 minutes) per IP
|
|
39
|
-
const ip = getClientIp(request);
|
|
40
|
+
const ip = getClientIp(request, getTrustedProxyHeaders(emdash.config));
|
|
40
41
|
const rateLimit = await checkRateLimit(emdash.db, ip, "magic-link/send", 3, 300);
|
|
41
42
|
if (!rateLimit.allowed) {
|
|
42
43
|
// Return success-shaped response to avoid revealing rate limit
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /_emdash/api/auth/mode
|
|
3
|
+
*
|
|
4
|
+
* Public endpoint that returns the active authentication mode.
|
|
5
|
+
* Used by the login page to determine which login UI to render.
|
|
6
|
+
*
|
|
7
|
+
* Unlike the full manifest endpoint, this is intentionally public
|
|
8
|
+
* and returns only the auth mode — no collection schemas, plugin
|
|
9
|
+
* info, or other internal details.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { APIRoute } from "astro";
|
|
13
|
+
|
|
14
|
+
import { getAuthMode } from "#auth/mode.js";
|
|
15
|
+
|
|
16
|
+
export const prerender = false;
|
|
17
|
+
|
|
18
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
19
|
+
const { emdash } = locals;
|
|
20
|
+
|
|
21
|
+
const authMode = getAuthMode(emdash?.config);
|
|
22
|
+
|
|
23
|
+
// Only check signup for passkey auth (external providers handle their own)
|
|
24
|
+
let signupEnabled = false;
|
|
25
|
+
if (emdash?.db && authMode.type === "passkey") {
|
|
26
|
+
try {
|
|
27
|
+
const { sql } = await import("kysely");
|
|
28
|
+
const result = await sql<{ cnt: unknown }>`
|
|
29
|
+
SELECT COUNT(*) as cnt FROM allowed_domains WHERE enabled = 1
|
|
30
|
+
`.execute(emdash.db);
|
|
31
|
+
signupEnabled = Number(result.rows[0]?.cnt ?? 0) > 0;
|
|
32
|
+
} catch {
|
|
33
|
+
// Table may not exist yet
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Collect pluggable auth providers (from authProviders config)
|
|
38
|
+
const providers = (emdash?.config?.authProviders ?? []).map((p) => ({
|
|
39
|
+
id: p.id,
|
|
40
|
+
label: p.label,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
return Response.json(
|
|
44
|
+
{
|
|
45
|
+
data: {
|
|
46
|
+
authMode: authMode.type === "external" ? authMode.providerType : "passkey",
|
|
47
|
+
signupEnabled,
|
|
48
|
+
providers,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
headers: {
|
|
53
|
+
"Cache-Control": "private, no-store",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
};
|
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
|
|
19
19
|
|
|
20
20
|
import { getPublicOrigin } from "#api/public-url.js";
|
|
21
|
+
import { finalizeSetup } from "#api/setup-complete.js";
|
|
21
22
|
import { createOAuthStateStore } from "#auth/oauth-state-store.js";
|
|
23
|
+
import { OptionsRepository } from "#db/repositories/options.js";
|
|
22
24
|
|
|
23
25
|
type ProviderName = "github" | "google";
|
|
24
26
|
|
|
@@ -126,10 +128,22 @@ export const GET: APIRoute = async ({ params, request, locals, session, redirect
|
|
|
126
128
|
);
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
const adapter = createKyselyAdapter(emdash.db);
|
|
132
|
+
const stateStore = createOAuthStateStore(emdash.db);
|
|
133
|
+
|
|
129
134
|
const config: OAuthConsumerConfig = {
|
|
130
135
|
baseUrl: `${getPublicOrigin(url, emdash?.config)}/_emdash`,
|
|
131
136
|
providers,
|
|
132
137
|
canSelfSignup: async (email: string) => {
|
|
138
|
+
// During setup: first user becomes admin.
|
|
139
|
+
// Check setup_complete flag instead of countUsers() to avoid
|
|
140
|
+
// a TOCTOU race where concurrent callbacks both see 0 users.
|
|
141
|
+
const options = new OptionsRepository(emdash.db);
|
|
142
|
+
const setupComplete = await options.get("emdash:setup_complete");
|
|
143
|
+
if (setupComplete !== true && setupComplete !== "true") {
|
|
144
|
+
return { allowed: true, role: Role.ADMIN };
|
|
145
|
+
}
|
|
146
|
+
|
|
133
147
|
// Extract domain from email
|
|
134
148
|
const domain = email.split("@")[1]?.toLowerCase();
|
|
135
149
|
if (!domain) {
|
|
@@ -168,10 +182,16 @@ export const GET: APIRoute = async ({ params, request, locals, session, redirect
|
|
|
168
182
|
},
|
|
169
183
|
};
|
|
170
184
|
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
185
|
+
const options = new OptionsRepository(emdash.db);
|
|
186
|
+
const setupCompleteBefore = await options.get("emdash:setup_complete");
|
|
174
187
|
const user = await handleOAuthCallback(config, adapter, provider, code, state, stateStore);
|
|
188
|
+
const isFirstUser = setupCompleteBefore !== true && setupCompleteBefore !== "true";
|
|
189
|
+
|
|
190
|
+
// Finalize setup outside the transaction (idempotent, safe if two callbacks race).
|
|
191
|
+
if (isFirstUser) {
|
|
192
|
+
await finalizeSetup(emdash.db);
|
|
193
|
+
console.log(`[oauth] Setup complete: created admin user via ${provider} (${user.email})`);
|
|
194
|
+
}
|
|
175
195
|
|
|
176
196
|
// Create session
|
|
177
197
|
if (session) {
|