@activecollab/components
Version:
ActiveCollab Components
432 lines (415 loc) • 22.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.CreateLoginCodeDialogs = exports.BackupCodesDialogs = void 0;
var _react = _interopRequireWildcard(require("react"));
var _styledComponents = _interopRequireDefault(require("styled-components"));
var _Button = require("../../components/Button");
var _Dialog = require("../../components/Dialog");
var _IconButton = require("../../components/IconButton");
var _Icons = require("../../components/Icons");
var _InfoBox = require("../../components/InfoBox");
var _Input = require("../../components/Input");
var _Label = require("../../components/Label");
var _Loaders = require("../../components/Loaders");
var _ToastMessage = require("../../components/ToastMessage");
var _Tooltip = require("../../components/Tooltip");
var _Transitions = require("../../components/Transitions");
var _Typography = require("../../components/Typography");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
/**
* Self-hosted 2FA dialogs shared by the People stories (Users list + User
* profile). Two flows, both reached from the user three-dot menu and both
* gated (in the menu) behind the self-hosted + 2FA-enabled guard:
*
* - **Backup Codes** (own account, `self` menu): a single dialog that both
* explains the destructive regenerate and takes the password (identity
* confirmation is "valid password + submit", no separate confirm step).
* Submitting opens the reveal dialog, which shows a skeleton while the new
* set "loads", then the 10 codes once.
* - **Create Login Code** (another user, `member`/`client` menu): an owner
* re-enters their own password, then a one-time code is revealed once with
* its expiry and a "user notified" note. (Task #18.)
*
* Everything is mocked locally: passwords aren't checked, the "load" is a
* timer, and the codes are static samples.
*/
var SANS = '-apple-system, BlinkMacSystemFont, "Roboto", "Helvetica Neue", Arial, sans-serif';
var MONO = '"SF Mono", "Roboto Mono", Menlo, Consolas, "Courier New", monospace';
/**
* DS `InputPassword` calls `onChange` with the string value at runtime, but its
* type is still inherited from the native input (`ChangeEventHandler`). Adapt a
* string handler so TypeScript is satisfied without lying about the value.
*/
var asInputChange = function asInputChange(fn) {
return fn;
};
/* Sample codes (10+10 A-Z0-9), mirroring the real generator's format. */
var SAMPLE_BACKUP_CODES = ["4K7Q9ZM2XN-8V3TJRD6WP", "P2W8XK4M9Q-3RJ7VND5TZ", "9ZM2XN4K7Q-6WP8V3TJRD", "TJRD6WP8V3-2XN4K7Q9ZM", "V3TJRD6WP8-K7Q9ZM2XN4", "8V3TJRD6WP-Q9ZM2XN4K7", "RD5TZP2W8X-M9Q3RJ7VND", "3RJ7VND5TZ-8XK4M9QP2W", "N4K7Q9ZM2X-6WP8V3TJRD", "Q9ZM2XN4K7-8V3TJRD6WP"];
var SAMPLE_LOGIN_CODE = "4K7Q9ZM2XN-8V3TJRD6WP";
/* ------------------------------------------------------------------ */
/* Toast (reuses the fixed upper-right anchor pattern from headers) */
/* ------------------------------------------------------------------ */
var StyledToastAnchor = _styledComponents.default.div.withConfig({
displayName: "TwoFactorDialogs__StyledToastAnchor",
componentId: "sc-nn5qbh-0"
})(["position:fixed;top:16px;right:16px;z-index:10000;pointer-events:none;.c-toast-message{pointer-events:auto;}"]);
var CopyToast = function CopyToast(_ref) {
var open = _ref.open,
toastKey = _ref.toastKey,
text = _ref.text,
onClose = _ref.onClose;
return /*#__PURE__*/_react.default.createElement(StyledToastAnchor, null, /*#__PURE__*/_react.default.createElement(_Transitions.SlideFromTop, {
in: open
}, /*#__PURE__*/_react.default.createElement(_ToastMessage.ToastMessage, {
key: toastKey,
className: "c-toast-message",
type: "success",
text: text,
dismissible: true,
dropShadow: true,
timeout: 3000,
onClose: onClose
})));
};
/* ------------------------------------------------------------------ */
/* Shared password-gate body (describe + password in one dialog) */
/* ------------------------------------------------------------------ */
var StyledGateBody = _styledComponents.default.div.withConfig({
displayName: "TwoFactorDialogs__StyledGateBody",
componentId: "sc-nn5qbh-1"
})(["font-family:", ";display:flex;flex-direction:column;gap:16px;.pg-field{display:flex;flex-direction:column;gap:4px;}.pg-input.c-input-wrapper,.pg-input{width:100%;max-width:none;}"], SANS);
/* ------------------------------------------------------------------ */
/* Backup Codes — combined describe + password dialog */
/* ------------------------------------------------------------------ */
/**
* The full Backup Codes flow: the combined regenerate/password dialog, then the
* one-time reveal dialog with its loading skeleton. Cancelling the first dialog
* is a pure no-op — no codes are issued, no toast.
*/
var BackupCodesDialogs = exports.BackupCodesDialogs = function BackupCodesDialogs(_ref2) {
var open = _ref2.open,
onClose = _ref2.onClose;
var _useState = (0, _react.useState)(""),
_useState2 = _slicedToArray(_useState, 2),
password = _useState2[0],
setPassword = _useState2[1];
var _useState3 = (0, _react.useState)(false),
_useState4 = _slicedToArray(_useState3, 2),
attempted = _useState4[0],
setAttempted = _useState4[1];
var _useState5 = (0, _react.useState)(false),
_useState6 = _slicedToArray(_useState5, 2),
revealOpen = _useState6[0],
setRevealOpen = _useState6[1];
var _useState7 = (0, _react.useState)(true),
_useState8 = _slicedToArray(_useState7, 2),
loading = _useState8[0],
setLoading = _useState8[1];
var _useState9 = (0, _react.useState)(false),
_useState0 = _slicedToArray(_useState9, 2),
copyOpen = _useState0[0],
setCopyOpen = _useState0[1];
var _useState1 = (0, _react.useState)(0),
_useState10 = _slicedToArray(_useState1, 2),
copyKey = _useState10[0],
setCopyKey = _useState10[1];
// Reset the gate each time it (re)opens.
(0, _react.useEffect)(function () {
if (open) {
setPassword("");
setAttempted(false);
}
}, [open]);
// Simulate the API generating the new set: a ~1s skeleton on reveal.
(0, _react.useEffect)(function () {
if (!revealOpen) {
return undefined;
}
setLoading(true);
var timer = window.setTimeout(function () {
return setLoading(false);
}, 1100);
return function () {
return window.clearTimeout(timer);
};
}, [revealOpen]);
var passwordInvalid = attempted && password.trim() === "";
var handleConfirm = function handleConfirm() {
if (password.trim() === "") {
setAttempted(true);
return;
}
// Valid password + submit is all the confirmation we need: close the gate
// and open the reveal (which regenerates the set).
onClose();
setRevealOpen(true);
};
var handleCopy = function handleCopy() {
setCopyOpen(true);
setCopyKey(function (k) {
return k + 1;
});
};
var closeReveal = function closeReveal() {
setRevealOpen(false);
};
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_Dialog.Dialog, {
open: open,
onClose: onClose,
disableCloseOnEsc: true
}, /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.Title, null, "Regenerate Backup Codes"), /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.ContentDivider, null), /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.Content, null, /*#__PURE__*/_react.default.createElement(StyledGateBody, null, /*#__PURE__*/_react.default.createElement(_Typography.Body2, {
color: "secondary",
lineHeight: "loose"
}, "This drops all of your existing backup codes and creates a new set of 10. Any codes you saved before will stop working. The new codes are shown only once. Enter your password to confirm it's you."), /*#__PURE__*/_react.default.createElement("div", {
className: "pg-field"
}, /*#__PURE__*/_react.default.createElement(_Label.Label, {
htmlFor: "backup-codes-password"
}, "Your Password"), /*#__PURE__*/_react.default.createElement(_Input.InputPassword, {
id: "backup-codes-password",
wrapperClassName: "pg-input",
value: password,
onChange: asInputChange(function (value) {
setPassword(value);
setAttempted(false);
}),
invalid: passwordInvalid,
errorMessage: passwordInvalid ? "Your password is required." : undefined,
autoFocus: true,
autoComplete: "current-password"
})))), /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.ContentDivider, null), /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.Actions, null, /*#__PURE__*/_react.default.createElement(_Button.Button, {
variant: "primary",
style: {
marginRight: 12
},
onClick: handleConfirm
}, "Regenerate Codes"), /*#__PURE__*/_react.default.createElement(_Button.Button, {
variant: "secondary",
onClick: onClose
}, "Cancel"))), /*#__PURE__*/_react.default.createElement(BackupCodesRevealDialog, {
open: revealOpen,
loading: loading,
onClose: closeReveal,
onCopy: handleCopy
}), /*#__PURE__*/_react.default.createElement(CopyToast, {
open: copyOpen,
toastKey: copyKey,
text: "Backup codes copied to clipboard.",
onClose: function onClose() {
return setCopyOpen(false);
}
}));
};
/* ------------------------------------------------------------------ */
/* Backup Codes — one-time reveal (skeleton while loading) */
/* ------------------------------------------------------------------ */
var StyledRevealBody = _styledComponents.default.div.withConfig({
displayName: "TwoFactorDialogs__StyledRevealBody",
componentId: "sc-nn5qbh-2"
})(["font-family:", ";display:flex;flex-direction:column;gap:16px;.bc-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px 16px;}.bc-code{font-family:", ";font-size:14px;letter-spacing:0.5px;color:var(--color-theme-900);padding:6px 10px;border-radius:6px;background-color:var(--color-theme-200);text-align:center;}.bc-skeleton{height:32px;border-radius:6px;width:100%;}"], SANS, MONO);
var BackupCodesRevealDialog = function BackupCodesRevealDialog(_ref3) {
var open = _ref3.open,
loading = _ref3.loading,
onClose = _ref3.onClose,
onCopy = _ref3.onCopy;
return /*#__PURE__*/_react.default.createElement(_Dialog.Dialog, {
open: open,
onClose: onClose,
disableCloseOnEsc: true
}, /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.Title, null, "Your Backup Codes"), /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.ContentDivider, null), /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.Content, null, /*#__PURE__*/_react.default.createElement(StyledRevealBody, null, /*#__PURE__*/_react.default.createElement(_Typography.Body2, {
color: "secondary",
lineHeight: "loose"
}, "Use a backup code to log in when email isn't available. Each code works once."), /*#__PURE__*/_react.default.createElement("div", {
className: "bc-grid"
}, loading ? [...Array(10)].map(function (_, index) {
return /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
key: index,
className: "bc-skeleton"
});
}) : SAMPLE_BACKUP_CODES.map(function (code) {
return /*#__PURE__*/_react.default.createElement("div", {
key: code,
className: "bc-code"
}, code);
})), /*#__PURE__*/_react.default.createElement(_InfoBox.InfoBox, {
type: "note",
showIcon: true,
title: "Codes are shown only once"
}, /*#__PURE__*/_react.default.createElement(_Typography.Body2, {
color: "secondary"
}, "Save them somewhere safe, they can't be displayed again. You can generate a new set from your profile at any time.")))), /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.ContentDivider, null), /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.Actions, null, /*#__PURE__*/_react.default.createElement(_Button.Button, {
variant: "primary",
style: {
marginRight: 12
},
onClick: onClose,
disabled: loading
}, "I've Saved My Codes"), /*#__PURE__*/_react.default.createElement(_Button.Button, {
variant: "secondary",
onClick: onCopy,
disabled: loading
}, "Copy Codes")));
};
/* ------------------------------------------------------------------ */
/* Create Login Code — password gate + one-time reveal */
/* ------------------------------------------------------------------ */
var StyledCodeRow = _styledComponents.default.div.withConfig({
displayName: "TwoFactorDialogs__StyledCodeRow",
componentId: "sc-nn5qbh-3"
})(["font-family:", ";display:flex;flex-direction:column;gap:16px;.lc-label{display:block;margin-bottom:6px;}.lc-code-field{display:flex;align-items:center;gap:8px;padding:8px 12px;border-radius:8px;background-color:var(--color-theme-200);}.lc-code{font-family:", ";font-size:15px;letter-spacing:0.5px;color:var(--color-theme-900);flex:1 1 auto;}.lc-copy svg{fill:var(--color-theme-600);}.lc-caption{display:block;margin-top:6px;}"], SANS, MONO);
/* Title row for the reveal dialog: heading on the left, X close on the right
(the app's Use Token dialog pattern). Applied via className since
Dialog.Title doesn't accept inline style. */
var StyledRevealTitle = _styledComponents.default.div.withConfig({
displayName: "TwoFactorDialogs__StyledRevealTitle",
componentId: "sc-nn5qbh-4"
})(["display:flex;justify-content:space-between;align-items:center;width:100%;.lc-close svg{fill:var(--color-theme-600);}"]);
/**
* Owner-minted login code flow (task #18): password gate first, then the
* one-time reveal. The reveal has no footer — an X in the title closes it —
* mirroring the app's Use Token dialog.
*/
var CreateLoginCodeDialogs = exports.CreateLoginCodeDialogs = function CreateLoginCodeDialogs(_ref4) {
var open = _ref4.open,
onClose = _ref4.onClose,
userName = _ref4.userName;
var firstName = (0, _react.useMemo)(function () {
return userName.split(" ")[0] || userName;
}, [userName]);
var _useState11 = (0, _react.useState)(""),
_useState12 = _slicedToArray(_useState11, 2),
password = _useState12[0],
setPassword = _useState12[1];
var _useState13 = (0, _react.useState)(false),
_useState14 = _slicedToArray(_useState13, 2),
attempted = _useState14[0],
setAttempted = _useState14[1];
var _useState15 = (0, _react.useState)(false),
_useState16 = _slicedToArray(_useState15, 2),
revealOpen = _useState16[0],
setRevealOpen = _useState16[1];
// Expiry copy: computed as "now + 10 min" at the moment the code is minted.
var _useState17 = (0, _react.useState)(""),
_useState18 = _slicedToArray(_useState17, 2),
expiry = _useState18[0],
setExpiry = _useState18[1];
var _useState19 = (0, _react.useState)(false),
_useState20 = _slicedToArray(_useState19, 2),
copyOpen = _useState20[0],
setCopyOpen = _useState20[1];
var _useState21 = (0, _react.useState)(0),
_useState22 = _slicedToArray(_useState21, 2),
copyKey = _useState22[0],
setCopyKey = _useState22[1];
(0, _react.useEffect)(function () {
if (open) {
setPassword("");
setAttempted(false);
}
}, [open]);
var passwordInvalid = attempted && password.trim() === "";
var handleConfirm = function handleConfirm() {
if (password.trim() === "") {
setAttempted(true);
return;
}
var when = new Date(Date.now() + 10 * 60000);
var hh = String(when.getHours()).padStart(2, "0");
var mm = String(when.getMinutes()).padStart(2, "0");
setExpiry("".concat(hh, ":").concat(mm));
onClose();
setRevealOpen(true);
};
var handleCopy = function handleCopy() {
setCopyOpen(true);
setCopyKey(function (k) {
return k + 1;
});
};
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_Dialog.Dialog, {
open: open,
onClose: onClose,
disableCloseOnEsc: true
}, /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.Title, null, "Confirm Password"), /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.ContentDivider, null), /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.Content, null, /*#__PURE__*/_react.default.createElement(StyledGateBody, null, /*#__PURE__*/_react.default.createElement(_Typography.Body2, {
color: "secondary",
lineHeight: "loose"
}, "You're about to create a one-time login code for ", userName, ". Any code they already had pending will be replaced."), /*#__PURE__*/_react.default.createElement("div", {
className: "pg-field"
}, /*#__PURE__*/_react.default.createElement(_Label.Label, {
htmlFor: "login-code-password"
}, "Your Password"), /*#__PURE__*/_react.default.createElement(_Input.InputPassword, {
id: "login-code-password",
wrapperClassName: "pg-input",
value: password,
onChange: asInputChange(function (value) {
setPassword(value);
setAttempted(false);
}),
invalid: passwordInvalid,
errorMessage: passwordInvalid ? "Your password is required." : undefined,
autoFocus: true,
autoComplete: "current-password"
})))), /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.ContentDivider, null), /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.Actions, null, /*#__PURE__*/_react.default.createElement(_Button.Button, {
variant: "primary",
style: {
marginRight: 12
},
onClick: handleConfirm
}, "Confirm"), /*#__PURE__*/_react.default.createElement(_Button.Button, {
variant: "secondary",
onClick: onClose
}, "Cancel"))), /*#__PURE__*/_react.default.createElement(_Dialog.Dialog, {
open: revealOpen,
onClose: function onClose() {
return setRevealOpen(false);
}
}, /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.Title, {
disableDefaultHeading: true
}, /*#__PURE__*/_react.default.createElement(StyledRevealTitle, null, /*#__PURE__*/_react.default.createElement(_Typography.Header3, null, "One-Time Login Code"), /*#__PURE__*/_react.default.createElement(_IconButton.IconButton, {
variant: "text gray",
className: "lc-close",
onClick: function onClick() {
return setRevealOpen(false);
}
}, /*#__PURE__*/_react.default.createElement(_Icons.CancelCrossIcon, null)))), /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.ContentDivider, null), /*#__PURE__*/_react.default.createElement(_Dialog.Dialog.Content, null, /*#__PURE__*/_react.default.createElement(StyledCodeRow, null, /*#__PURE__*/_react.default.createElement(_Typography.Body2, {
color: "secondary",
lineHeight: "loose"
}, "Share this code with ", firstName, " through a secure channel. They'll still need their password to log in, the code alone won't work."), /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement(_Label.Label, {
className: "lc-label"
}, "Login Code"), /*#__PURE__*/_react.default.createElement("div", {
className: "lc-code-field"
}, /*#__PURE__*/_react.default.createElement("span", {
className: "lc-code"
}, SAMPLE_LOGIN_CODE), /*#__PURE__*/_react.default.createElement(_Tooltip.Tooltip, {
title: "Copy code"
}, /*#__PURE__*/_react.default.createElement(_IconButton.IconButton, {
variant: "text gray",
className: "lc-copy",
onClick: handleCopy
}, /*#__PURE__*/_react.default.createElement(_Icons.CopyIcon, null)))), /*#__PURE__*/_react.default.createElement(_Typography.Caption1, {
color: "tertiary",
className: "lc-caption"
}, "Expires at ", expiry, " (in 10 minutes), it can't be shown again.")), /*#__PURE__*/_react.default.createElement(_InfoBox.InfoBox, {
type: "default",
showIcon: true,
title: "".concat(firstName, " has been notified")
}, /*#__PURE__*/_react.default.createElement(_Typography.Body2, {
color: "secondary"
}, "They received an email saying you created this code for their account. The email doesn't include the code."))))), /*#__PURE__*/_react.default.createElement(CopyToast, {
open: copyOpen,
toastKey: copyKey,
text: "Login code copied to clipboard.",
onClose: function onClose() {
return setCopyOpen(false);
}
}));
};
//# sourceMappingURL=TwoFactorDialogs.js.map