feat: implement webauthn device rename with dialog (#4427)
* feat: add loading skeleton to webauthn devices list in settings ui * 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 * feat: implement webauthn device rename with dialog * refactor: remove `@root` from import paths * refactor: remove `@root` from import pathspull/4435/head^2
parent
24d947624b
commit
33520daa10
|
@ -156,3 +156,7 @@ export enum WebauthnTouchState {
|
|||
InProgress = 2,
|
||||
Failure = 3,
|
||||
}
|
||||
|
||||
export interface WebauthnDeviceUpdateRequest {
|
||||
description: string;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
PublicKeyCredentialJSON,
|
||||
PublicKeyCredentialRequestOptionsJSON,
|
||||
PublicKeyCredentialRequestOptionsStatus,
|
||||
WebauthnDeviceUpdateRequest,
|
||||
} from "@models/Webauthn";
|
||||
import {
|
||||
OptionalDataServiceResponse,
|
||||
|
@ -401,3 +402,10 @@ export async function deleteDevice(deviceID: string): Promise<number> {
|
|||
let response = await axios.delete(`${WebauthnDevicesPath}/${deviceID}`);
|
||||
return response.status;
|
||||
}
|
||||
|
||||
export async function updateDevice(deviceID: string, description: string): Promise<number> {
|
||||
let response = await axios.put<ServiceResponse<WebauthnDeviceUpdateRequest>>(`${WebauthnDevicesPath}/${deviceID}`, {
|
||||
description: description,
|
||||
});
|
||||
return response.status;
|
||||
}
|
||||
|
|
|
@ -181,7 +181,7 @@ const RegisterWebauthn = function (props: Props) {
|
|||
onChange={(v) => setName(v.target.value.substring(0, 30))}
|
||||
onFocus={() => setNameError(false)}
|
||||
autoCapitalize="none"
|
||||
autoComplete="username"
|
||||
autoComplete="webauthn-name"
|
||||
onKeyPress={(ev) => {
|
||||
if (ev.key === "Enter") {
|
||||
if (!deviceName.length) {
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import React, { MutableRefObject, useRef, useState } from "react";
|
||||
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import FixedTextField from "@components/FixedTextField";
|
||||
import { WebauthnDevice } from "@models/Webauthn";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
device: WebauthnDevice | undefined;
|
||||
handleClose: (ok: boolean, name: string) => void;
|
||||
}
|
||||
|
||||
export default function WebauthnDeviceEditDialog(props: Props) {
|
||||
const { t: translate } = useTranslation();
|
||||
const [deviceName, setName] = useState("");
|
||||
const nameRef = useRef() as MutableRefObject<HTMLInputElement>;
|
||||
const [nameError, setNameError] = useState(false);
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!deviceName.length) {
|
||||
setNameError(true);
|
||||
} else {
|
||||
props.handleClose(true, deviceName);
|
||||
}
|
||||
setName("");
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
props.handleClose(false, "");
|
||||
setName("");
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={props.open} onClose={handleCancel}>
|
||||
<DialogTitle>{`Edit ${props.device ? props.device.description : "(unknown)"}`}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>Enter a new name for this device:</DialogContentText>
|
||||
<FixedTextField
|
||||
// TODO (PR: #806, Issue: #511) potentially refactor
|
||||
autoFocus
|
||||
inputRef={nameRef}
|
||||
id="name-textfield"
|
||||
label={translate("Name")}
|
||||
variant="standard"
|
||||
required
|
||||
value={deviceName}
|
||||
error={nameError}
|
||||
fullWidth
|
||||
disabled={false}
|
||||
onChange={(v) => setName(v.target.value.substring(0, 30))}
|
||||
onFocus={() => {
|
||||
setNameError(false);
|
||||
}}
|
||||
autoCapitalize="none"
|
||||
autoComplete="webauthn-name"
|
||||
onKeyPress={(ev) => {
|
||||
if (ev.key === "Enter") {
|
||||
handleConfirm();
|
||||
ev.preventDefault();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCancel}>Cancel</Button>
|
||||
<Button onClick={handleConfirm}>Update</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
|
@ -25,9 +25,11 @@ import { WebauthnDevice } from "@models/Webauthn";
|
|||
interface Props {
|
||||
device: WebauthnDevice;
|
||||
deleting: boolean;
|
||||
editing: boolean;
|
||||
webauthnShowDetails: boolean;
|
||||
handleWebAuthnDetailsChange: () => void;
|
||||
handleDelete: () => void;
|
||||
handleEdit: () => void;
|
||||
}
|
||||
|
||||
export default function WebauthnDeviceItem(props: Props) {
|
||||
|
@ -51,11 +53,15 @@ export default function WebauthnDeviceItem(props: Props) {
|
|||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Stack direction="row" spacing={1} alignItems="center" justifyContent="center">
|
||||
<Tooltip title={translate("Edit")} placement="bottom">
|
||||
<IconButton aria-label="edit">
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{props.editing ? (
|
||||
<CircularProgress color="inherit" size={24} />
|
||||
) : (
|
||||
<Tooltip title={translate("Edit")} placement="bottom">
|
||||
<IconButton aria-label="edit" onClick={props.handleEdit}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{props.deleting ? (
|
||||
<CircularProgress color="inherit" size={24} />
|
||||
) : (
|
||||
|
|
|
@ -22,9 +22,10 @@ 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 { deleteDevice, updateDevice } from "@services/Webauthn";
|
||||
|
||||
import WebauthnDeviceDeleteDialog from "./WebauthnDeviceDeleteDialog";
|
||||
import WebauthnDeviceEditDialog from "./WebauthnDeviceEditDialog";
|
||||
import WebauthnDeviceItem from "./WebauthnDeviceItem";
|
||||
|
||||
interface Props {
|
||||
|
@ -33,6 +34,7 @@ interface Props {
|
|||
|
||||
interface WebauthnDeviceDisplay extends WebauthnDevice {
|
||||
deleting: boolean;
|
||||
editing: boolean;
|
||||
}
|
||||
|
||||
export default function WebauthnDevices(props: Props) {
|
||||
|
@ -42,11 +44,13 @@ export default function WebauthnDevices(props: Props) {
|
|||
const { createInfoNotification, createErrorNotification } = useNotifications();
|
||||
const [webauthnShowDetails, setWebauthnShowDetails] = useState<number>(-1);
|
||||
const [deletingIdx, setDeletingIdx] = useState<number>(-1);
|
||||
const [editingIdx, setEditingIdx] = useState<number>(-1);
|
||||
const [registrationInProgress, setRegistrationInProgress] = useState(false);
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
const [webauthnDevices, setWebauthnDevices] = useState<WebauthnDeviceDisplay[]>([]);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
|
@ -90,6 +94,29 @@ export default function WebauthnDevices(props: Props) {
|
|||
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<void>, redirectRoute: string) => {
|
||||
if (props.state.authentication_level >= AuthenticationLevel.TwoFactor) {
|
||||
navigate(redirectRoute);
|
||||
|
@ -115,6 +142,11 @@ export default function WebauthnDevices(props: Props) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<WebauthnDeviceEditDialog
|
||||
device={editingIdx > -1 ? webauthnDevices[editingIdx] : undefined}
|
||||
open={editDialogOpen}
|
||||
handleClose={handleEditItemConfirm}
|
||||
/>
|
||||
<WebauthnDeviceDeleteDialog
|
||||
device={deletingIdx > -1 ? webauthnDevices[deletingIdx] : undefined}
|
||||
open={deleteDialogOpen}
|
||||
|
@ -150,10 +182,14 @@ export default function WebauthnDevices(props: Props) {
|
|||
<WebauthnDeviceItem
|
||||
device={x}
|
||||
deleting={x.deleting}
|
||||
editing={x.editing}
|
||||
webauthnShowDetails={webauthnShowDetails === idx}
|
||||
handleWebAuthnDetailsChange={() => {
|
||||
handleWebAuthnDetailsChange(idx);
|
||||
}}
|
||||
handleEdit={() => {
|
||||
handleEditItem(idx);
|
||||
}}
|
||||
handleDelete={() => {
|
||||
handleDeleteItem(idx);
|
||||
}}
|
||||
|
|
Loading…
Reference in New Issue