refactor: adjust settings components
parent
f2ee86472d
commit
4239db6171
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import React, { Fragment, useState } from "react";
|
||||||
|
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import EditIcon from "@mui/icons-material/Edit";
|
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 { ButtonProps } from "@mui/material/Button";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { useNotifications } from "@hooks/NotificationsContext";
|
||||||
import { WebauthnDevice } from "@models/Webauthn";
|
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 {
|
interface Props {
|
||||||
|
index: number;
|
||||||
device: WebauthnDevice;
|
device: WebauthnDevice;
|
||||||
deleting: boolean;
|
handleDeviceEdit(index: number, device: WebauthnDevice): void;
|
||||||
editing: boolean;
|
handleDeviceDelete(device: WebauthnDevice): void;
|
||||||
webauthnShowDetails: boolean;
|
|
||||||
handleWebAuthnDetailsChange: () => void;
|
|
||||||
handleDetails: () => void;
|
|
||||||
handleDelete: () => void;
|
|
||||||
handleEdit: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebauthnDeviceItem(props: Props) {
|
export default function WebauthnDeviceItem(props: Props) {
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
|
const { createErrorNotification } = useNotifications();
|
||||||
|
|
||||||
|
const [showDialogDetails, setShowDialogDetails] = useState<boolean>(false);
|
||||||
|
const [showDialogEdit, setShowDialogEdit] = useState<boolean>(false);
|
||||||
|
const [showDialogDelete, setShowDialogDelete] = useState<boolean>(false);
|
||||||
|
const [loadingEdit, setLoadingEdit] = useState<boolean>(false);
|
||||||
|
const [loadingDelete, setLoadingDelete] = useState<boolean>(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 (
|
return (
|
||||||
<Stack direction="row" spacing={1} alignItems="center">
|
<Fragment>
|
||||||
<KeyRoundedIcon fontSize="large" />
|
<WebauthnDeviceDetailsDialog
|
||||||
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
device={props.device}
|
||||||
<Box>
|
open={showDialogDetails}
|
||||||
<Typography display="inline" sx={{ fontWeight: "bold" }}>
|
handleClose={() => {
|
||||||
{props.device.description}
|
setShowDialogDetails(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<WebauthnDeviceEditDialog device={props.device} open={showDialogEdit} handleClose={handleEdit} />
|
||||||
|
<WebauthnDeviceDeleteDialog device={props.device} open={showDialogDelete} handleClose={handleDelete} />
|
||||||
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
|
<KeyRoundedIcon fontSize="large" />
|
||||||
|
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
||||||
|
<Box>
|
||||||
|
<Typography display="inline" sx={{ fontWeight: "bold" }}>
|
||||||
|
{props.device.description}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
display="inline"
|
||||||
|
variant="body2"
|
||||||
|
>{` (${props.device.attestation_type.toUpperCase()})`}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography>Added {props.device.created_at.toString()}</Typography>
|
||||||
|
<Typography>
|
||||||
|
{props.device.last_used_at === undefined
|
||||||
|
? translate("Never used")
|
||||||
|
: "Last used " + props.device.last_used_at.toString()}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
</Stack>
|
||||||
display="inline"
|
<Button
|
||||||
variant="body2"
|
variant="outlined"
|
||||||
>{` (${props.device.attestation_type.toUpperCase()})`}</Typography>
|
color="primary"
|
||||||
</Box>
|
startIcon={<InfoOutlinedIcon />}
|
||||||
<Typography>Added {props.device.created_at.toString()}</Typography>
|
onClick={() => setShowDialogDetails(true)}
|
||||||
<Typography>
|
>
|
||||||
{props.device.last_used_at === undefined
|
{translate("Info")}
|
||||||
? translate("Never used")
|
</Button>
|
||||||
: "Last used " + props.device.last_used_at.toString()}
|
<LoadingButton
|
||||||
</Typography>
|
loading={loadingEdit}
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<EditIcon />}
|
||||||
|
onClick={() => setShowDialogEdit(true)}
|
||||||
|
>
|
||||||
|
{translate("Edit")}
|
||||||
|
</LoadingButton>
|
||||||
|
<LoadingButton
|
||||||
|
loading={loadingDelete}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<DeleteIcon />}
|
||||||
|
onClick={() => setShowDialogDelete(true)}
|
||||||
|
>
|
||||||
|
{translate("Remove")}
|
||||||
|
</LoadingButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Button variant="outlined" color="primary" startIcon={<InfoOutlinedIcon />} onClick={props.handleDetails}>
|
</Fragment>
|
||||||
{translate("Info")}
|
|
||||||
</Button>
|
|
||||||
<LoadingButton
|
|
||||||
loading={props.editing}
|
|
||||||
variant="outlined"
|
|
||||||
color="primary"
|
|
||||||
startIcon={<EditIcon />}
|
|
||||||
onClick={props.handleEdit}
|
|
||||||
>
|
|
||||||
{translate("Edit")}
|
|
||||||
</LoadingButton>
|
|
||||||
<LoadingButton
|
|
||||||
loading={props.deleting}
|
|
||||||
variant="outlined"
|
|
||||||
color="secondary"
|
|
||||||
startIcon={<DeleteIcon />}
|
|
||||||
onClick={props.handleDelete}
|
|
||||||
>
|
|
||||||
{translate("Remove")}
|
|
||||||
</LoadingButton>
|
|
||||||
</Stack>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { RegisterWebauthnRoute } from "@constants/Routes";
|
import { RegisterWebauthnRoute } from "@constants/Routes";
|
||||||
import { useNotifications } from "@hooks/NotificationsContext";
|
import { useNotifications } from "@hooks/NotificationsContext";
|
||||||
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 LoadingPage from "@views/LoadingPage/LoadingPage";
|
||||||
import { deleteDevice, updateDevice } from "@services/Webauthn";
|
import WebauthnDevicesStack from "@views/Settings/TwoFactorAuthentication/WebauthnDevicesStack";
|
||||||
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";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
state: AutheliaState;
|
state: AutheliaState;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WebauthnDeviceDisplay extends WebauthnDevice {
|
|
||||||
deleting: boolean;
|
|
||||||
editing: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function WebauthnDevices(props: Props) {
|
export default function WebauthnDevices(props: Props) {
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { createInfoNotification, createErrorNotification } = useNotifications();
|
const { createInfoNotification, createErrorNotification } = useNotifications();
|
||||||
const [webauthnShowDetails, setWebauthnShowDetails] = useState<number>(-1);
|
|
||||||
const [detailsIdx, setDetailsIdx] = 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 [webauthnDevices, setWebauthnDevices] = useState<WebauthnDeviceDisplay[]>([]);
|
|
||||||
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<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) {
|
||||||
|
@ -140,24 +46,7 @@ export default function WebauthnDevices(props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment>
|
||||||
<WebauthnDeviceDetailsDialog
|
|
||||||
device={detailsIdx > -1 ? webauthnDevices[detailsIdx] : undefined}
|
|
||||||
open={detailsDialogOpen}
|
|
||||||
handleClose={() => {
|
|
||||||
setDetailsDialogOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<WebauthnDeviceEditDialog
|
|
||||||
device={editingIdx > -1 ? webauthnDevices[editingIdx] : undefined}
|
|
||||||
open={editDialogOpen}
|
|
||||||
handleClose={handleEditItemConfirm}
|
|
||||||
/>
|
|
||||||
<WebauthnDeviceDeleteDialog
|
|
||||||
device={deletingIdx > -1 ? webauthnDevices[deletingIdx] : undefined}
|
|
||||||
open={deleteDialogOpen}
|
|
||||||
handleClose={handleDeleteItemConfirm}
|
|
||||||
/>
|
|
||||||
<Paper variant="outlined">
|
<Paper variant="outlined">
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
|
@ -169,41 +58,12 @@ export default function WebauthnDevices(props: Props) {
|
||||||
{"Add new device"}
|
{"Add new device"}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
{ready ? (
|
<Suspense fallback={<LoadingPage />}>
|
||||||
<Stack spacing={3}>
|
<WebauthnDevicesStack />
|
||||||
{webauthnDevices
|
</Suspense>
|
||||||
? webauthnDevices.map((x, idx) => (
|
|
||||||
<WebauthnDeviceItem
|
|
||||||
device={x}
|
|
||||||
deleting={x.deleting}
|
|
||||||
editing={x.editing}
|
|
||||||
webauthnShowDetails={webauthnShowDetails === idx}
|
|
||||||
handleWebAuthnDetailsChange={() => {
|
|
||||||
handleWebAuthnDetailsChange(idx);
|
|
||||||
}}
|
|
||||||
handleDetails={() => {
|
|
||||||
handleDetailsItem(idx);
|
|
||||||
}}
|
|
||||||
handleEdit={() => {
|
|
||||||
handleEditItem(idx);
|
|
||||||
}}
|
|
||||||
handleDelete={() => {
|
|
||||||
handleDeleteItem(idx);
|
|
||||||
}}
|
|
||||||
key={`webauthn-device-${idx}`}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
: null}
|
|
||||||
</Stack>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Skeleton height={20} />
|
|
||||||
<Skeleton height={40} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<WebauthnDevice[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Stack spacing={3}>
|
||||||
|
{devices
|
||||||
|
? devices.map((x, idx) => (
|
||||||
|
<WebauthnDeviceItem
|
||||||
|
index={idx}
|
||||||
|
device={x}
|
||||||
|
handleDeviceEdit={handleEdit}
|
||||||
|
handleDeviceDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue