feat: implement webauthn device delete confirmation (#4426)

* refactor: move webauthn device index knowledge out of webauthndeviceitem

* feat: implement webauthn device delete confirmation

* fix: don't unset deleting idx for dialog on webauthn device delete

* refactor: remove `@root` from import paths
pull/4427/head^2
Stephen Kent 2022-11-26 15:46:24 -08:00 committed by GitHub
parent b842a22236
commit 24d947624b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 150 additions and 93 deletions

View File

@ -397,7 +397,7 @@ export async function performAssertionCeremony(
return AssertionResult.Failure;
}
export async function deleteDevice(deviceID: number): Promise<number> {
export async function deleteDevice(deviceID: string): Promise<number> {
let response = await axios.delete(`${WebauthnDevicesPath}/${deviceID}`);
return response.status;
}

View File

@ -0,0 +1,41 @@
import React from "react";
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
import { WebauthnDevice } from "@models/Webauthn";
interface Props {
open: boolean;
device: WebauthnDevice | undefined;
handleClose: (ok: boolean) => void;
}
export default function WebauthnDeviceDeleteDialog(props: Props) {
const handleCancel = () => {
props.handleClose(false);
};
return (
<Dialog open={props.open} onClose={handleCancel}>
<DialogTitle>{`Remove ${props.device ? props.device.description : "(unknown)"}`}</DialogTitle>
<DialogContent>
<DialogContentText>
{`Are you sure you want to remove the device "${
props.device ? props.device.description : "(unknown)"
}" from your account?`}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleCancel}>Cancel</Button>
<Button
onClick={() => {
props.handleClose(true);
}}
autoFocus
>
Remove
</Button>
</DialogActions>
</Dialog>
);
}

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React from "react";
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
@ -20,49 +20,26 @@ import {
} from "@mui/material";
import { useTranslation } from "react-i18next";
import { useNotifications } from "@hooks/NotificationsContext";
import { WebauthnDevice } from "@root/models/Webauthn";
import { deleteDevice } from "@root/services/Webauthn";
import { WebauthnDevice } from "@models/Webauthn";
interface Props {
device: WebauthnDevice;
webauthnShowDetails: number;
idx: number;
handleWebAuthnDetailsChange: (idx: number) => void;
handleDeleteItem: (idx: number) => void;
deleting: boolean;
webauthnShowDetails: boolean;
handleWebAuthnDetailsChange: () => void;
handleDelete: () => void;
}
export default function WebauthnDeviceItem(props: Props) {
const { t: translate } = useTranslation("settings");
const { createErrorNotification } = useNotifications();
const [deleting, setDeleting] = useState(false);
const handleDelete = async () => {
setDeleting(true);
const status = await deleteDevice(props.device.id);
setDeleting(false);
if (status !== 200) {
createErrorNotification(translate("There was a problem deleting the device"));
return;
}
props.handleDeleteItem(props.idx);
};
return (
<React.Fragment>
<TableRow sx={{ "& > *": { borderBottom: "unset" } }} key={props.device.kid.toString()}>
<TableCell>
<Tooltip title={translate("Show Details")} placement="right">
<IconButton
aria-label="expand row"
size="small"
onClick={() => props.handleWebAuthnDetailsChange(props.idx)}
>
{props.webauthnShowDetails === props.idx ? (
<KeyboardArrowUpIcon />
) : (
<KeyboardArrowDownIcon />
)}
<IconButton aria-label="expand row" size="small" onClick={props.handleWebAuthnDetailsChange}>
{props.webauthnShowDetails ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</Tooltip>
</TableCell>
@ -79,11 +56,11 @@ export default function WebauthnDeviceItem(props: Props) {
<EditIcon />
</IconButton>
</Tooltip>
{deleting ? (
{props.deleting ? (
<CircularProgress color="inherit" size={24} />
) : (
<Tooltip title={translate("Delete")} placement="bottom">
<IconButton aria-label="delete" onClick={handleDelete}>
<IconButton aria-label="delete" onClick={props.handleDelete}>
<DeleteIcon />
</IconButton>
</Tooltip>
@ -93,7 +70,7 @@ export default function WebauthnDeviceItem(props: Props) {
</TableRow>
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={4}>
<Collapse in={props.webauthnShowDetails === props.idx} timeout="auto" unmountOnExit>
<Collapse in={props.webauthnShowDetails} timeout="auto" unmountOnExit>
<Grid container spacing={2} sx={{ mb: 3, margin: 1 }}>
<Grid item xs={12} sm={12} md={12} lg={12} xl={12}>
<Box sx={{ margin: 1 }}>

View File

@ -18,32 +18,43 @@ import { useNavigate } from "react-router-dom";
import { RegisterWebauthnRoute } from "@constants/Routes";
import { useNotifications } from "@hooks/NotificationsContext";
import { WebauthnDevice } from "@root/models/Webauthn";
import { initiateWebauthnRegistrationProcess } from "@root/services/RegisterDevice";
import { AutheliaState, AuthenticationLevel } from "@root/services/State";
import { getWebauthnDevices } from "@root/services/UserWebauthnDevices";
import { WebauthnDevice } from "@models/Webauthn";
import { initiateWebauthnRegistrationProcess } from "@services/RegisterDevice";
import { AutheliaState, AuthenticationLevel } from "@services/State";
import { getWebauthnDevices } from "@services/UserWebauthnDevices";
import { deleteDevice } from "@services/Webauthn";
import WebauthnDeviceDeleteDialog from "./WebauthnDeviceDeleteDialog";
import WebauthnDeviceItem from "./WebauthnDeviceItem";
interface Props {
state: AutheliaState;
}
export default function TwoFactorAuthSettings(props: Props) {
interface WebauthnDeviceDisplay extends WebauthnDevice {
deleting: boolean;
}
export default function WebauthnDevices(props: Props) {
const { t: translate } = useTranslation("settings");
const navigate = useNavigate();
const { createInfoNotification, createErrorNotification } = useNotifications();
const [webauthnShowDetails, setWebauthnShowDetails] = useState<number>(-1);
const [deletingIdx, setDeletingIdx] = useState<number>(-1);
const [registrationInProgress, setRegistrationInProgress] = useState(false);
const [ready, setReady] = useState(false);
const [webauthnDevices, setWebauthnDevices] = useState<WebauthnDevice[] | undefined>();
const [webauthnDevices, setWebauthnDevices] = useState<WebauthnDeviceDisplay[]>([]);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
useEffect(() => {
(async function () {
const devices = await getWebauthnDevices();
setWebauthnDevices(devices);
const devicesDisplay = devices.map((x, idx) => {
return { ...x, deleting: false } as WebauthnDeviceDisplay;
});
setWebauthnDevices(devicesDisplay);
setReady(true);
})();
}, []);
@ -57,6 +68,23 @@ export default function TwoFactorAuthSettings(props: Props) {
};
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);
@ -86,6 +114,12 @@ export default function TwoFactorAuthSettings(props: Props) {
};
return (
<>
<WebauthnDeviceDeleteDialog
device={deletingIdx > -1 ? webauthnDevices[deletingIdx] : undefined}
open={deleteDialogOpen}
handleClose={handleDeleteItemConfirm}
/>
<Paper variant="outlined">
<Box sx={{ p: 3 }}>
<Stack spacing={2}>
@ -115,10 +149,14 @@ export default function TwoFactorAuthSettings(props: Props) {
return (
<WebauthnDeviceItem
device={x}
idx={idx}
webauthnShowDetails={webauthnShowDetails}
handleWebAuthnDetailsChange={handleWebAuthnDetailsChange}
handleDeleteItem={handleDeleteItem}
deleting={x.deleting}
webauthnShowDetails={webauthnShowDetails === idx}
handleWebAuthnDetailsChange={() => {
handleWebAuthnDetailsChange(idx);
}}
handleDelete={() => {
handleDeleteItem(idx);
}}
key={`webauthn-device-${idx}`}
/>
);
@ -137,5 +175,6 @@ export default function TwoFactorAuthSettings(props: Props) {
</Stack>
</Box>
</Paper>
</>
);
}