From 1d1fa29239882f05a126e55794dbce84400ce88b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Jun 2024 20:17:02 +0530 Subject: [PATCH 01/19] Fix lint --- web/apps/accounts/.eslintrc.js | 1 - web/apps/accounts/src/pages/passkeys/verify.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/web/apps/accounts/.eslintrc.js b/web/apps/accounts/.eslintrc.js index 41bd549f98..3b5c297ccd 100644 --- a/web/apps/accounts/.eslintrc.js +++ b/web/apps/accounts/.eslintrc.js @@ -13,7 +13,6 @@ module.exports = { "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unsafe-argument": "off", - "@typescript-eslint/restrict-template-expressions": "off", "react-refresh/only-export-components": "off", }, }; diff --git a/web/apps/accounts/src/pages/passkeys/verify.tsx b/web/apps/accounts/src/pages/passkeys/verify.tsx index 26cbbc7b77..c9c2c374c8 100644 --- a/web/apps/accounts/src/pages/passkeys/verify.tsx +++ b/web/apps/accounts/src/pages/passkeys/verify.tsx @@ -50,7 +50,7 @@ const Page = () => { // Ensure that redirectURL is whitelisted, otherwise show an invalid // "login" URL error to the user. if (!redirectURL || !isWhitelistedRedirect(redirectURL)) { - log.error(`Redirect URL '${redirectURL}' is not whitelisted`); + log.error(`Redirect '${redirect}' is not whitelisted`); setStatus("unknownRedirect"); return; } From 68ebd1ef69cccdae199218bd9e797c7e8fc0f31d Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Jun 2024 20:24:09 +0530 Subject: [PATCH 02/19] Fix more lints --- web/apps/accounts/.eslintrc.js | 3 --- web/apps/accounts/src/pages/_app.tsx | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/web/apps/accounts/.eslintrc.js b/web/apps/accounts/.eslintrc.js index 3b5c297ccd..e00dc0a795 100644 --- a/web/apps/accounts/.eslintrc.js +++ b/web/apps/accounts/.eslintrc.js @@ -9,10 +9,7 @@ module.exports = { "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-misused-promises": "off", "@typescript-eslint/no-floating-promises": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unsafe-argument": "off", "react-refresh/only-export-components": "off", }, }; diff --git a/web/apps/accounts/src/pages/_app.tsx b/web/apps/accounts/src/pages/_app.tsx index 73c437cfd2..505ba96af5 100644 --- a/web/apps/accounts/src/pages/_app.tsx +++ b/web/apps/accounts/src/pages/_app.tsx @@ -103,8 +103,7 @@ export default function App({ Component, pageProps }: AppProps) { justifyContent: "center", alignItems: "center", zIndex: 2000, - backgroundColor: (theme as any).colors - .background.base, + backgroundColor: theme.colors.background.base, })} > From 5150dc00e1a3408cc38f694c08665279f6863085 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Jun 2024 20:25:53 +0530 Subject: [PATCH 03/19] Fix lint --- web/apps/accounts/.eslintrc.js | 1 - web/apps/accounts/src/pages/_app.tsx | 2 +- web/apps/auth/src/pages/_app.tsx | 2 +- web/apps/photos/src/pages/_app.tsx | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/web/apps/accounts/.eslintrc.js b/web/apps/accounts/.eslintrc.js index e00dc0a795..7858067171 100644 --- a/web/apps/accounts/.eslintrc.js +++ b/web/apps/accounts/.eslintrc.js @@ -8,7 +8,6 @@ module.exports = { "react-hooks/exhaustive-deps": "off", "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-misused-promises": "off", - "@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/no-explicit-any": "off", "react-refresh/only-export-components": "off", }, diff --git a/web/apps/accounts/src/pages/_app.tsx b/web/apps/accounts/src/pages/_app.tsx index 505ba96af5..8f65540719 100644 --- a/web/apps/accounts/src/pages/_app.tsx +++ b/web/apps/accounts/src/pages/_app.tsx @@ -57,7 +57,7 @@ export default function App({ Component, pageProps }: AppProps) { const [themeColor] = useLocalState(LS_KEYS.THEME, THEME_COLOR.DARK); useEffect(() => { - setupI18n().finally(() => setIsI18nReady(true)); + void setupI18n().finally(() => setIsI18nReady(true)); logUnhandledErrorsAndRejections(true); return () => logUnhandledErrorsAndRejections(false); }, []); diff --git a/web/apps/auth/src/pages/_app.tsx b/web/apps/auth/src/pages/_app.tsx index 1777d2915d..e0c6a1a766 100644 --- a/web/apps/auth/src/pages/_app.tsx +++ b/web/apps/auth/src/pages/_app.tsx @@ -75,7 +75,7 @@ export default function App({ Component, pageProps }: AppProps) { ); useEffect(() => { - setupI18n().finally(() => setIsI18nReady(true)); + void setupI18n().finally(() => setIsI18nReady(true)); const userId = (getData(LS_KEYS.USER) as User)?.id; logStartupBanner(appName, userId); logUnhandledErrorsAndRejections(true); diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx index a9cfbdb510..2a4d23435f 100644 --- a/web/apps/photos/src/pages/_app.tsx +++ b/web/apps/photos/src/pages/_app.tsx @@ -152,7 +152,7 @@ export default function App({ Component, pageProps }: AppProps) { ); useEffect(() => { - setupI18n().finally(() => setIsI18nReady(true)); + void setupI18n().finally(() => setIsI18nReady(true)); const userId = (getData(LS_KEYS.USER) as User)?.id; logStartupBanner(appName, userId); logUnhandledErrorsAndRejections(true); From a2e93489f2a6e9cd5e535a108e4d1408a7142788 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Jun 2024 20:28:31 +0530 Subject: [PATCH 04/19] Fix lints --- web/apps/accounts/.eslintrc.js | 1 - web/apps/accounts/src/pages/_app.tsx | 2 +- web/packages/shared/components/DialogBoxV2/types.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/web/apps/accounts/.eslintrc.js b/web/apps/accounts/.eslintrc.js index 7858067171..fc6d400d50 100644 --- a/web/apps/accounts/.eslintrc.js +++ b/web/apps/accounts/.eslintrc.js @@ -6,7 +6,6 @@ module.exports = { "react/react-in-jsx-scope": "off", "react/prop-types": "off", "react-hooks/exhaustive-deps": "off", - "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-misused-promises": "off", "@typescript-eslint/no-explicit-any": "off", "react-refresh/only-export-components": "off", diff --git a/web/apps/accounts/src/pages/_app.tsx b/web/apps/accounts/src/pages/_app.tsx index 8f65540719..8d14ea7cd4 100644 --- a/web/apps/accounts/src/pages/_app.tsx +++ b/web/apps/accounts/src/pages/_app.tsx @@ -92,7 +92,7 @@ export default function App({ Component, pageProps }: AppProps) { sx={{ zIndex: 1600 }} open={dialogBoxV2View} onClose={closeDialogBoxV2} - attributes={dialogBoxAttributeV2 as any} + attributes={dialogBoxAttributeV2} /> diff --git a/web/packages/shared/components/DialogBoxV2/types.ts b/web/packages/shared/components/DialogBoxV2/types.ts index f2ac31f0b1..d7db388600 100644 --- a/web/packages/shared/components/DialogBoxV2/types.ts +++ b/web/packages/shared/components/DialogBoxV2/types.ts @@ -13,7 +13,7 @@ export interface DialogBoxAttributesV2 { title?: React.ReactNode; staticBackdrop?: boolean; nonClosable?: boolean; - content?: any; + content?: React.ReactNode; close?: { text?: string; variant?: ButtonProps["color"]; From d2fd7dea975be0d87f5bd13fac08a5d20e312f79 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Jun 2024 20:40:36 +0530 Subject: [PATCH 05/19] Add an exception --- web/apps/accounts/.eslintrc.js | 1 - web/packages/build-config/eslintrc-base.js | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/web/apps/accounts/.eslintrc.js b/web/apps/accounts/.eslintrc.js index fc6d400d50..eb420ede9e 100644 --- a/web/apps/accounts/.eslintrc.js +++ b/web/apps/accounts/.eslintrc.js @@ -6,7 +6,6 @@ module.exports = { "react/react-in-jsx-scope": "off", "react/prop-types": "off", "react-hooks/exhaustive-deps": "off", - "@typescript-eslint/no-misused-promises": "off", "@typescript-eslint/no-explicit-any": "off", "react-refresh/only-export-components": "off", }, diff --git a/web/packages/build-config/eslintrc-base.js b/web/packages/build-config/eslintrc-base.js index 3e65638c1b..95d9976521 100644 --- a/web/packages/build-config/eslintrc-base.js +++ b/web/packages/build-config/eslintrc-base.js @@ -25,5 +25,20 @@ module.exports = { ignoreArrowShorthand: true, }, ], + /* + Allow async functions to be passed as JSX attributes expected to be + functions that return void (typically onFoo event handlers). + + This should be safe since we have registered global unhandled Promise + handlers. + */ + "@typescript-eslint/no-misused-promises": [ + "error", + { + checksVoidReturn: { + attributes: false, + }, + }, + ], }, }; From 48fc9664573558b5b5b550f9edebf6e3cb920104 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Jun 2024 20:52:43 +0530 Subject: [PATCH 06/19] Fix --- web/apps/accounts/.eslintrc.js | 1 - web/apps/accounts/src/pages/passkeys/verify.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/web/apps/accounts/.eslintrc.js b/web/apps/accounts/.eslintrc.js index eb420ede9e..0d08aaf93b 100644 --- a/web/apps/accounts/.eslintrc.js +++ b/web/apps/accounts/.eslintrc.js @@ -4,7 +4,6 @@ module.exports = { /* TODO: Temporary overrides */ rules: { "react/react-in-jsx-scope": "off", - "react/prop-types": "off", "react-hooks/exhaustive-deps": "off", "@typescript-eslint/no-explicit-any": "off", "react-refresh/only-export-components": "off", diff --git a/web/apps/accounts/src/pages/passkeys/verify.tsx b/web/apps/accounts/src/pages/passkeys/verify.tsx index c9c2c374c8..013eaceb05 100644 --- a/web/apps/accounts/src/pages/passkeys/verify.tsx +++ b/web/apps/accounts/src/pages/passkeys/verify.tsx @@ -7,7 +7,7 @@ import EnteSpinner from "@ente/shared/components/EnteSpinner"; import InfoIcon from "@mui/icons-material/Info"; import { Paper, Typography, styled } from "@mui/material"; import { t } from "i18next"; -import { useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { beginPasskeyAuthentication, finishPasskeyAuthentication, From 25f2fc46a954952def15f704d2dfc01539e546b4 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Jun 2024 20:55:06 +0530 Subject: [PATCH 07/19] Fix --- web/apps/accounts/.eslintrc.js | 2 -- web/apps/accounts/src/pages/_app.tsx | 8 +++++--- web/apps/auth/src/pages/_app.tsx | 14 +++++++++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/web/apps/accounts/.eslintrc.js b/web/apps/accounts/.eslintrc.js index 0d08aaf93b..551ea06642 100644 --- a/web/apps/accounts/.eslintrc.js +++ b/web/apps/accounts/.eslintrc.js @@ -3,9 +3,7 @@ module.exports = { ignorePatterns: ["next.config.js", "next-env.d.ts"], /* TODO: Temporary overrides */ rules: { - "react/react-in-jsx-scope": "off", "react-hooks/exhaustive-deps": "off", - "@typescript-eslint/no-explicit-any": "off", "react-refresh/only-export-components": "off", }, }; diff --git a/web/apps/accounts/src/pages/_app.tsx b/web/apps/accounts/src/pages/_app.tsx index 8d14ea7cd4..dd47bd8441 100644 --- a/web/apps/accounts/src/pages/_app.tsx +++ b/web/apps/accounts/src/pages/_app.tsx @@ -19,7 +19,7 @@ import { ThemeProvider } from "@mui/material/styles"; import { t } from "i18next"; import type { AppProps } from "next/app"; import { useRouter } from "next/router"; -import { createContext, useContext, useEffect, useState } from "react"; +import React, { createContext, useContext, useEffect, useState } from "react"; import "styles/global.css"; /** The accounts app has no extra properties on top of the base context. */ @@ -31,7 +31,7 @@ export const AppContext = createContext(undefined); /** Utility hook to reduce amount of boilerplate in account related pages. */ export const useAppContext = () => ensure(useContext(AppContext)); -export default function App({ Component, pageProps }: AppProps) { +const App: React.FC = ({ Component, pageProps }) => { const appName: AppName = "accounts"; const [isI18nReady, setIsI18nReady] = useState(false); @@ -115,4 +115,6 @@ export default function App({ Component, pageProps }: AppProps) { ); -} +}; + +export default App; diff --git a/web/apps/auth/src/pages/_app.tsx b/web/apps/auth/src/pages/_app.tsx index e0c6a1a766..573e96aeb7 100644 --- a/web/apps/auth/src/pages/_app.tsx +++ b/web/apps/auth/src/pages/_app.tsx @@ -31,7 +31,13 @@ import { ThemeProvider } from "@mui/material/styles"; import { t } from "i18next"; import type { AppProps } from "next/app"; import { useRouter } from "next/router"; -import { createContext, useContext, useEffect, useRef, useState } from "react"; +import React, { + createContext, + useContext, + useEffect, + useRef, + useState, +} from "react"; import LoadingBar, { type LoadingBarRef } from "react-top-loading-bar"; import "../../public/css/global.css"; @@ -52,7 +58,7 @@ export const AppContext = createContext(undefined); /** Utility hook to reduce amount of boilerplate in account related pages. */ export const useAppContext = () => ensure(useContext(AppContext)); -export default function App({ Component, pageProps }: AppProps) { +const App: React.FC = ({ Component, pageProps }) => { const appName: AppName = "auth"; const router = useRouter(); @@ -198,4 +204,6 @@ export default function App({ Component, pageProps }: AppProps) { ); -} +}; + +export default App; From 309d3321b90109f4c24a75d857e77b0a16446d6b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Jun 2024 21:21:01 +0530 Subject: [PATCH 08/19] Fix lint --- web/apps/accounts/.eslintrc.js | 1 - web/apps/accounts/src/components/context.ts | 15 +++++++++++++++ web/apps/accounts/src/pages/_app.tsx | 16 ++++------------ web/apps/accounts/src/pages/passkeys/index.tsx | 2 +- 4 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 web/apps/accounts/src/components/context.ts diff --git a/web/apps/accounts/.eslintrc.js b/web/apps/accounts/.eslintrc.js index 551ea06642..0f5863fc88 100644 --- a/web/apps/accounts/.eslintrc.js +++ b/web/apps/accounts/.eslintrc.js @@ -4,6 +4,5 @@ module.exports = { /* TODO: Temporary overrides */ rules: { "react-hooks/exhaustive-deps": "off", - "react-refresh/only-export-components": "off", }, }; diff --git a/web/apps/accounts/src/components/context.ts b/web/apps/accounts/src/components/context.ts new file mode 100644 index 0000000000..d6a2755e41 --- /dev/null +++ b/web/apps/accounts/src/components/context.ts @@ -0,0 +1,15 @@ +import type { BaseAppContextT } from "@/next/types/app"; +import { ensure } from "@/utils/ensure"; +import { createContext, useContext } from "react"; + +/** The accounts app has no extra properties on top of the base context. */ +type AppContextT = BaseAppContextT; + +/** The React {@link Context} available to all pages. */ +export const AppContext = createContext(undefined); + +/** + * Utility hook to get the {@link AppContextT}, throwing an exception if it is + * not defined. + */ +export const useAppContext = (): AppContextT => ensure(useContext(AppContext)); diff --git a/web/apps/accounts/src/pages/_app.tsx b/web/apps/accounts/src/pages/_app.tsx index dd47bd8441..dc543ba0a8 100644 --- a/web/apps/accounts/src/pages/_app.tsx +++ b/web/apps/accounts/src/pages/_app.tsx @@ -1,8 +1,7 @@ import { CustomHead } from "@/next/components/Head"; import { setupI18n } from "@/next/i18n"; import { logUnhandledErrorsAndRejections } from "@/next/log-web"; -import { appTitle, type AppName, type BaseAppContextT } from "@/next/types/app"; -import { ensure } from "@/utils/ensure"; +import { appTitle, type AppName } from "@/next/types/app"; import { PAGES } from "@ente/accounts/constants/pages"; import { accountLogout } from "@ente/accounts/services/logout"; import { Overlay } from "@ente/shared/components/Container"; @@ -16,21 +15,14 @@ import { getTheme } from "@ente/shared/themes"; import { THEME_COLOR } from "@ente/shared/themes/constants"; import { CssBaseline, useMediaQuery } from "@mui/material"; import { ThemeProvider } from "@mui/material/styles"; +import { AppContext } from "components/context"; import { t } from "i18next"; import type { AppProps } from "next/app"; import { useRouter } from "next/router"; -import React, { createContext, useContext, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; + import "styles/global.css"; -/** The accounts app has no extra properties on top of the base context. */ -type AppContextT = BaseAppContextT; - -/** The React {@link Context} available to all pages. */ -export const AppContext = createContext(undefined); - -/** Utility hook to reduce amount of boilerplate in account related pages. */ -export const useAppContext = () => ensure(useContext(AppContext)); - const App: React.FC = ({ Component, pageProps }) => { const appName: AppName = "accounts"; diff --git a/web/apps/accounts/src/pages/passkeys/index.tsx b/web/apps/accounts/src/pages/passkeys/index.tsx index 6472eeb18e..4f95759e0d 100644 --- a/web/apps/accounts/src/pages/passkeys/index.tsx +++ b/web/apps/accounts/src/pages/passkeys/index.tsx @@ -18,8 +18,8 @@ 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 { useAppContext } from "components/context"; import { t } from "i18next"; -import { useAppContext } from "pages/_app"; import React, { useEffect, useState } from "react"; import { deletePasskey, From 07cd9be3f4c42d328f4ebe12623afd2de7e926b0 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 12 Jun 2024 09:46:21 +0530 Subject: [PATCH 09/19] [docs] Mention yarn pretty --- docs/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 6d3f926361..b53178489a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -44,7 +44,8 @@ yarn dev For an editor, VSCode is a good choice. Also install the Prettier extension for VSCode, and set VSCode to format on save. This way the editor will automatically format and wrap the text using the project's standard, so you can just focus on -the content. +the content. You can also format without VSCode by using the `yarn pretty` +command. ## Have fun! From e5bc7b218d8a944fc8090c7cf63c17f2d1fcc038 Mon Sep 17 00:00:00 2001 From: Jay <118880285+Jishnuraj9@users.noreply.github.com> Date: Wed, 12 Jun 2024 10:12:38 +0530 Subject: [PATCH 10/19] doc-addlink --- docs/docs/photos/faq/security-and-privacy.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/docs/photos/faq/security-and-privacy.md b/docs/docs/photos/faq/security-and-privacy.md index 2970c9aff6..9406982e52 100644 --- a/docs/docs/photos/faq/security-and-privacy.md +++ b/docs/docs/photos/faq/security-and-privacy.md @@ -97,3 +97,5 @@ your own instead of contacting support to ask them to delete your account. Note that both Ente photos and Ente auth data will be deleted when you delete your account (irrespective of which app you delete it from) since both photos and auth use the same underlying account. + +To know details of how your data is deleted, including when you delete your account, please see https://ente.io/blog/how-ente-deletes-data/. \ No newline at end of file From 504bae5dd87ae61dfd5b25261c85ba26a14023fe Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 12 Jun 2024 10:44:12 +0530 Subject: [PATCH 11/19] [docs] Passkeys --- docs/docs/.vitepress/sidebar.ts | 4 ++ docs/docs/photos/features/passkeys.md | 62 +++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 docs/docs/photos/features/passkeys.md diff --git a/docs/docs/.vitepress/sidebar.ts b/docs/docs/.vitepress/sidebar.ts index ec6691bbe6..478f070c09 100644 --- a/docs/docs/.vitepress/sidebar.ts +++ b/docs/docs/.vitepress/sidebar.ts @@ -44,6 +44,10 @@ export const sidebar = [ link: "/photos/features/location-tags", }, { text: "Map", link: "/photos/features/map" }, + { + text: "Passkeys", + link: "/photos/features/passkeys", + }, { text: "Public link", link: "/photos/features/public-link", diff --git a/docs/docs/photos/features/passkeys.md b/docs/docs/photos/features/passkeys.md new file mode 100644 index 0000000000..f485c4f9b6 --- /dev/null +++ b/docs/docs/photos/features/passkeys.md @@ -0,0 +1,62 @@ +--- +title: Passkeys +description: Using passkeys as a second factor for your Ente account +--- + +# Passkeys + +> [!CAUTION] +> +> This is preview documentation for an upcoming feature. This feature has not +> yet been released yet, so the steps below will not work currently. + +Passkeys are a new authentication mechanism that uses strong cryptography built +into devices, like Windows Hello or Apple's Touch ID. **You can use passkeys as +a second factor to secure your Ente account.** + +> [!TIP] +> +> Passkeys are the colloquial term for a WebAuthn (Web Authentication) +> credentials. To know more technical details about how our passkey verification +> works, you can see this +> [technical note in our source code](https://github.com/ente-io/ente/blob/main/web/docs/webauthn-passkeys.md). + +## Passkeys and TOTP + +Ente already supports TOTP codes (in fact, we built an +[entire app](https://ente.io/auth/) to store them...). Passkeys serve as an +alternative 2FA (second factor) mechanism. + +If you add a passkey to your Ente account, it will be used instead of any +existing 2FA codes that you have configured (if any). + +## Enabling and disabling passkeys + +Passkeys get enabled if you add one (or more) passkeys to your account. +Conversely, passkeys get disabled if you remove all your existing passkeys. + +To add and remove passkeys, use the _Passkey_ option in the settings menu. This +will open up _accounts.ente.io_, where you can manage your passkeys. + +## Login with passkeys + +If passkeys are enabled, then _accounts.ente.io_ will automatically open when +you log into your Ente account on a new device. Here you can follow the +instructions given by the browser to verify your passkey. + +> These instructions different for each browser and device, but generally they +> will ask you to use the same mechanism that you used when you created the +> passkey to verify it (scanning a QR code, using your fingerprint, pressing the +> key on your Yubikey or other security key hardware etc). + +## Recovery + +If you are unable to login with your passkey (e.g. if you have misplaced the +hardware key that you used to store your passkey), then you can **recover your +account by using your Ente recovery key**. + +During login, press cancel on the browser dialog to verify your passkey, and +then select the "Recover two-factor" option in the error message that gets +shown. This will take you to a place where you can enter your Ente recovery key +and login into your account. Now you can go to the _Passkey_ page to delete the +lost passkey and/or add a new one. From f7a500b811bb87275988e553c425d257a5ee64c5 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 12 Jun 2024 11:16:24 +0530 Subject: [PATCH 12/19] [web] Fix exhaustive-deps lints Refs: https://legacy.reactjs.org/docs/hooks-reference.html#usestate > React guarantees that setState function identity is stable and won't change on rerenders. This is why it's safe to omit from the useEffect or useCallback dependency list. --- web/apps/accounts/.eslintrc.js | 4 --- web/apps/accounts/src/pages/_app.tsx | 12 +++---- .../accounts/src/pages/passkeys/index.tsx | 34 +++++++++---------- 3 files changed, 22 insertions(+), 28 deletions(-) diff --git a/web/apps/accounts/.eslintrc.js b/web/apps/accounts/.eslintrc.js index 0f5863fc88..b1aff972c9 100644 --- a/web/apps/accounts/.eslintrc.js +++ b/web/apps/accounts/.eslintrc.js @@ -1,8 +1,4 @@ module.exports = { extends: ["@/build-config/eslintrc-next"], ignorePatterns: ["next.config.js", "next-env.d.ts"], - /* TODO: Temporary overrides */ - rules: { - "react-hooks/exhaustive-deps": "off", - }, }; diff --git a/web/apps/accounts/src/pages/_app.tsx b/web/apps/accounts/src/pages/_app.tsx index dc543ba0a8..af73ad8c61 100644 --- a/web/apps/accounts/src/pages/_app.tsx +++ b/web/apps/accounts/src/pages/_app.tsx @@ -19,7 +19,7 @@ import { AppContext } from "components/context"; import { t } from "i18next"; import type { AppProps } from "next/app"; import { useRouter } from "next/router"; -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import "styles/global.css"; @@ -28,7 +28,7 @@ const App: React.FC = ({ Component, pageProps }) => { const [isI18nReady, setIsI18nReady] = useState(false); - const [showNavbar, setShowNavBar] = useState(false); + const [showNavbar, setShowNavbar] = useState(false); const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState< DialogBoxAttributesV2 | undefined @@ -40,8 +40,6 @@ const App: React.FC = ({ Component, pageProps }) => { setDialogBoxV2View(true); }, [dialogBoxAttributeV2]); - const showNavBar = (show: boolean) => setShowNavBar(show); - const isMobile = useMediaQuery("(max-width: 428px)"); const router = useRouter(); @@ -58,14 +56,14 @@ const App: React.FC = ({ Component, pageProps }) => { const theme = getTheme(themeColor, "photos"); - const logout = () => { + const logout = useCallback(() => { void accountLogout().then(() => router.push(PAGES.ROOT)); - }; + }, [router]); const appContext = { appName, logout, - showNavBar, + showNavBar: setShowNavbar, isMobile, setDialogBoxAttributesV2, }; diff --git a/web/apps/accounts/src/pages/passkeys/index.tsx b/web/apps/accounts/src/pages/passkeys/index.tsx index 4f95759e0d..0a3d06a6cb 100644 --- a/web/apps/accounts/src/pages/passkeys/index.tsx +++ b/web/apps/accounts/src/pages/passkeys/index.tsx @@ -20,7 +20,7 @@ import KeyIcon from "@mui/icons-material/Key"; import { Box, Button, Stack, Typography, useMediaQuery } from "@mui/material"; import { useAppContext } from "components/context"; import { t } from "i18next"; -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { deletePasskey, getPasskeys, @@ -39,6 +39,14 @@ const Page: React.FC = () => { Passkey | undefined >(); + const showPasskeyFetchFailedErrorDialog = useCallback(() => { + setDialogBoxAttributesV2({ + title: t("ERROR"), + content: t("passkey_fetch_failed"), + close: {}, + }); + }, [setDialogBoxAttributesV2]); + useEffect(() => { showNavBar(true); @@ -51,30 +59,22 @@ const Page: React.FC = () => { log.error("Missing accounts token"); showPasskeyFetchFailedErrorDialog(); } - }, []); + }, [showNavBar, showPasskeyFetchFailedErrorDialog]); - useEffect(() => { - if (token) { - void refreshPasskeys(); - } - }, [token]); - - const refreshPasskeys = async () => { + const refreshPasskeys = useCallback(async () => { try { setPasskeys(await getPasskeys(ensure(token))); } catch (e) { log.error("Failed to fetch passkeys", e); showPasskeyFetchFailedErrorDialog(); } - }; + }, [token, showPasskeyFetchFailedErrorDialog]); - const showPasskeyFetchFailedErrorDialog = () => { - setDialogBoxAttributesV2({ - title: t("ERROR"), - content: t("passkey_fetch_failed"), - close: {}, - }); - }; + useEffect(() => { + if (token) { + void refreshPasskeys(); + } + }, [token, refreshPasskeys]); const handleSelectPasskey = (passkey: Passkey) => { setSelectedPasskey(passkey); From eb8ce32acbb0ec1085d5a50c3074033502fec26e Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 12 Jun 2024 12:22:41 +0530 Subject: [PATCH 13/19] Register enteauth url for macos --- auth/macos/Runner/Info.plist | 1 + 1 file changed, 1 insertion(+) diff --git a/auth/macos/Runner/Info.plist b/auth/macos/Runner/Info.plist index 418d695c9f..b48ea505b9 100644 --- a/auth/macos/Runner/Info.plist +++ b/auth/macos/Runner/Info.plist @@ -38,6 +38,7 @@ CFBundleURLSchemes otpauth + enteauth From 860ca9852b500a2a0012fb206f98946330c28a38 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 12 Jun 2024 12:23:11 +0530 Subject: [PATCH 14/19] Show dialog when passkey verification response is processed --- auth/lib/services/user_service.dart | 52 +++++++++++++++++------------ 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/auth/lib/services/user_service.dart b/auth/lib/services/user_service.dart index 9294680598..a0829cfa1e 100644 --- a/auth/lib/services/user_service.dart +++ b/auth/lib/services/user_service.dart @@ -267,31 +267,41 @@ class UserService { } Future onPassKeyVerified(BuildContext context, Map response) async { - final userPassword = Configuration.instance.getVolatilePassword(); - if (userPassword == null) throw Exception("volatile password is null"); + final ProgressDialog dialog = + createProgressDialog(context, context.l10n.pleaseWait); + await dialog.show(); + try { + final userPassword = Configuration.instance.getVolatilePassword(); + if (userPassword == null) throw Exception("volatile password is null"); - await _saveConfiguration(response); + await _saveConfiguration(response); - Widget page; - if (Configuration.instance.getEncryptedToken() != null) { - await Configuration.instance.decryptSecretsAndGetKeyEncKey( - userPassword, - Configuration.instance.getKeyAttributes()!, + Widget page; + if (Configuration.instance.getEncryptedToken() != null) { + await Configuration.instance.decryptSecretsAndGetKeyEncKey( + userPassword, + Configuration.instance.getKeyAttributes()!, + ); + page = const HomePage(); + } else { + throw Exception("unexpected response during passkey verification"); + } + await dialog.hide(); + + // ignore: unawaited_futures + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (BuildContext context) { + return page; + }, + ), + (route) => route.isFirst, ); - page = const HomePage(); - } else { - throw Exception("unexpected response during passkey verification"); + } catch (e) { + _logger.severe(e); + await dialog.hide(); + rethrow; } - - // ignore: unawaited_futures - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (BuildContext context) { - return page; - }, - ), - (route) => route.isFirst, - ); } Future verifyEmail( From d15d2437fbffb7dc3de4f3bbfa42614ad0ab3061 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 12 Jun 2024 14:00:58 +0530 Subject: [PATCH 15/19] Minor refactor --- auth/lib/services/user_service.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/auth/lib/services/user_service.dart b/auth/lib/services/user_service.dart index a0829cfa1e..bd411da47b 100644 --- a/auth/lib/services/user_service.dart +++ b/auth/lib/services/user_service.dart @@ -271,16 +271,16 @@ class UserService { createProgressDialog(context, context.l10n.pleaseWait); await dialog.show(); try { - final userPassword = Configuration.instance.getVolatilePassword(); + final userPassword = _config.getVolatilePassword(); if (userPassword == null) throw Exception("volatile password is null"); await _saveConfiguration(response); Widget page; - if (Configuration.instance.getEncryptedToken() != null) { - await Configuration.instance.decryptSecretsAndGetKeyEncKey( + if (_config.getEncryptedToken() != null) { + await _config.decryptSecretsAndGetKeyEncKey( userPassword, - Configuration.instance.getKeyAttributes()!, + _config.getKeyAttributes()!, ); page = const HomePage(); } else { From 2e442c215280143ccd86b617d9f8a0895a73aaa3 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 12 Jun 2024 14:02:17 +0530 Subject: [PATCH 16/19] Handle case when account is already logged in --- auth/lib/ui/passkey_page.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/auth/lib/ui/passkey_page.dart b/auth/lib/ui/passkey_page.dart index f04c0494e6..b27b9e3a7c 100644 --- a/auth/lib/ui/passkey_page.dart +++ b/auth/lib/ui/passkey_page.dart @@ -8,6 +8,7 @@ import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/ui/components/buttons/button_widget.dart'; import 'package:ente_auth/ui/components/models/button_type.dart'; import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:ente_auth/utils/toast_util.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -61,6 +62,11 @@ class _PasskeyPageState extends State { } try { if (mounted && link.toLowerCase().startsWith("enteauth://passkey")) { + if (Configuration.instance.isLoggedIn()) { + _logger.info('ignored deeplink: already configured'); + showToast(context, 'Account is already configured.'); + return; + } final String? uri = Uri.parse(link).queryParameters['response']; String base64String = uri!.toString(); while (base64String.length % 4 != 0) { From bfca0730b2b05aba58add3004e8105fc0df7acb7 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 12 Jun 2024 14:25:36 +0530 Subject: [PATCH 17/19] Rearrange in reading order --- desktop/src/main.ts | 172 +++++++++++++++++++++++--------------------- 1 file changed, 89 insertions(+), 83 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index df16703fd6..180fce2acf 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -61,6 +61,94 @@ export const allowWindowClose = (): void => { shouldAllowWindowClose = true; }; +/** + * The app's entry point. + * + * We call this at the end of this file. + */ +const main = () => { + const gotTheLock = app.requestSingleInstanceLock(); + if (!gotTheLock) { + app.quit(); + return; + } + + let mainWindow: BrowserWindow | undefined; + + initLogging(); + logStartupBanner(); + registerForEnteLinks(); + // The order of the next two calls is important + setupRendererServer(); + registerPrivilegedSchemes(); + migrateLegacyWatchStoreIfNeeded(); + + app.on("second-instance", () => { + // Someone tried to run a second instance, we should focus our window. + if (mainWindow) { + mainWindow.show(); + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + } + }); + + // Emitted once, when Electron has finished initializing. + // + // Note that some Electron APIs can only be used after this event occurs. + void app.whenReady().then(() => { + void (async () => { + // Create window and prepare for the renderer. + mainWindow = createMainWindow(); + + // Setup IPC and streams. + const watcher = createWatcher(mainWindow); + attachIPCHandlers(); + attachFSWatchIPCHandlers(watcher); + attachLogoutIPCHandler(watcher); + registerStreamProtocol(); + + // Configure the renderer's environment. + const webContents = mainWindow.webContents; + setDownloadPath(webContents); + allowExternalLinks(webContents); + allowAllCORSOrigins(webContents); + + // Start loading the renderer. + void mainWindow.loadURL(rendererURL); + + // Continue on with the rest of the startup sequence. + Menu.setApplicationMenu(await createApplicationMenu(mainWindow)); + setupTrayItem(mainWindow); + setupAutoUpdater(mainWindow); + + try { + await deleteLegacyDiskCacheDirIfExists(); + await deleteLegacyKeysStoreIfExists(); + } catch (e) { + // Log but otherwise ignore errors during non-critical startup + // actions. + log.error("Ignoring startup error", e); + } + })(); + }); + + // This is a macOS only event. Show our window when the user activates the + // app, e.g. by clicking on its dock icon. + app.on("activate", () => mainWindow?.show()); + + app.on("before-quit", () => { + if (mainWindow) saveWindowBounds(mainWindow); + allowWindowClose(); + }); + + const handleOpenURLWithWindow = (url: string) => { + log.info(`Attempting to handle request to open URL: ${url}`); + if (mainWindow) handleEnteLinks(mainWindow, url); + else setTimeout(() => handleOpenURLWithWindow(url), 1000); + }; + app.on("open-url", (_, url) => handleOpenURLWithWindow(url)); +}; + /** * Log a standard startup banner. * @@ -464,87 +552,5 @@ const deleteLegacyKeysStoreIfExists = async () => { } }; -const main = () => { - const gotTheLock = app.requestSingleInstanceLock(); - if (!gotTheLock) { - app.quit(); - return; - } - - let mainWindow: BrowserWindow | undefined; - - initLogging(); - logStartupBanner(); - registerForEnteLinks(); - // The order of the next two calls is important - setupRendererServer(); - registerPrivilegedSchemes(); - migrateLegacyWatchStoreIfNeeded(); - - app.on("second-instance", () => { - // Someone tried to run a second instance, we should focus our window. - if (mainWindow) { - mainWindow.show(); - if (mainWindow.isMinimized()) mainWindow.restore(); - mainWindow.focus(); - } - }); - - // Emitted once, when Electron has finished initializing. - // - // Note that some Electron APIs can only be used after this event occurs. - void app.whenReady().then(() => { - void (async () => { - // Create window and prepare for the renderer. - mainWindow = createMainWindow(); - - // Setup IPC and streams. - const watcher = createWatcher(mainWindow); - attachIPCHandlers(); - attachFSWatchIPCHandlers(watcher); - attachLogoutIPCHandler(watcher); - registerStreamProtocol(); - - // Configure the renderer's environment. - const webContents = mainWindow.webContents; - setDownloadPath(webContents); - allowExternalLinks(webContents); - allowAllCORSOrigins(webContents); - - // Start loading the renderer. - void mainWindow.loadURL(rendererURL); - - // Continue on with the rest of the startup sequence. - Menu.setApplicationMenu(await createApplicationMenu(mainWindow)); - setupTrayItem(mainWindow); - setupAutoUpdater(mainWindow); - - try { - await deleteLegacyDiskCacheDirIfExists(); - await deleteLegacyKeysStoreIfExists(); - } catch (e) { - // Log but otherwise ignore errors during non-critical startup - // actions. - log.error("Ignoring startup error", e); - } - })(); - }); - - // This is a macOS only event. Show our window when the user activates the - // app, e.g. by clicking on its dock icon. - app.on("activate", () => mainWindow?.show()); - - app.on("before-quit", () => { - if (mainWindow) saveWindowBounds(mainWindow); - allowWindowClose(); - }); - - const handleOpenURLWithWindow = (url: string) => { - log.info(`Attempting to handle request to open URL: ${url}`); - if (mainWindow) handleEnteLinks(mainWindow, url); - else setTimeout(() => handleOpenURLWithWindow(url), 1000); - }; - app.on("open-url", (_, url) => handleOpenURLWithWindow(url)); -}; - +// Go for it. main(); From f529460eda8c14839ca64802651f81d1c661abc0 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 12 Jun 2024 14:33:07 +0530 Subject: [PATCH 18/19] Handle deeplinks on Linux --- desktop/src/main.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 180fce2acf..50f69759da 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -83,13 +83,26 @@ const main = () => { registerPrivilegedSchemes(); migrateLegacyWatchStoreIfNeeded(); - app.on("second-instance", () => { + /** + * Handle an open URL request, but ensuring that we have a mainWindow. + */ + const handleOpenURLEnsuringWindow = (url: string) => { + log.info(`Attempting to handle request to open URL: ${url}`); + if (mainWindow) handleEnteLinks(mainWindow, url); + else setTimeout(() => handleOpenURLEnsuringWindow(url), 1000); + }; + + app.on("second-instance", (_, argv: string[]) => { // Someone tried to run a second instance, we should focus our window. if (mainWindow) { mainWindow.show(); if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.focus(); } + // On Windows and Linux, this is how we get deeplinks. + // See: registerForEnteLinks + const url = argv.pop(); + if (url) handleOpenURLEnsuringWindow(url); }); // Emitted once, when Electron has finished initializing. @@ -141,12 +154,8 @@ const main = () => { allowWindowClose(); }); - const handleOpenURLWithWindow = (url: string) => { - log.info(`Attempting to handle request to open URL: ${url}`); - if (mainWindow) handleEnteLinks(mainWindow, url); - else setTimeout(() => handleOpenURLWithWindow(url), 1000); - }; - app.on("open-url", (_, url) => handleOpenURLWithWindow(url)); + // On macOS, this is how we get deeplinks. See: registerForEnteLinks + app.on("open-url", (_, url) => handleOpenURLEnsuringWindow(url)); }; /** @@ -233,6 +242,8 @@ const registerPrivilegedSchemes = () => { * Implementation notes: * - https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app * - This works only when the app is packaged. + * - On Windows and Linux, we get the deeplink in the "second-instance" event. + * - On macOS, we get the deeplink in the "open-url" event. */ const registerForEnteLinks = () => app.setAsDefaultProtocolClient("ente"); From 30c368d99fe79e88314944aa7d587d287ce0d6ed Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 12 Jun 2024 15:13:32 +0530 Subject: [PATCH 19/19] [docs] Add a note about AppImage desktop integration --- docs/docs/photos/faq/security-and-privacy.md | 3 +- .../troubleshooting/desktop-install/index.md | 28 +++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/docs/docs/photos/faq/security-and-privacy.md b/docs/docs/photos/faq/security-and-privacy.md index 9406982e52..9dadf8bdf1 100644 --- a/docs/docs/photos/faq/security-and-privacy.md +++ b/docs/docs/photos/faq/security-and-privacy.md @@ -98,4 +98,5 @@ Note that both Ente photos and Ente auth data will be deleted when you delete your account (irrespective of which app you delete it from) since both photos and auth use the same underlying account. -To know details of how your data is deleted, including when you delete your account, please see https://ente.io/blog/how-ente-deletes-data/. \ No newline at end of file +To know details of how your data is deleted, including when you delete your +account, please see https://ente.io/blog/how-ente-deletes-data/. diff --git a/docs/docs/photos/troubleshooting/desktop-install/index.md b/docs/docs/photos/troubleshooting/desktop-install/index.md index 7410c7818e..d5084be503 100644 --- a/docs/docs/photos/troubleshooting/desktop-install/index.md +++ b/docs/docs/photos/troubleshooting/desktop-install/index.md @@ -9,6 +9,9 @@ The latest version of the Ente Photos desktop app can be downloaded from [ente.io/download](https://ente.io/download). If you're having trouble, please see if any of the following cases apply. +- [Windows](#windows) +- [Linux](#linux) + ## Windows If the app stops with an "A JavaScript error occurred in the main process - The @@ -22,7 +25,26 @@ This is what the error looks like: You can install the Microsoft VC++ redistributable runtime from here:
https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#latest-microsoft-visual-c-redistributable-version -## AppImages on ARM64 Linux +## Linux + +### AppImage desktop integration + +AppImages are not fully standalone, and they require additional steps to enable +full "desktop integration": + +- Showing the app icon, +- Surfacing the app in the list of installed apps, +- Handling redirection after passkey verification. + +All the ways of enabling AppImage desktop integration are mentioned in +[AppImage documentation](https://docs.appimage.org/user-guide/run-appimages.html#integrating-appimages-into-the-desktop). + +For example, you can download the +[appimaged](https://github.com/probonopd/go-appimage/releases) AppImage, run it, +and then download the Ente Photos AppImage into your `~/Downloads` folder. +_appimaged_ will then pick it up automatically. + +### AppImages on ARM64 If you're on an ARM64 machine running Linux, and the AppImages doesn't do anything when you run it, you will need to run the following command on your @@ -42,7 +64,7 @@ details, see the following upstream issues: - libz.so: cannot open shared object file with Ubuntu arm64 - [electron-userland/electron-builder/issues/7835](https://github.com/electron-userland/electron-builder/issues/7835) -## AppImage says it requires FUSE +### AppImage says it requires FUSE See [docs.appimage.org](https://docs.appimage.org/user-guide/troubleshooting/fuse.html#the-appimage-tells-me-it-needs-fuse-to-run). @@ -53,7 +75,7 @@ tl;dr; for example, on Ubuntu, sudo apt install libfuse2 ``` -## Linux SUID error +### Linux SUID error On some Linux distributions, if you run the AppImage from the CLI, it might fail with the following error: