@10x-media/impersonation 0.1.0-beta.1 → 0.1.0-beta.2
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 +6 -0
- package/LICENSE +21 -21
- package/dist/endpoints/codes.js +22 -0
- package/dist/endpoints/codes.js.map +1 -0
- package/dist/translations/errorKey.js +26 -0
- package/dist/translations/errorKey.js.map +1 -0
- package/dist/translations/lookup.js +12 -7
- package/dist/translations/lookup.js.map +1 -1
- package/dist/ui/EndSessionMenuItem.js +1 -1
- package/dist/ui/ImpersonationBar.d.ts +8 -1
- package/dist/ui/ImpersonationBar.js +22 -22
- package/dist/ui/ImpersonationBar.js.map +1 -1
- package/dist/ui/ImpersonationProvider.js +14 -6
- package/dist/ui/ImpersonationProvider.js.map +1 -1
- package/dist/ui/StartConfirmModal.js +2 -1
- package/dist/ui/StartConfirmModal.js.map +1 -1
- package/dist/ui/api.js +2 -21
- package/dist/ui/api.js.map +1 -1
- package/dist/ui/sessionEnds.js +28 -0
- package/dist/ui/sessionEnds.js.map +1 -0
- package/dist/ui/useImpersonation.js +1 -0
- package/dist/ui/useImpersonation.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @10x-media/impersonation
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- The admin bar no longer throws a hydration error on page load: its session countdown appears once the bar has mounted. The bar's text, button labels and exit errors now take overrides from the `translations` option and from `i18n.translations`, as the plugin's other admin strings do.
|
|
8
|
+
|
|
3
9
|
## 0.1.0-beta.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 10x Media GmbH
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 10x Media GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/endpoints/codes.ts
|
|
2
|
+
const FAILURE_CODES = [
|
|
3
|
+
"alreadyImpersonating",
|
|
4
|
+
"failed",
|
|
5
|
+
"forbidden",
|
|
6
|
+
"impersonatorGone",
|
|
7
|
+
"impersonatorSessionExpired",
|
|
8
|
+
"invalidBody",
|
|
9
|
+
"notImpersonating",
|
|
10
|
+
"origin",
|
|
11
|
+
"reasonRequired",
|
|
12
|
+
"selfTarget",
|
|
13
|
+
"targetNotFound",
|
|
14
|
+
"targetTrashed",
|
|
15
|
+
"targetUnverified",
|
|
16
|
+
"unsupportedAuth",
|
|
17
|
+
"unsupportedCollection"
|
|
18
|
+
];
|
|
19
|
+
//#endregion
|
|
20
|
+
export { FAILURE_CODES };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=codes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codes.js","names":[],"sources":["../../src/endpoints/codes.ts"],"sourcesContent":["export const FAILURE_CODES = [\n\t'alreadyImpersonating',\n\t'failed',\n\t'forbidden',\n\t'impersonatorGone',\n\t'impersonatorSessionExpired',\n\t'invalidBody',\n\t'notImpersonating',\n\t'origin',\n\t'reasonRequired',\n\t'selfTarget',\n\t'targetNotFound',\n\t'targetTrashed',\n\t'targetUnverified',\n\t'unsupportedAuth',\n\t'unsupportedCollection',\n] as const\n\nexport type FailureCode = (typeof FAILURE_CODES)[number]\n"],"mappings":";AAAA,MAAa,gBAAgB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { keys } from "./keys.js";
|
|
2
|
+
//#region src/translations/errorKey.ts
|
|
3
|
+
/** The message key for a failure code an endpoint answers with. */
|
|
4
|
+
const errorKey = (code) => {
|
|
5
|
+
return {
|
|
6
|
+
alreadyImpersonating: keys.errorAlreadyImpersonating,
|
|
7
|
+
failed: keys.errorFailed,
|
|
8
|
+
forbidden: keys.errorForbidden,
|
|
9
|
+
impersonatorGone: keys.errorImpersonatorGone,
|
|
10
|
+
impersonatorSessionExpired: keys.errorImpersonatorSessionExpired,
|
|
11
|
+
invalidBody: keys.errorInvalidBody,
|
|
12
|
+
notImpersonating: keys.errorNotImpersonating,
|
|
13
|
+
origin: keys.errorOrigin,
|
|
14
|
+
reasonRequired: keys.errorReasonRequired,
|
|
15
|
+
selfTarget: keys.errorSelfTarget,
|
|
16
|
+
targetNotFound: keys.errorTargetNotFound,
|
|
17
|
+
targetTrashed: keys.errorTargetTrashed,
|
|
18
|
+
targetUnverified: keys.errorTargetUnverified,
|
|
19
|
+
unsupportedAuth: keys.errorUnsupportedAuth,
|
|
20
|
+
unsupportedCollection: keys.errorUnsupportedCollection
|
|
21
|
+
}[code] ?? keys.errorFailed;
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { errorKey };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=errorKey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errorKey.js","names":[],"sources":["../../src/translations/errorKey.ts"],"sourcesContent":["import { keys, type TranslationKey } from './keys'\n\n/** The message key for a failure code an endpoint answers with. */\nexport const errorKey = (code: string) => {\n\tconst map: Record<string, TranslationKey> = {\n\t\talreadyImpersonating: keys.errorAlreadyImpersonating,\n\t\tfailed: keys.errorFailed,\n\t\tforbidden: keys.errorForbidden,\n\t\timpersonatorGone: keys.errorImpersonatorGone,\n\t\timpersonatorSessionExpired: keys.errorImpersonatorSessionExpired,\n\t\tinvalidBody: keys.errorInvalidBody,\n\t\tnotImpersonating: keys.errorNotImpersonating,\n\t\torigin: keys.errorOrigin,\n\t\treasonRequired: keys.errorReasonRequired,\n\t\tselfTarget: keys.errorSelfTarget,\n\t\ttargetNotFound: keys.errorTargetNotFound,\n\t\ttargetTrashed: keys.errorTargetTrashed,\n\t\ttargetUnverified: keys.errorTargetUnverified,\n\t\tunsupportedAuth: keys.errorUnsupportedAuth,\n\t\tunsupportedCollection: keys.errorUnsupportedCollection,\n\t}\n\treturn map[code] ?? keys.errorFailed\n}\n"],"mappings":";;;AAGA,MAAa,YAAY,SAAiB;CAkBzC,OAAO;EAhBN,sBAAsB,KAAK;EAC3B,QAAQ,KAAK;EACb,WAAW,KAAK;EAChB,kBAAkB,KAAK;EACvB,4BAA4B,KAAK;EACjC,aAAa,KAAK;EAClB,kBAAkB,KAAK;EACvB,QAAQ,KAAK;EACb,gBAAgB,KAAK;EACrB,YAAY,KAAK;EACjB,gBAAgB,KAAK;EACrB,eAAe,KAAK;EACpB,kBAAkB,KAAK;EACvB,iBAAiB,KAAK;EACtB,uBAAuB,KAAK;CAEpB,EAAE,SAAS,KAAK;AAC1B"}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { translations } from "./index.js";
|
|
2
2
|
//#region src/translations/lookup.ts
|
|
3
|
-
const
|
|
4
|
-
/** Resolve a plugin string in `locale`, falling back to English. */
|
|
5
|
-
const messageFor = (locale, key) => {
|
|
6
|
-
const code = (locale ?? "en").split("-")[0] ?? "en";
|
|
3
|
+
const valueIn = (source, locale, key) => {
|
|
7
4
|
const separator = key.indexOf(":");
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
const value = source?.[locale]?.[key.slice(0, separator)]?.[key.slice(separator + 1)];
|
|
6
|
+
return typeof value === "string" ? value : void 0;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a plugin string in `locale`, falling back to English. Pass the config's
|
|
10
|
+
* `i18n.translations` as `source` so host and `translations` option overrides apply; the plugin's
|
|
11
|
+
* bundled strings back it.
|
|
12
|
+
*/
|
|
13
|
+
const messageFor = (locale, key, source) => {
|
|
14
|
+
const code = (locale ?? "en").split("-")[0] ?? "en";
|
|
15
|
+
return valueIn(source, code, key) ?? valueIn(translations, code, key) ?? valueIn(source, "en", key) ?? valueIn(translations, "en", key) ?? key;
|
|
11
16
|
};
|
|
12
17
|
/** Replace every `token` without interpreting `$` in `value`. */
|
|
13
18
|
const fillTemplate = (template, token, value) => template.split(token).join(value);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lookup.js","names":[],"sources":["../../src/translations/lookup.ts"],"sourcesContent":["import { translations } from './index'\nimport type { TranslationKey } from './keys'\n\nconst
|
|
1
|
+
{"version":3,"file":"lookup.js","names":[],"sources":["../../src/translations/lookup.ts"],"sourcesContent":["import { translations } from './index'\nimport type { TranslationKey } from './keys'\n\ntype Nested = Record<string, Record<string, Record<string, unknown> | undefined> | undefined>\n\nconst valueIn = (source: unknown, locale: string, key: TranslationKey): string | undefined => {\n\tconst separator = key.indexOf(':')\n\tconst value = (source as Nested | null | undefined)?.[locale]?.[key.slice(0, separator)]?.[\n\t\tkey.slice(separator + 1)\n\t]\n\treturn typeof value === 'string' ? value : undefined\n}\n\n/**\n * Resolve a plugin string in `locale`, falling back to English. Pass the config's\n * `i18n.translations` as `source` so host and `translations` option overrides apply; the plugin's\n * bundled strings back it.\n */\nexport const messageFor = (\n\tlocale: null | string | undefined,\n\tkey: TranslationKey,\n\tsource?: unknown\n): string => {\n\tconst code = (locale ?? 'en').split('-')[0] ?? 'en'\n\treturn (\n\t\tvalueIn(source, code, key) ??\n\t\tvalueIn(translations, code, key) ??\n\t\tvalueIn(source, 'en', key) ??\n\t\tvalueIn(translations, 'en', key) ??\n\t\tkey\n\t)\n}\n\n/** Replace every `token` without interpreting `$` in `value`. */\nexport const fillTemplate = (template: string, token: string, value: string): string =>\n\ttemplate.split(token).join(value)\n"],"mappings":";;AAKA,MAAM,WAAW,QAAiB,QAAgB,QAA4C;CAC7F,MAAM,YAAY,IAAI,QAAQ,GAAG;CACjC,MAAM,QAAS,SAAuC,UAAU,IAAI,MAAM,GAAG,SAAS,KACrF,IAAI,MAAM,YAAY,CAAC;CAExB,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC5C;;;;;;AAOA,MAAa,cACZ,QACA,KACA,WACY;CACZ,MAAM,QAAQ,UAAU,MAAM,MAAM,GAAG,EAAE,MAAM;CAC/C,OACC,QAAQ,QAAQ,MAAM,GAAG,KACzB,QAAQ,cAAc,MAAM,GAAG,KAC/B,QAAQ,QAAQ,MAAM,GAAG,KACzB,QAAQ,cAAc,MAAM,GAAG,KAC/B;AAEF;;AAGA,MAAa,gBAAgB,UAAkB,OAAe,UAC7D,SAAS,MAAM,KAAK,EAAE,KAAK,KAAK"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { keys } from "../translations/keys.js";
|
|
3
3
|
import { useTranslation as useTranslation$1 } from "../translations/useTranslation.js";
|
|
4
|
-
import { errorKey } from "
|
|
4
|
+
import { errorKey } from "../translations/errorKey.js";
|
|
5
5
|
import { useImpersonation } from "./useImpersonation.js";
|
|
6
6
|
import { useState } from "react";
|
|
7
7
|
import { Button, ConfirmationModal, toast, useDocumentInfo, useModal } from "@payloadcms/ui";
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { FailureCode } from "../endpoints/codes.js";
|
|
1
2
|
//#region src/ui/ImpersonationBar.d.ts
|
|
2
3
|
type ImpersonationBarProps = {
|
|
3
4
|
actingAs: string;
|
|
4
5
|
adminRoute: string;
|
|
5
6
|
apiPath: string;
|
|
6
7
|
frontendUrl?: string;
|
|
7
|
-
impersonatorLocale?: null | string;
|
|
8
|
+
impersonatorLocale?: null | string; /** Resolved by the provider from the config's translations, so overrides reach the client. */
|
|
9
|
+
messages?: {
|
|
10
|
+
collapseBar: string;
|
|
11
|
+
errors: Partial<Record<FailureCode, string>>;
|
|
12
|
+
expandBar: string;
|
|
13
|
+
};
|
|
8
14
|
openWebsite: string;
|
|
9
15
|
pluginName: string;
|
|
10
16
|
returnTo: string;
|
|
@@ -21,6 +27,7 @@ declare const ImpersonationBar: ({
|
|
|
21
27
|
apiPath,
|
|
22
28
|
frontendUrl,
|
|
23
29
|
impersonatorLocale,
|
|
30
|
+
messages,
|
|
24
31
|
openWebsite,
|
|
25
32
|
pluginName,
|
|
26
33
|
returnTo,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { CLIENT_FETCH_TIMEOUT_MS } from "../plugin/constants.js";
|
|
3
3
|
import { keys } from "../translations/keys.js";
|
|
4
|
-
import { errorKey
|
|
4
|
+
import { errorKey } from "../translations/errorKey.js";
|
|
5
|
+
import { goAfterSwitch, postImpersonation } from "./api.js";
|
|
5
6
|
import { useImpersonation } from "./useImpersonation.js";
|
|
6
|
-
import {
|
|
7
|
+
import { messageFor } from "../translations/lookup.js";
|
|
7
8
|
import { anchorsFor, nearestEdge, projectThrow, readBarPlacement, readViewport, writeBarPlacement } from "./barPlacement.js";
|
|
9
|
+
import { sessionEndsLabel } from "./sessionEnds.js";
|
|
8
10
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
11
|
import { Button, Pill, toast } from "@payloadcms/ui";
|
|
10
12
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -17,18 +19,7 @@ const SESSION_GONE = new Set([
|
|
|
17
19
|
"impersonatorSessionExpired",
|
|
18
20
|
"notImpersonating"
|
|
19
21
|
]);
|
|
20
|
-
const
|
|
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" }) => {
|
|
22
|
+
const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, impersonatorLocale, messages, openWebsite, pluginName, returnTo, sessionEndsAt, sessionEndsAtTemplate, sessionEndsInTemplate, showFrontendLink, targetName, tone = "warning" }) => {
|
|
32
23
|
const { setStatus } = useImpersonation();
|
|
33
24
|
const [mounted, setMounted] = useState(false);
|
|
34
25
|
const onLogin = (usePathname() ?? "") === `${adminRoute}/login`;
|
|
@@ -41,7 +32,7 @@ const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, imperson
|
|
|
41
32
|
width: 1200
|
|
42
33
|
} : readViewport());
|
|
43
34
|
const [dragPoint, setDragPoint] = useState(null);
|
|
44
|
-
const [now, setNow] = useState(
|
|
35
|
+
const [now, setNow] = useState(null);
|
|
45
36
|
const chipRef = useRef(null);
|
|
46
37
|
const expiredProbe = useRef(false);
|
|
47
38
|
const dragOffset = useRef(null);
|
|
@@ -91,6 +82,7 @@ const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, imperson
|
|
|
91
82
|
useEffect(() => {
|
|
92
83
|
if (!sessionEndsAt || !visible || onLogin) return;
|
|
93
84
|
expiredProbe.current = false;
|
|
85
|
+
setNow(Date.now());
|
|
94
86
|
const timer = window.setInterval(() => {
|
|
95
87
|
const next = Date.now();
|
|
96
88
|
setNow(next);
|
|
@@ -113,7 +105,8 @@ const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, imperson
|
|
|
113
105
|
try {
|
|
114
106
|
const result = await postImpersonation(`${apiPath}/exit`, {});
|
|
115
107
|
if (!result.ok) {
|
|
116
|
-
|
|
108
|
+
const code = result.error ?? "failed";
|
|
109
|
+
toast.error(messages?.errors[code] ?? messageFor(impersonatorLocale, errorKey(code)));
|
|
117
110
|
if (result.error && SESSION_GONE.has(result.error)) {
|
|
118
111
|
setStatus({ active: false });
|
|
119
112
|
setVisible(false);
|
|
@@ -267,7 +260,7 @@ const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, imperson
|
|
|
267
260
|
className: "impersonation-chip__name",
|
|
268
261
|
children: targetName
|
|
269
262
|
}), /* @__PURE__ */ jsx("button", {
|
|
270
|
-
"aria-label": messageFor(impersonatorLocale, keys.expandBar),
|
|
263
|
+
"aria-label": messages?.expandBar ?? messageFor(impersonatorLocale, keys.expandBar),
|
|
271
264
|
className: "impersonation-bar__icon",
|
|
272
265
|
onClick: (event) => {
|
|
273
266
|
if (dragged.current) {
|
|
@@ -281,8 +274,15 @@ const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, imperson
|
|
|
281
274
|
})]
|
|
282
275
|
});
|
|
283
276
|
}
|
|
284
|
-
const
|
|
285
|
-
|
|
277
|
+
const endsLabel = sessionEndsLabel({
|
|
278
|
+
endsAt: sessionEndsAt,
|
|
279
|
+
locale: impersonatorLocale,
|
|
280
|
+
now,
|
|
281
|
+
templates: {
|
|
282
|
+
endsAt: sessionEndsAtTemplate,
|
|
283
|
+
endsIn: sessionEndsInTemplate
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
286
|
return /* @__PURE__ */ jsxs("div", {
|
|
287
287
|
"aria-live": "polite",
|
|
288
288
|
className: tone === "quiet" ? "impersonation-bar impersonation-bar--quiet" : "impersonation-bar",
|
|
@@ -296,9 +296,9 @@ const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, imperson
|
|
|
296
296
|
size: "small",
|
|
297
297
|
children: pluginName
|
|
298
298
|
}), /* @__PURE__ */ jsx("span", { children: actingAs })]
|
|
299
|
-
}),
|
|
299
|
+
}), sessionEndsAt ? /* @__PURE__ */ jsx("div", {
|
|
300
300
|
className: "impersonation-bar__time",
|
|
301
|
-
children: endsLabel
|
|
301
|
+
children: endsLabel ?? "\xA0"
|
|
302
302
|
}) : null]
|
|
303
303
|
}), /* @__PURE__ */ jsxs("div", {
|
|
304
304
|
className: "impersonation-bar__actions",
|
|
@@ -317,7 +317,7 @@ const ImpersonationBar = ({ actingAs, adminRoute, apiPath, frontendUrl, imperson
|
|
|
317
317
|
children: returnTo
|
|
318
318
|
}),
|
|
319
319
|
/* @__PURE__ */ jsx("button", {
|
|
320
|
-
"aria-label": messageFor(impersonatorLocale, keys.collapseBar),
|
|
320
|
+
"aria-label": messages?.collapseBar ?? messageFor(impersonatorLocale, keys.collapseBar),
|
|
321
321
|
className: "impersonation-bar__icon",
|
|
322
322
|
onClick: collapse,
|
|
323
323
|
type: "button",
|
|
@@ -1 +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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"}
|
|
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 { messageFor } from '../translations/lookup'\nimport { errorKey, type FailureCode, 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 { sessionEndsLabel } from './sessionEnds'\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\nexport type ImpersonationBarProps = {\n\tactingAs: string\n\tadminRoute: string\n\tapiPath: string\n\tfrontendUrl?: string\n\timpersonatorLocale?: null | string\n\t/** Resolved by the provider from the config's translations, so overrides reach the client. */\n\tmessages?: {\n\t\tcollapseBar: string\n\t\terrors: Partial<Record<FailureCode, string>>\n\t\texpandBar: string\n\t}\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\tmessages,\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<null | number>(null)\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\tsetNow(Date.now())\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\tconst code = result.error ?? 'failed'\n\t\t\t\ttoast.error(messages?.errors[code] ?? messageFor(impersonatorLocale, errorKey(code)))\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={messages?.expandBar ?? 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 endsLabel = sessionEndsLabel({\n\t\tendsAt: sessionEndsAt,\n\t\tlocale: impersonatorLocale,\n\t\tnow,\n\t\ttemplates: { endsAt: sessionEndsAtTemplate, endsIn: sessionEndsInTemplate },\n\t})\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{sessionEndsAt ? (\n\t\t\t\t\t<div className=\"impersonation-bar__time\">{endsLabel ?? '\\u00a0'}</div>\n\t\t\t\t) : 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={messages?.collapseBar ?? 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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"}
|
|
@@ -8,9 +8,11 @@ import { readHintCookie } from "../plugin/lookup.js";
|
|
|
8
8
|
import { startableCollectionSlugs } from "../plugin/startable.js";
|
|
9
9
|
import { keys } from "../translations/keys.js";
|
|
10
10
|
import { resolveTargetFilters } from "../access/filterTargets.js";
|
|
11
|
+
import { errorKey } from "../translations/errorKey.js";
|
|
11
12
|
import { ImpersonationClientConfig } from "./ImpersonationConfig.js";
|
|
12
13
|
import { fillTemplate, messageFor } from "../translations/lookup.js";
|
|
13
14
|
import { ImpersonationBar } from "./ImpersonationBar.js";
|
|
15
|
+
import { FAILURE_CODES } from "../endpoints/codes.js";
|
|
14
16
|
import { createLocalReq } from "payload";
|
|
15
17
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
16
18
|
import { headers } from "next/headers";
|
|
@@ -90,6 +92,7 @@ const ImpersonationProvider = async ({ children, i18n, payload, user }) => {
|
|
|
90
92
|
children
|
|
91
93
|
});
|
|
92
94
|
const locale = barSource.impersonatorLocale;
|
|
95
|
+
const message = (key) => messageFor(locale, key, payload.config.i18n.translations);
|
|
93
96
|
const targetName = labelUser(options.ui.userLabel, {
|
|
94
97
|
email: barSource.targetEmail,
|
|
95
98
|
title: barSource.targetTitle
|
|
@@ -99,8 +102,8 @@ const ImpersonationProvider = async ({ children, i18n, payload, user }) => {
|
|
|
99
102
|
title: barSource.impersonatorTitle
|
|
100
103
|
}) ?? String(barSource.impersonatorId ?? "");
|
|
101
104
|
const isImpersonatorSide = barSource.side === "impersonator" && barSource.mode === "parallel";
|
|
102
|
-
const actingAs = isImpersonatorSide ? fillTemplate(
|
|
103
|
-
const returnTo = isImpersonatorSide ?
|
|
105
|
+
const actingAs = isImpersonatorSide ? fillTemplate(message(keys.sessionActiveOnSite), "{{name}}", String(targetName)) : fillTemplate(message(keys.actingAs), "{{name}}", String(targetName));
|
|
106
|
+
const returnTo = isImpersonatorSide ? message(keys.endSession) : fillTemplate(message(keys.returnTo), "{{name}}", String(impersonatorLabel));
|
|
104
107
|
return /* @__PURE__ */ jsxs(ImpersonationClientConfig, {
|
|
105
108
|
value: clientValue,
|
|
106
109
|
children: [children, /* @__PURE__ */ jsx(ImpersonationBar, {
|
|
@@ -109,12 +112,17 @@ const ImpersonationProvider = async ({ children, i18n, payload, user }) => {
|
|
|
109
112
|
apiPath,
|
|
110
113
|
frontendUrl: payload.config.serverURL || void 0,
|
|
111
114
|
impersonatorLocale: locale,
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
messages: {
|
|
116
|
+
collapseBar: message(keys.collapseBar),
|
|
117
|
+
errors: Object.fromEntries(FAILURE_CODES.map((code) => [code, message(errorKey(code))])),
|
|
118
|
+
expandBar: message(keys.expandBar)
|
|
119
|
+
},
|
|
120
|
+
openWebsite: message(keys.openWebsite),
|
|
121
|
+
pluginName: message(keys.pluginName),
|
|
114
122
|
returnTo,
|
|
115
123
|
sessionEndsAt: barSource.absoluteExpiresAt,
|
|
116
|
-
sessionEndsAtTemplate:
|
|
117
|
-
sessionEndsInTemplate:
|
|
124
|
+
sessionEndsAtTemplate: message(keys.sessionEndsAt),
|
|
125
|
+
sessionEndsInTemplate: message(keys.sessionEndsIn),
|
|
118
126
|
showFrontendLink: isImpersonatorSide,
|
|
119
127
|
targetName,
|
|
120
128
|
tone: isImpersonatorSide ? "quiet" : "warning"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImpersonationProvider.js","names":["nextHeaders"],"sources":["../../src/ui/ImpersonationProvider.tsx"],"sourcesContent":["import { headers as nextHeaders } from 'next/headers'\nimport type { ServerProps } from 'payload'\nimport { createLocalReq } from 'payload'\nimport type { ReactNode } from 'react'\n\nimport { resolveTargetFilters } from '../access/filterTargets'\nimport { statusFromRow } from '../getImpersonation'\nimport { labelUser } from '../ids'\nimport { IMPERSONATION_SID_PREFIX } from '../plugin/constants'\nimport { readHintCookie } from '../plugin/lookup'\nimport { getRegistry } from '../plugin/registry'\nimport { startableCollectionSlugs } from '../plugin/startable'\nimport {\n\tcachedFindOpenBySid,\n\timpersonationSide,\n\tisPastAbsoluteExpiry,\n\trelationOf,\n} from '../session/resolve'\nimport { keys } from '../translations/keys'\nimport { fillTemplate, messageFor } from '../translations/lookup'\nimport { asAuthUser, boundSid } from '../types'\nimport { ImpersonationBar } from './ImpersonationBar'\nimport { ImpersonationClientConfig } from './ImpersonationConfig'\n\nexport type ImpersonationProviderProps = ServerProps & { children?: ReactNode }\n\nexport const ImpersonationProvider = async ({\n\tchildren,\n\ti18n,\n\tpayload,\n\tuser,\n}: ImpersonationProviderProps) => {\n\tconst options = getRegistry(payload.config)\n\tif (!options) {\n\t\treturn children\n\t}\n\n\tconst startable = startableCollectionSlugs(payload.config.collections, options)\n\tlet targets: Awaited<ReturnType<typeof resolveTargetFilters>> = Object.fromEntries(\n\t\tstartable.map((slug) => [slug, true as const])\n\t)\n\tif (user && options.access.filterTargets) {\n\t\tconst headerList = await nextHeaders()\n\t\tconst req = await createLocalReq({ req: { headers: headerList, i18n } as never, user }, payload)\n\t\ttargets = await resolveTargetFilters({ collections: startable, options, req })\n\t}\n\n\tconst apiPath = `${payload.config.routes.api}${options.apiPath}`\n\tconst sid = boundSid(user, options.session.binding)\n\tconst painted = user ? asAuthUser(user)._impersonation : undefined\n\tconst paintedLive = painted && !isPastAbsoluteExpiry(painted) ? painted : undefined\n\tconst hint = await readHintCookie(options.hintCookieName)\n\tconst shouldLookup =\n\t\t!painted &&\n\t\t(Boolean(options.session.issue) ||\n\t\t\tBoolean(hint) ||\n\t\t\tBoolean(sid?.startsWith(IMPERSONATION_SID_PREFIX)))\n\n\tlet row = null\n\tif (!painted && user && sid && shouldLookup) {\n\t\trow = await cachedFindOpenBySid(sid, options, payload)\n\t\tif (row && isPastAbsoluteExpiry(row)) {\n\t\t\trow = null\n\t\t}\n\t}\n\n\tconst status = paintedLive\n\t\t? {\n\t\t\t\tabsoluteExpiresAt: paintedLive.absoluteExpiresAt ?? null,\n\t\t\t\tactive: true as const,\n\t\t\t\timpersonator: paintedLive.impersonator,\n\t\t\t\timpersonatorLocale: paintedLive.impersonatorLocale ?? null,\n\t\t\t\tmode: paintedLive.mode,\n\t\t\t\tside: 'target' as const,\n\t\t\t\tstartedAt: paintedLive.startedAt,\n\t\t\t\ttarget: paintedLive.target,\n\t\t\t\ttargetEmail: paintedLive.targetEmail ?? null,\n\t\t\t\ttargetTitle: paintedLive.targetTitle ?? paintedLive.targetEmail ?? null,\n\t\t\t}\n\t\t: row && sid\n\t\t\t? statusFromRow(row, sid)\n\t\t\t: { active: false as const }\n\n\tconst barSource = paintedLive\n\t\t? {\n\t\t\t\tabsoluteExpiresAt: paintedLive.absoluteExpiresAt,\n\t\t\t\timpersonatorEmail: paintedLive.impersonatorEmail,\n\t\t\t\timpersonatorId: paintedLive.impersonator.id,\n\t\t\t\timpersonatorTitle: paintedLive.impersonatorTitle,\n\t\t\t\timpersonatorLocale: paintedLive.impersonatorLocale,\n\t\t\t\tmode: paintedLive.mode,\n\t\t\t\tside: 'target' as const,\n\t\t\t\ttargetEmail: paintedLive.targetEmail,\n\t\t\t\ttargetId: paintedLive.target.id,\n\t\t\t\ttargetTitle: paintedLive.targetTitle,\n\t\t\t}\n\t\t: row && sid\n\t\t\t? {\n\t\t\t\t\tabsoluteExpiresAt: row.absoluteExpiresAt,\n\t\t\t\t\timpersonatorEmail: row.impersonatorEmail,\n\t\t\t\t\timpersonatorId: relationOf(row.impersonator)?.id,\n\t\t\t\t\timpersonatorTitle: row.impersonatorTitle,\n\t\t\t\t\timpersonatorLocale: row.impersonatorLocale,\n\t\t\t\t\tmode: row.mode,\n\t\t\t\t\tside: impersonationSide(row, sid),\n\t\t\t\t\ttargetEmail: row.targetEmail,\n\t\t\t\t\ttargetId: relationOf(row.target)?.id,\n\t\t\t\t\ttargetTitle: row.targetTitle,\n\t\t\t\t}\n\t\t\t: null\n\n\tconst clientValue = {\n\t\tapiPath,\n\t\tcardEmail: options.ui.cardEmail,\n\t\treasonMode: options.reason,\n\t\tsessionCollection: options.collectionSlug,\n\t\tstatus,\n\t\ttargetFilters: targets,\n\t}\n\n\tif (!options.ui.bar || !barSource) {\n\t\treturn <ImpersonationClientConfig value={clientValue}>{children}</ImpersonationClientConfig>\n\t}\n\n\tconst locale = barSource.impersonatorLocale\n\tconst targetName =\n\t\tlabelUser(options.ui.userLabel, {\n\t\t\temail: barSource.targetEmail,\n\t\t\ttitle: barSource.targetTitle,\n\t\t}) ?? String(barSource.targetId ?? '')\n\tconst impersonatorLabel =\n\t\tlabelUser(options.ui.userLabel, {\n\t\t\temail: barSource.impersonatorEmail,\n\t\t\ttitle: barSource.impersonatorTitle,\n\t\t}) ?? String(barSource.impersonatorId ?? '')\n\tconst isImpersonatorSide = barSource.side === 'impersonator' && barSource.mode === 'parallel'\n\tconst actingAs = isImpersonatorSide\n\t\t? fillTemplate(
|
|
1
|
+
{"version":3,"file":"ImpersonationProvider.js","names":["nextHeaders"],"sources":["../../src/ui/ImpersonationProvider.tsx"],"sourcesContent":["import { headers as nextHeaders } from 'next/headers'\nimport type { ServerProps } from 'payload'\nimport { createLocalReq } from 'payload'\nimport type { ReactNode } from 'react'\n\nimport { resolveTargetFilters } from '../access/filterTargets'\nimport { FAILURE_CODES } from '../endpoints/codes'\nimport { statusFromRow } from '../getImpersonation'\nimport { labelUser } from '../ids'\nimport { IMPERSONATION_SID_PREFIX } from '../plugin/constants'\nimport { readHintCookie } from '../plugin/lookup'\nimport { getRegistry } from '../plugin/registry'\nimport { startableCollectionSlugs } from '../plugin/startable'\nimport {\n\tcachedFindOpenBySid,\n\timpersonationSide,\n\tisPastAbsoluteExpiry,\n\trelationOf,\n} from '../session/resolve'\nimport { errorKey } from '../translations/errorKey'\nimport { keys, type TranslationKey } from '../translations/keys'\nimport { fillTemplate, messageFor } from '../translations/lookup'\nimport { asAuthUser, boundSid } from '../types'\nimport { ImpersonationBar } from './ImpersonationBar'\nimport { ImpersonationClientConfig } from './ImpersonationConfig'\n\nexport type ImpersonationProviderProps = ServerProps & { children?: ReactNode }\n\nexport const ImpersonationProvider = async ({\n\tchildren,\n\ti18n,\n\tpayload,\n\tuser,\n}: ImpersonationProviderProps) => {\n\tconst options = getRegistry(payload.config)\n\tif (!options) {\n\t\treturn children\n\t}\n\n\tconst startable = startableCollectionSlugs(payload.config.collections, options)\n\tlet targets: Awaited<ReturnType<typeof resolveTargetFilters>> = Object.fromEntries(\n\t\tstartable.map((slug) => [slug, true as const])\n\t)\n\tif (user && options.access.filterTargets) {\n\t\tconst headerList = await nextHeaders()\n\t\tconst req = await createLocalReq({ req: { headers: headerList, i18n } as never, user }, payload)\n\t\ttargets = await resolveTargetFilters({ collections: startable, options, req })\n\t}\n\n\tconst apiPath = `${payload.config.routes.api}${options.apiPath}`\n\tconst sid = boundSid(user, options.session.binding)\n\tconst painted = user ? asAuthUser(user)._impersonation : undefined\n\tconst paintedLive = painted && !isPastAbsoluteExpiry(painted) ? painted : undefined\n\tconst hint = await readHintCookie(options.hintCookieName)\n\tconst shouldLookup =\n\t\t!painted &&\n\t\t(Boolean(options.session.issue) ||\n\t\t\tBoolean(hint) ||\n\t\t\tBoolean(sid?.startsWith(IMPERSONATION_SID_PREFIX)))\n\n\tlet row = null\n\tif (!painted && user && sid && shouldLookup) {\n\t\trow = await cachedFindOpenBySid(sid, options, payload)\n\t\tif (row && isPastAbsoluteExpiry(row)) {\n\t\t\trow = null\n\t\t}\n\t}\n\n\tconst status = paintedLive\n\t\t? {\n\t\t\t\tabsoluteExpiresAt: paintedLive.absoluteExpiresAt ?? null,\n\t\t\t\tactive: true as const,\n\t\t\t\timpersonator: paintedLive.impersonator,\n\t\t\t\timpersonatorLocale: paintedLive.impersonatorLocale ?? null,\n\t\t\t\tmode: paintedLive.mode,\n\t\t\t\tside: 'target' as const,\n\t\t\t\tstartedAt: paintedLive.startedAt,\n\t\t\t\ttarget: paintedLive.target,\n\t\t\t\ttargetEmail: paintedLive.targetEmail ?? null,\n\t\t\t\ttargetTitle: paintedLive.targetTitle ?? paintedLive.targetEmail ?? null,\n\t\t\t}\n\t\t: row && sid\n\t\t\t? statusFromRow(row, sid)\n\t\t\t: { active: false as const }\n\n\tconst barSource = paintedLive\n\t\t? {\n\t\t\t\tabsoluteExpiresAt: paintedLive.absoluteExpiresAt,\n\t\t\t\timpersonatorEmail: paintedLive.impersonatorEmail,\n\t\t\t\timpersonatorId: paintedLive.impersonator.id,\n\t\t\t\timpersonatorTitle: paintedLive.impersonatorTitle,\n\t\t\t\timpersonatorLocale: paintedLive.impersonatorLocale,\n\t\t\t\tmode: paintedLive.mode,\n\t\t\t\tside: 'target' as const,\n\t\t\t\ttargetEmail: paintedLive.targetEmail,\n\t\t\t\ttargetId: paintedLive.target.id,\n\t\t\t\ttargetTitle: paintedLive.targetTitle,\n\t\t\t}\n\t\t: row && sid\n\t\t\t? {\n\t\t\t\t\tabsoluteExpiresAt: row.absoluteExpiresAt,\n\t\t\t\t\timpersonatorEmail: row.impersonatorEmail,\n\t\t\t\t\timpersonatorId: relationOf(row.impersonator)?.id,\n\t\t\t\t\timpersonatorTitle: row.impersonatorTitle,\n\t\t\t\t\timpersonatorLocale: row.impersonatorLocale,\n\t\t\t\t\tmode: row.mode,\n\t\t\t\t\tside: impersonationSide(row, sid),\n\t\t\t\t\ttargetEmail: row.targetEmail,\n\t\t\t\t\ttargetId: relationOf(row.target)?.id,\n\t\t\t\t\ttargetTitle: row.targetTitle,\n\t\t\t\t}\n\t\t\t: null\n\n\tconst clientValue = {\n\t\tapiPath,\n\t\tcardEmail: options.ui.cardEmail,\n\t\treasonMode: options.reason,\n\t\tsessionCollection: options.collectionSlug,\n\t\tstatus,\n\t\ttargetFilters: targets,\n\t}\n\n\tif (!options.ui.bar || !barSource) {\n\t\treturn <ImpersonationClientConfig value={clientValue}>{children}</ImpersonationClientConfig>\n\t}\n\n\tconst locale = barSource.impersonatorLocale\n\tconst message = (key: TranslationKey) => messageFor(locale, key, payload.config.i18n.translations)\n\tconst targetName =\n\t\tlabelUser(options.ui.userLabel, {\n\t\t\temail: barSource.targetEmail,\n\t\t\ttitle: barSource.targetTitle,\n\t\t}) ?? String(barSource.targetId ?? '')\n\tconst impersonatorLabel =\n\t\tlabelUser(options.ui.userLabel, {\n\t\t\temail: barSource.impersonatorEmail,\n\t\t\ttitle: barSource.impersonatorTitle,\n\t\t}) ?? String(barSource.impersonatorId ?? '')\n\tconst isImpersonatorSide = barSource.side === 'impersonator' && barSource.mode === 'parallel'\n\tconst actingAs = isImpersonatorSide\n\t\t? fillTemplate(message(keys.sessionActiveOnSite), '{{name}}', String(targetName))\n\t\t: fillTemplate(message(keys.actingAs), '{{name}}', String(targetName))\n\tconst returnTo = isImpersonatorSide\n\t\t? message(keys.endSession)\n\t\t: fillTemplate(message(keys.returnTo), '{{name}}', String(impersonatorLabel))\n\n\treturn (\n\t\t<ImpersonationClientConfig value={clientValue}>\n\t\t\t{children}\n\t\t\t<ImpersonationBar\n\t\t\t\tactingAs={actingAs}\n\t\t\t\tadminRoute={payload.config.routes.admin}\n\t\t\t\tapiPath={apiPath}\n\t\t\t\tfrontendUrl={payload.config.serverURL || undefined}\n\t\t\t\timpersonatorLocale={locale}\n\t\t\t\tmessages={{\n\t\t\t\t\tcollapseBar: message(keys.collapseBar),\n\t\t\t\t\terrors: Object.fromEntries(FAILURE_CODES.map((code) => [code, message(errorKey(code))])),\n\t\t\t\t\texpandBar: message(keys.expandBar),\n\t\t\t\t}}\n\t\t\t\topenWebsite={message(keys.openWebsite)}\n\t\t\t\tpluginName={message(keys.pluginName)}\n\t\t\t\treturnTo={returnTo}\n\t\t\t\tsessionEndsAt={barSource.absoluteExpiresAt}\n\t\t\t\tsessionEndsAtTemplate={message(keys.sessionEndsAt)}\n\t\t\t\tsessionEndsInTemplate={message(keys.sessionEndsIn)}\n\t\t\t\tshowFrontendLink={isImpersonatorSide}\n\t\t\t\ttargetName={targetName}\n\t\t\t\ttone={isImpersonatorSide ? 'quiet' : 'warning'}\n\t\t\t/>\n\t\t</ImpersonationClientConfig>\n\t)\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { keys } from "../translations/keys.js";
|
|
3
3
|
import { useTranslation as useTranslation$1 } from "../translations/useTranslation.js";
|
|
4
|
-
import { errorKey
|
|
4
|
+
import { errorKey } from "../translations/errorKey.js";
|
|
5
|
+
import { goAfterSwitch, postImpersonation } from "./api.js";
|
|
5
6
|
import { useRef, useState } from "react";
|
|
6
7
|
import { ConfirmationModal, TextInput, toast } from "@payloadcms/ui";
|
|
7
8
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StartConfirmModal.js","names":["useTranslation"],"sources":["../../src/ui/StartConfirmModal.tsx"],"sourcesContent":["'use client'\n\nimport { ConfirmationModal, TextInput, toast } from '@payloadcms/ui'\nimport { type ChangeEvent, useRef, useState } from 'react'\n\nimport { keys } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\nimport { errorKey, goAfterSwitch, postImpersonation } from './api'\n\nexport type StartTarget = {\n\tcollection: string\n\tid: number | string\n\tlabel: string\n}\n\nexport type StartConfirmModalProps = {\n\tapiPath: string\n\tmodalSlug: string\n\treasonMode: 'off' | 'optional' | 'required'\n\ttarget: null | StartTarget\n}\n\n/**\n * Payload `ConfirmationModal` for starting a session. Reason uses `TextInput`\n * in the body when the host asked for optional or required notes.\n *\n * `key={generation}` remounts after a failed confirm so the native Loading\n * state resets. Throw still keeps the modal open.\n */\nexport const StartConfirmModal = ({\n\tapiPath,\n\tmodalSlug,\n\treasonMode,\n\ttarget,\n}: StartConfirmModalProps) => {\n\tconst { t } = useTranslation()\n\tconst [reason, setReason] = useState('')\n\tconst [generation, setGeneration] = useState(0)\n\tconst reasonRef = useRef(reason)\n\treasonRef.current = reason\n\tconst targetRef = useRef(target)\n\ttargetRef.current = target\n\n\tconst failConfirm = (message: string): never => {\n\t\tsetGeneration((current) => current + 1)\n\t\tthrow new Error(message)\n\t}\n\n\tconst onConfirm = async () => {\n\t\tconst next = targetRef.current\n\t\tif (!next) {\n\t\t\treturn\n\t\t}\n\t\tif (reasonMode === 'required' && !reasonRef.current.trim()) {\n\t\t\ttoast.error(t(keys.errorReasonRequired))\n\t\t\tfailConfirm('reason required')\n\t\t}\n\t\tconst result = await postImpersonation(`${apiPath}/start`, {\n\t\t\tcollection: next.collection,\n\t\t\tid: next.id,\n\t\t\treason: reasonMode === 'off' ? undefined : reasonRef.current,\n\t\t})\n\t\tif (!result.ok) {\n\t\t\ttoast.error(result.error ? t(errorKey(result.error)) : t(keys.errorFailed))\n\t\t\tfailConfirm(result.error ?? 'failed')\n\t\t}\n\t\tgoAfterSwitch(result.redirect)\n\t}\n\n\treturn (\n\t\t<ConfirmationModal\n\t\t\tbody={\n\t\t\t\t<>\n\t\t\t\t\t<p>{target?.label}</p>\n\t\t\t\t\t{reasonMode === 'off' ? null : (\n\t\t\t\t\t\t<TextInput\n\t\t\t\t\t\t\tlabel={t(keys.reasonLabel)}\n\t\t\t\t\t\t\tonChange={(event: ChangeEvent<HTMLInputElement>) => setReason(event.target.value)}\n\t\t\t\t\t\t\tpath={`${modalSlug}-reason`}\n\t\t\t\t\t\t\tplaceholder={reasonMode === 'required' ? undefined : t(keys.reasonPlaceholder)}\n\t\t\t\t\t\t\trequired={reasonMode === 'required'}\n\t\t\t\t\t\t\tvalue={reason}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t</>\n\t\t\t}\n\t\t\tclassName=\"impersonation-confirm-modal\"\n\t\t\tconfirmLabel={t(keys.confirm)}\n\t\t\theading={t(keys.confirmTitle)}\n\t\t\tkey={generation}\n\t\t\tmodalSlug={modalSlug}\n\t\t\tonConfirm={onConfirm}\n\t\t/>\n\t)\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"StartConfirmModal.js","names":["useTranslation"],"sources":["../../src/ui/StartConfirmModal.tsx"],"sourcesContent":["'use client'\n\nimport { ConfirmationModal, TextInput, toast } from '@payloadcms/ui'\nimport { type ChangeEvent, useRef, useState } from 'react'\n\nimport { keys } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\nimport { errorKey, goAfterSwitch, postImpersonation } from './api'\n\nexport type StartTarget = {\n\tcollection: string\n\tid: number | string\n\tlabel: string\n}\n\nexport type StartConfirmModalProps = {\n\tapiPath: string\n\tmodalSlug: string\n\treasonMode: 'off' | 'optional' | 'required'\n\ttarget: null | StartTarget\n}\n\n/**\n * Payload `ConfirmationModal` for starting a session. Reason uses `TextInput`\n * in the body when the host asked for optional or required notes.\n *\n * `key={generation}` remounts after a failed confirm so the native Loading\n * state resets. Throw still keeps the modal open.\n */\nexport const StartConfirmModal = ({\n\tapiPath,\n\tmodalSlug,\n\treasonMode,\n\ttarget,\n}: StartConfirmModalProps) => {\n\tconst { t } = useTranslation()\n\tconst [reason, setReason] = useState('')\n\tconst [generation, setGeneration] = useState(0)\n\tconst reasonRef = useRef(reason)\n\treasonRef.current = reason\n\tconst targetRef = useRef(target)\n\ttargetRef.current = target\n\n\tconst failConfirm = (message: string): never => {\n\t\tsetGeneration((current) => current + 1)\n\t\tthrow new Error(message)\n\t}\n\n\tconst onConfirm = async () => {\n\t\tconst next = targetRef.current\n\t\tif (!next) {\n\t\t\treturn\n\t\t}\n\t\tif (reasonMode === 'required' && !reasonRef.current.trim()) {\n\t\t\ttoast.error(t(keys.errorReasonRequired))\n\t\t\tfailConfirm('reason required')\n\t\t}\n\t\tconst result = await postImpersonation(`${apiPath}/start`, {\n\t\t\tcollection: next.collection,\n\t\t\tid: next.id,\n\t\t\treason: reasonMode === 'off' ? undefined : reasonRef.current,\n\t\t})\n\t\tif (!result.ok) {\n\t\t\ttoast.error(result.error ? t(errorKey(result.error)) : t(keys.errorFailed))\n\t\t\tfailConfirm(result.error ?? 'failed')\n\t\t}\n\t\tgoAfterSwitch(result.redirect)\n\t}\n\n\treturn (\n\t\t<ConfirmationModal\n\t\t\tbody={\n\t\t\t\t<>\n\t\t\t\t\t<p>{target?.label}</p>\n\t\t\t\t\t{reasonMode === 'off' ? null : (\n\t\t\t\t\t\t<TextInput\n\t\t\t\t\t\t\tlabel={t(keys.reasonLabel)}\n\t\t\t\t\t\t\tonChange={(event: ChangeEvent<HTMLInputElement>) => setReason(event.target.value)}\n\t\t\t\t\t\t\tpath={`${modalSlug}-reason`}\n\t\t\t\t\t\t\tplaceholder={reasonMode === 'required' ? undefined : t(keys.reasonPlaceholder)}\n\t\t\t\t\t\t\trequired={reasonMode === 'required'}\n\t\t\t\t\t\t\tvalue={reason}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t</>\n\t\t\t}\n\t\t\tclassName=\"impersonation-confirm-modal\"\n\t\t\tconfirmLabel={t(keys.confirm)}\n\t\t\theading={t(keys.confirmTitle)}\n\t\t\tkey={generation}\n\t\t\tmodalSlug={modalSlug}\n\t\t\tonConfirm={onConfirm}\n\t\t/>\n\t)\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA6BA,MAAa,qBAAqB,EACjC,SACA,WACA,YACA,aAC6B;CAC7B,MAAM,EAAE,MAAMA,iBAAe;CAC7B,MAAM,CAAC,QAAQ,aAAa,SAAS,EAAE;CACvC,MAAM,CAAC,YAAY,iBAAiB,SAAS,CAAC;CAC9C,MAAM,YAAY,OAAO,MAAM;CAC/B,UAAU,UAAU;CACpB,MAAM,YAAY,OAAO,MAAM;CAC/B,UAAU,UAAU;CAEpB,MAAM,eAAe,YAA2B;EAC/C,eAAe,YAAY,UAAU,CAAC;EACtC,MAAM,IAAI,MAAM,OAAO;CACxB;CAEA,MAAM,YAAY,YAAY;EAC7B,MAAM,OAAO,UAAU;EACvB,IAAI,CAAC,MACJ;EAED,IAAI,eAAe,cAAc,CAAC,UAAU,QAAQ,KAAK,GAAG;GAC3D,MAAM,MAAM,EAAE,KAAK,mBAAmB,CAAC;GACvC,YAAY,iBAAiB;EAC9B;EACA,MAAM,SAAS,MAAM,kBAAkB,GAAG,QAAQ,SAAS;GAC1D,YAAY,KAAK;GACjB,IAAI,KAAK;GACT,QAAQ,eAAe,QAAQ,KAAA,IAAY,UAAU;EACtD,CAAC;EACD,IAAI,CAAC,OAAO,IAAI;GACf,MAAM,MAAM,OAAO,QAAQ,EAAE,SAAS,OAAO,KAAK,CAAC,IAAI,EAAE,KAAK,WAAW,CAAC;GAC1E,YAAY,OAAO,SAAS,QAAQ;EACrC;EACA,cAAc,OAAO,QAAQ;CAC9B;CAEA,OACC,oBAAC,mBAAD;EACC,MACC,qBAAA,UAAA,EAAA,UAAA,CACC,oBAAC,KAAD,EAAA,UAAI,QAAQ,MAAS,CAAA,GACpB,eAAe,QAAQ,OACvB,oBAAC,WAAD;GACC,OAAO,EAAE,KAAK,WAAW;GACzB,WAAW,UAAyC,UAAU,MAAM,OAAO,KAAK;GAChF,MAAM,GAAG,UAAU;GACnB,aAAa,eAAe,aAAa,KAAA,IAAY,EAAE,KAAK,iBAAiB;GAC7E,UAAU,eAAe;GACzB,OAAO;EACP,CAAA,CAED,EAAA,CAAA;EAEH,WAAU;EACV,cAAc,EAAE,KAAK,OAAO;EAC5B,SAAS,EAAE,KAAK,YAAY;EAEjB;EACA;CACX,GAHK,UAGL;AAEH"}
|
package/dist/ui/api.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { CLIENT_FETCH_TIMEOUT_MS } from "../plugin/constants.js";
|
|
3
|
-
import
|
|
3
|
+
import "../translations/errorKey.js";
|
|
4
4
|
//#region src/ui/api.ts
|
|
5
5
|
const jsonHeaders = { "Content-Type": "application/json" };
|
|
6
6
|
const postImpersonation = async (path, body) => {
|
|
@@ -41,26 +41,7 @@ const goAfterSwitch = (redirect) => {
|
|
|
41
41
|
}
|
|
42
42
|
window.location.assign(next);
|
|
43
43
|
};
|
|
44
|
-
const errorKey = (code) => {
|
|
45
|
-
return {
|
|
46
|
-
alreadyImpersonating: keys.errorAlreadyImpersonating,
|
|
47
|
-
failed: keys.errorFailed,
|
|
48
|
-
forbidden: keys.errorForbidden,
|
|
49
|
-
impersonatorGone: keys.errorImpersonatorGone,
|
|
50
|
-
impersonatorSessionExpired: keys.errorImpersonatorSessionExpired,
|
|
51
|
-
invalidBody: keys.errorInvalidBody,
|
|
52
|
-
notImpersonating: keys.errorNotImpersonating,
|
|
53
|
-
origin: keys.errorOrigin,
|
|
54
|
-
reasonRequired: keys.errorReasonRequired,
|
|
55
|
-
selfTarget: keys.errorSelfTarget,
|
|
56
|
-
targetNotFound: keys.errorTargetNotFound,
|
|
57
|
-
targetTrashed: keys.errorTargetTrashed,
|
|
58
|
-
targetUnverified: keys.errorTargetUnverified,
|
|
59
|
-
unsupportedAuth: keys.errorUnsupportedAuth,
|
|
60
|
-
unsupportedCollection: keys.errorUnsupportedCollection
|
|
61
|
-
}[code] ?? keys.errorFailed;
|
|
62
|
-
};
|
|
63
44
|
//#endregion
|
|
64
|
-
export {
|
|
45
|
+
export { goAfterSwitch, postImpersonation };
|
|
65
46
|
|
|
66
47
|
//# sourceMappingURL=api.js.map
|
package/dist/ui/api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.js","names":[],"sources":["../../src/ui/api.ts"],"sourcesContent":["'use client'\n\nimport type { FailureCode } from '../endpoints/codes'\nimport { CLIENT_FETCH_TIMEOUT_MS } from '../plugin/constants'\
|
|
1
|
+
{"version":3,"file":"api.js","names":[],"sources":["../../src/ui/api.ts"],"sourcesContent":["'use client'\n\nimport type { FailureCode } from '../endpoints/codes'\nimport { CLIENT_FETCH_TIMEOUT_MS } from '../plugin/constants'\n\nexport { errorKey } from '../translations/errorKey'\nexport type { FailureCode }\n\nconst jsonHeaders = { 'Content-Type': 'application/json' }\n\nexport const postImpersonation = async (\n\tpath: string,\n\tbody: unknown\n): Promise<{ error?: FailureCode; ok: boolean; redirect?: string; status: number }> => {\n\ttry {\n\t\tconst response = await fetch(path, {\n\t\t\tbody: JSON.stringify(body ?? {}),\n\t\t\tcredentials: 'include',\n\t\t\theaders: jsonHeaders,\n\t\t\tmethod: 'POST',\n\t\t\tsignal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS),\n\t\t})\n\t\tlet parsed: { error?: FailureCode; redirect?: string } = {}\n\t\ttry {\n\t\t\tparsed = (await response.json()) as { error?: FailureCode; redirect?: string }\n\t\t} catch {\n\t\t\tparsed = {}\n\t\t}\n\t\treturn {\n\t\t\terror: parsed.error,\n\t\t\tok: response.ok,\n\t\t\tredirect: parsed.redirect,\n\t\t\tstatus: response.status,\n\t\t}\n\t} catch {\n\t\treturn { error: 'failed', ok: false, status: 0 }\n\t}\n}\n\n/** Reload when the redirect is the current path; `assign` would be a no-op. */\nexport const goAfterSwitch = (redirect?: string) => {\n\tconst next = redirect ?? '/'\n\tconst path = new URL(next, window.location.origin).pathname\n\tif (path === window.location.pathname) {\n\t\twindow.location.reload()\n\t\treturn\n\t}\n\twindow.location.assign(next)\n}\n"],"mappings":";;;;AAQA,MAAM,cAAc,EAAE,gBAAgB,mBAAmB;AAEzD,MAAa,oBAAoB,OAChC,MACA,SACsF;CACtF,IAAI;EACH,MAAM,WAAW,MAAM,MAAM,MAAM;GAClC,MAAM,KAAK,UAAU,QAAQ,CAAC,CAAC;GAC/B,aAAa;GACb,SAAS;GACT,QAAQ;GACR,QAAQ,YAAY,QAAQ,uBAAuB;EACpD,CAAC;EACD,IAAI,SAAqD,CAAC;EAC1D,IAAI;GACH,SAAU,MAAM,SAAS,KAAK;EAC/B,QAAQ;GACP,SAAS,CAAC;EACX;EACA,OAAO;GACN,OAAO,OAAO;GACd,IAAI,SAAS;GACb,UAAU,OAAO;GACjB,QAAQ,SAAS;EAClB;CACD,QAAQ;EACP,OAAO;GAAE,OAAO;GAAU,IAAI;GAAO,QAAQ;EAAE;CAChD;AACD;;AAGA,MAAa,iBAAiB,aAAsB;CACnD,MAAM,OAAO,YAAY;CAEzB,IADa,IAAI,IAAI,MAAM,OAAO,SAAS,MAAM,EAAE,aACtC,OAAO,SAAS,UAAU;EACtC,OAAO,SAAS,OAAO;EACvB;CACD;CACA,OAAO,SAAS,OAAO,IAAI;AAC5B"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { fillTemplate } from "../translations/lookup.js";
|
|
2
|
+
//#region src/ui/sessionEnds.ts
|
|
3
|
+
const formatCountdown = (expiresAt, now) => {
|
|
4
|
+
const remaining = new Date(expiresAt).getTime() - now;
|
|
5
|
+
if (!Number.isFinite(remaining) || remaining <= 0) return "0s";
|
|
6
|
+
const totalSeconds = Math.floor(remaining / 1e3);
|
|
7
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
8
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
9
|
+
const seconds = totalSeconds % 60;
|
|
10
|
+
if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m ${String(seconds).padStart(2, "0")}s`;
|
|
11
|
+
if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
12
|
+
return `${seconds}s`;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The bar's "session ends" line, or null while `now` is null. The server and the browser read
|
|
16
|
+
* different clocks and time zones, so the bar renders no time until it has mounted, or it would not
|
|
17
|
+
* hydrate.
|
|
18
|
+
*/
|
|
19
|
+
const sessionEndsLabel = ({ endsAt, locale, now, templates }) => {
|
|
20
|
+
if (!endsAt || now === null) return null;
|
|
21
|
+
const countdown = formatCountdown(endsAt, now);
|
|
22
|
+
if (countdown === "0s") return null;
|
|
23
|
+
return countdown ? fillTemplate(templates.endsIn, "{{countdown}}", countdown) : fillTemplate(templates.endsAt, "{{time}}", new Date(endsAt).toLocaleTimeString(locale ?? void 0));
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { sessionEndsLabel };
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=sessionEnds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessionEnds.js","names":[],"sources":["../../src/ui/sessionEnds.ts"],"sourcesContent":["import { fillTemplate } from '../translations/lookup'\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\n/**\n * The bar's \"session ends\" line, or null while `now` is null. The server and the browser read\n * different clocks and time zones, so the bar renders no time until it has mounted, or it would not\n * hydrate.\n */\nexport const sessionEndsLabel = ({\n\tendsAt,\n\tlocale,\n\tnow,\n\ttemplates,\n}: {\n\tendsAt?: null | string\n\tlocale?: null | string\n\tnow: null | number\n\ttemplates: { endsAt: string; endsIn: string }\n}): null | string => {\n\tif (!endsAt || now === null) {\n\t\treturn null\n\t}\n\tconst countdown = formatCountdown(endsAt, now)\n\tif (countdown === '0s') {\n\t\treturn null\n\t}\n\treturn countdown\n\t\t? fillTemplate(templates.endsIn, '{{countdown}}', countdown)\n\t\t: fillTemplate(\n\t\t\t\ttemplates.endsAt,\n\t\t\t\t'{{time}}',\n\t\t\t\tnew Date(endsAt).toLocaleTimeString(locale ?? undefined)\n\t\t\t)\n}\n"],"mappings":";;AAEA,MAAM,mBAAmB,WAAmB,QAA+B;CAC1E,MAAM,YAAY,IAAI,KAAK,SAAS,EAAE,QAAQ,IAAI;CAClD,IAAI,CAAC,OAAO,SAAS,SAAS,KAAK,aAAa,GAC/C,OAAO;CAER,MAAM,eAAe,KAAK,MAAM,YAAY,GAAI;CAChD,MAAM,QAAQ,KAAK,MAAM,eAAe,IAAI;CAC5C,MAAM,UAAU,KAAK,MAAO,eAAe,OAAQ,EAAE;CACrD,MAAM,UAAU,eAAe;CAC/B,IAAI,QAAQ,GACX,OAAO,GAAG,MAAM,IAAI,OAAO,OAAO,EAAE,SAAS,GAAG,GAAG,EAAE,IAAI,OAAO,OAAO,EAAE,SAAS,GAAG,GAAG,EAAE;CAE3F,IAAI,UAAU,GACb,OAAO,GAAG,QAAQ,IAAI,OAAO,OAAO,EAAE,SAAS,GAAG,GAAG,EAAE;CAExD,OAAO,GAAG,QAAQ;AACnB;;;;;;AAOA,MAAa,oBAAoB,EAChC,QACA,QACA,KACA,gBAMoB;CACpB,IAAI,CAAC,UAAU,QAAQ,MACtB,OAAO;CAER,MAAM,YAAY,gBAAgB,QAAQ,GAAG;CAC7C,IAAI,cAAc,MACjB,OAAO;CAER,OAAO,YACJ,aAAa,UAAU,QAAQ,iBAAiB,SAAS,IACzD,aACA,UAAU,QACV,YACA,IAAI,KAAK,MAAM,EAAE,mBAAmB,UAAU,KAAA,CAAS,CACxD;AACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useImpersonation.js","names":[],"sources":["../../src/ui/useImpersonation.ts"],"sourcesContent":["'use client'\n\nimport { useConfig } from '@payloadcms/ui'\nimport type { Where } from 'payload'\n\nimport type { FailureCode } from '../endpoints/codes'\nimport type { ImpersonationStatus } from '../getImpersonation'\nimport { errorKey, goAfterSwitch, postImpersonation } from './api'\nimport { useImpersonationClient } from './ImpersonationConfig'\n\nexport type UseImpersonation = {\n\tapiPath: string\n\tcardEmail: boolean\n\tend: (id: number | string) => Promise<{ error?: FailureCode; ok: boolean; status: number }>\n\texit: () => Promise<{ error?: FailureCode; ok: boolean; status: number }>\n\treasonMode: 'off' | 'optional' | 'required'\n\tsessionCollection: string\n\tsetStatus: (status: ImpersonationStatus) => void\n\tstart: (args: {\n\t\tcollection: string\n\t\tid: number | string\n\t\treason?: string\n\t\tredirect?: string\n\t}) => Promise<{ error?: FailureCode; ok: boolean; redirect?: string; status: number }>\n\tstatus: ImpersonationStatus\n\ttargetFilters: Record<string, true | Where>\n}\n\nconst noopSetStatus = (_status: ImpersonationStatus): void => {}\n\n/**\n * Headless client API. The provider is always registered, so this works from\n * any admin component. Built-in switcher, bar, and document button use it.\n */\nexport const useImpersonation = (): UseImpersonation => {\n\tconst plugin = useImpersonationClient()\n\tconst { config } = useConfig()\n\tconst apiPath = plugin?.apiPath ?? `${config.routes.api}/impersonation`\n\tconst reasonMode = plugin?.reasonMode ?? 'off'\n\tconst sessionCollection = plugin?.sessionCollection ?? 'impersonation-sessions'\n\tconst targetFilters = plugin?.targetFilters ?? {}\n\tconst cardEmail = plugin?.cardEmail ?? true\n\tconst status = plugin?.status ?? { active: false }\n\tconst setStatus = plugin?.setStatus ?? noopSetStatus\n\n\treturn {\n\t\tapiPath,\n\t\tcardEmail,\n\t\tend: (id) => postImpersonation(`${apiPath}/${encodeURIComponent(String(id))}/end`, {}),\n\t\texit: () => postImpersonation(`${apiPath}/exit`, {}),\n\t\treasonMode,\n\t\tsessionCollection,\n\t\tsetStatus,\n\t\tstart: (args) => postImpersonation(`${apiPath}/start`, args),\n\t\tstatus,\n\t\ttargetFilters,\n\t}\n}\n\nexport { errorKey, goAfterSwitch, postImpersonation }\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"useImpersonation.js","names":[],"sources":["../../src/ui/useImpersonation.ts"],"sourcesContent":["'use client'\n\nimport { useConfig } from '@payloadcms/ui'\nimport type { Where } from 'payload'\n\nimport type { FailureCode } from '../endpoints/codes'\nimport type { ImpersonationStatus } from '../getImpersonation'\nimport { errorKey, goAfterSwitch, postImpersonation } from './api'\nimport { useImpersonationClient } from './ImpersonationConfig'\n\nexport type UseImpersonation = {\n\tapiPath: string\n\tcardEmail: boolean\n\tend: (id: number | string) => Promise<{ error?: FailureCode; ok: boolean; status: number }>\n\texit: () => Promise<{ error?: FailureCode; ok: boolean; status: number }>\n\treasonMode: 'off' | 'optional' | 'required'\n\tsessionCollection: string\n\tsetStatus: (status: ImpersonationStatus) => void\n\tstart: (args: {\n\t\tcollection: string\n\t\tid: number | string\n\t\treason?: string\n\t\tredirect?: string\n\t}) => Promise<{ error?: FailureCode; ok: boolean; redirect?: string; status: number }>\n\tstatus: ImpersonationStatus\n\ttargetFilters: Record<string, true | Where>\n}\n\nconst noopSetStatus = (_status: ImpersonationStatus): void => {}\n\n/**\n * Headless client API. The provider is always registered, so this works from\n * any admin component. Built-in switcher, bar, and document button use it.\n */\nexport const useImpersonation = (): UseImpersonation => {\n\tconst plugin = useImpersonationClient()\n\tconst { config } = useConfig()\n\tconst apiPath = plugin?.apiPath ?? `${config.routes.api}/impersonation`\n\tconst reasonMode = plugin?.reasonMode ?? 'off'\n\tconst sessionCollection = plugin?.sessionCollection ?? 'impersonation-sessions'\n\tconst targetFilters = plugin?.targetFilters ?? {}\n\tconst cardEmail = plugin?.cardEmail ?? true\n\tconst status = plugin?.status ?? { active: false }\n\tconst setStatus = plugin?.setStatus ?? noopSetStatus\n\n\treturn {\n\t\tapiPath,\n\t\tcardEmail,\n\t\tend: (id) => postImpersonation(`${apiPath}/${encodeURIComponent(String(id))}/end`, {}),\n\t\texit: () => postImpersonation(`${apiPath}/exit`, {}),\n\t\treasonMode,\n\t\tsessionCollection,\n\t\tsetStatus,\n\t\tstart: (args) => postImpersonation(`${apiPath}/start`, args),\n\t\tstatus,\n\t\ttargetFilters,\n\t}\n}\n\nexport { errorKey, goAfterSwitch, postImpersonation }\n"],"mappings":";;;;;;AA4BA,MAAM,iBAAiB,YAAuC,CAAC;;;;;AAM/D,MAAa,yBAA2C;CACvD,MAAM,SAAS,uBAAuB;CACtC,MAAM,EAAE,WAAW,UAAU;CAC7B,MAAM,UAAU,QAAQ,WAAW,GAAG,OAAO,OAAO,IAAI;CACxD,MAAM,aAAa,QAAQ,cAAc;CACzC,MAAM,oBAAoB,QAAQ,qBAAqB;CACvD,MAAM,gBAAgB,QAAQ,iBAAiB,CAAC;CAChD,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,SAAS,QAAQ,UAAU,EAAE,QAAQ,MAAM;CAGjD,OAAO;EACN;EACA;EACA,MAAM,OAAO,kBAAkB,GAAG,QAAQ,GAAG,mBAAmB,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;EACrF,YAAY,kBAAkB,GAAG,QAAQ,QAAQ,CAAC,CAAC;EACnD;EACA;EACA,WATiB,QAAQ,aAAa;EAUtC,QAAQ,SAAS,kBAAkB,GAAG,QAAQ,SAAS,IAAI;EAC3D;EACA;CACD;AACD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@10x-media/impersonation",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
4
|
"description": "Let an authorised account sign in as another user without their password, then return. Every session is recorded.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -89,11 +89,11 @@
|
|
|
89
89
|
"tsdown": "0.22.1",
|
|
90
90
|
"typescript": "5.9.3",
|
|
91
91
|
"vitest": "4.1.7",
|
|
92
|
+
"@10x-media/tsconfig": "0.0.0",
|
|
92
93
|
"@10x-media/tsdown-config": "0.0.0",
|
|
93
94
|
"@10x-media/dual-session": "0.1.0-beta.2",
|
|
94
|
-
"@10x-media/
|
|
95
|
-
"@10x-media/payload-test-harness": "0.0.0"
|
|
96
|
-
"@10x-media/vitest-config": "0.0.0"
|
|
95
|
+
"@10x-media/vitest-config": "0.0.0",
|
|
96
|
+
"@10x-media/payload-test-harness": "0.0.0"
|
|
97
97
|
},
|
|
98
98
|
"publishConfig": {
|
|
99
99
|
"access": "public"
|