Merge branch 'main' into discovery-3
This commit is contained in:
@@ -267,31 +267,41 @@ class UserService {
|
||||
}
|
||||
|
||||
Future<void> 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 = _config.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 (_config.getEncryptedToken() != null) {
|
||||
await _config.decryptSecretsAndGetKeyEncKey(
|
||||
userPassword,
|
||||
_config.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<void> verifyEmail(
|
||||
|
||||
@@ -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<PasskeyPage> {
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>otpauth</string>
|
||||
<string>enteauth</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
@@ -61,6 +61,103 @@ 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();
|
||||
|
||||
/**
|
||||
* 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.
|
||||
//
|
||||
// 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();
|
||||
});
|
||||
|
||||
// On macOS, this is how we get deeplinks. See: registerForEnteLinks
|
||||
app.on("open-url", (_, url) => handleOpenURLEnsuringWindow(url));
|
||||
};
|
||||
|
||||
/**
|
||||
* Log a standard startup banner.
|
||||
*
|
||||
@@ -145,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");
|
||||
|
||||
@@ -464,87 +563,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();
|
||||
|
||||
@@ -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!
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -97,3 +97,6 @@ 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/.
|
||||
|
||||
62
docs/docs/photos/features/passkeys.md
Normal file
62
docs/docs/photos/features/passkeys.md
Normal file
@@ -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.
|
||||
@@ -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:<br/>
|
||||
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:
|
||||
|
||||
@@ -1,19 +1,4 @@
|
||||
module.exports = {
|
||||
extends: ["@/build-config/eslintrc-next"],
|
||||
ignorePatterns: ["next.config.js", "next-env.d.ts"],
|
||||
/* TODO: Temporary overrides */
|
||||
rules: {
|
||||
"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-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",
|
||||
"@typescript-eslint/restrict-template-expressions": "off",
|
||||
"react-refresh/only-export-components": "off",
|
||||
},
|
||||
};
|
||||
|
||||
15
web/apps/accounts/src/components/context.ts
Normal file
15
web/apps/accounts/src/components/context.ts
Normal file
@@ -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<AppContextT | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Utility hook to get the {@link AppContextT}, throwing an exception if it is
|
||||
* not defined.
|
||||
*/
|
||||
export const useAppContext = (): AppContextT => ensure(useContext(AppContext));
|
||||
@@ -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,27 +15,20 @@ 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 { createContext, useContext, useEffect, useState } from "react";
|
||||
import React, { useCallback, 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<AppContextT | undefined>(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<AppProps> = ({ Component, pageProps }) => {
|
||||
const appName: AppName = "accounts";
|
||||
|
||||
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
|
||||
|
||||
const [showNavbar, setShowNavBar] = useState(false);
|
||||
const [showNavbar, setShowNavbar] = useState(false);
|
||||
|
||||
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
|
||||
DialogBoxAttributesV2 | undefined
|
||||
@@ -48,8 +40,6 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
setDialogBoxV2View(true);
|
||||
}, [dialogBoxAttributeV2]);
|
||||
|
||||
const showNavBar = (show: boolean) => setShowNavBar(show);
|
||||
|
||||
const isMobile = useMediaQuery("(max-width: 428px)");
|
||||
|
||||
const router = useRouter();
|
||||
@@ -57,7 +47,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);
|
||||
}, []);
|
||||
@@ -66,14 +56,14 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -92,7 +82,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
sx={{ zIndex: 1600 }}
|
||||
open={dialogBoxV2View}
|
||||
onClose={closeDialogBoxV2}
|
||||
attributes={dialogBoxAttributeV2 as any}
|
||||
attributes={dialogBoxAttributeV2}
|
||||
/>
|
||||
|
||||
<AppContext.Provider value={appContext}>
|
||||
@@ -103,8 +93,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,
|
||||
})}
|
||||
>
|
||||
<EnteSpinner />
|
||||
@@ -116,4 +105,6 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
</ThemeProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -18,9 +18,9 @@ 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 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);
|
||||
|
||||
@@ -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,
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<AppContextT | undefined>(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<AppProps> = ({ Component, pageProps }) => {
|
||||
const appName: AppName = "auth";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -75,7 +81,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);
|
||||
@@ -198,4 +204,6 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
</ThemeProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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"];
|
||||
|
||||
Reference in New Issue
Block a user