UNPKG

@activecollab/components

Version:

ActiveCollab Components

432 lines (415 loc) 22.5 kB
"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