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 pathspull/4427/head^2
parent
b842a22236
commit
24d947624b
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 }}>
|
||||
|
|
|
@ -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,56 +114,67 @@ export default function TwoFactorAuthSettings(props: Props) {
|
|||
};
|
||||
|
||||
return (
|
||||
<Paper variant="outlined">
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Stack spacing={2}>
|
||||
<Box>
|
||||
<Typography variant="h5">Webauthn Devices</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button variant="outlined" color="primary" onClick={handleAddKeyButtonClick}>
|
||||
{"Add new device"}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box>
|
||||
{ready ? (
|
||||
<>
|
||||
{webauthnDevices ? (
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell />
|
||||
<TableCell>{translate("Name")}</TableCell>
|
||||
<TableCell>{translate("Enabled")}</TableCell>
|
||||
<TableCell align="center">{translate("Actions")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{webauthnDevices.map((x, idx) => {
|
||||
return (
|
||||
<WebauthnDeviceItem
|
||||
device={x}
|
||||
idx={idx}
|
||||
webauthnShowDetails={webauthnShowDetails}
|
||||
handleWebAuthnDetailsChange={handleWebAuthnDetailsChange}
|
||||
handleDeleteItem={handleDeleteItem}
|
||||
key={`webauthn-device-${idx}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Skeleton height={20} />
|
||||
<Skeleton height={40} />
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
<>
|
||||
<WebauthnDeviceDeleteDialog
|
||||
device={deletingIdx > -1 ? webauthnDevices[deletingIdx] : undefined}
|
||||
open={deleteDialogOpen}
|
||||
handleClose={handleDeleteItemConfirm}
|
||||
/>
|
||||
<Paper variant="outlined">
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Stack spacing={2}>
|
||||
<Box>
|
||||
<Typography variant="h5">Webauthn Devices</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button variant="outlined" color="primary" onClick={handleAddKeyButtonClick}>
|
||||
{"Add new device"}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box>
|
||||
{ready ? (
|
||||
<>
|
||||
{webauthnDevices ? (
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell />
|
||||
<TableCell>{translate("Name")}</TableCell>
|
||||
<TableCell>{translate("Enabled")}</TableCell>
|
||||
<TableCell align="center">{translate("Actions")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{webauthnDevices.map((x, idx) => {
|
||||
return (
|
||||
<WebauthnDeviceItem
|
||||
device={x}
|
||||
deleting={x.deleting}
|
||||
webauthnShowDetails={webauthnShowDetails === idx}
|
||||
handleWebAuthnDetailsChange={() => {
|
||||
handleWebAuthnDetailsChange(idx);
|
||||
}}
|
||||
handleDelete={() => {
|
||||
handleDeleteItem(idx);
|
||||
}}
|
||||
key={`webauthn-device-${idx}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Skeleton height={20} />
|
||||
<Skeleton height={40} />
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue