@10x-media/impersonation 0.1.0-beta.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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/access/filterTargets.js +51 -0
- package/dist/access/filterTargets.js.map +1 -0
- package/dist/auth/cookies.js +49 -0
- package/dist/auth/cookies.js.map +1 -0
- package/dist/auth/expiryCookies.js +59 -0
- package/dist/auth/expiryCookies.js.map +1 -0
- package/dist/auth/issue.js +126 -0
- package/dist/auth/issue.js.map +1 -0
- package/dist/auth/mode.js +69 -0
- package/dist/auth/mode.js.map +1 -0
- package/dist/auth/origin.js +55 -0
- package/dist/auth/origin.js.map +1 -0
- package/dist/auth/revoke.js +24 -0
- package/dist/auth/revoke.js.map +1 -0
- package/dist/auth/tenantCookie.js +74 -0
- package/dist/auth/tenantCookie.js.map +1 -0
- package/dist/endpoints/codes.d.ts +6 -0
- package/dist/endpoints/current.js +66 -0
- package/dist/endpoints/current.js.map +1 -0
- package/dist/endpoints/exit.js +242 -0
- package/dist/endpoints/exit.js.map +1 -0
- package/dist/endpoints/shared.js +76 -0
- package/dist/endpoints/shared.js.map +1 -0
- package/dist/endpoints/start.js +332 -0
- package/dist/endpoints/start.js.map +1 -0
- package/dist/endpoints/terminate.js +73 -0
- package/dist/endpoints/terminate.js.map +1 -0
- package/dist/exports/client.d.ts +9 -0
- package/dist/exports/client.js +10 -0
- package/dist/exports/i18n.d.ts +3 -0
- package/dist/exports/i18n.js +3 -0
- package/dist/exports/rsc.d.ts +4 -0
- package/dist/exports/rsc.js +4 -0
- package/dist/exports/types.d.ts +3 -0
- package/dist/exports/types.js +1 -0
- package/dist/getImpersonation.d.ts +49 -0
- package/dist/getImpersonation.js +78 -0
- package/dist/getImpersonation.js.map +1 -0
- package/dist/ids.js +27 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin/assertTargets.js +23 -0
- package/dist/plugin/assertTargets.js.map +1 -0
- package/dist/plugin/constants.d.ts +6 -0
- package/dist/plugin/constants.js +20 -0
- package/dist/plugin/constants.js.map +1 -0
- package/dist/plugin/decorateAuth.js +99 -0
- package/dist/plugin/decorateAuth.js.map +1 -0
- package/dist/plugin/lookup.js +23 -0
- package/dist/plugin/lookup.js.map +1 -0
- package/dist/plugin/normalizeOptions.js +83 -0
- package/dist/plugin/normalizeOptions.js.map +1 -0
- package/dist/plugin/registerAdmin.js +49 -0
- package/dist/plugin/registerAdmin.js.map +1 -0
- package/dist/plugin/registerCollection.js +183 -0
- package/dist/plugin/registerCollection.js.map +1 -0
- package/dist/plugin/registerEndpoints.js +19 -0
- package/dist/plugin/registerEndpoints.js.map +1 -0
- package/dist/plugin/registerHooks.js +59 -0
- package/dist/plugin/registerHooks.js.map +1 -0
- package/dist/plugin/registerRetention.js +57 -0
- package/dist/plugin/registerRetention.js.map +1 -0
- package/dist/plugin/registerTranslations.js +19 -0
- package/dist/plugin/registerTranslations.js.map +1 -0
- package/dist/plugin/registry.js +11 -0
- package/dist/plugin/registry.js.map +1 -0
- package/dist/plugin/startable.js +21 -0
- package/dist/plugin/startable.js.map +1 -0
- package/dist/session/close.js +65 -0
- package/dist/session/close.js.map +1 -0
- package/dist/session/closeStale.d.ts +11 -0
- package/dist/session/closeStale.js +65 -0
- package/dist/session/closeStale.js.map +1 -0
- package/dist/session/locks.js +32 -0
- package/dist/session/locks.js.map +1 -0
- package/dist/session/resolve.js +91 -0
- package/dist/session/resolve.js.map +1 -0
- package/dist/translations/ar.js +52 -0
- package/dist/translations/ar.js.map +1 -0
- package/dist/translations/de.js +52 -0
- package/dist/translations/de.js.map +1 -0
- package/dist/translations/en.js +52 -0
- package/dist/translations/en.js.map +1 -0
- package/dist/translations/es.js +52 -0
- package/dist/translations/es.js.map +1 -0
- package/dist/translations/fr.js +52 -0
- package/dist/translations/fr.js.map +1 -0
- package/dist/translations/id.js +52 -0
- package/dist/translations/id.js.map +1 -0
- package/dist/translations/index.d.ts +24 -0
- package/dist/translations/index.js +49 -0
- package/dist/translations/index.js.map +1 -0
- package/dist/translations/keys.d.ts +56 -0
- package/dist/translations/keys.js +56 -0
- package/dist/translations/keys.js.map +1 -0
- package/dist/translations/ko.js +52 -0
- package/dist/translations/ko.js.map +1 -0
- package/dist/translations/lookup.js +17 -0
- package/dist/translations/lookup.js.map +1 -0
- package/dist/translations/pt.js +52 -0
- package/dist/translations/pt.js.map +1 -0
- package/dist/translations/ru.js +52 -0
- package/dist/translations/ru.js.map +1 -0
- package/dist/translations/server.js +13 -0
- package/dist/translations/server.js.map +1 -0
- package/dist/translations/uk.js +52 -0
- package/dist/translations/uk.js.map +1 -0
- package/dist/translations/useTranslation.js +12 -0
- package/dist/translations/useTranslation.js.map +1 -0
- package/dist/translations/zh.js +52 -0
- package/dist/translations/zh.js.map +1 -0
- package/dist/types.d.ts +214 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/EndSessionMenuItem.d.ts +5 -0
- package/dist/ui/EndSessionMenuItem.js +45 -0
- package/dist/ui/EndSessionMenuItem.js.map +1 -0
- package/dist/ui/ImpersonateIcon.js +20 -0
- package/dist/ui/ImpersonateIcon.js.map +1 -0
- package/dist/ui/ImpersonationAction.d.ts +10 -0
- package/dist/ui/ImpersonationAction.js +48 -0
- package/dist/ui/ImpersonationAction.js.map +1 -0
- package/dist/ui/ImpersonationBar.d.ts +36 -0
- package/dist/ui/ImpersonationBar.js +357 -0
- package/dist/ui/ImpersonationBar.js.map +1 -0
- package/dist/ui/ImpersonationConfig.js +25 -0
- package/dist/ui/ImpersonationConfig.js.map +1 -0
- package/dist/ui/ImpersonationDocumentButton.d.ts +5 -0
- package/dist/ui/ImpersonationDocumentButton.js +85 -0
- package/dist/ui/ImpersonationDocumentButton.js.map +1 -0
- package/dist/ui/ImpersonationFrontendBanner.d.ts +41 -0
- package/dist/ui/ImpersonationFrontendBanner.js +58 -0
- package/dist/ui/ImpersonationFrontendBanner.js.map +1 -0
- package/dist/ui/ImpersonationFrontendExit.d.ts +17 -0
- package/dist/ui/ImpersonationFrontendExit.js +45 -0
- package/dist/ui/ImpersonationFrontendExit.js.map +1 -0
- package/dist/ui/ImpersonationProvider.d.ts +16 -0
- package/dist/ui/ImpersonationProvider.js +127 -0
- package/dist/ui/ImpersonationProvider.js.map +1 -0
- package/dist/ui/ImpersonationSwitcher.d.ts +21 -0
- package/dist/ui/ImpersonationSwitcher.js +251 -0
- package/dist/ui/ImpersonationSwitcher.js.map +1 -0
- package/dist/ui/ImpersonationUserCard.d.ts +31 -0
- package/dist/ui/ImpersonationUserCard.js +51 -0
- package/dist/ui/ImpersonationUserCard.js.map +1 -0
- package/dist/ui/StartConfirmModal.d.ts +28 -0
- package/dist/ui/StartConfirmModal.js +65 -0
- package/dist/ui/StartConfirmModal.js.map +1 -0
- package/dist/ui/api.js +66 -0
- package/dist/ui/api.js.map +1 -0
- package/dist/ui/barPlacement.js +107 -0
- package/dist/ui/barPlacement.js.map +1 -0
- package/dist/ui/frontendBanner.css +22 -0
- package/dist/ui/impersonation.css +244 -0
- package/dist/ui/useImpersonation.d.ts +43 -0
- package/dist/ui/useImpersonation.js +36 -0
- package/dist/ui/useImpersonation.js.map +1 -0
- package/package.json +126 -0
package/dist/ids.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ids.js","names":[],"sources":["../src/ids.ts"],"sourcesContent":["import type { CollectionSlug, Payload } from 'payload'\n\nimport type { UserLabel } from './types'\n\n/** Payload ids are strings on Mongo and numbers on Postgres; compare as text. */\nexport const idsEqual = (a: unknown, b: unknown): boolean =>\n\ta != null && b != null && String(a) === String(b)\n\nexport const asId = (value: unknown): number | string | null => {\n\tif (typeof value === 'number' && Number.isFinite(value)) {\n\t\treturn value\n\t}\n\tif (typeof value === 'string' && value.length > 0) {\n\t\treturn value\n\t}\n\treturn null\n}\n\n/** Index `payload.collections` with a runtime slug. Generated types are a closed Record. */\nexport const collectionBySlug = (payload: Payload, slug: string) =>\n\tpayload.collections[slug as CollectionSlug]\n\n/**\n * Display name for a user doc: its collection's `useAsTitle` value, else the\n * email. Snapshotted on the session row so every surface shows the same name.\n */\nexport const userTitle = (\n\tpayload: Payload,\n\tslug: string,\n\tdoc: null | Record<string, unknown> | undefined\n): string | undefined => {\n\tconst field = collectionBySlug(payload, slug)?.config.admin?.useAsTitle\n\tconst value = field && field !== 'id' ? doc?.[field] : undefined\n\tif (typeof value === 'string' && value.trim()) {\n\t\treturn value.trim()\n\t}\n\tif (typeof value === 'number') {\n\t\treturn String(value)\n\t}\n\treturn typeof doc?.email === 'string' && doc.email ? doc.email : undefined\n}\n\n/** Pick the snapshotted title or email per `ui.userLabel`; either falls back to the other. */\nexport const labelUser = (\n\tmode: UserLabel,\n\tuser: { email?: null | string; title?: null | string }\n): string | undefined =>\n\t(mode === 'email' ? user.email || user.title : user.title || user.email) || undefined\n"],"mappings":";;AAKA,MAAa,YAAY,GAAY,MACpC,KAAK,QAAQ,KAAK,QAAQ,OAAO,CAAC,MAAM,OAAO,CAAC;AAEjD,MAAa,QAAQ,UAA2C;CAC/D,IAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GACrD,OAAO;CAER,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAC/C,OAAO;CAER,OAAO;AACR;;AAGA,MAAa,oBAAoB,SAAkB,SAClD,QAAQ,YAAY;;;;;AAMrB,MAAa,aACZ,SACA,MACA,QACwB;CACxB,MAAM,QAAQ,iBAAiB,SAAS,IAAI,GAAG,OAAO,OAAO;CAC7D,MAAM,QAAQ,SAAS,UAAU,OAAO,MAAM,SAAS,KAAA;CACvD,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,GAC3C,OAAO,MAAM,KAAK;CAEnB,IAAI,OAAO,UAAU,UACpB,OAAO,OAAO,KAAK;CAEpB,OAAO,OAAO,KAAK,UAAU,YAAY,IAAI,QAAQ,IAAI,QAAQ,KAAA;AAClE;;AAGA,MAAa,aACZ,MACA,UAEC,SAAS,UAAU,KAAK,SAAS,KAAK,QAAQ,KAAK,SAAS,KAAK,UAAU,KAAA"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ImpersonationPluginOptions } from "./types.js";
|
|
2
|
+
import { ImpersonationStatus, getImpersonation } from "./getImpersonation.js";
|
|
3
|
+
import { closeStaleImpersonations } from "./session/closeStale.js";
|
|
4
|
+
import { PLUGIN_SLUG } from "./plugin/constants.js";
|
|
5
|
+
|
|
6
|
+
//#region src/index.d.ts
|
|
7
|
+
declare module 'payload' {
|
|
8
|
+
interface RegisteredPlugins {
|
|
9
|
+
'@10x-media/impersonation': ImpersonationPluginOptions;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Let an authorised account sign in as another user without their password.
|
|
14
|
+
* After start, `req.user` is the target. Every session is a closed-never-deleted
|
|
15
|
+
* row. Access is always a host function; the plugin never compares roles.
|
|
16
|
+
*/
|
|
17
|
+
declare const impersonation: (options: ImpersonationPluginOptions) => import("payload").Plugin;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { type ImpersonationPluginOptions, type ImpersonationStatus, PLUGIN_SLUG, type ImpersonationPluginOptions as PluginOptions, closeStaleImpersonations, getImpersonation, impersonation };
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PLUGIN_SLUG } from "./plugin/constants.js";
|
|
2
|
+
import { setRegistry } from "./plugin/registry.js";
|
|
3
|
+
import { getImpersonation } from "./getImpersonation.js";
|
|
4
|
+
import { assertHookedAuthCollections } from "./plugin/assertTargets.js";
|
|
5
|
+
import { decorateAuthStrategies } from "./plugin/decorateAuth.js";
|
|
6
|
+
import { normalizeOptions } from "./plugin/normalizeOptions.js";
|
|
7
|
+
import { registerAdmin } from "./plugin/registerAdmin.js";
|
|
8
|
+
import { registerCollection } from "./plugin/registerCollection.js";
|
|
9
|
+
import { registerEndpoints } from "./plugin/registerEndpoints.js";
|
|
10
|
+
import { registerHooks } from "./plugin/registerHooks.js";
|
|
11
|
+
import { closeStaleImpersonations } from "./session/closeStale.js";
|
|
12
|
+
import { registerRetention } from "./plugin/registerRetention.js";
|
|
13
|
+
import { registerTranslations } from "./plugin/registerTranslations.js";
|
|
14
|
+
import { definePlugin } from "payload";
|
|
15
|
+
//#region src/index.ts
|
|
16
|
+
/**
|
|
17
|
+
* Let an authorised account sign in as another user without their password.
|
|
18
|
+
* After start, `req.user` is the target. Every session is a closed-never-deleted
|
|
19
|
+
* row. Access is always a host function; the plugin never compares roles.
|
|
20
|
+
*/
|
|
21
|
+
const impersonation = definePlugin({
|
|
22
|
+
slug: PLUGIN_SLUG,
|
|
23
|
+
plugin: ({ config, plugins: _plugins, ...options }) => {
|
|
24
|
+
if (options.disabled === true) return config;
|
|
25
|
+
const resolved = normalizeOptions(options, config);
|
|
26
|
+
const hookedAuthSlugs = (config.collections ?? []).filter((collection) => Boolean(collection.auth)).map(({ slug }) => slug);
|
|
27
|
+
registerTranslations(config, options.translations);
|
|
28
|
+
setRegistry(config, resolved);
|
|
29
|
+
registerCollection(config, resolved);
|
|
30
|
+
registerEndpoints(config, resolved);
|
|
31
|
+
registerHooks(config, resolved);
|
|
32
|
+
registerAdmin(config, resolved);
|
|
33
|
+
registerRetention(config, resolved);
|
|
34
|
+
const priorOnInit = config.onInit;
|
|
35
|
+
config.onInit = async (payload) => {
|
|
36
|
+
assertHookedAuthCollections(payload, resolved, hookedAuthSlugs);
|
|
37
|
+
if (resolved.maxDuration === void 0) payload.logger.warn("@10x-media/impersonation: maxDuration is unset. An impersonation never expires on its own because refresh keeps extending the minted session.");
|
|
38
|
+
decorateAuthStrategies(payload, resolved);
|
|
39
|
+
try {
|
|
40
|
+
await closeStaleImpersonations(payload);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
payload.logger.error({
|
|
43
|
+
err: error,
|
|
44
|
+
msg: "@10x-media/impersonation: closeStaleImpersonations failed during onInit"
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
await priorOnInit?.(payload);
|
|
48
|
+
};
|
|
49
|
+
return config;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
//#endregion
|
|
53
|
+
export { PLUGIN_SLUG, closeStaleImpersonations, getImpersonation, impersonation };
|
|
54
|
+
|
|
55
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { type Config, definePlugin } from 'payload'\n\nimport { getImpersonation } from './getImpersonation'\nimport { assertHookedAuthCollections } from './plugin/assertTargets'\nimport { PLUGIN_SLUG } from './plugin/constants'\nimport { decorateAuthStrategies } from './plugin/decorateAuth'\nimport { normalizeOptions } from './plugin/normalizeOptions'\nimport { registerAdmin } from './plugin/registerAdmin'\nimport { registerCollection } from './plugin/registerCollection'\nimport { registerEndpoints } from './plugin/registerEndpoints'\nimport { registerHooks } from './plugin/registerHooks'\nimport { registerRetention } from './plugin/registerRetention'\nimport { registerTranslations } from './plugin/registerTranslations'\nimport { setRegistry } from './plugin/registry'\nimport { closeStaleImpersonations } from './session/closeStale'\nimport type { ImpersonationPluginOptions } from './types'\n\nexport type { ImpersonationStatus } from './getImpersonation'\nexport { PLUGIN_SLUG } from './plugin/constants'\nexport type {\n\tImpersonationPluginOptions,\n\tImpersonationPluginOptions as PluginOptions,\n} from './types'\nexport { closeStaleImpersonations, getImpersonation }\n\ndeclare module 'payload' {\n\tinterface RegisteredPlugins {\n\t\t'@10x-media/impersonation': ImpersonationPluginOptions\n\t}\n}\n\n/**\n * Let an authorised account sign in as another user without their password.\n * After start, `req.user` is the target. Every session is a closed-never-deleted\n * row. Access is always a host function; the plugin never compares roles.\n */\nexport const impersonation = definePlugin<ImpersonationPluginOptions>({\n\tslug: PLUGIN_SLUG,\n\tplugin: ({ config, plugins: _plugins, ...options }): Config => {\n\t\tif (options.disabled === true) {\n\t\t\treturn config\n\t\t}\n\n\t\tconst resolved = normalizeOptions(options, config)\n\t\tconst hookedAuthSlugs = (config.collections ?? [])\n\t\t\t.filter((collection) => Boolean(collection.auth))\n\t\t\t.map(({ slug }) => slug)\n\t\tregisterTranslations(config, options.translations)\n\t\tsetRegistry(config, resolved)\n\t\tregisterCollection(config, resolved)\n\t\tregisterEndpoints(config, resolved)\n\t\tregisterHooks(config, resolved)\n\t\tregisterAdmin(config, resolved)\n\t\tregisterRetention(config, resolved)\n\n\t\tconst priorOnInit = config.onInit\n\t\tconfig.onInit = async (payload) => {\n\t\t\tassertHookedAuthCollections(payload, resolved, hookedAuthSlugs)\n\n\t\t\tif (resolved.maxDuration === undefined) {\n\t\t\t\tpayload.logger.warn(\n\t\t\t\t\t'@10x-media/impersonation: maxDuration is unset. An impersonation never expires on its own because refresh keeps extending the minted session.'\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tdecorateAuthStrategies(payload, resolved)\n\n\t\t\ttry {\n\t\t\t\tawait closeStaleImpersonations(payload)\n\t\t\t} catch (error) {\n\t\t\t\tpayload.logger.error({\n\t\t\t\t\terr: error,\n\t\t\t\t\tmsg: '@10x-media/impersonation: closeStaleImpersonations failed during onInit',\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tawait priorOnInit?.(payload)\n\t\t}\n\n\t\treturn config\n\t},\n})\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAoCA,MAAa,gBAAgB,aAAyC;CACrE,MAAM;CACN,SAAS,EAAE,QAAQ,SAAS,UAAU,GAAG,cAAsB;EAC9D,IAAI,QAAQ,aAAa,MACxB,OAAO;EAGR,MAAM,WAAW,iBAAiB,SAAS,MAAM;EACjD,MAAM,mBAAmB,OAAO,eAAe,CAAC,GAC9C,QAAQ,eAAe,QAAQ,WAAW,IAAI,CAAC,EAC/C,KAAK,EAAE,WAAW,IAAI;EACxB,qBAAqB,QAAQ,QAAQ,YAAY;EACjD,YAAY,QAAQ,QAAQ;EAC5B,mBAAmB,QAAQ,QAAQ;EACnC,kBAAkB,QAAQ,QAAQ;EAClC,cAAc,QAAQ,QAAQ;EAC9B,cAAc,QAAQ,QAAQ;EAC9B,kBAAkB,QAAQ,QAAQ;EAElC,MAAM,cAAc,OAAO;EAC3B,OAAO,SAAS,OAAO,YAAY;GAClC,4BAA4B,SAAS,UAAU,eAAe;GAE9D,IAAI,SAAS,gBAAgB,KAAA,GAC5B,QAAQ,OAAO,KACd,+IACD;GAGD,uBAAuB,SAAS,QAAQ;GAExC,IAAI;IACH,MAAM,yBAAyB,OAAO;GACvC,SAAS,OAAO;IACf,QAAQ,OAAO,MAAM;KACpB,KAAK;KACL,KAAK;IACN,CAAC;GACF;GAEA,MAAM,cAAc,OAAO;EAC5B;EAEA,OAAO;CACR;AACD,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { collectionBySlug } from "../ids.js";
|
|
2
|
+
//#region src/plugin/assertTargets.ts
|
|
3
|
+
/**
|
|
4
|
+
* Auth collections that exist on the booted instance but were missing when this
|
|
5
|
+
* plugin ran. `relationTo` and `afterLogout` snapshot `config.collections`, so a
|
|
6
|
+
* later plugin's collection would be startable and then fail open on logout.
|
|
7
|
+
*/
|
|
8
|
+
const missingHookedAuthSlugs = (liveAuthSlugs, hookedAuthSlugs, targets) => {
|
|
9
|
+
const hooked = new Set(hookedAuthSlugs);
|
|
10
|
+
return (targets ?? liveAuthSlugs).filter((slug) => !hooked.has(slug));
|
|
11
|
+
};
|
|
12
|
+
const liveAuthSlugsOf = (payload, recordsSlug) => Object.values(payload.collections).filter((collection) => collection.config.auth && collection.config.slug !== recordsSlug).map((collection) => collection.config.slug);
|
|
13
|
+
const assertHookedAuthCollections = (payload, options, hookedAuthSlugs) => {
|
|
14
|
+
if (options.targets) {
|
|
15
|
+
for (const slug of options.targets) if (!collectionBySlug(payload, slug)) throw new Error(`@10x-media/impersonation: target collection "${slug}" is not in the config. Register impersonation after plugins that add auth collections.`);
|
|
16
|
+
}
|
|
17
|
+
const first = missingHookedAuthSlugs(liveAuthSlugsOf(payload, options.collectionSlug), hookedAuthSlugs, options.targets)[0];
|
|
18
|
+
if (first) throw new Error(`@10x-media/impersonation: auth collection "${first}" was registered after this plugin. List impersonation after plugins that add auth collections.`);
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
export { assertHookedAuthCollections };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=assertTargets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assertTargets.js","names":[],"sources":["../../src/plugin/assertTargets.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\nimport { collectionBySlug } from '../ids'\nimport type { ResolvedOptions } from '../types'\n\n/**\n * Auth collections that exist on the booted instance but were missing when this\n * plugin ran. `relationTo` and `afterLogout` snapshot `config.collections`, so a\n * later plugin's collection would be startable and then fail open on logout.\n */\nexport const missingHookedAuthSlugs = (\n\tliveAuthSlugs: string[],\n\thookedAuthSlugs: readonly string[],\n\ttargets: string[] | undefined\n): string[] => {\n\tconst hooked = new Set(hookedAuthSlugs)\n\tconst required = targets ?? liveAuthSlugs\n\treturn required.filter((slug) => !hooked.has(slug))\n}\n\nexport const liveAuthSlugsOf = (payload: Payload, recordsSlug: string): string[] =>\n\tObject.values(payload.collections)\n\t\t.filter((collection) => collection.config.auth && collection.config.slug !== recordsSlug)\n\t\t.map((collection) => collection.config.slug)\n\nexport const assertHookedAuthCollections = (\n\tpayload: Payload,\n\toptions: ResolvedOptions,\n\thookedAuthSlugs: readonly string[]\n): void => {\n\tif (options.targets) {\n\t\tfor (const slug of options.targets) {\n\t\t\tif (!collectionBySlug(payload, slug)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`@10x-media/impersonation: target collection \"${slug}\" is not in the config. Register impersonation after plugins that add auth collections.`\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\tconst missing = missingHookedAuthSlugs(\n\t\tliveAuthSlugsOf(payload, options.collectionSlug),\n\t\thookedAuthSlugs,\n\t\toptions.targets\n\t)\n\tconst first = missing[0]\n\tif (first) {\n\t\tthrow new Error(\n\t\t\t`@10x-media/impersonation: auth collection \"${first}\" was registered after this plugin. List impersonation after plugins that add auth collections.`\n\t\t)\n\t}\n}\n"],"mappings":";;;;;;;AAUA,MAAa,0BACZ,eACA,iBACA,YACc;CACd,MAAM,SAAS,IAAI,IAAI,eAAe;CAEtC,QADiB,WAAW,eACZ,QAAQ,SAAS,CAAC,OAAO,IAAI,IAAI,CAAC;AACnD;AAEA,MAAa,mBAAmB,SAAkB,gBACjD,OAAO,OAAO,QAAQ,WAAW,EAC/B,QAAQ,eAAe,WAAW,OAAO,QAAQ,WAAW,OAAO,SAAS,WAAW,EACvF,KAAK,eAAe,WAAW,OAAO,IAAI;AAE7C,MAAa,+BACZ,SACA,SACA,oBACU;CACV,IAAI,QAAQ;OACN,MAAM,QAAQ,QAAQ,SAC1B,IAAI,CAAC,iBAAiB,SAAS,IAAI,GAClC,MAAM,IAAI,MACT,gDAAgD,KAAK,wFACtD;CAAA;CAUH,MAAM,QALU,uBACf,gBAAgB,SAAS,QAAQ,cAAc,GAC/C,iBACA,QAAQ,OAEW,EAAE;CACtB,IAAI,OACH,MAAM,IAAI,MACT,8CAA8C,MAAM,gGACrD;AAEF"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/plugin/constants.ts
|
|
2
|
+
/** This plugin's registered slug, used to find its options on a booted config. */
|
|
3
|
+
const PLUGIN_SLUG = "@10x-media/impersonation";
|
|
4
|
+
/** `config.custom` key the resolved options live under. */
|
|
5
|
+
const REGISTRY_KEY = PLUGIN_SLUG;
|
|
6
|
+
const DEFAULT_COLLECTION_SLUG = "impersonation-sessions";
|
|
7
|
+
const DEFAULT_API_PATH = "/impersonation";
|
|
8
|
+
const DEFAULT_HINT_COOKIE = "impersonation-hint";
|
|
9
|
+
const DEFAULT_COOKIE_PREFIX = "payload";
|
|
10
|
+
const defaultClearOnSwitch = (cookiePrefix) => [`${cookiePrefix}-tenant`];
|
|
11
|
+
/** Client POSTs that busy-gate a control abort after this. */
|
|
12
|
+
const CLIENT_FETCH_TIMEOUT_MS = 15e3;
|
|
13
|
+
const ADMIN_GROUP = "System";
|
|
14
|
+
/** Prefixed onto minted target sids so decorateAuth can skip the DB on normal traffic. */
|
|
15
|
+
const IMPERSONATION_SID_PREFIX = "imp_";
|
|
16
|
+
const RESERVED_API_SEGMENTS = ["graphql", "graphql-playground"];
|
|
17
|
+
//#endregion
|
|
18
|
+
export { ADMIN_GROUP, CLIENT_FETCH_TIMEOUT_MS, DEFAULT_API_PATH, DEFAULT_COLLECTION_SLUG, DEFAULT_COOKIE_PREFIX, DEFAULT_HINT_COOKIE, IMPERSONATION_SID_PREFIX, PLUGIN_SLUG, REGISTRY_KEY, RESERVED_API_SEGMENTS, defaultClearOnSwitch };
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","names":[],"sources":["../../src/plugin/constants.ts"],"sourcesContent":["/** This plugin's registered slug, used to find its options on a booted config. */\nexport const PLUGIN_SLUG = '@10x-media/impersonation'\n\n/** `config.custom` key the resolved options live under. */\nexport const REGISTRY_KEY = PLUGIN_SLUG\n\nexport const DEFAULT_COLLECTION_SLUG = 'impersonation-sessions'\nexport const DEFAULT_API_PATH = '/impersonation'\nexport const DEFAULT_HINT_COOKIE = 'impersonation-hint'\nexport const DEFAULT_COOKIE_PREFIX = 'payload'\nexport const defaultClearOnSwitch = (cookiePrefix: string) => [`${cookiePrefix}-tenant`]\nexport const REASON_MAX_LENGTH = 500\n/** Client POSTs that busy-gate a control abort after this. */\nexport const CLIENT_FETCH_TIMEOUT_MS = 15_000\nexport const ADMIN_GROUP = 'System'\n/** Prefixed onto minted target sids so decorateAuth can skip the DB on normal traffic. */\nexport const IMPERSONATION_SID_PREFIX = 'imp_'\nexport const SWITCHER_PAGE_SIZE = 20\n\nexport const RESERVED_API_SEGMENTS = ['graphql', 'graphql-playground'] as const\n"],"mappings":";;AACA,MAAa,cAAc;;AAG3B,MAAa,eAAe;AAE5B,MAAa,0BAA0B;AACvC,MAAa,mBAAmB;AAChC,MAAa,sBAAsB;AACnC,MAAa,wBAAwB;AACrC,MAAa,wBAAwB,iBAAyB,CAAC,GAAG,aAAa,QAAQ;;AAGvF,MAAa,0BAA0B;AACvC,MAAa,cAAc;;AAE3B,MAAa,2BAA2B;AAGxC,MAAa,wBAAwB,CAAC,WAAW,oBAAoB"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { cachedFindOpenBySid, isPastAbsoluteExpiry, relationOf } from "../session/resolve.js";
|
|
2
|
+
import { absoluteExpiryCookies } from "../auth/expiryCookies.js";
|
|
3
|
+
import { closeAndRevoke } from "../session/close.js";
|
|
4
|
+
import { boundSid } from "../types.js";
|
|
5
|
+
import { shouldLookupImpersonation } from "./lookup.js";
|
|
6
|
+
//#region src/plugin/decorateAuth.ts
|
|
7
|
+
/**
|
|
8
|
+
* Runtime wrap of `payload.authStrategies` (assembled in `init()`, not a config
|
|
9
|
+
* field). Collection strategies run before `local-jwt` and return on the first
|
|
10
|
+
* user, so decorating only a collection strategy would miss every local-jwt
|
|
11
|
+
* login.
|
|
12
|
+
*
|
|
13
|
+
* No sid cache. A negative hit on the impersonator's sid would go stale the
|
|
14
|
+
* moment they start (findOpenBySid also matches impersonatorSid). Terminate
|
|
15
|
+
* must be visible on the next request, so positives stay uncached too.
|
|
16
|
+
*/
|
|
17
|
+
const decorateAuthStrategies = (payload, options) => {
|
|
18
|
+
payload.authStrategies = payload.authStrategies.map((strategy) => wrapStrategy({
|
|
19
|
+
options,
|
|
20
|
+
payload,
|
|
21
|
+
strategy
|
|
22
|
+
}));
|
|
23
|
+
};
|
|
24
|
+
const wrapStrategy = ({ options, payload, strategy }) => ({
|
|
25
|
+
name: strategy.name,
|
|
26
|
+
authenticate: async (args) => {
|
|
27
|
+
const result = await strategy.authenticate(args);
|
|
28
|
+
const user = result.user;
|
|
29
|
+
const sid = boundSid(user, options.session.binding);
|
|
30
|
+
if (!user || !sid) return result;
|
|
31
|
+
if (!shouldLookupImpersonation({
|
|
32
|
+
headers: args.headers,
|
|
33
|
+
options,
|
|
34
|
+
sid
|
|
35
|
+
})) return result;
|
|
36
|
+
const row = await cachedFindOpenBySid(sid, options, payload);
|
|
37
|
+
if (!row) return result;
|
|
38
|
+
if (isPastAbsoluteExpiry(row)) return expireAbsoluteCap({
|
|
39
|
+
canSetHeaders: args.canSetHeaders,
|
|
40
|
+
options,
|
|
41
|
+
payload,
|
|
42
|
+
result,
|
|
43
|
+
row,
|
|
44
|
+
sid,
|
|
45
|
+
user
|
|
46
|
+
});
|
|
47
|
+
const impersonator = relationOf(row.impersonator);
|
|
48
|
+
const target = relationOf(row.target);
|
|
49
|
+
if (options.decorateRequests && impersonator && target && sid === row.targetSid) user._impersonation = {
|
|
50
|
+
absoluteExpiresAt: row.absoluteExpiresAt ?? null,
|
|
51
|
+
id: row.id,
|
|
52
|
+
impersonator,
|
|
53
|
+
impersonatorEmail: row.impersonatorEmail,
|
|
54
|
+
impersonatorLocale: row.impersonatorLocale,
|
|
55
|
+
impersonatorTitle: row.impersonatorTitle,
|
|
56
|
+
mode: row.mode,
|
|
57
|
+
reason: row.reason,
|
|
58
|
+
startedAt: row.startedAt,
|
|
59
|
+
target,
|
|
60
|
+
targetEmail: row.targetEmail,
|
|
61
|
+
targetTitle: row.targetTitle
|
|
62
|
+
};
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* Parallel expiry must not clear `${cookiePrefix}-token`. That cookie is still
|
|
68
|
+
* the impersonator. Swap expiry clears it because it was overwritten on start.
|
|
69
|
+
*/
|
|
70
|
+
const expireAbsoluteCap = async ({ canSetHeaders, options, payload, result, row, sid, user }) => {
|
|
71
|
+
await closeAndRevoke({
|
|
72
|
+
endedBy: "expired",
|
|
73
|
+
options,
|
|
74
|
+
payload,
|
|
75
|
+
record: row
|
|
76
|
+
});
|
|
77
|
+
const isImpersonatorRequest = sid === row.impersonatorSid && sid !== row.targetSid;
|
|
78
|
+
if (!canSetHeaders) return isImpersonatorRequest ? result : { user: null };
|
|
79
|
+
const headers = result.responseHeaders ?? new Headers();
|
|
80
|
+
for (const cookie of await absoluteExpiryCookies({
|
|
81
|
+
options,
|
|
82
|
+
payload,
|
|
83
|
+
row,
|
|
84
|
+
sid,
|
|
85
|
+
user
|
|
86
|
+
})) headers.append("Set-Cookie", cookie);
|
|
87
|
+
if (isImpersonatorRequest) return {
|
|
88
|
+
...result,
|
|
89
|
+
responseHeaders: headers
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
responseHeaders: headers,
|
|
93
|
+
user: null
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
//#endregion
|
|
97
|
+
export { decorateAuthStrategies };
|
|
98
|
+
|
|
99
|
+
//# sourceMappingURL=decorateAuth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decorateAuth.js","names":[],"sources":["../../src/plugin/decorateAuth.ts"],"sourcesContent":["import type { AuthStrategy, Payload } from 'payload'\n\nimport { absoluteExpiryCookies } from '../auth/expiryCookies'\nimport { closeAndRevoke } from '../session/close'\nimport { cachedFindOpenBySid, isPastAbsoluteExpiry, relationOf } from '../session/resolve'\nimport type {\n\tImpersonatedUser,\n\tImpersonationActor,\n\tImpersonationRecord,\n\tResolvedOptions,\n} from '../types'\nimport { boundSid } from '../types'\nimport { shouldLookupImpersonation } from './lookup'\n\n/**\n * Runtime wrap of `payload.authStrategies` (assembled in `init()`, not a config\n * field). Collection strategies run before `local-jwt` and return on the first\n * user, so decorating only a collection strategy would miss every local-jwt\n * login.\n *\n * No sid cache. A negative hit on the impersonator's sid would go stale the\n * moment they start (findOpenBySid also matches impersonatorSid). Terminate\n * must be visible on the next request, so positives stay uncached too.\n */\nexport const decorateAuthStrategies = (payload: Payload, options: ResolvedOptions): void => {\n\tpayload.authStrategies = payload.authStrategies.map((strategy) =>\n\t\twrapStrategy({ options, payload, strategy })\n\t)\n}\n\nconst wrapStrategy = ({\n\toptions,\n\tpayload,\n\tstrategy,\n}: {\n\toptions: ResolvedOptions\n\tpayload: Payload\n\tstrategy: AuthStrategy\n}): AuthStrategy => ({\n\tname: strategy.name,\n\tauthenticate: async (args) => {\n\t\tconst result = await strategy.authenticate(args)\n\t\tconst user = result.user as ImpersonatedUser | null\n\t\tconst sid = boundSid(user, options.session.binding)\n\t\tif (!user || !sid) {\n\t\t\treturn result\n\t\t}\n\n\t\tif (!shouldLookupImpersonation({ headers: args.headers, options, sid })) {\n\t\t\treturn result\n\t\t}\n\n\t\tconst row = await cachedFindOpenBySid(sid, options, payload)\n\t\tif (!row) {\n\t\t\treturn result\n\t\t}\n\n\t\tif (isPastAbsoluteExpiry(row)) {\n\t\t\treturn expireAbsoluteCap({\n\t\t\t\tcanSetHeaders: args.canSetHeaders,\n\t\t\t\toptions,\n\t\t\t\tpayload,\n\t\t\t\tresult,\n\t\t\t\trow,\n\t\t\t\tsid,\n\t\t\t\tuser,\n\t\t\t})\n\t\t}\n\n\t\tconst impersonator = relationOf(row.impersonator)\n\t\tconst target = relationOf(row.target)\n\t\tif (options.decorateRequests && impersonator && target && sid === row.targetSid) {\n\t\t\tconst actor: ImpersonationActor = {\n\t\t\t\tabsoluteExpiresAt: row.absoluteExpiresAt ?? null,\n\t\t\t\tid: row.id,\n\t\t\t\timpersonator,\n\t\t\t\timpersonatorEmail: row.impersonatorEmail,\n\t\t\t\timpersonatorLocale: row.impersonatorLocale,\n\t\t\t\timpersonatorTitle: row.impersonatorTitle,\n\t\t\t\tmode: row.mode,\n\t\t\t\treason: row.reason,\n\t\t\t\tstartedAt: row.startedAt,\n\t\t\t\ttarget,\n\t\t\t\ttargetEmail: row.targetEmail,\n\t\t\t\ttargetTitle: row.targetTitle,\n\t\t\t}\n\t\t\tuser._impersonation = actor\n\t\t}\n\n\t\treturn result\n\t},\n})\n\n/**\n * Parallel expiry must not clear `${cookiePrefix}-token`. That cookie is still\n * the impersonator. Swap expiry clears it because it was overwritten on start.\n */\nconst expireAbsoluteCap = async ({\n\tcanSetHeaders,\n\toptions,\n\tpayload,\n\tresult,\n\trow,\n\tsid,\n\tuser,\n}: {\n\tcanSetHeaders: boolean | undefined\n\toptions: ResolvedOptions\n\tpayload: Payload\n\tresult: Awaited<ReturnType<AuthStrategy['authenticate']>>\n\trow: ImpersonationRecord\n\tsid: string\n\tuser: ImpersonatedUser\n}): Promise<Awaited<ReturnType<AuthStrategy['authenticate']>>> => {\n\tawait closeAndRevoke({ endedBy: 'expired', options, payload, record: row })\n\n\tconst isImpersonatorRequest = sid === row.impersonatorSid && sid !== row.targetSid\n\tif (!canSetHeaders) {\n\t\treturn isImpersonatorRequest ? result : { user: null }\n\t}\n\n\tconst headers = result.responseHeaders ?? new Headers()\n\tfor (const cookie of await absoluteExpiryCookies({ options, payload, row, sid, user })) {\n\t\theaders.append('Set-Cookie', cookie)\n\t}\n\tif (isImpersonatorRequest) {\n\t\treturn { ...result, responseHeaders: headers }\n\t}\n\treturn { responseHeaders: headers, user: null }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAwBA,MAAa,0BAA0B,SAAkB,YAAmC;CAC3F,QAAQ,iBAAiB,QAAQ,eAAe,KAAK,aACpD,aAAa;EAAE;EAAS;EAAS;CAAS,CAAC,CAC5C;AACD;AAEA,MAAM,gBAAgB,EACrB,SACA,SACA,gBAKoB;CACpB,MAAM,SAAS;CACf,cAAc,OAAO,SAAS;EAC7B,MAAM,SAAS,MAAM,SAAS,aAAa,IAAI;EAC/C,MAAM,OAAO,OAAO;EACpB,MAAM,MAAM,SAAS,MAAM,QAAQ,QAAQ,OAAO;EAClD,IAAI,CAAC,QAAQ,CAAC,KACb,OAAO;EAGR,IAAI,CAAC,0BAA0B;GAAE,SAAS,KAAK;GAAS;GAAS;EAAI,CAAC,GACrE,OAAO;EAGR,MAAM,MAAM,MAAM,oBAAoB,KAAK,SAAS,OAAO;EAC3D,IAAI,CAAC,KACJ,OAAO;EAGR,IAAI,qBAAqB,GAAG,GAC3B,OAAO,kBAAkB;GACxB,eAAe,KAAK;GACpB;GACA;GACA;GACA;GACA;GACA;EACD,CAAC;EAGF,MAAM,eAAe,WAAW,IAAI,YAAY;EAChD,MAAM,SAAS,WAAW,IAAI,MAAM;EACpC,IAAI,QAAQ,oBAAoB,gBAAgB,UAAU,QAAQ,IAAI,WAerE,KAAK,iBAAiB;GAbrB,mBAAmB,IAAI,qBAAqB;GAC5C,IAAI,IAAI;GACR;GACA,mBAAmB,IAAI;GACvB,oBAAoB,IAAI;GACxB,mBAAmB,IAAI;GACvB,MAAM,IAAI;GACV,QAAQ,IAAI;GACZ,WAAW,IAAI;GACf;GACA,aAAa,IAAI;GACjB,aAAa,IAAI;EAEQ;EAG3B,OAAO;CACR;AACD;;;;;AAMA,MAAM,oBAAoB,OAAO,EAChC,eACA,SACA,SACA,QACA,KACA,KACA,WASiE;CACjE,MAAM,eAAe;EAAE,SAAS;EAAW;EAAS;EAAS,QAAQ;CAAI,CAAC;CAE1E,MAAM,wBAAwB,QAAQ,IAAI,mBAAmB,QAAQ,IAAI;CACzE,IAAI,CAAC,eACJ,OAAO,wBAAwB,SAAS,EAAE,MAAM,KAAK;CAGtD,MAAM,UAAU,OAAO,mBAAmB,IAAI,QAAQ;CACtD,KAAK,MAAM,UAAU,MAAM,sBAAsB;EAAE;EAAS;EAAS;EAAK;EAAK;CAAK,CAAC,GACpF,QAAQ,OAAO,cAAc,MAAM;CAEpC,IAAI,uBACH,OAAO;EAAE,GAAG;EAAQ,iBAAiB;CAAQ;CAE9C,OAAO;EAAE,iBAAiB;EAAS,MAAM;CAAK;AAC/C"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import "./constants.js";
|
|
2
|
+
import { parseCookies } from "payload/shared";
|
|
3
|
+
//#region src/plugin/lookup.ts
|
|
4
|
+
/**
|
|
5
|
+
* Whether this authenticated request can belong to an impersonation row.
|
|
6
|
+
* Custom `session.issue` skips the prefix, so those hosts always look up.
|
|
7
|
+
*/
|
|
8
|
+
const shouldLookupImpersonation = ({ headers, options, sid }) => {
|
|
9
|
+
if (options.session.issue) return true;
|
|
10
|
+
if (sid.startsWith("imp_")) return true;
|
|
11
|
+
return parseCookies(headers).has(options.hintCookieName);
|
|
12
|
+
};
|
|
13
|
+
const readHintCookie = async (name) => {
|
|
14
|
+
try {
|
|
15
|
+
return (await (await import("next/headers")).cookies()).get(name)?.value;
|
|
16
|
+
} catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
export { readHintCookie, shouldLookupImpersonation };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=lookup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lookup.js","names":[],"sources":["../../src/plugin/lookup.ts"],"sourcesContent":["import { parseCookies } from 'payload/shared'\n\nimport type { ResolvedOptions } from '../types'\nimport { IMPERSONATION_SID_PREFIX } from './constants'\n\n/**\n * Whether this authenticated request can belong to an impersonation row.\n * Custom `session.issue` skips the prefix, so those hosts always look up.\n */\nexport const shouldLookupImpersonation = ({\n\theaders,\n\toptions,\n\tsid,\n}: {\n\theaders: Headers\n\toptions: Pick<ResolvedOptions, 'hintCookieName' | 'session'>\n\tsid: string\n}): boolean => {\n\tif (options.session.issue) {\n\t\treturn true\n\t}\n\tif (sid.startsWith(IMPERSONATION_SID_PREFIX)) {\n\t\treturn true\n\t}\n\treturn parseCookies(headers).has(options.hintCookieName)\n}\n\nexport const readHintCookie = async (name: string): Promise<string | undefined> => {\n\ttry {\n\t\tconst spec: string = 'next/headers'\n\t\tconst mod = (await import(spec)) as {\n\t\t\tcookies: () =>\n\t\t\t\t| { get: (key: string) => { value?: string } | undefined }\n\t\t\t\t| Promise<{ get: (key: string) => { value?: string } | undefined }>\n\t\t}\n\t\tconst store = await mod.cookies()\n\t\treturn store.get(name)?.value\n\t} catch {\n\t\treturn undefined\n\t}\n}\n"],"mappings":";;;;;;;AASA,MAAa,6BAA6B,EACzC,SACA,SACA,UAKc;CACd,IAAI,QAAQ,QAAQ,OACnB,OAAO;CAER,IAAI,IAAI,WAAA,MAAmC,GAC1C,OAAO;CAER,OAAO,aAAa,OAAO,EAAE,IAAI,QAAQ,cAAc;AACxD;AAEA,MAAa,iBAAiB,OAAO,SAA8C;CAClF,IAAI;EAQH,QAAO,OADa,MALD,OAAO,iBAKF,QAAQ,GACnB,IAAI,IAAI,GAAG;CACzB,QAAQ;EACP;CACD;AACD"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { RESERVED_API_SEGMENTS, defaultClearOnSwitch } from "./constants.js";
|
|
2
|
+
//#region src/plugin/normalizeOptions.ts
|
|
3
|
+
const PREFIX = "@10x-media/impersonation:";
|
|
4
|
+
const resolveAdminUserSlug = (config) => config.admin?.user ?? (config.collections ?? []).find(({ auth }) => Boolean(auth))?.slug ?? "users";
|
|
5
|
+
const authObject = (collection) => {
|
|
6
|
+
if (!collection?.auth) return null;
|
|
7
|
+
return typeof collection.auth === "object" ? collection.auth : {};
|
|
8
|
+
};
|
|
9
|
+
const firstApiSegment = (apiPath) => apiPath.replace(/^\//, "").split("/").filter(Boolean)[0] ?? "";
|
|
10
|
+
const resolveUi = (ui) => {
|
|
11
|
+
if (ui === false) return {
|
|
12
|
+
bar: false,
|
|
13
|
+
cardEmail: false,
|
|
14
|
+
documentAction: false,
|
|
15
|
+
headerAction: false,
|
|
16
|
+
recordAction: false,
|
|
17
|
+
sessionsCollection: false,
|
|
18
|
+
userLabel: "title"
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
bar: ui?.bar !== false,
|
|
22
|
+
card: ui?.card,
|
|
23
|
+
cardEmail: ui?.cardEmail !== false,
|
|
24
|
+
documentAction: ui?.documentAction !== false,
|
|
25
|
+
headerAction: ui?.headerAction !== false,
|
|
26
|
+
recordAction: ui?.recordAction !== false,
|
|
27
|
+
sessionsCollection: ui?.sessionsCollection === true,
|
|
28
|
+
userLabel: ui?.userLabel === "email" ? "email" : "title"
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Fill defaults and refuse a config that cannot bind or revoke sessions.
|
|
33
|
+
* `disabled: true` never reaches here.
|
|
34
|
+
*/
|
|
35
|
+
const normalizeOptions = (options, config) => {
|
|
36
|
+
if (typeof options.access?.impersonate !== "function") throw new Error(`${PREFIX} access.impersonate must be a function`);
|
|
37
|
+
const cookiePrefix = config.cookiePrefix ?? "payload";
|
|
38
|
+
const collectionSlug = options.collectionSlug ?? "impersonation-sessions";
|
|
39
|
+
const apiPath = options.apiPath ?? "/impersonation";
|
|
40
|
+
const hintCookieName = options.hintCookieName ?? "impersonation-hint";
|
|
41
|
+
if (hintCookieName.startsWith(cookiePrefix)) throw new Error(`${PREFIX} hintCookieName "${hintCookieName}" must not start with cookiePrefix "${cookiePrefix}". @payloadcms/next refresh/logout take the first cookie whose name starts with the prefix.`);
|
|
42
|
+
const segment = firstApiSegment(apiPath);
|
|
43
|
+
const occupied = new Set([
|
|
44
|
+
...RESERVED_API_SEGMENTS,
|
|
45
|
+
collectionSlug,
|
|
46
|
+
...(config.collections ?? []).map(({ slug }) => slug),
|
|
47
|
+
...(config.globals ?? []).map(({ slug }) => slug)
|
|
48
|
+
]);
|
|
49
|
+
if (!segment || occupied.has(segment)) throw new Error(`${PREFIX} apiPath "${apiPath}" collides with a collection, global, or reserved route segment "${segment}". handleEndpoints would treat it as that collection and drop these endpoints.`);
|
|
50
|
+
const adminSlug = resolveAdminUserSlug(config);
|
|
51
|
+
const adminAuth = authObject((config.collections ?? []).find(({ slug }) => slug === adminSlug));
|
|
52
|
+
if (adminAuth?.useSessions === false) throw new Error(`${PREFIX} the admin collection "${adminSlug}" has useSessions: false, so there is no sid to bind or revoke.`);
|
|
53
|
+
if (adminAuth?.disableLocalStrategy) throw new Error(`${PREFIX} the admin collection "${adminSlug}" has disableLocalStrategy, so the plugin cannot mint or restore a session.`);
|
|
54
|
+
const deleteAfterDays = options.retention ? options.retention.deleteAfterDays : void 0;
|
|
55
|
+
if (options.retention && (typeof deleteAfterDays !== "number" || !Number.isFinite(deleteAfterDays) || deleteAfterDays <= 0)) throw new Error(`${PREFIX} retention.deleteAfterDays must be a positive finite number.`);
|
|
56
|
+
return {
|
|
57
|
+
access: {
|
|
58
|
+
filterTargets: options.access.filterTargets,
|
|
59
|
+
impersonate: options.access.impersonate,
|
|
60
|
+
readRecords: options.access.readRecords ?? (() => false),
|
|
61
|
+
terminate: options.access.terminate ?? (() => false)
|
|
62
|
+
},
|
|
63
|
+
apiPath,
|
|
64
|
+
collectionSlug,
|
|
65
|
+
collectionOverrides: options.collection?.overrides,
|
|
66
|
+
cookies: { clearOnSwitch: options.cookies?.clearOnSwitch ?? defaultClearOnSwitch(cookiePrefix) },
|
|
67
|
+
decorateRequests: options.decorateRequests !== false,
|
|
68
|
+
hintCookieName,
|
|
69
|
+
maxDuration: options.maxDuration,
|
|
70
|
+
onEnd: options.onEnd,
|
|
71
|
+
onStart: options.onStart,
|
|
72
|
+
reason: options.reason ?? "off",
|
|
73
|
+
retention: options.retention ? options.retention : void 0,
|
|
74
|
+
security: { trustedOrigins: options.security?.trustedOrigins ?? [] },
|
|
75
|
+
session: options.session ?? {},
|
|
76
|
+
targets: options.targets,
|
|
77
|
+
ui: resolveUi(options.ui)
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
//#endregion
|
|
81
|
+
export { normalizeOptions };
|
|
82
|
+
|
|
83
|
+
//# sourceMappingURL=normalizeOptions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalizeOptions.js","names":[],"sources":["../../src/plugin/normalizeOptions.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug, Config } from 'payload'\n\nimport type { EnabledOptions, ResolvedOptions, ResolvedUi } from '../types'\nimport {\n\tDEFAULT_API_PATH,\n\tDEFAULT_COLLECTION_SLUG,\n\tDEFAULT_COOKIE_PREFIX,\n\tDEFAULT_HINT_COOKIE,\n\tdefaultClearOnSwitch,\n\tRESERVED_API_SEGMENTS,\n} from './constants'\n\nconst PREFIX = '@10x-media/impersonation:'\n\nexport const resolveAdminUserSlug = (config: Config): string =>\n\tconfig.admin?.user ??\n\t(config.collections ?? []).find(({ auth }) => Boolean(auth))?.slug ??\n\t'users'\n\nconst authObject = (collection: CollectionConfig | undefined) => {\n\tif (!collection?.auth) {\n\t\treturn null\n\t}\n\treturn typeof collection.auth === 'object' ? collection.auth : {}\n}\n\nconst firstApiSegment = (apiPath: string): string =>\n\tapiPath.replace(/^\\//, '').split('/').filter(Boolean)[0] ?? ''\n\nconst resolveUi = (ui: EnabledOptions['ui']): ResolvedUi => {\n\tif (ui === false) {\n\t\treturn {\n\t\t\tbar: false,\n\t\t\tcardEmail: false,\n\t\t\tdocumentAction: false,\n\t\t\theaderAction: false,\n\t\t\trecordAction: false,\n\t\t\tsessionsCollection: false,\n\t\t\tuserLabel: 'title',\n\t\t}\n\t}\n\treturn {\n\t\tbar: ui?.bar !== false,\n\t\tcard: ui?.card,\n\t\tcardEmail: ui?.cardEmail !== false,\n\t\tdocumentAction: ui?.documentAction !== false,\n\t\theaderAction: ui?.headerAction !== false,\n\t\trecordAction: ui?.recordAction !== false,\n\t\tsessionsCollection: ui?.sessionsCollection === true,\n\t\tuserLabel: ui?.userLabel === 'email' ? 'email' : 'title',\n\t}\n}\n\n/**\n * Fill defaults and refuse a config that cannot bind or revoke sessions.\n * `disabled: true` never reaches here.\n */\nexport const normalizeOptions = (options: EnabledOptions, config: Config): ResolvedOptions => {\n\tif (typeof options.access?.impersonate !== 'function') {\n\t\tthrow new Error(`${PREFIX} access.impersonate must be a function`)\n\t}\n\n\tconst cookiePrefix = config.cookiePrefix ?? DEFAULT_COOKIE_PREFIX\n\tconst collectionSlug = (options.collectionSlug ?? DEFAULT_COLLECTION_SLUG) as CollectionSlug\n\tconst apiPath = options.apiPath ?? DEFAULT_API_PATH\n\tconst hintCookieName = options.hintCookieName ?? DEFAULT_HINT_COOKIE\n\n\tif (hintCookieName.startsWith(cookiePrefix)) {\n\t\tthrow new Error(\n\t\t\t`${PREFIX} hintCookieName \"${hintCookieName}\" must not start with cookiePrefix \"${cookiePrefix}\". @payloadcms/next refresh/logout take the first cookie whose name starts with the prefix.`\n\t\t)\n\t}\n\n\tconst segment = firstApiSegment(apiPath)\n\tconst occupied = new Set<string>([\n\t\t...RESERVED_API_SEGMENTS,\n\t\tcollectionSlug,\n\t\t...(config.collections ?? []).map(({ slug }) => slug),\n\t\t...(config.globals ?? []).map(({ slug }) => slug),\n\t])\n\tif (!segment || occupied.has(segment)) {\n\t\tthrow new Error(\n\t\t\t`${PREFIX} apiPath \"${apiPath}\" collides with a collection, global, or reserved route segment \"${segment}\". handleEndpoints would treat it as that collection and drop these endpoints.`\n\t\t)\n\t}\n\n\tconst adminSlug = resolveAdminUserSlug(config)\n\tconst adminCollection = (config.collections ?? []).find(({ slug }) => slug === adminSlug)\n\tconst adminAuth = authObject(adminCollection)\n\tif (adminAuth?.useSessions === false) {\n\t\tthrow new Error(\n\t\t\t`${PREFIX} the admin collection \"${adminSlug}\" has useSessions: false, so there is no sid to bind or revoke.`\n\t\t)\n\t}\n\tif (adminAuth?.disableLocalStrategy) {\n\t\tthrow new Error(\n\t\t\t`${PREFIX} the admin collection \"${adminSlug}\" has disableLocalStrategy, so the plugin cannot mint or restore a session.`\n\t\t)\n\t}\n\n\tconst deleteAfterDays = options.retention ? options.retention.deleteAfterDays : undefined\n\tif (\n\t\toptions.retention &&\n\t\t(typeof deleteAfterDays !== 'number' ||\n\t\t\t!Number.isFinite(deleteAfterDays) ||\n\t\t\tdeleteAfterDays <= 0)\n\t) {\n\t\tthrow new Error(`${PREFIX} retention.deleteAfterDays must be a positive finite number.`)\n\t}\n\n\treturn {\n\t\taccess: {\n\t\t\tfilterTargets: options.access.filterTargets,\n\t\t\timpersonate: options.access.impersonate,\n\t\t\treadRecords: options.access.readRecords ?? (() => false),\n\t\t\tterminate: options.access.terminate ?? (() => false),\n\t\t},\n\t\tapiPath,\n\t\tcollectionSlug,\n\t\tcollectionOverrides: options.collection?.overrides,\n\t\tcookies: {\n\t\t\tclearOnSwitch: options.cookies?.clearOnSwitch ?? defaultClearOnSwitch(cookiePrefix),\n\t\t},\n\t\tdecorateRequests: options.decorateRequests !== false,\n\t\thintCookieName,\n\t\tmaxDuration: options.maxDuration,\n\t\tonEnd: options.onEnd,\n\t\tonStart: options.onStart,\n\t\treason: options.reason ?? 'off',\n\t\tretention: options.retention ? options.retention : undefined,\n\t\tsecurity: { trustedOrigins: options.security?.trustedOrigins ?? [] },\n\t\tsession: options.session ?? {},\n\t\ttargets: options.targets,\n\t\tui: resolveUi(options.ui),\n\t}\n}\n"],"mappings":";;AAYA,MAAM,SAAS;AAEf,MAAa,wBAAwB,WACpC,OAAO,OAAO,SACb,OAAO,eAAe,CAAC,GAAG,MAAM,EAAE,WAAW,QAAQ,IAAI,CAAC,GAAG,QAC9D;AAED,MAAM,cAAc,eAA6C;CAChE,IAAI,CAAC,YAAY,MAChB,OAAO;CAER,OAAO,OAAO,WAAW,SAAS,WAAW,WAAW,OAAO,CAAC;AACjE;AAEA,MAAM,mBAAmB,YACxB,QAAQ,QAAQ,OAAO,EAAE,EAAE,MAAM,GAAG,EAAE,OAAO,OAAO,EAAE,MAAM;AAE7D,MAAM,aAAa,OAAyC;CAC3D,IAAI,OAAO,OACV,OAAO;EACN,KAAK;EACL,WAAW;EACX,gBAAgB;EAChB,cAAc;EACd,cAAc;EACd,oBAAoB;EACpB,WAAW;CACZ;CAED,OAAO;EACN,KAAK,IAAI,QAAQ;EACjB,MAAM,IAAI;EACV,WAAW,IAAI,cAAc;EAC7B,gBAAgB,IAAI,mBAAmB;EACvC,cAAc,IAAI,iBAAiB;EACnC,cAAc,IAAI,iBAAiB;EACnC,oBAAoB,IAAI,uBAAuB;EAC/C,WAAW,IAAI,cAAc,UAAU,UAAU;CAClD;AACD;;;;;AAMA,MAAa,oBAAoB,SAAyB,WAAoC;CAC7F,IAAI,OAAO,QAAQ,QAAQ,gBAAgB,YAC1C,MAAM,IAAI,MAAM,GAAG,OAAO,uCAAuC;CAGlE,MAAM,eAAe,OAAO,gBAAA;CAC5B,MAAM,iBAAkB,QAAQ,kBAAA;CAChC,MAAM,UAAU,QAAQ,WAAA;CACxB,MAAM,iBAAiB,QAAQ,kBAAA;CAE/B,IAAI,eAAe,WAAW,YAAY,GACzC,MAAM,IAAI,MACT,GAAG,OAAO,mBAAmB,eAAe,sCAAsC,aAAa,4FAChG;CAGD,MAAM,UAAU,gBAAgB,OAAO;CACvC,MAAM,WAAW,IAAI,IAAY;EAChC,GAAG;EACH;EACA,IAAI,OAAO,eAAe,CAAC,GAAG,KAAK,EAAE,WAAW,IAAI;EACpD,IAAI,OAAO,WAAW,CAAC,GAAG,KAAK,EAAE,WAAW,IAAI;CACjD,CAAC;CACD,IAAI,CAAC,WAAW,SAAS,IAAI,OAAO,GACnC,MAAM,IAAI,MACT,GAAG,OAAO,YAAY,QAAQ,mEAAmE,QAAQ,+EAC1G;CAGD,MAAM,YAAY,qBAAqB,MAAM;CAE7C,MAAM,YAAY,YADO,OAAO,eAAe,CAAC,GAAG,MAAM,EAAE,WAAW,SAAS,SACpC,CAAC;CAC5C,IAAI,WAAW,gBAAgB,OAC9B,MAAM,IAAI,MACT,GAAG,OAAO,yBAAyB,UAAU,gEAC9C;CAED,IAAI,WAAW,sBACd,MAAM,IAAI,MACT,GAAG,OAAO,yBAAyB,UAAU,4EAC9C;CAGD,MAAM,kBAAkB,QAAQ,YAAY,QAAQ,UAAU,kBAAkB,KAAA;CAChF,IACC,QAAQ,cACP,OAAO,oBAAoB,YAC3B,CAAC,OAAO,SAAS,eAAe,KAChC,mBAAmB,IAEpB,MAAM,IAAI,MAAM,GAAG,OAAO,6DAA6D;CAGxF,OAAO;EACN,QAAQ;GACP,eAAe,QAAQ,OAAO;GAC9B,aAAa,QAAQ,OAAO;GAC5B,aAAa,QAAQ,OAAO,sBAAsB;GAClD,WAAW,QAAQ,OAAO,oBAAoB;EAC/C;EACA;EACA;EACA,qBAAqB,QAAQ,YAAY;EACzC,SAAS,EACR,eAAe,QAAQ,SAAS,iBAAiB,qBAAqB,YAAY,EACnF;EACA,kBAAkB,QAAQ,qBAAqB;EAC/C;EACA,aAAa,QAAQ;EACrB,OAAO,QAAQ;EACf,SAAS,QAAQ;EACjB,QAAQ,QAAQ,UAAU;EAC1B,WAAW,QAAQ,YAAY,QAAQ,YAAY,KAAA;EACnD,UAAU,EAAE,gBAAgB,QAAQ,UAAU,kBAAkB,CAAC,EAAE;EACnE,SAAS,QAAQ,WAAW,CAAC;EAC7B,SAAS,QAAQ;EACjB,IAAI,UAAU,QAAQ,EAAE;CACzB;AACD"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { isStartableAuthCollection } from "./startable.js";
|
|
2
|
+
//#region src/plugin/registerAdmin.ts
|
|
3
|
+
const pathOf = (component) => {
|
|
4
|
+
if (typeof component === "string") return component;
|
|
5
|
+
if (component && typeof component === "object" && "path" in component) return component.exportName ? `${component.path}#${component.exportName}` : component.path;
|
|
6
|
+
};
|
|
7
|
+
const ACTION = "@10x-media/impersonation/rsc#ImpersonationAction";
|
|
8
|
+
const PROVIDER = "@10x-media/impersonation/rsc#ImpersonationProvider";
|
|
9
|
+
const DOCUMENT_ACTION = "@10x-media/impersonation/client#ImpersonationDocumentButton";
|
|
10
|
+
const registerAdmin = (config, options) => {
|
|
11
|
+
config.admin ??= {};
|
|
12
|
+
config.admin.components ??= {};
|
|
13
|
+
config.admin.components.providers = [...config.admin.components.providers ?? [], PROVIDER];
|
|
14
|
+
const cardPath = options.ui.card ? pathOf(options.ui.card) : void 0;
|
|
15
|
+
if (cardPath) config.admin.dependencies = {
|
|
16
|
+
...config.admin.dependencies,
|
|
17
|
+
"impersonation-card": {
|
|
18
|
+
path: cardPath,
|
|
19
|
+
type: "component"
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
if (options.ui.headerAction) config.admin.components.actions = [...config.admin.components.actions ?? [], ACTION];
|
|
23
|
+
if (!options.ui.documentAction) return;
|
|
24
|
+
config.collections = (config.collections ?? []).map((collection) => {
|
|
25
|
+
if (!isStartableAuthCollection(collection, options)) return collection;
|
|
26
|
+
return withDocumentAction(collection);
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
const withDocumentAction = (collection) => {
|
|
30
|
+
const edit = collection.admin?.components?.edit;
|
|
31
|
+
const existing = edit && typeof edit === "object" && "beforeDocumentControls" in edit ? edit.beforeDocumentControls : void 0;
|
|
32
|
+
return {
|
|
33
|
+
...collection,
|
|
34
|
+
admin: {
|
|
35
|
+
...collection.admin,
|
|
36
|
+
components: {
|
|
37
|
+
...collection.admin?.components,
|
|
38
|
+
edit: {
|
|
39
|
+
...typeof edit === "object" ? edit : {},
|
|
40
|
+
beforeDocumentControls: [...existing ?? [], DOCUMENT_ACTION]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { registerAdmin };
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=registerAdmin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registerAdmin.js","names":[],"sources":["../../src/plugin/registerAdmin.ts"],"sourcesContent":["import type { CollectionConfig, Config, PayloadComponent } from 'payload'\n\nimport type { ResolvedOptions } from '../types'\nimport { isStartableAuthCollection } from './startable'\n\nexport const pathOf = (component: PayloadComponent): string | undefined => {\n\tif (typeof component === 'string') {\n\t\treturn component\n\t}\n\tif (component && typeof component === 'object' && 'path' in component) {\n\t\treturn component.exportName ? `${component.path}#${component.exportName}` : component.path\n\t}\n\treturn undefined\n}\n\nconst ACTION = '@10x-media/impersonation/rsc#ImpersonationAction'\nconst PROVIDER = '@10x-media/impersonation/rsc#ImpersonationProvider'\nconst DOCUMENT_ACTION = '@10x-media/impersonation/client#ImpersonationDocumentButton'\n\nexport const registerAdmin = (config: Config, options: ResolvedOptions): void => {\n\tconfig.admin ??= {}\n\tconfig.admin.components ??= {}\n\tconfig.admin.components.providers = [...(config.admin.components.providers ?? []), PROVIDER]\n\n\tconst cardPath = options.ui.card ? pathOf(options.ui.card) : undefined\n\tif (cardPath) {\n\t\tconfig.admin.dependencies = {\n\t\t\t...config.admin.dependencies,\n\t\t\t'impersonation-card': { path: cardPath, type: 'component' },\n\t\t}\n\t}\n\tif (options.ui.headerAction) {\n\t\tconfig.admin.components.actions = [...(config.admin.components.actions ?? []), ACTION]\n\t}\n\n\tif (!options.ui.documentAction) {\n\t\treturn\n\t}\n\n\tconfig.collections = (config.collections ?? []).map((collection) => {\n\t\tif (!isStartableAuthCollection(collection, options)) {\n\t\t\treturn collection\n\t\t}\n\t\treturn withDocumentAction(collection)\n\t})\n}\n\nconst withDocumentAction = (collection: CollectionConfig): CollectionConfig => {\n\tconst edit = collection.admin?.components?.edit\n\tconst existing = (\n\t\tedit && typeof edit === 'object' && 'beforeDocumentControls' in edit\n\t\t\t? edit.beforeDocumentControls\n\t\t\t: undefined\n\t) as string[] | undefined\n\n\treturn {\n\t\t...collection,\n\t\tadmin: {\n\t\t\t...collection.admin,\n\t\t\tcomponents: {\n\t\t\t\t...collection.admin?.components,\n\t\t\t\tedit: {\n\t\t\t\t\t...(typeof edit === 'object' ? edit : {}),\n\t\t\t\t\tbeforeDocumentControls: [...(existing ?? []), DOCUMENT_ACTION],\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n"],"mappings":";;AAKA,MAAa,UAAU,cAAoD;CAC1E,IAAI,OAAO,cAAc,UACxB,OAAO;CAER,IAAI,aAAa,OAAO,cAAc,YAAY,UAAU,WAC3D,OAAO,UAAU,aAAa,GAAG,UAAU,KAAK,GAAG,UAAU,eAAe,UAAU;AAGxF;AAEA,MAAM,SAAS;AACf,MAAM,WAAW;AACjB,MAAM,kBAAkB;AAExB,MAAa,iBAAiB,QAAgB,YAAmC;CAChF,OAAO,UAAU,CAAC;CAClB,OAAO,MAAM,eAAe,CAAC;CAC7B,OAAO,MAAM,WAAW,YAAY,CAAC,GAAI,OAAO,MAAM,WAAW,aAAa,CAAC,GAAI,QAAQ;CAE3F,MAAM,WAAW,QAAQ,GAAG,OAAO,OAAO,QAAQ,GAAG,IAAI,IAAI,KAAA;CAC7D,IAAI,UACH,OAAO,MAAM,eAAe;EAC3B,GAAG,OAAO,MAAM;EAChB,sBAAsB;GAAE,MAAM;GAAU,MAAM;EAAY;CAC3D;CAED,IAAI,QAAQ,GAAG,cACd,OAAO,MAAM,WAAW,UAAU,CAAC,GAAI,OAAO,MAAM,WAAW,WAAW,CAAC,GAAI,MAAM;CAGtF,IAAI,CAAC,QAAQ,GAAG,gBACf;CAGD,OAAO,eAAe,OAAO,eAAe,CAAC,GAAG,KAAK,eAAe;EACnE,IAAI,CAAC,0BAA0B,YAAY,OAAO,GACjD,OAAO;EAER,OAAO,mBAAmB,UAAU;CACrC,CAAC;AACF;AAEA,MAAM,sBAAsB,eAAmD;CAC9E,MAAM,OAAO,WAAW,OAAO,YAAY;CAC3C,MAAM,WACL,QAAQ,OAAO,SAAS,YAAY,4BAA4B,OAC7D,KAAK,yBACL,KAAA;CAGJ,OAAO;EACN,GAAG;EACH,OAAO;GACN,GAAG,WAAW;GACd,YAAY;IACX,GAAG,WAAW,OAAO;IACrB,MAAM;KACL,GAAI,OAAO,SAAS,WAAW,OAAO,CAAC;KACvC,wBAAwB,CAAC,GAAI,YAAY,CAAC,GAAI,eAAe;IAC9D;GACD;EACD;CACD;AACD"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { ADMIN_GROUP } from "./constants.js";
|
|
2
|
+
import { keys } from "../translations/keys.js";
|
|
3
|
+
import { labelForKey } from "../translations/server.js";
|
|
4
|
+
//#region src/plugin/registerCollection.ts
|
|
5
|
+
const deny = () => false;
|
|
6
|
+
const buildRecordsCollection = ({ authSlugs, options }) => {
|
|
7
|
+
const relationTo = authSlugs.length > 0 ? authSlugs : ["users"];
|
|
8
|
+
return {
|
|
9
|
+
slug: options.collectionSlug,
|
|
10
|
+
access: {
|
|
11
|
+
create: deny,
|
|
12
|
+
delete: deny,
|
|
13
|
+
read: options.access.readRecords,
|
|
14
|
+
update: deny
|
|
15
|
+
},
|
|
16
|
+
admin: {
|
|
17
|
+
defaultColumns: [
|
|
18
|
+
"target",
|
|
19
|
+
"impersonator",
|
|
20
|
+
"startedAt",
|
|
21
|
+
"endedAt",
|
|
22
|
+
"endedBy"
|
|
23
|
+
],
|
|
24
|
+
group: ADMIN_GROUP,
|
|
25
|
+
hidden: !options.ui.sessionsCollection,
|
|
26
|
+
useAsTitle: "targetTitle",
|
|
27
|
+
...options.ui.recordAction ? { components: { edit: { beforeDocumentControls: ["@10x-media/impersonation/client#EndSessionMenuItem"] } } } : {}
|
|
28
|
+
},
|
|
29
|
+
fields: [
|
|
30
|
+
{
|
|
31
|
+
name: "impersonator",
|
|
32
|
+
type: "relationship",
|
|
33
|
+
label: labelForKey(keys.fieldImpersonator),
|
|
34
|
+
relationTo,
|
|
35
|
+
required: true
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "target",
|
|
39
|
+
type: "relationship",
|
|
40
|
+
label: labelForKey(keys.fieldTarget),
|
|
41
|
+
relationTo,
|
|
42
|
+
required: true
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "impersonatorEmail",
|
|
46
|
+
type: "text",
|
|
47
|
+
index: true
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "targetEmail",
|
|
51
|
+
type: "text",
|
|
52
|
+
index: true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "impersonatorTitle",
|
|
56
|
+
type: "text"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "targetTitle",
|
|
60
|
+
type: "text"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "mode",
|
|
64
|
+
type: "select",
|
|
65
|
+
label: labelForKey(keys.fieldMode),
|
|
66
|
+
options: [{
|
|
67
|
+
label: "swap",
|
|
68
|
+
value: "swap"
|
|
69
|
+
}, {
|
|
70
|
+
label: "parallel",
|
|
71
|
+
value: "parallel"
|
|
72
|
+
}],
|
|
73
|
+
required: true
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "targetSid",
|
|
77
|
+
type: "text",
|
|
78
|
+
index: true,
|
|
79
|
+
required: true
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "impersonatorSid",
|
|
83
|
+
type: "text",
|
|
84
|
+
index: true,
|
|
85
|
+
required: true
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "impersonatorTenantCookie",
|
|
89
|
+
type: "text"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "startedAt",
|
|
93
|
+
type: "date",
|
|
94
|
+
label: labelForKey(keys.fieldStartedAt),
|
|
95
|
+
required: true
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "endedAt",
|
|
99
|
+
type: "date",
|
|
100
|
+
index: true,
|
|
101
|
+
label: labelForKey(keys.fieldEndedAt)
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "endedBy",
|
|
105
|
+
type: "select",
|
|
106
|
+
label: labelForKey(keys.fieldEndedBy),
|
|
107
|
+
options: [
|
|
108
|
+
{
|
|
109
|
+
label: "exit",
|
|
110
|
+
value: "exit"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
label: "logout",
|
|
114
|
+
value: "logout"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
label: "terminated",
|
|
118
|
+
value: "terminated"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
label: "expired",
|
|
122
|
+
value: "expired"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
label: "impersonatorGone",
|
|
126
|
+
value: "impersonatorGone"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
label: "targetGone",
|
|
130
|
+
value: "targetGone"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
label: "failed",
|
|
134
|
+
value: "failed"
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "reason",
|
|
140
|
+
maxLength: 500,
|
|
141
|
+
type: "text",
|
|
142
|
+
label: labelForKey(keys.fieldReason)
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "ip",
|
|
146
|
+
type: "text"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "userAgent",
|
|
150
|
+
type: "text"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "impersonatorLocale",
|
|
154
|
+
type: "text"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "absoluteExpiresAt",
|
|
158
|
+
type: "date"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "targetLocked",
|
|
162
|
+
type: "checkbox"
|
|
163
|
+
}
|
|
164
|
+
],
|
|
165
|
+
indexes: [{ fields: ["targetSid", "endedAt"] }, { fields: ["impersonatorSid", "endedAt"] }],
|
|
166
|
+
labels: {
|
|
167
|
+
plural: labelForKey(keys.collectionPlural),
|
|
168
|
+
singular: labelForKey(keys.collectionSingular)
|
|
169
|
+
},
|
|
170
|
+
timestamps: true
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
const registerCollection = (config, options) => {
|
|
174
|
+
const built = buildRecordsCollection({
|
|
175
|
+
authSlugs: (config.collections ?? []).filter((collection) => Boolean(collection.auth)).map(({ slug }) => slug),
|
|
176
|
+
options
|
|
177
|
+
});
|
|
178
|
+
config.collections = [...config.collections ?? [], options.collectionOverrides ? options.collectionOverrides(built) : built];
|
|
179
|
+
};
|
|
180
|
+
//#endregion
|
|
181
|
+
export { registerCollection };
|
|
182
|
+
|
|
183
|
+
//# sourceMappingURL=registerCollection.js.map
|