@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.
Files changed (163) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +49 -0
  4. package/dist/access/filterTargets.js +51 -0
  5. package/dist/access/filterTargets.js.map +1 -0
  6. package/dist/auth/cookies.js +49 -0
  7. package/dist/auth/cookies.js.map +1 -0
  8. package/dist/auth/expiryCookies.js +59 -0
  9. package/dist/auth/expiryCookies.js.map +1 -0
  10. package/dist/auth/issue.js +126 -0
  11. package/dist/auth/issue.js.map +1 -0
  12. package/dist/auth/mode.js +69 -0
  13. package/dist/auth/mode.js.map +1 -0
  14. package/dist/auth/origin.js +55 -0
  15. package/dist/auth/origin.js.map +1 -0
  16. package/dist/auth/revoke.js +24 -0
  17. package/dist/auth/revoke.js.map +1 -0
  18. package/dist/auth/tenantCookie.js +74 -0
  19. package/dist/auth/tenantCookie.js.map +1 -0
  20. package/dist/endpoints/codes.d.ts +6 -0
  21. package/dist/endpoints/current.js +66 -0
  22. package/dist/endpoints/current.js.map +1 -0
  23. package/dist/endpoints/exit.js +242 -0
  24. package/dist/endpoints/exit.js.map +1 -0
  25. package/dist/endpoints/shared.js +76 -0
  26. package/dist/endpoints/shared.js.map +1 -0
  27. package/dist/endpoints/start.js +332 -0
  28. package/dist/endpoints/start.js.map +1 -0
  29. package/dist/endpoints/terminate.js +73 -0
  30. package/dist/endpoints/terminate.js.map +1 -0
  31. package/dist/exports/client.d.ts +9 -0
  32. package/dist/exports/client.js +10 -0
  33. package/dist/exports/i18n.d.ts +3 -0
  34. package/dist/exports/i18n.js +3 -0
  35. package/dist/exports/rsc.d.ts +4 -0
  36. package/dist/exports/rsc.js +4 -0
  37. package/dist/exports/types.d.ts +3 -0
  38. package/dist/exports/types.js +1 -0
  39. package/dist/getImpersonation.d.ts +49 -0
  40. package/dist/getImpersonation.js +78 -0
  41. package/dist/getImpersonation.js.map +1 -0
  42. package/dist/ids.js +27 -0
  43. package/dist/ids.js.map +1 -0
  44. package/dist/index.d.ts +20 -0
  45. package/dist/index.js +55 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/plugin/assertTargets.js +23 -0
  48. package/dist/plugin/assertTargets.js.map +1 -0
  49. package/dist/plugin/constants.d.ts +6 -0
  50. package/dist/plugin/constants.js +20 -0
  51. package/dist/plugin/constants.js.map +1 -0
  52. package/dist/plugin/decorateAuth.js +99 -0
  53. package/dist/plugin/decorateAuth.js.map +1 -0
  54. package/dist/plugin/lookup.js +23 -0
  55. package/dist/plugin/lookup.js.map +1 -0
  56. package/dist/plugin/normalizeOptions.js +83 -0
  57. package/dist/plugin/normalizeOptions.js.map +1 -0
  58. package/dist/plugin/registerAdmin.js +49 -0
  59. package/dist/plugin/registerAdmin.js.map +1 -0
  60. package/dist/plugin/registerCollection.js +183 -0
  61. package/dist/plugin/registerCollection.js.map +1 -0
  62. package/dist/plugin/registerEndpoints.js +19 -0
  63. package/dist/plugin/registerEndpoints.js.map +1 -0
  64. package/dist/plugin/registerHooks.js +59 -0
  65. package/dist/plugin/registerHooks.js.map +1 -0
  66. package/dist/plugin/registerRetention.js +57 -0
  67. package/dist/plugin/registerRetention.js.map +1 -0
  68. package/dist/plugin/registerTranslations.js +19 -0
  69. package/dist/plugin/registerTranslations.js.map +1 -0
  70. package/dist/plugin/registry.js +11 -0
  71. package/dist/plugin/registry.js.map +1 -0
  72. package/dist/plugin/startable.js +21 -0
  73. package/dist/plugin/startable.js.map +1 -0
  74. package/dist/session/close.js +65 -0
  75. package/dist/session/close.js.map +1 -0
  76. package/dist/session/closeStale.d.ts +11 -0
  77. package/dist/session/closeStale.js +65 -0
  78. package/dist/session/closeStale.js.map +1 -0
  79. package/dist/session/locks.js +32 -0
  80. package/dist/session/locks.js.map +1 -0
  81. package/dist/session/resolve.js +91 -0
  82. package/dist/session/resolve.js.map +1 -0
  83. package/dist/translations/ar.js +52 -0
  84. package/dist/translations/ar.js.map +1 -0
  85. package/dist/translations/de.js +52 -0
  86. package/dist/translations/de.js.map +1 -0
  87. package/dist/translations/en.js +52 -0
  88. package/dist/translations/en.js.map +1 -0
  89. package/dist/translations/es.js +52 -0
  90. package/dist/translations/es.js.map +1 -0
  91. package/dist/translations/fr.js +52 -0
  92. package/dist/translations/fr.js.map +1 -0
  93. package/dist/translations/id.js +52 -0
  94. package/dist/translations/id.js.map +1 -0
  95. package/dist/translations/index.d.ts +24 -0
  96. package/dist/translations/index.js +49 -0
  97. package/dist/translations/index.js.map +1 -0
  98. package/dist/translations/keys.d.ts +56 -0
  99. package/dist/translations/keys.js +56 -0
  100. package/dist/translations/keys.js.map +1 -0
  101. package/dist/translations/ko.js +52 -0
  102. package/dist/translations/ko.js.map +1 -0
  103. package/dist/translations/lookup.js +17 -0
  104. package/dist/translations/lookup.js.map +1 -0
  105. package/dist/translations/pt.js +52 -0
  106. package/dist/translations/pt.js.map +1 -0
  107. package/dist/translations/ru.js +52 -0
  108. package/dist/translations/ru.js.map +1 -0
  109. package/dist/translations/server.js +13 -0
  110. package/dist/translations/server.js.map +1 -0
  111. package/dist/translations/uk.js +52 -0
  112. package/dist/translations/uk.js.map +1 -0
  113. package/dist/translations/useTranslation.js +12 -0
  114. package/dist/translations/useTranslation.js.map +1 -0
  115. package/dist/translations/zh.js +52 -0
  116. package/dist/translations/zh.js.map +1 -0
  117. package/dist/types.d.ts +214 -0
  118. package/dist/types.js +11 -0
  119. package/dist/types.js.map +1 -0
  120. package/dist/ui/EndSessionMenuItem.d.ts +5 -0
  121. package/dist/ui/EndSessionMenuItem.js +45 -0
  122. package/dist/ui/EndSessionMenuItem.js.map +1 -0
  123. package/dist/ui/ImpersonateIcon.js +20 -0
  124. package/dist/ui/ImpersonateIcon.js.map +1 -0
  125. package/dist/ui/ImpersonationAction.d.ts +10 -0
  126. package/dist/ui/ImpersonationAction.js +48 -0
  127. package/dist/ui/ImpersonationAction.js.map +1 -0
  128. package/dist/ui/ImpersonationBar.d.ts +36 -0
  129. package/dist/ui/ImpersonationBar.js +357 -0
  130. package/dist/ui/ImpersonationBar.js.map +1 -0
  131. package/dist/ui/ImpersonationConfig.js +25 -0
  132. package/dist/ui/ImpersonationConfig.js.map +1 -0
  133. package/dist/ui/ImpersonationDocumentButton.d.ts +5 -0
  134. package/dist/ui/ImpersonationDocumentButton.js +85 -0
  135. package/dist/ui/ImpersonationDocumentButton.js.map +1 -0
  136. package/dist/ui/ImpersonationFrontendBanner.d.ts +41 -0
  137. package/dist/ui/ImpersonationFrontendBanner.js +58 -0
  138. package/dist/ui/ImpersonationFrontendBanner.js.map +1 -0
  139. package/dist/ui/ImpersonationFrontendExit.d.ts +17 -0
  140. package/dist/ui/ImpersonationFrontendExit.js +45 -0
  141. package/dist/ui/ImpersonationFrontendExit.js.map +1 -0
  142. package/dist/ui/ImpersonationProvider.d.ts +16 -0
  143. package/dist/ui/ImpersonationProvider.js +127 -0
  144. package/dist/ui/ImpersonationProvider.js.map +1 -0
  145. package/dist/ui/ImpersonationSwitcher.d.ts +21 -0
  146. package/dist/ui/ImpersonationSwitcher.js +251 -0
  147. package/dist/ui/ImpersonationSwitcher.js.map +1 -0
  148. package/dist/ui/ImpersonationUserCard.d.ts +31 -0
  149. package/dist/ui/ImpersonationUserCard.js +51 -0
  150. package/dist/ui/ImpersonationUserCard.js.map +1 -0
  151. package/dist/ui/StartConfirmModal.d.ts +28 -0
  152. package/dist/ui/StartConfirmModal.js +65 -0
  153. package/dist/ui/StartConfirmModal.js.map +1 -0
  154. package/dist/ui/api.js +66 -0
  155. package/dist/ui/api.js.map +1 -0
  156. package/dist/ui/barPlacement.js +107 -0
  157. package/dist/ui/barPlacement.js.map +1 -0
  158. package/dist/ui/frontendBanner.css +22 -0
  159. package/dist/ui/impersonation.css +244 -0
  160. package/dist/ui/useImpersonation.d.ts +43 -0
  161. package/dist/ui/useImpersonation.js +36 -0
  162. package/dist/ui/useImpersonation.js.map +1 -0
  163. package/package.json +126 -0
