refactor: simplified

Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
feat-otp-email-verify
James Elliott 2023-04-22 14:19:57 +10:00
parent 56aeb1bd86
commit 18b1fc5ed2
No known key found for this signature in database
GPG Key ID: 0F1C4A096E857E49
32 changed files with 441 additions and 322 deletions

View File

@ -13,20 +13,20 @@
"Clone Warning": "Clone Warning",
"Created": "Created",
"Delete": "Delete",
"Description": "Description",
"Details": "Details",
"Display extended information for this WebAuthn credential": "Display extended information for this WebAuthn credential",
"Edit": "Edit",
"Edit information for this WebAuthn credential": "Edit information for this WebAuthn credential",
"Edit WebAuthn Credential": "Edit WebAuthn Credential",
"Enabled": "Enabled",
"Enter a new name for this WebAuthn credential": "Enter a new name for this WebAuthn credential:",
"Enter a new description for this WebAuthn credential": "Enter a new description for this WebAuthn credential:",
"Enter a description for this credential": "Enter a description for this credential",
"Extended WebAuthn credential information for security key": "Extended WebAuthn credential information for security key {{description}}",
"Identifier": "Identifier",
"Last Used": "Last Used",
"Last Used when": "Last Used {{when, datetime}}",
"Manage your security keys": "Manage your security keys",
"Name": "Name",
"No": "No",
"No WebAuthn Credentials have been registered. If you'd like to register one click add.": "No WebAuthn Credentials have been registered. If you'd like to register one click add.",
"Overview": "Overview",

View File

@ -36,6 +36,7 @@
"qrcode.react": "3.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-draggable": "^4.4.5",
"react-i18next": "12.2.0",
"react-loading": "2.0.3",
"react-router-dom": "6.10.0",

View File

@ -64,6 +64,9 @@ dependencies:
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
react-draggable:
specifier: ^4.4.5
version: 4.4.5(react-dom@18.2.0)(react@18.2.0)
react-i18next:
specifier: 12.2.0
version: 12.2.0(i18next@22.4.15)(react-dom@18.2.0)(react@18.2.0)
@ -4370,6 +4373,7 @@ packages:
/eslint-config-prettier@8.8.0(eslint@8.38.0):
resolution: {integrity: sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==}
hasBin: true
peerDependencies:
eslint: '>=7.0.0'
dependencies:
@ -6485,6 +6489,18 @@ packages:
react: 18.2.0
scheduler: 0.23.0
/react-draggable@4.4.5(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==}
peerDependencies:
react: '>= 16.3.0 || 18'
react-dom: '>= 16.3.0'
dependencies:
clsx: 1.2.1
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-i18next@12.2.0(i18next@22.4.15)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-5XeVgSygaGfyFmDd2WcXvINRw2WEC1XviW1LXY/xLOEMzsCFRwKqfnHN+hUjla8ZipbVJR27GCMSuTr0BhBBBQ==}
peerDependencies:
@ -7165,6 +7181,7 @@ packages:
/ts-node@10.9.1(@types/node@18.15.13)(typescript@5.0.4):
resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
hasBin: true
peerDependencies:
'@swc/core': '>=1.2.50'
'@swc/wasm': '>=1.2.50'
@ -7196,6 +7213,7 @@ packages:
/tsconfck@2.1.1(typescript@5.0.4):
resolution: {integrity: sha512-ZPCkJBKASZBmBUNqGHmRhdhM8pJYDdOXp4nRgj/O0JwUwsMq50lCDRQP/M5GBNAA0elPrq4gAeu4dkaVCuKWww==}
engines: {node: ^14.13.1 || ^16 || >=18}
hasBin: true
peerDependencies:
typescript: ^4.3.5 || ^5.0.0
peerDependenciesMeta:
@ -7337,6 +7355,7 @@ packages:
/update-browserslist-db@1.0.10(browserslist@4.21.5):
resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
dependencies:
@ -7456,6 +7475,7 @@ packages:
/vite@3.2.5(@types/node@18.15.13):
resolution: {integrity: sha512-4mVEpXpSOgrssFZAOmGIr85wPHKvaDAcXqxVxVRZhljkJOMZi1ibLibzjLHzJvcok8BMguLc7g1W6W/GqZbLdQ==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
peerDependencies:
'@types/node': '>= 14'
less: '*'
@ -7489,6 +7509,7 @@ packages:
/vite@4.3.1(@types/node@18.15.13):
resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
peerDependencies:
'@types/node': '>= 14'
less: '*'
@ -7538,6 +7559,7 @@ packages:
/vitest@0.30.1(happy-dom@9.8.4):
resolution: {integrity: sha512-y35WTrSTlTxfMLttgQk4rHcaDkbHQwDP++SNwPb+7H8yb13Q3cu2EixrtHzF27iZ8v0XCciSsLg00RkPAzB/aA==}
engines: {node: '>=v14.18.0'}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@vitest/browser': '*'

View File

@ -37,11 +37,11 @@ const Brand = function (props: Props) {
);
};
export default Brand;
const useStyles = makeStyles((theme: Theme) => ({
links: {
fontSize: "0.7em",
color: grey[500],
},
}));
export default Brand;

View File

