@dyrected/core 2.5.40 → 2.5.42
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/app-config-BLMX-9MN.d.cts +1045 -0
- package/dist/app-config-BLMX-9MN.d.ts +1045 -0
- package/dist/chunk-4C4L6SD2.js +1524 -0
- package/dist/chunk-FCXE5CVC.js +1528 -0
- package/dist/index-BKYugiYp.d.cts +1988 -0
- package/dist/index-BKYugiYp.d.ts +1988 -0
- package/dist/index-BSWCiAen.d.cts +1983 -0
- package/dist/index-BSWCiAen.d.ts +1983 -0
- package/dist/index-BfLDb3b0.d.cts +1467 -0
- package/dist/index-BfLDb3b0.d.ts +1467 -0
- package/dist/index-Bi4sX_eC.d.cts +1491 -0
- package/dist/index-Bi4sX_eC.d.ts +1491 -0
- package/dist/index-C9Vp1DCT.d.cts +1563 -0
- package/dist/index-C9Vp1DCT.d.ts +1563 -0
- package/dist/index-CFezd8p1.d.cts +1986 -0
- package/dist/index-CFezd8p1.d.ts +1986 -0
- package/dist/index-DR2z1rrf.d.cts +1503 -0
- package/dist/index-DR2z1rrf.d.ts +1503 -0
- package/dist/index-DfkPT4NR.d.cts +1584 -0
- package/dist/index-DfkPT4NR.d.ts +1584 -0
- package/dist/index-xB1lMg79.d.cts +1456 -0
- package/dist/index-xB1lMg79.d.ts +1456 -0
- package/dist/index.cjs +78 -5
- package/dist/index.d.cts +77 -3
- package/dist/index.d.ts +77 -3
- package/dist/index.js +6 -2
- package/dist/server.cjs +462 -10
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +395 -7
- package/package.json +1 -1
package/dist/server.cjs
CHANGED
|
@@ -395,13 +395,15 @@ async function executeFieldBeforeChange(fields, data, originalDoc, user, db) {
|
|
|
395
395
|
let updatedValue = value;
|
|
396
396
|
if (field.hooks?.beforeChange) {
|
|
397
397
|
for (const hook of field.hooks.beforeChange) {
|
|
398
|
-
|
|
398
|
+
const typedHook = hook;
|
|
399
|
+
const hookArgs = {
|
|
399
400
|
value: updatedValue,
|
|
400
401
|
originalDoc: originalDoc ?? void 0,
|
|
401
402
|
data: result,
|
|
402
403
|
user,
|
|
403
404
|
db
|
|
404
|
-
}
|
|
405
|
+
};
|
|
406
|
+
updatedValue = await typedHook(hookArgs);
|
|
405
407
|
}
|
|
406
408
|
result[field.name] = updatedValue;
|
|
407
409
|
}
|
|
@@ -461,12 +463,14 @@ async function executeFieldAfterRead(fields, doc, user, db) {
|
|
|
461
463
|
let updatedValue = value;
|
|
462
464
|
if (field.hooks?.afterRead) {
|
|
463
465
|
for (const hook of field.hooks.afterRead) {
|
|
464
|
-
|
|
466
|
+
const typedHook = hook;
|
|
467
|
+
const hookArgs = {
|
|
465
468
|
value: updatedValue,
|
|
466
469
|
doc: result,
|
|
467
470
|
user,
|
|
468
471
|
db
|
|
469
|
-
}
|
|
472
|
+
};
|
|
473
|
+
updatedValue = await typedHook(hookArgs);
|
|
470
474
|
}
|
|
471
475
|
result[field.name] = updatedValue;
|
|
472
476
|
}
|
|
@@ -2090,13 +2094,414 @@ var AuthController = class {
|
|
|
2090
2094
|
}
|
|
2091
2095
|
};
|
|
2092
2096
|
|
|
2093
|
-
// src/controllers/
|
|
2094
|
-
var
|
|
2097
|
+
// src/controllers/admin-auth.controller.ts
|
|
2098
|
+
var import_node_crypto2 = require("crypto");
|
|
2095
2099
|
var import_node_util3 = require("util");
|
|
2100
|
+
var import_jose2 = require("jose");
|
|
2101
|
+
|
|
2102
|
+
// src/utils/admin-auth.ts
|
|
2103
|
+
function getAdminAuthCollection(config) {
|
|
2104
|
+
const requestedSlug = config.adminAuth?.collectionSlug;
|
|
2105
|
+
if (requestedSlug) {
|
|
2106
|
+
return config.collections.find((collection) => collection.slug === requestedSlug) ?? null;
|
|
2107
|
+
}
|
|
2108
|
+
return config.collections.find((collection) => collection.slug === "__admins") ?? config.collections.find((collection) => collection.auth) ?? null;
|
|
2109
|
+
}
|
|
2110
|
+
function getPublicAdminAuthConfig(adminAuth) {
|
|
2111
|
+
const providers = (adminAuth?.providers ?? []).map((provider) => ({
|
|
2112
|
+
id: provider.id,
|
|
2113
|
+
type: provider.type,
|
|
2114
|
+
displayName: provider.displayName || humanizeProviderName(provider.id, provider.type),
|
|
2115
|
+
autoRedirect: provider.autoRedirect
|
|
2116
|
+
}));
|
|
2117
|
+
return {
|
|
2118
|
+
mode: adminAuth?.mode ?? "local",
|
|
2119
|
+
collectionSlug: adminAuth?.collectionSlug,
|
|
2120
|
+
provisioningMode: adminAuth?.provisioningMode,
|
|
2121
|
+
providers
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
function humanizeProviderName(id, type) {
|
|
2125
|
+
const cleaned = id.replace(/[-_]+/g, " ").trim();
|
|
2126
|
+
if (!cleaned) return type.toUpperCase();
|
|
2127
|
+
return cleaned.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// src/controllers/admin-auth.controller.ts
|
|
2131
|
+
var AdminAuthController = class {
|
|
2132
|
+
constructor(config) {
|
|
2133
|
+
this.config = config;
|
|
2134
|
+
}
|
|
2135
|
+
config;
|
|
2136
|
+
providers(c) {
|
|
2137
|
+
return c.json(getPublicAdminAuthConfig(this.config.adminAuth));
|
|
2138
|
+
}
|
|
2139
|
+
async start(c) {
|
|
2140
|
+
const requestConfig = await this.getRequestConfig(c);
|
|
2141
|
+
const provider = this.getProvider(requestConfig, c.req.param("provider"));
|
|
2142
|
+
if (!provider) {
|
|
2143
|
+
return c.json({ error: true, message: "Admin auth provider not found." }, 404);
|
|
2144
|
+
}
|
|
2145
|
+
const siteId = this.getSiteId(c);
|
|
2146
|
+
const returnTo = this.normalizeReturnTo(c.req.query("returnTo"), c);
|
|
2147
|
+
if (provider.type === "oidc") {
|
|
2148
|
+
const redirectUrl = await this.buildOidcStartUrl(
|
|
2149
|
+
provider,
|
|
2150
|
+
returnTo,
|
|
2151
|
+
siteId,
|
|
2152
|
+
new URL(c.req.url).origin
|
|
2153
|
+
);
|
|
2154
|
+
return c.redirect(redirectUrl, 302);
|
|
2155
|
+
}
|
|
2156
|
+
if (!provider.startUrl) {
|
|
2157
|
+
return c.json({ error: true, message: "This provider does not expose a start URL." }, 400);
|
|
2158
|
+
}
|
|
2159
|
+
const url = new URL(provider.startUrl);
|
|
2160
|
+
url.searchParams.set("returnTo", returnTo);
|
|
2161
|
+
if (siteId) url.searchParams.set("siteId", siteId);
|
|
2162
|
+
url.searchParams.set("provider", provider.id);
|
|
2163
|
+
return c.redirect(url.toString(), 302);
|
|
2164
|
+
}
|
|
2165
|
+
async callback(c) {
|
|
2166
|
+
const requestConfig = await this.getRequestConfig(c);
|
|
2167
|
+
const provider = this.getProvider(requestConfig, c.req.param("provider"));
|
|
2168
|
+
if (!provider) {
|
|
2169
|
+
return c.json({ error: true, message: "Admin auth provider not found." }, 404);
|
|
2170
|
+
}
|
|
2171
|
+
try {
|
|
2172
|
+
const exchange = await this.completeProviderAuth(requestConfig, provider, c);
|
|
2173
|
+
const redirectUrl = new URL(exchange.returnTo);
|
|
2174
|
+
redirectUrl.searchParams.set("dyrectedExternalToken", exchange.token);
|
|
2175
|
+
redirectUrl.searchParams.set("dyrectedAdminCollection", exchange.collectionSlug);
|
|
2176
|
+
return c.redirect(redirectUrl.toString(), 302);
|
|
2177
|
+
} catch (error) {
|
|
2178
|
+
const message = error instanceof Error ? error.message : "External authentication failed.";
|
|
2179
|
+
const fallback = this.normalizeReturnTo(c.req.query("returnTo"), c);
|
|
2180
|
+
const redirectUrl = new URL(fallback);
|
|
2181
|
+
redirectUrl.searchParams.set("dyrectedExternalError", message);
|
|
2182
|
+
return c.redirect(redirectUrl.toString(), 302);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
async exchange(c) {
|
|
2186
|
+
const requestConfig = await this.getRequestConfig(c);
|
|
2187
|
+
const provider = this.getProvider(requestConfig, c.req.param("provider"));
|
|
2188
|
+
if (!provider) {
|
|
2189
|
+
return c.json({ error: true, message: "Admin auth provider not found." }, 404);
|
|
2190
|
+
}
|
|
2191
|
+
try {
|
|
2192
|
+
const exchange = await this.completeProviderAuth(requestConfig, provider, c);
|
|
2193
|
+
return c.json({
|
|
2194
|
+
token: exchange.token,
|
|
2195
|
+
collectionSlug: exchange.collectionSlug,
|
|
2196
|
+
providerId: provider.id
|
|
2197
|
+
});
|
|
2198
|
+
} catch (error) {
|
|
2199
|
+
const message = error instanceof Error ? error.message : "External authentication failed.";
|
|
2200
|
+
const status = message === "Access denied for this site." ? 403 : 401;
|
|
2201
|
+
return c.json({ error: true, message }, status);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
async logout(c) {
|
|
2205
|
+
return c.json({ success: true, message: "Logged out. Discard your token." });
|
|
2206
|
+
}
|
|
2207
|
+
getProvider(config, id) {
|
|
2208
|
+
if (config.adminAuth?.mode !== "external") return void 0;
|
|
2209
|
+
return config.adminAuth.providers.find((provider) => provider.id === id);
|
|
2210
|
+
}
|
|
2211
|
+
async completeProviderAuth(requestConfig, provider, c) {
|
|
2212
|
+
const resolved = provider.type === "oidc" ? await this.resolveOidcIdentity(provider, c) : await this.resolveCustomIdentity(provider, c);
|
|
2213
|
+
const identity = resolved.identity;
|
|
2214
|
+
const siteId = this.getSiteId(c);
|
|
2215
|
+
const adminCollection = getAdminAuthCollection(requestConfig);
|
|
2216
|
+
if (!adminCollection?.auth) {
|
|
2217
|
+
throw new Error("Admin auth collection is not configured.");
|
|
2218
|
+
}
|
|
2219
|
+
const db = c.get("config").db;
|
|
2220
|
+
if (!db) throw new Error("Database not configured.");
|
|
2221
|
+
let user = await this.findUserByExternalIdentity(db, adminCollection.slug, provider.id, identity.sub) ?? (identity.email ? (await db.find({
|
|
2222
|
+
collection: adminCollection.slug,
|
|
2223
|
+
where: { email: identity.email },
|
|
2224
|
+
limit: 1
|
|
2225
|
+
})).docs[0] : null);
|
|
2226
|
+
const provisioningMode = requestConfig.adminAuth?.provisioningMode ?? "jit_plus_membership_management";
|
|
2227
|
+
const allowJitProvisioning = provisioningMode !== "preprovisioned_only" && provider.allowJitProvisioning !== false;
|
|
2228
|
+
if (!user && !allowJitProvisioning) {
|
|
2229
|
+
throw new Error("This account has not been provisioned for admin access.");
|
|
2230
|
+
}
|
|
2231
|
+
const access = await this.resolveAccess(requestConfig, identity, provider.id, siteId, c, user);
|
|
2232
|
+
if (!access.allowed) {
|
|
2233
|
+
throw new Error("Access denied for this site.");
|
|
2234
|
+
}
|
|
2235
|
+
if (!user && allowJitProvisioning) {
|
|
2236
|
+
user = await db.create({
|
|
2237
|
+
collection: adminCollection.slug,
|
|
2238
|
+
data: {
|
|
2239
|
+
...await this.buildUserData(identity, provider.id, access.roles, access.data),
|
|
2240
|
+
password: await hashPassword((0, import_node_crypto2.randomBytes)(32).toString("hex"))
|
|
2241
|
+
}
|
|
2242
|
+
});
|
|
2243
|
+
} else if (user) {
|
|
2244
|
+
user = await db.update({
|
|
2245
|
+
collection: adminCollection.slug,
|
|
2246
|
+
id: user.id,
|
|
2247
|
+
data: await this.buildUserData(identity, provider.id, access.roles, access.data)
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
if (!user?.email) {
|
|
2251
|
+
throw new Error("Authenticated admin user is missing an email address.");
|
|
2252
|
+
}
|
|
2253
|
+
const token = await signCollectionToken({
|
|
2254
|
+
sub: user.id,
|
|
2255
|
+
email: user.email,
|
|
2256
|
+
collection: adminCollection.slug,
|
|
2257
|
+
providerId: provider.id,
|
|
2258
|
+
authSource: "external"
|
|
2259
|
+
});
|
|
2260
|
+
return {
|
|
2261
|
+
token,
|
|
2262
|
+
collectionSlug: adminCollection.slug,
|
|
2263
|
+
returnTo: resolved.returnTo
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
async resolveAccess(requestConfig, identity, providerId, siteId, c, user) {
|
|
2267
|
+
const resolved = await requestConfig.adminAuth?.resolveAccess?.({
|
|
2268
|
+
identity,
|
|
2269
|
+
providerId,
|
|
2270
|
+
siteId,
|
|
2271
|
+
workspaceId: c.get("workspaceId"),
|
|
2272
|
+
req: {
|
|
2273
|
+
method: c.req.method,
|
|
2274
|
+
path: c.req.path,
|
|
2275
|
+
url: c.req.url,
|
|
2276
|
+
headers: Object.fromEntries(c.req.raw.headers.entries())
|
|
2277
|
+
},
|
|
2278
|
+
user
|
|
2279
|
+
});
|
|
2280
|
+
if (resolved) return resolved;
|
|
2281
|
+
return { allowed: true, roles: identity.roles, data: {} };
|
|
2282
|
+
}
|
|
2283
|
+
async buildUserData(identity, providerId, roles, extraData) {
|
|
2284
|
+
return {
|
|
2285
|
+
...identity.email ? { email: identity.email } : {},
|
|
2286
|
+
...identity.name ? { name: identity.name } : {},
|
|
2287
|
+
...roles ? { roles } : {},
|
|
2288
|
+
...extraData ?? {},
|
|
2289
|
+
authProvider: providerId,
|
|
2290
|
+
externalSubject: identity.sub,
|
|
2291
|
+
authSource: "external",
|
|
2292
|
+
lastLoginAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
async findUserByExternalIdentity(db, collection, providerId, externalSubject) {
|
|
2296
|
+
const result = await db.find({
|
|
2297
|
+
collection,
|
|
2298
|
+
where: {
|
|
2299
|
+
authProvider: providerId,
|
|
2300
|
+
externalSubject
|
|
2301
|
+
},
|
|
2302
|
+
limit: 1
|
|
2303
|
+
});
|
|
2304
|
+
return result.docs[0] ?? null;
|
|
2305
|
+
}
|
|
2306
|
+
getSiteId(c) {
|
|
2307
|
+
return c.req.header("X-Site-Id") || c.get("siteId");
|
|
2308
|
+
}
|
|
2309
|
+
async getRequestConfig(c) {
|
|
2310
|
+
const siteId = this.getSiteId(c);
|
|
2311
|
+
if (!siteId || !this.config.onSchemaFetch) return this.config;
|
|
2312
|
+
const dynamic = await this.config.onSchemaFetch(siteId);
|
|
2313
|
+
return {
|
|
2314
|
+
...this.config,
|
|
2315
|
+
...dynamic.admin ? { admin: { ...this.config.admin, ...dynamic.admin } } : {},
|
|
2316
|
+
...dynamic.adminAuth ? { adminAuth: { ...this.config.adminAuth, ...dynamic.adminAuth } } : {},
|
|
2317
|
+
collections: dynamic.collections ? [...this.config.collections, ...dynamic.collections] : this.config.collections,
|
|
2318
|
+
globals: dynamic.globals ? [...this.config.globals, ...dynamic.globals] : this.config.globals
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
async buildOidcStartUrl(provider, returnTo, siteId, origin) {
|
|
2322
|
+
const discovery = await this.getOidcDiscovery(provider);
|
|
2323
|
+
const nonce = (0, import_node_crypto2.randomBytes)(16).toString("hex");
|
|
2324
|
+
const state = await this.signState({
|
|
2325
|
+
providerId: provider.id,
|
|
2326
|
+
returnTo,
|
|
2327
|
+
siteId,
|
|
2328
|
+
nonce
|
|
2329
|
+
});
|
|
2330
|
+
const url = new URL(provider.authorizationEndpoint || discovery.authorization_endpoint);
|
|
2331
|
+
url.searchParams.set("response_type", "code");
|
|
2332
|
+
url.searchParams.set("client_id", provider.clientId);
|
|
2333
|
+
url.searchParams.set("redirect_uri", provider.redirectUri || this.defaultCallbackPath(provider.id, origin));
|
|
2334
|
+
url.searchParams.set("scope", (provider.scopes ?? ["openid", "profile", "email"]).join(" "));
|
|
2335
|
+
url.searchParams.set("state", state);
|
|
2336
|
+
url.searchParams.set("nonce", nonce);
|
|
2337
|
+
return url.toString();
|
|
2338
|
+
}
|
|
2339
|
+
async resolveOidcIdentity(provider, c) {
|
|
2340
|
+
const code = c.req.query("code");
|
|
2341
|
+
const state = c.req.query("state");
|
|
2342
|
+
if (!code || !state) throw new Error("Missing OIDC callback parameters.");
|
|
2343
|
+
const parsedState = await this.verifyState(state, provider.id);
|
|
2344
|
+
const discovery = await this.getOidcDiscovery(provider);
|
|
2345
|
+
const redirectUri = provider.redirectUri || this.defaultCallbackPath(provider.id, new URL(c.req.url).origin);
|
|
2346
|
+
const tokenResponse = await fetch(provider.tokenEndpoint || discovery.token_endpoint, {
|
|
2347
|
+
method: "POST",
|
|
2348
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2349
|
+
body: new URLSearchParams({
|
|
2350
|
+
grant_type: "authorization_code",
|
|
2351
|
+
code,
|
|
2352
|
+
redirect_uri: redirectUri,
|
|
2353
|
+
client_id: provider.clientId,
|
|
2354
|
+
client_secret: provider.clientSecret
|
|
2355
|
+
})
|
|
2356
|
+
});
|
|
2357
|
+
if (!tokenResponse.ok) {
|
|
2358
|
+
throw new Error("OIDC token exchange failed.");
|
|
2359
|
+
}
|
|
2360
|
+
const tokenBody = await tokenResponse.json();
|
|
2361
|
+
const idToken = tokenBody.id_token;
|
|
2362
|
+
let claims = {};
|
|
2363
|
+
if (idToken) {
|
|
2364
|
+
const jwks = (0, import_jose2.createRemoteJWKSet)(new URL(discovery.jwks_uri));
|
|
2365
|
+
const { payload } = await (0, import_jose2.jwtVerify)(idToken, jwks, {
|
|
2366
|
+
issuer: provider.issuer,
|
|
2367
|
+
audience: provider.clientId
|
|
2368
|
+
});
|
|
2369
|
+
if (parsedState.nonce && payload.nonce !== parsedState.nonce) {
|
|
2370
|
+
throw new Error("OIDC nonce verification failed.");
|
|
2371
|
+
}
|
|
2372
|
+
claims = payload;
|
|
2373
|
+
} else if (tokenBody.access_token && (provider.userInfoEndpoint || discovery.userinfo_endpoint)) {
|
|
2374
|
+
const userInfo = await fetch(provider.userInfoEndpoint || discovery.userinfo_endpoint, {
|
|
2375
|
+
headers: { Authorization: `Bearer ${tokenBody.access_token}` }
|
|
2376
|
+
});
|
|
2377
|
+
if (!userInfo.ok) throw new Error("OIDC userinfo request failed.");
|
|
2378
|
+
claims = await userInfo.json();
|
|
2379
|
+
} else {
|
|
2380
|
+
throw new Error("OIDC provider did not return usable identity claims.");
|
|
2381
|
+
}
|
|
2382
|
+
return {
|
|
2383
|
+
identity: this.mapClaims(claims, provider.claimMapping),
|
|
2384
|
+
returnTo: parsedState.returnTo
|
|
2385
|
+
};
|
|
2386
|
+
}
|
|
2387
|
+
async resolveCustomIdentity(provider, c) {
|
|
2388
|
+
const body = c.req.method === "POST" ? await c.req.json().catch(() => ({})) : {};
|
|
2389
|
+
const tokenParam = provider.tokenParam || "token";
|
|
2390
|
+
const token = body?.[tokenParam] || c.req.query(tokenParam);
|
|
2391
|
+
if (!token) {
|
|
2392
|
+
throw new Error(`Missing ${tokenParam} for external auth exchange.`);
|
|
2393
|
+
}
|
|
2394
|
+
let claims;
|
|
2395
|
+
if (provider.secret) {
|
|
2396
|
+
const secret = new import_node_util3.TextEncoder().encode(provider.secret);
|
|
2397
|
+
const verified = await (0, import_jose2.jwtVerify)(token, secret, {
|
|
2398
|
+
issuer: provider.issuer,
|
|
2399
|
+
audience: provider.audience
|
|
2400
|
+
});
|
|
2401
|
+
claims = verified.payload;
|
|
2402
|
+
} else if (provider.jwksUri) {
|
|
2403
|
+
const jwks = (0, import_jose2.createRemoteJWKSet)(new URL(provider.jwksUri));
|
|
2404
|
+
const verified = await (0, import_jose2.jwtVerify)(token, jwks, {
|
|
2405
|
+
issuer: provider.issuer,
|
|
2406
|
+
audience: provider.audience
|
|
2407
|
+
});
|
|
2408
|
+
claims = verified.payload;
|
|
2409
|
+
} else {
|
|
2410
|
+
claims = (0, import_jose2.decodeJwt)(token);
|
|
2411
|
+
if (provider.issuer && claims.iss !== provider.issuer) {
|
|
2412
|
+
throw new Error("External auth issuer mismatch.");
|
|
2413
|
+
}
|
|
2414
|
+
if (provider.audience) {
|
|
2415
|
+
const aud = claims.aud;
|
|
2416
|
+
const matches = Array.isArray(aud) ? aud.includes(provider.audience) : aud === provider.audience;
|
|
2417
|
+
if (!matches) throw new Error("External auth audience mismatch.");
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
return {
|
|
2421
|
+
identity: this.mapClaims(claims, provider.claimMapping),
|
|
2422
|
+
returnTo: this.normalizeReturnTo(
|
|
2423
|
+
body?.returnTo || c.req.query("returnTo"),
|
|
2424
|
+
c
|
|
2425
|
+
)
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
mapClaims(claims, mapping) {
|
|
2429
|
+
const subKey = mapping?.sub || "sub";
|
|
2430
|
+
const emailKey = mapping?.email || "email";
|
|
2431
|
+
const nameKey = mapping?.name || "name";
|
|
2432
|
+
const rolesKey = mapping?.roles;
|
|
2433
|
+
const groupsKey = mapping?.groups;
|
|
2434
|
+
const siteIdsKey = mapping?.siteIds;
|
|
2435
|
+
const workspaceIdsKey = mapping?.workspaceIds;
|
|
2436
|
+
const sub = claims[subKey];
|
|
2437
|
+
if (typeof sub !== "string" || !sub) {
|
|
2438
|
+
throw new Error("External identity is missing a subject.");
|
|
2439
|
+
}
|
|
2440
|
+
return {
|
|
2441
|
+
sub,
|
|
2442
|
+
email: this.readStringClaim(claims[emailKey]),
|
|
2443
|
+
name: this.readStringClaim(claims[nameKey]),
|
|
2444
|
+
roles: this.readStringArrayClaim(rolesKey ? claims[rolesKey] : void 0),
|
|
2445
|
+
groups: this.readStringArrayClaim(groupsKey ? claims[groupsKey] : void 0),
|
|
2446
|
+
siteIds: this.readStringArrayClaim(siteIdsKey ? claims[siteIdsKey] : void 0),
|
|
2447
|
+
workspaceIds: this.readStringArrayClaim(workspaceIdsKey ? claims[workspaceIdsKey] : void 0),
|
|
2448
|
+
rawClaims: claims
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
readStringClaim(value) {
|
|
2452
|
+
return typeof value === "string" ? value : void 0;
|
|
2453
|
+
}
|
|
2454
|
+
readStringArrayClaim(value) {
|
|
2455
|
+
if (!value) return void 0;
|
|
2456
|
+
if (Array.isArray(value)) {
|
|
2457
|
+
return value.filter((entry) => typeof entry === "string");
|
|
2458
|
+
}
|
|
2459
|
+
if (typeof value === "string") return [value];
|
|
2460
|
+
return void 0;
|
|
2461
|
+
}
|
|
2462
|
+
async getOidcDiscovery(provider) {
|
|
2463
|
+
const discoveryUrl = new URL(
|
|
2464
|
+
provider.issuer.replace(/\/$/, "") + "/.well-known/openid-configuration"
|
|
2465
|
+
);
|
|
2466
|
+
const response = await fetch(discoveryUrl.toString());
|
|
2467
|
+
if (!response.ok) throw new Error("Failed to load OIDC discovery document.");
|
|
2468
|
+
return response.json();
|
|
2469
|
+
}
|
|
2470
|
+
defaultCallbackPath(providerId, origin = "") {
|
|
2471
|
+
return `${origin}/api/admin/auth/${providerId}/callback`;
|
|
2472
|
+
}
|
|
2473
|
+
async signState(payload) {
|
|
2474
|
+
return new import_jose2.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime("15m").sign(this.getStateSecret());
|
|
2475
|
+
}
|
|
2476
|
+
async verifyState(token, providerId) {
|
|
2477
|
+
const { payload } = await (0, import_jose2.jwtVerify)(token, this.getStateSecret());
|
|
2478
|
+
const state = payload;
|
|
2479
|
+
if (state.providerId !== providerId) {
|
|
2480
|
+
throw new Error("OIDC state verification failed.");
|
|
2481
|
+
}
|
|
2482
|
+
return state;
|
|
2483
|
+
}
|
|
2484
|
+
getStateSecret() {
|
|
2485
|
+
const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET;
|
|
2486
|
+
if (!secret) {
|
|
2487
|
+
throw new Error("[dyrected/core] DYRECTED_JWT_SECRET is not set.");
|
|
2488
|
+
}
|
|
2489
|
+
return new import_node_util3.TextEncoder().encode(secret);
|
|
2490
|
+
}
|
|
2491
|
+
normalizeReturnTo(returnTo, c) {
|
|
2492
|
+
if (returnTo) return returnTo;
|
|
2493
|
+
const url = new URL(c.req.url);
|
|
2494
|
+
return `${url.origin}/admin`;
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
|
|
2498
|
+
// src/controllers/preview.controller.ts
|
|
2499
|
+
var import_jose3 = require("jose");
|
|
2500
|
+
var import_node_util4 = require("util");
|
|
2096
2501
|
var PreviewController = class {
|
|
2097
2502
|
getSecret() {
|
|
2098
2503
|
const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET || "dyrected-preview-secret-change-me";
|
|
2099
|
-
return new
|
|
2504
|
+
return new import_node_util4.TextEncoder().encode(secret);
|
|
2100
2505
|
}
|
|
2101
2506
|
/**
|
|
2102
2507
|
* POST /api/preview-token
|
|
@@ -2107,7 +2512,7 @@ var PreviewController = class {
|
|
|
2107
2512
|
if (!body?.collectionSlug || !body?.data) {
|
|
2108
2513
|
return c.json({ error: true, message: "collectionSlug and data are required." }, 400);
|
|
2109
2514
|
}
|
|
2110
|
-
const token = await new
|
|
2515
|
+
const token = await new import_jose3.SignJWT({
|
|
2111
2516
|
collectionSlug: body.collectionSlug,
|
|
2112
2517
|
documentId: body.documentId,
|
|
2113
2518
|
data: body.data
|
|
@@ -2125,7 +2530,7 @@ var PreviewController = class {
|
|
|
2125
2530
|
return c.json({ error: true, message: "token query parameter is required." }, 400);
|
|
2126
2531
|
}
|
|
2127
2532
|
try {
|
|
2128
|
-
const { payload } = await (0,
|
|
2533
|
+
const { payload } = await (0, import_jose3.jwtVerify)(token, this.getSecret());
|
|
2129
2534
|
return c.json(payload);
|
|
2130
2535
|
} catch (err) {
|
|
2131
2536
|
return c.json({ error: true, message: "Invalid or expired preview token." }, 401);
|
|
@@ -3110,6 +3515,9 @@ function registerRoutes(app, config) {
|
|
|
3110
3515
|
if (dynamic.admin) {
|
|
3111
3516
|
config.admin = { ...config.admin, ...dynamic.admin };
|
|
3112
3517
|
}
|
|
3518
|
+
if (dynamic.adminAuth) {
|
|
3519
|
+
config.adminAuth = { ...config.adminAuth, ...dynamic.adminAuth };
|
|
3520
|
+
}
|
|
3113
3521
|
}
|
|
3114
3522
|
const user = c.get("user");
|
|
3115
3523
|
const accessArgs = { user, req: c.req, doc: null };
|
|
@@ -3198,7 +3606,8 @@ function registerRoutes(app, config) {
|
|
|
3198
3606
|
return c.json({
|
|
3199
3607
|
collections: filteredCollections,
|
|
3200
3608
|
globals: filteredGlobals,
|
|
3201
|
-
admin: config.admin || {}
|
|
3609
|
+
admin: config.admin || {},
|
|
3610
|
+
adminAuth: getPublicAdminAuthConfig(config.adminAuth)
|
|
3202
3611
|
});
|
|
3203
3612
|
});
|
|
3204
3613
|
app.get("/api/dyrected/options/:collection/:field", optionalAuth(), async (c) => {
|
|
@@ -3390,6 +3799,12 @@ function registerRoutes(app, config) {
|
|
|
3390
3799
|
app.delete(`${prefix}/media/:id`, accessGate(col, "delete"), (c) => mediaController.delete(c));
|
|
3391
3800
|
}
|
|
3392
3801
|
}
|
|
3802
|
+
const adminAuthController = new AdminAuthController(config);
|
|
3803
|
+
app.get("/api/admin/auth/providers", (c) => adminAuthController.providers(c));
|
|
3804
|
+
app.get("/api/admin/auth/:provider/start", (c) => adminAuthController.start(c));
|
|
3805
|
+
app.get("/api/admin/auth/:provider/callback", (c) => adminAuthController.callback(c));
|
|
3806
|
+
app.post("/api/admin/auth/:provider/exchange", (c) => adminAuthController.exchange(c));
|
|
3807
|
+
app.post("/api/admin/logout", (c) => adminAuthController.logout(c));
|
|
3393
3808
|
for (const collection of config.collections) {
|
|
3394
3809
|
if (!collection.auth) continue;
|
|
3395
3810
|
const path = `/api/collections/${collection.slug}`;
|
|
@@ -3625,6 +4040,10 @@ function normalizeConfig(config) {
|
|
|
3625
4040
|
const globals = config?.globals || [];
|
|
3626
4041
|
const needsAudit = collections.some((col) => col.audit);
|
|
3627
4042
|
const needsWorkflow = collections.some((col) => col.workflow);
|
|
4043
|
+
const adminAuthCollectionSlug = getAdminAuthCollection({
|
|
4044
|
+
collections,
|
|
4045
|
+
adminAuth: config.adminAuth
|
|
4046
|
+
})?.slug;
|
|
3628
4047
|
const normalizedCollections = collections.map((col) => {
|
|
3629
4048
|
let fields = col.fields || [];
|
|
3630
4049
|
const existingFieldNames = new Set(fields.map((f) => f.name));
|
|
@@ -3711,6 +4130,39 @@ function normalizeConfig(config) {
|
|
|
3711
4130
|
return field;
|
|
3712
4131
|
});
|
|
3713
4132
|
}
|
|
4133
|
+
if (adminAuthCollectionSlug && col.slug === adminAuthCollectionSlug && config.adminAuth?.mode === "external") {
|
|
4134
|
+
const externalAdminFields = [
|
|
4135
|
+
{
|
|
4136
|
+
name: "authProvider",
|
|
4137
|
+
type: "text",
|
|
4138
|
+
label: "Auth Provider",
|
|
4139
|
+
admin: { readOnly: true, hidden: true }
|
|
4140
|
+
},
|
|
4141
|
+
{
|
|
4142
|
+
name: "externalSubject",
|
|
4143
|
+
type: "text",
|
|
4144
|
+
label: "External Subject",
|
|
4145
|
+
admin: { readOnly: true, hidden: true }
|
|
4146
|
+
},
|
|
4147
|
+
{
|
|
4148
|
+
name: "authSource",
|
|
4149
|
+
type: "text",
|
|
4150
|
+
label: "Auth Source",
|
|
4151
|
+
admin: { readOnly: true, hidden: true }
|
|
4152
|
+
},
|
|
4153
|
+
{
|
|
4154
|
+
name: "lastLoginAt",
|
|
4155
|
+
type: "date",
|
|
4156
|
+
label: "Last Login At",
|
|
4157
|
+
admin: { readOnly: true, hidden: true }
|
|
4158
|
+
}
|
|
4159
|
+
];
|
|
4160
|
+
for (const field of externalAdminFields) {
|
|
4161
|
+
if (!existingFieldNames.has(field.name)) {
|
|
4162
|
+
fields = [...fields, field];
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
3714
4166
|
const updatedFieldNames = new Set(fields.map((f) => f.name));
|
|
3715
4167
|
const fieldsToInject = SYSTEM_FIELDS.filter((f) => !updatedFieldNames.has(f.name));
|
|
3716
4168
|
return {
|
package/dist/server.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as hono_types from 'hono/types';
|
|
2
2
|
import * as hono from 'hono';
|
|
3
3
|
import { Hono, Context } from 'hono';
|
|
4
|
-
import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './
|
|
4
|
+
import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './app-config-BLMX-9MN.cjs';
|
|
5
5
|
import * as hono_utils_http_status from 'hono/utils/http-status';
|
|
6
6
|
import * as hono_utils_types from 'hono/utils/types';
|
|
7
7
|
import 'lucide-react';
|
package/dist/server.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as hono_types from 'hono/types';
|
|
2
2
|
import * as hono from 'hono';
|
|
3
3
|
import { Hono, Context } from 'hono';
|
|
4
|
-
import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './
|
|
4
|
+
import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './app-config-BLMX-9MN.js';
|
|
5
5
|
import * as hono_utils_http_status from 'hono/utils/http-status';
|
|
6
6
|
import * as hono_utils_types from 'hono/utils/types';
|
|
7
7
|
import 'lucide-react';
|