mirror of
https://github.com/ente-io/ente.git
synced 2025-08-08 07:28:26 +00:00
Rename again
This commit is contained in:
parent
0e284752d1
commit
c983c43ba1
@ -5,7 +5,7 @@ const Page = () => {
|
||||
useEffect(() => {
|
||||
window.location.href = window.location.href.replace(
|
||||
"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 { 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 { 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 { showNavBar } = useAppContext();
|
||||
|
||||
const [passkeys, setPasskeys] = useState<Passkey[]>([]);
|
||||
const [showPasskeyDrawer, setShowPasskeyDrawer] = useState(false);
|
||||
const [selectedPasskey, setSelectedPasskey] = useState<
|
||||
Passkey | undefined
|
||||
>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
const pkg = urlParams.get("package");
|
||||
if (pkg) {
|
||||
// TODO-PK: mobile is not passing it. is that expected?
|
||||
setData(LS_KEYS.CLIENT_PACKAGE, { name: pkg });
|
||||
HTTPService.setHeaders({
|
||||
"X-Client-Package": pkg,
|
||||
});
|
||||
const refreshPasskeys = async () => {
|
||||
try {
|
||||
setPasskeys((await getPasskeys()) || []);
|
||||
} catch (e) {
|
||||
log.error("Failed to fetch passkeys", e);
|
||||
}
|
||||
};
|
||||
|
||||
const token = urlParams.get("token");
|
||||
if (!token) {
|
||||
log.error("Missing accounts token");
|
||||
useEffect(() => {
|
||||
if (!getToken()) {
|
||||
router.push("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
const user = getData(LS_KEYS.USER) || {};
|
||||
user.token = token;
|
||||
|
||||
setData(LS_KEYS.USER, user);
|
||||
|
||||
router.push("/passkeys/setup");
|
||||
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 (
|
||||
<VerticallyCentered>
|
||||
<EnteSpinner />
|
||||
</VerticallyCentered>
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
@ -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.
|
||||
const accountsToken = await getAccountsToken();
|
||||
const pkg = clientPackageName["photos"];
|
||||
const client = clientPackageName["photos"];
|
||||
|
||||
window.open(
|
||||
`${accountsAppURL()}/passkeys?token=${accountsToken}&package=${pkg}`,
|
||||
`${accountsAppURL()}/passkeys/handoff?token=${accountsToken}&client=${client}`,
|
||||
);
|
||||
} catch (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
|
||||
|
||||
Clients open a WebView with the URL
|
||||
`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
|
||||
`https://accounts.ente.io/passkeys/handoff?client=<clientPackageName>&token=<accountsToken>`.
|
||||
This page will parse the token and client package name for usage in subsequent
|
||||
Accounts-related API calls.
|
||||
|
||||
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.
|
||||
|
||||
```tsx
|
||||
window.location.href = `${accountsAppURL()}/passkeys/flow?passkeySessionID=${passkeySessionID}&redirect=${
|
||||
window.location.href = `${accountsAppURL()}/passkeys/verify?passkeySessionID=${passkeySessionID}&redirect=${
|
||||
window.location.origin
|
||||
}/passkeys/finish`;
|
||||
```
|
||||
|
@ -166,7 +166,7 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
isTwoFactorPasskeysEnabled: true,
|
||||
});
|
||||
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
|
||||
}/passkeys/finish`;
|
||||
return undefined;
|
||||
|
@ -85,7 +85,7 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
isTwoFactorPasskeysEnabled: true,
|
||||
});
|
||||
setIsFirstLogin(true);
|
||||
window.location.href = `${accountsAppURL()}/passkeys/flow?passkeySessionID=${passkeySessionID}&redirect=${
|
||||
window.location.href = `${accountsAppURL()}/passkeys/verify?passkeySessionID=${passkeySessionID}&redirect=${
|
||||
window.location.origin
|
||||
}/passkeys/finish`;
|
||||
router.push(PAGES.CREDENTIALS);
|
||||
|
@ -45,6 +45,5 @@ export enum ACCOUNTS_PAGES {
|
||||
VERIFY = "/verify",
|
||||
ROOT = "/",
|
||||
PASSKEYS = "/passkeys",
|
||||
ACCOUNT_HANDOFF = "/account-handoff",
|
||||
GENERATE = "/generate",
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user