@ -8,7 +8,7 @@ export interface Props {
children: ReactNode;
}
function ComponentOrLoading(props: Props) {
const ComponentOrLoading = function (props: Props) {
return (
<Fragment>
<div className={props.ready ? "hidden" : ""}>
@ -17,6 +17,6 @@ function ComponentOrLoading(props: Props) {
{props.ready ? props.children : null}
</Fragment>
);
}
};
export default ComponentOrLoading;

View File

@ -0,0 +1,80 @@
import React, { Fragment, ReactNode, useState } from "react";
import { Check, ContentCopy } from "@mui/icons-material";
import { Button, ButtonPropsVariantOverrides, CircularProgress, SxProps, Tooltip } from "@mui/material";
export interface Props {
variant?: "contained" | "outlined" | "text";
tooltip: string;
children: ReactNode;
childrenCopied?: ReactNode;
value: string | null;
xs?: number;
msTimeoutCopying?: number;
msTimeoutCopied?: number;
sx?: SxProps;
fullWidth?: boolean;
}
const msTmeoutDefaultCopying = 500;
const msTmeoutDefaultCopied = 2000;
const CopyButton = function (props: Props) {
const [isCopied, setIsCopied] = useState(false);
const [isCopying, setIsCopying] = useState(false);
const msTimeoutCopying = props.msTimeoutCopying ? props.msTimeoutCopying : msTmeoutDefaultCopying;
const msTimeoutCopied = props.msTimeoutCopied ? props.msTimeoutCopied : msTmeoutDefaultCopied;
const handleCopyToClipboard = () => {
if (isCopied || !props.value || props.value === "") {
return;
}
(async (value: string) => {
setIsCopying(true);
await navigator.clipboard.writeText(value);
setTimeout(() => {
setIsCopying(false);
setIsCopied(true);
}, msTimeoutCopying);
setTimeout(() => {
setIsCopied(false);
}, msTimeoutCopied);
})(props.value);
};
return props.value === null || props.value === "" ? (
<Button
variant={props.variant ? props.variant : "outlined"}
color={isCopied ? "success" : "primary"}
disabled
sx={props.sx}
fullWidth={props.fullWidth}
startIcon={
isCopying ? <CircularProgress color="inherit" size={20} /> : isCopied ? <Check /> : <ContentCopy />
}
>
{isCopied && props.childrenCopied ? props.childrenCopied : props.children}
</Button>
) : (
<Tooltip title={props.tooltip}>
<Button
variant={props.variant ? props.variant : "outlined"}
color={isCopied ? "success" : "primary"}
onClick={isCopying ? undefined : handleCopyToClipboard}
sx={props.sx}
fullWidth={props.fullWidth}
startIcon={
isCopying ? <CircularProgress color="inherit" size={20} /> : isCopied ? <Check /> : <ContentCopy />
}
>
{isCopied && props.childrenCopied ? props.childrenCopied : props.children}
</Button>
</Tooltip>
);
};
export default CopyButton;

View File

@ -27,8 +27,6 @@ const FixedTextField = function (props: TextFieldProps) {
);
};
export default FixedTextField;
const useStyles = makeStyles((theme: Theme) => ({
label: {
backgroundColor: theme.palette.background.default,
@ -36,3 +34,5 @@ const useStyles = makeStyles((theme: Theme) => ({
paddingRight: theme.spacing(0.1),
},
}));
export default FixedTextField;

View File

@ -11,7 +11,7 @@ export interface Props {
tooltip?: string;
}
export default function TypographyWithTooltip(props: Props): JSX.Element {
const TypographyWithTooltip = function (props: Props): JSX.Element {
return (
<Fragment>
{props.tooltip ? (
@ -23,4 +23,6 @@ export default function TypographyWithTooltip(props: Props): JSX.Element {
)}
</Fragment>
);
}
};
export default TypographyWithTooltip;

View File

@ -12,7 +12,7 @@ interface Props {
timeout: number;
}
export default function WebAuthnRegisterIcon(props: Props) {
const WebAuthnRegisterIcon = function (props: Props) {
const theme = useTheme();
const [timerPercent, triggerTimer] = useTimer(props.timeout);
@ -36,4 +36,6 @@ export default function WebAuthnRegisterIcon(props: Props) {
</IconWithContext>
</Box>
);
}
};
export default WebAuthnRegisterIcon;

View File

@ -15,7 +15,7 @@ interface Props {
webauthnTouchState: WebAuthnTouchState;
}
export default function WebAuthnTryIcon(props: Props) {
const WebAuthnTryIcon = function (props: Props) {
const touchTimeout = 30;
const theme = useTheme();
const [timerPercent, triggerTimer, clearTimer] = useTimer(touchTimeout * 1000 - 500);
@ -65,4 +65,6 @@ export default function WebAuthnTryIcon(props: Props) {
{failure}
</Box>
);
}
};
export default WebAuthnTryIcon;

View File

@ -15,8 +15,6 @@ interface NotificationContextProps {
const NotificationsContext = createContext<NotificationContextProps>({ notification: null, setNotification: () => {} });
export default NotificationsContext;
export function useNotifications() {
let useNotificationsProps = useContext(NotificationsContext);
@ -47,3 +45,5 @@ export function useNotifications() {
isActive,
};
}
export default NotificationsContext;

View File

@ -108,8 +108,6 @@ const LoginLayout = function (props: Props) {
);
};
export default LoginLayout;
const useStyles = makeStyles((theme: Theme) => ({
root: {
minHeight: "90vh",
@ -132,3 +130,5 @@ const useStyles = makeStyles((theme: Theme) => ({
paddingBottom: theme.spacing(),
},
}));
export default LoginLayout;

View File

@ -130,8 +130,6 @@ const SettingsLayout = function (props: Props) {
);
};
export default SettingsLayout;
interface NavItem {
keyname?: string;
text: string;
@ -172,61 +170,4 @@ const DrawerNavItem = function (props: NavItem) {
);
};
/*
interface SettingsMenuProps {
isXL: boolean;
handleClickMenuItem: () => void;
handleToggleDrawer: (open: boolean) => (event: SyntheticEvent) => void;
}
const SettingsMenu = function (props: SettingsMenuProps) {
const { t: translate } = useTranslation("settings");
const navigate = useRouterNavigate();
return (
<Box
sx={{
p: 2,
height: 1,
}}
>
{props.isXL ? null : (
<Fragment>
<IconButton sx={{ mb: 2 }} onClick={props.handleToggleDrawer(false)}>
<Close />
</IconButton>
<Divider sx={{ mb: 2 }} />
</Fragment>
)}
<List sx={{ mb: 2 }}>
<SettingsMenuItem
pathname={SettingsRoute}
text={translate("Overview")}
icon={<Dashboard color={"primary"} />}
onClick={props.handleClickMenuItem}
/>
<SettingsMenuItem
pathname={`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`}
text={translate("Two-Factor Authentication")}
icon={<SystemSecurityUpdateGoodIcon color={"primary"} />}
onClick={props.handleClickMenuItem}
/>
<ListItem
disablePadding
onClick={() => {
navigate(IndexRoute);
}}
>
<ListItemButton>
<ListItemIcon>
<Close color={"error"} />
</ListItemIcon>
<ListItemText primary={"Close"} />
</ListItemButton>
</ListItem>
</List>
</Box>
);
};
*/
export default SettingsLayout;

View File

@ -22,8 +22,6 @@ const BaseLoadingPage = function (props: Props) {
);
};
export default BaseLoadingPage;
const useStyles = makeStyles((theme: Theme) => ({
gridOuter: {
alignItems: "center",
@ -35,3 +33,5 @@ const useStyles = makeStyles((theme: Theme) => ({
display: "inline-block",
},
}));
export default BaseLoadingPage;

View File

@ -21,11 +21,11 @@ const Authenticated = function () {
);
};
export default Authenticated;
const useStyles = makeStyles((theme: Theme) => ({
iconContainer: {
marginBottom: theme.spacing(2),
flex: "0 0 100%",
},
}));
export default Authenticated;

View File

@ -40,8 +40,6 @@ const AuthenticatedView = function (props: Props) {
);
};
export default AuthenticatedView;
const useStyles = makeStyles((theme: Theme) => ({
mainContainer: {
border: "1px solid #d6d6d6",
@ -51,3 +49,5 @@ const useStyles = makeStyles((theme: Theme) => ({
marginBottom: theme.spacing(2),
},
}));
export default AuthenticatedView;

View File

@ -276,8 +276,6 @@ const useStyles = makeStyles((theme: Theme) => ({
preConfigure: {},
}));
export default ConsentView;
interface ComponentOrLoadingProps {
ready: boolean;
@ -294,3 +292,5 @@ function ComponentOrLoading(props: ComponentOrLoadingProps) {
</Fragment>
);
}
export default ConsentView;

View File

@ -227,8 +227,6 @@ const FirstFactorForm = function (props: Props) {
);
};
export default FirstFactorForm;
const useStyles = makeStyles((theme: Theme) => ({
actionRow: {
display: "flex",
@ -248,3 +246,5 @@ const useStyles = makeStyles((theme: Theme) => ({
justifyContent: "flex-end",
},
}));
export default FirstFactorForm;

View File

@ -211,8 +211,6 @@ const LoginPortal = function (props: Props) {
);
};
export default LoginPortal;
interface ComponentOrLoadingProps {
ready: boolean;
@ -229,3 +227,5 @@ function ComponentOrLoading(props: ComponentOrLoadingProps) {
</Fragment>
);
}
export default LoginPortal;

View File

@ -91,8 +91,6 @@ const DefaultDeviceSelectionContainer = function (props: Props) {
);
};
export default DefaultDeviceSelectionContainer;
interface DeviceItemProps {
id: number;
device: SelectableDevice;
@ -100,7 +98,7 @@ interface DeviceItemProps {
onSelect: () => void;
}
function DeviceItem(props: DeviceItemProps) {
const DeviceItem = function (props: DeviceItemProps) {
const className = "device-option-" + props.id;
const idName = "device-" + props.device.id;
const style = makeStyles((theme: Theme) => ({
@ -136,7 +134,7 @@ function DeviceItem(props: DeviceItemProps) {
</Button>
</Grid>
);
}
};
interface MethodItemProps {
id: number;
@ -145,7 +143,7 @@ interface MethodItemProps {
onSelect: () => void;
}
function MethodItem(props: MethodItemProps) {
const MethodItem = function (props: MethodItemProps) {
const className = "method-option-" + props.id;
const idName = "method-" + props.method;
const style = makeStyles((theme: Theme) => ({
@ -181,4 +179,6 @@ function MethodItem(props: MethodItemProps) {
</Button>
</Grid>
);
}
};
export default DefaultDeviceSelectionContainer;

View File

@ -45,8 +45,6 @@ const OTPDial = function (props: Props) {
);
};
export default OTPDial;
const useStyles = makeStyles((theme: Theme) => ({
timeProgress: {},
register: {
@ -85,3 +83,5 @@ function Icon(props: IconProps) {
</Fragment>
);
}
export default OTPDial;

View File

@ -10,7 +10,7 @@ interface Props {
handleClose: (ok: boolean) => void;
}
export default function DeleteDialog(props: Props) {
const DeleteDialog = function (props: Props) {
const { t: translate } = useTranslation("settings");
const handleCancel = () => {
@ -35,4 +35,6 @@ export default function DeleteDialog(props: Props) {
</DialogActions>
</Dialog>
);
}
};
export default DeleteDialog;

View File

@ -15,7 +15,7 @@ interface Props {
handleRefresh: () => void;
}
export default function TOTPDevice(props: Props) {
const TOTPDevice = function (props: Props) {
const { t: translate } = useTranslation("settings");
const { createSuccessNotification, createErrorNotification } = useNotifications();
@ -133,4 +133,6 @@ export default function TOTPDevice(props: Props) {
</Paper>
</Fragment>
);
}
};
export default TOTPDevice;

View File

@ -13,7 +13,7 @@ interface Props {
handleRefreshState: () => void;
}
export default function TOTPPanel(props: Props) {
const TOTPPanel = function (props: Props) {
const { t: translate } = useTranslation("settings");
const [showRegisterDialog, setShowRegisterDialog] = useState<boolean>(false);
@ -68,4 +68,6 @@ export default function TOTPPanel(props: Props) {
</Paper>
</Fragment>
);
}
};
export default TOTPPanel;

View File

@ -1,8 +1,7 @@
import React, { Fragment, useCallback, useEffect, useState } from "react";
import { IconDefinition, faCopy, faKey, faTimesCircle } from "@fortawesome/free-solid-svg-icons";
import { faTimesCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Visibility } from "@mui/icons-material";
import {
Box,
Button,
@ -15,13 +14,13 @@ import {
FormControl,
FormControlLabel,
FormLabel,
IconButton,
Link,
Radio,
RadioGroup,
Step,
StepLabel,
Stepper,
Switch,
TextField,
Theme,
Typography,
@ -34,9 +33,11 @@ import { QRCodeSVG } from "qrcode.react";
import { useTranslation } from "react-i18next";
import AppStoreBadges from "@components/AppStoreBadges";
import CopyButton from "@components/CopyButton";
import SuccessIcon from "@components/SuccessIcon";
import { GoogleAuthenticator } from "@constants/constants";
import { useNotifications } from "@hooks/NotificationsContext";
import { TOTPOptions, toAlgorithmString } from "@models/TOTPConfiguration";
import { toAlgorithmString } from "@models/TOTPConfiguration";
import { completeTOTPRegister, stopTOTPRegister } from "@services/OneTimePassword";
import { getTOTPSecret } from "@services/RegisterDevice";
import { getTOTPOptions } from "@services/UserInfoTOTPConfiguration";
@ -50,52 +51,64 @@ interface Props {
setClosed: () => void;
}
export default function TOTPRegisterDialogController(props: Props) {
interface Options {
algorithm: string;
length: number;
period: number;
}
interface AvailableOptions {
algorithms: string[];
lengths: number[];
periods: number[];
}
const TOTPRegisterDialogController = function (props: Props) {
const { t: translate } = useTranslation("settings");
const styles = useStyles();
const { createErrorNotification, createSuccessNotification } = useNotifications();
const { createErrorNotification } = useNotifications();
const [selected, setSelected] = useState<Options>({ algorithm: "", length: 6, period: 30 });
const [defaults, setDefaults] = useState<Options | null>(null);
const [available, setAvailable] = useState<AvailableOptions>({
algorithms: [],
lengths: [],
periods: [],
});
const [activeStep, setActiveStep] = useState(0);
const [options, setOptions] = useState<TOTPOptions | null>(null);
const [optionAlgorithm, setOptionAlgorithm] = useState("");
const [optionLength, setOptionLength] = useState(6);
const [optionPeriod, setOptionPeriod] = useState(30);
const [optionAlgorithms, setOptionAlgorithms] = useState<string[]>([]);
const [optionLengths, setOptionLengths] = useState<string[]>([]);
const [optionPeriods, setOptionPeriods] = useState<string[]>([]);
const [totpSecretURL, setTOTPSecretURL] = useState("");
const [totpSecretBase32, setTOTPSecretBase32] = useState<string | undefined>(undefined);
const [totpIsLoading, setTOTPIsLoading] = useState(false);
const [secretURL, setSecretURL] = useState<string | null>(null);
const [secretValue, setSecretValue] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [showAdvanced, setShowAdvanced] = useState(false);
const [hasErrored, setHasErrored] = useState(false);
const [dialValue, setDialValue] = useState("");
const [dialState, setDialState] = useState(State.Idle);
const [totpSecretURLHidden, setTOTPSecretURLHidden] = useState(true);
const [showQRCode, setShowQRCode] = useState(true);
const [success, setSuccess] = useState(false);
const resetStates = () => {
setOptions(null);
setOptionAlgorithm("");
setOptionLength(6);
setOptionPeriod(30);
setOptionAlgorithms([]);
setOptionLengths([]);
setOptionPeriods([]);
setTOTPSecretURL("");
setTOTPSecretBase32(undefined);
setTOTPIsLoading(false);
const resetStates = useCallback(() => {
if (defaults) {
setSelected(defaults);
}
setSecretURL(null);
setSecretValue(null);
setIsLoading(false);
setShowAdvanced(false);
setActiveStep(0);
setDialValue("");
setDialState(State.Idle);
setTOTPSecretURLHidden(true);
};
setShowQRCode(true);
}, [defaults]);
const handleClose = useCallback(() => {
(async () => {
props.setClosed();
if (totpSecretURL !== "") {
if (secretURL !== "") {
try {
await stopTOTPRegister();
} catch (err) {
@ -105,12 +118,16 @@ export default function TOTPRegisterDialogController(props: Props) {
resetStates();
})();
}, [totpSecretURL, props]);
}, [props, secretURL, resetStates]);
const handleFinished = useCallback(() => {
setSuccess(true);
setTimeout(() => {
props.setClosed();
resetStates();
}, [props]);
}, 750);
}, [props, resetStates]);
const handleOnClose = () => {
if (!props.open) {
@ -121,21 +138,29 @@ export default function TOTPRegisterDialogController(props: Props) {
};
useEffect(() => {
if (!props.open || activeStep !== 0 || options !== null) {
if (!props.open || activeStep !== 0 || defaults !== null) {
return;
}
(async () => {
const opts = await getTOTPOptions();
setOptions(opts);
setOptionAlgorithm(toAlgorithmString(opts.algorithm));
setOptionAlgorithms(opts.algorithms.map((algorithm) => toAlgorithmString(algorithm)));
setOptionLength(opts.length);
setOptionLengths(opts.lengths.map((length) => length.toString()));
setOptionPeriod(opts.period);
setOptionPeriods(opts.periods.map((period) => period.toString()));
const decoded = {
algorithm: toAlgorithmString(opts.algorithm),
length: opts.length,
period: opts.period,
};
setAvailable({
algorithms: opts.algorithms.map((algorithm) => toAlgorithmString(algorithm)),
lengths: opts.lengths,
periods: opts.periods,
});
setDefaults(decoded);
setSelected(decoded);
})();
}, [props.open, activeStep, options]);
}, [props.open, activeStep, defaults, selected]);
const handleSetStepPrevious = useCallback(() => {
if (activeStep === 0) {
@ -143,7 +168,9 @@ export default function TOTPRegisterDialogController(props: Props) {
}
setShowAdvanced(false);
setActiveStep((prevState) => (prevState -= 1));
setActiveStep((prevState) => {
return prevState - 1;
});
}, [activeStep]);
const handleSetStepNext = useCallback(() => {
@ -152,7 +179,9 @@ export default function TOTPRegisterDialogController(props: Props) {
}
setShowAdvanced(false);
setActiveStep((prevState) => (prevState += 1));
setActiveStep((prevState) => {
return prevState + 1;
});
}, [activeStep]);
useEffect(() => {
@ -161,12 +190,12 @@ export default function TOTPRegisterDialogController(props: Props) {
}
(async () => {
setTOTPIsLoading(true);
setIsLoading(true);
try {
const secret = await getTOTPSecret(optionAlgorithm, optionLength, optionPeriod);
setTOTPSecretURL(secret.otpauth_url);
setTOTPSecretBase32(secret.base32_secret);
const secret = await getTOTPSecret(selected.algorithm, selected.length, selected.period);
setSecretURL(secret.otpauth_url);
setSecretValue(secret.base32_secret);
} catch (err) {
console.error(err);
if ((err as Error).message.includes("Request failed with status code 403")) {
@ -183,12 +212,12 @@ export default function TOTPRegisterDialogController(props: Props) {
setHasErrored(true);
}
setTOTPIsLoading(false);
setIsLoading(false);
})();
}, [activeStep, createErrorNotification, optionAlgorithm, optionLength, optionPeriod, props.open, translate]);
}, [activeStep, createErrorNotification, selected, props.open, translate]);
useEffect(() => {
if (!props.open || activeStep !== 2 || dialValue.length !== optionLength) {
if (!props.open || activeStep !== 2 || dialState === State.InProgress || dialValue.length !== selected.length) {
return;
}
@ -207,63 +236,49 @@ export default function TOTPRegisterDialogController(props: Props) {
setDialState(State.Failure);
}
})();
}, [activeStep, dialValue, dialValue.length, optionLength, props.open]);
}, [activeStep, dialState, dialValue, dialValue.length, handleFinished, props.open, selected.length]);
const toggleAdvanced = () => {
setShowAdvanced((prevState) => !prevState);
};
const advanced =
options !== null &&
(optionAlgorithms.length !== 1 || optionAlgorithms.length !== 1 || optionPeriods.length !== 1);
defaults !== null &&
(available.algorithms.length !== 1 || available.lengths.length !== 1 || available.periods.length !== 1);
const hideAdvanced =
options === null || (optionAlgorithms.length <= 1 && optionPeriods.length <= 1 && optionLengths.length <= 1);
const disableAdvanced =
defaults === null ||
(available.algorithms.length <= 1 && available.lengths.length <= 1 && available.periods.length <= 1);
const hideAlgorithms = advanced && optionAlgorithms.length <= 1;
const hideLengths = advanced && optionLengths.length <= 1;
const hidePeriods = advanced && optionPeriods.length <= 1;
const qrcodeFuzzyStyle = totpIsLoading || hasErrored ? styles.fuzzy : undefined;
function SecretButton(text: string, action: string, icon: IconDefinition) {
const handleOnClick = (event: React.MouseEvent<HTMLButtonElement>) => {
(async () => {
event.preventDefault();
await navigator.clipboard.writeText(text);
createSuccessNotification(action);
})();
};
return (
<IconButton color="primary" onClick={handleOnClick} size="large">
<FontAwesomeIcon icon={icon} />
</IconButton>
);
}
const hideAlgorithms = advanced && available.algorithms.length <= 1;
const hideLengths = advanced && available.lengths.length <= 1;
const hidePeriods = advanced && available.periods.length <= 1;
const qrcodeFuzzyStyle = isLoading || hasErrored ? styles.fuzzy : undefined;
function renderStep(step: number) {
switch (step) {
case 0:
return (
<Fragment>
{options === null ? (
<Grid xs={12}>
{defaults === null ? (
<Grid xs={12} my={3}>
<Typography>Loading...</Typography>
</Grid>
) : (
<Grid container>
<Grid xs={12}>
<Grid xs={12} my={3}>
<Typography>{translate("To begin select next")}</Typography>
</Grid>
<Grid xs={12} hidden={hideAdvanced}>
<Button variant={"outlined"} color={"warning"} onClick={toggleAdvanced}>
{showAdvanced ? translate("Hide Advanced") : translate("Show Advanced")}
</Button>
<Grid xs={12} hidden={disableAdvanced}>
<FormControlLabel
disabled={disableAdvanced}
control={<Switch checked={showAdvanced} onChange={toggleAdvanced} />}
label={translate("Advanced")}
/>
</Grid>
<Grid
xs={12}
hidden={hideAdvanced || !showAdvanced}
hidden={disableAdvanced || !showAdvanced}
justifyContent={"center"}
alignItems={"center"}
>
@ -274,17 +289,23 @@ export default function TOTPRegisterDialogController(props: Props) {
<RadioGroup
row
aria-labelledby={"lbl-adv-algorithms"}
value={optionAlgorithm}
value={selected.algorithm}
hidden={hideAlgorithms}
style={{
justifyContent: "center",
}}
onChange={(e, value) => {
setOptionAlgorithm(value);
setSelected((prevState) => {
return {
...prevState,
algorithm: value,
};
});
e.preventDefault();
}}
>
{optionAlgorithms.map((algorithm) => (
{available.algorithms.map((algorithm) => (
<FormControlLabel
key={algorithm}
value={algorithm}
@ -299,22 +320,28 @@ export default function TOTPRegisterDialogController(props: Props) {
<RadioGroup
row
aria-labelledby={"lbl-adv-lengths"}
value={optionLength.toString()}
value={selected.length.toString()}
hidden={hideLengths}
style={{
justifyContent: "center",
}}
onChange={(e, value) => {
setOptionLength(parseInt(value));
setSelected((prevState) => {
return {
...prevState,
length: parseInt(value),
};
});
e.preventDefault();
}}
>
{optionLengths.map((length) => (
{available.lengths.map((length) => (
<FormControlLabel
key={length}
value={length}
key={length.toString()}
value={length.toString()}
control={<Radio />}
label={length}
label={length.toString()}
/>
))}
</RadioGroup>
@ -324,22 +351,28 @@ export default function TOTPRegisterDialogController(props: Props) {
<RadioGroup
row
aria-labelledby={"lbl-adv-periods"}
value={optionPeriod.toString()}
value={selected.period.toString()}
hidden={hidePeriods}
style={{
justifyContent: "center",
}}
onChange={(e, value) => {
setOptionPeriod(parseInt(value));
setSelected((prevState) => {
return {
...prevState,
period: parseInt(value),
};
});
e.preventDefault();
}}
>
{optionPeriods.map((period) => (
{available.periods.map((period) => (
<FormControlLabel
key={period}
value={period}
key={period.toString()}
value={period.toString()}
control={<Radio />}
label={period}
label={period.toString()}
/>
))}
</RadioGroup>
@ -352,52 +385,63 @@ export default function TOTPRegisterDialogController(props: Props) {
case 1:
return (
<Fragment>
<Grid xs={12}>
<Grid xs={12} my={2}>
<FormControlLabel
disabled={disableAdvanced}
control={
<Switch
checked={showQRCode}
onChange={() => {
setShowQRCode((value) => !value);
}}
/>
}
label={translate("QR Code")}
/>
</Grid>
<Grid xs={12} hidden={!showQRCode}>
<Box className={classnames(qrcodeFuzzyStyle, styles.qrcodeContainer)}>
<Link href={totpSecretURL} underline="hover">
<QRCodeSVG value={totpSecretURL} className={styles.qrcode} size={150} />
{!hasErrored && totpIsLoading ? (
{secretURL !== null ? (
<Link href={secretURL} underline="hover">
<QRCodeSVG value={secretURL} className={styles.qrcode} size={200} />
{!hasErrored && isLoading ? (
<CircularProgress className={styles.loader} size={128} />
) : null}
{hasErrored ? (
<FontAwesomeIcon className={styles.failureIcon} icon={faTimesCircle} />
) : null}
</Link>
) : null}
</Box>
</Grid>
<Grid xs={12}>
<Grid xs={12} hidden={showQRCode}>
<Grid container spacing={2} justifyContent={"center"}>
<Grid xs={2}>
<IconButton
color="primary"
onClick={() => {
setTOTPSecretURLHidden((value) => !value);
}}
size="large"
<Grid xs={4}>
<CopyButton
tooltip={translate("Click to Copy")}
value={secretURL}
childrenCopied={translate("Copied")}
fullWidth={true}
>
<Visibility />
</IconButton>
{translate("OTP URL")}
</CopyButton>
</Grid>
<Grid xs={2}>
{totpSecretBase32
? SecretButton(
totpSecretBase32,
translate("OTP Secret copied to clipboard"),
faKey,
)
: null}
<Grid xs={4}>
<CopyButton
tooltip={translate("Click to Copy")}
value={secretValue}
childrenCopied={translate("Copied")}
fullWidth={true}
>
{translate("Secret")}
</CopyButton>
</Grid>
<Grid xs={2}>
{totpSecretURL !== ""
? SecretButton(totpSecretURL, translate("OTP URL copied to clipboard"), faCopy)
: null}
</Grid>
<Grid xs={12} hidden={totpSecretURLHidden || totpSecretURL === ""}>
<Grid xs={12}>
<TextField
id="secret-url"
label={translate("Secret")}
className={styles.secret}
value={totpSecretURL}
value={secretURL === null ? "" : secretURL}
multiline={true}
InputProps={{
readOnly: true,
@ -412,7 +456,7 @@ export default function TOTPRegisterDialogController(props: Props) {
{translate("Need Google Authenticator?")}
</Typography>
<AppStoreBadges
iconSize={128}
iconSize={110}
targetBlank
className={styles.googleAuthenticatorBadges}
googlePlayLink={GoogleAuthenticator.googlePlay}
@ -426,13 +470,19 @@ export default function TOTPRegisterDialogController(props: Props) {
return (
<Fragment>
<Grid xs={12} paddingY={4}>
{success ? (
<Box className={styles.success}>
<SuccessIcon />
</Box>
) : (
<OTPDial
passcode={dialValue}
state={dialState}
digits={optionLength}
period={optionPeriod}
digits={selected.length}
period={selected.period}
onChange={setDialValue}
/>
)}
</Grid>
</Fragment>
);
@ -482,7 +532,7 @@ export default function TOTPRegisterDialogController(props: Props) {
</DialogActions>
</Dialog>
);
}
};
const useStyles = makeStyles((theme: Theme) => ({
qrcode: {
@ -520,4 +570,10 @@ const useStyles = makeStyles((theme: Theme) => ({
color: red[400],
fontSize: "128px",
},
success: {
marginBottom: theme.spacing(2),
flex: "0 0 100%",
},
}));
export default TOTPRegisterDialogController;

View File

@ -11,7 +11,7 @@ import WebAuthnDevicesPanel from "@views/Settings/TwoFactorAuthentication/WebAut
interface Props {}
export default function TwoFactorAuthSettings(props: Props) {
const TwoFactorAuthSettings = function (props: Props) {
const [refreshState, setRefreshState] = useState(0);
const { createErrorNotification } = useNotifications();
const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoPOST();
@ -78,4 +78,6 @@ export default function TwoFactorAuthSettings(props: Props) {
</Grid>
</Grid>
);
}
};
export default TwoFactorAuthSettings;

View File

@ -1,21 +1,22 @@
import React, { Fragment, useState } from "react";
import React, { Fragment } from "react";
import { Check, ContentCopy } from "@mui/icons-material";
import {
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Divider,
Tooltip,
Paper,
PaperProps,
Typography,
} from "@mui/material";
import Grid from "@mui/material/Unstable_Grid2";
import Draggable from "react-draggable";
import { useTranslation } from "react-i18next";
import CopyButton from "@components/CopyButton";
import { WebAuthnDevice, toTransportName } from "@models/WebAuthn";
interface Props {
@ -24,12 +25,19 @@ interface Props {
handleClose: () => void;
}
export default function WebAuthnDeviceDetailsDialog(props: Props) {
const WebAuthnDeviceDetailsDialog = function (props: Props) {
const { t: translate } = useTranslation("settings");
return (
<Dialog open={props.open} onClose={props.handleClose}>
<DialogTitle>{translate("WebAuthn Credential Details")}</DialogTitle>
<Dialog
open={props.open}
onClose={props.handleClose}
PaperComponent={PaperComponent}
aria-labelledby="webauthn-device-details-dialog-title"
>
<DialogTitle id="webauthn-device-details-dialog-title">
{translate("WebAuthn Credential Details")}
</DialogTitle>
<DialogContent>
<DialogContentText sx={{ mb: 3 }}>
{translate("Extended WebAuthn credential information for security key", {
@ -40,12 +48,6 @@ export default function WebAuthnDeviceDetailsDialog(props: Props) {
<Grid md={3} sx={{ display: { xs: "none", md: "block" } }}>
<Fragment />
</Grid>
<Grid xs={4} md={2}>
<PropertyCopyButton name={translate("KID")} value={props.device.kid.toString()} />
</Grid>
<Grid xs={8} md={4}>
<PropertyCopyButton name={translate("Public Key")} value={props.device.public_key.toString()} />
</Grid>
<Grid xs={12}>
<Divider />
</Grid>
@ -128,11 +130,29 @@ export default function WebAuthnDeviceDetailsDialog(props: Props) {
</Grid>
</DialogContent>
<DialogActions>
<CopyButton
variant={"contained"}
tooltip={`${translate("Click to copy the")} ${translate("KID")}`}
value={props.device.kid.toString()}
fullWidth={false}
childrenCopied={translate("Copied")}
>
{translate("KID")}
</CopyButton>
<CopyButton
variant={"contained"}
tooltip={`${translate("Click to copy the")} ${translate("Public Key")}`}
value={props.device.public_key.toString()}
fullWidth={false}
childrenCopied={translate("Copied")}
>
{translate("Public Key")}
</CopyButton>
<Button onClick={props.handleClose}>{translate("Close")}</Button>
</DialogActions>
</Dialog>
);
}
};
interface PropertyTextProps {
name: string;
@ -140,49 +160,6 @@ interface PropertyTextProps {
xs?: number;
}
function PropertyCopyButton(props: PropertyTextProps) {
const { t: translate } = useTranslation("settings");
const [copied, setCopied] = useState(false);
const [copying, setCopying] = useState(false);
const handleCopyToClipboard = () => {
if (copied) {
return;
}
(async () => {
setCopying(true);
await navigator.clipboard.writeText(props.value);
setTimeout(() => {
setCopying(false);
setCopied(true);
}, 500);
setTimeout(() => {
setCopied(false);
}, 2000);
})();
};
return (
<Tooltip title={`${translate("Click to copy the")} ${props.name}`}>
<Button
variant="outlined"
color={copied ? "success" : "primary"}
onClick={copying ? undefined : handleCopyToClipboard}
startIcon={
copying ? <CircularProgress color="inherit" size={20} /> : copied ? <Check /> : <ContentCopy />
}
>
{copied ? translate("Copied") : props.name}
</Button>
</Tooltip>
);
}
function PropertyText(props: PropertyTextProps) {
return (
<Grid xs={props.xs !== undefined ? props.xs : 12}>
@ -193,3 +170,13 @@ function PropertyText(props: PropertyTextProps) {
</Grid>
);
}
function PaperComponent(props: PaperProps) {
return (
<Draggable handle="#webauthn-device-details-dialog-title" cancel={'[class*="MuiDialogContent-root"]'}>
<Paper {...props} />
</Draggable>
);
}
export default WebAuthnDeviceDetailsDialog;

View File

@ -10,8 +10,7 @@ interface Props {
device: WebAuthnDevice;
handleClose: (ok: boolean, name: string) => void;
}
export default function WebAuthnDeviceEditDialog(props: Props) {
const WebAuthnDeviceEditDialog = function (props: Props) {
const { t: translate } = useTranslation("settings");
const [deviceName, setName] = useState("");
@ -36,12 +35,14 @@ export default function WebAuthnDeviceEditDialog(props: Props) {
<Dialog open={props.open} onClose={handleCancel}>
<DialogTitle>{translate("Edit WebAuthn Credential")}</DialogTitle>
<DialogContent>
<DialogContentText>{translate("Enter a new name for this WebAuthn credential")}</DialogContentText>
<DialogContentText>
{translate("Enter a new description for this WebAuthn credential")}
</DialogContentText>
<TextField
autoFocus
inputRef={nameRef}
id="name-textfield"
label={translate("Name")}
label={translate("Description")}
variant="standard"
required
value={deviceName}
@ -68,4 +69,6 @@ export default function WebAuthnDeviceEditDialog(props: Props) {
</DialogActions>
</Dialog>
);
}
};
export default WebAuthnDeviceEditDialog;

View File

@ -21,8 +21,7 @@ interface Props {
device: WebAuthnDevice;
handleEdit: () => void;
}
export default function WebAuthnDeviceItem(props: Props) {
const WebAuthnDeviceItem = function (props: Props) {
const { t: translate } = useTranslation("settings");
const { createSuccessNotification, createErrorNotification } = useNotifications();
@ -197,4 +196,6 @@ export default function WebAuthnDeviceItem(props: Props) {
</Paper>
</Grid>
);
}
};
export default WebAuthnDeviceItem;

View File

@ -8,6 +8,8 @@ import {
DialogContent,
DialogContentText,
DialogTitle,
Paper,
PaperProps,
Step,
StepLabel,
Stepper,
@ -18,6 +20,7 @@ import {
import Grid from "@mui/material/Unstable_Grid2";
import makeStyles from "@mui/styles/makeStyles";
import { PublicKeyCredentialCreationOptionsJSON } from "@simplewebauthn/typescript-types";
import Draggable from "react-draggable";
import { useTranslation } from "react-i18next";
import InformationIcon from "@components/InformationIcon";
@ -291,6 +294,14 @@ const WebAuthnDeviceRegisterDialog = function (props: Props) {
);
};
function PaperComponent(props: PaperProps) {
return (
<Draggable handle="#webauthn-device-details-dialog-title" cancel={'[class*="MuiDialogContent-root"]'}>
<Paper {...props} />
</Draggable>
);
}
export default WebAuthnDeviceRegisterDialog;
const useStyles = makeStyles((theme: Theme) => ({

View File

@ -12,8 +12,7 @@ interface Props {
devices: WebAuthnDevice[] | undefined;
handleRefreshState: () => void;
}
export default function WebAuthnDevicesPanel(props: Props) {
const WebAuthnDevicesPanel = function (props: Props) {
const { t: translate } = useTranslation("settings");
const [showRegisterDialog, setShowRegisterDialog] = useState<boolean>(false);
@ -63,4 +62,6 @@ export default function WebAuthnDevicesPanel(props: Props) {
</Paper>
</Fragment>
);
}
};
export default WebAuthnDevicesPanel;

View File

@ -10,7 +10,7 @@ interface Props {
handleRefreshState: () => void;
}
export default function WebAuthnDevicesStack(props: Props) {
const WebAuthnDevicesStack = function (props: Props) {
return (
<Grid container spacing={3}>
{props.devices.map((x, idx) => (
@ -18,4 +18,6 @@ export default function WebAuthnDevicesStack(props: Props) {
))}
</Grid>
);
}
};
export default WebAuthnDevicesStack;