diff --git a/web/apps/cast/src/services/render.ts b/web/apps/cast/src/services/render.ts index 672f95f269..750b9bac85 100644 --- a/web/apps/cast/src/services/render.ts +++ b/web/apps/cast/src/services/render.ts @@ -317,8 +317,9 @@ const downloadFile = async ( if (!isImageOrLivePhoto(file)) throw new Error("Can only cast images and live photos"); + const customOrigin = await customAPIOrigin(); + const getFile = () => { - const customOrigin = customAPIOrigin(); if (customOrigin) { // See: [Note: Passing credentials for self-hosted file fetches] const params = new URLSearchParams({ castToken }); diff --git a/web/apps/photos/src/components/Sidebar/index.tsx b/web/apps/photos/src/components/Sidebar/index.tsx index 7ec6f70559..463405be75 100644 --- a/web/apps/photos/src/components/Sidebar/index.tsx +++ b/web/apps/photos/src/components/Sidebar/index.tsx @@ -679,15 +679,15 @@ const ExitSection: React.FC = () => { const DebugSection: React.FC = () => { const appContext = useContext(AppContext); const [appVersion, setAppVersion] = useState(); + const [host, setHost] = useState(); const electron = globalThis.electron; useEffect(() => { - electron?.appVersion().then((v) => setAppVersion(v)); + void electron?.appVersion().then(setAppVersion); + void customAPIHost().then(setHost); }); - const host = customAPIHost(); - const confirmLogDownload = () => appContext.setDialogMessage({ title: t("DOWNLOAD_LOGS"), diff --git a/web/apps/photos/src/pages/index.tsx b/web/apps/photos/src/pages/index.tsx index ec37ca3bf8..ec6a10b911 100644 --- a/web/apps/photos/src/pages/index.tsx +++ b/web/apps/photos/src/pages/index.tsx @@ -22,7 +22,7 @@ import { t } from "i18next"; import { useRouter } from "next/router"; import { CarouselProvider, DotGroup, Slide, Slider } from "pure-react-carousel"; import "pure-react-carousel/dist/react-carousel.es.css"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import { Trans } from "react-i18next"; import { useAppContext } from "./_app"; @@ -31,14 +31,17 @@ export default function LandingPage() { const [loading, setLoading] = useState(true); const [showLogin, setShowLogin] = useState(true); - // This is kept as state because it can change as a result of user action - // while we're on this page (there currently isn't an event listener we can - // attach to for observing changes to local storage by the same window). - const [host, setHost] = useState(customAPIHost()); + const [host, setHost] = useState(); const router = useRouter(); + const refreshHost = useCallback( + () => void customAPIHost().then(setHost), + [], + ); + useEffect(() => { + refreshHost(); showNavBar(false); const currentURL = new URL(window.location.href); const albumsURL = new URL(albumsAppOrigin()); @@ -51,9 +54,7 @@ export default function LandingPage() { } else { handleNormalRedirect(); } - }, []); - - const handleMaybeChangeHost = () => setHost(customAPIHost()); + }, [refreshHost]); const handleAlbumsRedirect = async (currentURL: URL) => { const end = currentURL.hash.lastIndexOf("&"); @@ -117,7 +118,7 @@ export default function LandingPage() { const redirectToLoginPage = () => router.push(PAGES.LOGIN); return ( - + {loading ? ( ) : ( diff --git a/web/apps/photos/src/services/download/clients/photos.ts b/web/apps/photos/src/services/download/clients/photos.ts index 3740061805..110c51a093 100644 --- a/web/apps/photos/src/services/download/clients/photos.ts +++ b/web/apps/photos/src/services/download/clients/photos.ts @@ -19,10 +19,11 @@ export class PhotosDownloadClient implements DownloadClient { const token = this.token; if (!token) throw Error(CustomError.TOKEN_MISSING); + const customOrigin = await customAPIOrigin(); + // See: [Note: Passing credentials for self-hosted file fetches] const getThumbnail = () => { const opts = { responseType: "arraybuffer", timeout: this.timeout }; - const customOrigin = customAPIOrigin(); if (customOrigin) { const params = new URLSearchParams({ token }); return HTTPService.get( @@ -53,6 +54,8 @@ export class PhotosDownloadClient implements DownloadClient { const token = this.token; if (!token) throw Error(CustomError.TOKEN_MISSING); + const customOrigin = await customAPIOrigin(); + // See: [Note: Passing credentials for self-hosted file fetches] const getFile = () => { const opts = { @@ -61,7 +64,6 @@ export class PhotosDownloadClient implements DownloadClient { onDownloadProgress, }; - const customOrigin = customAPIOrigin(); if (customOrigin) { const params = new URLSearchParams({ token }); return HTTPService.get( @@ -89,6 +91,8 @@ export class PhotosDownloadClient implements DownloadClient { const token = this.token; if (!token) throw Error(CustomError.TOKEN_MISSING); + const customOrigin = await customAPIOrigin(); + // [Note: Passing credentials for self-hosted file fetches] // // Fetching files (or thumbnails) in the default self-hosted Ente @@ -126,7 +130,6 @@ export class PhotosDownloadClient implements DownloadClient { // signed URL and stream back the response. const getFile = () => { - const customOrigin = customAPIOrigin(); if (customOrigin) { const params = new URLSearchParams({ token }); return fetch( diff --git a/web/apps/photos/src/services/download/clients/publicAlbums.ts b/web/apps/photos/src/services/download/clients/publicAlbums.ts index 9875c8d0fc..d471591e63 100644 --- a/web/apps/photos/src/services/download/clients/publicAlbums.ts +++ b/web/apps/photos/src/services/download/clients/publicAlbums.ts @@ -20,6 +20,7 @@ export class PublicAlbumsDownloadClient implements DownloadClient { const accessToken = this.token; const accessTokenJWT = this.passwordToken; if (!accessToken) throw Error(CustomError.TOKEN_MISSING); + const customOrigin = await customAPIOrigin(); // See: [Note: Passing credentials for self-hosted file fetches] const getThumbnail = () => { @@ -27,7 +28,6 @@ export class PublicAlbumsDownloadClient implements DownloadClient { responseType: "arraybuffer", }; - const customOrigin = customAPIOrigin(); if (customOrigin) { const params = new URLSearchParams({ accessToken, @@ -67,6 +67,8 @@ export class PublicAlbumsDownloadClient implements DownloadClient { const accessTokenJWT = this.passwordToken; if (!accessToken) throw Error(CustomError.TOKEN_MISSING); + const customOrigin = await customAPIOrigin(); + // See: [Note: Passing credentials for self-hosted file fetches] const getFile = () => { const opts = { @@ -75,7 +77,6 @@ export class PublicAlbumsDownloadClient implements DownloadClient { onDownloadProgress, }; - const customOrigin = customAPIOrigin(); if (customOrigin) { const params = new URLSearchParams({ accessToken, @@ -112,9 +113,10 @@ export class PublicAlbumsDownloadClient implements DownloadClient { const accessTokenJWT = this.passwordToken; if (!accessToken) throw Error(CustomError.TOKEN_MISSING); + const customOrigin = await customAPIOrigin(); + // See: [Note: Passing credentials for self-hosted file fetches] const getFile = () => { - const customOrigin = customAPIOrigin(); if (customOrigin) { const params = new URLSearchParams({ accessToken, diff --git a/web/apps/photos/src/services/upload/uploadHttpClient.ts b/web/apps/photos/src/services/upload/uploadHttpClient.ts index 67e52c2143..badf03aa0d 100644 --- a/web/apps/photos/src/services/upload/uploadHttpClient.ts +++ b/web/apps/photos/src/services/upload/uploadHttpClient.ts @@ -117,9 +117,10 @@ class UploadHttpClient { progressTracker, ): Promise { try { + const origin = await uploaderOrigin(); await retryHTTPCall(() => HTTPService.put( - `${uploaderOrigin()}/file-upload`, + `${origin}/file-upload`, file, null, { @@ -173,9 +174,10 @@ class UploadHttpClient { progressTracker, ) { try { + const origin = await uploaderOrigin(); const response = await retryHTTPCall(async () => { const resp = await HTTPService.put( - `${uploaderOrigin()}/multipart-upload`, + `${origin}/multipart-upload`, filePart, null, { @@ -214,9 +216,10 @@ class UploadHttpClient { async completeMultipartUploadV2(completeURL: string, reqBody: any) { try { + const origin = await uploaderOrigin(); await retryHTTPCall(() => HTTPService.post( - `${uploaderOrigin()}/multipart-complete`, + `${origin}/multipart-complete`, reqBody, null, { diff --git a/web/packages/accounts/pages/login.tsx b/web/packages/accounts/pages/login.tsx index b6ea5ed207..170a190c9b 100644 --- a/web/packages/accounts/pages/login.tsx +++ b/web/packages/accounts/pages/login.tsx @@ -13,12 +13,12 @@ const Page: React.FC = ({ appContext }) => { const { appName, showNavBar } = appContext; const [loading, setLoading] = useState(true); + const [host, setHost] = useState(); const router = useRouter(); - const host = customAPIHost(); - useEffect(() => { + void customAPIHost().then(setHost); const user = getData(LS_KEYS.USER); if (user?.email) { router.push(PAGES.VERIFY); diff --git a/web/packages/accounts/pages/signup.tsx b/web/packages/accounts/pages/signup.tsx index c55a2a13e0..72fde8ba74 100644 --- a/web/packages/accounts/pages/signup.tsx +++ b/web/packages/accounts/pages/signup.tsx @@ -13,12 +13,12 @@ const Page: React.FC = ({ appContext }) => { const { appName } = appContext; const [loading, setLoading] = useState(true); + const [host, setHost] = useState(); const router = useRouter(); - const host = customAPIHost(); - useEffect(() => { + void customAPIHost().then(setHost); const user = getData(LS_KEYS.USER); if (user?.email) { router.push(PAGES.VERIFY); diff --git a/web/packages/next/origins.ts b/web/packages/next/origins.ts index 66e6a9aefa..b21dbd59a6 100644 --- a/web/packages/next/origins.ts +++ b/web/packages/next/origins.ts @@ -1,4 +1,5 @@ import { nullToUndefined } from "@/utils/transform"; +import { get, set } from "idb-keyval"; /** * Return the origin (scheme, host, port triple) that should be used for making @@ -7,7 +8,8 @@ import { nullToUndefined } from "@/utils/transform"; * This defaults "https://api.ente.io", Ente's production API servers. but can * be overridden when self hosting or developing (see {@link customAPIOrigin}). */ -export const apiOrigin = () => customAPIOrigin() ?? "https://api.ente.io"; +export const apiOrigin = async () => + (await customAPIOrigin()) ?? "https://api.ente.io"; /** * Return the overridden API origin, if one is defined by either (in priority @@ -20,10 +22,21 @@ export const apiOrigin = () => customAPIOrigin() ?? "https://api.ente.io"; * * Otherwise return undefined. */ -export const customAPIOrigin = () => - nullToUndefined(localStorage.getItem("apiOrigin")) ?? - process.env.NEXT_PUBLIC_ENTE_ENDPOINT ?? - undefined; +export const customAPIOrigin = async () => { + let origin = await get("apiOrigin"); + if (!origin) { + // TODO: Migration of apiOrigin from local storage to indexed DB + // Remove me after a bit (27 June 2024). + const legacyOrigin = localStorage.getItem("apiOrigin"); + if (legacyOrigin !== null) { + origin = nullToUndefined(legacyOrigin); + if (origin) await set("apiOrigin", origin); + localStorage.removeItem("apiOrigin"); + } + } + + return origin ?? process.env.NEXT_PUBLIC_ENTE_ENDPOINT ?? undefined; +}; /** * A convenience wrapper over {@link customAPIOrigin} that returns the only the @@ -31,8 +44,8 @@ export const customAPIOrigin = () => * * This is useful in places where we indicate the custom origin in the UI. */ -export const customAPIHost = () => { - const origin = customAPIOrigin(); +export const customAPIHost = async () => { + const origin = await customAPIOrigin(); return origin ? new URL(origin).host : undefined; }; @@ -44,8 +57,8 @@ export const customAPIHost = () => { * this value is set to the {@link customAPIOrigin} itself, effectively * bypassing the Cloudflare worker for non-Ente deployments. */ -export const uploaderOrigin = () => - customAPIOrigin() ?? "https://uploader.ente.io"; +export const uploaderOrigin = async () => + (await customAPIOrigin()) ?? "https://uploader.ente.io"; /** * Return the origin that serves the accounts app. diff --git a/web/packages/shared/components/LoginComponents.tsx b/web/packages/shared/components/LoginComponents.tsx index 8201ccacc9..46cdc102da 100644 --- a/web/packages/shared/components/LoginComponents.tsx +++ b/web/packages/shared/components/LoginComponents.tsx @@ -10,7 +10,7 @@ import EnteButton from "@ente/shared/components/EnteButton"; import { CircularProgress, Stack, Typography, styled } from "@mui/material"; import { t } from "i18next"; import { useRouter } from "next/router"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { VerticallyCentered } from "./Container"; import type { DialogBoxAttributesV2 } from "./DialogBoxV2/types"; import { genericErrorAttributes } from "./ErrorComponents"; @@ -48,7 +48,9 @@ const Header_ = styled("div")` export const LoginFlowFormFooter: React.FC = ({ children, }) => { - const host = customAPIHost(); + const [host, setHost] = useState(); + + useEffect(() => void customAPIHost().then(setHost), []); return (