refactor: simplified
Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>feat-otp-email-verify
parent
56aeb1bd86
commit
18b1fc5ed2
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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': '*'
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(() => {
|
||||
props.setClosed();
|
||||
resetStates();
|
||||
}, [props]);
|
||||
setSuccess(true);
|
||||
|
||||
setTimeout(() => {
|
||||
props.setClosed();
|
||||
resetStates();
|
||||
}, 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 ? (
|
||||
<CircularProgress className={styles.loader} size={128} />
|
||||
) : null}
|
||||
{hasErrored ? (
|
||||
<FontAwesomeIcon className={styles.failureIcon} icon={faTimesCircle} />
|
||||
) : null}
|
||||
</Link>
|
||||
{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}>
|
||||
<OTPDial
|
||||
passcode={dialValue}
|
||||
state={dialState}
|
||||
digits={optionLength}
|
||||
period={optionPeriod}
|
||||
onChange={setDialValue}
|
||||
/>
|
||||
{success ? (
|
||||
<Box className={styles.success}>
|
||||
<SuccessIcon />
|
||||
</Box>
|
||||
) : (
|
||||
<OTPDial
|
||||
passcode={dialValue}
|
||||
state={dialState}
|
||||
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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) => ({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue