Resurrect
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
|
||||
/** Legacy alias, remove once mobile code is updated (it is still in beta). */
|
||||
const Page = () => {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
router.push("/passkeys/setup");
|
||||
window.location.href = window.location.href.replace(
|
||||
"account-handoff",
|
||||
"passkeys",
|
||||
);
|
||||
}, []);
|
||||
|
||||
return <></>;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
|
||||
/** Legacy alias, remove once mobile code is updated (it is still in beta). */
|
||||
const Page = () => {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
router.push("/passkeys/verify");
|
||||
window.location.href = window.location.href.replace("flow", "verify");
|
||||
}, []);
|
||||
|
||||
return <></>;
|
||||
|
||||
@@ -1,418 +1,59 @@
|
||||
import log from "@/next/log";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import { CenteredFlex } from "@ente/shared/components/Container";
|
||||
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
|
||||
import EnteButton from "@ente/shared/components/EnteButton";
|
||||
import { EnteDrawer } from "@ente/shared/components/EnteDrawer";
|
||||
import FormPaper from "@ente/shared/components/Form/FormPaper";
|
||||
import InfoItem from "@ente/shared/components/Info/InfoItem";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import MenuItemDivider from "@ente/shared/components/Menu/MenuItemDivider";
|
||||
import { MenuItemGroup } from "@ente/shared/components/Menu/MenuItemGroup";
|
||||
import SingleInputForm from "@ente/shared/components/SingleInputForm";
|
||||
import Titlebar from "@ente/shared/components/Titlebar";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
import { formatDateTimeFull } from "@ente/shared/time/format";
|
||||
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import KeyIcon from "@mui/icons-material/Key";
|
||||
import { Box, Button, Stack, Typography, useMediaQuery } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import _sodium from "libsodium-wrappers";
|
||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
|
||||
import { useRouter } from "next/router";
|
||||
import { useAppContext } from "pages/_app";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import {
|
||||
Fragment,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { deletePasskey, renamePasskey } from "services/passkey";
|
||||
import {
|
||||
finishPasskeyRegistration,
|
||||
getPasskeyRegistrationOptions,
|
||||
getPasskeys,
|
||||
type Passkey,
|
||||
} from "../../services/passkey";
|
||||
|
||||
export const PasskeysContext = createContext(
|
||||
{} as {
|
||||
selectedPasskey: Passkey | null;
|
||||
setSelectedPasskey: Dispatch<SetStateAction<Passkey | null>>;
|
||||
setShowPasskeyDrawer: Dispatch<SetStateAction<boolean>>;
|
||||
refreshPasskeys: () => void;
|
||||
},
|
||||
);
|
||||
|
||||
const Passkeys = () => {
|
||||
const { showNavBar } = useAppContext();
|
||||
|
||||
const [selectedPasskey, setSelectedPasskey] = useState<Passkey | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const [showPasskeyDrawer, setShowPasskeyDrawer] = useState(false);
|
||||
|
||||
const [passkeys, setPasskeys] = useState<Passkey[]>([]);
|
||||
import { useEffect } from "react";
|
||||
|
||||
const AccountHandoff = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const checkLoggedIn = () => {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
const retrieveAccountData = () => {
|
||||
try {
|
||||
extractAccountsToken();
|
||||
|
||||
router.push("/passkeys");
|
||||
} catch (e) {
|
||||
log.error("Failed to deserialize and set passed user data", e);
|
||||
// Not much we can do here, but this redirect might be misleading.
|
||||
router.push("/login");
|
||||
}
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
checkLoggedIn();
|
||||
const data = await getPasskeys();
|
||||
setPasskeys(data.passkeys || []);
|
||||
const getClientPackageName = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const pkg = urlParams.get("package");
|
||||
if (!pkg) return;
|
||||
setData(LS_KEYS.CLIENT_PACKAGE, { name: pkg });
|
||||
HTTPService.setHeaders({
|
||||
"X-Client-Package": pkg,
|
||||
});
|
||||
};
|
||||
|
||||
const extractAccountsToken = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const token = urlParams.get("token");
|
||||
if (!token) {
|
||||
throw new Error("token not found");
|
||||
}
|
||||
|
||||
const user = getData(LS_KEYS.USER) || {};
|
||||
user.token = token;
|
||||
|
||||
setData(LS_KEYS.USER, user);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
showNavBar(true);
|
||||
init();
|
||||
getClientPackageName();
|
||||
retrieveAccountData();
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (
|
||||
inputValue: string,
|
||||
setFieldError: (errorMessage: string) => void,
|
||||
resetForm: () => void,
|
||||
) => {
|
||||
let response: {
|
||||
options: {
|
||||
publicKey: PublicKeyCredentialCreationOptions;
|
||||
};
|
||||
sessionID: string;
|
||||
};
|
||||
|
||||
try {
|
||||
response = await getPasskeyRegistrationOptions();
|
||||
} catch {
|
||||
setFieldError("Failed to begin registration");
|
||||
return;
|
||||
}
|
||||
|
||||
const options = response.options;
|
||||
|
||||
// TODO-PK: The types don't match.
|
||||
options.publicKey.challenge = _sodium.from_base64(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
options.publicKey.challenge,
|
||||
);
|
||||
options.publicKey.user.id = _sodium.from_base64(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
options.publicKey.user.id,
|
||||
);
|
||||
|
||||
// create new credential
|
||||
let newCredential: Credential;
|
||||
|
||||
try {
|
||||
newCredential = ensure(await navigator.credentials.create(options));
|
||||
} catch (e) {
|
||||
log.error("Error creating credential", e);
|
||||
setFieldError("Failed to create credential");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await finishPasskeyRegistration(
|
||||
inputValue,
|
||||
newCredential,
|
||||
response.sessionID,
|
||||
);
|
||||
} catch {
|
||||
setFieldError("Failed to finish registration");
|
||||
return;
|
||||
}
|
||||
|
||||
await init();
|
||||
resetForm();
|
||||
};
|
||||
|
||||
return (
|
||||
<PasskeysContext.Provider
|
||||
value={{
|
||||
selectedPasskey,
|
||||
setSelectedPasskey,
|
||||
setShowPasskeyDrawer,
|
||||
refreshPasskeys: init,
|
||||
}}
|
||||
>
|
||||
<CenteredFlex>
|
||||
<Box maxWidth="20rem">
|
||||
<Box marginBottom="1rem">
|
||||
<Typography>{t("PASSKEYS_DESCRIPTION")}</Typography>
|
||||
</Box>
|
||||
<FormPaper
|
||||
style={{
|
||||
padding: "1rem",
|
||||
}}
|
||||
>
|
||||
<SingleInputForm
|
||||
fieldType="text"
|
||||
placeholder={t("ENTER_PASSKEY_NAME")}
|
||||
buttonText={t("ADD_PASSKEY")}
|
||||
initialValue={""}
|
||||
callback={handleSubmit}
|
||||
submitButtonProps={{
|
||||
sx: {
|
||||
marginBottom: 1,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormPaper>
|
||||
<Box marginTop="1rem">
|
||||
<PasskeysList passkeys={passkeys} />
|
||||
</Box>
|
||||
</Box>
|
||||
</CenteredFlex>
|
||||
<ManagePasskeyDrawer open={showPasskeyDrawer} />
|
||||
</PasskeysContext.Provider>
|
||||
<VerticallyCentered>
|
||||
<EnteSpinner />
|
||||
</VerticallyCentered>
|
||||
);
|
||||
};
|
||||
|
||||
export default Passkeys;
|
||||
|
||||
interface PasskeysListProps {
|
||||
passkeys: Passkey[];
|
||||
}
|
||||
|
||||
const PasskeysList: React.FC<PasskeysListProps> = ({ passkeys }) => {
|
||||
return (
|
||||
<MenuItemGroup>
|
||||
{passkeys.map((passkey, i) => (
|
||||
<Fragment key={passkey.id}>
|
||||
<PasskeyListItem passkey={passkey} />
|
||||
{i < passkeys.length - 1 && <MenuItemDivider />}
|
||||
</Fragment>
|
||||
))}
|
||||
</MenuItemGroup>
|
||||
);
|
||||
};
|
||||
|
||||
interface PasskeyListItemProps {
|
||||
passkey: Passkey;
|
||||
}
|
||||
|
||||
const PasskeyListItem: React.FC<PasskeyListItemProps> = ({ passkey }) => {
|
||||
const { setSelectedPasskey, setShowPasskeyDrawer } =
|
||||
useContext(PasskeysContext);
|
||||
|
||||
return (
|
||||
<EnteMenuItem
|
||||
onClick={() => {
|
||||
setSelectedPasskey(passkey);
|
||||
setShowPasskeyDrawer(true);
|
||||
}}
|
||||
startIcon={<KeyIcon />}
|
||||
endIcon={<ChevronRightIcon />}
|
||||
label={passkey?.friendlyName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface ManagePasskeyDrawerProps {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
const ManagePasskeyDrawer: React.FC<ManagePasskeyDrawerProps> = (props) => {
|
||||
const { setShowPasskeyDrawer, refreshPasskeys, selectedPasskey } =
|
||||
useContext(PasskeysContext);
|
||||
|
||||
const [showDeletePasskeyModal, setShowDeletePasskeyModal] = useState(false);
|
||||
const [showRenamePasskeyModal, setShowRenamePasskeyModal] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EnteDrawer
|
||||
anchor="right"
|
||||
open={props.open}
|
||||
onClose={() => {
|
||||
setShowPasskeyDrawer(false);
|
||||
}}
|
||||
>
|
||||
{selectedPasskey && (
|
||||
<>
|
||||
<Stack spacing={"4px"} py={"12px"}>
|
||||
<Titlebar
|
||||
onClose={() => {
|
||||
setShowPasskeyDrawer(false);
|
||||
}}
|
||||
title="Manage Passkey"
|
||||
onRootClose={() => {
|
||||
setShowPasskeyDrawer(false);
|
||||
}}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={<CalendarTodayIcon />}
|
||||
title={t("CREATED_AT")}
|
||||
caption={
|
||||
`${formatDateTimeFull(
|
||||
selectedPasskey.createdAt / 1000,
|
||||
)}` || ""
|
||||
}
|
||||
loading={!selectedPasskey}
|
||||
hideEditOption
|
||||
/>
|
||||
<MenuItemGroup>
|
||||
<EnteMenuItem
|
||||
onClick={() => {
|
||||
setShowRenamePasskeyModal(true);
|
||||
}}
|
||||
startIcon={<EditIcon />}
|
||||
label={"Rename Passkey"}
|
||||
/>
|
||||
<MenuItemDivider />
|
||||
<EnteMenuItem
|
||||
onClick={() => {
|
||||
setShowDeletePasskeyModal(true);
|
||||
}}
|
||||
startIcon={<DeleteIcon />}
|
||||
label={"Delete Passkey"}
|
||||
color="critical"
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</EnteDrawer>
|
||||
<DeletePasskeyModal
|
||||
open={showDeletePasskeyModal}
|
||||
onClose={() => {
|
||||
setShowDeletePasskeyModal(false);
|
||||
refreshPasskeys();
|
||||
}}
|
||||
/>
|
||||
<RenamePasskeyModal
|
||||
open={showRenamePasskeyModal}
|
||||
onClose={() => {
|
||||
setShowRenamePasskeyModal(false);
|
||||
refreshPasskeys();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface DeletePasskeyModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DeletePasskeyModal: React.FC<DeletePasskeyModalProps> = (props) => {
|
||||
const { selectedPasskey, setShowPasskeyDrawer } =
|
||||
useContext(PasskeysContext);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const isMobile = useMediaQuery("(max-width: 428px)");
|
||||
|
||||
const doDelete = async () => {
|
||||
if (!selectedPasskey) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await deletePasskey(selectedPasskey.id);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
props.onClose();
|
||||
setShowPasskeyDrawer(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogBoxV2
|
||||
fullWidth
|
||||
open={props.open}
|
||||
onClose={props.onClose}
|
||||
fullScreen={isMobile}
|
||||
attributes={{
|
||||
title: t("DELETE_PASSKEY"),
|
||||
secondary: {
|
||||
action: props.onClose,
|
||||
text: t("CANCEL"),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack spacing={"8px"}>
|
||||
<Typography>{t("DELETE_PASSKEY_CONFIRMATION")}</Typography>
|
||||
<EnteButton
|
||||
type="submit"
|
||||
size="large"
|
||||
color="critical"
|
||||
loading={loading}
|
||||
onClick={doDelete}
|
||||
>
|
||||
{t("DELETE")}
|
||||
</EnteButton>
|
||||
<Button
|
||||
size="large"
|
||||
color={"secondary"}
|
||||
onClick={props.onClose}
|
||||
>
|
||||
{t("CANCEL")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</DialogBoxV2>
|
||||
);
|
||||
};
|
||||
|
||||
interface RenamePasskeyModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const RenamePasskeyModal: React.FC<RenamePasskeyModalProps> = (props) => {
|
||||
const { selectedPasskey } = useContext(PasskeysContext);
|
||||
|
||||
const isMobile = useMediaQuery("(max-width: 428px)");
|
||||
|
||||
const onSubmit = async (inputValue: string) => {
|
||||
if (!selectedPasskey) return;
|
||||
try {
|
||||
await renamePasskey(selectedPasskey.id, inputValue);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogBoxV2
|
||||
fullWidth
|
||||
open={props.open}
|
||||
onClose={props.onClose}
|
||||
fullScreen={isMobile}
|
||||
attributes={{
|
||||
title: t("RENAME_PASSKEY"),
|
||||
secondary: {
|
||||
action: props.onClose,
|
||||
text: t("CANCEL"),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SingleInputForm
|
||||
initialValue={selectedPasskey?.friendlyName}
|
||||
callback={onSubmit}
|
||||
placeholder={t("ENTER_PASSKEY_NAME")}
|
||||
buttonText={t("RENAME")}
|
||||
fieldType="text"
|
||||
secondaryButtonAction={props.onClose}
|
||||
submitButtonProps={{ sx: { mt: 1, mb: 2 } }}
|
||||
/>
|
||||
</DialogBoxV2>
|
||||
);
|
||||
};
|
||||
export default AccountHandoff;
|
||||
|
||||
@@ -1,59 +1,418 @@
|
||||
import log from "@/next/log";
|
||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import { CenteredFlex } from "@ente/shared/components/Container";
|
||||
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
|
||||
import EnteButton from "@ente/shared/components/EnteButton";
|
||||
import { EnteDrawer } from "@ente/shared/components/EnteDrawer";
|
||||
import FormPaper from "@ente/shared/components/Form/FormPaper";
|
||||
import InfoItem from "@ente/shared/components/Info/InfoItem";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
import MenuItemDivider from "@ente/shared/components/Menu/MenuItemDivider";
|
||||
import { MenuItemGroup } from "@ente/shared/components/Menu/MenuItemGroup";
|
||||
import SingleInputForm from "@ente/shared/components/SingleInputForm";
|
||||
import Titlebar from "@ente/shared/components/Titlebar";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
import { formatDateTimeFull } from "@ente/shared/time/format";
|
||||
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import KeyIcon from "@mui/icons-material/Key";
|
||||
import { Box, Button, Stack, Typography, useMediaQuery } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import _sodium from "libsodium-wrappers";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { useAppContext } from "pages/_app";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import {
|
||||
Fragment,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { deletePasskey, renamePasskey } from "services/passkey";
|
||||
import {
|
||||
finishPasskeyRegistration,
|
||||
getPasskeyRegistrationOptions,
|
||||
getPasskeys,
|
||||
type Passkey,
|
||||
} from "../../services/passkey";
|
||||
|
||||
export const PasskeysContext = createContext(
|
||||
{} as {
|
||||
selectedPasskey: Passkey | null;
|
||||
setSelectedPasskey: Dispatch<SetStateAction<Passkey | null>>;
|
||||
setShowPasskeyDrawer: Dispatch<SetStateAction<boolean>>;
|
||||
refreshPasskeys: () => void;
|
||||
},
|
||||
);
|
||||
|
||||
const Passkeys = () => {
|
||||
const { showNavBar } = useAppContext();
|
||||
|
||||
const [selectedPasskey, setSelectedPasskey] = useState<Passkey | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const [showPasskeyDrawer, setShowPasskeyDrawer] = useState(false);
|
||||
|
||||
const [passkeys, setPasskeys] = useState<Passkey[]>([]);
|
||||
|
||||
const AccountHandoff = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const retrieveAccountData = () => {
|
||||
try {
|
||||
extractAccountsToken();
|
||||
|
||||
router.push("/passkeys");
|
||||
} catch (e) {
|
||||
log.error("Failed to deserialize and set passed user data", e);
|
||||
// Not much we can do here, but this redirect might be misleading.
|
||||
const checkLoggedIn = () => {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
router.push("/login");
|
||||
}
|
||||
};
|
||||
|
||||
const getClientPackageName = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const pkg = urlParams.get("package");
|
||||
if (!pkg) return;
|
||||
setData(LS_KEYS.CLIENT_PACKAGE, { name: pkg });
|
||||
HTTPService.setHeaders({
|
||||
"X-Client-Package": pkg,
|
||||
});
|
||||
};
|
||||
|
||||
const extractAccountsToken = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const token = urlParams.get("token");
|
||||
if (!token) {
|
||||
throw new Error("token not found");
|
||||
}
|
||||
|
||||
const user = getData(LS_KEYS.USER) || {};
|
||||
user.token = token;
|
||||
|
||||
setData(LS_KEYS.USER, user);
|
||||
const init = async () => {
|
||||
checkLoggedIn();
|
||||
const data = await getPasskeys();
|
||||
setPasskeys(data.passkeys || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getClientPackageName();
|
||||
retrieveAccountData();
|
||||
showNavBar(true);
|
||||
init();
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (
|
||||
inputValue: string,
|
||||
setFieldError: (errorMessage: string) => void,
|
||||
resetForm: () => void,
|
||||
) => {
|
||||
let response: {
|
||||
options: {
|
||||
publicKey: PublicKeyCredentialCreationOptions;
|
||||
};
|
||||
sessionID: string;
|
||||
};
|
||||
|
||||
try {
|
||||
response = await getPasskeyRegistrationOptions();
|
||||
} catch {
|
||||
setFieldError("Failed to begin registration");
|
||||
return;
|
||||
}
|
||||
|
||||
const options = response.options;
|
||||
|
||||
// TODO-PK: The types don't match.
|
||||
options.publicKey.challenge = _sodium.from_base64(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
options.publicKey.challenge,
|
||||
);
|
||||
options.publicKey.user.id = _sodium.from_base64(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
options.publicKey.user.id,
|
||||
);
|
||||
|
||||
// create new credential
|
||||
let newCredential: Credential;
|
||||
|
||||
try {
|
||||
newCredential = ensure(await navigator.credentials.create(options));
|
||||
} catch (e) {
|
||||
log.error("Error creating credential", e);
|
||||
setFieldError("Failed to create credential");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await finishPasskeyRegistration(
|
||||
inputValue,
|
||||
newCredential,
|
||||
response.sessionID,
|
||||
);
|
||||
} catch {
|
||||
setFieldError("Failed to finish registration");
|
||||
return;
|
||||
}
|
||||
|
||||
await init();
|
||||
resetForm();
|
||||
};
|
||||
|
||||
return (
|
||||
<VerticallyCentered>
|
||||
<EnteSpinner />
|
||||
</VerticallyCentered>
|
||||
<PasskeysContext.Provider
|
||||
value={{
|
||||
selectedPasskey,
|
||||
setSelectedPasskey,
|
||||
setShowPasskeyDrawer,
|
||||
refreshPasskeys: init,
|
||||
}}
|
||||
>
|
||||
<CenteredFlex>
|
||||
<Box maxWidth="20rem">
|
||||
<Box marginBottom="1rem">
|
||||
<Typography>{t("PASSKEYS_DESCRIPTION")}</Typography>
|
||||
</Box>
|
||||
<FormPaper
|
||||
style={{
|
||||
padding: "1rem",
|
||||
}}
|
||||
>
|
||||
<SingleInputForm
|
||||
fieldType="text"
|
||||
placeholder={t("ENTER_PASSKEY_NAME")}
|
||||
buttonText={t("ADD_PASSKEY")}
|
||||
initialValue={""}
|
||||
callback={handleSubmit}
|
||||
submitButtonProps={{
|
||||
sx: {
|
||||
marginBottom: 1,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormPaper>
|
||||
<Box marginTop="1rem">
|
||||
<PasskeysList passkeys={passkeys} />
|
||||
</Box>
|
||||
</Box>
|
||||
</CenteredFlex>
|
||||
<ManagePasskeyDrawer open={showPasskeyDrawer} />
|
||||
</PasskeysContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountHandoff;
|
||||
export default Passkeys;
|
||||
|
||||
interface PasskeysListProps {
|
||||
passkeys: Passkey[];
|
||||
}
|
||||
|
||||
const PasskeysList: React.FC<PasskeysListProps> = ({ passkeys }) => {
|
||||
return (
|
||||
<MenuItemGroup>
|
||||
{passkeys.map((passkey, i) => (
|
||||
<Fragment key={passkey.id}>
|
||||
<PasskeyListItem passkey={passkey} />
|
||||
{i < passkeys.length - 1 && <MenuItemDivider />}
|
||||
</Fragment>
|
||||
))}
|
||||
</MenuItemGroup>
|
||||
);
|
||||
};
|
||||
|
||||
interface PasskeyListItemProps {
|
||||
passkey: Passkey;
|
||||
}
|
||||
|
||||
const PasskeyListItem: React.FC<PasskeyListItemProps> = ({ passkey }) => {
|
||||
const { setSelectedPasskey, setShowPasskeyDrawer } =
|
||||
useContext(PasskeysContext);
|
||||
|
||||
return (
|
||||
<EnteMenuItem
|
||||
onClick={() => {
|
||||
setSelectedPasskey(passkey);
|
||||
setShowPasskeyDrawer(true);
|
||||
}}
|
||||
startIcon={<KeyIcon />}
|
||||
endIcon={<ChevronRightIcon />}
|
||||
label={passkey?.friendlyName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface ManagePasskeyDrawerProps {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
const ManagePasskeyDrawer: React.FC<ManagePasskeyDrawerProps> = (props) => {
|
||||
const { setShowPasskeyDrawer, refreshPasskeys, selectedPasskey } =
|
||||
useContext(PasskeysContext);
|
||||
|
||||
const [showDeletePasskeyModal, setShowDeletePasskeyModal] = useState(false);
|
||||
const [showRenamePasskeyModal, setShowRenamePasskeyModal] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EnteDrawer
|
||||
anchor="right"
|
||||
open={props.open}
|
||||
onClose={() => {
|
||||
setShowPasskeyDrawer(false);
|
||||
}}
|
||||
>
|
||||
{selectedPasskey && (
|
||||
<>
|
||||
<Stack spacing={"4px"} py={"12px"}>
|
||||
<Titlebar
|
||||
onClose={() => {
|
||||
setShowPasskeyDrawer(false);
|
||||
}}
|
||||
title="Manage Passkey"
|
||||
onRootClose={() => {
|
||||
setShowPasskeyDrawer(false);
|
||||
}}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={<CalendarTodayIcon />}
|
||||
title={t("CREATED_AT")}
|
||||
caption={
|
||||
`${formatDateTimeFull(
|
||||
selectedPasskey.createdAt / 1000,
|
||||
)}` || ""
|
||||
}
|
||||
loading={!selectedPasskey}
|
||||
hideEditOption
|
||||
/>
|
||||
<MenuItemGroup>
|
||||
<EnteMenuItem
|
||||
onClick={() => {
|
||||
setShowRenamePasskeyModal(true);
|
||||
}}
|
||||
startIcon={<EditIcon />}
|
||||
label={"Rename Passkey"}
|
||||
/>
|
||||
<MenuItemDivider />
|
||||
<EnteMenuItem
|
||||
onClick={() => {
|
||||
setShowDeletePasskeyModal(true);
|
||||
}}
|
||||
startIcon={<DeleteIcon />}
|
||||
label={"Delete Passkey"}
|
||||
color="critical"
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</EnteDrawer>
|
||||
<DeletePasskeyModal
|
||||
open={showDeletePasskeyModal}
|
||||
onClose={() => {
|
||||
setShowDeletePasskeyModal(false);
|
||||
refreshPasskeys();
|
||||
}}
|
||||
/>
|
||||
<RenamePasskeyModal
|
||||
open={showRenamePasskeyModal}
|
||||
onClose={() => {
|
||||
setShowRenamePasskeyModal(false);
|
||||
refreshPasskeys();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface DeletePasskeyModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DeletePasskeyModal: React.FC<DeletePasskeyModalProps> = (props) => {
|
||||
const { selectedPasskey, setShowPasskeyDrawer } =
|
||||
useContext(PasskeysContext);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const isMobile = useMediaQuery("(max-width: 428px)");
|
||||
|
||||
const doDelete = async () => {
|
||||
if (!selectedPasskey) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await deletePasskey(selectedPasskey.id);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
props.onClose();
|
||||
setShowPasskeyDrawer(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogBoxV2
|
||||
fullWidth
|
||||
open={props.open}
|
||||
onClose={props.onClose}
|
||||
fullScreen={isMobile}
|
||||
attributes={{
|
||||
title: t("DELETE_PASSKEY"),
|
||||
secondary: {
|
||||
action: props.onClose,
|
||||
text: t("CANCEL"),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack spacing={"8px"}>
|
||||
<Typography>{t("DELETE_PASSKEY_CONFIRMATION")}</Typography>
|
||||
<EnteButton
|
||||
type="submit"
|
||||
size="large"
|
||||
color="critical"
|
||||
loading={loading}
|
||||
onClick={doDelete}
|
||||
>
|
||||
{t("DELETE")}
|
||||
</EnteButton>
|
||||
<Button
|
||||
size="large"
|
||||
color={"secondary"}
|
||||
onClick={props.onClose}
|
||||
>
|
||||
{t("CANCEL")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</DialogBoxV2>
|
||||
);
|
||||
};
|
||||
|
||||
interface RenamePasskeyModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const RenamePasskeyModal: React.FC<RenamePasskeyModalProps> = (props) => {
|
||||
const { selectedPasskey } = useContext(PasskeysContext);
|
||||
|
||||
const isMobile = useMediaQuery("(max-width: 428px)");
|
||||
|
||||
const onSubmit = async (inputValue: string) => {
|
||||
if (!selectedPasskey) return;
|
||||
try {
|
||||
await renamePasskey(selectedPasskey.id, inputValue);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogBoxV2
|
||||
fullWidth
|
||||
open={props.open}
|
||||
onClose={props.onClose}
|
||||
fullScreen={isMobile}
|
||||
attributes={{
|
||||
title: t("RENAME_PASSKEY"),
|
||||
secondary: {
|
||||
action: props.onClose,
|
||||
text: t("CANCEL"),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SingleInputForm
|
||||
initialValue={selectedPasskey?.friendlyName}
|
||||
callback={onSubmit}
|
||||
placeholder={t("ENTER_PASSKEY_NAME")}
|
||||
buttonText={t("RENAME")}
|
||||
fieldType="text"
|
||||
secondaryButtonAction={props.onClose}
|
||||
submitButtonProps={{ sx: { mt: 1, mb: 2 } }}
|
||||
/>
|
||||
</DialogBoxV2>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -509,7 +509,7 @@ const UtilitySection: React.FC<UtilitySectionProps> = ({ closeSidebar }) => {
|
||||
const pkg = clientPackageName["photos"];
|
||||
|
||||
window.open(
|
||||
`${accountsAppURL()}/passkeys/setup?package=${pkg}&token=${accountsToken}`,
|
||||
`${accountsAppURL()}/passkeys?token=${accountsToken}&package=${pkg}`,
|
||||
);
|
||||
} catch (e) {
|
||||
log.error("failed to redirect to accounts page", e);
|
||||
|
||||
@@ -53,7 +53,7 @@ used.** This restriction is a byproduct of the enablement for automatic login.
|
||||
### Automatically logging into Accounts
|
||||
|
||||
Clients open a WebView with the URL
|
||||
`https://accounts.ente.io/accounts-handoff?token=<accountsToken>&package=<app package name>`.
|
||||
`https://accounts.ente.io/passkeys?token=<accountsToken>&package=<app package name>`.
|
||||
This page will appear like a normal loading screen to the user, but in the
|
||||
background, the app parses the token and package for usage in subsequent
|
||||
Accounts-related API calls.
|
||||
|
||||
Reference in New Issue
Block a user