[web] Move custom apiOrigin to IndexDB (#2306)

Earlier we were storing the custom API origin setting in local storage.
Local storage is not accessible from web workers, which is a problem in
general (and in particular, this caused face indexing to fail since we
were not able to put the embeddings to remote since that code runs in a
web worker).

Move this to a Indexed DB. Do this in a way we can reuse the same table
for more such ad-hoc keys.
This commit is contained in:
Manav Rathi
2024-06-27 20:44:21 +05:30
committed by GitHub
39 changed files with 488 additions and 261 deletions

View File

@@ -1,5 +1,5 @@
import { isDevBuild } from "@/next/env";
import { apiOrigin } from "@/next/origins";
import { apiURL } from "@/next/origins";
import { clientPackageName } from "@/next/types/app";
import { TwoFactorAuthorizationResponse } from "@/next/types/credentials";
import { ensure } from "@/utils/ensure";
@@ -58,7 +58,7 @@ const GetPasskeysResponse = z.object({
* has no passkeys.
*/
export const getPasskeys = async (token: string) => {
const url = `${apiOrigin()}/passkeys`;
const url = await apiURL("/passkeys");
const res = await fetch(url, {
headers: accountsAuthenticatedRequestHeaders(token),
});
@@ -82,7 +82,7 @@ export const renamePasskey = async (
name: string,
) => {
const params = new URLSearchParams({ friendlyName: name });
const url = `${apiOrigin()}/passkeys/${id}`;
const url = await apiURL(`/passkeys/${id}`);
const res = await fetch(`${url}?${params.toString()}`, {
method: "PATCH",
headers: accountsAuthenticatedRequestHeaders(token),
@@ -98,7 +98,7 @@ export const renamePasskey = async (
* @param id The `id` of the existing passkey to delete.
*/
export const deletePasskey = async (token: string, id: string) => {
const url = `${apiOrigin()}/passkeys/${id}`;
const url = await apiURL(`/passkeys/${id}`);
const res = await fetch(url, {
method: "DELETE",
headers: accountsAuthenticatedRequestHeaders(token),
@@ -149,7 +149,7 @@ interface BeginPasskeyRegistrationResponse {
}
const beginPasskeyRegistration = async (token: string) => {
const url = `${apiOrigin()}/passkeys/registration/begin`;
const url = await apiURL("/passkeys/registration/begin");
const res = await fetch(url, {
method: "POST",
headers: accountsAuthenticatedRequestHeaders(token),
@@ -293,7 +293,7 @@ const finishPasskeyRegistration = async ({
const transports = attestationResponse.getTransports();
const params = new URLSearchParams({ friendlyName, sessionID });
const url = `${apiOrigin()}/passkeys/registration/finish`;
const url = await apiURL("/passkeys/registration/finish");
const res = await fetch(`${url}?${params.toString()}`, {
method: "POST",
headers: accountsAuthenticatedRequestHeaders(token),
@@ -414,7 +414,7 @@ export const passkeySessionAlreadyClaimedErrorMessage =
export const beginPasskeyAuthentication = async (
passkeySessionID: string,
): Promise<BeginPasskeyAuthenticationResponse> => {
const url = `${apiOrigin()}/users/two-factor/passkeys/begin`;
const url = await apiURL("/users/two-factor/passkeys/begin");
const res = await fetch(url, {
method: "POST",
body: JSON.stringify({ sessionID: passkeySessionID }),
@@ -504,7 +504,7 @@ export const finishPasskeyAuthentication = async ({
ceremonySessionID,
clientPackage,
});
const url = `${apiOrigin()}/users/two-factor/passkeys/finish`;
const url = await apiURL("/users/two-factor/passkeys/finish");
const res = await fetch(`${url}?${params.toString()}`, {
method: "POST",
headers: {

View File

@@ -1,5 +1,5 @@
import log from "@/next/log";
import { apiOrigin } from "@/next/origins";
import { apiURL } from "@/next/origins";
import ComlinkCryptoWorker from "@ente/shared/crypto";
import { ApiError, CustomError } from "@ente/shared/error";
import HTTPService from "@ente/shared/network/HTTPService";
@@ -81,7 +81,7 @@ interface AuthKey {
export const getAuthKey = async (): Promise<AuthKey> => {
try {
const resp = await HTTPService.get(
`${apiOrigin()}/authenticator/key`,
await apiURL("/authenticator/key"),
{},
{
"X-Auth-Token": getToken(),
@@ -108,7 +108,7 @@ export const getDiff = async (
): Promise<AuthEntity[]> => {
try {
const resp = await HTTPService.get(
`${apiOrigin()}/authenticator/entity/diff`,
await apiURL("/authenticator/entity/diff"),
{
sinceTime,
limit,

View File

@@ -17,7 +17,7 @@ import type {
} from "@/new/photos/types/file";
import { nameAndExtension } from "@/next/file";
import log from "@/next/log";
import { apiOrigin, customAPIOrigin } from "@/next/origins";
import { apiURL, customAPIOrigin } from "@/next/origins";
import { shuffled } from "@/utils/array";
import { ensure } from "@/utils/ensure";
import { wait } from "@/utils/promise";
@@ -164,7 +164,7 @@ const getEncryptedCollectionFiles = async (
let resp: AxiosResponse;
do {
resp = await HTTPService.get(
`${apiOrigin()}/cast/diff`,
await apiURL("/cast/diff"),
{ sinceTime },
{
"Cache-Control": "no-cache",
@@ -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 });

View File

@@ -22,7 +22,6 @@
"fast-srp-hap": "^2.0.4",
"ffmpeg-wasm": "file:./thirdparty/ffmpeg-wasm",
"hdbscan": "0.0.1-alpha.5",
"idb": "^8",
"leaflet": "^1.9.4",
"leaflet-defaulticon-compatibility": "^0.1.1",
"localforage": "^1.9.0",

View File

@@ -679,15 +679,15 @@ const ExitSection: React.FC = () => {
const DebugSection: React.FC = () => {
const appContext = useContext(AppContext);
const [appVersion, setAppVersion] = useState<string | undefined>();
const [host, setHost] = useState<string | undefined>();
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"),

View File

@@ -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 { useCallback, useEffect, useState } 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<string | undefined>();
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 (
<TappableContainer onMaybeChangeHost={handleMaybeChangeHost}>
<TappableContainer onMaybeChangeHost={refreshHost}>
{loading ? (
<EnteSpinner />
) : (

View File

@@ -1,5 +1,5 @@
import log from "@/next/log";
import { apiOrigin, paymentsAppOrigin } from "@/next/origins";
import { apiURL, paymentsAppOrigin } from "@/next/origins";
import HTTPService from "@ente/shared/network/HTTPService";
import {
LS_KEYS,
@@ -33,11 +33,11 @@ class billingService {
let response;
if (!token) {
response = await HTTPService.get(
`${apiOrigin()}/billing/plans/v2`,
await apiURL("/billing/plans/v2"),
);
} else {
response = await HTTPService.get(
`${apiOrigin()}/billing/user-plans`,
await apiURL("/billing/user-plans"),
null,
{
"X-Auth-Token": getToken(),
@@ -53,7 +53,7 @@ class billingService {
public async syncSubscription() {
try {
const response = await HTTPService.get(
`${apiOrigin()}/billing/subscription`,
await apiURL("/billing/subscription"),
null,
{
"X-Auth-Token": getToken(),
@@ -97,7 +97,7 @@ class billingService {
public async cancelSubscription() {
try {
const response = await HTTPService.post(
`${apiOrigin()}/billing/stripe/cancel-subscription`,
await apiURL("/billing/stripe/cancel-subscription"),
null,
null,
{
@@ -115,7 +115,7 @@ class billingService {
public async activateSubscription() {
try {
const response = await HTTPService.post(
`${apiOrigin()}/billing/stripe/activate-subscription`,
await apiURL("/billing/stripe/activate-subscription"),
null,
null,
{
@@ -139,7 +139,7 @@ class billingService {
return;
}
const response = await HTTPService.post(
`${apiOrigin()}/billing/verify-subscription`,
await apiURL("/billing/verify-subscription"),
{
paymentProvider: "stripe",
productID: null,
@@ -165,7 +165,7 @@ class billingService {
}
try {
await HTTPService.delete(
`${apiOrigin()}/family/leave`,
await apiURL("/family/leave"),
null,
null,
{
@@ -197,7 +197,7 @@ class billingService {
try {
const redirectURL = this.getRedirectURL();
const response = await HTTPService.get(
`${apiOrigin()}/billing/stripe/customer-portal`,
await apiURL("/billing/stripe/customer-portal"),
{ redirectURL },
{
"X-Auth-Token": getToken(),

View File

@@ -7,7 +7,7 @@ import {
VISIBILITY_STATE,
} from "@/new/photos/types/magicMetadata";
import log from "@/next/log";
import { apiOrigin } from "@/next/origins";
import { apiURL } from "@/next/origins";
import ComlinkCryptoWorker from "@ente/shared/crypto";
import { CustomError } from "@ente/shared/error";
import HTTPService from "@ente/shared/network/HTTPService";
@@ -181,7 +181,7 @@ const getCollections = async (
): Promise<Collection[]> => {
try {
const resp = await HTTPService.get(
`${apiOrigin()}/collections/v2`,
await apiURL("/collections/v2"),
{
sinceTime,
},
@@ -328,7 +328,7 @@ export const getCollection = async (
return;
}
const resp = await HTTPService.get(
`${apiOrigin()}/collections/${collectionID}`,
await apiURL(`/collections/${collectionID}`),
null,
{ "X-Auth-Token": token },
);
@@ -472,7 +472,7 @@ const postCollection = async (
): Promise<EncryptedCollection> => {
try {
const response = await HTTPService.post(
`${apiOrigin()}/collections`,
await apiURL("/collections"),
collectionData,
null,
{ "X-Auth-Token": token },
@@ -527,7 +527,7 @@ export const addToCollection = async (
files: fileKeysEncryptedWithNewCollection,
};
await HTTPService.post(
`${apiOrigin()}/collections/add-files`,
await apiURL("/collections/add-files"),
requestBody,
null,
{
@@ -557,7 +557,7 @@ export const restoreToCollection = async (
files: fileKeysEncryptedWithNewCollection,
};
await HTTPService.post(
`${apiOrigin()}/collections/restore-files`,
await apiURL("/collections/restore-files"),
requestBody,
null,
{
@@ -588,7 +588,7 @@ export const moveToCollection = async (
files: fileKeysEncryptedWithNewCollection,
};
await HTTPService.post(
`${apiOrigin()}/collections/move-files`,
await apiURL("/collections/move-files"),
requestBody,
null,
{
@@ -734,7 +734,7 @@ export const removeNonUserFiles = async (
};
await HTTPService.post(
`${apiOrigin()}/collections/v3/remove-files`,
await apiURL("/collections/v3/remove-files"),
request,
null,
{ "X-Auth-Token": token },
@@ -761,7 +761,7 @@ export const deleteCollection = async (
const token = getToken();
await HTTPService.delete(
`${apiOrigin()}/collections/v3/${collectionID}`,
await apiURL(`/collections/v3/${collectionID}`),
null,
{ collectionID, keepFiles },
{ "X-Auth-Token": token },
@@ -777,7 +777,7 @@ export const leaveSharedAlbum = async (collectionID: number) => {
const token = getToken();
await HTTPService.post(
`${apiOrigin()}/collections/leave/${collectionID}`,
await apiURL(`/collections/leave/${collectionID}`),
null,
null,
{ "X-Auth-Token": token },
@@ -815,7 +815,7 @@ export const updateCollectionMagicMetadata = async (
};
await HTTPService.put(
`${apiOrigin()}/collections/magic-metadata`,
await apiURL("/collections/magic-metadata"),
reqBody,
null,
{
@@ -859,7 +859,7 @@ export const updateSharedCollectionMagicMetadata = async (
};
await HTTPService.put(
`${apiOrigin()}/collections/sharee-magic-metadata`,
await apiURL("/collections/sharee-magic-metadata"),
reqBody,
null,
{
@@ -903,7 +903,7 @@ export const updatePublicCollectionMagicMetadata = async (
};
await HTTPService.put(
`${apiOrigin()}/collections/public-magic-metadata`,
await apiURL("/collections/public-magic-metadata"),
reqBody,
null,
{
@@ -938,7 +938,7 @@ export const renameCollection = async (
nameDecryptionNonce,
};
await HTTPService.post(
`${apiOrigin()}/collections/rename`,
await apiURL("/collections/rename"),
collectionRenameRequest,
null,
{
@@ -967,7 +967,7 @@ export const shareCollection = async (
encryptedKey,
};
await HTTPService.post(
`${apiOrigin()}/collections/share`,
await apiURL("/collections/share"),
shareCollectionRequest,
null,
{
@@ -991,7 +991,7 @@ export const unshareCollection = async (
email: withUserEmail,
};
await HTTPService.post(
`${apiOrigin()}/collections/unshare`,
await apiURL("/collections/unshare"),
shareCollectionRequest,
null,
{
@@ -1013,7 +1013,7 @@ export const createShareableURL = async (collection: Collection) => {
collectionID: collection.id,
};
const resp = await HTTPService.post(
`${apiOrigin()}/collections/share-url`,
await apiURL("/collections/share-url"),
createPublicAccessTokenRequest,
null,
{
@@ -1034,7 +1034,7 @@ export const deleteShareableURL = async (collection: Collection) => {
return null;
}
await HTTPService.delete(
`${apiOrigin()}/collections/share-url/${collection.id}`,
await apiURL(`/collections/share-url/${collection.id}`),
null,
null,
{
@@ -1056,7 +1056,7 @@ export const updateShareableURL = async (
return null;
}
const res = await HTTPService.put(
`${apiOrigin()}/collections/share-url`,
await apiURL("/collections/share-url"),
request,
null,
{

View File

@@ -3,7 +3,7 @@ import { FILE_TYPE } from "@/media/file-type";
import type { Metadata } from "@/media/types/file";
import { EnteFile } from "@/new/photos/types/file";
import log from "@/next/log";
import { apiOrigin } from "@/next/origins";
import { apiURL } from "@/next/origins";
import HTTPService from "@ente/shared/network/HTTPService";
import { getToken } from "@ente/shared/storage/localStorage/helpers";
@@ -146,7 +146,7 @@ function groupDupesByFileHashes(dupe: Duplicate) {
async function fetchDuplicateFileIDs() {
try {
const response = await HTTPService.get(
`${apiOrigin()}/files/duplicates`,
await apiURL("/files/duplicates"),
null,
{
"X-Auth-Token": getToken(),

View File

@@ -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(

View File

@@ -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,

View File

@@ -3,7 +3,7 @@ import { getAllLocalFiles } from "@/new/photos/services/files";
import { EnteFile } from "@/new/photos/types/file";
import { inWorker } from "@/next/env";
import log from "@/next/log";
import { apiOrigin } from "@/next/origins";
import { apiURL } from "@/next/origins";
import { workerBridge } from "@/next/worker/worker-bridge";
import ComlinkCryptoWorker from "@ente/shared/crypto";
import { CustomError } from "@ente/shared/error";
@@ -285,7 +285,7 @@ export const getEmbeddingsDiff = async (
return;
}
const response = await HTTPService.get(
`${apiOrigin()}/embeddings/diff`,
await apiURL("/embeddings/diff"),
{
sinceTime,
limit: DIFF_LIMIT,
@@ -314,7 +314,7 @@ export const putEmbedding = async (
throw Error(CustomError.TOKEN_MISSING);
}
const resp = await HTTPService.put(
`${apiOrigin()}/embeddings`,
await apiURL("/embeddings"),
putEmbeddingReq,
null,
{

View File

@@ -1,5 +1,5 @@
import log from "@/next/log";
import { apiOrigin } from "@/next/origins";
import { apiURL } from "@/next/origins";
import ComlinkCryptoWorker from "@ente/shared/crypto";
import HTTPService from "@ente/shared/network/HTTPService";
import localForage from "@ente/shared/storage/localForage";
@@ -58,7 +58,7 @@ const getEntityKey = async (type: EntityType) => {
return;
}
const resp = await HTTPService.get(
`${apiOrigin()}/user-entity/key`,
await apiURL("/user-entity/key"),
{
type,
},
@@ -173,7 +173,7 @@ const getEntityDiff = async (
return;
}
const resp = await HTTPService.get(
`${apiOrigin()}/user-entity/entity/diff`,
await apiURL("/user-entity/entity/diff"),
{
sinceTime: time,
type,

View File

@@ -14,7 +14,7 @@ import type { FaceIndex } from "./types";
* Index faces in a file, save the persist the results locally, and put them on
* remote.
*
* This class is instantiated in a Web Worker so as to not get in the way of the
* This class is instantiated in a web worker so as to not get in the way of the
* main thread. It could've been a bunch of free standing functions too, it is
* just a class for convenience of compatibility with how the rest of our
* comlink workers are structured.

View File

@@ -8,7 +8,7 @@ import {
} from "@/new/photos/types/file";
import { BulkUpdateMagicMetadataRequest } from "@/new/photos/types/magicMetadata";
import log from "@/next/log";
import { apiOrigin } from "@/next/origins";
import { apiURL } from "@/next/origins";
import ComlinkCryptoWorker from "@ente/shared/crypto";
import HTTPService from "@ente/shared/network/HTTPService";
import { getToken } from "@ente/shared/storage/localStorage/helpers";
@@ -70,7 +70,7 @@ export const getFiles = async (
break;
}
resp = await HTTPService.get(
`${apiOrigin()}/collections/v2/diff`,
await apiURL("/collections/v2/diff"),
{
collectionID: collection.id,
sinceTime: time,
@@ -139,7 +139,7 @@ export const trashFiles = async (filesToTrash: EnteFile[]) => {
})),
};
await HTTPService.post(
`${apiOrigin()}/files/trash`,
await apiURL("/files/trash"),
trashRequest,
null,
{
@@ -163,7 +163,7 @@ export const deleteFromTrash = async (filesToDelete: number[]) => {
for (const batch of batchedFilesToDelete) {
await HTTPService.post(
`${apiOrigin()}/trash/delete`,
await apiURL("/trash/delete"),
{ fileIDs: batch },
null,
{
@@ -206,7 +206,7 @@ export const updateFileMagicMetadata = async (
});
}
await HTTPService.put(
`${apiOrigin()}/files/magic-metadata`,
await apiURL("/files/magic-metadata"),
reqBody,
null,
{
@@ -253,7 +253,7 @@ export const updateFilePublicMagicMetadata = async (
});
}
await HTTPService.put(
`${apiOrigin()}/files/public-magic-metadata`,
await apiURL("/files/public-magic-metadata"),
reqBody,
null,
{

View File

@@ -18,6 +18,9 @@ export const photosLogout = async () => {
const ignoreError = (label: string, e: unknown) =>
log.error(`Ignoring error during logout (${label})`, e);
// Terminate any workers before clearing persistent state.
// See: [Note: Caching IDB instances in separate execution contexts].
await accountLogout();
try {

View File

@@ -1,6 +1,6 @@
import { EncryptedEnteFile, EnteFile } from "@/new/photos/types/file";
import log from "@/next/log";
import { apiOrigin } from "@/next/origins";
import { apiURL } from "@/next/origins";
import ComlinkCryptoWorker from "@ente/shared/crypto";
import { CustomError, parseSharingErrorCodes } from "@ente/shared/error";
import HTTPService from "@ente/shared/network/HTTPService";
@@ -252,7 +252,7 @@ const getPublicFiles = async (
break;
}
resp = await HTTPService.get(
`${apiOrigin()}/public-collection/diff`,
await apiURL("/public-collection/diff"),
{
sinceTime: time,
},
@@ -307,7 +307,7 @@ export const getPublicCollection = async (
return;
}
const resp = await HTTPService.get(
`${apiOrigin()}/public-collection/info`,
await apiURL("/public-collection/info"),
null,
{ "Cache-Control": "no-cache", "X-Auth-Access-Token": token },
);
@@ -357,7 +357,7 @@ export const verifyPublicCollectionPassword = async (
): Promise<string> => {
try {
const resp = await HTTPService.post(
`${apiOrigin()}/public-collection/verify-password`,
await apiURL("/public-collection/verify-password"),
{ passHash: passwordHash },
null,
{ "Cache-Control": "no-cache", "X-Auth-Access-Token": token },

View File

@@ -1,6 +1,6 @@
import { EnteFile } from "@/new/photos/types/file";
import log from "@/next/log";
import { apiOrigin } from "@/next/origins";
import { apiURL } from "@/next/origins";
import HTTPService from "@ente/shared/network/HTTPService";
import localForage from "@ente/shared/storage/localForage";
import { getToken } from "@ente/shared/storage/localStorage/helpers";
@@ -89,7 +89,7 @@ export const updateTrash = async (
break;
}
resp = await HTTPService.get(
`${apiOrigin()}/trash/v2/diff`,
await apiURL("/trash/v2/diff"),
{
sinceTime: time,
},
@@ -158,7 +158,7 @@ export const emptyTrash = async () => {
const lastUpdatedAt = await getLastSyncTime();
await HTTPService.post(
`${apiOrigin()}/trash/empty`,
await apiURL("/trash/empty"),
{ lastUpdatedAt },
null,
{

View File

@@ -1,6 +1,6 @@
import { EnteFile } from "@/new/photos/types/file";
import log from "@/next/log";
import { apiOrigin } from "@/next/origins";
import { apiURL } from "@/next/origins";
import { CustomError, handleUploadError } from "@ente/shared/error";
import HTTPService from "@ente/shared/network/HTTPService";
import { retryHTTPCall } from "./uploadHttpClient";
@@ -20,19 +20,15 @@ class PublicUploadHttpClient {
if (!token) {
throw Error(CustomError.TOKEN_MISSING);
}
const url = await apiURL("/public-collection/file");
const response = await retryHTTPCall(
() =>
HTTPService.post(
`${apiOrigin()}/public-collection/file`,
uploadFile,
null,
{
"X-Auth-Access-Token": token,
...(passwordToken && {
"X-Auth-Access-Token-JWT": passwordToken,
}),
},
),
HTTPService.post(url, uploadFile, null, {
"X-Auth-Access-Token": token,
...(passwordToken && {
"X-Auth-Access-Token-JWT": passwordToken,
}),
}),
handleUploadError,
);
return response.data;
@@ -55,7 +51,7 @@ class PublicUploadHttpClient {
throw Error(CustomError.TOKEN_MISSING);
}
this.uploadURLFetchInProgress = HTTPService.get(
`${apiOrigin()}/public-collection/upload-urls`,
await apiURL("/public-collection/upload-urls"),
{
count: Math.min(MAX_URL_REQUESTS, count * 2),
},
@@ -91,7 +87,7 @@ class PublicUploadHttpClient {
throw Error(CustomError.TOKEN_MISSING);
}
const response = await HTTPService.get(
`${apiOrigin()}/public-collection/multipart-upload-urls`,
await apiURL("/public-collection/multipart-upload-urls"),
{
count,
},

View File

@@ -1,6 +1,6 @@
import { EnteFile } from "@/new/photos/types/file";
import log from "@/next/log";
import { apiOrigin, uploaderOrigin } from "@/next/origins";
import { apiURL, uploaderOrigin } from "@/next/origins";
import { wait } from "@/utils/promise";
import { CustomError, handleUploadError } from "@ente/shared/error";
import HTTPService from "@ente/shared/network/HTTPService";
@@ -18,9 +18,10 @@ class UploadHttpClient {
if (!token) {
return;
}
const url = await apiURL("/files");
const response = await retryHTTPCall(
() =>
HTTPService.post(`${apiOrigin()}/files`, uploadFile, null, {
HTTPService.post(url, uploadFile, null, {
"X-Auth-Token": token,
}),
handleUploadError,
@@ -41,7 +42,7 @@ class UploadHttpClient {
return;
}
this.uploadURLFetchInProgress = HTTPService.get(
`${apiOrigin()}/files/upload-urls`,
await apiURL("/files/upload-urls"),
{
count: Math.min(MAX_URL_REQUESTS, count * 2),
},
@@ -71,7 +72,7 @@ class UploadHttpClient {
return;
}
const response = await HTTPService.get(
`${apiOrigin()}/files/multipart-upload-urls`,
await apiURL("/files/multipart-upload-urls"),
{
count,
},
@@ -117,9 +118,10 @@ class UploadHttpClient {
progressTracker,
): Promise<string> {
try {
const origin = await uploaderOrigin();
await retryHTTPCall(() =>
HTTPService.put(
`${uploaderOrigin()}/file-upload`,
`${origin}/file-upload`,
file,
null,
{
@@ -173,9 +175,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 +217,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,
{

View File

@@ -1,5 +1,5 @@
import log from "@/next/log";
import { apiOrigin, customAPIOrigin, familyAppOrigin } from "@/next/origins";
import { apiURL, customAPIOrigin, familyAppOrigin } from "@/next/origins";
import { putAttributes } from "@ente/accounts/api/user";
import { ApiError } from "@ente/shared/error";
import HTTPService from "@ente/shared/network/HTTPService";
@@ -23,7 +23,7 @@ export const getPublicKey = async (email: string) => {
const token = getToken();
const resp = await HTTPService.get(
`${apiOrigin()}/users/public-key`,
await apiURL("/users/public-key"),
{ email },
{
"X-Auth-Token": token,
@@ -36,7 +36,7 @@ export const getPaymentToken = async () => {
const token = getToken();
const resp = await HTTPService.get(
`${apiOrigin()}/users/payment-token`,
await apiURL("/users/payment-token"),
null,
{
"X-Auth-Token": token,
@@ -50,7 +50,7 @@ export const getFamiliesToken = async () => {
const token = getToken();
const resp = await HTTPService.get(
`${apiOrigin()}/users/families-token`,
await apiURL("/users/families-token"),
null,
{
"X-Auth-Token": token,
@@ -68,7 +68,7 @@ export const getRoadmapRedirectURL = async () => {
const token = getToken();
const resp = await HTTPService.get(
`${apiOrigin()}/users/roadmap/v2`,
await apiURL("/users/roadmap/v2"),
null,
{
"X-Auth-Token": token,
@@ -84,7 +84,7 @@ export const getRoadmapRedirectURL = async () => {
export const isTokenValid = async (token: string) => {
try {
const resp = await HTTPService.get(
`${apiOrigin()}/users/session-validity/v2`,
await apiURL("/users/session-validity/v2"),
null,
{
"X-Auth-Token": token,
@@ -123,7 +123,7 @@ export const isTokenValid = async (token: string) => {
export const getTwoFactorStatus = async () => {
const resp = await HTTPService.get(
`${apiOrigin()}/users/two-factor/status`,
await apiURL("/users/two-factor/status"),
null,
{
"X-Auth-Token": getToken(),
@@ -137,7 +137,7 @@ export const getUserDetailsV2 = async (): Promise<UserDetails> => {
const token = getToken();
const resp = await HTTPService.get(
`${apiOrigin()}/users/details/v2`,
await apiURL("/users/details/v2"),
null,
{
"X-Auth-Token": token,
@@ -168,7 +168,7 @@ export const getAccountDeleteChallenge = async () => {
const token = getToken();
const resp = await HTTPService.get(
`${apiOrigin()}/users/delete-challenge`,
await apiURL("/users/delete-challenge"),
null,
{
"X-Auth-Token": token,
@@ -193,7 +193,7 @@ export const deleteAccount = async (
}
await HTTPService.delete(
`${apiOrigin()}/users/delete`,
await apiURL("/users/delete"),
{ challenge, reason, feedback },
null,
{
@@ -211,7 +211,7 @@ export const getFaceSearchEnabledStatus = async () => {
const token = getToken();
const resp: AxiosResponse<GetRemoteStoreValueResponse> =
await HTTPService.get(
`${apiOrigin()}/remote-store`,
await apiURL("/remote-store"),
{
key: "faceSearchEnabled",
defaultValue: false,
@@ -231,7 +231,7 @@ export const updateFaceSearchEnabledStatus = async (newStatus: boolean) => {
try {
const token = getToken();
await HTTPService.post(
`${apiOrigin()}/remote-store/update`,
await apiURL("/remote-store/update"),
{
key: "faceSearchEnabled",
value: newStatus.toString(),
@@ -262,7 +262,7 @@ export const getMapEnabledStatus = async () => {
const token = getToken();
const resp: AxiosResponse<GetRemoteStoreValueResponse> =
await HTTPService.get(
`${apiOrigin()}/remote-store`,
await apiURL("/remote-store"),
{
key: "mapEnabled",
defaultValue: false,
@@ -282,7 +282,7 @@ export const updateMapEnabledStatus = async (newStatus: boolean) => {
try {
const token = getToken();
await HTTPService.post(
`${apiOrigin()}/remote-store/update`,
await apiURL("/remote-store/update"),
{
key: "mapEnabled",
value: newStatus.toString(),
@@ -318,7 +318,7 @@ export async function getDisableCFUploadProxyFlag(): Promise<boolean> {
// In such cases, disable the Cloudflare upload proxy (which won't work for
// self-hosters), and instead just directly use the upload URLs that museum
// gives us.
if (customAPIOrigin()) return true;
if (await customAPIOrigin()) return true;
try {
const featureFlags = (

View File

@@ -169,7 +169,7 @@ For more details, see [translations.md](translations.md).
## Infrastructure
- [comlink](https://github.com/GoogleChromeLabs/comlink) provides a minimal
layer on top of Web Workers to make them more easier to use.
layer on top of web workers to make them more easier to use.
- [idb](https://github.com/jakearchibald/idb) provides a promise API over the
browser-native IndexedDB APIs.

View File

@@ -28,6 +28,9 @@ IndexedDB is a transactional NoSQL store provided by browsers. It has quite
large storage limits, and data is stored per origin (and remains persistent
across tab restarts).
Unlike local storage, IndexedDB is also accessible from web workers and so we
also use IndexedDB for storing ad-hoc key value pairs.
Older code used the LocalForage library for storing things in Indexed DB. This
library falls back to localStorage in case Indexed DB storage is not available.

View File

@@ -1,5 +1,5 @@
import log from "@/next/log";
import { apiOrigin } from "@/next/origins";
import { apiURL } from "@/next/origins";
import type {
CompleteSRPSetupRequest,
CompleteSRPSetupResponse,
@@ -21,7 +21,7 @@ export const getSRPAttributes = async (
): Promise<SRPAttributes | null> => {
try {
const resp = await HTTPService.get(
`${apiOrigin()}/users/srp/attributes`,
await apiURL("/users/srp/attributes"),
{
email,
},
@@ -39,7 +39,7 @@ export const startSRPSetup = async (
): Promise<SetupSRPResponse> => {
try {
const resp = await HTTPService.post(
`${apiOrigin()}/users/srp/setup`,
await apiURL("/users/srp/setup"),
setupSRPRequest,
undefined,
{
@@ -60,7 +60,7 @@ export const completeSRPSetup = async (
) => {
try {
const resp = await HTTPService.post(
`${apiOrigin()}/users/srp/complete`,
await apiURL("/users/srp/complete"),
completeSRPSetupRequest,
undefined,
{
@@ -77,7 +77,7 @@ export const completeSRPSetup = async (
export const createSRPSession = async (srpUserID: string, srpA: string) => {
try {
const resp = await HTTPService.post(
`${apiOrigin()}/users/srp/create-session`,
await apiURL("/users/srp/create-session"),
{
srpUserID,
srpA,
@@ -97,7 +97,7 @@ export const verifySRPSession = async (
) => {
try {
const resp = await HTTPService.post(
`${apiOrigin()}/users/srp/verify-session`,
await apiURL("/users/srp/verify-session"),
{
sessionID,
srpUserID,
@@ -125,7 +125,7 @@ export const updateSRPAndKeys = async (
): Promise<UpdateSRPAndKeysResponse> => {
try {
const resp = await HTTPService.post(
`${apiOrigin()}/users/srp/update`,
await apiURL("/users/srp/update"),
updateSRPAndKeyRequest,
undefined,
{

View File

@@ -1,4 +1,4 @@
import { apiOrigin } from "@/next/origins";
import { apiURL } from "@/next/origins";
import type { AppName } from "@/next/types/app";
import type {
RecoveryKey,
@@ -14,25 +14,32 @@ import { getToken } from "@ente/shared/storage/localStorage/helpers";
import type { KeyAttributes } from "@ente/shared/user/types";
import { HttpStatusCode } from "axios";
export const sendOtt = (appName: AppName, email: string) => {
return HTTPService.post(`${apiOrigin()}/users/ott`, {
export const sendOtt = async (appName: AppName, email: string) => {
return HTTPService.post(await apiURL("/users/ott"), {
email,
client: appName == "auth" ? "totp" : "web",
});
};
export const verifyOtt = (email: string, ott: string, referral: string) => {
export const verifyOtt = async (
email: string,
ott: string,
referral: string,
) => {
const cleanedReferral = `web:${referral?.trim() || ""}`;
return HTTPService.post(`${apiOrigin()}/users/verify-email`, {
return HTTPService.post(await apiURL("/users/verify-email"), {
email,
ott,
source: cleanedReferral,
});
};
export const putAttributes = (token: string, keyAttributes: KeyAttributes) =>
export const putAttributes = async (
token: string,
keyAttributes: KeyAttributes,
) =>
HTTPService.put(
`${apiOrigin()}/users/attributes`,
await apiURL("/users/attributes"),
{ keyAttributes },
undefined,
{
@@ -43,7 +50,7 @@ export const putAttributes = (token: string, keyAttributes: KeyAttributes) =>
export const logout = async () => {
try {
const token = getToken();
await HTTPService.post(`${apiOrigin()}/users/logout`, null, undefined, {
await HTTPService.post(await apiURL("/users/logout"), null, undefined, {
"X-Auth-Token": token,
});
} catch (e) {
@@ -64,7 +71,7 @@ export const logout = async () => {
export const verifyTwoFactor = async (code: string, sessionID: string) => {
const resp = await HTTPService.post(
`${apiOrigin()}/users/two-factor/verify`,
await apiURL("/users/two-factor/verify"),
{
code,
sessionID,
@@ -81,7 +88,7 @@ export const recoverTwoFactor = async (
twoFactorType: TwoFactorType,
) => {
const resp = await HTTPService.get(
`${apiOrigin()}/users/two-factor/recover`,
await apiURL("/users/two-factor/recover"),
{
sessionID,
twoFactorType,
@@ -96,7 +103,7 @@ export const removeTwoFactor = async (
twoFactorType: TwoFactorType,
) => {
const resp = await HTTPService.post(
`${apiOrigin()}/users/two-factor/remove`,
await apiURL("/users/two-factor/remove"),
{
sessionID,
secret,
@@ -108,7 +115,7 @@ export const removeTwoFactor = async (
export const changeEmail = async (email: string, ott: string) => {
await HTTPService.post(
`${apiOrigin()}/users/change-email`,
await apiURL("/users/change-email"),
{
email,
ott,
@@ -121,7 +128,7 @@ export const changeEmail = async (email: string, ott: string) => {
};
export const sendOTTForEmailChange = async (email: string) => {
await HTTPService.post(`${apiOrigin()}/users/ott`, {
await HTTPService.post(await apiURL("/users/ott"), {
email,
client: "web",
purpose: "change",
@@ -130,7 +137,7 @@ export const sendOTTForEmailChange = async (email: string) => {
export const setupTwoFactor = async () => {
const resp = await HTTPService.post(
`${apiOrigin()}/users/two-factor/setup`,
await apiURL("/users/two-factor/setup"),
null,
undefined,
{
@@ -145,7 +152,7 @@ export const enableTwoFactor = async (
recoveryEncryptedTwoFactorSecret: B64EncryptionResult,
) => {
await HTTPService.post(
`${apiOrigin()}/users/two-factor/enable`,
await apiURL("/users/two-factor/enable"),
{
code,
encryptedTwoFactorSecret:
@@ -160,9 +167,9 @@ export const enableTwoFactor = async (
);
};
export const setRecoveryKey = (token: string, recoveryKey: RecoveryKey) =>
export const setRecoveryKey = async (token: string, recoveryKey: RecoveryKey) =>
HTTPService.put(
`${apiOrigin()}/users/recovery-key`,
await apiURL("/users/recovery-key"),
recoveryKey,
undefined,
{
@@ -172,7 +179,7 @@ export const setRecoveryKey = (token: string, recoveryKey: RecoveryKey) =>
export const disableTwoFactor = async () => {
await HTTPService.post(
`${apiOrigin()}/users/two-factor/disable`,
await apiURL("/users/two-factor/disable"),
null,
undefined,
{

View File

@@ -13,12 +13,12 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
const { appName, showNavBar } = appContext;
const [loading, setLoading] = useState(true);
const [host, setHost] = useState<string | undefined>();
const router = useRouter();
const host = customAPIHost();
useEffect(() => {
void customAPIHost().then(setHost);
const user = getData(LS_KEYS.USER);
if (user?.email) {
router.push(PAGES.VERIFY);

View File

@@ -13,12 +13,12 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
const { appName } = appContext;
const [loading, setLoading] = useState(true);
const [host, setHost] = useState<string | undefined>();
const router = useRouter();
const host = customAPIHost();
useEffect(() => {
void customAPIHost().then(setHost);
const user = getData(LS_KEYS.USER);
if (user?.email) {
router.push(PAGES.VERIFY);

View File

@@ -1,5 +1,6 @@
import { clearBlobCaches } from "@/next/blob-cache";
import { clearHTTPState } from "@/next/http";
import { clearKVDB } from "@/next/kv";
import log from "@/next/log";
import InMemoryStore from "@ente/shared/storage/InMemoryStore";
import localForage from "@ente/shared/storage/localForage";
@@ -24,36 +25,41 @@ export const accountLogout = async () => {
try {
await remoteLogout();
} catch (e) {
ignoreError("remote", e);
ignoreError("Remote", e);
}
try {
InMemoryStore.clear();
} catch (e) {
ignoreError("in-memory store", e);
ignoreError("In-memory store", e);
}
try {
clearKeys();
} catch (e) {
ignoreError("session store", e);
ignoreError("Session storage", e);
}
try {
clearData();
} catch (e) {
ignoreError("local storage", e);
ignoreError("Local storage", e);
}
try {
await localForage.clear();
} catch (e) {
ignoreError("local forage", e);
ignoreError("Local forage", e);
}
try {
await clearBlobCaches();
} catch (e) {
ignoreError("cache", e);
ignoreError("Blob cache", e);
}
try {
clearHTTPState();
} catch (e) {
ignoreError("http", e);
ignoreError("HTTP", e);
}
try {
await clearKVDB();
} catch (e) {
ignoreError("KV DB", e);
}
};

View File

@@ -1,6 +1,6 @@
import { clientPackageHeaderIfPresent } from "@/next/http";
import log from "@/next/log";
import { accountsAppOrigin, apiOrigin } from "@/next/origins";
import { accountsAppOrigin, apiURL } from "@/next/origins";
import type { AppName } from "@/next/types/app";
import { clientPackageName } from "@/next/types/app";
import { TwoFactorAuthorizationResponse } from "@/next/types/credentials";
@@ -139,7 +139,7 @@ export const isPasskeyRecoveryEnabled = async () => {
const token = getToken();
const resp = await HTTPService.get(
`${apiOrigin()}/users/two-factor/recovery-status`,
await apiURL("/users/two-factor/recovery-status"),
{},
{
"X-Auth-Token": token,
@@ -166,7 +166,7 @@ const configurePasskeyRecovery = async (
const token = getToken();
const resp = await HTTPService.post(
`${apiOrigin()}/users/two-factor/passkeys/configure-recovery`,
await apiURL("/users/two-factor/passkeys/configure-recovery"),
{
secret,
userSecretCipher,
@@ -196,7 +196,7 @@ const getAccountsToken = async () => {
const token = getToken();
const resp = await HTTPService.get(
`${apiOrigin()}/users/accounts-token`,
await apiURL("/users/accounts-token"),
undefined,
{
"X-Auth-Token": token,
@@ -234,7 +234,7 @@ export const passkeySessionExpiredErrorMessage = "Passkey session has expired";
export const checkPasskeyVerificationStatus = async (
sessionID: string,
): Promise<TwoFactorAuthorizationResponse | undefined> => {
const url = `${apiOrigin()}/users/two-factor/passkeys/get-token`;
const url = await apiURL("/users/two-factor/passkeys/get-token");
const params = new URLSearchParams({ sessionID });
const res = await fetch(`${url}?${params.toString()}`, {
headers: clientPackageHeaderIfPresent(),

View File

@@ -1,6 +1,6 @@
import { authenticatedRequestHeaders } from "@/next/http";
import { ensureLocalUser } from "@/next/local-user";
import { apiOrigin } from "@/next/origins";
import { apiURL } from "@/next/origins";
import { ensure } from "@/utils/ensure";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import type { KeyAttributes } from "@ente/shared/user/types";
@@ -62,7 +62,7 @@ type SessionValidity =
* subsequently.
*/
export const checkSessionValidity = async (): Promise<SessionValidity> => {
const url = `${apiOrigin()}/users/session-validity/v2`;
const url = await apiURL("/users/session-validity/v2");
const res = await fetch(url, {
headers: authenticatedRequestHeaders(),
});

View File

@@ -7,6 +7,7 @@
"@/utils": "*",
"@ente/shared": "*",
"formik": "^2.4",
"idb": "^8",
"zod": "^3"
},
"devDependencies": {}

View File

@@ -1,3 +1,4 @@
import { getKV, removeKV, setKV } from "@/next/kv";
import log from "@/next/log";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import {
@@ -14,7 +15,7 @@ import {
} from "@mui/material";
import { useFormik } from "formik";
import { t } from "i18next";
import React from "react";
import React, { useEffect, useState } from "react";
import { z } from "zod";
import { FocusVisibleButton } from "./FocusVisibleButton";
import { SlideTransition } from "./SlideTransition";
@@ -38,9 +39,61 @@ export const DevSettings: React.FC<DevSettingsProps> = ({ open, onClose }) => {
if (reason != "backdropClick") onClose();
};
return (
<Dialog
{...{ open, fullScreen }}
onClose={handleDialogClose}
TransitionComponent={SlideTransition}
maxWidth="xs"
>
<Contents {...{ onClose }} />
</Dialog>
);
};
type ContentsProps = Pick<DevSettingsProps, "onClose">;
const Contents: React.FC<ContentsProps> = (props) => {
// We need two nested components.
//
// - The initialAPIOrigin cannot be in our parent (the top level
// DevSettings) otherwise it gets preserved across dialog reopens
// instead of being read from storage on opening the dialog.
//
// - The initialAPIOrigin cannot be in our child (Form) because Formik
// doesn't have supported for async initial values.
const [initialAPIOrigin, setInitialAPIOrigin] = useState<
string | undefined
>();
useEffect(
() =>
void getKV("apiOrigin").then((o) =>
setInitialAPIOrigin(
// TODO: Migration of apiOrigin from local storage to indexed DB
// Remove me after a bit (27 June 2024).
o ?? localStorage.getItem("apiOrigin") ?? "",
),
),
[],
);
// Even though this is async, this should be instantanous, we're just
// reading the value from the local IndexedDB.
if (initialAPIOrigin === undefined) return <></>;
return <Form {...{ initialAPIOrigin }} {...props} />;
};
type FormProps = ContentsProps & {
/** The initial value of API origin to prefill in the text input field. */
initialAPIOrigin: string;
};
const Form: React.FC<FormProps> = ({ initialAPIOrigin, onClose }) => {
const form = useFormik({
initialValues: {
apiOrigin: localStorage.getItem("apiOrigin") ?? "",
apiOrigin: initialAPIOrigin,
},
validate: ({ apiOrigin }) => {
try {
@@ -77,79 +130,72 @@ export const DevSettings: React.FC<DevSettingsProps> = ({ open, onClose }) => {
!!form.errors.apiOrigin;
return (
<Dialog
{...{ open, fullScreen }}
onClose={handleDialogClose}
TransitionComponent={SlideTransition}
maxWidth="xs"
>
<form onSubmit={form.handleSubmit}>
<DialogTitle>{t("developer_settings")}</DialogTitle>
<DialogContent
sx={{
"&&": {
paddingBlock: "8px",
},
}}
>
<TextField
fullWidth
autoFocus
id="apiOrigin"
name="apiOrigin"
label={t("server_endpoint")}
placeholder="http://localhost:8080"
value={form.values.apiOrigin}
onChange={form.handleChange}
onBlur={form.handleBlur}
error={hasError}
helperText={
hasError
? form.errors.apiOrigin
: " " /* always show an empty string to prevent a layout shift */
}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Link
href="https://help.ente.io/self-hosting/guides/custom-server/"
target="_blank"
rel="noopener"
<form onSubmit={form.handleSubmit}>
<DialogTitle>{t("developer_settings")}</DialogTitle>
<DialogContent
sx={{
"&&": {
paddingBlock: "8px",
},
}}
>
<TextField
fullWidth
autoFocus
id="apiOrigin"
name="apiOrigin"
label={t("server_endpoint")}
placeholder="http://localhost:8080"
value={form.values.apiOrigin}
onChange={form.handleChange}
onBlur={form.handleBlur}
error={hasError}
helperText={
hasError
? form.errors.apiOrigin
: " " /* always show an empty string to prevent a layout shift */
}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Link
href="https://help.ente.io/self-hosting/guides/custom-server/"
target="_blank"
rel="noopener"
>
<IconButton
aria-label={t("more_information")}
color="secondary"
edge="end"
>
<IconButton
aria-label={t("more_information")}
color="secondary"
edge="end"
>
<InfoOutlinedIcon />
</IconButton>
</Link>
</InputAdornment>
),
}}
/>
</DialogContent>
<DialogActions>
<FocusVisibleButton
type="submit"
color="accent"
fullWidth
disabled={form.isSubmitting}
disableRipple
>
{t("save")}
</FocusVisibleButton>
<FocusVisibleButton
onClick={onClose}
color="secondary"
fullWidth
disableRipple
>
{t("CANCEL")}
</FocusVisibleButton>
</DialogActions>
</form>
</Dialog>
<InfoOutlinedIcon />
</IconButton>
</Link>
</InputAdornment>
),
}}
/>
</DialogContent>
<DialogActions>
<FocusVisibleButton
type="submit"
color="accent"
fullWidth
disabled={form.isSubmitting}
disableRipple
>
{t("save")}
</FocusVisibleButton>
<FocusVisibleButton
onClick={onClose}
color="secondary"
fullWidth
disableRipple
>
{t("CANCEL")}
</FocusVisibleButton>
</DialogActions>
</form>
);
};
@@ -167,6 +213,9 @@ export const DevSettings: React.FC<DevSettingsProps> = ({ open, onClose }) => {
*/
const updateAPIOrigin = async (origin: string) => {
if (!origin) {
await removeKV("apiOrigin");
// TODO: Migration of apiOrigin from local storage to indexed DB
// Remove me after a bit (27 June 2024).
localStorage.removeItem("apiOrigin");
return;
}
@@ -181,7 +230,7 @@ const updateAPIOrigin = async (origin: string) => {
throw new Error("Invalid response");
}
localStorage.setItem("apiOrigin", origin);
await setKV("apiOrigin", origin);
};
const PingResponse = z.object({

View File

@@ -1,5 +1,5 @@
import { authenticatedRequestHeaders } from "@/next/http";
import { apiOrigin } from "@/next/origins";
import { apiURL } from "@/next/origins";
import { nullToUndefined } from "@/utils/transform";
// import ComlinkCryptoWorker from "@ente/shared/crypto";
import { z } from "zod";
@@ -166,7 +166,7 @@ const getEmbeddingsDiff = async (
sinceTime: `${sinceTime}`,
limit: `${diffLimit}`,
});
const url = `${apiOrigin()}/embeddings/diff`;
const url = await apiURL("/embeddings/diff");
const res = await fetch(`${url}?${params.toString()}`, {
headers: authenticatedRequestHeaders(),
});

View File

@@ -2,7 +2,7 @@ import { isDevBuild } from "@/next/env";
import { authenticatedRequestHeaders } from "@/next/http";
import { localUser } from "@/next/local-user";
import log from "@/next/log";
import { apiOrigin } from "@/next/origins";
import { apiURL } from "@/next/origins";
import { nullToUndefined } from "@/utils/transform";
import { z } from "zod";
@@ -65,7 +65,7 @@ const fetchAndSaveFeatureFlags = () =>
.then(saveFlagJSONString);
const fetchFeatureFlags = async () => {
const url = `${apiOrigin()}/remote-store/feature-flags`;
const url = await apiURL("/remote-store/feature-flags");
const res = await fetch(url, {
headers: authenticatedRequestHeaders(),
});

View File

@@ -15,7 +15,7 @@ export const isDevBuild = process.env.NODE_ENV === "development";
* `true` if we're running in the default global context (aka the main thread)
* of a web browser.
*
* In particular, this is `false` when we're running in a Web Worker,
* In particular, this is `false` when we're running in a web worker,
* irrespecitve of whether the worker is running in a Node.js context or a web
* browser context.
*

125
web/packages/next/kv.ts Normal file
View File

@@ -0,0 +1,125 @@
import { deleteDB, openDB, type DBSchema } from "idb";
import log from "./log";
/**
* Key value store schema.
*
* The use IndexedDB to store arbitrary key-value pairs. The functional
* motivation is to allow these to also be accessed from web workers (local
* storage is limited to the main thread).
*
* The "kv" database consists of one object store, "kv". Each entry is a string.
* The key is also a string.
*/
interface KVDBSchema extends DBSchema {
kv: {
key: string;
value: string;
};
}
/**
* A lazily-created, cached promise for KV DB.
*
* [Note: Caching IDB instances in separate execution contexts]
*
* We open the database once (on access), and thereafter save and reuse this
* promise each time something wants to connect to it.
*
* This promise can subsequently get cleared if we need to relinquish our
* connection (e.g. if another client wants to open the face DB with a newer
* version of the schema).
*
* It can also get cleared on logout. In all such cases, it'll automatically get
* recreated on next access.
*
* Note that this is module specific state, so each execution context (main
* thread, web worker) that calls the functions in this module will its own
* promise to the database. To ensure that all connections get torn down
* correctly, we need to perform the following logout sequence:
*
* 1. Terminate all the workers which might have one of the instances in
* memory. This closes their connections.
*
* 2. Delete the database on the main thread.
*/
let _kvDB: ReturnType<typeof openKVDB> | undefined;
const openKVDB = async () => {
const db = await openDB<KVDBSchema>("kv", 1, {
upgrade(db) {
db.createObjectStore("kv");
},
blocking() {
log.info(
"Another client is attempting to open a new version of KV DB",
);
db.close();
_kvDB = undefined;
},
blocked() {
log.warn(
"Waiting for an existing client to close their connection so that we can update the KV DB version",
);
},
terminated() {
log.warn("Our connection to KV DB was unexpectedly terminated");
_kvDB = undefined;
},
});
return db;
};
/**
* @returns a lazily created, cached connection to the KV DB.
*/
const kvDB = () => (_kvDB ??= openKVDB());
/**
* Clear all key values stored in the KV db.
*
* This is meant to be called during logout in the main thread.
*/
export const clearKVDB = async () => {
try {
if (_kvDB) (await _kvDB).close();
} catch (e) {
log.warn("Ignoring error when trying to close KV DB", e);
}
_kvDB = undefined;
return deleteDB("kv", {
blocked() {
log.warn(
"Waiting for an existing client to close their connection so that we can delete the KV DB",
);
},
});
};
/**
* Return the value stored corresponding to {@link key}, or `undefined` if there
* is no such entry.
*/
export const getKV = async (key: string) => {
const db = await kvDB();
return await db.get("kv", key);
};
/**
* Save the given {@link value} corresponding to {@link key}, overwriting any
* existing value.
*/
export const setKV = async (key: string, value: string) => {
const db = await kvDB();
await db.put("kv", value, key);
};
/**
* Remove the entry corresponding to {@link key} (if any).
*/
export const removeKV = async (key: string) => {
const db = await kvDB();
await db.delete("kv", key);
};

View File

@@ -1,4 +1,4 @@
import { nullToUndefined } from "@/utils/transform";
import { getKV, setKV } from "@/next/kv";
/**
* Return the origin (scheme, host, port triple) that should be used for making
@@ -7,7 +7,21 @@ 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";
/**
* A convenience function to construct an endpoint in a one-liner.
*
* This avoids us having to create a temporary variable or otherwise complicate
* the call sites since async functions cannot be used inside template literals.
*
* @param path The URL path usually, but can be anything that needs to be
* suffixed to the origin. It must begin with a "/".
*
* @returns path prefixed by {@link apiOrigin}.
*/
export const apiURL = async (path: string) => (await apiOrigin()) + path;
/**
* Return the overridden API origin, if one is defined by either (in priority
@@ -20,10 +34,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 getKV("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 = legacyOrigin;
if (origin) await setKV("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 +56,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 +69,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.

View File

@@ -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<React.PropsWithChildren> = ({
children,
}) => {
const host = customAPIHost();
const [host, setHost] = useState<string | undefined>();
useEffect(() => void customAPIHost().then(setHost), []);
return (
<FormPaperFooter>

View File

@@ -1,5 +1,5 @@
import log from "@/next/log";
import { apiOrigin } from "@/next/origins";
import { apiURL } from "@/next/origins";
import { ApiError } from "../error";
import { getToken } from "../storage/localStorage/helpers";
import HTTPService from "./HTTPService";
@@ -11,7 +11,7 @@ class CastGateway {
let resp;
try {
resp = await HTTPService.get(
`${apiOrigin()}/cast/cast-data/${code}`,
await apiURL(`/cast/cast-data/${code}`),
);
} catch (e) {
log.error("failed to getCastData", e);
@@ -24,7 +24,7 @@ class CastGateway {
try {
const token = getToken();
await HTTPService.delete(
apiOrigin() + "/cast/revoke-all-tokens/",
await apiURL("/cast/revoke-all-tokens/"),
undefined,
undefined,
{
@@ -42,7 +42,7 @@ class CastGateway {
try {
const token = getToken();
resp = await HTTPService.get(
`${apiOrigin()}/cast/device-info/${code}`,
await apiURL(`/cast/device-info/${code}`),
undefined,
{
"X-Auth-Token": token,
@@ -60,7 +60,7 @@ class CastGateway {
public async registerDevice(publicKey: string): Promise<string> {
const resp = await HTTPService.post(
apiOrigin() + "/cast/device-info/",
await apiURL("/cast/device-info/"),
{
publicKey: publicKey,
},
@@ -76,7 +76,7 @@ class CastGateway {
) {
const token = getToken();
await HTTPService.post(
apiOrigin() + "/cast/cast-data/",
await apiURL("/cast/cast-data/"),
{
deviceCode: `${code}`,
encPayload: castPayload,