refactor: simplified

Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
pull/5053/head
James Elliott 2023-04-22 14:19:57 +10:00
parent 5fc0ac98f0
commit f06bdec683
No known key found for this signature in database
GPG Key ID: 0F1C4A096E857E49
32 changed files with 441 additions and 322 deletions

View File

@ -13,20 +13,20 @@
"Clone Warning": "Clone Warning", "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",

View File

@ -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.3.1", "react-i18next": "12.3.1",
"react-loading": "2.0.3", "react-loading": "2.0.3",
"react-router-dom": "6.11.2", "react-router-dom": "6.11.2",

View File

@ -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.3.1 specifier: 12.3.1
version: 12.3.1(i18next@22.5.0)(react-dom@18.2.0)(react@18.2.0) version: 12.3.1(i18next@22.5.0)(react-dom@18.2.0)(react@18.2.0)
@ -4370,6 +4373,7 @@ packages:
/eslint-config-prettier@8.8.0(eslint@8.41.0): /eslint-config-prettier@8.8.0(eslint@8.41.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:
@ -6489,6 +6493,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.3.1(i18next@22.5.0)(react-dom@18.2.0)(react@18.2.0): /react-i18next@12.3.1(i18next@22.5.0)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-5v8E2XjZDFzK7K87eSwC7AJcAkcLt5xYZ4+yTPDAW1i7C93oOY1dnr4BaQM7un4Hm+GmghuiPvevWwlca5PwDA==} resolution: {integrity: sha512-5v8E2XjZDFzK7K87eSwC7AJcAkcLt5xYZ4+yTPDAW1i7C93oOY1dnr4BaQM7un4Hm+GmghuiPvevWwlca5PwDA==}
peerDependencies: peerDependencies:
@ -7162,6 +7178,7 @@ packages:
/ts-node@10.9.1(@types/node@20.2.5)(typescript@5.0.4): /ts-node@10.9.1(@types/node@20.2.5)(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'
@ -7193,6 +7210,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:
@ -7334,6 +7352,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:
@ -7454,6 +7473,7 @@ packages:
/vite@3.2.5(@types/node@18.16.5): /vite@3.2.5(@types/node@18.16.5):
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: '*'
@ -7487,6 +7507,7 @@ packages:
/vite@4.3.9(@types/node@20.2.5): /vite@4.3.9(@types/node@20.2.5):
resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==}
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: '*'
@ -7536,6 +7557,7 @@ packages:
/vitest@0.31.1(happy-dom@9.20.3): /vitest@0.31.1(happy-dom@9.20.3):
resolution: {integrity: sha512-/dOoOgzoFk/5pTvg1E65WVaobknWREN15+HF+0ucudo3dDG/vCZoXTQrjIfEaWvQXmqScwkRodrTbM/ScMpRcQ==} resolution: {integrity: sha512-/dOoOgzoFk/5pTvg1E65WVaobknWREN15+HF+0ucudo3dDG/vCZoXTQrjIfEaWvQXmqScwkRodrTbM/ScMpRcQ==}
engines: {node: '>=v14.18.0'} engines: {node: '>=v14.18.0'}
hasBin: true
peerDependencies: peerDependencies:
'@edge-runtime/vm': '*' '@edge-runtime/vm': '*'
'@vitest/browser': '*' '@vitest/browser': '*'

View File

@ -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;

View File

@ -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;

View File

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

View File

@ -27,8 +27,6 @@ const FixedTextField = function (props: TextFieldProps) {
); );
}; };
export default FixedTextField;
const useStyles = makeStyles((theme: Theme) => ({ 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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>
);
};
*/

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -1,21 +1,22 @@
import React, { Fragment, useState } from "react"; import React, { Fragment } from "react";
import { Check, ContentCopy } from "@mui/icons-material";
import { 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;

View File

@ -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;

View File

@ -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;

View File

@ -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) => ({

View File

@ -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;

View File

@ -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;