mirror of
https://github.com/ente-io/ente.git
synced 2025-08-08 23:39:30 +00:00
Rename again
This commit is contained in:
parent
0e284752d1
commit
c983c43ba1
@ -5,7 +5,7 @@ const Page = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.location.href = window.location.href.replace(
|
window.location.href = window.location.href.replace(
|
||||||
"account-handoff",
|
"account-handoff",
|
||||||
"passkeys",
|
"passkeys/handoff",
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
50
web/apps/accounts/src/pages/passkeys/handoff.tsx
Normal file
50
web/apps/accounts/src/pages/passkeys/handoff.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
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 { useRouter } from "next/router";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse credentials passed as query parameters by one of our client apps, save
|
||||||
|
* them to local storage, and then redirect to the passkeys listing.
|
||||||
|
*/
|
||||||
|
const Page: React.FC = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
const client = urlParams.get("client");
|
||||||
|
if (client) {
|
||||||
|
// TODO-PK: mobile is not passing it. is that expected?
|
||||||
|
setData(LS_KEYS.CLIENT_PACKAGE, { name: client });
|
||||||
|
HTTPService.setHeaders({
|
||||||
|
"X-Client-Package": client,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = urlParams.get("token");
|
||||||
|
if (!token) {
|
||||||
|
log.error("Missing accounts token");
|
||||||
|
router.push("/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = getData(LS_KEYS.USER) || {};
|
||||||
|
user.token = token;
|
||||||
|
|
||||||
|
setData(LS_KEYS.USER, user);
|
||||||
|
|
||||||
|
router.push("/passkeys");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VerticallyCentered>
|
||||||
|
<EnteSpinner />
|
||||||
|
</VerticallyCentered>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
@ -1,46 +1,371 @@
|
|||||||
import log from "@/next/log";
|
import log from "@/next/log";
|
||||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
import { ensure } from "@/utils/ensure";
|
||||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
import { CenteredFlex } from "@ente/shared/components/Container";
|
||||||
import HTTPService from "@ente/shared/network/HTTPService";
|
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
|
||||||
import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
|
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 { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { useEffect } from "react";
|
import { useAppContext } from "pages/_app";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
deletePasskey,
|
||||||
|
registerPasskey,
|
||||||
|
renamePasskey,
|
||||||
|
} from "services/passkey";
|
||||||
|
import { getPasskeys, type Passkey } from "../../services/passkey";
|
||||||
|
|
||||||
const Page: React.FC = () => {
|
const Page: React.FC = () => {
|
||||||
|
const { showNavBar } = useAppContext();
|
||||||
|
|
||||||
|
const [passkeys, setPasskeys] = useState<Passkey[]>([]);
|
||||||
|
const [showPasskeyDrawer, setShowPasskeyDrawer] = useState(false);
|
||||||
|
const [selectedPasskey, setSelectedPasskey] = useState<
|
||||||
|
Passkey | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
const refreshPasskeys = async () => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
try {
|
||||||
|
setPasskeys((await getPasskeys()) || []);
|
||||||
const pkg = urlParams.get("package");
|
} catch (e) {
|
||||||
if (pkg) {
|
log.error("Failed to fetch passkeys", e);
|
||||||
// TODO-PK: mobile is not passing it. is that expected?
|
|
||||||
setData(LS_KEYS.CLIENT_PACKAGE, { name: pkg });
|
|
||||||
HTTPService.setHeaders({
|
|
||||||
"X-Client-Package": pkg,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const token = urlParams.get("token");
|
useEffect(() => {
|
||||||
if (!token) {
|
if (!getToken()) {
|
||||||
log.error("Missing accounts token");
|
|
||||||
router.push("/login");
|
router.push("/login");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = getData(LS_KEYS.USER) || {};
|
showNavBar(true);
|
||||||
user.token = token;
|
void refreshPasskeys();
|
||||||
|
|
||||||
setData(LS_KEYS.USER, user);
|
|
||||||
|
|
||||||
router.push("/passkeys/setup");
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleSelectPasskey = (passkey: Passkey) => {
|
||||||
|
setSelectedPasskey(passkey);
|
||||||
|
setShowPasskeyDrawer(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrawerClose = () => {
|
||||||
|
setShowPasskeyDrawer(false);
|
||||||
|
// Don't clear the selected passkey, let the stale value be so that the
|
||||||
|
// drawer closing animation is nicer.
|
||||||
|
//
|
||||||
|
// The value will get overwritten the next time we open the drawer for a
|
||||||
|
// different passkey, so this will not have a functional impact.
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (
|
||||||
|
inputValue: string,
|
||||||
|
setFieldError: (errorMessage: string) => void,
|
||||||
|
resetForm: () => void,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await registerPasskey(inputValue);
|
||||||
|
} catch (e) {
|
||||||
|
log.error("Failed to register a new passkey", e);
|
||||||
|
// TODO-PK: localize
|
||||||
|
setFieldError("Could not add passkey");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await refreshPasskeys();
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticallyCentered>
|
<>
|
||||||
<EnteSpinner />
|
<CenteredFlex>
|
||||||
</VerticallyCentered>
|
<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}
|
||||||
|
onSelectPasskey={handleSelectPasskey}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</CenteredFlex>
|
||||||
|
<ManagePasskeyDrawer
|
||||||
|
open={showPasskeyDrawer}
|
||||||
|
onClose={handleDrawerClose}
|
||||||
|
passkey={selectedPasskey}
|
||||||
|
refreshPasskeys={() => void refreshPasskeys}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
||||||
|
|
||||||
|
interface PasskeysListProps {
|
||||||
|
/** The list of {@link Passkey}s to show. */
|
||||||
|
passkeys: Passkey[];
|
||||||
|
/**
|
||||||
|
* Callback to invoke when an passkey in the list is clicked.
|
||||||
|
*
|
||||||
|
* It is passed the corresponding {@link Passkey}.
|
||||||
|
*/
|
||||||
|
onSelectPasskey: (passkey: Passkey) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PasskeysList: React.FC<PasskeysListProps> = ({
|
||||||
|
passkeys,
|
||||||
|
onSelectPasskey,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<MenuItemGroup>
|
||||||
|
{passkeys.map((passkey, i) => (
|
||||||
|
<React.Fragment key={passkey.id}>
|
||||||
|
<PasskeyListItem
|
||||||
|
passkey={passkey}
|
||||||
|
onClick={onSelectPasskey}
|
||||||
|
/>
|
||||||
|
{i < passkeys.length - 1 && <MenuItemDivider />}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</MenuItemGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PasskeyListItemProps {
|
||||||
|
/** The passkey to show in the item. */
|
||||||
|
passkey: Passkey;
|
||||||
|
/**
|
||||||
|
* Callback to invoke when the item is clicked.
|
||||||
|
*
|
||||||
|
* It is passed the item's {@link passkey}.
|
||||||
|
*/
|
||||||
|
onClick: (passkey: Passkey) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PasskeyListItem: React.FC<PasskeyListItemProps> = ({
|
||||||
|
passkey,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<EnteMenuItem
|
||||||
|
onClick={() => onClick(passkey)}
|
||||||
|
startIcon={<KeyIcon />}
|
||||||
|
endIcon={<ChevronRightIcon />}
|
||||||
|
label={passkey.friendlyName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ManagePasskeyDrawerProps {
|
||||||
|
/** If `true`, then the drawer is shown. */
|
||||||
|
open: boolean;
|
||||||
|
/*** Callback to invoke when the drawer wants to be closed. */
|
||||||
|
onClose: () => void;
|
||||||
|
/**
|
||||||
|
* The {@link Passkey} whose details should be shown in the drawer.
|
||||||
|
*
|
||||||
|
* The cannot be undefined logically, but the types don't currently reflect
|
||||||
|
* that reality and indicate this as undefined (this is mostly to retain the
|
||||||
|
* identity of the drawer component in a way that it animates when opening
|
||||||
|
* and closing instead of instantly getting removed from the DOM).
|
||||||
|
*/
|
||||||
|
passkey: Passkey | undefined;
|
||||||
|
refreshPasskeys: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ManagePasskeyDrawer: React.FC<ManagePasskeyDrawerProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
passkey,
|
||||||
|
refreshPasskeys,
|
||||||
|
}) => {
|
||||||
|
const selectedPasskey = ensure(passkey);
|
||||||
|
|
||||||
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
|
const [showRenameDialog, setShowRenameDialog] = useState(false);
|
||||||
|
|
||||||
|
const createdAt = formatDateTimeFull(selectedPasskey.createdAt / 1000);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EnteDrawer anchor="right" {...{ open, onClose }}>
|
||||||
|
<Stack spacing={"4px"} py={"12px"}>
|
||||||
|
<Titlebar
|
||||||
|
onClose={onClose}
|
||||||
|
// TODO-PK: Localize (more below too)
|
||||||
|
title="Manage Passkey"
|
||||||
|
onRootClose={onClose}
|
||||||
|
/>
|
||||||
|
<InfoItem
|
||||||
|
icon={<CalendarTodayIcon />}
|
||||||
|
title={t("CREATED_AT")}
|
||||||
|
caption={createdAt}
|
||||||
|
loading={false}
|
||||||
|
hideEditOption
|
||||||
|
/>
|
||||||
|
<MenuItemGroup>
|
||||||
|
<EnteMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setShowRenameDialog(true);
|
||||||
|
}}
|
||||||
|
startIcon={<EditIcon />}
|
||||||
|
label={"Rename Passkey"}
|
||||||
|
/>
|
||||||
|
<MenuItemDivider />
|
||||||
|
<EnteMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setShowDeleteDialog(true);
|
||||||
|
}}
|
||||||
|
startIcon={<DeleteIcon />}
|
||||||
|
label={"Delete Passkey"}
|
||||||
|
color="critical"
|
||||||
|
/>
|
||||||
|
</MenuItemGroup>
|
||||||
|
</Stack>
|
||||||
|
</EnteDrawer>
|
||||||
|
|
||||||
|
<DeletePasskeyDialog
|
||||||
|
open={showDeleteDialog}
|
||||||
|
onClose={() => {
|
||||||
|
setShowDeleteDialog(false);
|
||||||
|
refreshPasskeys();
|
||||||
|
}}
|
||||||
|
passkey={selectedPasskey}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RenamePasskeyDialog
|
||||||
|
open={showRenameDialog}
|
||||||
|
onClose={() => {
|
||||||
|
setShowRenameDialog(false);
|
||||||
|
refreshPasskeys();
|
||||||
|
}}
|
||||||
|
passkey={selectedPasskey}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DeletePasskeyDialogProps {
|
||||||
|
/** If `true`, then the dialog is shown. */
|
||||||
|
open: boolean;
|
||||||
|
/*** Callback to invoke when the dialog wants to be closed. */
|
||||||
|
onClose: () => void;
|
||||||
|
/** The {@link Passkey} to delete. */
|
||||||
|
passkey: Passkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeletePasskeyDialog: React.FC<DeletePasskeyDialogProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
passkey,
|
||||||
|
}) => {
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const fullScreen = useMediaQuery("(max-width: 428px)");
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
await deletePasskey(passkey.id);
|
||||||
|
onClose();
|
||||||
|
} catch (e) {
|
||||||
|
log.error("Failed to delete passkey", e);
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogBoxV2
|
||||||
|
fullWidth
|
||||||
|
{...{ open, onClose, fullScreen }}
|
||||||
|
attributes={{ title: t("DELETE_PASSKEY") }}
|
||||||
|
>
|
||||||
|
<Stack spacing={"8px"}>
|
||||||
|
<Typography>{t("DELETE_PASSKEY_CONFIRMATION")}</Typography>
|
||||||
|
<EnteButton
|
||||||
|
type="submit"
|
||||||
|
size="large"
|
||||||
|
color="critical"
|
||||||
|
loading={isDeleting}
|
||||||
|
onClick={handleConfirm}
|
||||||
|
>
|
||||||
|
{t("DELETE")}
|
||||||
|
</EnteButton>
|
||||||
|
<Button size="large" color={"secondary"} onClick={onClose}>
|
||||||
|
{t("CANCEL")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</DialogBoxV2>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RenamePasskeyDialogProps {
|
||||||
|
/** If `true`, then the dialog is shown. */
|
||||||
|
open: boolean;
|
||||||
|
/*** Callback to invoke when the dialog wants to be closed. */
|
||||||
|
onClose: () => void;
|
||||||
|
/** The {@link Passkey} to rename. */
|
||||||
|
passkey: Passkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RenamePasskeyDialog: React.FC<RenamePasskeyDialogProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
passkey,
|
||||||
|
}) => {
|
||||||
|
const fullScreen = useMediaQuery("(max-width: 428px)");
|
||||||
|
|
||||||
|
const onSubmit = async (inputValue: string) => {
|
||||||
|
try {
|
||||||
|
await renamePasskey(passkey.id, inputValue);
|
||||||
|
onClose();
|
||||||
|
} catch (e) {
|
||||||
|
log.error("Failed to rename passkey", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogBoxV2
|
||||||
|
fullWidth
|
||||||
|
{...{ open, onClose, fullScreen }}
|
||||||
|
attributes={{ title: t("RENAME_PASSKEY") }}
|
||||||
|
>
|
||||||
|
<SingleInputForm
|
||||||
|
initialValue={passkey?.friendlyName}
|
||||||
|
callback={onSubmit}
|
||||||
|
placeholder={t("ENTER_PASSKEY_NAME")}
|
||||||
|
buttonText={t("RENAME")}
|
||||||
|
fieldType="text"
|
||||||
|
secondaryButtonAction={onClose}
|
||||||
|
submitButtonProps={{ sx: { mt: 1, mb: 2 } }}
|
||||||
|
/>
|
||||||
|
</DialogBoxV2>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,371 +0,0 @@
|
|||||||
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 { useRouter } from "next/router";
|
|
||||||
import { useAppContext } from "pages/_app";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import {
|
|
||||||
deletePasskey,
|
|
||||||
registerPasskey,
|
|
||||||
renamePasskey,
|
|
||||||
} from "services/passkey";
|
|
||||||
import { getPasskeys, type Passkey } from "../../services/passkey";
|
|
||||||
|
|
||||||
const Page: React.FC = () => {
|
|
||||||
const { showNavBar } = useAppContext();
|
|
||||||
|
|
||||||
const [passkeys, setPasskeys] = useState<Passkey[]>([]);
|
|
||||||
const [showPasskeyDrawer, setShowPasskeyDrawer] = useState(false);
|
|
||||||
const [selectedPasskey, setSelectedPasskey] = useState<
|
|
||||||
Passkey | undefined
|
|
||||||
>();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const refreshPasskeys = async () => {
|
|
||||||
try {
|
|
||||||
setPasskeys((await getPasskeys()) || []);
|
|
||||||
} catch (e) {
|
|
||||||
log.error("Failed to fetch passkeys", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!getToken()) {
|
|
||||||
router.push("/login");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showNavBar(true);
|
|
||||||
void refreshPasskeys();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSelectPasskey = (passkey: Passkey) => {
|
|
||||||
setSelectedPasskey(passkey);
|
|
||||||
setShowPasskeyDrawer(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrawerClose = () => {
|
|
||||||
setShowPasskeyDrawer(false);
|
|
||||||
// Don't clear the selected passkey, let the stale value be so that the
|
|
||||||
// drawer closing animation is nicer.
|
|
||||||
//
|
|
||||||
// The value will get overwritten the next time we open the drawer for a
|
|
||||||
// different passkey, so this will not have a functional impact.
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (
|
|
||||||
inputValue: string,
|
|
||||||
setFieldError: (errorMessage: string) => void,
|
|
||||||
resetForm: () => void,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
await registerPasskey(inputValue);
|
|
||||||
} catch (e) {
|
|
||||||
log.error("Failed to register a new passkey", e);
|
|
||||||
// TODO-PK: localize
|
|
||||||
setFieldError("Could not add passkey");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await refreshPasskeys();
|
|
||||||
resetForm();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<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}
|
|
||||||
onSelectPasskey={handleSelectPasskey}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</CenteredFlex>
|
|
||||||
<ManagePasskeyDrawer
|
|
||||||
open={showPasskeyDrawer}
|
|
||||||
onClose={handleDrawerClose}
|
|
||||||
passkey={selectedPasskey}
|
|
||||||
refreshPasskeys={() => void refreshPasskeys}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
|
|
||||||
interface PasskeysListProps {
|
|
||||||
/** The list of {@link Passkey}s to show. */
|
|
||||||
passkeys: Passkey[];
|
|
||||||
/**
|
|
||||||
* Callback to invoke when an passkey in the list is clicked.
|
|
||||||
*
|
|
||||||
* It is passed the corresponding {@link Passkey}.
|
|
||||||
*/
|
|
||||||
onSelectPasskey: (passkey: Passkey) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PasskeysList: React.FC<PasskeysListProps> = ({
|
|
||||||
passkeys,
|
|
||||||
onSelectPasskey,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<MenuItemGroup>
|
|
||||||
{passkeys.map((passkey, i) => (
|
|
||||||
<React.Fragment key={passkey.id}>
|
|
||||||
<PasskeyListItem
|
|
||||||
passkey={passkey}
|
|
||||||
onClick={onSelectPasskey}
|
|
||||||
/>
|
|
||||||
{i < passkeys.length - 1 && <MenuItemDivider />}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</MenuItemGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface PasskeyListItemProps {
|
|
||||||
/** The passkey to show in the item. */
|
|
||||||
passkey: Passkey;
|
|
||||||
/**
|
|
||||||
* Callback to invoke when the item is clicked.
|
|
||||||
*
|
|
||||||
* It is passed the item's {@link passkey}.
|
|
||||||
*/
|
|
||||||
onClick: (passkey: Passkey) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PasskeyListItem: React.FC<PasskeyListItemProps> = ({
|
|
||||||
passkey,
|
|
||||||
onClick,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<EnteMenuItem
|
|
||||||
onClick={() => onClick(passkey)}
|
|
||||||
startIcon={<KeyIcon />}
|
|
||||||
endIcon={<ChevronRightIcon />}
|
|
||||||
label={passkey.friendlyName}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ManagePasskeyDrawerProps {
|
|
||||||
/** If `true`, then the drawer is shown. */
|
|
||||||
open: boolean;
|
|
||||||
/*** Callback to invoke when the drawer wants to be closed. */
|
|
||||||
onClose: () => void;
|
|
||||||
/**
|
|
||||||
* The {@link Passkey} whose details should be shown in the drawer.
|
|
||||||
*
|
|
||||||
* The cannot be undefined logically, but the types don't currently reflect
|
|
||||||
* that reality and indicate this as undefined (this is mostly to retain the
|
|
||||||
* identity of the drawer component in a way that it animates when opening
|
|
||||||
* and closing instead of instantly getting removed from the DOM).
|
|
||||||
*/
|
|
||||||
passkey: Passkey | undefined;
|
|
||||||
refreshPasskeys: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ManagePasskeyDrawer: React.FC<ManagePasskeyDrawerProps> = ({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
passkey,
|
|
||||||
refreshPasskeys,
|
|
||||||
}) => {
|
|
||||||
const selectedPasskey = ensure(passkey);
|
|
||||||
|
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
||||||
const [showRenameDialog, setShowRenameDialog] = useState(false);
|
|
||||||
|
|
||||||
const createdAt = formatDateTimeFull(selectedPasskey.createdAt / 1000);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<EnteDrawer anchor="right" {...{ open, onClose }}>
|
|
||||||
<Stack spacing={"4px"} py={"12px"}>
|
|
||||||
<Titlebar
|
|
||||||
onClose={onClose}
|
|
||||||
// TODO-PK: Localize (more below too)
|
|
||||||
title="Manage Passkey"
|
|
||||||
onRootClose={onClose}
|
|
||||||
/>
|
|
||||||
<InfoItem
|
|
||||||
icon={<CalendarTodayIcon />}
|
|
||||||
title={t("CREATED_AT")}
|
|
||||||
caption={createdAt}
|
|
||||||
loading={false}
|
|
||||||
hideEditOption
|
|
||||||
/>
|
|
||||||
<MenuItemGroup>
|
|
||||||
<EnteMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
setShowRenameDialog(true);
|
|
||||||
}}
|
|
||||||
startIcon={<EditIcon />}
|
|
||||||
label={"Rename Passkey"}
|
|
||||||
/>
|
|
||||||
<MenuItemDivider />
|
|
||||||
<EnteMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
setShowDeleteDialog(true);
|
|
||||||
}}
|
|
||||||
startIcon={<DeleteIcon />}
|
|
||||||
label={"Delete Passkey"}
|
|
||||||
color="critical"
|
|
||||||
/>
|
|
||||||
</MenuItemGroup>
|
|
||||||
</Stack>
|
|
||||||
</EnteDrawer>
|
|
||||||
|
|
||||||
<DeletePasskeyDialog
|
|
||||||
open={showDeleteDialog}
|
|
||||||
onClose={() => {
|
|
||||||
setShowDeleteDialog(false);
|
|
||||||
refreshPasskeys();
|
|
||||||
}}
|
|
||||||
passkey={selectedPasskey}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RenamePasskeyDialog
|
|
||||||
open={showRenameDialog}
|
|
||||||
onClose={() => {
|
|
||||||
setShowRenameDialog(false);
|
|
||||||
refreshPasskeys();
|
|
||||||
}}
|
|
||||||
passkey={selectedPasskey}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface DeletePasskeyDialogProps {
|
|
||||||
/** If `true`, then the dialog is shown. */
|
|
||||||
open: boolean;
|
|
||||||
/*** Callback to invoke when the dialog wants to be closed. */
|
|
||||||
onClose: () => void;
|
|
||||||
/** The {@link Passkey} to delete. */
|
|
||||||
passkey: Passkey;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DeletePasskeyDialog: React.FC<DeletePasskeyDialogProps> = ({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
passkey,
|
|
||||||
}) => {
|
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
|
||||||
const fullScreen = useMediaQuery("(max-width: 428px)");
|
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
|
||||||
setIsDeleting(true);
|
|
||||||
try {
|
|
||||||
await deletePasskey(passkey.id);
|
|
||||||
onClose();
|
|
||||||
} catch (e) {
|
|
||||||
log.error("Failed to delete passkey", e);
|
|
||||||
} finally {
|
|
||||||
setIsDeleting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogBoxV2
|
|
||||||
fullWidth
|
|
||||||
{...{ open, onClose, fullScreen }}
|
|
||||||
attributes={{ title: t("DELETE_PASSKEY") }}
|
|
||||||
>
|
|
||||||
<Stack spacing={"8px"}>
|
|
||||||
<Typography>{t("DELETE_PASSKEY_CONFIRMATION")}</Typography>
|
|
||||||
<EnteButton
|
|
||||||
type="submit"
|
|
||||||
size="large"
|
|
||||||
color="critical"
|
|
||||||
loading={isDeleting}
|
|
||||||
onClick={handleConfirm}
|
|
||||||
>
|
|
||||||
{t("DELETE")}
|
|
||||||
</EnteButton>
|
|
||||||
<Button size="large" color={"secondary"} onClick={onClose}>
|
|
||||||
{t("CANCEL")}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</DialogBoxV2>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface RenamePasskeyDialogProps {
|
|
||||||
/** If `true`, then the dialog is shown. */
|
|
||||||
open: boolean;
|
|
||||||
/*** Callback to invoke when the dialog wants to be closed. */
|
|
||||||
onClose: () => void;
|
|
||||||
/** The {@link Passkey} to rename. */
|
|
||||||
passkey: Passkey;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RenamePasskeyDialog: React.FC<RenamePasskeyDialogProps> = ({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
passkey,
|
|
||||||
}) => {
|
|
||||||
const fullScreen = useMediaQuery("(max-width: 428px)");
|
|
||||||
|
|
||||||
const onSubmit = async (inputValue: string) => {
|
|
||||||
try {
|
|
||||||
await renamePasskey(passkey.id, inputValue);
|
|
||||||
onClose();
|
|
||||||
} catch (e) {
|
|
||||||
log.error("Failed to rename passkey", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogBoxV2
|
|
||||||
fullWidth
|
|
||||||
{...{ open, onClose, fullScreen }}
|
|
||||||
attributes={{ title: t("RENAME_PASSKEY") }}
|
|
||||||
>
|
|
||||||
<SingleInputForm
|
|
||||||
initialValue={passkey?.friendlyName}
|
|
||||||
callback={onSubmit}
|
|
||||||
placeholder={t("ENTER_PASSKEY_NAME")}
|
|
||||||
buttonText={t("RENAME")}
|
|
||||||
fieldType="text"
|
|
||||||
secondaryButtonAction={onClose}
|
|
||||||
submitButtonProps={{ sx: { mt: 1, mb: 2 } }}
|
|
||||||
/>
|
|
||||||
</DialogBoxV2>
|
|
||||||
);
|
|
||||||
};
|
|
@ -506,10 +506,10 @@ const UtilitySection: React.FC<UtilitySectionProps> = ({ closeSidebar }) => {
|
|||||||
|
|
||||||
// Ente Accounts specific JWT token.
|
// Ente Accounts specific JWT token.
|
||||||
const accountsToken = await getAccountsToken();
|
const accountsToken = await getAccountsToken();
|
||||||
const pkg = clientPackageName["photos"];
|
const client = clientPackageName["photos"];
|
||||||
|
|
||||||
window.open(
|
window.open(
|
||||||
`${accountsAppURL()}/passkeys?token=${accountsToken}&package=${pkg}`,
|
`${accountsAppURL()}/passkeys/handoff?token=${accountsToken}&client=${client}`,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("failed to redirect to accounts page", e);
|
log.error("failed to redirect to accounts page", e);
|
||||||
|
@ -53,9 +53,8 @@ used.** This restriction is a byproduct of the enablement for automatic login.
|
|||||||
### Automatically logging into Accounts
|
### Automatically logging into Accounts
|
||||||
|
|
||||||
Clients open a WebView with the URL
|
Clients open a WebView with the URL
|
||||||
`https://accounts.ente.io/passkeys?token=<accountsToken>&package=<app package name>`.
|
`https://accounts.ente.io/passkeys/handoff?client=<clientPackageName>&token=<accountsToken>`.
|
||||||
This page will appear like a normal loading screen to the user, but in the
|
This page will parse the token and client package name for usage in subsequent
|
||||||
background, the app parses the token and package for usage in subsequent
|
|
||||||
Accounts-related API calls.
|
Accounts-related API calls.
|
||||||
|
|
||||||
If valid, the user will be automatically redirected to the passkeys management
|
If valid, the user will be automatically redirected to the passkeys management
|
||||||
@ -342,7 +341,7 @@ credential authentication. We use Accounts as the central WebAuthn hub because
|
|||||||
credentials are locked to an FQDN.
|
credentials are locked to an FQDN.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
window.location.href = `${accountsAppURL()}/passkeys/flow?passkeySessionID=${passkeySessionID}&redirect=${
|
window.location.href = `${accountsAppURL()}/passkeys/verify?passkeySessionID=${passkeySessionID}&redirect=${
|
||||||
window.location.origin
|
window.location.origin
|
||||||
}/passkeys/finish`;
|
}/passkeys/finish`;
|
||||||
```
|
```
|
||||||
|
@ -166,7 +166,7 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
|||||||
isTwoFactorPasskeysEnabled: true,
|
isTwoFactorPasskeysEnabled: true,
|
||||||
});
|
});
|
||||||
InMemoryStore.set(MS_KEYS.REDIRECT_URL, PAGES.ROOT);
|
InMemoryStore.set(MS_KEYS.REDIRECT_URL, PAGES.ROOT);
|
||||||
window.location.href = `${accountsAppURL()}/passkeys/flow?passkeySessionID=${passkeySessionID}&redirect=${
|
window.location.href = `${accountsAppURL()}/passkeys/verify?passkeySessionID=${passkeySessionID}&redirect=${
|
||||||
window.location.origin
|
window.location.origin
|
||||||
}/passkeys/finish`;
|
}/passkeys/finish`;
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -85,7 +85,7 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
|||||||
isTwoFactorPasskeysEnabled: true,
|
isTwoFactorPasskeysEnabled: true,
|
||||||
});
|
});
|
||||||
setIsFirstLogin(true);
|
setIsFirstLogin(true);
|
||||||
window.location.href = `${accountsAppURL()}/passkeys/flow?passkeySessionID=${passkeySessionID}&redirect=${
|
window.location.href = `${accountsAppURL()}/passkeys/verify?passkeySessionID=${passkeySessionID}&redirect=${
|
||||||
window.location.origin
|
window.location.origin
|
||||||
}/passkeys/finish`;
|
}/passkeys/finish`;
|
||||||
router.push(PAGES.CREDENTIALS);
|
router.push(PAGES.CREDENTIALS);
|
||||||
|
@ -45,6 +45,5 @@ export enum ACCOUNTS_PAGES {
|
|||||||
VERIFY = "/verify",
|
VERIFY = "/verify",
|
||||||
ROOT = "/",
|
ROOT = "/",
|
||||||
PASSKEYS = "/passkeys",
|
PASSKEYS = "/passkeys",
|
||||||
ACCOUNT_HANDOFF = "/account-handoff",
|
|
||||||
GENERATE = "/generate",
|
GENERATE = "/generate",
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user