feat: backport changes
Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>feat-otp-verification
parent
6c89ee1f9c
commit
53668e0c60
|
@ -1,6 +1,9 @@
|
|||
ALTER TABLE webauthn_devices
|
||||
RENAME TO _bkp_DOWN_V0008_webauthn_devices;
|
||||
|
||||
DROP INDEX IF EXISTS webauthn_devices_kid_key;
|
||||
DROP INDEX IF EXISTS webauthn_devices_lookup_key;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webauthn_devices (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import { useRemoteCall } from "@hooks/RemoteCall";
|
||||
import { getUserWebAuthnDevices } from "@services/UserWebAuthnDevices";
|
||||
|
||||
export function useUserWebAuthnDevices() {
|
||||
return useRemoteCall(getUserWebAuthnDevices, []);
|
||||
}
|
|
@ -2,7 +2,12 @@ import { WebAuthnDevice } from "@models/WebAuthn";
|
|||
import { WebAuthnDevicesPath } from "@services/Api";
|
||||
import { GetWithOptionalData } from "@services/Client";
|
||||
|
||||
// getWebAuthnDevices returns the list of webauthn devices for the authenticated user.
|
||||
export async function getWebAuthnDevices(): Promise<WebAuthnDevice[] | null> {
|
||||
return GetWithOptionalData<WebAuthnDevice[] | null>(WebAuthnDevicesPath);
|
||||
export async function getUserWebAuthnDevices(): Promise<WebAuthnDevice[]> {
|
||||
const res = await GetWithOptionalData<WebAuthnDevice[] | null>(WebAuthnDevicesPath);
|
||||
|
||||
if (res === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
|
|
@ -245,7 +245,7 @@ export async function finishRegistration(response: RegistrationResponseJSON) {
|
|||
return result;
|
||||
}
|
||||
|
||||
export async function deleteDevice(deviceID: string) {
|
||||
export async function deleteUserWebAuthnDevice(deviceID: string) {
|
||||
return await axios<AuthenticationOKResponse>({
|
||||
method: "DELETE",
|
||||
url: `${WebAuthnDevicePath}/${deviceID}`,
|
||||
|
@ -253,7 +253,7 @@ export async function deleteDevice(deviceID: string) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function updateDevice(deviceID: string, description: string) {
|
||||
export async function updateUserWebAuthnDevice(deviceID: string, description: string) {
|
||||
return await axios<AuthenticationOKResponse>({
|
||||
method: "PUT",
|
||||
url: `${WebAuthnDevicePath}/${deviceID}`,
|
||||
|
|
|
@ -3,39 +3,33 @@ import React from "react";
|
|||
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { WebAuthnDevice } from "@models/WebAuthn";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
device: WebAuthnDevice;
|
||||
title: string;
|
||||
text: string;
|
||||
handleClose: (ok: boolean) => void;
|
||||
}
|
||||
|
||||
export default function WebAuthnDeviceDeleteDialog(props: Props) {
|
||||
export default function DeleteDialog(props: Props) {
|
||||
const { t: translate } = useTranslation("settings");
|
||||
|
||||
const handleCancel = () => {
|
||||
props.handleClose(false);
|
||||
};
|
||||
|
||||
const handleRemove = () => {
|
||||
props.handleClose(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={props.open} onClose={handleCancel}>
|
||||
<DialogTitle>{translate("Remove WebAuthn Credential")}</DialogTitle>
|
||||
<DialogTitle>{props.title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{translate("Are you sure you want to remove the WebAuthn credential from from your account", {
|
||||
description: props.device.description,
|
||||
})}
|
||||
</DialogContentText>
|
||||
<DialogContentText>{props.text}</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCancel}>{translate("Cancel")}</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
props.handleClose(true);
|
||||
}}
|
||||
autoFocus
|
||||
>
|
||||
<Button onClick={handleRemove} color={"error"}>
|
||||
{translate("Remove")}
|
||||
</Button>
|
||||
</DialogActions>
|
|
@ -1,19 +1,64 @@
|
|||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { Grid } from "@mui/material";
|
||||
import Grid from "@mui/material/Unstable_Grid2";
|
||||
|
||||
import { AutheliaState } from "@services/State";
|
||||
import WebAuthnDevices from "@views/Settings/TwoFactorAuthentication/WebAuthnDevices";
|
||||
import { useNotifications } from "@hooks/NotificationsContext";
|
||||
import { useUserInfoPOST } from "@hooks/UserInfo";
|
||||
import { useUserWebAuthnDevices } from "@hooks/WebAuthnDevices";
|
||||
import WebAuthnDevicesPanel from "@views/Settings/TwoFactorAuthentication/WebAuthnDevicesPanel";
|
||||
|
||||
interface Props {
|
||||
state: AutheliaState;
|
||||
}
|
||||
interface Props {}
|
||||
|
||||
export default function TwoFactorAuthSettings(props: Props) {
|
||||
const [refreshState, setRefreshState] = useState(0);
|
||||
const { createErrorNotification } = useNotifications();
|
||||
const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoPOST();
|
||||
const [userWebAuthnDevices, fetchUserWebAuthnDevices, , fetchUserWebAuthnDevicesError] = useUserWebAuthnDevices();
|
||||
const [hasTOTP, setHasTOTP] = useState(false);
|
||||
const [hasWebAuthn, setHasWebAuthn] = useState(false);
|
||||
|
||||
const handleRefreshState = () => {
|
||||
setRefreshState((refreshState) => refreshState + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserInfo();
|
||||
}, [fetchUserInfo, refreshState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userInfo === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (userInfo.has_webauthn !== hasWebAuthn) {
|
||||
setHasWebAuthn(userInfo.has_webauthn);
|
||||
}
|
||||
|
||||
if (userInfo.has_totp !== hasTOTP) {
|
||||
setHasTOTP(userInfo.has_totp);
|
||||
}
|
||||
}, [hasTOTP, hasWebAuthn, userInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserWebAuthnDevices();
|
||||
}, [fetchUserWebAuthnDevices, hasWebAuthn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchUserInfoError) {
|
||||
createErrorNotification("There was an issue retrieving user preferences");
|
||||
}
|
||||
}, [fetchUserInfoError, createErrorNotification]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchUserWebAuthnDevicesError) {
|
||||
createErrorNotification("There was an issue retrieving One Time Password Configuration");
|
||||
}
|
||||
}, [fetchUserWebAuthnDevicesError, createErrorNotification]);
|
||||
|
||||
return (
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<WebAuthnDevices state={props.state} />
|
||||
<Grid xs={12}>
|
||||
<WebAuthnDevicesPanel devices={userWebAuthnDevices} handleRefreshState={handleRefreshState} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { Fragment, useState } from "react";
|
||||
|
||||
import { Check, ContentCopy } from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
|
@ -10,10 +9,11 @@ import {
|
|||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Stack,
|
||||
Divider,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import Grid from "@mui/material/Unstable_Grid2";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { WebAuthnDevice, toTransportName } from "@models/WebAuthn";
|
||||
|
@ -24,7 +24,7 @@ interface Props {
|
|||
handleClose: () => void;
|
||||
}
|
||||
|
||||
export default function WebAuthnDetailsDeleteDialog(props: Props) {
|
||||
export default function WebAuthnDeviceDetailsDialog(props: Props) {
|
||||
const { t: translate } = useTranslation("settings");
|
||||
|
||||
return (
|
||||
|
@ -36,16 +36,19 @@ export default function WebAuthnDetailsDeleteDialog(props: Props) {
|
|||
description: props.device.description,
|
||||
})}
|
||||
</DialogContentText>
|
||||
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
||||
<Box paddingBottom={2}>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<PropertyCopyButton name={translate("Identifier")} value={props.device.kid.toString()} />
|
||||
<PropertyCopyButton
|
||||
name={translate("Public Key")}
|
||||
value={props.device.public_key.toString()}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Grid container spacing={2}>
|
||||
<Grid md={3} sx={{ display: { xs: "none", md: "block" } }}>
|
||||
<Fragment />
|
||||
</Grid>
|
||||
<Grid xs={4} md={2}>
|
||||
<PropertyCopyButton name={translate("KID")} value={props.device.kid.toString()} />
|
||||
</Grid>
|
||||
<Grid xs={8} md={4}>
|
||||
<PropertyCopyButton name={translate("Public Key")} value={props.device.public_key.toString()} />
|
||||
</Grid>
|
||||
<Grid xs={12}>
|
||||
<Divider />
|
||||
</Grid>
|
||||
<PropertyText name={translate("Description")} value={props.device.description} />
|
||||
<PropertyText name={translate("Relying Party ID")} value={props.device.rpid} />
|
||||
<PropertyText
|
||||
|
@ -53,7 +56,10 @@ export default function WebAuthnDetailsDeleteDialog(props: Props) {
|
|||
value={props.device.aaguid === undefined ? "N/A" : props.device.aaguid}
|
||||
/>
|
||||
<PropertyText name={translate("Attestation Type")} value={props.device.attestation_type} />
|
||||
<PropertyText name={translate("Attachment")} value={props.device.attachment} />
|
||||
<PropertyText
|
||||
name={translate("Attachment")}
|
||||
value={props.device.attachment === "" ? translate("Unknown") : props.device.attachment}
|
||||
/>
|
||||
<PropertyText
|
||||
name={translate("Discoverable")}
|
||||
value={props.device.discoverable ? translate("Yes") : translate("No")}
|
||||
|
@ -85,7 +91,41 @@ export default function WebAuthnDetailsDeleteDialog(props: Props) {
|
|||
value={props.device.clone_warning ? translate("Yes") : translate("No")}
|
||||
/>
|
||||
<PropertyText name={translate("Usage Count")} value={`${props.device.sign_count}`} />
|
||||
</Stack>
|
||||
<PropertyText
|
||||
name={translate("Added")}
|
||||
value={translate("{{when, datetime}}", {
|
||||
when: new Date(props.device.created_at),
|
||||
formatParams: {
|
||||
when: {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<PropertyText
|
||||
name={translate("Last Used")}
|
||||
value={
|
||||
props.device.last_used_at
|
||||
? translate("{{when, datetime}}", {
|
||||
when: new Date(props.device.last_used_at),
|
||||
formatParams: {
|
||||
when: {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
},
|
||||
},
|
||||
})
|
||||
: translate("Never")
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.handleClose}>{translate("Close")}</Button>
|
||||
|
@ -97,6 +137,7 @@ export default function WebAuthnDetailsDeleteDialog(props: Props) {
|
|||
interface PropertyTextProps {
|
||||
name: string;
|
||||
value: string;
|
||||
xs?: number;
|
||||
}
|
||||
|
||||
function PropertyCopyButton(props: PropertyTextProps) {
|
||||
|
@ -144,11 +185,11 @@ function PropertyCopyButton(props: PropertyTextProps) {
|
|||
|
||||
function PropertyText(props: PropertyTextProps) {
|
||||
return (
|
||||
<Box>
|
||||
<Grid xs={props.xs !== undefined ? props.xs : 12}>
|
||||
<Typography display="inline" sx={{ fontWeight: "bold" }}>
|
||||
{`${props.name}: `}
|
||||
</Typography>
|
||||
<Typography display="inline">{props.value}</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import React, { Fragment, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { Fingerprint } from "@mui/icons-material";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import { Box, Button, CircularProgress, Paper, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import { CircularProgress, Paper, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Grid from "@mui/material/Unstable_Grid2";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useNotifications } from "@hooks/NotificationsContext";
|
||||
import { WebAuthnDevice } from "@models/WebAuthn";
|
||||
import { deleteDevice, updateDevice } from "@services/WebAuthn";
|
||||
import WebAuthnDeviceDeleteDialog from "@views/Settings/TwoFactorAuthentication/WebAuthnDeviceDeleteDialog";
|
||||
import { deleteUserWebAuthnDevice, updateUserWebAuthnDevice } from "@services/WebAuthn";
|
||||
import DeleteDialog from "@views/Settings/TwoFactorAuthentication/DeleteDialog";
|
||||
import WebAuthnDeviceDetailsDialog from "@views/Settings/TwoFactorAuthentication/WebAuthnDeviceDetailsDialog";
|
||||
import WebAuthnDeviceEditDialog from "@views/Settings/TwoFactorAuthentication/WebAuthnDeviceEditDialog";
|
||||
|
||||
|
@ -41,7 +43,7 @@ export default function WebAuthnDeviceItem(props: Props) {
|
|||
|
||||
setLoadingEdit(true);
|
||||
|
||||
const response = await updateDevice(props.device.id, name);
|
||||
const response = await updateUserWebAuthnDevice(props.device.id, name);
|
||||
|
||||
setLoadingEdit(false);
|
||||
|
||||
|
@ -73,7 +75,7 @@ export default function WebAuthnDeviceItem(props: Props) {
|
|||
|
||||
setLoadingDelete(true);
|
||||
|
||||
const response = await deleteDevice(props.device.id);
|
||||
const response = await deleteUserWebAuthnDevice(props.device.id);
|
||||
|
||||
setLoadingDelete(false);
|
||||
|
||||
|
@ -97,9 +99,7 @@ export default function WebAuthnDeviceItem(props: Props) {
|
|||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Paper variant="outlined">
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Grid xs={12} md={6} xl={3}>
|
||||
<WebAuthnDeviceDetailsDialog
|
||||
device={props.device}
|
||||
open={showDialogDetails}
|
||||
|
@ -108,15 +108,21 @@ export default function WebAuthnDeviceItem(props: Props) {
|
|||
}}
|
||||
/>
|
||||
<WebAuthnDeviceEditDialog device={props.device} open={showDialogEdit} handleClose={handleEdit} />
|
||||
<WebAuthnDeviceDeleteDialog
|
||||
device={props.device}
|
||||
<DeleteDialog
|
||||
open={showDialogDelete}
|
||||
handleClose={handleDelete}
|
||||
title={translate("Remove WebAuthn Credential")}
|
||||
text={translate("Are you sure you want to remove the WebAuthn credential from from your account", {
|
||||
description: props.device.description,
|
||||
})}
|
||||
/>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Paper variant="outlined">
|
||||
<Grid container spacing={1} alignItems="center" padding={3}>
|
||||
<Grid xs={12} sm={6} md={6}>
|
||||
<Grid container>
|
||||
<Grid xs={12}>
|
||||
<Stack direction={"row"} spacing={1} alignItems={"center"}>
|
||||
<Fingerprint fontSize="large" color={"warning"} />
|
||||
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
||||
<Box>
|
||||
<Typography display="inline" sx={{ fontWeight: "bold" }}>
|
||||
{props.device.description}
|
||||
</Typography>
|
||||
|
@ -124,9 +130,11 @@ export default function WebAuthnDeviceItem(props: Props) {
|
|||
display="inline"
|
||||
variant="body2"
|
||||
>{` (${props.device.attestation_type.toUpperCase()})`}</Typography>
|
||||
</Box>
|
||||
<Typography variant={"caption"}>
|
||||
{translate("Added", {
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid xs={12}>
|
||||
<Typography variant={"caption"} sx={{ display: { xs: "none", md: "block" } }}>
|
||||
{translate("Added when", {
|
||||
when: new Date(props.device.created_at),
|
||||
formatParams: {
|
||||
when: {
|
||||
|
@ -139,10 +147,12 @@ export default function WebAuthnDeviceItem(props: Props) {
|
|||
},
|
||||
})}
|
||||
</Typography>
|
||||
<Typography variant={"caption"}>
|
||||
</Grid>
|
||||
<Grid xs={12}>
|
||||
<Typography variant={"caption"} sx={{ display: { xs: "none", md: "block" } }}>
|
||||
{props.device.last_used_at === undefined
|
||||
? translate("Never used")
|
||||
: translate("Last Used", {
|
||||
: translate("Last Used when", {
|
||||
when: new Date(props.device.last_used_at),
|
||||
formatParams: {
|
||||
when: {
|
||||
|
@ -155,43 +165,36 @@ export default function WebAuthnDeviceItem(props: Props) {
|
|||
},
|
||||
})}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid xs={12} md={7} xl={5}>
|
||||
<Stack direction={"row"} spacing={1}>
|
||||
<Tooltip title={translate("Display extended information for this WebAuthn credential")}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<InfoOutlinedIcon />}
|
||||
onClick={() => setShowDialogDetails(true)}
|
||||
>
|
||||
{translate("Info")}
|
||||
</Button>
|
||||
<IconButton color="primary" onClick={() => setShowDialogDetails(true)}>
|
||||
<InfoOutlinedIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={translate("Edit information for this WebAuthn credential")}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
<IconButton
|
||||
color="primary"
|
||||
startIcon={loadingEdit ? <CircularProgress color="inherit" size={20} /> : <EditIcon />}
|
||||
onClick={loadingEdit ? undefined : () => setShowDialogEdit(true)}
|
||||
>
|
||||
{translate("Edit")}
|
||||
</Button>
|
||||
{loadingEdit ? <CircularProgress color="inherit" size={20} /> : <EditIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={translate("Remove this WebAuthn credential")}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
<IconButton
|
||||
color="primary"
|
||||
startIcon={
|
||||
loadingDelete ? <CircularProgress color="inherit" size={20} /> : <DeleteIcon />
|
||||
}
|
||||
onClick={loadingDelete ? undefined : () => setShowDialogDelete(true)}
|
||||
>
|
||||
{translate("Remove")}
|
||||
</Button>
|
||||
{loadingDelete ? <CircularProgress color="inherit" size={20} /> : <DeleteIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Fragment>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Grid,
|
||||
Step,
|
||||
StepLabel,
|
||||
Stepper,
|
||||
|
@ -16,6 +15,7 @@ import {
|
|||
Theme,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import Grid from "@mui/material/Unstable_Grid2";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import { PublicKeyCredentialCreationOptionsJSON } from "@simplewebauthn/typescript-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
@ -30,7 +30,6 @@ const steps = ["Description", "Verification"];
|
|||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
setCancelled: () => void;
|
||||
}
|
||||
|
||||
|
@ -65,7 +64,7 @@ const WebAuthnDeviceRegisterDialog = function (props: Props) {
|
|||
}, [props]);
|
||||
|
||||
const performCredentialCreation = useCallback(async () => {
|
||||
if (options === null) {
|
||||
if (!props.open || options === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -106,10 +105,10 @@ const WebAuthnDeviceRegisterDialog = function (props: Props) {
|
|||
"Failed to register your device. The identity verification process might have timed out.",
|
||||
);
|
||||
}
|
||||
}, [options, createErrorNotification, handleClose]);
|
||||
}, [props.open, options, createErrorNotification, handleClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state !== WebAuthnTouchState.Failure || activeStep !== 0 || !props.open) {
|
||||
if (!props.open || state !== WebAuthnTouchState.Failure || activeStep !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -126,7 +125,12 @@ const WebAuthnDeviceRegisterDialog = function (props: Props) {
|
|||
})();
|
||||
}, [props.open, activeStep, options, performCredentialCreation]);
|
||||
|
||||
const handleNext = useCallback(async () => {
|
||||
const handleNext = useCallback(() => {
|
||||
if (!props.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
(async function () {
|
||||
if (credentialDescription.length === 0 || credentialDescription.length > 64) {
|
||||
setErrorDescription(true);
|
||||
createErrorNotification(
|
||||
|
@ -159,7 +163,10 @@ const WebAuthnDeviceRegisterDialog = function (props: Props) {
|
|||
translate("Error occurred obtaining the WebAuthn Credential creation options."),
|
||||
);
|
||||
}
|
||||
}, [createErrorNotification, credentialDescription, translate]);
|
||||
|
||||
await performCredentialCreation();
|
||||
})();
|
||||
}, [createErrorNotification, credentialDescription, performCredentialCreation, props.open, translate]);
|
||||
|
||||
const handleCredentialDescription = useCallback(
|
||||
(description: string) => {
|
||||
|
@ -184,7 +191,7 @@ const WebAuthnDeviceRegisterDialog = function (props: Props) {
|
|||
{translate("Enter a description for this credential")}
|
||||
</Typography>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12}>
|
||||
<Grid xs={12}>
|
||||
<TextField
|
||||
inputRef={nameRef}
|
||||
id="name-textfield"
|
||||
|
@ -225,7 +232,7 @@ const WebAuthnDeviceRegisterDialog = function (props: Props) {
|
|||
}
|
||||
|
||||
const handleOnClose = () => {
|
||||
if (activeStep === 0 || !props.open) {
|
||||
if (!props.open || activeStep === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -242,7 +249,7 @@ const WebAuthnDeviceRegisterDialog = function (props: Props) {
|
|||
)}
|
||||
</DialogContentText>
|
||||
<Grid container spacing={0} alignItems={"center"} justifyContent={"center"} textAlign={"center"}>
|
||||
<Grid item xs={12}>
|
||||
<Grid xs={12}>
|
||||
<Stepper activeStep={activeStep}>
|
||||
{steps.map((label, index) => {
|
||||
const stepProps: { completed?: boolean } = {};
|
||||
|
@ -257,9 +264,7 @@ const WebAuthnDeviceRegisterDialog = function (props: Props) {
|
|||
})}
|
||||
</Stepper>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
{renderStep(activeStep)}
|
||||
</Grid>
|
||||
<Grid xs={12}>{renderStep(activeStep)}</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
import React, { Fragment, Suspense, useState } from "react";
|
||||
|
||||
import { Box, Button, Paper, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { AutheliaState } from "@services/State";
|
||||
import LoadingPage from "@views/LoadingPage/LoadingPage";
|
||||
import WebAuthnDeviceRegisterDialog from "@views/Settings/TwoFactorAuthentication/WebAuthnDeviceRegisterDialog";
|
||||
import WebAuthnDevicesStack from "@views/Settings/TwoFactorAuthentication/WebAuthnDevicesStack";
|
||||
|
||||
interface Props {
|
||||
state: AutheliaState;
|
||||
}
|
||||
|
||||
export default function WebAuthnDevices(props: Props) {
|
||||
const { t: translate } = useTranslation("settings");
|
||||
|
||||
const [showWebAuthnDeviceRegisterDialog, setShowWebAuthnDeviceRegisterDialog] = useState<boolean>(false);
|
||||
const [refreshState, setRefreshState] = useState<number>(0);
|
||||
|
||||
const handleIncrementRefreshState = () => {
|
||||
setRefreshState((refreshState) => refreshState + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<WebAuthnDeviceRegisterDialog
|
||||
open={showWebAuthnDeviceRegisterDialog}
|
||||
onClose={() => {
|
||||
handleIncrementRefreshState();
|
||||
}}
|
||||
setCancelled={() => {
|
||||
setShowWebAuthnDeviceRegisterDialog(false);
|
||||
handleIncrementRefreshState();
|
||||
}}
|
||||
/>
|
||||
<Paper variant="outlined">
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Stack spacing={2}>
|
||||
<Box>
|
||||
<Typography variant="h5">{translate("WebAuthn Credentials")}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Tooltip title={translate("Click to add a WebAuthn credential to your account")}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setShowWebAuthnDeviceRegisterDialog(true);
|
||||
}}
|
||||
>
|
||||
{translate("Add Credential")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Suspense fallback={<LoadingPage />}>
|
||||
<WebAuthnDevicesStack
|
||||
refreshState={refreshState}
|
||||
incrementRefreshState={handleIncrementRefreshState}
|
||||
/>
|
||||
</Suspense>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import React, { Fragment, useState } from "react";
|
||||
|
||||
import { Button, Paper, Tooltip, Typography } from "@mui/material";
|
||||
import Grid from "@mui/material/Unstable_Grid2";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { WebAuthnDevice } from "@models/WebAuthn";
|
||||
import WebAuthnDeviceRegisterDialog from "@views/Settings/TwoFactorAuthentication/WebAuthnDeviceRegisterDialog";
|
||||
import WebAuthnDevicesStack from "@views/Settings/TwoFactorAuthentication/WebAuthnDevicesStack";
|
||||
|
||||
interface Props {
|
||||
devices: WebAuthnDevice[] | undefined;
|
||||
handleRefreshState: () => void;
|
||||
}
|
||||
|
||||
export default function WebAuthnDevicesPanel(props: Props) {
|
||||
const { t: translate } = useTranslation("settings");
|
||||
|
||||
const [showRegisterDialog, setShowRegisterDialog] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<WebAuthnDeviceRegisterDialog
|
||||
open={showRegisterDialog}
|
||||
setCancelled={() => {
|
||||
setShowRegisterDialog(false);
|
||||
props.handleRefreshState();
|
||||
}}
|
||||
/>
|
||||
<Paper variant={"outlined"}>
|
||||
<Grid container spacing={2} padding={2}>
|
||||
<Grid xs={12}>
|
||||
<Typography variant="h5">{translate("WebAuthn Credentials")}</Typography>
|
||||
</Grid>
|
||||
<Grid xs={4} md={2}>
|
||||
<Tooltip title={translate("Click to add a WebAuthn credential to your account")}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setShowRegisterDialog(true);
|
||||
}}
|
||||
>
|
||||
{translate("Add")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid xs={12}>
|
||||
{props.devices === undefined || props.devices.length === 0 ? (
|
||||
<Typography variant={"subtitle2"}>
|
||||
{translate(
|
||||
"No WebAuthn Credentials have been registered. If you'd like to register one click add.",
|
||||
)}
|
||||
</Typography>
|
||||
) : (
|
||||
<WebAuthnDevicesStack
|
||||
devices={props.devices}
|
||||
handleRefreshState={props.handleRefreshState}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -1,41 +1,21 @@
|
|||
import React, { Fragment, useEffect, useState } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Grid from "@mui/material/Unstable_Grid2";
|
||||
|
||||
import { WebAuthnDevice } from "@models/WebAuthn";
|
||||
import { getWebAuthnDevices } from "@services/UserWebAuthnDevices";
|
||||
import WebAuthnDeviceItem from "@views/Settings/TwoFactorAuthentication/WebAuthnDeviceItem";
|
||||
|
||||
interface Props {
|
||||
refreshState: number;
|
||||
incrementRefreshState: () => void;
|
||||
devices: WebAuthnDevice[];
|
||||
handleRefreshState: () => void;
|
||||
}
|
||||
|
||||
export default function WebAuthnDevicesStack(props: Props) {
|
||||
const { t: translate } = useTranslation("settings");
|
||||
|
||||
const [devices, setDevices] = useState<WebAuthnDevice[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
setDevices(null);
|
||||
const devices = await getWebAuthnDevices();
|
||||
setDevices(devices);
|
||||
})();
|
||||
}, [props.refreshState]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{devices !== null && devices.length !== 0 ? (
|
||||
<Stack spacing={3}>
|
||||
{devices.map((x, idx) => (
|
||||
<WebAuthnDeviceItem key={idx} index={idx} device={x} handleEdit={props.incrementRefreshState} />
|
||||
<Grid container spacing={3}>
|
||||
{props.devices.map((x, idx) => (
|
||||
<WebAuthnDeviceItem key={idx} index={idx} device={x} handleEdit={props.handleRefreshState} />
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Typography>{translate("No Registered WebAuthn Credentials")}</Typography>
|
||||
)}
|
||||
</Fragment>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue