diff --git a/internal/server/locales/en/settings.json b/internal/server/locales/en/settings.json index ff550c74b..14737f718 100644 --- a/internal/server/locales/en/settings.json +++ b/internal/server/locales/en/settings.json @@ -13,20 +13,20 @@ "Clone Warning": "Clone Warning", "Created": "Created", "Delete": "Delete", + "Description": "Description", "Details": "Details", "Display extended information for this WebAuthn credential": "Display extended information for this WebAuthn credential", "Edit": "Edit", "Edit information for this WebAuthn credential": "Edit information for this WebAuthn credential", "Edit WebAuthn Credential": "Edit WebAuthn Credential", "Enabled": "Enabled", - "Enter a new name for this WebAuthn credential": "Enter a new name for this WebAuthn credential:", + "Enter a new description for this WebAuthn credential": "Enter a new description for this WebAuthn credential:", "Enter a description for this credential": "Enter a description for this credential", "Extended WebAuthn credential information for security key": "Extended WebAuthn credential information for security key {{description}}", "Identifier": "Identifier", "Last Used": "Last Used", "Last Used when": "Last Used {{when, datetime}}", "Manage your security keys": "Manage your security keys", - "Name": "Name", "No": "No", "No WebAuthn Credentials have been registered. If you'd like to register one click add.": "No WebAuthn Credentials have been registered. If you'd like to register one click add.", "Overview": "Overview", diff --git a/web/package.json b/web/package.json index cae8d05d0..7400e0156 100644 --- a/web/package.json +++ b/web/package.json @@ -36,6 +36,7 @@ "qrcode.react": "3.1.0", "react": "18.2.0", "react-dom": "18.2.0", + "react-draggable": "^4.4.5", "react-i18next": "12.2.0", "react-loading": "2.0.3", "react-router-dom": "6.10.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index d82caa4f5..3be32b9f2 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -64,6 +64,9 @@ dependencies: react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) + react-draggable: + specifier: ^4.4.5 + version: 4.4.5(react-dom@18.2.0)(react@18.2.0) react-i18next: specifier: 12.2.0 version: 12.2.0(i18next@22.4.15)(react-dom@18.2.0)(react@18.2.0) @@ -4370,6 +4373,7 @@ packages: /eslint-config-prettier@8.8.0(eslint@8.38.0): resolution: {integrity: sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==} + hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: @@ -6485,6 +6489,18 @@ packages: react: 18.2.0 scheduler: 0.23.0 + /react-draggable@4.4.5(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==} + peerDependencies: + react: '>= 16.3.0 || 18' + react-dom: '>= 16.3.0' + dependencies: + clsx: 1.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-i18next@12.2.0(i18next@22.4.15)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-5XeVgSygaGfyFmDd2WcXvINRw2WEC1XviW1LXY/xLOEMzsCFRwKqfnHN+hUjla8ZipbVJR27GCMSuTr0BhBBBQ==} peerDependencies: @@ -7165,6 +7181,7 @@ packages: /ts-node@10.9.1(@types/node@18.15.13)(typescript@5.0.4): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true peerDependencies: '@swc/core': '>=1.2.50' '@swc/wasm': '>=1.2.50' @@ -7196,6 +7213,7 @@ packages: /tsconfck@2.1.1(typescript@5.0.4): resolution: {integrity: sha512-ZPCkJBKASZBmBUNqGHmRhdhM8pJYDdOXp4nRgj/O0JwUwsMq50lCDRQP/M5GBNAA0elPrq4gAeu4dkaVCuKWww==} engines: {node: ^14.13.1 || ^16 || >=18} + hasBin: true peerDependencies: typescript: ^4.3.5 || ^5.0.0 peerDependenciesMeta: @@ -7337,6 +7355,7 @@ packages: /update-browserslist-db@1.0.10(browserslist@4.21.5): resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} + hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: @@ -7456,6 +7475,7 @@ packages: /vite@3.2.5(@types/node@18.15.13): resolution: {integrity: sha512-4mVEpXpSOgrssFZAOmGIr85wPHKvaDAcXqxVxVRZhljkJOMZi1ibLibzjLHzJvcok8BMguLc7g1W6W/GqZbLdQ==} engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true peerDependencies: '@types/node': '>= 14' less: '*' @@ -7489,6 +7509,7 @@ packages: /vite@4.3.1(@types/node@18.15.13): resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==} engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true peerDependencies: '@types/node': '>= 14' less: '*' @@ -7538,6 +7559,7 @@ packages: /vitest@0.30.1(happy-dom@9.8.4): resolution: {integrity: sha512-y35WTrSTlTxfMLttgQk4rHcaDkbHQwDP++SNwPb+7H8yb13Q3cu2EixrtHzF27iZ8v0XCciSsLg00RkPAzB/aA==} engines: {node: '>=v14.18.0'} + hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@vitest/browser': '*' diff --git a/web/src/components/Brand.tsx b/web/src/components/Brand.tsx index 797c7f363..0ca13b0b7 100644 --- a/web/src/components/Brand.tsx +++ b/web/src/components/Brand.tsx @@ -37,11 +37,11 @@ const Brand = function (props: Props) { ); }; -export default Brand; - const useStyles = makeStyles((theme: Theme) => ({ links: { fontSize: "0.7em", color: grey[500], }, })); + +export default Brand; diff --git a/web/src/components/ComponentOrLoading.tsx b/web/src/components/ComponentOrLoading.tsx index 13e62647f..2ff6948cf 100644 --- a/web/src/components/ComponentOrLoading.tsx +++ b/web/src/components/ComponentOrLoading.tsx @@ -8,7 +8,7 @@ export interface Props { children: ReactNode; } -function ComponentOrLoading(props: Props) { +const ComponentOrLoading = function (props: Props) { return (
@@ -17,6 +17,6 @@ function ComponentOrLoading(props: Props) { {props.ready ? props.children : null} ); -} +}; export default ComponentOrLoading; diff --git a/web/src/components/CopyButton.tsx b/web/src/components/CopyButton.tsx new file mode 100644 index 000000000..84d2c9d70 --- /dev/null +++ b/web/src/components/CopyButton.tsx @@ -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 === "" ? ( + + ) : ( + + + + ); +}; + +export default CopyButton; diff --git a/web/src/components/FixedTextField.tsx b/web/src/components/FixedTextField.tsx index f877ccab9..5ca6bb1e6 100644 --- a/web/src/components/FixedTextField.tsx +++ b/web/src/components/FixedTextField.tsx @@ -27,8 +27,6 @@ const FixedTextField = function (props: TextFieldProps) { ); }; -export default FixedTextField; - const useStyles = makeStyles((theme: Theme) => ({ label: { backgroundColor: theme.palette.background.default, @@ -36,3 +34,5 @@ const useStyles = makeStyles((theme: Theme) => ({ paddingRight: theme.spacing(0.1), }, })); + +export default FixedTextField; diff --git a/web/src/components/TypographyWithTooltip.tsx b/web/src/components/TypographyWithTooltip.tsx index 1a876bf8a..340a261c7 100644 --- a/web/src/components/TypographyWithTooltip.tsx +++ b/web/src/components/TypographyWithTooltip.tsx @@ -11,7 +11,7 @@ export interface Props { tooltip?: string; } -export default function TypographyWithTooltip(props: Props): JSX.Element { +const TypographyWithTooltip = function (props: Props): JSX.Element { return ( {props.tooltip ? ( @@ -23,4 +23,6 @@ export default function TypographyWithTooltip(props: Props): JSX.Element { )} ); -} +}; + +export default TypographyWithTooltip; diff --git a/web/src/components/WebAuthnRegisterIcon.tsx b/web/src/components/WebAuthnRegisterIcon.tsx index 76ab9f502..add960937 100644 --- a/web/src/components/WebAuthnRegisterIcon.tsx +++ b/web/src/components/WebAuthnRegisterIcon.tsx @@ -12,7 +12,7 @@ interface Props { timeout: number; } -export default function WebAuthnRegisterIcon(props: Props) { +const WebAuthnRegisterIcon = function (props: Props) { const theme = useTheme(); const [timerPercent, triggerTimer] = useTimer(props.timeout); @@ -36,4 +36,6 @@ export default function WebAuthnRegisterIcon(props: Props) { ); -} +}; + +export default WebAuthnRegisterIcon; diff --git a/web/src/components/WebAuthnTryIcon.tsx b/web/src/components/WebAuthnTryIcon.tsx index 42bacacf0..be243a24e 100644 --- a/web/src/components/WebAuthnTryIcon.tsx +++ b/web/src/components/WebAuthnTryIcon.tsx @@ -15,7 +15,7 @@ interface Props { webauthnTouchState: WebAuthnTouchState; } -export default function WebAuthnTryIcon(props: Props) { +const WebAuthnTryIcon = function (props: Props) { const touchTimeout = 30; const theme = useTheme(); const [timerPercent, triggerTimer, clearTimer] = useTimer(touchTimeout * 1000 - 500); @@ -65,4 +65,6 @@ export default function WebAuthnTryIcon(props: Props) { {failure} ); -} +}; + +export default WebAuthnTryIcon; diff --git a/web/src/hooks/NotificationsContext.ts b/web/src/hooks/NotificationsContext.ts index 06d512415..4d470432a 100644 --- a/web/src/hooks/NotificationsContext.ts +++ b/web/src/hooks/NotificationsContext.ts @@ -15,8 +15,6 @@ interface NotificationContextProps { const NotificationsContext = createContext({ notification: null, setNotification: () => {} }); -export default NotificationsContext; - export function useNotifications() { let useNotificationsProps = useContext(NotificationsContext); @@ -47,3 +45,5 @@ export function useNotifications() { isActive, }; } + +export default NotificationsContext; diff --git a/web/src/layouts/LoginLayout.tsx b/web/src/layouts/LoginLayout.tsx index c6320f245..80ea57034 100644 --- a/web/src/layouts/LoginLayout.tsx +++ b/web/src/layouts/LoginLayout.tsx @@ -108,8 +108,6 @@ const LoginLayout = function (props: Props) { ); }; -export default LoginLayout; - const useStyles = makeStyles((theme: Theme) => ({ root: { minHeight: "90vh", @@ -132,3 +130,5 @@ const useStyles = makeStyles((theme: Theme) => ({ paddingBottom: theme.spacing(), }, })); + +export default LoginLayout; diff --git a/web/src/layouts/SettingsLayout.tsx b/web/src/layouts/SettingsLayout.tsx index b2d7ed6cf..2f9fbea67 100644 --- a/web/src/layouts/SettingsLayout.tsx +++ b/web/src/layouts/SettingsLayout.tsx @@ -130,8 +130,6 @@ const SettingsLayout = function (props: Props) { ); }; -export default SettingsLayout; - interface NavItem { keyname?: string; text: string; @@ -172,61 +170,4 @@ const DrawerNavItem = function (props: NavItem) { ); }; -/* -interface SettingsMenuProps { - isXL: boolean; - handleClickMenuItem: () => void; - handleToggleDrawer: (open: boolean) => (event: SyntheticEvent) => void; -} - -const SettingsMenu = function (props: SettingsMenuProps) { - const { t: translate } = useTranslation("settings"); - const navigate = useRouterNavigate(); - - return ( - - {props.isXL ? null : ( - - - - - - - )} - - } - onClick={props.handleClickMenuItem} - /> - } - onClick={props.handleClickMenuItem} - /> - { - navigate(IndexRoute); - }} - > - - - - - - - - - - ); -}; - - */ +export default SettingsLayout; diff --git a/web/src/views/LoadingPage/BaseLoadingPage.tsx b/web/src/views/LoadingPage/BaseLoadingPage.tsx index cf53cb917..7ef83356e 100644 --- a/web/src/views/LoadingPage/BaseLoadingPage.tsx +++ b/web/src/views/LoadingPage/BaseLoadingPage.tsx @@ -22,8 +22,6 @@ const BaseLoadingPage = function (props: Props) { ); }; -export default BaseLoadingPage; - const useStyles = makeStyles((theme: Theme) => ({ gridOuter: { alignItems: "center", @@ -35,3 +33,5 @@ const useStyles = makeStyles((theme: Theme) => ({ display: "inline-block", }, })); + +export default BaseLoadingPage; diff --git a/web/src/views/LoginPortal/Authenticated.tsx b/web/src/views/LoginPortal/Authenticated.tsx index 78ce21f4a..c841a4945 100644 --- a/web/src/views/LoginPortal/Authenticated.tsx +++ b/web/src/views/LoginPortal/Authenticated.tsx @@ -21,11 +21,11 @@ const Authenticated = function () { ); }; -export default Authenticated; - const useStyles = makeStyles((theme: Theme) => ({ iconContainer: { marginBottom: theme.spacing(2), flex: "0 0 100%", }, })); + +export default Authenticated; diff --git a/web/src/views/LoginPortal/AuthenticatedView/AuthenticatedView.tsx b/web/src/views/LoginPortal/AuthenticatedView/AuthenticatedView.tsx index d17f570e1..3930f304b 100644 --- a/web/src/views/LoginPortal/AuthenticatedView/AuthenticatedView.tsx +++ b/web/src/views/LoginPortal/AuthenticatedView/AuthenticatedView.tsx @@ -40,8 +40,6 @@ const AuthenticatedView = function (props: Props) { ); }; -export default AuthenticatedView; - const useStyles = makeStyles((theme: Theme) => ({ mainContainer: { border: "1px solid #d6d6d6", @@ -51,3 +49,5 @@ const useStyles = makeStyles((theme: Theme) => ({ marginBottom: theme.spacing(2), }, })); + +export default AuthenticatedView; diff --git a/web/src/views/LoginPortal/ConsentView/ConsentView.tsx b/web/src/views/LoginPortal/ConsentView/ConsentView.tsx index 8325bf59e..3956b4d47 100644 --- a/web/src/views/LoginPortal/ConsentView/ConsentView.tsx +++ b/web/src/views/LoginPortal/ConsentView/ConsentView.tsx @@ -276,8 +276,6 @@ const useStyles = makeStyles((theme: Theme) => ({ preConfigure: {}, })); -export default ConsentView; - interface ComponentOrLoadingProps { ready: boolean; @@ -294,3 +292,5 @@ function ComponentOrLoading(props: ComponentOrLoadingProps) { ); } + +export default ConsentView; diff --git a/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx b/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx index 6b3322bdf..4da9e5805 100644 --- a/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx +++ b/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx @@ -227,8 +227,6 @@ const FirstFactorForm = function (props: Props) { ); }; -export default FirstFactorForm; - const useStyles = makeStyles((theme: Theme) => ({ actionRow: { display: "flex", @@ -248,3 +246,5 @@ const useStyles = makeStyles((theme: Theme) => ({ justifyContent: "flex-end", }, })); + +export default FirstFactorForm; diff --git a/web/src/views/LoginPortal/LoginPortal.tsx b/web/src/views/LoginPortal/LoginPortal.tsx index e85c460ba..ed710ee00 100644 --- a/web/src/views/LoginPortal/LoginPortal.tsx +++ b/web/src/views/LoginPortal/LoginPortal.tsx @@ -211,8 +211,6 @@ const LoginPortal = function (props: Props) { ); }; -export default LoginPortal; - interface ComponentOrLoadingProps { ready: boolean; @@ -229,3 +227,5 @@ function ComponentOrLoading(props: ComponentOrLoadingProps) { ); } + +export default LoginPortal; diff --git a/web/src/views/LoginPortal/SecondFactor/DeviceSelectionContainer.tsx b/web/src/views/LoginPortal/SecondFactor/DeviceSelectionContainer.tsx index 4397eb7c4..773da7513 100644 --- a/web/src/views/LoginPortal/SecondFactor/DeviceSelectionContainer.tsx +++ b/web/src/views/LoginPortal/SecondFactor/DeviceSelectionContainer.tsx @@ -91,8 +91,6 @@ const DefaultDeviceSelectionContainer = function (props: Props) { ); }; -export default DefaultDeviceSelectionContainer; - interface DeviceItemProps { id: number; device: SelectableDevice; @@ -100,7 +98,7 @@ interface DeviceItemProps { onSelect: () => void; } -function DeviceItem(props: DeviceItemProps) { +const DeviceItem = function (props: DeviceItemProps) { const className = "device-option-" + props.id; const idName = "device-" + props.device.id; const style = makeStyles((theme: Theme) => ({ @@ -136,7 +134,7 @@ function DeviceItem(props: DeviceItemProps) { ); -} +}; interface MethodItemProps { id: number; @@ -145,7 +143,7 @@ interface MethodItemProps { onSelect: () => void; } -function MethodItem(props: MethodItemProps) { +const MethodItem = function (props: MethodItemProps) { const className = "method-option-" + props.id; const idName = "method-" + props.method; const style = makeStyles((theme: Theme) => ({ @@ -181,4 +179,6 @@ function MethodItem(props: MethodItemProps) { ); -} +}; + +export default DefaultDeviceSelectionContainer; diff --git a/web/src/views/LoginPortal/SecondFactor/OTPDial.tsx b/web/src/views/LoginPortal/SecondFactor/OTPDial.tsx index 5de55beab..0f046f536 100644 --- a/web/src/views/LoginPortal/SecondFactor/OTPDial.tsx +++ b/web/src/views/LoginPortal/SecondFactor/OTPDial.tsx @@ -45,8 +45,6 @@ const OTPDial = function (props: Props) { ); }; -export default OTPDial; - const useStyles = makeStyles((theme: Theme) => ({ timeProgress: {}, register: { @@ -85,3 +83,5 @@ function Icon(props: IconProps) { ); } + +export default OTPDial; diff --git a/web/src/views/Settings/TwoFactorAuthentication/DeleteDialog.tsx b/web/src/views/Settings/TwoFactorAuthentication/DeleteDialog.tsx index 2aa8b2aef..fc5159825 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/DeleteDialog.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/DeleteDialog.tsx @@ -10,7 +10,7 @@ interface Props { handleClose: (ok: boolean) => void; } -export default function DeleteDialog(props: Props) { +const DeleteDialog = function (props: Props) { const { t: translate } = useTranslation("settings"); const handleCancel = () => { @@ -35,4 +35,6 @@ export default function DeleteDialog(props: Props) { ); -} +}; + +export default DeleteDialog; diff --git a/web/src/views/Settings/TwoFactorAuthentication/TOTPDevice.tsx b/web/src/views/Settings/TwoFactorAuthentication/TOTPDevice.tsx index 8743373c1..b8ba62e2e 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/TOTPDevice.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/TOTPDevice.tsx @@ -15,7 +15,7 @@ interface Props { handleRefresh: () => void; } -export default function TOTPDevice(props: Props) { +const TOTPDevice = function (props: Props) { const { t: translate } = useTranslation("settings"); const { createSuccessNotification, createErrorNotification } = useNotifications(); @@ -133,4 +133,6 @@ export default function TOTPDevice(props: Props) { ); -} +}; + +export default TOTPDevice; diff --git a/web/src/views/Settings/TwoFactorAuthentication/TOTPPanel.tsx b/web/src/views/Settings/TwoFactorAuthentication/TOTPPanel.tsx index d036f4c36..3f14c94b8 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/TOTPPanel.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/TOTPPanel.tsx @@ -13,7 +13,7 @@ interface Props { handleRefreshState: () => void; } -export default function TOTPPanel(props: Props) { +const TOTPPanel = function (props: Props) { const { t: translate } = useTranslation("settings"); const [showRegisterDialog, setShowRegisterDialog] = useState(false); @@ -68,4 +68,6 @@ export default function TOTPPanel(props: Props) { ); -} +}; + +export default TOTPPanel; diff --git a/web/src/views/Settings/TwoFactorAuthentication/TOTPRegisterDialogController.tsx b/web/src/views/Settings/TwoFactorAuthentication/TOTPRegisterDialogController.tsx index fa4215c11..45e78b131 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/TOTPRegisterDialogController.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/TOTPRegisterDialogController.tsx @@ -1,8 +1,7 @@ import React, { Fragment, useCallback, useEffect, useState } from "react"; -import { IconDefinition, faCopy, faKey, faTimesCircle } from "@fortawesome/free-solid-svg-icons"; +import { faTimesCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Visibility } from "@mui/icons-material"; import { Box, Button, @@ -15,13 +14,13 @@ import { FormControl, FormControlLabel, FormLabel, - IconButton, Link, Radio, RadioGroup, Step, StepLabel, Stepper, + Switch, TextField, Theme, Typography, @@ -34,9 +33,11 @@ import { QRCodeSVG } from "qrcode.react"; import { useTranslation } from "react-i18next"; import AppStoreBadges from "@components/AppStoreBadges"; +import CopyButton from "@components/CopyButton"; +import SuccessIcon from "@components/SuccessIcon"; import { GoogleAuthenticator } from "@constants/constants"; import { useNotifications } from "@hooks/NotificationsContext"; -import { TOTPOptions, toAlgorithmString } from "@models/TOTPConfiguration"; +import { toAlgorithmString } from "@models/TOTPConfiguration"; import { completeTOTPRegister, stopTOTPRegister } from "@services/OneTimePassword"; import { getTOTPSecret } from "@services/RegisterDevice"; import { getTOTPOptions } from "@services/UserInfoTOTPConfiguration"; @@ -50,52 +51,64 @@ interface Props { setClosed: () => void; } -export default function TOTPRegisterDialogController(props: Props) { +interface Options { + algorithm: string; + length: number; + period: number; +} + +interface AvailableOptions { + algorithms: string[]; + lengths: number[]; + periods: number[]; +} + +const TOTPRegisterDialogController = function (props: Props) { const { t: translate } = useTranslation("settings"); const styles = useStyles(); - const { createErrorNotification, createSuccessNotification } = useNotifications(); + const { createErrorNotification } = useNotifications(); + + const [selected, setSelected] = useState({ algorithm: "", length: 6, period: 30 }); + const [defaults, setDefaults] = useState(null); + const [available, setAvailable] = useState({ + algorithms: [], + lengths: [], + periods: [], + }); const [activeStep, setActiveStep] = useState(0); - const [options, setOptions] = useState(null); - const [optionAlgorithm, setOptionAlgorithm] = useState(""); - const [optionLength, setOptionLength] = useState(6); - const [optionPeriod, setOptionPeriod] = useState(30); - const [optionAlgorithms, setOptionAlgorithms] = useState([]); - const [optionLengths, setOptionLengths] = useState([]); - const [optionPeriods, setOptionPeriods] = useState([]); - const [totpSecretURL, setTOTPSecretURL] = useState(""); - const [totpSecretBase32, setTOTPSecretBase32] = useState(undefined); - const [totpIsLoading, setTOTPIsLoading] = useState(false); + + const [secretURL, setSecretURL] = useState(null); + const [secretValue, setSecretValue] = useState(null); + const [isLoading, setIsLoading] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); const [hasErrored, setHasErrored] = useState(false); const [dialValue, setDialValue] = useState(""); const [dialState, setDialState] = useState(State.Idle); - const [totpSecretURLHidden, setTOTPSecretURLHidden] = useState(true); + const [showQRCode, setShowQRCode] = useState(true); + const [success, setSuccess] = useState(false); - const resetStates = () => { - setOptions(null); - setOptionAlgorithm(""); - setOptionLength(6); - setOptionPeriod(30); - setOptionAlgorithms([]); - setOptionLengths([]); - setOptionPeriods([]); - setTOTPSecretURL(""); - setTOTPSecretBase32(undefined); - setTOTPIsLoading(false); + const resetStates = useCallback(() => { + if (defaults) { + setSelected(defaults); + } + + setSecretURL(null); + setSecretValue(null); + setIsLoading(false); setShowAdvanced(false); setActiveStep(0); setDialValue(""); setDialState(State.Idle); - setTOTPSecretURLHidden(true); - }; + setShowQRCode(true); + }, [defaults]); const handleClose = useCallback(() => { (async () => { props.setClosed(); - if (totpSecretURL !== "") { + if (secretURL !== "") { try { await stopTOTPRegister(); } catch (err) { @@ -105,12 +118,16 @@ export default function TOTPRegisterDialogController(props: Props) { resetStates(); })(); - }, [totpSecretURL, props]); + }, [props, secretURL, resetStates]); const handleFinished = useCallback(() => { - props.setClosed(); - resetStates(); - }, [props]); + setSuccess(true); + + setTimeout(() => { + props.setClosed(); + resetStates(); + }, 750); + }, [props, resetStates]); const handleOnClose = () => { if (!props.open) { @@ -121,21 +138,29 @@ export default function TOTPRegisterDialogController(props: Props) { }; useEffect(() => { - if (!props.open || activeStep !== 0 || options !== null) { + if (!props.open || activeStep !== 0 || defaults !== null) { return; } (async () => { const opts = await getTOTPOptions(); - setOptions(opts); - setOptionAlgorithm(toAlgorithmString(opts.algorithm)); - setOptionAlgorithms(opts.algorithms.map((algorithm) => toAlgorithmString(algorithm))); - setOptionLength(opts.length); - setOptionLengths(opts.lengths.map((length) => length.toString())); - setOptionPeriod(opts.period); - setOptionPeriods(opts.periods.map((period) => period.toString())); + + const decoded = { + algorithm: toAlgorithmString(opts.algorithm), + length: opts.length, + period: opts.period, + }; + + setAvailable({ + algorithms: opts.algorithms.map((algorithm) => toAlgorithmString(algorithm)), + lengths: opts.lengths, + periods: opts.periods, + }); + + setDefaults(decoded); + setSelected(decoded); })(); - }, [props.open, activeStep, options]); + }, [props.open, activeStep, defaults, selected]); const handleSetStepPrevious = useCallback(() => { if (activeStep === 0) { @@ -143,7 +168,9 @@ export default function TOTPRegisterDialogController(props: Props) { } setShowAdvanced(false); - setActiveStep((prevState) => (prevState -= 1)); + setActiveStep((prevState) => { + return prevState - 1; + }); }, [activeStep]); const handleSetStepNext = useCallback(() => { @@ -152,7 +179,9 @@ export default function TOTPRegisterDialogController(props: Props) { } setShowAdvanced(false); - setActiveStep((prevState) => (prevState += 1)); + setActiveStep((prevState) => { + return prevState + 1; + }); }, [activeStep]); useEffect(() => { @@ -161,12 +190,12 @@ export default function TOTPRegisterDialogController(props: Props) { } (async () => { - setTOTPIsLoading(true); + setIsLoading(true); try { - const secret = await getTOTPSecret(optionAlgorithm, optionLength, optionPeriod); - setTOTPSecretURL(secret.otpauth_url); - setTOTPSecretBase32(secret.base32_secret); + const secret = await getTOTPSecret(selected.algorithm, selected.length, selected.period); + setSecretURL(secret.otpauth_url); + setSecretValue(secret.base32_secret); } catch (err) { console.error(err); if ((err as Error).message.includes("Request failed with status code 403")) { @@ -183,12 +212,12 @@ export default function TOTPRegisterDialogController(props: Props) { setHasErrored(true); } - setTOTPIsLoading(false); + setIsLoading(false); })(); - }, [activeStep, createErrorNotification, optionAlgorithm, optionLength, optionPeriod, props.open, translate]); + }, [activeStep, createErrorNotification, selected, props.open, translate]); useEffect(() => { - if (!props.open || activeStep !== 2 || dialValue.length !== optionLength) { + if (!props.open || activeStep !== 2 || dialState === State.InProgress || dialValue.length !== selected.length) { return; } @@ -207,63 +236,49 @@ export default function TOTPRegisterDialogController(props: Props) { setDialState(State.Failure); } })(); - }, [activeStep, dialValue, dialValue.length, optionLength, props.open]); + }, [activeStep, dialState, dialValue, dialValue.length, handleFinished, props.open, selected.length]); const toggleAdvanced = () => { setShowAdvanced((prevState) => !prevState); }; const advanced = - options !== null && - (optionAlgorithms.length !== 1 || optionAlgorithms.length !== 1 || optionPeriods.length !== 1); + defaults !== null && + (available.algorithms.length !== 1 || available.lengths.length !== 1 || available.periods.length !== 1); - const hideAdvanced = - options === null || (optionAlgorithms.length <= 1 && optionPeriods.length <= 1 && optionLengths.length <= 1); + const disableAdvanced = + defaults === null || + (available.algorithms.length <= 1 && available.lengths.length <= 1 && available.periods.length <= 1); - const hideAlgorithms = advanced && optionAlgorithms.length <= 1; - const hideLengths = advanced && optionLengths.length <= 1; - const hidePeriods = advanced && optionPeriods.length <= 1; - const qrcodeFuzzyStyle = totpIsLoading || hasErrored ? styles.fuzzy : undefined; - - function SecretButton(text: string, action: string, icon: IconDefinition) { - const handleOnClick = (event: React.MouseEvent) => { - (async () => { - event.preventDefault(); - - await navigator.clipboard.writeText(text); - createSuccessNotification(action); - })(); - }; - - return ( - - - - ); - } + const hideAlgorithms = advanced && available.algorithms.length <= 1; + const hideLengths = advanced && available.lengths.length <= 1; + const hidePeriods = advanced && available.periods.length <= 1; + const qrcodeFuzzyStyle = isLoading || hasErrored ? styles.fuzzy : undefined; function renderStep(step: number) { switch (step) { case 0: return ( - {options === null ? ( - + {defaults === null ? ( + Loading... ) : ( - + {translate("To begin select next")} - ); -} +}; + +export default TwoFactorAuthSettings; diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDeviceDetailsDialog.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDeviceDetailsDialog.tsx index 255dd89b8..8145859f6 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDeviceDetailsDialog.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDeviceDetailsDialog.tsx @@ -1,21 +1,22 @@ -import React, { Fragment, useState } from "react"; +import React, { Fragment } from "react"; -import { Check, ContentCopy } from "@mui/icons-material"; import { Button, - CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, - Tooltip, + Paper, + PaperProps, Typography, } from "@mui/material"; import Grid from "@mui/material/Unstable_Grid2"; +import Draggable from "react-draggable"; import { useTranslation } from "react-i18next"; +import CopyButton from "@components/CopyButton"; import { WebAuthnDevice, toTransportName } from "@models/WebAuthn"; interface Props { @@ -24,12 +25,19 @@ interface Props { handleClose: () => void; } -export default function WebAuthnDeviceDetailsDialog(props: Props) { +const WebAuthnDeviceDetailsDialog = function (props: Props) { const { t: translate } = useTranslation("settings"); return ( - - {translate("WebAuthn Credential Details")} + + + {translate("WebAuthn Credential Details")} + {translate("Extended WebAuthn credential information for security key", { @@ -40,12 +48,6 @@ export default function WebAuthnDeviceDetailsDialog(props: Props) { - - - - - - @@ -128,11 +130,29 @@ export default function WebAuthnDeviceDetailsDialog(props: Props) { + + {translate("KID")} + + + {translate("Public Key")} + ); -} +}; interface PropertyTextProps { name: string; @@ -140,49 +160,6 @@ interface PropertyTextProps { xs?: number; } -function PropertyCopyButton(props: PropertyTextProps) { - const { t: translate } = useTranslation("settings"); - - const [copied, setCopied] = useState(false); - const [copying, setCopying] = useState(false); - - const handleCopyToClipboard = () => { - if (copied) { - return; - } - - (async () => { - setCopying(true); - - await navigator.clipboard.writeText(props.value); - - setTimeout(() => { - setCopying(false); - setCopied(true); - }, 500); - - setTimeout(() => { - setCopied(false); - }, 2000); - })(); - }; - - return ( - - - - ); -} - function PropertyText(props: PropertyTextProps) { return ( @@ -193,3 +170,13 @@ function PropertyText(props: PropertyTextProps) { ); } + +function PaperComponent(props: PaperProps) { + return ( + + + + ); +} + +export default WebAuthnDeviceDetailsDialog; diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDeviceEditDialog.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDeviceEditDialog.tsx index a76041710..a3fc79f8d 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDeviceEditDialog.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDeviceEditDialog.tsx @@ -10,8 +10,7 @@ interface Props { device: WebAuthnDevice; handleClose: (ok: boolean, name: string) => void; } - -export default function WebAuthnDeviceEditDialog(props: Props) { +const WebAuthnDeviceEditDialog = function (props: Props) { const { t: translate } = useTranslation("settings"); const [deviceName, setName] = useState(""); @@ -36,12 +35,14 @@ export default function WebAuthnDeviceEditDialog(props: Props) { {translate("Edit WebAuthn Credential")} - {translate("Enter a new name for this WebAuthn credential")} + + {translate("Enter a new description for this WebAuthn credential")} + ); -} +}; + +export default WebAuthnDeviceEditDialog; diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDeviceItem.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDeviceItem.tsx index bdccb1cc4..5f136cde5 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDeviceItem.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDeviceItem.tsx @@ -21,8 +21,7 @@ interface Props { device: WebAuthnDevice; handleEdit: () => void; } - -export default function WebAuthnDeviceItem(props: Props) { +const WebAuthnDeviceItem = function (props: Props) { const { t: translate } = useTranslation("settings"); const { createSuccessNotification, createErrorNotification } = useNotifications(); @@ -197,4 +196,6 @@ export default function WebAuthnDeviceItem(props: Props) { ); -} +}; + +export default WebAuthnDeviceItem; diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDeviceRegisterDialog.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDeviceRegisterDialog.tsx index de5fa4e37..26da98e3a 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDeviceRegisterDialog.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDeviceRegisterDialog.tsx @@ -8,6 +8,8 @@ import { DialogContent, DialogContentText, DialogTitle, + Paper, + PaperProps, Step, StepLabel, Stepper, @@ -18,6 +20,7 @@ import { import Grid from "@mui/material/Unstable_Grid2"; import makeStyles from "@mui/styles/makeStyles"; import { PublicKeyCredentialCreationOptionsJSON } from "@simplewebauthn/typescript-types"; +import Draggable from "react-draggable"; import { useTranslation } from "react-i18next"; import InformationIcon from "@components/InformationIcon"; @@ -291,6 +294,14 @@ const WebAuthnDeviceRegisterDialog = function (props: Props) { ); }; +function PaperComponent(props: PaperProps) { + return ( + + + + ); +} + export default WebAuthnDeviceRegisterDialog; const useStyles = makeStyles((theme: Theme) => ({ diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDevicesPanel.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDevicesPanel.tsx index 233d94b24..a6ffa4c3c 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDevicesPanel.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDevicesPanel.tsx @@ -12,8 +12,7 @@ interface Props { devices: WebAuthnDevice[] | undefined; handleRefreshState: () => void; } - -export default function WebAuthnDevicesPanel(props: Props) { +const WebAuthnDevicesPanel = function (props: Props) { const { t: translate } = useTranslation("settings"); const [showRegisterDialog, setShowRegisterDialog] = useState(false); @@ -63,4 +62,6 @@ export default function WebAuthnDevicesPanel(props: Props) { ); -} +}; + +export default WebAuthnDevicesPanel; diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDevicesStack.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDevicesStack.tsx index 3d9463451..282331a14 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDevicesStack.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebAuthnDevicesStack.tsx @@ -10,7 +10,7 @@ interface Props { handleRefreshState: () => void; } -export default function WebAuthnDevicesStack(props: Props) { +const WebAuthnDevicesStack = function (props: Props) { return ( {props.devices.map((x, idx) => ( @@ -18,4 +18,6 @@ export default function WebAuthnDevicesStack(props: Props) { ))} ); -} +}; + +export default WebAuthnDevicesStack;