@@ -0,0 +1,48 @@
1
+ import { collectionBySlug } from "../ids.js";
2
+ import { cachedFindOpenBySid } from "../session/resolve.js";
3
+ import "../plugin/constants.js";
4
+ import { getRegistry } from "../plugin/registry.js";
5
+ import { asAuthUser, boundSid } from "../types.js";
6
+ import { readHintCookie } from "../plugin/lookup.js";
7
+ import { isStartableAuthCollection } from "../plugin/startable.js";
8
+ import { ImpersonationSwitcher } from "./ImpersonationSwitcher.js";
9
+ import { getFromImportMap } from "payload/shared";
10
+ import { jsx } from "react/jsx-runtime";
11
+ //#region src/ui/ImpersonationAction.tsx
12
+ const ImpersonationAction = async ({ payload, user }) => {
13
+ const options = getRegistry(payload.config);
14
+ if (!options?.ui.headerAction || !user) return null;
15
+ if (user.collection !== payload.config.admin.user) return null;
16
+ if (asAuthUser(user)._impersonation) return null;
17
+ const sid = boundSid(user, options.session.binding);
18
+ if (!sid) return null;
19
+ const hint = await readHintCookie(options.hintCookieName);
20
+ if (Boolean(options.session.issue) || Boolean(hint) || sid.startsWith("imp_")) {
21
+ if (await cachedFindOpenBySid(sid, options, payload)) return null;
22
+ }
23
+ const collections = payload.config.collections.flatMap((collection) => {
24
+ if (!isStartableAuthCollection(collection, options)) return [];
25
+ const plural = collectionBySlug(payload, collection.slug)?.config.labels?.plural;
26
+ return [{
27
+ label: plural ? String(plural) : collection.slug,
28
+ slug: collection.slug,
29
+ useAsTitle: collection.admin?.useAsTitle ?? "email"
30
+ }];
31
+ });
32
+ if (collections.length === 0) return null;
33
+ const Card = options.ui.card ? getFromImportMap({
34
+ importMap: payload.importMap,
35
+ PayloadComponent: options.ui.card,
36
+ silent: true
37
+ }) : void 0;
38
+ if (options.ui.card && !Card) payload.logger.warn("@10x-media/impersonation: ui.card is not in the admin import map. Run generate:importmap. The default card is shown until then.");
39
+ return /* @__PURE__ */ jsx(ImpersonationSwitcher, {
40
+ Card,
41
+ collections,
42
+ viewerId: user.id
43
+ });
44
+ };
45
+ //#endregion
46
+ export { ImpersonationAction };
47
+
48
+ //# sourceMappingURL=ImpersonationAction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImpersonationAction.js","names":[],"sources":["../../src/ui/ImpersonationAction.tsx"],"sourcesContent":["import type { ServerProps } from 'payload'\nimport { getFromImportMap } from 'payload/shared'\nimport type { ComponentType } from 'react'\n\nimport { collectionBySlug } from '../ids'\nimport { IMPERSONATION_SID_PREFIX } from '../plugin/constants'\nimport { readHintCookie } from '../plugin/lookup'\nimport { getRegistry } from '../plugin/registry'\nimport { isStartableAuthCollection } from '../plugin/startable'\nimport { cachedFindOpenBySid } from '../session/resolve'\nimport { asAuthUser, boundSid } from '../types'\nimport { ImpersonationSwitcher } from './ImpersonationSwitcher'\nimport type { ImpersonationUserCardProps } from './ImpersonationUserCard'\n\nexport const ImpersonationAction = async ({ payload, user }: ServerProps) => {\n\tconst options = getRegistry(payload.config)\n\tif (!options?.ui.headerAction || !user) {\n\t\treturn null\n\t}\n\n\tif (user.collection !== payload.config.admin.user) {\n\t\treturn null\n\t}\n\n\tif (asAuthUser(user)._impersonation) {\n\t\treturn null\n\t}\n\n\tconst sid = boundSid(user, options.session.binding)\n\tif (!sid) {\n\t\treturn null\n\t}\n\n\tconst hint = await readHintCookie(options.hintCookieName)\n\tconst shouldLookup =\n\t\tBoolean(options.session.issue) || Boolean(hint) || sid.startsWith(IMPERSONATION_SID_PREFIX)\n\tif (shouldLookup) {\n\t\tconst active = await cachedFindOpenBySid(sid, options, payload)\n\t\tif (active) {\n\t\t\treturn null\n\t\t}\n\t}\n\n\tconst collections = payload.config.collections.flatMap((collection) => {\n\t\tif (!isStartableAuthCollection(collection, options)) {\n\t\t\treturn []\n\t\t}\n\t\tconst registered = collectionBySlug(payload, collection.slug)\n\t\tconst plural = registered?.config.labels?.plural\n\t\treturn [\n\t\t\t{\n\t\t\t\tlabel: plural ? String(plural) : collection.slug,\n\t\t\t\tslug: collection.slug,\n\t\t\t\tuseAsTitle: collection.admin?.useAsTitle ?? 'email',\n\t\t\t},\n\t\t]\n\t})\n\n\tif (collections.length === 0) {\n\t\treturn null\n\t}\n\n\tconst Card = options.ui.card\n\t\t? getFromImportMap<ComponentType<ImpersonationUserCardProps>>({\n\t\t\t\timportMap: payload.importMap,\n\t\t\t\tPayloadComponent: options.ui.card,\n\t\t\t\tsilent: true,\n\t\t\t})\n\t\t: undefined\n\tif (options.ui.card && !Card) {\n\t\tpayload.logger.warn(\n\t\t\t'@10x-media/impersonation: ui.card is not in the admin import map. Run generate:importmap. The default card is shown until then.'\n\t\t)\n\t}\n\n\treturn <ImpersonationSwitcher Card={Card} collections={collections} viewerId={user.id} />\n}\n"],"mappings":";;;;;;;;;;;AAcA,MAAa,sBAAsB,OAAO,EAAE,SAAS,WAAwB;CAC5E,MAAM,UAAU,YAAY,QAAQ,MAAM;CAC1C,IAAI,CAAC,SAAS,GAAG,gBAAgB,CAAC,MACjC,OAAO;CAGR,IAAI,KAAK,eAAe,QAAQ,OAAO,MAAM,MAC5C,OAAO;CAGR,IAAI,WAAW,IAAI,EAAE,gBACpB,OAAO;CAGR,MAAM,MAAM,SAAS,MAAM,QAAQ,QAAQ,OAAO;CAClD,IAAI,CAAC,KACJ,OAAO;CAGR,MAAM,OAAO,MAAM,eAAe,QAAQ,cAAc;CAGxD,IADC,QAAQ,QAAQ,QAAQ,KAAK,KAAK,QAAQ,IAAI,KAAK,IAAI,WAAA,MAAmC;MAGtF,MADiB,oBAAoB,KAAK,SAAS,OAAO,GAE7D,OAAO;CAAA;CAIT,MAAM,cAAc,QAAQ,OAAO,YAAY,SAAS,eAAe;EACtE,IAAI,CAAC,0BAA0B,YAAY,OAAO,GACjD,OAAO,CAAC;EAGT,MAAM,SADa,iBAAiB,SAAS,WAAW,IAChC,GAAG,OAAO,QAAQ;EAC1C,OAAO,CACN;GACC,OAAO,SAAS,OAAO,MAAM,IAAI,WAAW;GAC5C,MAAM,WAAW;GACjB,YAAY,WAAW,OAAO,cAAc;EAC7C,CACD;CACD,CAAC;CAED,IAAI,YAAY,WAAW,GAC1B,OAAO;CAGR,MAAM,OAAO,QAAQ,GAAG,OACrB,iBAA4D;EAC5D,WAAW,QAAQ;EACnB,kBAAkB,QAAQ,GAAG;EAC7B,QAAQ;CACT,CAAC,IACA,KAAA;CACH,IAAI,QAAQ,GAAG,QAAQ,CAAC,MACvB,QAAQ,OAAO,KACd,iIACD;CAGD,OAAO,oBAAC,uBAAD;EAA6B;EAAmB;EAAa,UAAU,KAAK;CAAK,CAAA;AACzF"}
@@ -0,0 +1,36 @@
1
+ //#region src/ui/ImpersonationBar.d.ts
2
+ type ImpersonationBarProps = {
3
+ actingAs: string;
4
+ adminRoute: string;
5
+ apiPath: string;
6
+ frontendUrl?: string;
7
+ impersonatorLocale?: null | string;
8
+ openWebsite: string;
9
+ pluginName: string;
10
+ returnTo: string;
11
+ sessionEndsAt?: null | string;
12
+ sessionEndsAtTemplate: string;
13
+ sessionEndsInTemplate: string;
14
+ showFrontendLink?: boolean; /** Collapsed chip label. */
15
+ targetName: string;
16
+ tone?: 'quiet' | 'warning';
17
+ };
18
+ declare const ImpersonationBar: ({
19
+ actingAs,
20
+ adminRoute,
21
+ apiPath,
22
+ frontendUrl,
23
+ impersonatorLocale,
24
+ openWebsite,
25
+ pluginName,
26
+ returnTo,
27
+ sessionEndsAt,
28
+ sessionEndsAtTemplate,
29
+ sessionEndsInTemplate,
30
+ showFrontendLink,
31
+ targetName,
32
+ tone
33
+ }: ImpersonationBarProps) => import("react/jsx-runtime").JSX.Element | null;
34
+ //#endregion
35
+ export { ImpersonationBar };
36
+ //# sourceMappingURL=ImpersonationBar.d.ts.map
@@ -0,0 +1,357 @@
1
+ "use client";
2
+ import { CLIENT_FETCH_TIMEOUT_MS } from "../plugin/constants.js";
3
+ import { keys } from "../translations/keys.js";
4
+ import { errorKey, goAfterSwitch, postImpersonation } from "./api.js";
5
+ import { useImpersonation } from "./useImpersonation.js";
6
+ import { fillTemplate, messageFor } from "../translations/lookup.js";
7
+ import { anchorsFor, nearestEdge, projectThrow, readBarPlacement, readViewport, writeBarPlacement } from "./barPlacement.js";
8
+ import { useCallback, useEffect, useRef, useState } from "react";
9
+ import { Button, Pill, toast } from "@payloadcms/ui";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ import { usePathname } from "next/navigation";
12
+ import "./impersonation.css";
13
+ //#region src/ui/ImpersonationBar.tsx
14
+ const SESSION_GONE = new Set([
15
+ "forbidden",
16
+ "impersonatorGone",
17
+ "impersonatorSessionExpired",
18
+ "notImpersonating"
19
+ ]);
20
+ const formatCountdown = (expiresAt, now) => {
21
+ const remaining = new Date(expiresAt).getTime() - now;
22
+ if (!Number.isFinite(remaining) || remaining <= 0) return "0s";
23
+ const totalSeconds = Math.floor(remaining / 1e3);
24
+ const hours = Math.floor(totalSeconds / 3600);
25
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
26
+ const seconds = totalSeconds % 60;
27
+ if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m ${String(seconds).padStart(2, "0")}s`;
28
+ if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
29
+ return `${seconds}s`;
30
+ };
31
+ const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, impersonatorLocale, openWebsite, pluginName, returnTo, sessionEndsAt, sessionEndsAtTemplate, sessionEndsInTemplate, showFrontendLink, targetName, tone = "warning" }) => {
32
+ const { setStatus } = useImpersonation();
33
+ const [mounted, setMounted] = useState(false);
34
+ const onLogin = (usePathname() ?? "") === `${adminRoute}/login`;
35
+ const [busy, setBusy] = useState(false);
36
+ const [visible, setVisible] = useState(true);
37
+ const [collapsed, setCollapsed] = useState(false);
38
+ const [edge, setEdge] = useState("right");
39
+ const [viewport, setViewport] = useState(() => typeof document === "undefined" ? {
40
+ height: 800,
41
+ width: 1200
42
+ } : readViewport());
43
+ const [dragPoint, setDragPoint] = useState(null);
44
+ const [now, setNow] = useState(() => Date.now());
45
+ const chipRef = useRef(null);
46
+ const expiredProbe = useRef(false);
47
+ const dragOffset = useRef(null);
48
+ const dragPointRef = useRef(null);
49
+ const dragOrigin = useRef(null);
50
+ const dragged = useRef(false);
51
+ const lastSample = useRef(null);
52
+ const velocity = useRef({
53
+ x: 0,
54
+ y: 0
55
+ });
56
+ const endChipDragRef = useRef(() => {});
57
+ const onWindowPointerEnd = useRef(() => {
58
+ endChipDragRef.current();
59
+ });
60
+ useEffect(() => {
61
+ const stored = readBarPlacement();
62
+ setCollapsed(stored.collapsed);
63
+ setEdge(stored.edge);
64
+ setMounted(true);
65
+ }, []);
66
+ useEffect(() => {
67
+ if (!collapsed) return;
68
+ const update = () => setViewport(readViewport());
69
+ update();
70
+ window.addEventListener("resize", update);
71
+ return () => window.removeEventListener("resize", update);
72
+ }, [collapsed]);
73
+ const probe = useCallback(async () => {
74
+ try {
75
+ const body = await (await fetch(apiPath, {
76
+ credentials: "include",
77
+ signal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS)
78
+ })).json();
79
+ if (typeof body.active !== "boolean") return;
80
+ setStatus(body.active ? body : { active: false });
81
+ setVisible(body.active);
82
+ } catch {}
83
+ }, [apiPath, setStatus]);
84
+ useEffect(() => {
85
+ if (onLogin) {
86
+ setVisible(false);
87
+ return;
88
+ }
89
+ probe();
90
+ }, [onLogin, probe]);
91
+ useEffect(() => {
92
+ if (!sessionEndsAt || !visible || onLogin) return;
93
+ expiredProbe.current = false;
94
+ const timer = window.setInterval(() => {
95
+ const next = Date.now();
96
+ setNow(next);
97
+ if (expiredProbe.current) return;
98
+ if (new Date(sessionEndsAt).getTime() - next <= 0) {
99
+ expiredProbe.current = true;
100
+ probe();
101
+ }
102
+ }, 1e3);
103
+ return () => window.clearInterval(timer);
104
+ }, [
105
+ onLogin,
106
+ probe,
107
+ sessionEndsAt,
108
+ visible
109
+ ]);
110
+ const onExit = async () => {
111
+ if (busy) return;
112
+ setBusy(true);
113
+ try {
114
+ const result = await postImpersonation(`${apiPath}/exit`, {});
115
+ if (!result.ok) {
116
+ toast.error(messageFor(impersonatorLocale, errorKey(result.error ?? "failed")));
117
+ if (result.error && SESSION_GONE.has(result.error)) {
118
+ setStatus({ active: false });
119
+ setVisible(false);
120
+ goAfterSwitch(adminRoute);
121
+ }
122
+ return;
123
+ }
124
+ goAfterSwitch(adminRoute);
125
+ } finally {
126
+ setBusy(false);
127
+ }
128
+ };
129
+ const collapse = () => {
130
+ const next = {
131
+ collapsed: true,
132
+ edge
133
+ };
134
+ setCollapsed(true);
135
+ writeBarPlacement(next);
136
+ };
137
+ const expand = () => {
138
+ const next = {
139
+ collapsed: false,
140
+ edge
141
+ };
142
+ setCollapsed(false);
143
+ setDragPoint(null);
144
+ writeBarPlacement(next);
145
+ };
146
+ const endChipDrag = () => {
147
+ if (!dragOffset.current && !dragOrigin.current) return;
148
+ window.removeEventListener("pointerup", onWindowPointerEnd.current);
149
+ window.removeEventListener("pointercancel", onWindowPointerEnd.current);
150
+ const point = dragPointRef.current;
151
+ const didDrag = dragged.current;
152
+ dragOffset.current = null;
153
+ dragOrigin.current = null;
154
+ dragPointRef.current = null;
155
+ if (!didDrag) {
156
+ expand();
157
+ return;
158
+ }
159
+ const rect = chipRef.current?.getBoundingClientRect();
160
+ if (!point || !rect) {
161
+ setDragPoint(null);
162
+ return;
163
+ }
164
+ const header = Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--app-header-height"));
165
+ const anchors = anchorsFor({
166
+ header: Number.isFinite(header) ? header : 48,
167
+ size: {
168
+ height: rect.height,
169
+ width: rect.width
170
+ },
171
+ viewport: readViewport()
172
+ });
173
+ const nextEdge = nearestEdge(projectThrow(point, velocity.current), anchors);
174
+ setEdge(nextEdge);
175
+ setDragPoint(null);
176
+ writeBarPlacement({
177
+ collapsed: true,
178
+ edge: nextEdge
179
+ });
180
+ };
181
+ endChipDragRef.current = endChipDrag;
182
+ const onChipPointerDown = (event) => {
183
+ if (event.button !== 0) return;
184
+ const rect = chipRef.current?.getBoundingClientRect();
185
+ if (!rect) return;
186
+ dragged.current = false;
187
+ dragOrigin.current = {
188
+ x: event.clientX,
189
+ y: event.clientY
190
+ };
191
+ dragOffset.current = {
192
+ x: event.clientX - rect.left,
193
+ y: event.clientY - rect.top
194
+ };
195
+ lastSample.current = {
196
+ t: event.timeStamp,
197
+ x: event.clientX,
198
+ y: event.clientY
199
+ };
200
+ velocity.current = {
201
+ x: 0,
202
+ y: 0
203
+ };
204
+ window.addEventListener("pointerup", onWindowPointerEnd.current);
205
+ window.addEventListener("pointercancel", onWindowPointerEnd.current);
206
+ event.currentTarget.setPointerCapture(event.pointerId);
207
+ };
208
+ const onChipPointerMove = (event) => {
209
+ if (!dragOffset.current || !dragOrigin.current) return;
210
+ if (event.pointerType !== "touch" && event.buttons === 0) {
211
+ endChipDrag();
212
+ return;
213
+ }
214
+ const travel = Math.hypot(event.clientX - dragOrigin.current.x, event.clientY - dragOrigin.current.y);
215
+ if (!dragged.current && travel < 4) return;
216
+ dragged.current = true;
217
+ const next = {
218
+ x: event.clientX - dragOffset.current.x,
219
+ y: event.clientY - dragOffset.current.y
220
+ };
221
+ const previous = lastSample.current;
222
+ const dt = previous ? event.timeStamp - previous.t : 0;
223
+ if (previous && dt > 0) velocity.current = {
224
+ x: (event.clientX - previous.x) / dt,
225
+ y: (event.clientY - previous.y) / dt
226
+ };
227
+ lastSample.current = {
228
+ t: event.timeStamp,
229
+ x: event.clientX,
230
+ y: event.clientY
231
+ };
232
+ dragPointRef.current = next;
233
+ setDragPoint(next);
234
+ };
235
+ if (!visible || onLogin) return null;
236
+ if (collapsed) {
237
+ const rect = chipRef.current?.getBoundingClientRect();
238
+ const header = mounted ? Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--app-header-height")) : 48;
239
+ const anchors = anchorsFor({
240
+ header: Number.isFinite(header) ? header : 48,
241
+ size: {
242
+ height: rect?.height ?? 36,
243
+ width: rect?.width ?? 168
244
+ },
245
+ viewport
246
+ });
247
+ const point = dragPoint ?? anchors[edge];
248
+ return /* @__PURE__ */ jsxs("div", {
249
+ "aria-label": actingAs,
250
+ "aria-live": "polite",
251
+ className: [
252
+ "impersonation-chip",
253
+ tone === "quiet" ? "impersonation-chip--quiet" : null,
254
+ dragPoint ? "impersonation-chip--dragging" : null
255
+ ].filter(Boolean).join(" "),
256
+ onPointerCancel: endChipDrag,
257
+ onPointerDown: onChipPointerDown,
258
+ onPointerMove: onChipPointerMove,
259
+ onPointerUp: endChipDrag,
260
+ ref: chipRef,
261
+ role: "status",
262
+ style: {
263
+ left: point.x,
264
+ top: point.y
265
+ },
266
+ children: [/* @__PURE__ */ jsx("span", {
267
+ className: "impersonation-chip__name",
268
+ children: targetName
269
+ }), /* @__PURE__ */ jsx("button", {
270
+ "aria-label": messageFor(impersonatorLocale, keys.expandBar),
271
+ className: "impersonation-bar__icon",
272
+ onClick: (event) => {
273
+ if (dragged.current) {
274
+ event.preventDefault();
275
+ return;
276
+ }
277
+ expand();
278
+ },
279
+ type: "button",
280
+ children: /* @__PURE__ */ jsx(ExpandIcon, {})
281
+ })]
282
+ });
283
+ }
284
+ const countdown = sessionEndsAt ? formatCountdown(sessionEndsAt, now) : null;
285
+ const endsLabel = countdown && countdown !== "0s" ? fillTemplate(sessionEndsInTemplate, "{{countdown}}", countdown) : sessionEndsAt && countdown !== "0s" ? fillTemplate(sessionEndsAtTemplate, "{{time}}", new Date(sessionEndsAt).toLocaleTimeString(impersonatorLocale ?? void 0)) : null;
286
+ return /* @__PURE__ */ jsxs("div", {
287
+ "aria-live": "polite",
288
+ className: tone === "quiet" ? "impersonation-bar impersonation-bar--quiet" : "impersonation-bar",
289
+ role: "status",
290
+ children: [/* @__PURE__ */ jsxs("div", {
291
+ className: "impersonation-bar__meta",
292
+ children: [/* @__PURE__ */ jsxs("div", {
293
+ className: "impersonation-bar__copy",
294
+ children: [/* @__PURE__ */ jsx(Pill, {
295
+ pillStyle: tone === "quiet" ? "success" : "warning",
296
+ size: "small",
297
+ children: pluginName
298
+ }), /* @__PURE__ */ jsx("span", { children: actingAs })]
299
+ }), endsLabel ? /* @__PURE__ */ jsx("div", {
300
+ className: "impersonation-bar__time",
301
+ children: endsLabel
302
+ }) : null]
303
+ }), /* @__PURE__ */ jsxs("div", {
304
+ className: "impersonation-bar__actions",
305
+ children: [
306
+ showFrontendLink && frontendUrl ? /* @__PURE__ */ jsx("a", {
307
+ className: "impersonation-bar__link",
308
+ href: frontendUrl,
309
+ children: openWebsite
310
+ }) : null,
311
+ /* @__PURE__ */ jsx(Button, {
312
+ buttonStyle: "pill",
313
+ disabled: busy,
314
+ margin: false,
315
+ onClick: () => void onExit(),
316
+ size: "small",
317
+ children: returnTo
318
+ }),
319
+ /* @__PURE__ */ jsx("button", {
320
+ "aria-label": messageFor(impersonatorLocale, keys.collapseBar),
321
+ className: "impersonation-bar__icon",
322
+ onClick: collapse,
323
+ type: "button",
324
+ children: /* @__PURE__ */ jsx(CollapseIcon, {})
325
+ })
326
+ ]
327
+ })]
328
+ });
329
+ };
330
+ const CollapseIcon = () => /* @__PURE__ */ jsx("svg", {
331
+ "aria-hidden": "true",
332
+ fill: "none",
333
+ height: "16",
334
+ viewBox: "0 0 16 16",
335
+ width: "16",
336
+ children: /* @__PURE__ */ jsx("path", {
337
+ d: "M3.5 6 8 10.5 12.5 6",
338
+ stroke: "currentColor",
339
+ strokeWidth: "1.5"
340
+ })
341
+ });
342
+ const ExpandIcon = () => /* @__PURE__ */ jsx("svg", {
343
+ "aria-hidden": "true",
344
+ fill: "none",
345
+ height: "16",
346
+ viewBox: "0 0 16 16",
347
+ width: "16",
348
+ children: /* @__PURE__ */ jsx("path", {
349
+ d: "M3 6h10M3 10h10",
350
+ stroke: "currentColor",
351
+ strokeWidth: "1.5"
352
+ })
353
+ });
354
+ //#endregion
355
+ export { ImpersonationBar };
356
+
357
+ //# sourceMappingURL=ImpersonationBar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImpersonationBar.js","names":[],"sources":["../../src/ui/ImpersonationBar.tsx"],"sourcesContent":["'use client'\n\nimport { Button, Pill, toast } from '@payloadcms/ui'\nimport { usePathname } from 'next/navigation'\nimport {\n\ttype PointerEvent as ReactPointerEvent,\n\tuseCallback,\n\tuseEffect,\n\tuseRef,\n\tuseState,\n} from 'react'\n\nimport type { ImpersonationStatus } from '../getImpersonation'\nimport { CLIENT_FETCH_TIMEOUT_MS } from '../plugin/constants'\nimport { keys } from '../translations/keys'\nimport { fillTemplate, messageFor } from '../translations/lookup'\nimport { errorKey, goAfterSwitch, postImpersonation } from './api'\nimport {\n\tanchorsFor,\n\ttype BarEdge,\n\tnearestEdge,\n\ttype Point,\n\tprojectThrow,\n\treadBarPlacement,\n\treadViewport,\n\twriteBarPlacement,\n} from './barPlacement'\nimport { useImpersonation } from './useImpersonation'\nimport './impersonation.css'\n\nconst SESSION_GONE = new Set([\n\t'forbidden',\n\t'impersonatorGone',\n\t'impersonatorSessionExpired',\n\t'notImpersonating',\n])\n\nconst formatCountdown = (expiresAt: string, now: number): string | null => {\n\tconst remaining = new Date(expiresAt).getTime() - now\n\tif (!Number.isFinite(remaining) || remaining <= 0) {\n\t\treturn '0s'\n\t}\n\tconst totalSeconds = Math.floor(remaining / 1000)\n\tconst hours = Math.floor(totalSeconds / 3600)\n\tconst minutes = Math.floor((totalSeconds % 3600) / 60)\n\tconst seconds = totalSeconds % 60\n\tif (hours > 0) {\n\t\treturn `${hours}h ${String(minutes).padStart(2, '0')}m ${String(seconds).padStart(2, '0')}s`\n\t}\n\tif (minutes > 0) {\n\t\treturn `${minutes}m ${String(seconds).padStart(2, '0')}s`\n\t}\n\treturn `${seconds}s`\n}\n\nexport type ImpersonationBarProps = {\n\tactingAs: string\n\tadminRoute: string\n\tapiPath: string\n\tfrontendUrl?: string\n\timpersonatorLocale?: null | string\n\topenWebsite: string\n\tpluginName: string\n\treturnTo: string\n\tsessionEndsAt?: null | string\n\tsessionEndsAtTemplate: string\n\tsessionEndsInTemplate: string\n\tshowFrontendLink?: boolean\n\t/** Collapsed chip label. */\n\ttargetName: string\n\ttone?: 'quiet' | 'warning'\n}\n\nexport const ImpersonationBar = ({\n\tactingAs,\n\tadminRoute,\n\tapiPath,\n\tfrontendUrl,\n\timpersonatorLocale,\n\topenWebsite,\n\tpluginName,\n\treturnTo,\n\tsessionEndsAt,\n\tsessionEndsAtTemplate,\n\tsessionEndsInTemplate,\n\tshowFrontendLink,\n\ttargetName,\n\ttone = 'warning',\n}: ImpersonationBarProps) => {\n\tconst { setStatus } = useImpersonation()\n\tconst [mounted, setMounted] = useState(false)\n\tconst pathname = usePathname() ?? ''\n\tconst onLogin = pathname === `${adminRoute}/login`\n\tconst [busy, setBusy] = useState(false)\n\tconst [visible, setVisible] = useState(true)\n\tconst [collapsed, setCollapsed] = useState(false)\n\tconst [edge, setEdge] = useState<BarEdge>('right')\n\tconst [viewport, setViewport] = useState(() =>\n\t\ttypeof document === 'undefined' ? { height: 800, width: 1200 } : readViewport()\n\t)\n\tconst [dragPoint, setDragPoint] = useState<null | Point>(null)\n\tconst [now, setNow] = useState(() => Date.now())\n\tconst chipRef = useRef<HTMLDivElement>(null)\n\tconst expiredProbe = useRef(false)\n\tconst dragOffset = useRef<null | Point>(null)\n\tconst dragPointRef = useRef<null | Point>(null)\n\tconst dragOrigin = useRef<null | Point>(null)\n\tconst dragged = useRef(false)\n\tconst lastSample = useRef<null | { t: number; x: number; y: number }>(null)\n\tconst velocity = useRef<Point>({ x: 0, y: 0 })\n\tconst endChipDragRef = useRef<() => void>(() => {})\n\tconst onWindowPointerEnd = useRef(() => {\n\t\tendChipDragRef.current()\n\t})\n\n\tuseEffect(() => {\n\t\tconst stored = readBarPlacement()\n\t\tsetCollapsed(stored.collapsed)\n\t\tsetEdge(stored.edge)\n\t\tsetMounted(true)\n\t}, [])\n\n\tuseEffect(() => {\n\t\tif (!collapsed) {\n\t\t\treturn\n\t\t}\n\t\tconst update = () => setViewport(readViewport())\n\t\tupdate()\n\t\twindow.addEventListener('resize', update)\n\t\treturn () => window.removeEventListener('resize', update)\n\t}, [collapsed])\n\n\tconst probe = useCallback(async () => {\n\t\ttry {\n\t\t\tconst response = await fetch(apiPath, {\n\t\t\t\tcredentials: 'include',\n\t\t\t\tsignal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS),\n\t\t\t})\n\t\t\tconst body = (await response.json()) as { active?: boolean }\n\t\t\tif (typeof body.active !== 'boolean') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tsetStatus(body.active ? (body as ImpersonationStatus) : { active: false })\n\t\t\tsetVisible(body.active)\n\t\t} catch {\n\t\t\t// A timeout or a non-JSON body leaves the painted bar in place.\n\t\t}\n\t}, [apiPath, setStatus])\n\n\tuseEffect(() => {\n\t\tif (onLogin) {\n\t\t\tsetVisible(false)\n\t\t\treturn\n\t\t}\n\t\tvoid probe()\n\t}, [onLogin, probe])\n\n\tuseEffect(() => {\n\t\tif (!sessionEndsAt || !visible || onLogin) {\n\t\t\treturn\n\t\t}\n\t\texpiredProbe.current = false\n\t\tconst timer = window.setInterval(() => {\n\t\t\tconst next = Date.now()\n\t\t\tsetNow(next)\n\t\t\tif (expiredProbe.current) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (new Date(sessionEndsAt).getTime() - next <= 0) {\n\t\t\t\texpiredProbe.current = true\n\t\t\t\tvoid probe()\n\t\t\t}\n\t\t}, 1000)\n\t\treturn () => window.clearInterval(timer)\n\t}, [onLogin, probe, sessionEndsAt, visible])\n\n\tconst onExit = async () => {\n\t\tif (busy) {\n\t\t\treturn\n\t\t}\n\t\tsetBusy(true)\n\t\ttry {\n\t\t\tconst result = await postImpersonation(`${apiPath}/exit`, {})\n\t\t\tif (!result.ok) {\n\t\t\t\ttoast.error(messageFor(impersonatorLocale, errorKey(result.error ?? 'failed')))\n\t\t\t\tif (result.error && SESSION_GONE.has(result.error)) {\n\t\t\t\t\tsetStatus({ active: false })\n\t\t\t\t\tsetVisible(false)\n\t\t\t\t\tgoAfterSwitch(adminRoute)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tgoAfterSwitch(adminRoute)\n\t\t} finally {\n\t\t\tsetBusy(false)\n\t\t}\n\t}\n\n\tconst collapse = () => {\n\t\tconst next = { collapsed: true, edge }\n\t\tsetCollapsed(true)\n\t\twriteBarPlacement(next)\n\t}\n\n\tconst expand = () => {\n\t\tconst next = { collapsed: false, edge }\n\t\tsetCollapsed(false)\n\t\tsetDragPoint(null)\n\t\twriteBarPlacement(next)\n\t}\n\n\tconst endChipDrag = () => {\n\t\tif (!dragOffset.current && !dragOrigin.current) {\n\t\t\treturn\n\t\t}\n\t\twindow.removeEventListener('pointerup', onWindowPointerEnd.current)\n\t\twindow.removeEventListener('pointercancel', onWindowPointerEnd.current)\n\t\tconst point = dragPointRef.current\n\t\tconst didDrag = dragged.current\n\t\tdragOffset.current = null\n\t\tdragOrigin.current = null\n\t\tdragPointRef.current = null\n\t\tif (!didDrag) {\n\t\t\texpand()\n\t\t\treturn\n\t\t}\n\t\tconst rect = chipRef.current?.getBoundingClientRect()\n\t\tif (!point || !rect) {\n\t\t\tsetDragPoint(null)\n\t\t\treturn\n\t\t}\n\t\tconst header = Number.parseFloat(\n\t\t\tgetComputedStyle(document.documentElement).getPropertyValue('--app-header-height')\n\t\t)\n\t\tconst anchors = anchorsFor({\n\t\t\theader: Number.isFinite(header) ? header : 48,\n\t\t\tsize: { height: rect.height, width: rect.width },\n\t\t\tviewport: readViewport(),\n\t\t})\n\t\tconst nextEdge = nearestEdge(projectThrow(point, velocity.current), anchors)\n\t\tsetEdge(nextEdge)\n\t\tsetDragPoint(null)\n\t\twriteBarPlacement({ collapsed: true, edge: nextEdge })\n\t}\n\tendChipDragRef.current = endChipDrag\n\n\tconst onChipPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {\n\t\tif (event.button !== 0) {\n\t\t\treturn\n\t\t}\n\t\tconst rect = chipRef.current?.getBoundingClientRect()\n\t\tif (!rect) {\n\t\t\treturn\n\t\t}\n\t\tdragged.current = false\n\t\tdragOrigin.current = { x: event.clientX, y: event.clientY }\n\t\tdragOffset.current = { x: event.clientX - rect.left, y: event.clientY - rect.top }\n\t\tlastSample.current = { t: event.timeStamp, x: event.clientX, y: event.clientY }\n\t\tvelocity.current = { x: 0, y: 0 }\n\t\twindow.addEventListener('pointerup', onWindowPointerEnd.current)\n\t\twindow.addEventListener('pointercancel', onWindowPointerEnd.current)\n\t\tevent.currentTarget.setPointerCapture(event.pointerId)\n\t}\n\n\tconst onChipPointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {\n\t\tif (!dragOffset.current || !dragOrigin.current) {\n\t\t\treturn\n\t\t}\n\t\tif (event.pointerType !== 'touch' && event.buttons === 0) {\n\t\t\tendChipDrag()\n\t\t\treturn\n\t\t}\n\t\tconst travel = Math.hypot(\n\t\t\tevent.clientX - dragOrigin.current.x,\n\t\t\tevent.clientY - dragOrigin.current.y\n\t\t)\n\t\tif (!dragged.current && travel < 4) {\n\t\t\treturn\n\t\t}\n\t\tdragged.current = true\n\t\tconst next = {\n\t\t\tx: event.clientX - dragOffset.current.x,\n\t\t\ty: event.clientY - dragOffset.current.y,\n\t\t}\n\t\tconst previous = lastSample.current\n\t\tconst dt = previous ? event.timeStamp - previous.t : 0\n\t\tif (previous && dt > 0) {\n\t\t\tvelocity.current = {\n\t\t\t\tx: (event.clientX - previous.x) / dt,\n\t\t\t\ty: (event.clientY - previous.y) / dt,\n\t\t\t}\n\t\t}\n\t\tlastSample.current = { t: event.timeStamp, x: event.clientX, y: event.clientY }\n\t\tdragPointRef.current = next\n\t\tsetDragPoint(next)\n\t}\n\n\tif (!visible || onLogin) {\n\t\treturn null\n\t}\n\n\tif (collapsed) {\n\t\tconst rect = chipRef.current?.getBoundingClientRect()\n\t\tconst header = mounted\n\t\t\t? Number.parseFloat(\n\t\t\t\t\tgetComputedStyle(document.documentElement).getPropertyValue('--app-header-height')\n\t\t\t\t)\n\t\t\t: 48\n\t\tconst anchors = anchorsFor({\n\t\t\theader: Number.isFinite(header) ? header : 48,\n\t\t\tsize: { height: rect?.height ?? 36, width: rect?.width ?? 168 },\n\t\t\tviewport,\n\t\t})\n\t\tconst point = dragPoint ?? anchors[edge]\n\t\treturn (\n\t\t\t<div\n\t\t\t\taria-label={actingAs}\n\t\t\t\taria-live=\"polite\"\n\t\t\t\tclassName={[\n\t\t\t\t\t'impersonation-chip',\n\t\t\t\t\ttone === 'quiet' ? 'impersonation-chip--quiet' : null,\n\t\t\t\t\tdragPoint ? 'impersonation-chip--dragging' : null,\n\t\t\t\t]\n\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t.join(' ')}\n\t\t\t\tonPointerCancel={endChipDrag}\n\t\t\t\tonPointerDown={onChipPointerDown}\n\t\t\t\tonPointerMove={onChipPointerMove}\n\t\t\t\tonPointerUp={endChipDrag}\n\t\t\t\tref={chipRef}\n\t\t\t\trole=\"status\"\n\t\t\t\tstyle={{ left: point.x, top: point.y }}\n\t\t\t>\n\t\t\t\t<span className=\"impersonation-chip__name\">{targetName}</span>\n\t\t\t\t<button\n\t\t\t\t\taria-label={messageFor(impersonatorLocale, keys.expandBar)}\n\t\t\t\t\tclassName=\"impersonation-bar__icon\"\n\t\t\t\t\tonClick={(event) => {\n\t\t\t\t\t\tif (dragged.current) {\n\t\t\t\t\t\t\tevent.preventDefault()\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\texpand()\n\t\t\t\t\t}}\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t>\n\t\t\t\t\t<ExpandIcon />\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t)\n\t}\n\n\tconst countdown = sessionEndsAt ? formatCountdown(sessionEndsAt, now) : null\n\tconst endsLabel =\n\t\tcountdown && countdown !== '0s'\n\t\t\t? fillTemplate(sessionEndsInTemplate, '{{countdown}}', countdown)\n\t\t\t: sessionEndsAt && countdown !== '0s'\n\t\t\t\t? fillTemplate(\n\t\t\t\t\t\tsessionEndsAtTemplate,\n\t\t\t\t\t\t'{{time}}',\n\t\t\t\t\t\tnew Date(sessionEndsAt).toLocaleTimeString(impersonatorLocale ?? undefined)\n\t\t\t\t\t)\n\t\t\t\t: null\n\n\treturn (\n\t\t<div\n\t\t\taria-live=\"polite\"\n\t\t\tclassName={\n\t\t\t\ttone === 'quiet' ? 'impersonation-bar impersonation-bar--quiet' : 'impersonation-bar'\n\t\t\t}\n\t\t\trole=\"status\"\n\t\t>\n\t\t\t<div className=\"impersonation-bar__meta\">\n\t\t\t\t<div className=\"impersonation-bar__copy\">\n\t\t\t\t\t<Pill pillStyle={tone === 'quiet' ? 'success' : 'warning'} size=\"small\">\n\t\t\t\t\t\t{pluginName}\n\t\t\t\t\t</Pill>\n\t\t\t\t\t<span>{actingAs}</span>\n\t\t\t\t</div>\n\t\t\t\t{endsLabel ? <div className=\"impersonation-bar__time\">{endsLabel}</div> : null}\n\t\t\t</div>\n\t\t\t<div className=\"impersonation-bar__actions\">\n\t\t\t\t{showFrontendLink && frontendUrl ? (\n\t\t\t\t\t<a className=\"impersonation-bar__link\" href={frontendUrl}>\n\t\t\t\t\t\t{openWebsite}\n\t\t\t\t\t</a>\n\t\t\t\t) : null}\n\t\t\t\t<Button\n\t\t\t\t\tbuttonStyle=\"pill\"\n\t\t\t\t\tdisabled={busy}\n\t\t\t\t\tmargin={false}\n\t\t\t\t\tonClick={() => void onExit()}\n\t\t\t\t\tsize=\"small\"\n\t\t\t\t>\n\t\t\t\t\t{returnTo}\n\t\t\t\t</Button>\n\t\t\t\t<button\n\t\t\t\t\taria-label={messageFor(impersonatorLocale, keys.collapseBar)}\n\t\t\t\t\tclassName=\"impersonation-bar__icon\"\n\t\t\t\t\tonClick={collapse}\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t>\n\t\t\t\t\t<CollapseIcon />\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\nconst CollapseIcon = () => (\n\t<svg aria-hidden=\"true\" fill=\"none\" height=\"16\" viewBox=\"0 0 16 16\" width=\"16\">\n\t\t<path d=\"M3.5 6 8 10.5 12.5 6\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n\t</svg>\n)\n\nconst ExpandIcon = () => (\n\t<svg aria-hidden=\"true\" fill=\"none\" height=\"16\" viewBox=\"0 0 16 16\" width=\"16\">\n\t\t<path d=\"M3 6h10M3 10h10\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n\t</svg>\n)\n"],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { createContext, useContext, useEffect, useMemo, useState } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/ui/ImpersonationConfig.tsx
5
+ const Context = createContext(null);
6
+ const ImpersonationClientConfig = ({ children, value }) => {
7
+ const [status, setStatus] = useState(value.status);
8
+ useEffect(() => {
9
+ setStatus(value.status);
10
+ }, [value.status]);
11
+ const live = useMemo(() => ({
12
+ ...value,
13
+ setStatus,
14
+ status
15
+ }), [status, value]);
16
+ return /* @__PURE__ */ jsx(Context.Provider, {
17
+ value: live,
18
+ children
19
+ });
20
+ };
21
+ const useImpersonationClient = () => useContext(Context);
22
+ //#endregion
23
+ export { ImpersonationClientConfig, useImpersonationClient };
24
+
25
+ //# sourceMappingURL=ImpersonationConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImpersonationConfig.js","names":[],"sources":["../../src/ui/ImpersonationConfig.tsx"],"sourcesContent":["'use client'\n\nimport type { Where } from 'payload'\nimport { createContext, type ReactNode, useContext, useEffect, useMemo, useState } from 'react'\n\nimport type { ImpersonationStatus } from '../getImpersonation'\n\nexport type TargetFilterMap = Record<string, true | Where>\n\nexport type ImpersonationClientOptions = {\n\tapiPath: string\n\tcardEmail: boolean\n\treasonMode: 'off' | 'optional' | 'required'\n\tsessionCollection: string\n\tsetStatus: (status: ImpersonationStatus) => void\n\tstatus: ImpersonationStatus\n\ttargetFilters: TargetFilterMap\n}\n\ntype ServerClientOptions = Omit<ImpersonationClientOptions, 'setStatus'>\n\nconst Context = createContext<ImpersonationClientOptions | null>(null)\n\nexport const ImpersonationClientConfig = ({\n\tchildren,\n\tvalue,\n}: {\n\tchildren: ReactNode\n\tvalue: ServerClientOptions\n}) => {\n\tconst [status, setStatus] = useState(value.status)\n\tuseEffect(() => {\n\t\tsetStatus(value.status)\n\t}, [value.status])\n\tconst live = useMemo(() => ({ ...value, setStatus, status }), [status, value])\n\treturn <Context.Provider value={live}>{children}</Context.Provider>\n}\n\nexport const useImpersonationClient = () => useContext(Context)\n"],"mappings":";;;;AAqBA,MAAM,UAAU,cAAiD,IAAI;AAErE,MAAa,6BAA6B,EACzC,UACA,YAIK;CACL,MAAM,CAAC,QAAQ,aAAa,SAAS,MAAM,MAAM;CACjD,gBAAgB;EACf,UAAU,MAAM,MAAM;CACvB,GAAG,CAAC,MAAM,MAAM,CAAC;CACjB,MAAM,OAAO,eAAe;EAAE,GAAG;EAAO;EAAW;CAAO,IAAI,CAAC,QAAQ,KAAK,CAAC;CAC7E,OAAO,oBAAC,QAAQ,UAAT;EAAkB,OAAO;EAAO;CAA2B,CAAA;AACnE;AAEA,MAAa,+BAA+B,WAAW,OAAO"}
@@ -0,0 +1,5 @@
1
+ //#region src/ui/ImpersonationDocumentButton.d.ts
2
+ declare const ImpersonationDocumentButton: () => import("react/jsx-runtime").JSX.Element | null;
3
+ //#endregion
4
+ export { ImpersonationDocumentButton };
5
+ //# sourceMappingURL=ImpersonationDocumentButton.d.ts.map
@@ -0,0 +1,85 @@
1
+ "use client";
2
+ import { keys } from "../translations/keys.js";
3
+ import { useTranslation as useTranslation$1 } from "../translations/useTranslation.js";
4
+ import { useImpersonation } from "./useImpersonation.js";
5
+ import { ImpersonateIcon } from "./ImpersonateIcon.js";
6
+ import { StartConfirmModal } from "./StartConfirmModal.js";
7
+ import { useEffect, useMemo, useState } from "react";
8
+ import { Button, useAuth, useConfig, useDocumentInfo, useDocumentTitle, useModal } from "@payloadcms/ui";
9
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
+ //#region src/ui/ImpersonationDocumentButton.tsx
11
+ const CONFIRM_SLUG = "impersonation-confirm-document";
12
+ const ImpersonationDocumentButton = () => {
13
+ const { collectionSlug, data, id } = useDocumentInfo();
14
+ const { title } = useDocumentTitle();
15
+ const { user } = useAuth();
16
+ const { config } = useConfig();
17
+ const { t } = useTranslation$1();
18
+ const { openModal } = useModal();
19
+ const { apiPath, reasonMode, status, targetFilters } = useImpersonation();
20
+ const filter = collectionSlug ? targetFilters[collectionSlug] : void 0;
21
+ const [matchesFilter, setMatchesFilter] = useState(filter === true);
22
+ useEffect(() => {
23
+ if (!collectionSlug || id === void 0 || filter === void 0 || filter === true) {
24
+ setMatchesFilter(filter === true);
25
+ return;
26
+ }
27
+ const controller = new AbortController();
28
+ const params = new URLSearchParams({
29
+ depth: "0",
30
+ limit: "1",
31
+ where: JSON.stringify({ and: [{ id: { equals: id } }, filter] })
32
+ });
33
+ fetch(`${config.routes.api}/${collectionSlug}?${params.toString()}`, {
34
+ credentials: "include",
35
+ signal: controller.signal
36
+ }).then(async (response) => {
37
+ if (!response.ok) return { docs: [] };
38
+ return await response.json();
39
+ }).then((body) => {
40
+ if (!controller.signal.aborted) setMatchesFilter((body.docs?.length ?? 0) > 0);
41
+ }).catch(() => {
42
+ if (!controller.signal.aborted) setMatchesFilter(false);
43
+ });
44
+ return () => controller.abort();
45
+ }, [
46
+ collectionSlug,
47
+ config.routes.api,
48
+ filter,
49
+ id
50
+ ]);
51
+ const target = useMemo(() => {
52
+ if (!collectionSlug || id === void 0 || filter === void 0) return null;
53
+ return {
54
+ collection: collectionSlug,
55
+ id,
56
+ label: (data && typeof data === "object" ? String(data.name ?? data.email ?? "") : "") || title || String(id)
57
+ };
58
+ }, [
59
+ collectionSlug,
60
+ data,
61
+ filter,
62
+ id,
63
+ title
64
+ ]);
65
+ const self = user && collectionSlug === user.collection && id !== void 0 && String(user.id) === String(id);
66
+ if (!target || !matchesFilter || status.active || self) return null;
67
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
68
+ buttonStyle: "subtle",
69
+ className: "impersonation-document-button",
70
+ icon: /* @__PURE__ */ jsx(ImpersonateIcon, {}),
71
+ iconPosition: "left",
72
+ margin: false,
73
+ onClick: () => openModal(CONFIRM_SLUG),
74
+ children: t(keys.switchToUser)
75
+ }), /* @__PURE__ */ jsx(StartConfirmModal, {
76
+ apiPath,
77
+ modalSlug: CONFIRM_SLUG,
78
+ reasonMode,
79
+ target
80
+ }, `${target.collection}:${target.id}`)] });
81
+ };
82
+ //#endregion
83
+ export { ImpersonationDocumentButton };
84
+
85
+ //# sourceMappingURL=ImpersonationDocumentButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImpersonationDocumentButton.js","names":["useTranslation"],"sources":["../../src/ui/ImpersonationDocumentButton.tsx"],"sourcesContent":["'use client'\n\nimport {\n\tButton,\n\tuseAuth,\n\tuseConfig,\n\tuseDocumentInfo,\n\tuseDocumentTitle,\n\tuseModal,\n} from '@payloadcms/ui'\nimport { useEffect, useMemo, useState } from 'react'\n\nimport { keys } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\nimport { ImpersonateIcon } from './ImpersonateIcon'\nimport { StartConfirmModal, type StartTarget } from './StartConfirmModal'\nimport { useImpersonation } from './useImpersonation'\n\nconst CONFIRM_SLUG = 'impersonation-confirm-document'\n\nexport const ImpersonationDocumentButton = () => {\n\tconst { collectionSlug, data, id } = useDocumentInfo()\n\tconst { title } = useDocumentTitle()\n\tconst { user } = useAuth()\n\tconst { config } = useConfig()\n\tconst { t } = useTranslation()\n\tconst { openModal } = useModal()\n\tconst { apiPath, reasonMode, status, targetFilters } = useImpersonation()\n\tconst filter = collectionSlug ? targetFilters[collectionSlug] : undefined\n\tconst [matchesFilter, setMatchesFilter] = useState(filter === true)\n\n\tuseEffect(() => {\n\t\tif (!collectionSlug || id === undefined || filter === undefined || filter === true) {\n\t\t\tsetMatchesFilter(filter === true)\n\t\t\treturn\n\t\t}\n\t\tconst controller = new AbortController()\n\t\tconst params = new URLSearchParams({\n\t\t\tdepth: '0',\n\t\t\tlimit: '1',\n\t\t\twhere: JSON.stringify({ and: [{ id: { equals: id } }, filter] }),\n\t\t})\n\t\tvoid fetch(`${config.routes.api}/${collectionSlug}?${params.toString()}`, {\n\t\t\tcredentials: 'include',\n\t\t\tsignal: controller.signal,\n\t\t})\n\t\t\t.then(async (response) => {\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\treturn { docs: [] }\n\t\t\t\t}\n\t\t\t\treturn (await response.json()) as { docs?: unknown[] }\n\t\t\t})\n\t\t\t.then((body) => {\n\t\t\t\tif (!controller.signal.aborted) {\n\t\t\t\t\tsetMatchesFilter((body.docs?.length ?? 0) > 0)\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {\n\t\t\t\tif (!controller.signal.aborted) {\n\t\t\t\t\tsetMatchesFilter(false)\n\t\t\t\t}\n\t\t\t})\n\t\treturn () => controller.abort()\n\t}, [collectionSlug, config.routes.api, filter, id])\n\n\tconst target = useMemo<null | StartTarget>(() => {\n\t\tif (!collectionSlug || id === undefined || filter === undefined) {\n\t\t\treturn null\n\t\t}\n\t\tconst fromData =\n\t\t\tdata && typeof data === 'object'\n\t\t\t\t? String(\n\t\t\t\t\t\t(data as { email?: string; name?: string }).name ??\n\t\t\t\t\t\t\t(data as { email?: string }).email ??\n\t\t\t\t\t\t\t''\n\t\t\t\t\t)\n\t\t\t\t: ''\n\t\treturn {\n\t\t\tcollection: collectionSlug,\n\t\t\tid,\n\t\t\tlabel: fromData || title || String(id),\n\t\t}\n\t}, [collectionSlug, data, filter, id, title])\n\n\tconst self =\n\t\tuser && collectionSlug === user.collection && id !== undefined && String(user.id) === String(id)\n\n\tif (!target || !matchesFilter || status.active || self) {\n\t\treturn null\n\t}\n\n\treturn (\n\t\t<>\n\t\t\t<Button\n\t\t\t\tbuttonStyle=\"subtle\"\n\t\t\t\tclassName=\"impersonation-document-button\"\n\t\t\t\ticon={<ImpersonateIcon />}\n\t\t\t\ticonPosition=\"left\"\n\t\t\t\tmargin={false}\n\t\t\t\tonClick={() => openModal(CONFIRM_SLUG)}\n\t\t\t>\n\t\t\t\t{t(keys.switchToUser)}\n\t\t\t</Button>\n\t\t\t<StartConfirmModal\n\t\t\t\tapiPath={apiPath}\n\t\t\t\tkey={`${target.collection}:${target.id}`}\n\t\t\t\tmodalSlug={CONFIRM_SLUG}\n\t\t\t\treasonMode={reasonMode}\n\t\t\t\ttarget={target}\n\t\t\t/>\n\t\t</>\n\t)\n}\n"],"mappings":";;;;;;;;;;AAkBA,MAAM,eAAe;AAErB,MAAa,oCAAoC;CAChD,MAAM,EAAE,gBAAgB,MAAM,OAAO,gBAAgB;CACrD,MAAM,EAAE,UAAU,iBAAiB;CACnC,MAAM,EAAE,SAAS,QAAQ;CACzB,MAAM,EAAE,WAAW,UAAU;CAC7B,MAAM,EAAE,MAAMA,iBAAe;CAC7B,MAAM,EAAE,cAAc,SAAS;CAC/B,MAAM,EAAE,SAAS,YAAY,QAAQ,kBAAkB,iBAAiB;CACxE,MAAM,SAAS,iBAAiB,cAAc,kBAAkB,KAAA;CAChE,MAAM,CAAC,eAAe,oBAAoB,SAAS,WAAW,IAAI;CAElE,gBAAgB;EACf,IAAI,CAAC,kBAAkB,OAAO,KAAA,KAAa,WAAW,KAAA,KAAa,WAAW,MAAM;GACnF,iBAAiB,WAAW,IAAI;GAChC;EACD;EACA,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,SAAS,IAAI,gBAAgB;GAClC,OAAO;GACP,OAAO;GACP,OAAO,KAAK,UAAU,EAAE,KAAK,CAAC,EAAE,IAAI,EAAE,QAAQ,GAAG,EAAE,GAAG,MAAM,EAAE,CAAC;EAChE,CAAC;EACD,MAAW,GAAG,OAAO,OAAO,IAAI,GAAG,eAAe,GAAG,OAAO,SAAS,KAAK;GACzE,aAAa;GACb,QAAQ,WAAW;EACpB,CAAC,EACC,KAAK,OAAO,aAAa;GACzB,IAAI,CAAC,SAAS,IACb,OAAO,EAAE,MAAM,CAAC,EAAE;GAEnB,OAAQ,MAAM,SAAS,KAAK;EAC7B,CAAC,EACA,MAAM,SAAS;GACf,IAAI,CAAC,WAAW,OAAO,SACtB,kBAAkB,KAAK,MAAM,UAAU,KAAK,CAAC;EAE/C,CAAC,EACA,YAAY;GACZ,IAAI,CAAC,WAAW,OAAO,SACtB,iBAAiB,KAAK;EAExB,CAAC;EACF,aAAa,WAAW,MAAM;CAC/B,GAAG;EAAC;EAAgB,OAAO,OAAO;EAAK;EAAQ;CAAE,CAAC;CAElD,MAAM,SAAS,cAAkC;EAChD,IAAI,CAAC,kBAAkB,OAAO,KAAA,KAAa,WAAW,KAAA,GACrD,OAAO;EAUR,OAAO;GACN,YAAY;GACZ;GACA,QAVA,QAAQ,OAAO,SAAS,WACrB,OACC,KAA2C,QAC1C,KAA4B,SAC7B,EACF,IACC,OAIgB,SAAS,OAAO,EAAE;EACtC;CACD,GAAG;EAAC;EAAgB;EAAM;EAAQ;EAAI;CAAK,CAAC;CAE5C,MAAM,OACL,QAAQ,mBAAmB,KAAK,cAAc,OAAO,KAAA,KAAa,OAAO,KAAK,EAAE,MAAM,OAAO,EAAE;CAEhG,IAAI,CAAC,UAAU,CAAC,iBAAiB,OAAO,UAAU,MACjD,OAAO;CAGR,OACC,qBAAA,UAAA,EAAA,UAAA,CACC,oBAAC,QAAD;EACC,aAAY;EACZ,WAAU;EACV,MAAM,oBAAC,iBAAD,CAAkB,CAAA;EACxB,cAAa;EACb,QAAQ;EACR,eAAe,UAAU,YAAY;YAEpC,EAAE,KAAK,YAAY;CACb,CAAA,GACR,oBAAC,mBAAD;EACU;EAET,WAAW;EACC;EACJ;CACR,GAJK,GAAG,OAAO,WAAW,GAAG,OAAO,IAIpC,CACA,EAAA,CAAA;AAEJ"}