[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:
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 />
|
||||
) : (
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"@/utils": "*",
|
||||
"@ente/shared": "*",
|
||||
"formik": "^2.4",
|
||||
"idb": "^8",
|
||||
"zod": "^3"
|
||||
},
|
||||
"devDependencies": {}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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
125
web/packages/next/kv.ts
Normal 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);
|
||||
};
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user