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,
|
InProgress = 2,
|
||||||
Failure = 3,
|
Failure = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WebauthnDeviceUpdateRequest {
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
PublicKeyCredentialJSON,
|
PublicKeyCredentialJSON,
|
||||||
PublicKeyCredentialRequestOptionsJSON,
|
PublicKeyCredentialRequestOptionsJSON,
|
||||||
PublicKeyCredentialRequestOptionsStatus,
|
PublicKeyCredentialRequestOptionsStatus,
|
||||||
|
WebauthnDeviceUpdateRequest,
|
||||||
} from "@models/Webauthn";
|
} from "@models/Webauthn";
|
||||||
import {
|
import {
|
||||||
OptionalDataServiceResponse,
|
OptionalDataServiceResponse,
|
||||||
|
@ -401,3 +402,10 @@ export async function deleteDevice(deviceID: string): Promise<number> {
|
||||||
let response = await axios.delete(`${WebauthnDevicesPath}/${deviceID}`);
|
let response = await axios.delete(`${WebauthnDevicesPath}/${deviceID}`);
|
||||||
return response.status;
|
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))}
|
onChange={(v) => setName(v.target.value.substring(0, 30))}
|
||||||
onFocus={() => setNameError(false)}
|
onFocus={() => setNameError(false)}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoComplete="username"
|
autoComplete="webauthn-name"
|
||||||
onKeyPress={(ev) => {
|
onKeyPress={(ev) => {
|
||||||
if (ev.key === "Enter") {
|
if (ev.key === "Enter") {
|
||||||
if (!deviceName.length) {
|
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 {
|
interface Props {
|
||||||
device: WebauthnDevice;
|
device: WebauthnDevice;
|
||||||
deleting: boolean;
|
deleting: boolean;
|
||||||
|
editing: boolean;
|
||||||
webauthnShowDetails: boolean;
|
webauthnShowDetails: boolean;
|
||||||
handleWebAuthnDetailsChange: () => void;
|
handleWebAuthnDetailsChange: () => void;
|
||||||
handleDelete: () => void;
|
handleDelete: () => void;
|
||||||
|
handleEdit: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebauthnDeviceItem(props: Props) {
|
export default function WebauthnDeviceItem(props: Props) {
|
||||||
|
@ -51,11 +53,15 @@ export default function WebauthnDeviceItem(props: Props) {
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
<Stack direction="row" spacing={1} alignItems="center" justifyContent="center">
|
<Stack direction="row" spacing={1} alignItems="center" justifyContent="center">
|
||||||
<Tooltip title={translate("Edit")} placement="bottom">
|
{props.editing ? (
|
||||||
<IconButton aria-label="edit">
|
<CircularProgress color="inherit" size={24} />
|
||||||
<EditIcon />
|
) : (
|
||||||
</IconButton>
|
<Tooltip title={translate("Edit")} placement="bottom">
|
||||||
</Tooltip>
|
<IconButton aria-label="edit" onClick={props.handleEdit}>
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
{props.deleting ? (
|
{props.deleting ? (
|
||||||
<CircularProgress color="inherit" size={24} />
|
<CircularProgress color="inherit" size={24} />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -22,9 +22,10 @@ import { WebauthnDevice } from "@models/Webauthn";
|
||||||
import { initiateWebauthnRegistrationProcess } from "@services/RegisterDevice";
|
import { initiateWebauthnRegistrationProcess } from "@services/RegisterDevice";
|
||||||
import { AutheliaState, AuthenticationLevel } from "@services/State";
|
import { AutheliaState, AuthenticationLevel } from "@services/State";
|
||||||
import { getWebauthnDevices } from "@services/UserWebauthnDevices";
|
import { getWebauthnDevices } from "@services/UserWebauthnDevices";
|
||||||
import { deleteDevice } from "@services/Webauthn";
|
import { deleteDevice, updateDevice } from "@services/Webauthn";
|
||||||
|
|
||||||
import WebauthnDeviceDeleteDialog from "./WebauthnDeviceDeleteDialog";
|
import WebauthnDeviceDeleteDialog from "./WebauthnDeviceDeleteDialog";
|
||||||
|
import WebauthnDeviceEditDialog from "./WebauthnDeviceEditDialog";
|
||||||
import WebauthnDeviceItem from "./WebauthnDeviceItem";
|
import WebauthnDeviceItem from "./WebauthnDeviceItem";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -33,6 +34,7 @@ interface Props {
|
||||||
|
|
||||||
interface WebauthnDeviceDisplay extends WebauthnDevice {
|
interface WebauthnDeviceDisplay extends WebauthnDevice {
|
||||||
deleting: boolean;
|
deleting: boolean;
|
||||||
|
editing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebauthnDevices(props: Props) {
|
export default function WebauthnDevices(props: Props) {
|
||||||
|
@ -42,11 +44,13 @@ export default function WebauthnDevices(props: Props) {
|
||||||
const { createInfoNotification, createErrorNotification } = useNotifications();
|
const { createInfoNotification, createErrorNotification } = useNotifications();
|
||||||
const [webauthnShowDetails, setWebauthnShowDetails] = useState<number>(-1);
|
const [webauthnShowDetails, setWebauthnShowDetails] = useState<number>(-1);
|
||||||
const [deletingIdx, setDeletingIdx] = useState<number>(-1);
|
const [deletingIdx, setDeletingIdx] = useState<number>(-1);
|
||||||
|
const [editingIdx, setEditingIdx] = useState<number>(-1);
|
||||||
const [registrationInProgress, setRegistrationInProgress] = useState(false);
|
const [registrationInProgress, setRegistrationInProgress] = useState(false);
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
const [webauthnDevices, setWebauthnDevices] = useState<WebauthnDeviceDisplay[]>([]);
|
const [webauthnDevices, setWebauthnDevices] = useState<WebauthnDeviceDisplay[]>([]);
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async function () {
|
(async function () {
|
||||||
|
@ -90,6 +94,29 @@ export default function WebauthnDevices(props: Props) {
|
||||||
setWebauthnDevices(updatedDevices);
|
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) => {
|
const initiateRegistration = async (initiateRegistrationFunc: () => Promise<void>, redirectRoute: string) => {
|
||||||
if (props.state.authentication_level >= AuthenticationLevel.TwoFactor) {
|
if (props.state.authentication_level >= AuthenticationLevel.TwoFactor) {
|
||||||
navigate(redirectRoute);
|
navigate(redirectRoute);
|
||||||
|
@ -115,6 +142,11 @@ export default function WebauthnDevices(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<WebauthnDeviceEditDialog
|
||||||
|
device={editingIdx > -1 ? webauthnDevices[editingIdx] : undefined}
|
||||||
|
open={editDialogOpen}
|
||||||
|
handleClose={handleEditItemConfirm}
|
||||||
|
/>
|
||||||
<WebauthnDeviceDeleteDialog
|
<WebauthnDeviceDeleteDialog
|
||||||
device={deletingIdx > -1 ? webauthnDevices[deletingIdx] : undefined}
|
device={deletingIdx > -1 ? webauthnDevices[deletingIdx] : undefined}
|
||||||
open={deleteDialogOpen}
|
open={deleteDialogOpen}
|
||||||
|
@ -150,10 +182,14 @@ export default function WebauthnDevices(props: Props) {
|
||||||
<WebauthnDeviceItem
|
<WebauthnDeviceItem
|
||||||
device={x}
|
device={x}
|
||||||
deleting={x.deleting}
|
deleting={x.deleting}
|
||||||
|
editing={x.editing}
|
||||||
webauthnShowDetails={webauthnShowDetails === idx}
|
webauthnShowDetails={webauthnShowDetails === idx}
|
||||||
handleWebAuthnDetailsChange={() => {
|
handleWebAuthnDetailsChange={() => {
|
||||||
handleWebAuthnDetailsChange(idx);
|
handleWebAuthnDetailsChange(idx);
|
||||||
}}
|
}}
|
||||||
|
handleEdit={() => {
|
||||||
|
handleEditItem(idx);
|
||||||
|
}}
|
||||||
handleDelete={() => {
|
handleDelete={() => {
|
||||||
handleDeleteItem(idx);
|
handleDeleteItem(idx);
|
||||||
}}
|
}}
|
||||||
|
|
Loading…
Reference in New Issue