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",
|
"Clone Warning": "Clone Warning",
|
||||||
"Created": "Created",
|
"Created": "Created",
|
||||||
"Delete": "Delete",
|
"Delete": "Delete",
|
||||||
|
"Description": "Description",
|
||||||
"Details": "Details",
|
"Details": "Details",
|
||||||
"Display extended information for this WebAuthn credential": "Display extended information for this WebAuthn credential",
|
"Display extended information for this WebAuthn credential": "Display extended information for this WebAuthn credential",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
"Edit information for this WebAuthn credential": "Edit information for this WebAuthn credential",
|
"Edit information for this WebAuthn credential": "Edit information for this WebAuthn credential",
|
||||||
"Edit WebAuthn Credential": "Edit WebAuthn Credential",
|
"Edit WebAuthn Credential": "Edit WebAuthn Credential",
|
||||||
"Enabled": "Enabled",
|
"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",
|
"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}}",
|
"Extended WebAuthn credential information for security key": "Extended WebAuthn credential information for security key {{description}}",
|
||||||
"Identifier": "Identifier",
|
"Identifier": "Identifier",
|
||||||
"Last Used": "Last Used",
|
"Last Used": "Last Used",
|
||||||
"Last Used when": "Last Used {{when, datetime}}",
|
"Last Used when": "Last Used {{when, datetime}}",
|
||||||
"Manage your security keys": "Manage your security keys",
|
"Manage your security keys": "Manage your security keys",
|
||||||
"Name": "Name",
|
|
||||||
"No": "No",
|
"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.",
|
"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",
|
"Overview": "Overview",
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
"qrcode.react": "3.1.0",
|
"qrcode.react": "3.1.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-draggable": "^4.4.5",
|
||||||
"react-i18next": "12.2.0",
|
"react-i18next": "12.2.0",
|
||||||
"react-loading": "2.0.3",
|
"react-loading": "2.0.3",
|
||||||
"react-router-dom": "6.10.0",
|
"react-router-dom": "6.10.0",
|
||||||
|
|
|
@ -64,6 +64,9 @@ dependencies:
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: 18.2.0
|
specifier: 18.2.0
|
||||||
version: 18.2.0(react@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:
|
react-i18next:
|
||||||
specifier: 12.2.0
|
specifier: 12.2.0
|
||||||
version: 12.2.0(i18next@22.4.15)(react-dom@18.2.0)(react@18.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):
|
/eslint-config-prettier@8.8.0(eslint@8.38.0):
|
||||||
resolution: {integrity: sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==}
|
resolution: {integrity: sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: '>=7.0.0'
|
eslint: '>=7.0.0'
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6485,6 +6489,18 @@ packages:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
scheduler: 0.23.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):
|
/react-i18next@12.2.0(i18next@22.4.15)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-5XeVgSygaGfyFmDd2WcXvINRw2WEC1XviW1LXY/xLOEMzsCFRwKqfnHN+hUjla8ZipbVJR27GCMSuTr0BhBBBQ==}
|
resolution: {integrity: sha512-5XeVgSygaGfyFmDd2WcXvINRw2WEC1XviW1LXY/xLOEMzsCFRwKqfnHN+hUjla8ZipbVJR27GCMSuTr0BhBBBQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -7165,6 +7181,7 @@ packages:
|
||||||
|
|
||||||
/ts-node@10.9.1(@types/node@18.15.13)(typescript@5.0.4):
|
/ts-node@10.9.1(@types/node@18.15.13)(typescript@5.0.4):
|
||||||
resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
|
resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@swc/core': '>=1.2.50'
|
'@swc/core': '>=1.2.50'
|
||||||
'@swc/wasm': '>=1.2.50'
|
'@swc/wasm': '>=1.2.50'
|
||||||
|
@ -7196,6 +7213,7 @@ packages:
|
||||||
/tsconfck@2.1.1(typescript@5.0.4):
|
/tsconfck@2.1.1(typescript@5.0.4):
|
||||||
resolution: {integrity: sha512-ZPCkJBKASZBmBUNqGHmRhdhM8pJYDdOXp4nRgj/O0JwUwsMq50lCDRQP/M5GBNAA0elPrq4gAeu4dkaVCuKWww==}
|
resolution: {integrity: sha512-ZPCkJBKASZBmBUNqGHmRhdhM8pJYDdOXp4nRgj/O0JwUwsMq50lCDRQP/M5GBNAA0elPrq4gAeu4dkaVCuKWww==}
|
||||||
engines: {node: ^14.13.1 || ^16 || >=18}
|
engines: {node: ^14.13.1 || ^16 || >=18}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: ^4.3.5 || ^5.0.0
|
typescript: ^4.3.5 || ^5.0.0
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
|
@ -7337,6 +7355,7 @@ packages:
|
||||||
|
|
||||||
/update-browserslist-db@1.0.10(browserslist@4.21.5):
|
/update-browserslist-db@1.0.10(browserslist@4.21.5):
|
||||||
resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==}
|
resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
browserslist: '>= 4.21.0'
|
browserslist: '>= 4.21.0'
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -7456,6 +7475,7 @@ packages:
|
||||||
/vite@3.2.5(@types/node@18.15.13):
|
/vite@3.2.5(@types/node@18.15.13):
|
||||||
resolution: {integrity: sha512-4mVEpXpSOgrssFZAOmGIr85wPHKvaDAcXqxVxVRZhljkJOMZi1ibLibzjLHzJvcok8BMguLc7g1W6W/GqZbLdQ==}
|
resolution: {integrity: sha512-4mVEpXpSOgrssFZAOmGIr85wPHKvaDAcXqxVxVRZhljkJOMZi1ibLibzjLHzJvcok8BMguLc7g1W6W/GqZbLdQ==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/node': '>= 14'
|
'@types/node': '>= 14'
|
||||||
less: '*'
|
less: '*'
|
||||||
|
@ -7489,6 +7509,7 @@ packages:
|
||||||
/vite@4.3.1(@types/node@18.15.13):
|
/vite@4.3.1(@types/node@18.15.13):
|
||||||
resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==}
|
resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/node': '>= 14'
|
'@types/node': '>= 14'
|
||||||
less: '*'
|
less: '*'
|
||||||
|
@ -7538,6 +7559,7 @@ packages:
|
||||||
/vitest@0.30.1(happy-dom@9.8.4):
|
/vitest@0.30.1(happy-dom@9.8.4):
|
||||||
resolution: {integrity: sha512-y35WTrSTlTxfMLttgQk4rHcaDkbHQwDP++SNwPb+7H8yb13Q3cu2EixrtHzF27iZ8v0XCciSsLg00RkPAzB/aA==}
|
resolution: {integrity: sha512-y35WTrSTlTxfMLttgQk4rHcaDkbHQwDP++SNwPb+7H8yb13Q3cu2EixrtHzF27iZ8v0XCciSsLg00RkPAzB/aA==}
|
||||||
engines: {node: '>=v14.18.0'}
|
engines: {node: '>=v14.18.0'}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@edge-runtime/vm': '*'
|
'@edge-runtime/vm': '*'
|
||||||
'@vitest/browser': '*'
|
'@vitest/browser': '*'
|
||||||
|
|
|
@ -37,11 +37,11 @@ const Brand = function (props: Props) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Brand;
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
links: {
|
links: {
|
||||||
fontSize: "0.7em",
|
fontSize: "0.7em",
|
||||||
color: grey[500],
|
color: grey[500],
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export default Brand;
|
||||||
|
|
|
@ -8,7 +8,7 @@ export interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComponentOrLoading(props: Props) {
|
const ComponentOrLoading = function (props: Props) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className={props.ready ? "hidden" : ""}>
|
<div className={props.ready ? "hidden" : ""}>
|
||||||
|
@ -17,6 +17,6 @@ function ComponentOrLoading(props: Props) {
|
||||||
{props.ready ? props.children : null}
|
{props.ready ? props.children : null}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ComponentOrLoading;
|
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) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
label: {
|
label: {
|
||||||
backgroundColor: theme.palette.background.default,
|
backgroundColor: theme.palette.background.default,
|
||||||
|
@ -36,3 +34,5 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||||
paddingRight: theme.spacing(0.1),
|
paddingRight: theme.spacing(0.1),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export default FixedTextField;
|
||||||
|
|
|
@ -11,7 +11,7 @@ export interface Props {
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TypographyWithTooltip(props: Props): JSX.Element {
|
const TypographyWithTooltip = function (props: Props): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{props.tooltip ? (
|
{props.tooltip ? (
|
||||||
|
@ -23,4 +23,6 @@ export default function TypographyWithTooltip(props: Props): JSX.Element {
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default TypographyWithTooltip;
|
||||||
|
|
|
@ -12,7 +12,7 @@ interface Props {
|
||||||
timeout: number;
|
timeout: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebAuthnRegisterIcon(props: Props) {
|
const WebAuthnRegisterIcon = function (props: Props) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [timerPercent, triggerTimer] = useTimer(props.timeout);
|
const [timerPercent, triggerTimer] = useTimer(props.timeout);
|
||||||
|
|
||||||
|
@ -36,4 +36,6 @@ export default function WebAuthnRegisterIcon(props: Props) {
|
||||||
</IconWithContext>
|
</IconWithContext>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default WebAuthnRegisterIcon;
|
||||||
|
|
|
@ -15,7 +15,7 @@ interface Props {
|
||||||
webauthnTouchState: WebAuthnTouchState;
|
webauthnTouchState: WebAuthnTouchState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebAuthnTryIcon(props: Props) {
|
const WebAuthnTryIcon = function (props: Props) {
|
||||||
const touchTimeout = 30;
|
const touchTimeout = 30;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [timerPercent, triggerTimer, clearTimer] = useTimer(touchTimeout * 1000 - 500);
|
const [timerPercent, triggerTimer, clearTimer] = useTimer(touchTimeout * 1000 - 500);
|
||||||
|
@ -65,4 +65,6 @@ export default function WebAuthnTryIcon(props: Props) {
|
||||||
{failure}
|
{failure}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default WebAuthnTryIcon;
|
||||||
|
|
|
@ -15,8 +15,6 @@ interface NotificationContextProps {
|
||||||
|
|
||||||
const NotificationsContext = createContext<NotificationContextProps>({ notification: null, setNotification: () => {} });
|
const NotificationsContext = createContext<NotificationContextProps>({ notification: null, setNotification: () => {} });
|
||||||
|
|
||||||
export default NotificationsContext;
|
|
||||||
|
|
||||||
export function useNotifications() {
|
export function useNotifications() {
|
||||||
let useNotificationsProps = useContext(NotificationsContext);
|
let useNotificationsProps = useContext(NotificationsContext);
|
||||||
|
|
||||||
|
@ -47,3 +45,5 @@ export function useNotifications() {
|
||||||
isActive,
|
isActive,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default NotificationsContext;
|
||||||
|
|
|
@ -108,8 +108,6 @@ const LoginLayout = function (props: Props) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LoginLayout;
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
root: {
|
root: {
|
||||||
minHeight: "90vh",
|
minHeight: "90vh",
|
||||||
|
@ -132,3 +130,5 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||||
paddingBottom: theme.spacing(),
|
paddingBottom: theme.spacing(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export default LoginLayout;
|
||||||
|
|
|
@ -130,8 +130,6 @@ const SettingsLayout = function (props: Props) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingsLayout;
|
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
keyname?: string;
|
keyname?: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
@ -172,61 +170,4 @@ const DrawerNavItem = function (props: NavItem) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
export default SettingsLayout;
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
|
@ -22,8 +22,6 @@ const BaseLoadingPage = function (props: Props) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BaseLoadingPage;
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
gridOuter: {
|
gridOuter: {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
@ -35,3 +33,5 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export default BaseLoadingPage;
|
||||||
|
|
|
@ -21,11 +21,11 @@ const Authenticated = function () {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Authenticated;
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
iconContainer: {
|
iconContainer: {
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
flex: "0 0 100%",
|
flex: "0 0 100%",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export default Authenticated;
|
||||||
|
|
|
@ -40,8 +40,6 @@ const AuthenticatedView = function (props: Props) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AuthenticatedView;
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
mainContainer: {
|
mainContainer: {
|
||||||
border: "1px solid #d6d6d6",
|
border: "1px solid #d6d6d6",
|
||||||
|
@ -51,3 +49,5 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export default AuthenticatedView;
|
||||||
|
|
|
@ -276,8 +276,6 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||||
preConfigure: {},
|
preConfigure: {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default ConsentView;
|
|
||||||
|
|
||||||
interface ComponentOrLoadingProps {
|
interface ComponentOrLoadingProps {
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
|
|
||||||
|
@ -294,3 +292,5 @@ function ComponentOrLoading(props: ComponentOrLoadingProps) {
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default ConsentView;
|
||||||
|
|
|
@ -227,8 +227,6 @@ const FirstFactorForm = function (props: Props) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FirstFactorForm;
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
actionRow: {
|
actionRow: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
@ -248,3 +246,5 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export default FirstFactorForm;
|
||||||
|
|
|
@ -211,8 +211,6 @@ const LoginPortal = function (props: Props) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LoginPortal;
|
|
||||||
|
|
||||||
interface ComponentOrLoadingProps {
|
interface ComponentOrLoadingProps {
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
|
|
||||||
|
@ -229,3 +227,5 @@ function ComponentOrLoading(props: ComponentOrLoadingProps) {
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default LoginPortal;
|
||||||
|
|
|
@ -91,8 +91,6 @@ const DefaultDeviceSelectionContainer = function (props: Props) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DefaultDeviceSelectionContainer;
|
|
||||||
|
|
||||||
interface DeviceItemProps {
|
interface DeviceItemProps {
|
||||||
id: number;
|
id: number;
|
||||||
device: SelectableDevice;
|
device: SelectableDevice;
|
||||||
|
@ -100,7 +98,7 @@ interface DeviceItemProps {
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeviceItem(props: DeviceItemProps) {
|
const DeviceItem = function (props: DeviceItemProps) {
|
||||||
const className = "device-option-" + props.id;
|
const className = "device-option-" + props.id;
|
||||||
const idName = "device-" + props.device.id;
|
const idName = "device-" + props.device.id;
|
||||||
const style = makeStyles((theme: Theme) => ({
|
const style = makeStyles((theme: Theme) => ({
|
||||||
|
@ -136,7 +134,7 @@ function DeviceItem(props: DeviceItemProps) {
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface MethodItemProps {
|
interface MethodItemProps {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -145,7 +143,7 @@ interface MethodItemProps {
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MethodItem(props: MethodItemProps) {
|
const MethodItem = function (props: MethodItemProps) {
|
||||||
const className = "method-option-" + props.id;
|
const className = "method-option-" + props.id;
|
||||||
const idName = "method-" + props.method;
|
const idName = "method-" + props.method;
|
||||||
const style = makeStyles((theme: Theme) => ({
|
const style = makeStyles((theme: Theme) => ({
|
||||||
|
@ -181,4 +179,6 @@ function MethodItem(props: MethodItemProps) {
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default DefaultDeviceSelectionContainer;
|
||||||
|
|
|
@ -45,8 +45,6 @@ const OTPDial = function (props: Props) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OTPDial;
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
timeProgress: {},
|
timeProgress: {},
|
||||||
register: {
|
register: {
|
||||||
|
@ -85,3 +83,5 @@ function Icon(props: IconProps) {
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default OTPDial;
|
||||||
|
|
|
@ -10,7 +10,7 @@ interface Props {
|
||||||
handleClose: (ok: boolean) => void;
|
handleClose: (ok: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DeleteDialog(props: Props) {
|
const DeleteDialog = function (props: Props) {
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
|
@ -35,4 +35,6 @@ export default function DeleteDialog(props: Props) {
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default DeleteDialog;
|
||||||
|
|
|
@ -15,7 +15,7 @@ interface Props {
|
||||||
handleRefresh: () => void;
|
handleRefresh: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TOTPDevice(props: Props) {
|
const TOTPDevice = function (props: Props) {
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const { createSuccessNotification, createErrorNotification } = useNotifications();
|
const { createSuccessNotification, createErrorNotification } = useNotifications();
|
||||||
|
@ -133,4 +133,6 @@ export default function TOTPDevice(props: Props) {
|
||||||
</Paper>
|
</Paper>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default TOTPDevice;
|
||||||
|
|
|
@ -13,7 +13,7 @@ interface Props {
|
||||||
handleRefreshState: () => void;
|
handleRefreshState: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TOTPPanel(props: Props) {
|
const TOTPPanel = function (props: Props) {
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const [showRegisterDialog, setShowRegisterDialog] = useState<boolean>(false);
|
const [showRegisterDialog, setShowRegisterDialog] = useState<boolean>(false);
|
||||||
|
@ -68,4 +68,6 @@ export default function TOTPPanel(props: Props) {
|
||||||
</Paper>
|
</Paper>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default TOTPPanel;
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import React, { Fragment, useCallback, useEffect, useState } from "react";
|
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { Visibility } from "@mui/icons-material";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
@ -15,13 +14,13 @@ import {
|
||||||
FormControl,
|
FormControl,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
IconButton,
|
|
||||||
Link,
|
Link,
|
||||||
Radio,
|
Radio,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
Step,
|
Step,
|
||||||
StepLabel,
|
StepLabel,
|
||||||
Stepper,
|
Stepper,
|
||||||
|
Switch,
|
||||||
TextField,
|
TextField,
|
||||||
Theme,
|
Theme,
|
||||||
Typography,
|
Typography,
|
||||||
|
@ -34,9 +33,11 @@ import { QRCodeSVG } from "qrcode.react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import AppStoreBadges from "@components/AppStoreBadges";
|
import AppStoreBadges from "@components/AppStoreBadges";
|
||||||
|
import CopyButton from "@components/CopyButton";
|
||||||
|
import SuccessIcon from "@components/SuccessIcon";
|
||||||
import { GoogleAuthenticator } from "@constants/constants";
|
import { GoogleAuthenticator } from "@constants/constants";
|
||||||
import { useNotifications } from "@hooks/NotificationsContext";
|
import { useNotifications } from "@hooks/NotificationsContext";
|
||||||
import { TOTPOptions, toAlgorithmString } from "@models/TOTPConfiguration";
|
import { toAlgorithmString } from "@models/TOTPConfiguration";
|
||||||
import { completeTOTPRegister, stopTOTPRegister } from "@services/OneTimePassword";
|
import { completeTOTPRegister, stopTOTPRegister } from "@services/OneTimePassword";
|
||||||
import { getTOTPSecret } from "@services/RegisterDevice";
|
import { getTOTPSecret } from "@services/RegisterDevice";
|
||||||
import { getTOTPOptions } from "@services/UserInfoTOTPConfiguration";
|
import { getTOTPOptions } from "@services/UserInfoTOTPConfiguration";
|
||||||
|
@ -50,52 +51,64 @@ interface Props {
|
||||||
setClosed: () => void;
|
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 { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const styles = useStyles();
|
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 [activeStep, setActiveStep] = useState(0);
|
||||||
const [options, setOptions] = useState<TOTPOptions | null>(null);
|
|
||||||
const [optionAlgorithm, setOptionAlgorithm] = useState("");
|
const [secretURL, setSecretURL] = useState<string | null>(null);
|
||||||
const [optionLength, setOptionLength] = useState(6);
|
const [secretValue, setSecretValue] = useState<string | null>(null);
|
||||||
const [optionPeriod, setOptionPeriod] = useState(30);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
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 [showAdvanced, setShowAdvanced] = useState(false);
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||||
const [hasErrored, setHasErrored] = useState(false);
|
const [hasErrored, setHasErrored] = useState(false);
|
||||||
const [dialValue, setDialValue] = useState("");
|
const [dialValue, setDialValue] = useState("");
|
||||||
const [dialState, setDialState] = useState(State.Idle);
|
const [dialState, setDialState] = useState(State.Idle);
|
||||||
const [totpSecretURLHidden, setTOTPSecretURLHidden] = useState(true);
|
const [showQRCode, setShowQRCode] = useState(true);
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
|
||||||
const resetStates = () => {
|
const resetStates = useCallback(() => {
|
||||||
setOptions(null);
|
if (defaults) {
|
||||||
setOptionAlgorithm("");
|
setSelected(defaults);
|
||||||
setOptionLength(6);
|
}
|
||||||
setOptionPeriod(30);
|
|
||||||
setOptionAlgorithms([]);
|
setSecretURL(null);
|
||||||
setOptionLengths([]);
|
setSecretValue(null);
|
||||||
setOptionPeriods([]);
|
setIsLoading(false);
|
||||||
setTOTPSecretURL("");
|
|
||||||
setTOTPSecretBase32(undefined);
|
|
||||||
setTOTPIsLoading(false);
|
|
||||||
setShowAdvanced(false);
|
setShowAdvanced(false);
|
||||||
setActiveStep(0);
|
setActiveStep(0);
|
||||||
setDialValue("");
|
setDialValue("");
|
||||||
setDialState(State.Idle);
|
setDialState(State.Idle);
|
||||||
setTOTPSecretURLHidden(true);
|
setShowQRCode(true);
|
||||||
};
|
}, [defaults]);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
props.setClosed();
|
props.setClosed();
|
||||||
|
|
||||||
if (totpSecretURL !== "") {
|
if (secretURL !== "") {
|
||||||
try {
|
try {
|
||||||
await stopTOTPRegister();
|
await stopTOTPRegister();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -105,12 +118,16 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
|
|
||||||
resetStates();
|
resetStates();
|
||||||
})();
|
})();
|
||||||
}, [totpSecretURL, props]);
|
}, [props, secretURL, resetStates]);
|
||||||
|
|
||||||
const handleFinished = useCallback(() => {
|
const handleFinished = useCallback(() => {
|
||||||
props.setClosed();
|
setSuccess(true);
|
||||||
resetStates();
|
|
||||||
}, [props]);
|
setTimeout(() => {
|
||||||
|
props.setClosed();
|
||||||
|
resetStates();
|
||||||
|
}, 750);
|
||||||
|
}, [props, resetStates]);
|
||||||
|
|
||||||
const handleOnClose = () => {
|
const handleOnClose = () => {
|
||||||
if (!props.open) {
|
if (!props.open) {
|
||||||
|
@ -121,21 +138,29 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.open || activeStep !== 0 || options !== null) {
|
if (!props.open || activeStep !== 0 || defaults !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const opts = await getTOTPOptions();
|
const opts = await getTOTPOptions();
|
||||||
setOptions(opts);
|
|
||||||
setOptionAlgorithm(toAlgorithmString(opts.algorithm));
|
const decoded = {
|
||||||
setOptionAlgorithms(opts.algorithms.map((algorithm) => toAlgorithmString(algorithm)));
|
algorithm: toAlgorithmString(opts.algorithm),
|
||||||
setOptionLength(opts.length);
|
length: opts.length,
|
||||||
setOptionLengths(opts.lengths.map((length) => length.toString()));
|
period: opts.period,
|
||||||
setOptionPeriod(opts.period);
|
};
|
||||||
setOptionPeriods(opts.periods.map((period) => period.toString()));
|
|
||||||
|
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(() => {
|
const handleSetStepPrevious = useCallback(() => {
|
||||||
if (activeStep === 0) {
|
if (activeStep === 0) {
|
||||||
|
@ -143,7 +168,9 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
setShowAdvanced(false);
|
setShowAdvanced(false);
|
||||||
setActiveStep((prevState) => (prevState -= 1));
|
setActiveStep((prevState) => {
|
||||||
|
return prevState - 1;
|
||||||
|
});
|
||||||
}, [activeStep]);
|
}, [activeStep]);
|
||||||
|
|
||||||
const handleSetStepNext = useCallback(() => {
|
const handleSetStepNext = useCallback(() => {
|
||||||
|
@ -152,7 +179,9 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
setShowAdvanced(false);
|
setShowAdvanced(false);
|
||||||
setActiveStep((prevState) => (prevState += 1));
|
setActiveStep((prevState) => {
|
||||||
|
return prevState + 1;
|
||||||
|
});
|
||||||
}, [activeStep]);
|
}, [activeStep]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -161,12 +190,12 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
setTOTPIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const secret = await getTOTPSecret(optionAlgorithm, optionLength, optionPeriod);
|
const secret = await getTOTPSecret(selected.algorithm, selected.length, selected.period);
|
||||||
setTOTPSecretURL(secret.otpauth_url);
|
setSecretURL(secret.otpauth_url);
|
||||||
setTOTPSecretBase32(secret.base32_secret);
|
setSecretValue(secret.base32_secret);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
if ((err as Error).message.includes("Request failed with status code 403")) {
|
if ((err as Error).message.includes("Request failed with status code 403")) {
|
||||||
|
@ -183,12 +212,12 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
setHasErrored(true);
|
setHasErrored(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTOTPIsLoading(false);
|
setIsLoading(false);
|
||||||
})();
|
})();
|
||||||
}, [activeStep, createErrorNotification, optionAlgorithm, optionLength, optionPeriod, props.open, translate]);
|
}, [activeStep, createErrorNotification, selected, props.open, translate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.open || activeStep !== 2 || dialValue.length !== optionLength) {
|
if (!props.open || activeStep !== 2 || dialState === State.InProgress || dialValue.length !== selected.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,63 +236,49 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
setDialState(State.Failure);
|
setDialState(State.Failure);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [activeStep, dialValue, dialValue.length, optionLength, props.open]);
|
}, [activeStep, dialState, dialValue, dialValue.length, handleFinished, props.open, selected.length]);
|
||||||
|
|
||||||
const toggleAdvanced = () => {
|
const toggleAdvanced = () => {
|
||||||
setShowAdvanced((prevState) => !prevState);
|
setShowAdvanced((prevState) => !prevState);
|
||||||
};
|
};
|
||||||
|
|
||||||
const advanced =
|
const advanced =
|
||||||
options !== null &&
|
defaults !== null &&
|
||||||
(optionAlgorithms.length !== 1 || optionAlgorithms.length !== 1 || optionPeriods.length !== 1);
|
(available.algorithms.length !== 1 || available.lengths.length !== 1 || available.periods.length !== 1);
|
||||||
|
|
||||||
const hideAdvanced =
|
const disableAdvanced =
|
||||||
options === null || (optionAlgorithms.length <= 1 && optionPeriods.length <= 1 && optionLengths.length <= 1);
|
defaults === null ||
|
||||||
|
(available.algorithms.length <= 1 && available.lengths.length <= 1 && available.periods.length <= 1);
|
||||||
|
|
||||||
const hideAlgorithms = advanced && optionAlgorithms.length <= 1;
|
const hideAlgorithms = advanced && available.algorithms.length <= 1;
|
||||||
const hideLengths = advanced && optionLengths.length <= 1;
|
const hideLengths = advanced && available.lengths.length <= 1;
|
||||||
const hidePeriods = advanced && optionPeriods.length <= 1;
|
const hidePeriods = advanced && available.periods.length <= 1;
|
||||||
const qrcodeFuzzyStyle = totpIsLoading || hasErrored ? styles.fuzzy : undefined;
|
const qrcodeFuzzyStyle = isLoading || 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderStep(step: number) {
|
function renderStep(step: number) {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case 0:
|
case 0:
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{options === null ? (
|
{defaults === null ? (
|
||||||
<Grid xs={12}>
|
<Grid xs={12} my={3}>
|
||||||
<Typography>Loading...</Typography>
|
<Typography>Loading...</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
) : (
|
) : (
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid xs={12}>
|
<Grid xs={12} my={3}>
|
||||||
<Typography>{translate("To begin select next")}</Typography>
|
<Typography>{translate("To begin select next")}</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid xs={12} hidden={hideAdvanced}>
|
<Grid xs={12} hidden={disableAdvanced}>
|
||||||
<Button variant={"outlined"} color={"warning"} onClick={toggleAdvanced}>
|
<FormControlLabel
|
||||||
{showAdvanced ? translate("Hide Advanced") : translate("Show Advanced")}
|
disabled={disableAdvanced}
|
||||||
</Button>
|
control={<Switch checked={showAdvanced} onChange={toggleAdvanced} />}
|
||||||
|
label={translate("Advanced")}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid
|
<Grid
|
||||||
xs={12}
|
xs={12}
|
||||||
hidden={hideAdvanced || !showAdvanced}
|
hidden={disableAdvanced || !showAdvanced}
|
||||||
justifyContent={"center"}
|
justifyContent={"center"}
|
||||||
alignItems={"center"}
|
alignItems={"center"}
|
||||||
>
|
>
|
||||||
|
@ -274,17 +289,23 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
row
|
row
|
||||||
aria-labelledby={"lbl-adv-algorithms"}
|
aria-labelledby={"lbl-adv-algorithms"}
|
||||||
value={optionAlgorithm}
|
value={selected.algorithm}
|
||||||
hidden={hideAlgorithms}
|
hidden={hideAlgorithms}
|
||||||
style={{
|
style={{
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
onChange={(e, value) => {
|
onChange={(e, value) => {
|
||||||
setOptionAlgorithm(value);
|
setSelected((prevState) => {
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
algorithm: value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{optionAlgorithms.map((algorithm) => (
|
{available.algorithms.map((algorithm) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={algorithm}
|
key={algorithm}
|
||||||
value={algorithm}
|
value={algorithm}
|
||||||
|
@ -299,22 +320,28 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
row
|
row
|
||||||
aria-labelledby={"lbl-adv-lengths"}
|
aria-labelledby={"lbl-adv-lengths"}
|
||||||
value={optionLength.toString()}
|
value={selected.length.toString()}
|
||||||
hidden={hideLengths}
|
hidden={hideLengths}
|
||||||
style={{
|
style={{
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
onChange={(e, value) => {
|
onChange={(e, value) => {
|
||||||
setOptionLength(parseInt(value));
|
setSelected((prevState) => {
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
length: parseInt(value),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{optionLengths.map((length) => (
|
{available.lengths.map((length) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={length}
|
key={length.toString()}
|
||||||
value={length}
|
value={length.toString()}
|
||||||
control={<Radio />}
|
control={<Radio />}
|
||||||
label={length}
|
label={length.toString()}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
@ -324,22 +351,28 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
row
|
row
|
||||||
aria-labelledby={"lbl-adv-periods"}
|
aria-labelledby={"lbl-adv-periods"}
|
||||||
value={optionPeriod.toString()}
|
value={selected.period.toString()}
|
||||||
hidden={hidePeriods}
|
hidden={hidePeriods}
|
||||||
style={{
|
style={{
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
onChange={(e, value) => {
|
onChange={(e, value) => {
|
||||||
setOptionPeriod(parseInt(value));
|
setSelected((prevState) => {
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
period: parseInt(value),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{optionPeriods.map((period) => (
|
{available.periods.map((period) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={period}
|
key={period.toString()}
|
||||||
value={period}
|
value={period.toString()}
|
||||||
control={<Radio />}
|
control={<Radio />}
|
||||||
label={period}
|
label={period.toString()}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
@ -352,52 +385,63 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
case 1:
|
case 1:
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<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)}>
|
<Box className={classnames(qrcodeFuzzyStyle, styles.qrcodeContainer)}>
|
||||||
<Link href={totpSecretURL} underline="hover">
|
{secretURL !== null ? (
|
||||||
<QRCodeSVG value={totpSecretURL} className={styles.qrcode} size={150} />
|
<Link href={secretURL} underline="hover">
|
||||||
{!hasErrored && totpIsLoading ? (
|
<QRCodeSVG value={secretURL} className={styles.qrcode} size={200} />
|
||||||
<CircularProgress className={styles.loader} size={128} />
|
{!hasErrored && isLoading ? (
|
||||||
) : null}
|
<CircularProgress className={styles.loader} size={128} />
|
||||||
{hasErrored ? (
|
) : null}
|
||||||
<FontAwesomeIcon className={styles.failureIcon} icon={faTimesCircle} />
|
{hasErrored ? (
|
||||||
) : null}
|
<FontAwesomeIcon className={styles.failureIcon} icon={faTimesCircle} />
|
||||||
</Link>
|
) : null}
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid xs={12}>
|
<Grid xs={12} hidden={showQRCode}>
|
||||||
<Grid container spacing={2} justifyContent={"center"}>
|
<Grid container spacing={2} justifyContent={"center"}>
|
||||||
<Grid xs={2}>
|
<Grid xs={4}>
|
||||||
<IconButton
|
<CopyButton
|
||||||
color="primary"
|
tooltip={translate("Click to Copy")}
|
||||||
onClick={() => {
|
value={secretURL}
|
||||||
setTOTPSecretURLHidden((value) => !value);
|
childrenCopied={translate("Copied")}
|
||||||
}}
|
fullWidth={true}
|
||||||
size="large"
|
|
||||||
>
|
>
|
||||||
<Visibility />
|
{translate("OTP URL")}
|
||||||
</IconButton>
|
</CopyButton>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid xs={2}>
|
<Grid xs={4}>
|
||||||
{totpSecretBase32
|
<CopyButton
|
||||||
? SecretButton(
|
tooltip={translate("Click to Copy")}
|
||||||
totpSecretBase32,
|
value={secretValue}
|
||||||
translate("OTP Secret copied to clipboard"),
|
childrenCopied={translate("Copied")}
|
||||||
faKey,
|
fullWidth={true}
|
||||||
)
|
>
|
||||||
: null}
|
{translate("Secret")}
|
||||||
|
</CopyButton>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid xs={2}>
|
<Grid xs={12}>
|
||||||
{totpSecretURL !== ""
|
|
||||||
? SecretButton(totpSecretURL, translate("OTP URL copied to clipboard"), faCopy)
|
|
||||||
: null}
|
|
||||||
</Grid>
|
|
||||||
<Grid xs={12} hidden={totpSecretURLHidden || totpSecretURL === ""}>
|
|
||||||
<TextField
|
<TextField
|
||||||
id="secret-url"
|
id="secret-url"
|
||||||
label={translate("Secret")}
|
label={translate("Secret")}
|
||||||
className={styles.secret}
|
className={styles.secret}
|
||||||
value={totpSecretURL}
|
value={secretURL === null ? "" : secretURL}
|
||||||
multiline={true}
|
multiline={true}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
|
@ -412,7 +456,7 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
{translate("Need Google Authenticator?")}
|
{translate("Need Google Authenticator?")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<AppStoreBadges
|
<AppStoreBadges
|
||||||
iconSize={128}
|
iconSize={110}
|
||||||
targetBlank
|
targetBlank
|
||||||
className={styles.googleAuthenticatorBadges}
|
className={styles.googleAuthenticatorBadges}
|
||||||
googlePlayLink={GoogleAuthenticator.googlePlay}
|
googlePlayLink={GoogleAuthenticator.googlePlay}
|
||||||
|
@ -426,13 +470,19 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Grid xs={12} paddingY={4}>
|
<Grid xs={12} paddingY={4}>
|
||||||
<OTPDial
|
{success ? (
|
||||||
passcode={dialValue}
|
<Box className={styles.success}>
|
||||||
state={dialState}
|
<SuccessIcon />
|
||||||
digits={optionLength}
|
</Box>
|
||||||
period={optionPeriod}
|
) : (
|
||||||
onChange={setDialValue}
|
<OTPDial
|
||||||
/>
|
passcode={dialValue}
|
||||||
|
state={dialState}
|
||||||
|
digits={selected.length}
|
||||||
|
period={selected.period}
|
||||||
|
onChange={setDialValue}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
@ -482,7 +532,7 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
qrcode: {
|
qrcode: {
|
||||||
|
@ -520,4 +570,10 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||||
color: red[400],
|
color: red[400],
|
||||||
fontSize: "128px",
|
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 {}
|
interface Props {}
|
||||||
|
|
||||||
export default function TwoFactorAuthSettings(props: Props) {
|
const TwoFactorAuthSettings = function (props: Props) {
|
||||||
const [refreshState, setRefreshState] = useState(0);
|
const [refreshState, setRefreshState] = useState(0);
|
||||||
const { createErrorNotification } = useNotifications();
|
const { createErrorNotification } = useNotifications();
|
||||||
const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoPOST();
|
const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoPOST();
|
||||||
|
@ -78,4 +78,6 @@ export default function TwoFactorAuthSettings(props: Props) {
|
||||||
</Grid>
|
</Grid>
|
||||||
</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 {
|
import {
|
||||||
Button,
|
Button,
|
||||||
CircularProgress,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Divider,
|
Divider,
|
||||||
Tooltip,
|
Paper,
|
||||||
|
PaperProps,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import Grid from "@mui/material/Unstable_Grid2";
|
import Grid from "@mui/material/Unstable_Grid2";
|
||||||
|
import Draggable from "react-draggable";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import CopyButton from "@components/CopyButton";
|
||||||
import { WebAuthnDevice, toTransportName } from "@models/WebAuthn";
|
import { WebAuthnDevice, toTransportName } from "@models/WebAuthn";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -24,12 +25,19 @@ interface Props {
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebAuthnDeviceDetailsDialog(props: Props) {
|
const WebAuthnDeviceDetailsDialog = function (props: Props) {
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onClose={props.handleClose}>
|
<Dialog
|
||||||
<DialogTitle>{translate("WebAuthn Credential Details")}</DialogTitle>
|
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>
|
<DialogContent>
|
||||||
<DialogContentText sx={{ mb: 3 }}>
|
<DialogContentText sx={{ mb: 3 }}>
|
||||||
{translate("Extended WebAuthn credential information for security key", {
|
{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" } }}>
|
<Grid md={3} sx={{ display: { xs: "none", md: "block" } }}>
|
||||||
<Fragment />
|
<Fragment />
|
||||||
</Grid>
|
</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}>
|
<Grid xs={12}>
|
||||||
<Divider />
|
<Divider />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -128,11 +130,29 @@ export default function WebAuthnDeviceDetailsDialog(props: Props) {
|
||||||
</Grid>
|
</Grid>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<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>
|
<Button onClick={props.handleClose}>{translate("Close")}</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface PropertyTextProps {
|
interface PropertyTextProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -140,49 +160,6 @@ interface PropertyTextProps {
|
||||||
xs?: number;
|
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) {
|
function PropertyText(props: PropertyTextProps) {
|
||||||
return (
|
return (
|
||||||
<Grid xs={props.xs !== undefined ? props.xs : 12}>
|
<Grid xs={props.xs !== undefined ? props.xs : 12}>
|
||||||
|
@ -193,3 +170,13 @@ function PropertyText(props: PropertyTextProps) {
|
||||||
</Grid>
|
</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;
|
device: WebAuthnDevice;
|
||||||
handleClose: (ok: boolean, name: string) => void;
|
handleClose: (ok: boolean, name: string) => void;
|
||||||
}
|
}
|
||||||
|
const WebAuthnDeviceEditDialog = function (props: Props) {
|
||||||
export default function WebAuthnDeviceEditDialog(props: Props) {
|
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const [deviceName, setName] = useState("");
|
const [deviceName, setName] = useState("");
|
||||||
|
@ -36,12 +35,14 @@ export default function WebAuthnDeviceEditDialog(props: Props) {
|
||||||
<Dialog open={props.open} onClose={handleCancel}>
|
<Dialog open={props.open} onClose={handleCancel}>
|
||||||
<DialogTitle>{translate("Edit WebAuthn Credential")}</DialogTitle>
|
<DialogTitle>{translate("Edit WebAuthn Credential")}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>{translate("Enter a new name for this WebAuthn credential")}</DialogContentText>
|
<DialogContentText>
|
||||||
|
{translate("Enter a new description for this WebAuthn credential")}
|
||||||
|
</DialogContentText>
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
inputRef={nameRef}
|
inputRef={nameRef}
|
||||||
id="name-textfield"
|
id="name-textfield"
|
||||||
label={translate("Name")}
|
label={translate("Description")}
|
||||||
variant="standard"
|
variant="standard"
|
||||||
required
|
required
|
||||||
value={deviceName}
|
value={deviceName}
|
||||||
|
@ -68,4 +69,6 @@ export default function WebAuthnDeviceEditDialog(props: Props) {
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default WebAuthnDeviceEditDialog;
|
||||||
|
|
|
@ -21,8 +21,7 @@ interface Props {
|
||||||
device: WebAuthnDevice;
|
device: WebAuthnDevice;
|
||||||
handleEdit: () => void;
|
handleEdit: () => void;
|
||||||
}
|
}
|
||||||
|
const WebAuthnDeviceItem = function (props: Props) {
|
||||||
export default function WebAuthnDeviceItem(props: Props) {
|
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const { createSuccessNotification, createErrorNotification } = useNotifications();
|
const { createSuccessNotification, createErrorNotification } = useNotifications();
|
||||||
|
@ -197,4 +196,6 @@ export default function WebAuthnDeviceItem(props: Props) {
|
||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default WebAuthnDeviceItem;
|
||||||
|
|
|
@ -8,6 +8,8 @@ import {
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
Paper,
|
||||||
|
PaperProps,
|
||||||
Step,
|
Step,
|
||||||
StepLabel,
|
StepLabel,
|
||||||
Stepper,
|
Stepper,
|
||||||
|
@ -18,6 +20,7 @@ import {
|
||||||
import Grid from "@mui/material/Unstable_Grid2";
|
import Grid from "@mui/material/Unstable_Grid2";
|
||||||
import makeStyles from "@mui/styles/makeStyles";
|
import makeStyles from "@mui/styles/makeStyles";
|
||||||
import { PublicKeyCredentialCreationOptionsJSON } from "@simplewebauthn/typescript-types";
|
import { PublicKeyCredentialCreationOptionsJSON } from "@simplewebauthn/typescript-types";
|
||||||
|
import Draggable from "react-draggable";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import InformationIcon from "@components/InformationIcon";
|
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;
|
export default WebAuthnDeviceRegisterDialog;
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
|
|
@ -12,8 +12,7 @@ interface Props {
|
||||||
devices: WebAuthnDevice[] | undefined;
|
devices: WebAuthnDevice[] | undefined;
|
||||||
handleRefreshState: () => void;
|
handleRefreshState: () => void;
|
||||||
}
|
}
|
||||||
|
const WebAuthnDevicesPanel = function (props: Props) {
|
||||||
export default function WebAuthnDevicesPanel(props: Props) {
|
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const [showRegisterDialog, setShowRegisterDialog] = useState<boolean>(false);
|
const [showRegisterDialog, setShowRegisterDialog] = useState<boolean>(false);
|
||||||
|
@ -63,4 +62,6 @@ export default function WebAuthnDevicesPanel(props: Props) {
|
||||||
</Paper>
|
</Paper>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default WebAuthnDevicesPanel;
|
||||||
|
|
|
@ -10,7 +10,7 @@ interface Props {
|
||||||
handleRefreshState: () => void;
|
handleRefreshState: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebAuthnDevicesStack(props: Props) {
|
const WebAuthnDevicesStack = function (props: Props) {
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{props.devices.map((x, idx) => (
|
{props.devices.map((x, idx) => (
|
||||||
|
@ -18,4 +18,6 @@ export default function WebAuthnDevicesStack(props: Props) {
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default WebAuthnDevicesStack;
|
||||||
|
|
Loading…
Reference in New Issue