diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceItem.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceItem.tsx index 32aa8f6de..6173b39b0 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceItem.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceItem.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { Fragment, useState } from "react"; import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; @@ -8,64 +8,131 @@ import { Box, Button, CircularProgress, Stack, Typography } from "@mui/material" import { ButtonProps } from "@mui/material/Button"; import { useTranslation } from "react-i18next"; +import { useNotifications } from "@hooks/NotificationsContext"; import { WebauthnDevice } from "@models/Webauthn"; +import { deleteDevice, updateDevice } from "@services/Webauthn"; +import WebauthnDeviceDeleteDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceDeleteDialog"; +import WebauthnDeviceDetailsDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceDetailsDialog"; +import WebauthnDeviceEditDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceEditDialog"; interface Props { + index: number; device: WebauthnDevice; - deleting: boolean; - editing: boolean; - webauthnShowDetails: boolean; - handleWebAuthnDetailsChange: () => void; - handleDetails: () => void; - handleDelete: () => void; - handleEdit: () => void; + handleDeviceEdit(index: number, device: WebauthnDevice): void; + handleDeviceDelete(device: WebauthnDevice): void; } export default function WebauthnDeviceItem(props: Props) { const { t: translate } = useTranslation("settings"); + const { createErrorNotification } = useNotifications(); + + const [showDialogDetails, setShowDialogDetails] = useState(false); + const [showDialogEdit, setShowDialogEdit] = useState(false); + const [showDialogDelete, setShowDialogDelete] = useState(false); + const [loadingEdit, setLoadingEdit] = useState(false); + const [loadingDelete, setLoadingDelete] = useState(false); + + const handleEdit = async (ok: boolean, name: string) => { + setShowDialogEdit(false); + + if (!ok) { + return; + } + + setLoadingEdit(true); + + const status = await updateDevice(props.device.id, name); + + setLoadingEdit(false); + + if (status !== 200) { + createErrorNotification(translate("There was a problem updating the device")); + return; + } + + props.handleDeviceEdit(props.index, { ...props.device, description: name }); + }; + + const handleDelete = async (ok: boolean) => { + setShowDialogDelete(false); + + if (!ok) { + return; + } + + setLoadingDelete(true); + + const status = await deleteDevice(props.device.id); + + setLoadingDelete(false); + + if (status !== 200) { + createErrorNotification(translate("There was a problem deleting the device")); + return; + } + + props.handleDeviceDelete(props.device); + }; + return ( - - - - - - {props.device.description} + + { + setShowDialogDetails(false); + }} + /> + + + + + + + + {props.device.description} + + {` (${props.device.attestation_type.toUpperCase()})`} + + Added {props.device.created_at.toString()} + + {props.device.last_used_at === undefined + ? translate("Never used") + : "Last used " + props.device.last_used_at.toString()} - {` (${props.device.attestation_type.toUpperCase()})`} - - Added {props.device.created_at.toString()} - - {props.device.last_used_at === undefined - ? translate("Never used") - : "Last used " + props.device.last_used_at.toString()} - + + + } + onClick={() => setShowDialogEdit(true)} + > + {translate("Edit")} + + } + onClick={() => setShowDialogDelete(true)} + > + {translate("Remove")} + - - } - onClick={props.handleEdit} - > - {translate("Edit")} - - } - onClick={props.handleDelete} - > - {translate("Remove")} - - + ); } diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevices.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevices.tsx index f5532b55b..55f446ed5 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevices.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevices.tsx @@ -1,120 +1,26 @@ -import React, { useEffect, useState } from "react"; +import React, { Fragment, Suspense, useState } from "react"; -import { Box, Button, Paper, Skeleton, Stack, Typography } from "@mui/material"; +import { Box, Button, Paper, Stack, Typography } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { RegisterWebauthnRoute } from "@constants/Routes"; import { useNotifications } from "@hooks/NotificationsContext"; -import { WebauthnDevice } from "@models/Webauthn"; import { initiateWebauthnRegistrationProcess } from "@services/RegisterDevice"; import { AutheliaState, AuthenticationLevel } from "@services/State"; -import { getWebauthnDevices } from "@services/UserWebauthnDevices"; -import { deleteDevice, updateDevice } from "@services/Webauthn"; -import WebauthnDeviceDeleteDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceDeleteDialog"; -import WebauthnDeviceDetailsDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceDetailsDialog"; -import WebauthnDeviceEditDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceEditDialog"; -import WebauthnDeviceItem from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceItem"; +import LoadingPage from "@views/LoadingPage/LoadingPage"; +import WebauthnDevicesStack from "@views/Settings/TwoFactorAuthentication/WebauthnDevicesStack"; interface Props { state: AutheliaState; } -interface WebauthnDeviceDisplay extends WebauthnDevice { - deleting: boolean; - editing: boolean; -} - export default function WebauthnDevices(props: Props) { const { t: translate } = useTranslation("settings"); const navigate = useNavigate(); const { createInfoNotification, createErrorNotification } = useNotifications(); - const [webauthnShowDetails, setWebauthnShowDetails] = useState(-1); - const [detailsIdx, setDetailsIdx] = useState(-1); - const [deletingIdx, setDeletingIdx] = useState(-1); - const [editingIdx, setEditingIdx] = useState(-1); const [registrationInProgress, setRegistrationInProgress] = useState(false); - const [ready, setReady] = useState(false); - - const [webauthnDevices, setWebauthnDevices] = useState([]); - const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [editDialogOpen, setEditDialogOpen] = useState(false); - - useEffect(() => { - (async function () { - const devices = await getWebauthnDevices(); - const devicesDisplay = devices.map((x, idx) => { - return { - ...x, - deleting: false, - editing: false, - } as WebauthnDeviceDisplay; - }); - setWebauthnDevices(devicesDisplay); - setReady(true); - })(); - }, []); - - const handleWebAuthnDetailsChange = (idx: number) => { - if (webauthnShowDetails === idx) { - setWebauthnShowDetails(-1); - } else { - setWebauthnShowDetails(idx); - } - }; - - const handleDetailsItem = async (idx: number) => { - setDetailsIdx(idx); - setDetailsDialogOpen(true); - }; - - const handleDeleteItem = async (idx: number) => { - setDeletingIdx(idx); - setDeleteDialogOpen(true); - }; - - const handleDeleteItemConfirm = async (ok: boolean) => { - setDeleteDialogOpen(false); - const idx = deletingIdx; - if (ok !== true) { - return; - } - webauthnDevices[idx].deleting = true; - const status = await deleteDevice(webauthnDevices[idx].id); - if (status !== 200) { - webauthnDevices[idx].deleting = false; - createErrorNotification(translate("There was a problem deleting the device")); - return; - } - let updatedDevices = [...webauthnDevices]; - updatedDevices.splice(idx, 1); - setWebauthnDevices(updatedDevices); - }; - - const handleEditItem = async (idx: number) => { - setEditingIdx(idx); - setEditDialogOpen(true); - }; - - const handleEditItemConfirm = async (ok: boolean, name: string) => { - setEditDialogOpen(false); - const idx = editingIdx; - if (ok !== true) { - return; - } - webauthnDevices[idx].editing = true; - const status = await updateDevice(webauthnDevices[idx].id, name); - webauthnDevices[idx].editing = false; - if (status !== 200) { - createErrorNotification(translate("There was a problem updating the device")); - return; - } - let updatedDevices = [...webauthnDevices]; - updatedDevices[idx].description = name; - setWebauthnDevices(updatedDevices); - }; const initiateRegistration = async (initiateRegistrationFunc: () => Promise, redirectRoute: string) => { if (props.state.authentication_level >= AuthenticationLevel.TwoFactor) { @@ -140,24 +46,7 @@ export default function WebauthnDevices(props: Props) { }; return ( - <> - -1 ? webauthnDevices[detailsIdx] : undefined} - open={detailsDialogOpen} - handleClose={() => { - setDetailsDialogOpen(false); - }} - /> - -1 ? webauthnDevices[editingIdx] : undefined} - open={editDialogOpen} - handleClose={handleEditItemConfirm} - /> - -1 ? webauthnDevices[deletingIdx] : undefined} - open={deleteDialogOpen} - handleClose={handleDeleteItemConfirm} - /> + @@ -169,41 +58,12 @@ export default function WebauthnDevices(props: Props) { {"Add new device"} - {ready ? ( - - {webauthnDevices - ? webauthnDevices.map((x, idx) => ( - { - handleWebAuthnDetailsChange(idx); - }} - handleDetails={() => { - handleDetailsItem(idx); - }} - handleEdit={() => { - handleEditItem(idx); - }} - handleDelete={() => { - handleDeleteItem(idx); - }} - key={`webauthn-device-${idx}`} - /> - )) - : null} - - ) : ( - <> - - - - )} + }> + + - + ); } diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevicesStack.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevicesStack.tsx new file mode 100644 index 000000000..90ed0e8e0 --- /dev/null +++ b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevicesStack.tsx @@ -0,0 +1,51 @@ +import React, { useEffect, useState } from "react"; + +import { Stack } from "@mui/material"; + +import { WebauthnDevice } from "@models/Webauthn"; +import { getWebauthnDevices } from "@services/UserWebauthnDevices"; +import WebauthnDeviceItem from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceItem"; + +interface Props {} + +export default function WebauthnDevicesStack(props: Props) { + const [devices, setDevices] = useState([]); + + useEffect(() => { + (async function () { + const devices = await getWebauthnDevices(); + setDevices(devices); + })(); + }, []); + + const handleEdit = (index: number, device: WebauthnDevice) => { + const nextDevices = devices.map((d, i) => { + if (i === index) { + return device; + } else { + return d; + } + }); + + setDevices(nextDevices); + }; + + const handleDelete = (device: WebauthnDevice) => { + setDevices(devices.filter((d) => d.id !== device.id && d.kid !== device.kid)); + }; + + return ( + + {devices + ? devices.map((x, idx) => ( + + )) + : null} + + ); +}