[web/desktop] Allow self-hosters to set custom endpoints (#2271)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { isDevBuild } from "@/next/env";
|
||||
import { apiOrigin } from "@/next/origins";
|
||||
import { clientPackageName } from "@/next/types/app";
|
||||
import { TwoFactorAuthorizationResponse } from "@/next/types/credentials";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
toB64URLSafeNoPadding,
|
||||
toB64URLSafeNoPaddingString,
|
||||
} from "@ente/shared/crypto/internal/libsodium";
|
||||
import { apiOrigin } from "@ente/shared/network/api";
|
||||
import { z } from "zod";
|
||||
|
||||
/** Return true if the user's browser supports WebAuthn (Passkeys). */
|
||||
@@ -354,14 +354,6 @@ export const isWhitelistedRedirect = (redirectURL: URL) =>
|
||||
? _isWhitelistedRedirect(redirectURL)
|
||||
: true;
|
||||
|
||||
const _isWhitelistedRedirect = (redirectURL: URL) =>
|
||||
(isDevBuild && redirectURL.hostname.endsWith("localhost")) ||
|
||||
redirectURL.host.endsWith(".ente.io") ||
|
||||
redirectURL.host.endsWith(".ente.sh") ||
|
||||
redirectURL.protocol == "ente:" ||
|
||||
redirectURL.protocol == "enteauth:" ||
|
||||
redirectURL.protocol == "ente-cli:";
|
||||
|
||||
export const shouldRestrictToWhitelistedRedirect = () => {
|
||||
// host includes port, hostname is sans port
|
||||
const hostname = new URL(window.location.origin).hostname;
|
||||
@@ -372,6 +364,14 @@ export const shouldRestrictToWhitelistedRedirect = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const _isWhitelistedRedirect = (redirectURL: URL) =>
|
||||
(isDevBuild && redirectURL.hostname.endsWith("localhost")) ||
|
||||
redirectURL.host.endsWith(".ente.io") ||
|
||||
redirectURL.host.endsWith(".ente.sh") ||
|
||||
redirectURL.protocol == "ente:" ||
|
||||
redirectURL.protocol == "enteauth:" ||
|
||||
redirectURL.protocol == "ente-cli:";
|
||||
|
||||
export interface BeginPasskeyAuthenticationResponse {
|
||||
/**
|
||||
* An identifier for this authentication ceremony / session.
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import log from "@/next/log";
|
||||
import { apiOrigin } from "@/next/origins";
|
||||
import ComlinkCryptoWorker from "@ente/shared/crypto";
|
||||
import { ApiError, CustomError } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getEndpoint } from "@ente/shared/network/api";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
import { getActualKey } from "@ente/shared/user";
|
||||
import { HttpStatusCode } from "axios";
|
||||
import { codeFromURIString, type Code } from "services/code";
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
export const getAuthCodes = async (): Promise<Code[]> => {
|
||||
const masterKey = await getActualKey();
|
||||
try {
|
||||
@@ -83,7 +81,7 @@ interface AuthKey {
|
||||
export const getAuthKey = async (): Promise<AuthKey> => {
|
||||
try {
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/authenticator/key`,
|
||||
`${apiOrigin()}/authenticator/key`,
|
||||
{},
|
||||
{
|
||||
"X-Auth-Token": getToken(),
|
||||
@@ -110,7 +108,7 @@ export const getDiff = async (
|
||||
): Promise<AuthEntity[]> => {
|
||||
try {
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/authenticator/entity/diff`,
|
||||
`${apiOrigin()}/authenticator/entity/diff`,
|
||||
{
|
||||
sinceTime,
|
||||
limit,
|
||||
|
||||
@@ -11,13 +11,13 @@ import { heicToJPEG } from "@/media/heic-convert";
|
||||
import { decodeLivePhoto } from "@/media/live-photo";
|
||||
import { nameAndExtension } from "@/next/file";
|
||||
import log from "@/next/log";
|
||||
import { apiOrigin, customAPIOrigin } from "@/next/origins";
|
||||
import { shuffled } from "@/utils/array";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import { wait } from "@/utils/promise";
|
||||
import ComlinkCryptoWorker from "@ente/shared/crypto";
|
||||
import { ApiError } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { apiOrigin, customAPIOrigin } from "@ente/shared/network/api";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import type { CastData } from "services/cast-data";
|
||||
import { detectMediaMIMEType } from "services/detect-type";
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"exifr": "^7.1.3",
|
||||
"fast-srp-hap": "^2.0.4",
|
||||
"ffmpeg-wasm": "file:./thirdparty/ffmpeg-wasm",
|
||||
"formik": "^2.1.5",
|
||||
"hdbscan": "0.0.1-alpha.5",
|
||||
"idb": "^8",
|
||||
"leaflet": "^1.9.4",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import log from "@/next/log";
|
||||
import { savedLogs } from "@/next/log-web";
|
||||
import { customAPIHost } from "@/next/origins";
|
||||
import { openAccountsManagePasskeysPage } from "@ente/accounts/services/passkey";
|
||||
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
|
||||
import { EnteLogo } from "@ente/shared/components/EnteLogo";
|
||||
@@ -685,6 +686,8 @@ const DebugSection: React.FC = () => {
|
||||
electron?.appVersion().then((v) => setAppVersion(v));
|
||||
});
|
||||
|
||||
const host = customAPIHost();
|
||||
|
||||
const confirmLogDownload = () =>
|
||||
appContext.setDialogMessage({
|
||||
title: t("DOWNLOAD_LOGS"),
|
||||
@@ -707,21 +710,6 @@ const DebugSection: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<EnteMenuItem
|
||||
onClick={confirmLogDownload}
|
||||
variant="mini"
|
||||
label={t("DOWNLOAD_UPLOAD_LOGS")}
|
||||
/>
|
||||
{appVersion && (
|
||||
<Typography
|
||||
py={"14px"}
|
||||
px={"16px"}
|
||||
color="text.muted"
|
||||
variant="mini"
|
||||
>
|
||||
{appVersion}
|
||||
</Typography>
|
||||
)}
|
||||
{isInternalUserViaEmailCheck() && (
|
||||
<EnteMenuItem
|
||||
variant="secondary"
|
||||
@@ -729,6 +717,17 @@ const DebugSection: React.FC = () => {
|
||||
label={"Test Upload"}
|
||||
/>
|
||||
)}
|
||||
<EnteMenuItem
|
||||
onClick={confirmLogDownload}
|
||||
variant="mini"
|
||||
label={t("DOWNLOAD_UPLOAD_LOGS")}
|
||||
/>
|
||||
<Stack py={"14px"} px={"16px"} gap={"24px"} color="text.muted">
|
||||
{appVersion && (
|
||||
<Typography variant="mini">{appVersion}</Typography>
|
||||
)}
|
||||
{host && <Typography variant="mini">{host}</Typography>}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { getPaymentsURL } from "@ente/shared/network/api";
|
||||
|
||||
export const getDesktopRedirectURL = () =>
|
||||
`${getPaymentsURL()}/desktop-redirect`;
|
||||
@@ -1,19 +1,21 @@
|
||||
import { DevSettings } from "@/new/photos/components/DevSettings";
|
||||
import log from "@/next/log";
|
||||
import { albumsAppOrigin, customAPIHost } from "@/next/origins";
|
||||
import { Login } from "@ente/accounts/components/Login";
|
||||
import { SignUp } from "@ente/accounts/components/SignUp";
|
||||
import { EnteLogo } from "@ente/shared/components/EnteLogo";
|
||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
|
||||
import { saveKeyInSessionStore } from "@ente/shared/crypto/helpers";
|
||||
import { getAlbumsURL } from "@ente/shared/network/api";
|
||||
import localForage from "@ente/shared/storage/localForage";
|
||||
import { getData, LS_KEYS } from "@ente/shared/storage/localStorage";
|
||||
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
import { getKey, SESSION_KEYS } from "@ente/shared/storage/sessionStorage";
|
||||
import { SESSION_KEYS, getKey } from "@ente/shared/storage/sessionStorage";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
styled,
|
||||
Typography,
|
||||
styled,
|
||||
type TypographyProps,
|
||||
} from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
@@ -26,14 +28,20 @@ import { useAppContext } from "./_app";
|
||||
|
||||
export default function LandingPage() {
|
||||
const { appName, showNavBar, setDialogMessage } = useAppContext();
|
||||
const router = useRouter();
|
||||
|
||||
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 router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
showNavBar(false);
|
||||
const currentURL = new URL(window.location.href);
|
||||
const albumsURL = new URL(getAlbumsURL());
|
||||
const albumsURL = new URL(albumsAppOrigin());
|
||||
currentURL.pathname = router.pathname;
|
||||
if (
|
||||
currentURL.host === albumsURL.host &&
|
||||
@@ -45,6 +53,8 @@ export default function LandingPage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleMaybeChangeHost = () => setHost(customAPIHost());
|
||||
|
||||
const handleAlbumsRedirect = async (currentURL: URL) => {
|
||||
const end = currentURL.hash.lastIndexOf("&");
|
||||
const hash = currentURL.hash.slice(1, end !== -1 ? end : undefined);
|
||||
@@ -107,7 +117,7 @@ export default function LandingPage() {
|
||||
const redirectToLoginPage = () => router.push(PAGES.LOGIN);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TappableContainer onMaybeChangeHost={handleMaybeChangeHost}>
|
||||
{loading ? (
|
||||
<EnteSpinner />
|
||||
) : (
|
||||
@@ -129,23 +139,81 @@ export default function LandingPage() {
|
||||
<Button size="large" onClick={redirectToLoginPage}>
|
||||
{t("EXISTING_USER")}
|
||||
</Button>
|
||||
<MobileBoxFooter {...{ host }} />
|
||||
</MobileBox>
|
||||
<DesktopBox>
|
||||
<SideBox>
|
||||
{showLogin ? (
|
||||
<Login {...{ signUp, appName }} />
|
||||
<Login {...{ signUp, appName, host }} />
|
||||
) : (
|
||||
<SignUp {...{ router, appName, login }} />
|
||||
<SignUp {...{ router, appName, login, host }} />
|
||||
)}
|
||||
</SideBox>
|
||||
</DesktopBox>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
</TappableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled("div")`
|
||||
interface TappableContainerProps {
|
||||
/**
|
||||
* Called when the user closes the dialog to set a custom server.
|
||||
*
|
||||
* This is our chance to re-read the value of the custom API origin from
|
||||
* local storage since the user might've changed it.
|
||||
*/
|
||||
onMaybeChangeHost: () => void;
|
||||
}
|
||||
|
||||
const TappableContainer: React.FC<
|
||||
React.PropsWithChildren<TappableContainerProps>
|
||||
> = ({ onMaybeChangeHost, children }) => {
|
||||
// [Note: Configuring custom server]
|
||||
//
|
||||
// Allow the user to tap 7 times anywhere on the onboarding screen to bring
|
||||
// up a page where they can configure the endpoint that the app should
|
||||
// connect to.
|
||||
//
|
||||
// See: https://help.ente.io/self-hosting/guides/custom-server/
|
||||
const [tapCount, setTapCount] = useState(0);
|
||||
const [showDevSettings, setShowDevSettings] = useState(false);
|
||||
|
||||
const handleClick: React.MouseEventHandler = (event) => {
|
||||
// Don't allow this when running on (e.g.) web.ente.io.
|
||||
if (!shouldAllowChangingAPIOrigin()) return;
|
||||
|
||||
// Ignore clicks on buttons when counting up towards 7.
|
||||
if (event.target instanceof HTMLButtonElement) return;
|
||||
|
||||
// Ignore clicks when the dialog is already open.
|
||||
if (showDevSettings) return;
|
||||
|
||||
// Otherwise increase the tap count,
|
||||
setTapCount(tapCount + 1);
|
||||
// And show the dev settings dialog when it reaches 7.
|
||||
if (tapCount + 1 == 7) {
|
||||
setTapCount(0);
|
||||
setShowDevSettings(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setShowDevSettings(false);
|
||||
onMaybeChangeHost();
|
||||
};
|
||||
|
||||
return (
|
||||
<TappableContainer_ onClick={handleClick}>
|
||||
<>
|
||||
<DevSettings open={showDevSettings} onClose={handleClose} />
|
||||
{children}
|
||||
</>
|
||||
</TappableContainer_>
|
||||
);
|
||||
};
|
||||
|
||||
const TappableContainer_ = styled("div")`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
@@ -157,6 +225,15 @@ const Container = styled("div")`
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Disable the ability to set the custom server when we're running on our own
|
||||
* production deployment.
|
||||
*/
|
||||
const shouldAllowChangingAPIOrigin = () => {
|
||||
const hostname = new URL(window.location.origin).hostname;
|
||||
return !(hostname.endsWith(".ente.io") || hostname.endsWith(".ente.sh"));
|
||||
};
|
||||
|
||||
const SlideContainer = styled("div")`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -174,6 +251,35 @@ const Logo_ = styled("div")`
|
||||
margin-block-end: 64px;
|
||||
`;
|
||||
|
||||
const MobileBox = styled("div")`
|
||||
display: none;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
max-width: 375px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface MobileBoxFooterProps {
|
||||
host: string | undefined;
|
||||
}
|
||||
|
||||
const MobileBoxFooter: React.FC<MobileBoxFooterProps> = ({ host }) => {
|
||||
return (
|
||||
<Box pt={4} textAlign="center">
|
||||
{host && (
|
||||
<Typography variant="mini" color="text.faint">
|
||||
{host}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const DesktopBox = styled("div")`
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
@@ -188,19 +294,6 @@ const DesktopBox = styled("div")`
|
||||
}
|
||||
`;
|
||||
|
||||
const MobileBox = styled("div")`
|
||||
display: none;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
max-width: 375px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const SideBox = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import log from "@/next/log";
|
||||
import { apiOrigin, paymentsAppOrigin } from "@/next/origins";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getEndpoint, getPaymentsURL } from "@ente/shared/network/api";
|
||||
import {
|
||||
LS_KEYS,
|
||||
removeData,
|
||||
setData,
|
||||
} from "@ente/shared/storage/localStorage";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
import { getDesktopRedirectURL } from "constants/billing";
|
||||
import isElectron from "is-electron";
|
||||
import { Plan, Subscription } from "types/billing";
|
||||
import { getPaymentToken } from "./userService";
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
enum PaymentActionType {
|
||||
Buy = "buy",
|
||||
Update = "update",
|
||||
@@ -36,11 +33,11 @@ class billingService {
|
||||
let response;
|
||||
if (!token) {
|
||||
response = await HTTPService.get(
|
||||
`${ENDPOINT}/billing/plans/v2`,
|
||||
`${apiOrigin()}/billing/plans/v2`,
|
||||
);
|
||||
} else {
|
||||
response = await HTTPService.get(
|
||||
`${ENDPOINT}/billing/user-plans`,
|
||||
`${apiOrigin()}/billing/user-plans`,
|
||||
null,
|
||||
{
|
||||
"X-Auth-Token": getToken(),
|
||||
@@ -56,7 +53,7 @@ class billingService {
|
||||
public async syncSubscription() {
|
||||
try {
|
||||
const response = await HTTPService.get(
|
||||
`${ENDPOINT}/billing/subscription`,
|
||||
`${apiOrigin()}/billing/subscription`,
|
||||
null,
|
||||
{
|
||||
"X-Auth-Token": getToken(),
|
||||
@@ -100,7 +97,7 @@ class billingService {
|
||||
public async cancelSubscription() {
|
||||
try {
|
||||
const response = await HTTPService.post(
|
||||
`${ENDPOINT}/billing/stripe/cancel-subscription`,
|
||||
`${apiOrigin()}/billing/stripe/cancel-subscription`,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
@@ -118,7 +115,7 @@ class billingService {
|
||||
public async activateSubscription() {
|
||||
try {
|
||||
const response = await HTTPService.post(
|
||||
`${ENDPOINT}/billing/stripe/activate-subscription`,
|
||||
`${apiOrigin()}/billing/stripe/activate-subscription`,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
@@ -142,7 +139,7 @@ class billingService {
|
||||
return;
|
||||
}
|
||||
const response = await HTTPService.post(
|
||||
`${ENDPOINT}/billing/verify-subscription`,
|
||||
`${apiOrigin()}/billing/verify-subscription`,
|
||||
{
|
||||
paymentProvider: "stripe",
|
||||
productID: null,
|
||||
@@ -167,9 +164,14 @@ class billingService {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await HTTPService.delete(`${ENDPOINT}/family/leave`, null, null, {
|
||||
"X-Auth-Token": getToken(),
|
||||
});
|
||||
await HTTPService.delete(
|
||||
`${apiOrigin()}/family/leave`,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"X-Auth-Token": getToken(),
|
||||
},
|
||||
);
|
||||
removeData(LS_KEYS.FAMILY_DATA);
|
||||
} catch (e) {
|
||||
log.error("/family/leave failed", e);
|
||||
@@ -184,7 +186,7 @@ class billingService {
|
||||
) {
|
||||
try {
|
||||
const redirectURL = this.getRedirectURL();
|
||||
window.location.href = `${getPaymentsURL()}?productID=${productID}&paymentToken=${paymentToken}&action=${action}&redirectURL=${redirectURL}`;
|
||||
window.location.href = `${paymentsAppOrigin()}?productID=${productID}&paymentToken=${paymentToken}&action=${action}&redirectURL=${redirectURL}`;
|
||||
} catch (e) {
|
||||
log.error("unable to get payments url", e);
|
||||
throw e;
|
||||
@@ -195,7 +197,7 @@ class billingService {
|
||||
try {
|
||||
const redirectURL = this.getRedirectURL();
|
||||
const response = await HTTPService.get(
|
||||
`${ENDPOINT}/billing/stripe/customer-portal`,
|
||||
`${apiOrigin()}/billing/stripe/customer-portal`,
|
||||
{ redirectURL },
|
||||
{
|
||||
"X-Auth-Token": getToken(),
|
||||
@@ -210,7 +212,7 @@ class billingService {
|
||||
|
||||
public getRedirectURL() {
|
||||
if (isElectron()) {
|
||||
return getDesktopRedirectURL();
|
||||
return `${paymentsAppOrigin()}/desktop-redirect`;
|
||||
} else {
|
||||
return `${window.location.origin}/gallery`;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { getEndpoint } from "@ente/shared/network/api";
|
||||
import localForage from "@ente/shared/storage/localForage";
|
||||
import { getData, LS_KEYS } from "@ente/shared/storage/localStorage";
|
||||
|
||||
import log from "@/next/log";
|
||||
import { apiOrigin } from "@/next/origins";
|
||||
import ComlinkCryptoWorker from "@ente/shared/crypto";
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import localForage from "@ente/shared/storage/localForage";
|
||||
import { getData, LS_KEYS } from "@ente/shared/storage/localStorage";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
import { getActualKey } from "@ente/shared/user";
|
||||
import type { User } from "@ente/shared/user/types";
|
||||
@@ -77,7 +76,6 @@ import {
|
||||
import { getLocalFiles } from "./fileService";
|
||||
import { getPublicKey } from "./userService";
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
const COLLECTION_TABLE = "collections";
|
||||
const COLLECTION_UPDATION_TIME = "collection-updation-time";
|
||||
const HIDDEN_COLLECTION_IDS = "hidden-collection-ids";
|
||||
@@ -183,7 +181,7 @@ const getCollections = async (
|
||||
): Promise<Collection[]> => {
|
||||
try {
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/collections/v2`,
|
||||
`${apiOrigin()}/collections/v2`,
|
||||
{
|
||||
sinceTime,
|
||||
},
|
||||
@@ -330,7 +328,7 @@ export const getCollection = async (
|
||||
return;
|
||||
}
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/collections/${collectionID}`,
|
||||
`${apiOrigin()}/collections/${collectionID}`,
|
||||
null,
|
||||
{ "X-Auth-Token": token },
|
||||
);
|
||||
@@ -474,7 +472,7 @@ const postCollection = async (
|
||||
): Promise<EncryptedCollection> => {
|
||||
try {
|
||||
const response = await HTTPService.post(
|
||||
`${ENDPOINT}/collections`,
|
||||
`${apiOrigin()}/collections`,
|
||||
collectionData,
|
||||
null,
|
||||
{ "X-Auth-Token": token },
|
||||
@@ -529,7 +527,7 @@ export const addToCollection = async (
|
||||
files: fileKeysEncryptedWithNewCollection,
|
||||
};
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/collections/add-files`,
|
||||
`${apiOrigin()}/collections/add-files`,
|
||||
requestBody,
|
||||
null,
|
||||
{
|
||||
@@ -559,7 +557,7 @@ export const restoreToCollection = async (
|
||||
files: fileKeysEncryptedWithNewCollection,
|
||||
};
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/collections/restore-files`,
|
||||
`${apiOrigin()}/collections/restore-files`,
|
||||
requestBody,
|
||||
null,
|
||||
{
|
||||
@@ -590,7 +588,7 @@ export const moveToCollection = async (
|
||||
files: fileKeysEncryptedWithNewCollection,
|
||||
};
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/collections/move-files`,
|
||||
`${apiOrigin()}/collections/move-files`,
|
||||
requestBody,
|
||||
null,
|
||||
{
|
||||
@@ -736,7 +734,7 @@ export const removeNonUserFiles = async (
|
||||
};
|
||||
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/collections/v3/remove-files`,
|
||||
`${apiOrigin()}/collections/v3/remove-files`,
|
||||
request,
|
||||
null,
|
||||
{ "X-Auth-Token": token },
|
||||
@@ -763,7 +761,7 @@ export const deleteCollection = async (
|
||||
const token = getToken();
|
||||
|
||||
await HTTPService.delete(
|
||||
`${ENDPOINT}/collections/v3/${collectionID}`,
|
||||
`${apiOrigin()}/collections/v3/${collectionID}`,
|
||||
null,
|
||||
{ collectionID, keepFiles },
|
||||
{ "X-Auth-Token": token },
|
||||
@@ -779,7 +777,7 @@ export const leaveSharedAlbum = async (collectionID: number) => {
|
||||
const token = getToken();
|
||||
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/collections/leave/${collectionID}`,
|
||||
`${apiOrigin()}/collections/leave/${collectionID}`,
|
||||
null,
|
||||
null,
|
||||
{ "X-Auth-Token": token },
|
||||
@@ -817,7 +815,7 @@ export const updateCollectionMagicMetadata = async (
|
||||
};
|
||||
|
||||
await HTTPService.put(
|
||||
`${ENDPOINT}/collections/magic-metadata`,
|
||||
`${apiOrigin()}/collections/magic-metadata`,
|
||||
reqBody,
|
||||
null,
|
||||
{
|
||||
@@ -861,7 +859,7 @@ export const updateSharedCollectionMagicMetadata = async (
|
||||
};
|
||||
|
||||
await HTTPService.put(
|
||||
`${ENDPOINT}/collections/sharee-magic-metadata`,
|
||||
`${apiOrigin()}/collections/sharee-magic-metadata`,
|
||||
reqBody,
|
||||
null,
|
||||
{
|
||||
@@ -905,7 +903,7 @@ export const updatePublicCollectionMagicMetadata = async (
|
||||
};
|
||||
|
||||
await HTTPService.put(
|
||||
`${ENDPOINT}/collections/public-magic-metadata`,
|
||||
`${apiOrigin()}/collections/public-magic-metadata`,
|
||||
reqBody,
|
||||
null,
|
||||
{
|
||||
@@ -940,7 +938,7 @@ export const renameCollection = async (
|
||||
nameDecryptionNonce,
|
||||
};
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/collections/rename`,
|
||||
`${apiOrigin()}/collections/rename`,
|
||||
collectionRenameRequest,
|
||||
null,
|
||||
{
|
||||
@@ -969,7 +967,7 @@ export const shareCollection = async (
|
||||
encryptedKey,
|
||||
};
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/collections/share`,
|
||||
`${apiOrigin()}/collections/share`,
|
||||
shareCollectionRequest,
|
||||
null,
|
||||
{
|
||||
@@ -993,7 +991,7 @@ export const unshareCollection = async (
|
||||
email: withUserEmail,
|
||||
};
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/collections/unshare`,
|
||||
`${apiOrigin()}/collections/unshare`,
|
||||
shareCollectionRequest,
|
||||
null,
|
||||
{
|
||||
@@ -1015,7 +1013,7 @@ export const createShareableURL = async (collection: Collection) => {
|
||||
collectionID: collection.id,
|
||||
};
|
||||
const resp = await HTTPService.post(
|
||||
`${ENDPOINT}/collections/share-url`,
|
||||
`${apiOrigin()}/collections/share-url`,
|
||||
createPublicAccessTokenRequest,
|
||||
null,
|
||||
{
|
||||
@@ -1036,7 +1034,7 @@ export const deleteShareableURL = async (collection: Collection) => {
|
||||
return null;
|
||||
}
|
||||
await HTTPService.delete(
|
||||
`${ENDPOINT}/collections/share-url/${collection.id}`,
|
||||
`${apiOrigin()}/collections/share-url/${collection.id}`,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
@@ -1058,7 +1056,7 @@ export const updateShareableURL = async (
|
||||
return null;
|
||||
}
|
||||
const res = await HTTPService.put(
|
||||
`${ENDPOINT}/collections/share-url`,
|
||||
`${apiOrigin()}/collections/share-url`,
|
||||
request,
|
||||
null,
|
||||
{
|
||||
|
||||
@@ -2,13 +2,11 @@ import { hasFileHash } from "@/media/file";
|
||||
import { FILE_TYPE } from "@/media/file-type";
|
||||
import type { Metadata } from "@/media/types/file";
|
||||
import log from "@/next/log";
|
||||
import { apiOrigin } from "@/next/origins";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getEndpoint } from "@ente/shared/network/api";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
import { EnteFile } from "types/file";
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
interface DuplicatesResponse {
|
||||
duplicates: Array<{
|
||||
fileIDs: number[];
|
||||
@@ -148,7 +146,7 @@ function groupDupesByFileHashes(dupe: Duplicate) {
|
||||
async function fetchDuplicateFileIDs() {
|
||||
try {
|
||||
const response = await HTTPService.get(
|
||||
`${ENDPOINT}/files/duplicates`,
|
||||
`${apiOrigin()}/files/duplicates`,
|
||||
null,
|
||||
{
|
||||
"X-Auth-Token": getToken(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { customAPIOrigin } from "@/next/origins";
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { customAPIOrigin } from "@ente/shared/network/api";
|
||||
import { retryAsyncFunction } from "@ente/shared/utils";
|
||||
import { DownloadClient } from "services/download";
|
||||
import { EnteFile } from "types/file";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { customAPIOrigin } from "@/next/origins";
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { customAPIOrigin } from "@ente/shared/network/api";
|
||||
import { retryAsyncFunction } from "@ente/shared/utils";
|
||||
import { DownloadClient } from "services/download";
|
||||
import { EnteFile } from "types/file";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { inWorker } from "@/next/env";
|
||||
import log from "@/next/log";
|
||||
import { apiOrigin } from "@/next/origins";
|
||||
import { workerBridge } from "@/next/worker/worker-bridge";
|
||||
import ComlinkCryptoWorker from "@ente/shared/crypto";
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getEndpoint } from "@ente/shared/network/api";
|
||||
import localForage from "@ente/shared/storage/localForage";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
import type {
|
||||
@@ -285,7 +285,7 @@ export const getEmbeddingsDiff = async (
|
||||
return;
|
||||
}
|
||||
const response = await HTTPService.get(
|
||||
`${getEndpoint()}/embeddings/diff`,
|
||||
`${apiOrigin()}/embeddings/diff`,
|
||||
{
|
||||
sinceTime,
|
||||
limit: DIFF_LIMIT,
|
||||
@@ -314,7 +314,7 @@ export const putEmbedding = async (
|
||||
throw Error(CustomError.TOKEN_MISSING);
|
||||
}
|
||||
const resp = await HTTPService.put(
|
||||
`${getEndpoint()}/embeddings`,
|
||||
`${apiOrigin()}/embeddings`,
|
||||
putEmbeddingReq,
|
||||
null,
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import log from "@/next/log";
|
||||
import { apiOrigin } from "@/next/origins";
|
||||
import ComlinkCryptoWorker from "@ente/shared/crypto";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getEndpoint } from "@ente/shared/network/api";
|
||||
import localForage from "@ente/shared/storage/localForage";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
import { getActualKey } from "@ente/shared/user";
|
||||
@@ -15,8 +15,6 @@ import {
|
||||
} from "types/entity";
|
||||
import { getLatestVersionEntities } from "utils/entity";
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
const DIFF_LIMIT = 500;
|
||||
|
||||
const ENTITY_TABLES: Record<EntityType, string> = {
|
||||
@@ -60,7 +58,7 @@ const getEntityKey = async (type: EntityType) => {
|
||||
return;
|
||||
}
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/user-entity/key`,
|
||||
`${apiOrigin()}/user-entity/key`,
|
||||
{
|
||||
type,
|
||||
},
|
||||
@@ -175,7 +173,7 @@ const getEntityDiff = async (
|
||||
return;
|
||||
}
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/user-entity/entity/diff`,
|
||||
`${apiOrigin()}/user-entity/entity/diff`,
|
||||
{
|
||||
sinceTime: time,
|
||||
type,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import log from "@/next/log";
|
||||
import { apiOrigin } from "@/next/origins";
|
||||
import ComlinkCryptoWorker from "@ente/shared/crypto";
|
||||
import { Events, eventBus } from "@ente/shared/events";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getEndpoint } from "@ente/shared/network/api";
|
||||
import localForage from "@ente/shared/storage/localForage";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
import { REQUEST_BATCH_SIZE } from "constants/api";
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
setCollectionLastSyncTime,
|
||||
} from "./collectionService";
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
const FILES_TABLE = "files";
|
||||
const HIDDEN_FILES_TABLE = "hidden-files";
|
||||
|
||||
@@ -118,7 +117,7 @@ export const getFiles = async (
|
||||
break;
|
||||
}
|
||||
resp = await HTTPService.get(
|
||||
`${ENDPOINT}/collections/v2/diff`,
|
||||
`${apiOrigin()}/collections/v2/diff`,
|
||||
{
|
||||
collectionID: collection.id,
|
||||
sinceTime: time,
|
||||
@@ -187,7 +186,7 @@ export const trashFiles = async (filesToTrash: EnteFile[]) => {
|
||||
})),
|
||||
};
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/files/trash`,
|
||||
`${apiOrigin()}/files/trash`,
|
||||
trashRequest,
|
||||
null,
|
||||
{
|
||||
@@ -211,7 +210,7 @@ export const deleteFromTrash = async (filesToDelete: number[]) => {
|
||||
|
||||
for (const batch of batchedFilesToDelete) {
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/trash/delete`,
|
||||
`${apiOrigin()}/trash/delete`,
|
||||
{ fileIDs: batch },
|
||||
null,
|
||||
{
|
||||
@@ -253,9 +252,14 @@ export const updateFileMagicMetadata = async (
|
||||
},
|
||||
});
|
||||
}
|
||||
await HTTPService.put(`${ENDPOINT}/files/magic-metadata`, reqBody, null, {
|
||||
"X-Auth-Token": token,
|
||||
});
|
||||
await HTTPService.put(
|
||||
`${apiOrigin()}/files/magic-metadata`,
|
||||
reqBody,
|
||||
null,
|
||||
{
|
||||
"X-Auth-Token": token,
|
||||
},
|
||||
);
|
||||
return fileWithUpdatedMagicMetadataList.map(
|
||||
({ file, updatedMagicMetadata }): EnteFile => ({
|
||||
...file,
|
||||
@@ -296,7 +300,7 @@ export const updateFilePublicMagicMetadata = async (
|
||||
});
|
||||
}
|
||||
await HTTPService.put(
|
||||
`${ENDPOINT}/files/public-magic-metadata`,
|
||||
`${apiOrigin()}/files/public-magic-metadata`,
|
||||
reqBody,
|
||||
null,
|
||||
{
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import log from "@/next/log";
|
||||
import { apiOrigin } from "@/next/origins";
|
||||
import ComlinkCryptoWorker from "@ente/shared/crypto";
|
||||
import { CustomError, parseSharingErrorCodes } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getEndpoint } from "@ente/shared/network/api";
|
||||
import localForage from "@ente/shared/storage/localForage";
|
||||
import { Collection, CollectionPublicMagicMetadata } from "types/collection";
|
||||
import { EncryptedEnteFile, EnteFile } from "types/file";
|
||||
import { LocalSavedPublicCollectionFiles } from "types/publicCollection";
|
||||
import { decryptFile, mergeMetadata, sortFiles } from "utils/file";
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
const PUBLIC_COLLECTION_FILES_TABLE = "public-collection-files";
|
||||
const PUBLIC_COLLECTIONS_TABLE = "public-collections";
|
||||
const PUBLIC_REFERRAL_CODE = "public-referral-code";
|
||||
@@ -253,7 +252,7 @@ const getPublicFiles = async (
|
||||
break;
|
||||
}
|
||||
resp = await HTTPService.get(
|
||||
`${ENDPOINT}/public-collection/diff`,
|
||||
`${apiOrigin()}/public-collection/diff`,
|
||||
{
|
||||
sinceTime: time,
|
||||
},
|
||||
@@ -308,7 +307,7 @@ export const getPublicCollection = async (
|
||||
return;
|
||||
}
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/public-collection/info`,
|
||||
`${apiOrigin()}/public-collection/info`,
|
||||
null,
|
||||
{ "Cache-Control": "no-cache", "X-Auth-Access-Token": token },
|
||||
);
|
||||
@@ -358,7 +357,7 @@ export const verifyPublicCollectionPassword = async (
|
||||
): Promise<string> => {
|
||||
try {
|
||||
const resp = await HTTPService.post(
|
||||
`${ENDPOINT}/public-collection/verify-password`,
|
||||
`${apiOrigin()}/public-collection/verify-password`,
|
||||
{ passHash: passwordHash },
|
||||
null,
|
||||
{ "Cache-Control": "no-cache", "X-Auth-Access-Token": token },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import log from "@/next/log";
|
||||
import { apiOrigin } from "@/next/origins";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getEndpoint } from "@ente/shared/network/api";
|
||||
import localForage from "@ente/shared/storage/localForage";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
import { Collection } from "types/collection";
|
||||
@@ -14,8 +14,6 @@ const TRASH = "file-trash";
|
||||
const TRASH_TIME = "trash-time";
|
||||
const DELETED_COLLECTION = "deleted-collection";
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
async function getLocalTrash() {
|
||||
const trash = (await localForage.getItem<Trash>(TRASH)) || [];
|
||||
return trash;
|
||||
@@ -91,7 +89,7 @@ export const updateTrash = async (
|
||||
break;
|
||||
}
|
||||
resp = await HTTPService.get(
|
||||
`${ENDPOINT}/trash/v2/diff`,
|
||||
`${apiOrigin()}/trash/v2/diff`,
|
||||
{
|
||||
sinceTime: time,
|
||||
},
|
||||
@@ -160,7 +158,7 @@ export const emptyTrash = async () => {
|
||||
const lastUpdatedAt = await getLastSyncTime();
|
||||
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/trash/empty`,
|
||||
`${apiOrigin()}/trash/empty`,
|
||||
{ lastUpdatedAt },
|
||||
null,
|
||||
{
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import log from "@/next/log";
|
||||
import { apiOrigin } from "@/next/origins";
|
||||
import { CustomError, handleUploadError } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getEndpoint } from "@ente/shared/network/api";
|
||||
import { EnteFile } from "types/file";
|
||||
import { retryHTTPCall } from "./uploadHttpClient";
|
||||
import { MultipartUploadURLs, UploadFile, UploadURL } from "./uploadService";
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
const MAX_URL_REQUESTS = 50;
|
||||
|
||||
class PublicUploadHttpClient {
|
||||
@@ -25,7 +23,7 @@ class PublicUploadHttpClient {
|
||||
const response = await retryHTTPCall(
|
||||
() =>
|
||||
HTTPService.post(
|
||||
`${ENDPOINT}/public-collection/file`,
|
||||
`${apiOrigin()}/public-collection/file`,
|
||||
uploadFile,
|
||||
null,
|
||||
{
|
||||
@@ -57,7 +55,7 @@ class PublicUploadHttpClient {
|
||||
throw Error(CustomError.TOKEN_MISSING);
|
||||
}
|
||||
this.uploadURLFetchInProgress = HTTPService.get(
|
||||
`${ENDPOINT}/public-collection/upload-urls`,
|
||||
`${apiOrigin()}/public-collection/upload-urls`,
|
||||
{
|
||||
count: Math.min(MAX_URL_REQUESTS, count * 2),
|
||||
},
|
||||
@@ -93,7 +91,7 @@ class PublicUploadHttpClient {
|
||||
throw Error(CustomError.TOKEN_MISSING);
|
||||
}
|
||||
const response = await HTTPService.get(
|
||||
`${ENDPOINT}/public-collection/multipart-upload-urls`,
|
||||
`${apiOrigin()}/public-collection/multipart-upload-urls`,
|
||||
{
|
||||
count,
|
||||
},
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import log from "@/next/log";
|
||||
import { apiOrigin, uploaderOrigin } from "@/next/origins";
|
||||
import { wait } from "@/utils/promise";
|
||||
import { CustomError, handleUploadError } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getEndpoint, getUploadEndpoint } from "@ente/shared/network/api";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
import { EnteFile } from "types/file";
|
||||
import { MultipartUploadURLs, UploadFile, UploadURL } from "./uploadService";
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
const UPLOAD_ENDPOINT = getUploadEndpoint();
|
||||
|
||||
const MAX_URL_REQUESTS = 50;
|
||||
|
||||
class UploadHttpClient {
|
||||
@@ -23,7 +20,7 @@ class UploadHttpClient {
|
||||
}
|
||||
const response = await retryHTTPCall(
|
||||
() =>
|
||||
HTTPService.post(`${ENDPOINT}/files`, uploadFile, null, {
|
||||
HTTPService.post(`${apiOrigin()}/files`, uploadFile, null, {
|
||||
"X-Auth-Token": token,
|
||||
}),
|
||||
handleUploadError,
|
||||
@@ -44,7 +41,7 @@ class UploadHttpClient {
|
||||
return;
|
||||
}
|
||||
this.uploadURLFetchInProgress = HTTPService.get(
|
||||
`${ENDPOINT}/files/upload-urls`,
|
||||
`${apiOrigin()}/files/upload-urls`,
|
||||
{
|
||||
count: Math.min(MAX_URL_REQUESTS, count * 2),
|
||||
},
|
||||
@@ -74,7 +71,7 @@ class UploadHttpClient {
|
||||
return;
|
||||
}
|
||||
const response = await HTTPService.get(
|
||||
`${ENDPOINT}/files/multipart-upload-urls`,
|
||||
`${apiOrigin()}/files/multipart-upload-urls`,
|
||||
{
|
||||
count,
|
||||
},
|
||||
@@ -122,7 +119,7 @@ class UploadHttpClient {
|
||||
try {
|
||||
await retryHTTPCall(() =>
|
||||
HTTPService.put(
|
||||
`${UPLOAD_ENDPOINT}/file-upload`,
|
||||
`${uploaderOrigin()}/file-upload`,
|
||||
file,
|
||||
null,
|
||||
{
|
||||
@@ -178,7 +175,7 @@ class UploadHttpClient {
|
||||
try {
|
||||
const response = await retryHTTPCall(async () => {
|
||||
const resp = await HTTPService.put(
|
||||
`${UPLOAD_ENDPOINT}/multipart-upload`,
|
||||
`${uploaderOrigin()}/multipart-upload`,
|
||||
filePart,
|
||||
null,
|
||||
{
|
||||
@@ -219,7 +216,7 @@ class UploadHttpClient {
|
||||
try {
|
||||
await retryHTTPCall(() =>
|
||||
HTTPService.post(
|
||||
`${UPLOAD_ENDPOINT}/multipart-complete`,
|
||||
`${uploaderOrigin()}/multipart-complete`,
|
||||
reqBody,
|
||||
null,
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import log from "@/next/log";
|
||||
import { apiOrigin, 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";
|
||||
import { getEndpoint, getFamilyPortalURL } from "@ente/shared/network/api";
|
||||
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
|
||||
import {
|
||||
getToken,
|
||||
@@ -17,15 +17,13 @@ import {
|
||||
} from "types/user";
|
||||
import { getLocalFamilyData, isPartOfFamily } from "utils/user/family";
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
const HAS_SET_KEYS = "hasSetKeys";
|
||||
|
||||
export const getPublicKey = async (email: string) => {
|
||||
const token = getToken();
|
||||
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/public-key`,
|
||||
`${apiOrigin()}/users/public-key`,
|
||||
{ email },
|
||||
{
|
||||
"X-Auth-Token": token,
|
||||
@@ -38,7 +36,7 @@ export const getPaymentToken = async () => {
|
||||
const token = getToken();
|
||||
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/payment-token`,
|
||||
`${apiOrigin()}/users/payment-token`,
|
||||
null,
|
||||
{
|
||||
"X-Auth-Token": token,
|
||||
@@ -52,7 +50,7 @@ export const getFamiliesToken = async () => {
|
||||
const token = getToken();
|
||||
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/families-token`,
|
||||
`${apiOrigin()}/users/families-token`,
|
||||
null,
|
||||
{
|
||||
"X-Auth-Token": token,
|
||||
@@ -70,7 +68,7 @@ export const getRoadmapRedirectURL = async () => {
|
||||
const token = getToken();
|
||||
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/roadmap/v2`,
|
||||
`${apiOrigin()}/users/roadmap/v2`,
|
||||
null,
|
||||
{
|
||||
"X-Auth-Token": token,
|
||||
@@ -86,7 +84,7 @@ export const getRoadmapRedirectURL = async () => {
|
||||
export const isTokenValid = async (token: string) => {
|
||||
try {
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/session-validity/v2`,
|
||||
`${apiOrigin()}/users/session-validity/v2`,
|
||||
null,
|
||||
{
|
||||
"X-Auth-Token": token,
|
||||
@@ -125,7 +123,7 @@ export const isTokenValid = async (token: string) => {
|
||||
|
||||
export const getTwoFactorStatus = async () => {
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/two-factor/status`,
|
||||
`${apiOrigin()}/users/two-factor/status`,
|
||||
null,
|
||||
{
|
||||
"X-Auth-Token": getToken(),
|
||||
@@ -139,7 +137,7 @@ export const getUserDetailsV2 = async (): Promise<UserDetails> => {
|
||||
const token = getToken();
|
||||
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/details/v2`,
|
||||
`${apiOrigin()}/users/details/v2`,
|
||||
null,
|
||||
{
|
||||
"X-Auth-Token": token,
|
||||
@@ -156,7 +154,7 @@ export const getFamilyPortalRedirectURL = async () => {
|
||||
try {
|
||||
const jwtToken = await getFamiliesToken();
|
||||
const isFamilyCreated = isPartOfFamily(getLocalFamilyData());
|
||||
return `${getFamilyPortalURL()}?token=${jwtToken}&isFamilyCreated=${isFamilyCreated}&redirectURL=${
|
||||
return `${familyAppOrigin()}?token=${jwtToken}&isFamilyCreated=${isFamilyCreated}&redirectURL=${
|
||||
window.location.origin
|
||||
}/gallery`;
|
||||
} catch (e) {
|
||||
@@ -170,7 +168,7 @@ export const getAccountDeleteChallenge = async () => {
|
||||
const token = getToken();
|
||||
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/users/delete-challenge`,
|
||||
`${apiOrigin()}/users/delete-challenge`,
|
||||
null,
|
||||
{
|
||||
"X-Auth-Token": token,
|
||||
@@ -195,7 +193,7 @@ export const deleteAccount = async (
|
||||
}
|
||||
|
||||
await HTTPService.delete(
|
||||
`${ENDPOINT}/users/delete`,
|
||||
`${apiOrigin()}/users/delete`,
|
||||
{ challenge, reason, feedback },
|
||||
null,
|
||||
{
|
||||
@@ -213,7 +211,7 @@ export const getFaceSearchEnabledStatus = async () => {
|
||||
const token = getToken();
|
||||
const resp: AxiosResponse<GetRemoteStoreValueResponse> =
|
||||
await HTTPService.get(
|
||||
`${ENDPOINT}/remote-store`,
|
||||
`${apiOrigin()}/remote-store`,
|
||||
{
|
||||
key: "faceSearchEnabled",
|
||||
defaultValue: false,
|
||||
@@ -233,7 +231,7 @@ export const updateFaceSearchEnabledStatus = async (newStatus: boolean) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/remote-store/update`,
|
||||
`${apiOrigin()}/remote-store/update`,
|
||||
{
|
||||
key: "faceSearchEnabled",
|
||||
value: newStatus.toString(),
|
||||
@@ -264,7 +262,7 @@ export const getMapEnabledStatus = async () => {
|
||||
const token = getToken();
|
||||
const resp: AxiosResponse<GetRemoteStoreValueResponse> =
|
||||
await HTTPService.get(
|
||||
`${ENDPOINT}/remote-store`,
|
||||
`${apiOrigin()}/remote-store`,
|
||||
{
|
||||
key: "mapEnabled",
|
||||
defaultValue: false,
|
||||
@@ -284,7 +282,7 @@ export const updateMapEnabledStatus = async (newStatus: boolean) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/remote-store/update`,
|
||||
`${apiOrigin()}/remote-store/update`,
|
||||
{
|
||||
key: "mapEnabled",
|
||||
value: newStatus.toString(),
|
||||
@@ -314,10 +312,13 @@ export const updateMapEnabledStatus = async (newStatus: boolean) => {
|
||||
* rename this to say getUseDirectUpload).
|
||||
*/
|
||||
export async function getDisableCFUploadProxyFlag(): Promise<boolean> {
|
||||
// If NEXT_PUBLIC_ENTE_ENDPOINT is set, that means we're not running a
|
||||
// production deployment. Disable the Cloudflare upload proxy, and instead
|
||||
// just directly use the upload URLs that museum gives us.
|
||||
if (process.env.NEXT_PUBLIC_ENTE_ENDPOINT) return true;
|
||||
// If a custom origin is set, that means we're not running a production
|
||||
// deployment (maybe we're running locally, or being self-hosted).
|
||||
//
|
||||
// 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;
|
||||
|
||||
try {
|
||||
const featureFlags = (
|
||||
|
||||
@@ -46,9 +46,7 @@ The root `package.json` also has a convenience dev dependency:
|
||||
- [concurrently](https://github.com/open-cli-tools/concurrently) for spawning
|
||||
parallel tasks when we invoke various yarn scripts.
|
||||
|
||||
## Utils
|
||||
|
||||
### Crypto
|
||||
## Crypto
|
||||
|
||||
We use [libsodium](https://libsodium.gitbook.io/doc/) for encryption, key
|
||||
generation etc. Specifically, we use its WebAssembly and JS wrappers made using
|
||||
@@ -70,6 +68,27 @@ builds (See this [issue](https://github.com/jedisct1/libsodium.js/issues/326)).
|
||||
Updating it is not a big problem, it is just a pending chore - we want to test a
|
||||
bit more exhaustively when changing the crypto layer.
|
||||
|
||||
## Meta frameworks
|
||||
|
||||
### Next.js
|
||||
|
||||
[Next.js](https://nextjs.org) ("next") provides the meta framework for both the
|
||||
Photos and the Auth app, and also for some of the sidecar apps like accounts and
|
||||
cast.
|
||||
|
||||
We use a limited subset of Next. The main thing we get out of it is a reasonable
|
||||
set of defaults for bundling our app into a static export which we can then
|
||||
deploy to our webserver. In addition, the Next.js page router is convenient.
|
||||
Apart from this, while we use a few tidbits from Next.js here and there, overall
|
||||
our apps are regular React SPAs, and are not particularly tied to Next.
|
||||
|
||||
### Vite
|
||||
|
||||
For some of our newer code, we have started to use [Vite](https://vitejs.dev).
|
||||
It is more lower level than Next, but the bells and whistles it doesn't have are
|
||||
the bells and whistles (and the accompanying complexity) that we don't need in
|
||||
some cases.
|
||||
|
||||
## UI
|
||||
|
||||
### React
|
||||
@@ -77,13 +96,21 @@ bit more exhaustively when changing the crypto layer.
|
||||
[React](https://react.dev) ("react") is our core framework. It also has a
|
||||
sibling "react-dom" package that renders JSX to the DOM.
|
||||
|
||||
### MUI and Emotion
|
||||
### MUI and Material Icons
|
||||
|
||||
We use [MUI](https://mui.com) ("@mui/material"), which is a React component
|
||||
library, to get a base set of components.
|
||||
We use [MUI](https://mui.com)'s
|
||||
|
||||
- [@mui/material](https://mui.com/material-ui/getting-started/installation/),
|
||||
which is a React component library, to get a base set of components; and
|
||||
|
||||
- [@mui/material-icons](https://mui.com/material-ui/material-icons/). which
|
||||
provides Material icons exported as React components (a `SvgIcon`).
|
||||
|
||||
### Emotion
|
||||
|
||||
MUI uses [Emotion](https://emotion.sh/) (a styled-component variant) as its
|
||||
preferred CSS-in-JS library.
|
||||
preferred CSS-in-JS library, and we use the same in our code too to reduce
|
||||
moving parts.
|
||||
|
||||
Emotion itself comes in many parts, of which we need the following:
|
||||
|
||||
@@ -133,28 +160,13 @@ with Next.js.
|
||||
|
||||
For more details, see [translations.md](translations.md).
|
||||
|
||||
## Meta frameworks
|
||||
### Others
|
||||
|
||||
### Next.js
|
||||
- [formik](https://github.com/jaredpalmer/formik) provides an easier to use
|
||||
abstraction for dealing with form state, validation and submission states
|
||||
when using React.
|
||||
|
||||
[Next.js](https://nextjs.org) ("next") provides the meta framework for both the
|
||||
Photos and the Auth app, and also for some of the sidecar apps like accounts and
|
||||
cast.
|
||||
|
||||
We use a limited subset of Next. The main thing we get out of it is a reasonable
|
||||
set of defaults for bundling our app into a static export which we can then
|
||||
deploy to our webserver. In addition, the Next.js page router is convenient.
|
||||
Apart from this, while we use a few tidbits from Next.js here and there, overall
|
||||
our apps are regular React SPAs, and are not particularly tied to Next.
|
||||
|
||||
### Vite
|
||||
|
||||
For some of our newer code, we have started to use [Vite](https://vitejs.dev).
|
||||
It is more lower level than Next, but the bells and whistles it doesn't have are
|
||||
the bells and whistles (and the accompanying complexity) that we don't need in
|
||||
some cases.
|
||||
|
||||
## General
|
||||
## Infrastructure
|
||||
|
||||
- [comlink](https://github.com/GoogleChromeLabs/comlink) provides a minimal
|
||||
layer on top of Web Workers to make them more easier to use.
|
||||
@@ -182,6 +194,8 @@ some cases.
|
||||
|
||||
## Photos app specific
|
||||
|
||||
### General
|
||||
|
||||
- [react-dropzone](https://github.com/react-dropzone/react-dropzone/) is a
|
||||
React hook to create a drag-and-drop input zone.
|
||||
|
||||
@@ -189,7 +203,7 @@ some cases.
|
||||
for converting arbitrary strings into strings that are suitable for being
|
||||
used as filenames.
|
||||
|
||||
## Face search
|
||||
### Face search
|
||||
|
||||
- [transformation-matrix](https://github.com/chrvadala/transformation-matrix)
|
||||
is used for performing 2D affine transformations using transformation
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import log from "@/next/log";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getEndpoint } from "@ente/shared/network/api";
|
||||
|
||||
import { apiOrigin } from "@/next/origins";
|
||||
import type {
|
||||
CompleteSRPSetupRequest,
|
||||
CompleteSRPSetupResponse,
|
||||
@@ -15,17 +13,19 @@ import type {
|
||||
UpdateSRPAndKeysResponse,
|
||||
} from "@ente/accounts/types/srp";
|
||||
import { ApiError, CustomError } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { HttpStatusCode } from "axios";
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
export const getSRPAttributes = async (
|
||||
email: string,
|
||||
): Promise<SRPAttributes | null> => {
|
||||
try {
|
||||
const resp = await HTTPService.get(`${ENDPOINT}/users/srp/attributes`, {
|
||||
email,
|
||||
});
|
||||
const resp = await HTTPService.get(
|
||||
`${apiOrigin()}/users/srp/attributes`,
|
||||
{
|
||||
email,
|
||||
},
|
||||
);
|
||||
return (resp.data as GetSRPAttributesResponse).attributes;
|
||||
} catch (e) {
|
||||
log.error("failed to get SRP attributes", e);
|
||||
@@ -39,7 +39,7 @@ export const startSRPSetup = async (
|
||||
): Promise<SetupSRPResponse> => {
|
||||
try {
|
||||
const resp = await HTTPService.post(
|
||||
`${ENDPOINT}/users/srp/setup`,
|
||||
`${apiOrigin()}/users/srp/setup`,
|
||||
setupSRPRequest,
|
||||
undefined,
|
||||
{
|
||||
@@ -60,7 +60,7 @@ export const completeSRPSetup = async (
|
||||
) => {
|
||||
try {
|
||||
const resp = await HTTPService.post(
|
||||
`${ENDPOINT}/users/srp/complete`,
|
||||
`${apiOrigin()}/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(
|
||||
`${ENDPOINT}/users/srp/create-session`,
|
||||
`${apiOrigin()}/users/srp/create-session`,
|
||||
{
|
||||
srpUserID,
|
||||
srpA,
|
||||
@@ -97,7 +97,7 @@ export const verifySRPSession = async (
|
||||
) => {
|
||||
try {
|
||||
const resp = await HTTPService.post(
|
||||
`${ENDPOINT}/users/srp/verify-session`,
|
||||
`${apiOrigin()}/users/srp/verify-session`,
|
||||
{
|
||||
sessionID,
|
||||
srpUserID,
|
||||
@@ -125,7 +125,7 @@ export const updateSRPAndKeys = async (
|
||||
): Promise<UpdateSRPAndKeysResponse> => {
|
||||
try {
|
||||
const resp = await HTTPService.post(
|
||||
`${ENDPOINT}/users/srp/update`,
|
||||
`${apiOrigin()}/users/srp/update`,
|
||||
updateSRPAndKeyRequest,
|
||||
undefined,
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiOrigin } from "@/next/origins";
|
||||
import type { AppName } from "@/next/types/app";
|
||||
import type {
|
||||
RecoveryKey,
|
||||
@@ -9,15 +10,12 @@ import type {
|
||||
import type { B64EncryptionResult } from "@ente/shared/crypto/types";
|
||||
import { ApiError, CustomError } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getEndpoint } from "@ente/shared/network/api";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
import type { KeyAttributes } from "@ente/shared/user/types";
|
||||
import { HttpStatusCode } from "axios";
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
export const sendOtt = (appName: AppName, email: string) => {
|
||||
return HTTPService.post(`${ENDPOINT}/users/ott`, {
|
||||
return HTTPService.post(`${apiOrigin()}/users/ott`, {
|
||||
email,
|
||||
client: appName == "auth" ? "totp" : "web",
|
||||
});
|
||||
@@ -25,7 +23,7 @@ export const sendOtt = (appName: AppName, email: string) => {
|
||||
|
||||
export const verifyOtt = (email: string, ott: string, referral: string) => {
|
||||
const cleanedReferral = `web:${referral?.trim() || ""}`;
|
||||
return HTTPService.post(`${ENDPOINT}/users/verify-email`, {
|
||||
return HTTPService.post(`${apiOrigin()}/users/verify-email`, {
|
||||
email,
|
||||
ott,
|
||||
source: cleanedReferral,
|
||||
@@ -34,7 +32,7 @@ export const verifyOtt = (email: string, ott: string, referral: string) => {
|
||||
|
||||
export const putAttributes = (token: string, keyAttributes: KeyAttributes) =>
|
||||
HTTPService.put(
|
||||
`${ENDPOINT}/users/attributes`,
|
||||
`${apiOrigin()}/users/attributes`,
|
||||
{ keyAttributes },
|
||||
undefined,
|
||||
{
|
||||
@@ -45,7 +43,7 @@ export const putAttributes = (token: string, keyAttributes: KeyAttributes) =>
|
||||
export const logout = async () => {
|
||||
try {
|
||||
const token = getToken();
|
||||
await HTTPService.post(`${ENDPOINT}/users/logout`, null, undefined, {
|
||||
await HTTPService.post(`${apiOrigin()}/users/logout`, null, undefined, {
|
||||
"X-Auth-Token": token,
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -65,10 +63,13 @@ export const logout = async () => {
|
||||
};
|
||||
|
||||
export const verifyTwoFactor = async (code: string, sessionID: string) => {
|
||||
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/verify`, {
|
||||
code,
|
||||
sessionID,
|
||||
});
|
||||
const resp = await HTTPService.post(
|
||||
`${apiOrigin()}/users/two-factor/verify`,
|
||||
{
|
||||
code,
|
||||
sessionID,
|
||||
},
|
||||
);
|
||||
return resp.data as UserVerificationResponse;
|
||||
};
|
||||
|
||||
@@ -79,10 +80,13 @@ export const recoverTwoFactor = async (
|
||||
sessionID: string,
|
||||
twoFactorType: TwoFactorType,
|
||||
) => {
|
||||
const resp = await HTTPService.get(`${ENDPOINT}/users/two-factor/recover`, {
|
||||
sessionID,
|
||||
twoFactorType,
|
||||
});
|
||||
const resp = await HTTPService.get(
|
||||
`${apiOrigin()}/users/two-factor/recover`,
|
||||
{
|
||||
sessionID,
|
||||
twoFactorType,
|
||||
},
|
||||
);
|
||||
return resp.data as TwoFactorRecoveryResponse;
|
||||
};
|
||||
|
||||
@@ -91,17 +95,20 @@ export const removeTwoFactor = async (
|
||||
secret: string,
|
||||
twoFactorType: TwoFactorType,
|
||||
) => {
|
||||
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/remove`, {
|
||||
sessionID,
|
||||
secret,
|
||||
twoFactorType,
|
||||
});
|
||||
const resp = await HTTPService.post(
|
||||
`${apiOrigin()}/users/two-factor/remove`,
|
||||
{
|
||||
sessionID,
|
||||
secret,
|
||||
twoFactorType,
|
||||
},
|
||||
);
|
||||
return resp.data as TwoFactorVerificationResponse;
|
||||
};
|
||||
|
||||
export const changeEmail = async (email: string, ott: string) => {
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/users/change-email`,
|
||||
`${apiOrigin()}/users/change-email`,
|
||||
{
|
||||
email,
|
||||
ott,
|
||||
@@ -114,7 +121,7 @@ export const changeEmail = async (email: string, ott: string) => {
|
||||
};
|
||||
|
||||
export const sendOTTForEmailChange = async (email: string) => {
|
||||
await HTTPService.post(`${ENDPOINT}/users/ott`, {
|
||||
await HTTPService.post(`${apiOrigin()}/users/ott`, {
|
||||
email,
|
||||
client: "web",
|
||||
purpose: "change",
|
||||
@@ -123,7 +130,7 @@ export const sendOTTForEmailChange = async (email: string) => {
|
||||
|
||||
export const setupTwoFactor = async () => {
|
||||
const resp = await HTTPService.post(
|
||||
`${ENDPOINT}/users/two-factor/setup`,
|
||||
`${apiOrigin()}/users/two-factor/setup`,
|
||||
null,
|
||||
undefined,
|
||||
{
|
||||
@@ -138,7 +145,7 @@ export const enableTwoFactor = async (
|
||||
recoveryEncryptedTwoFactorSecret: B64EncryptionResult,
|
||||
) => {
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/users/two-factor/enable`,
|
||||
`${apiOrigin()}/users/two-factor/enable`,
|
||||
{
|
||||
code,
|
||||
encryptedTwoFactorSecret:
|
||||
@@ -154,13 +161,18 @@ export const enableTwoFactor = async (
|
||||
};
|
||||
|
||||
export const setRecoveryKey = (token: string, recoveryKey: RecoveryKey) =>
|
||||
HTTPService.put(`${ENDPOINT}/users/recovery-key`, recoveryKey, undefined, {
|
||||
"X-Auth-Token": token,
|
||||
});
|
||||
HTTPService.put(
|
||||
`${apiOrigin()}/users/recovery-key`,
|
||||
recoveryKey,
|
||||
undefined,
|
||||
{
|
||||
"X-Auth-Token": token,
|
||||
},
|
||||
);
|
||||
|
||||
export const disableTwoFactor = async () => {
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/users/two-factor/disable`,
|
||||
`${apiOrigin()}/users/two-factor/disable`,
|
||||
null,
|
||||
undefined,
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ import SingleInputForm, {
|
||||
type SingleInputFormProps,
|
||||
} from "@ente/shared/components/SingleInputForm";
|
||||
import { LS_KEYS, setData } from "@ente/shared/storage/localStorage";
|
||||
import { Input } from "@mui/material";
|
||||
import { Input, Stack, Typography } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { getSRPAttributes } from "../api/srp";
|
||||
@@ -17,9 +17,11 @@ import { PAGES } from "../constants/pages";
|
||||
interface LoginProps {
|
||||
signUp: () => void;
|
||||
appName: AppName;
|
||||
/** Reactive value of {@link customAPIHost}. */
|
||||
host: string | undefined;
|
||||
}
|
||||
|
||||
export function Login({ appName, signUp }: LoginProps) {
|
||||
export function Login({ appName, signUp, host }: LoginProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const loginUser: SingleInputFormProps["callback"] = async (
|
||||
@@ -63,7 +65,17 @@ export function Login({ appName, signUp }: LoginProps) {
|
||||
/>
|
||||
|
||||
<FormPaperFooter>
|
||||
<LinkButton onClick={signUp}>{t("NO_ACCOUNT")}</LinkButton>
|
||||
<Stack gap={4}>
|
||||
<LinkButton onClick={signUp}>{t("NO_ACCOUNT")}</LinkButton>
|
||||
|
||||
<Typography
|
||||
variant="mini"
|
||||
color="text.faint"
|
||||
minHeight={"32px"}
|
||||
>
|
||||
{host ?? "" /* prevent layout shift with a minHeight */}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</FormPaperFooter>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
Link,
|
||||
Stack,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
@@ -53,9 +54,11 @@ interface SignUpProps {
|
||||
router: NextRouter;
|
||||
login: () => void;
|
||||
appName: AppName;
|
||||
/** Reactive value of {@link customAPIHost}. */
|
||||
host: string | undefined;
|
||||
}
|
||||
|
||||
export function SignUp({ router, appName, login }: SignUpProps) {
|
||||
export function SignUp({ router, appName, login, host }: SignUpProps) {
|
||||
const [acceptTerms, setAcceptTerms] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
@@ -310,7 +313,19 @@ export function SignUp({ router, appName, login }: SignUpProps) {
|
||||
</Formik>
|
||||
|
||||
<FormPaperFooter>
|
||||
<LinkButton onClick={login}>{t("ACCOUNT_EXISTS")}</LinkButton>
|
||||
<Stack gap={4}>
|
||||
<LinkButton onClick={login}>
|
||||
{t("ACCOUNT_EXISTS")}
|
||||
</LinkButton>
|
||||
|
||||
<Typography
|
||||
variant="mini"
|
||||
color="text.faint"
|
||||
minHeight={"32px"}
|
||||
>
|
||||
{host ?? "" /* prevent layout shift with a minHeight */}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</FormPaperFooter>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { isDevBuild } from "@/next/env";
|
||||
import log from "@/next/log";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||
import FormPaper from "@ente/shared/components/Form/FormPaper";
|
||||
import FormPaperFooter from "@ente/shared/components/Form/FormPaper/Footer";
|
||||
import LinkButton from "@ente/shared/components/LinkButton";
|
||||
import {
|
||||
ConnectionDetails,
|
||||
LoginFlowFormFooter,
|
||||
PasswordHeader,
|
||||
VerifyingPasskey,
|
||||
} from "@ente/shared/components/LoginComponents";
|
||||
@@ -42,6 +40,7 @@ import {
|
||||
setKey,
|
||||
} from "@ente/shared/storage/sessionStorage";
|
||||
import type { KeyAttributes, User } from "@ente/shared/user/types";
|
||||
import { Stack } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -321,16 +320,16 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
srpAttributes={srpAttributes}
|
||||
/>
|
||||
|
||||
<FormPaperFooter style={{ justifyContent: "space-between" }}>
|
||||
<LinkButton onClick={() => router.push(PAGES.RECOVER)}>
|
||||
{t("FORGOT_PASSWORD")}
|
||||
</LinkButton>
|
||||
<LinkButton onClick={logout}>
|
||||
{t("CHANGE_EMAIL")}
|
||||
</LinkButton>
|
||||
</FormPaperFooter>
|
||||
|
||||
{isDevBuild && <ConnectionDetails />}
|
||||
<LoginFlowFormFooter>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<LinkButton onClick={() => router.push(PAGES.RECOVER)}>
|
||||
{t("FORGOT_PASSWORD")}
|
||||
</LinkButton>
|
||||
<LinkButton onClick={logout}>
|
||||
{t("CHANGE_EMAIL")}
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
</LoginFlowFormFooter>
|
||||
</FormPaper>
|
||||
</VerticallyCentered>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { customAPIHost } from "@/next/origins";
|
||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||
import FormPaper from "@ente/shared/components/Form/FormPaper";
|
||||
@@ -15,6 +16,8 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const host = customAPIHost();
|
||||
|
||||
useEffect(() => {
|
||||
const user = getData(LS_KEYS.USER);
|
||||
if (user?.email) {
|
||||
@@ -24,7 +27,7 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
showNavBar(true);
|
||||
}, []);
|
||||
|
||||
const register = () => {
|
||||
const signUp = () => {
|
||||
router.push(PAGES.SIGNUP);
|
||||
};
|
||||
|
||||
@@ -35,7 +38,7 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
) : (
|
||||
<VerticallyCentered>
|
||||
<FormPaper>
|
||||
<Login signUp={register} appName={appName} />
|
||||
<Login {...{ appName, signUp, host }} />
|
||||
</FormPaper>
|
||||
</VerticallyCentered>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { customAPIHost } from "@/next/origins";
|
||||
import { PAGES } from "@ente/accounts/constants/pages";
|
||||
import { LS_KEYS, getData } from "@ente/shared//storage/localStorage";
|
||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
@@ -15,6 +16,8 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const host = customAPIHost();
|
||||
|
||||
useEffect(() => {
|
||||
const user = getData(LS_KEYS.USER);
|
||||
if (user?.email) {
|
||||
@@ -34,7 +37,7 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
<EnteSpinner />
|
||||
) : (
|
||||
<FormPaper>
|
||||
<SignUp login={login} router={router} appName={appName} />
|
||||
<SignUp {...{ appName, login, router, host }} />
|
||||
</FormPaper>
|
||||
)}
|
||||
</VerticallyCentered>
|
||||
|
||||
@@ -3,10 +3,12 @@ import type { UserVerificationResponse } from "@ente/accounts/types/user";
|
||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||
import FormPaper from "@ente/shared/components/Form/FormPaper";
|
||||
import FormPaperFooter from "@ente/shared/components/Form/FormPaper/Footer";
|
||||
import FormPaperTitle from "@ente/shared/components/Form/FormPaper/Title";
|
||||
import LinkButton from "@ente/shared/components/LinkButton";
|
||||
import { VerifyingPasskey } from "@ente/shared/components/LoginComponents";
|
||||
import {
|
||||
LoginFlowFormFooter,
|
||||
VerifyingPasskey,
|
||||
} from "@ente/shared/components/LoginComponents";
|
||||
import SingleInputForm, {
|
||||
type SingleInputFormProps,
|
||||
} from "@ente/shared/components/SingleInputForm";
|
||||
@@ -20,7 +22,7 @@ import {
|
||||
} from "@ente/shared/storage/localStorage/helpers";
|
||||
import { clearKeys } from "@ente/shared/storage/sessionStorage";
|
||||
import type { KeyAttributes, User } from "@ente/shared/user/types";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { HttpStatusCode } from "axios";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -225,18 +227,20 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
callback={onSubmit}
|
||||
/>
|
||||
|
||||
<FormPaperFooter style={{ justifyContent: "space-between" }}>
|
||||
{resend === 0 && (
|
||||
<LinkButton onClick={resendEmail}>
|
||||
{t("RESEND_MAIL")}
|
||||
<LoginFlowFormFooter>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
{resend === 0 && (
|
||||
<LinkButton onClick={resendEmail}>
|
||||
{t("RESEND_MAIL")}
|
||||
</LinkButton>
|
||||
)}
|
||||
{resend === 1 && <span>{t("SENDING")}</span>}
|
||||
{resend === 2 && <span>{t("SENT")}</span>}
|
||||
<LinkButton onClick={logout}>
|
||||
{t("CHANGE_EMAIL")}
|
||||
</LinkButton>
|
||||
)}
|
||||
{resend === 1 && <span>{t("SENDING")}</span>}
|
||||
{resend === 2 && <span>{t("SENT")}</span>}
|
||||
<LinkButton onClick={logout}>
|
||||
{t("CHANGE_EMAIL")}
|
||||
</LinkButton>
|
||||
</FormPaperFooter>
|
||||
</Stack>
|
||||
</LoginFlowFormFooter>
|
||||
</FormPaper>
|
||||
</VerticallyCentered>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { clientPackageHeaderIfPresent } from "@/next/http";
|
||||
import log from "@/next/log";
|
||||
import { accountsAppOrigin, apiOrigin } from "@/next/origins";
|
||||
import type { AppName } from "@/next/types/app";
|
||||
import { clientPackageName } from "@/next/types/app";
|
||||
import { TwoFactorAuthorizationResponse } from "@/next/types/credentials";
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
} from "@ente/shared/crypto/internal/libsodium";
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { accountsAppURL, apiOrigin } from "@ente/shared/network/api";
|
||||
import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore";
|
||||
import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
@@ -48,7 +48,7 @@ export const passkeyVerificationRedirectURL = (
|
||||
redirect,
|
||||
...recoverOption,
|
||||
});
|
||||
return `${accountsAppURL()}/passkeys/verify?${params.toString()}`;
|
||||
return `${accountsAppOrigin()}/passkeys/verify?${params.toString()}`;
|
||||
};
|
||||
|
||||
interface OpenPasskeyVerificationURLOptions {
|
||||
@@ -131,7 +131,7 @@ export const openAccountsManagePasskeysPage = async () => {
|
||||
const token = await getAccountsToken();
|
||||
const params = new URLSearchParams({ token });
|
||||
|
||||
window.open(`${accountsAppURL()}/passkeys?${params.toString()}`);
|
||||
window.open(`${accountsAppOrigin()}/passkeys?${params.toString()}`);
|
||||
};
|
||||
|
||||
export const isPasskeyRecoveryEnabled = async () => {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"@/next": "*",
|
||||
"@/utils": "*",
|
||||
"@ente/shared": "*",
|
||||
"formik": "^2.4",
|
||||
"zod": "^3"
|
||||
},
|
||||
"devDependencies": {}
|
||||
|
||||
182
web/packages/new/photos/components/DevSettings.tsx
Normal file
182
web/packages/new/photos/components/DevSettings.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import log from "@/next/log";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
Link,
|
||||
TextField,
|
||||
useMediaQuery,
|
||||
type ModalProps,
|
||||
} from "@mui/material";
|
||||
import { useFormik } from "formik";
|
||||
import { t } from "i18next";
|
||||
import React from "react";
|
||||
import { z } from "zod";
|
||||
import { FocusVisibleButton } from "./FocusVisibleButton";
|
||||
import { SlideTransition } from "./SlideTransition";
|
||||
|
||||
interface DevSettingsProps {
|
||||
/** If `true`, then the dialog is shown. */
|
||||
open: boolean;
|
||||
/** Called when the dialog wants to be closed. */
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A dialog allowing the user to set the API origin that the app connects to.
|
||||
* See: [Note: Configuring custom server].
|
||||
*/
|
||||
export const DevSettings: React.FC<DevSettingsProps> = ({ open, onClose }) => {
|
||||
const fullScreen = useMediaQuery("(max-width: 428px)");
|
||||
|
||||
const handleDialogClose: ModalProps["onClose"] = (_, reason: string) => {
|
||||
// Don't close on backdrop clicks.
|
||||
if (reason != "backdropClick") onClose();
|
||||
};
|
||||
|
||||
const form = useFormik({
|
||||
initialValues: {
|
||||
apiOrigin: localStorage.getItem("apiOrigin") ?? "",
|
||||
},
|
||||
validate: ({ apiOrigin }) => {
|
||||
try {
|
||||
apiOrigin && new URL(apiOrigin);
|
||||
} catch {
|
||||
return { apiOrigin: "Invalid endpoint" };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
onSubmit: async (values, { setSubmitting, setErrors }) => {
|
||||
try {
|
||||
await updateAPIOrigin(values.apiOrigin);
|
||||
} catch (e) {
|
||||
// The person using this functionality is likely a developer and
|
||||
// might be helped more by the original error instead of a
|
||||
// friendlier but less specific message.
|
||||
setErrors({
|
||||
apiOrigin: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(false);
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
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={
|
||||
form.touched.apiOrigin && !!form.errors.apiOrigin
|
||||
}
|
||||
helperText={
|
||||
(form.touched.apiOrigin && 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"
|
||||
>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Save {@link origin} to local storage after verifying it with a ping.
|
||||
*
|
||||
* The given {@link origin} will be verifying by making an API call to the
|
||||
* `/ping` endpoint. If that succeeds, then it will be saved to local storage,
|
||||
* and all subsequent API calls will use it as the {@link apiOrigin}.
|
||||
*
|
||||
* See: [Note: Configuring custom server].
|
||||
*
|
||||
* @param origin The new API origin to use. Pass an empty string to clear the
|
||||
* previously saved API origin (if any).
|
||||
*/
|
||||
const updateAPIOrigin = async (origin: string) => {
|
||||
if (!origin) {
|
||||
localStorage.removeItem("apiOrigin");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${origin}/ping`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
|
||||
try {
|
||||
PingResponse.parse(await res.json());
|
||||
} catch (e) {
|
||||
log.error("Invalid response", e);
|
||||
throw new Error("Invalid response");
|
||||
}
|
||||
|
||||
localStorage.setItem("apiOrigin", origin);
|
||||
};
|
||||
|
||||
const PingResponse = z.object({
|
||||
message: z.enum(["pong"]),
|
||||
});
|
||||
10
web/packages/new/photos/components/FocusVisibleButton.tsx
Normal file
10
web/packages/new/photos/components/FocusVisibleButton.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Button, styled } from "@mui/material";
|
||||
|
||||
/** A MUI {@link Button} that shows a keyboard focus indicator. */
|
||||
export const FocusVisibleButton = styled(Button)`
|
||||
/* Show an outline when the button gains keyboard focus, e.g. when the user
|
||||
tabs to it. */
|
||||
&.Mui-focusVisible {
|
||||
outline: 1px solid #aaa;
|
||||
}
|
||||
`;
|
||||
16
web/packages/new/photos/components/SlideTransition.tsx
Normal file
16
web/packages/new/photos/components/SlideTransition.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import Slide from "@mui/material/Slide";
|
||||
import type { TransitionProps } from "@mui/material/transitions";
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* A React component that can be passed as the `TransitionComponent` props to a
|
||||
* MUI {@link Dialog} to get it to use a slide transition (default is fade).
|
||||
*/
|
||||
export const SlideTransition = React.forwardRef(function Transition(
|
||||
props: TransitionProps & {
|
||||
children: React.ReactElement;
|
||||
},
|
||||
ref: React.Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ut } from "@/next/i18n";
|
||||
import ArrowForward from "@mui/icons-material/ArrowForward";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
@@ -10,21 +10,21 @@ import {
|
||||
styled,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
import Slide from "@mui/material/Slide";
|
||||
import type { TransitionProps } from "@mui/material/transitions";
|
||||
import React, { useEffect } from "react";
|
||||
import { didShowWhatsNew } from "../services/changelog";
|
||||
import { FocusVisibleButton } from "./FocusVisibleButton";
|
||||
import { SlideTransition } from "./SlideTransition";
|
||||
|
||||
interface WhatsNewProps {
|
||||
/** If `true`, then the dialog is shown. */
|
||||
open: boolean;
|
||||
/** Callback to invoke when the dialog wants to be closed. */
|
||||
/** Called when the dialog wants to be closed. */
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a dialog showing a short summary of interesting-for-the-user things
|
||||
* since the last time this dialog was shown.
|
||||
* A dialog showing a short summary of interesting-for-the-user things since the
|
||||
* last time this dialog was shown.
|
||||
*/
|
||||
export const WhatsNew: React.FC<WhatsNewProps> = ({ open, onClose }) => {
|
||||
const fullScreen = useMediaQuery("(max-width: 428px)");
|
||||
@@ -39,36 +39,27 @@ export const WhatsNew: React.FC<WhatsNewProps> = ({ open, onClose }) => {
|
||||
TransitionComponent={SlideTransition}
|
||||
maxWidth="xs"
|
||||
>
|
||||
<DialogTitle>{"What's new"}</DialogTitle>
|
||||
<DialogTitle>{ut("What's new")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<ChangelogContent />
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<StyledButton
|
||||
<FocusVisibleButton
|
||||
onClick={onClose}
|
||||
color="accent"
|
||||
fullWidth
|
||||
disableRipple
|
||||
endIcon={<ArrowForward />}
|
||||
>
|
||||
<ButtonContents>{"Continue"}</ButtonContents>
|
||||
</StyledButton>
|
||||
<ButtonContents>{ut("Continue")}</ButtonContents>
|
||||
</FocusVisibleButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const SlideTransition = React.forwardRef(function Transition(
|
||||
props: TransitionProps & {
|
||||
children: React.ReactElement;
|
||||
},
|
||||
ref: React.Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
const ChangelogContent: React.FC = () => {
|
||||
// NOTE: Remember to update changelogVersion when changing the content
|
||||
// below.
|
||||
@@ -78,16 +69,19 @@ const ChangelogContent: React.FC = () => {
|
||||
<li>
|
||||
<Typography>
|
||||
<Typography color="primary">
|
||||
Support for Passkeys
|
||||
{ut("Support for Passkeys")}
|
||||
</Typography>
|
||||
Passkeys can now be used as a second factor authentication
|
||||
mechanism.
|
||||
{ut(
|
||||
"Passkeys can now be used as a second factor authentication mechanism.",
|
||||
)}
|
||||
</Typography>
|
||||
</li>
|
||||
<li>
|
||||
<Typography color="primary">Window size</Typography>
|
||||
<Typography color="primary">{ut("Window size")}</Typography>
|
||||
<Typography>
|
||||
{"The app's window will remember its size and position."}
|
||||
{ut(
|
||||
"The app's window will remember its size and position.",
|
||||
)}
|
||||
</Typography>
|
||||
</li>
|
||||
</StyledUL>
|
||||
@@ -102,14 +96,6 @@ const StyledUL = styled("ul")`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
/* Show an outline when the button gains keyboard focus, e.g. when the user
|
||||
tabs to it. */
|
||||
&.Mui-focusVisible {
|
||||
outline: 1px solid #aaa;
|
||||
}
|
||||
`;
|
||||
|
||||
const ButtonContents = styled("div")`
|
||||
/* Make the button text fill the entire space so the endIcon shows at the
|
||||
trailing edge of the button. */
|
||||
|
||||
@@ -2,8 +2,8 @@ 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 { nullToUndefined } from "@/utils/transform";
|
||||
import { apiOrigin } from "@ente/shared/network/api";
|
||||
import { z } from "zod";
|
||||
|
||||
let _fetchTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
@@ -261,3 +261,15 @@ export const setLocaleInUse = async (locale: SupportedLocale) => {
|
||||
localStorage.setItem("locale", locale);
|
||||
return i18n.changeLanguage(locale);
|
||||
};
|
||||
|
||||
/**
|
||||
* A no-op marker for strings that, for various reasons, are not translated.
|
||||
*
|
||||
* This function does nothing, it just returns back the passed it string
|
||||
* verbatim. It is only kept as a way for us to keep track of strings that are
|
||||
* not translated (and for some reason, are currently not meant to be), but
|
||||
* still are user visible.
|
||||
*
|
||||
* It is the sibling of the {@link t} function provided by i18next.
|
||||
*/
|
||||
export const ut = (s: string) => s;
|
||||
|
||||
@@ -639,5 +639,9 @@
|
||||
"redirect_close_instructions": "You can close this window after the app opens.",
|
||||
"redirect_again": "Redirect again",
|
||||
"autogenerated_first_album_name": "My First Album",
|
||||
"autogenerated_default_album_name": "New Album"
|
||||
"autogenerated_default_album_name": "New Album",
|
||||
"developer_settings": "Developer settings",
|
||||
"server_endpoint": "Server endpoint",
|
||||
"more_information": "More information",
|
||||
"save": "Save"
|
||||
}
|
||||
|
||||
87
web/packages/next/origins.ts
Normal file
87
web/packages/next/origins.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { nullToUndefined } from "@/utils/transform";
|
||||
|
||||
/**
|
||||
* Return the origin (scheme, host, port triple) that should be used for making
|
||||
* API requests to museum.
|
||||
*
|
||||
* 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";
|
||||
|
||||
/**
|
||||
* Return the overridden API origin, if one is defined by either (in priority
|
||||
* order):
|
||||
*
|
||||
* - Setting the custom server on the landing page (See: [Note: Configuring
|
||||
* custom server]); or by
|
||||
*
|
||||
* - Setting the `NEXT_PUBLIC_ENTE_ENDPOINT` environment variable.
|
||||
*
|
||||
* Otherwise return undefined.
|
||||
*/
|
||||
export const customAPIOrigin = () =>
|
||||
nullToUndefined(localStorage.getItem("apiOrigin")) ??
|
||||
process.env.NEXT_PUBLIC_ENTE_ENDPOINT ??
|
||||
undefined;
|
||||
|
||||
/**
|
||||
* A convenience wrapper over {@link customAPIOrigin} that returns the only the
|
||||
* host part of the custom origin (if any).
|
||||
*
|
||||
* This is useful in places where we indicate the custom origin in the UI.
|
||||
*/
|
||||
export const customAPIHost = () => {
|
||||
const origin = customAPIOrigin();
|
||||
return origin ? new URL(origin).host : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the origin that should be used for uploading files.
|
||||
*
|
||||
* This defaults to `https://uploader.ente.io`, serviced by a Cloudflare worker
|
||||
* (see infra/workers/uploader). But if a {@link customAPIOrigin} is set then
|
||||
* 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";
|
||||
|
||||
/**
|
||||
* Return the origin that serves the accounts app.
|
||||
*
|
||||
* Defaults to our production instance, "https://accounts.ente.io", but can be
|
||||
* overridden by setting the `NEXT_PUBLIC_ENTE_ACCOUNTS_URL` environment
|
||||
* variable.
|
||||
*/
|
||||
export const accountsAppOrigin = () =>
|
||||
process.env.NEXT_PUBLIC_ENTE_ACCOUNTS_URL ?? `https://accounts.ente.io`;
|
||||
|
||||
/**
|
||||
* Return the origin that serves public albums.
|
||||
*
|
||||
* Defaults to our production instance, "https://albums.ente.io", but can be
|
||||
* overridden by setting the `NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT` environment
|
||||
* variable.
|
||||
*/
|
||||
export const albumsAppOrigin = () =>
|
||||
process.env.NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT ?? "https://albums.ente.io";
|
||||
|
||||
/**
|
||||
* Return the origin that serves the family dashboard which can be used to
|
||||
* create or manage family plans..
|
||||
*
|
||||
* Defaults to our production instance, "https://family.ente.io", but can be
|
||||
* overridden by setting the `NEXT_PUBLIC_ENTE_FAMILY_URL` environment variable.
|
||||
*/
|
||||
export const familyAppOrigin = () =>
|
||||
process.env.NEXT_PUBLIC_ENTE_FAMILY_URL ?? "https://family.ente.io";
|
||||
|
||||
/**
|
||||
* Return the origin that serves the payments app.
|
||||
*
|
||||
* Defaults to our production instance, "https://payments.ente.io", but can be
|
||||
* overridden by setting the `NEXT_PUBLIC_ENTE_PAYMENTS_URL` environment variable.
|
||||
*/
|
||||
export const paymentsAppOrigin = () =>
|
||||
process.env.NEXT_PUBLIC_ENTE_PAYMENTS_URL ?? "https://payments.ente.io";
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isDevBuild } from "@/next/env";
|
||||
import log from "@/next/log";
|
||||
import { customAPIHost } from "@/next/origins";
|
||||
import type { BaseAppContextT } from "@/next/types/app";
|
||||
import {
|
||||
checkPasskeyVerificationStatus,
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
saveCredentialsAndNavigateTo,
|
||||
} from "@ente/accounts/services/passkey";
|
||||
import EnteButton from "@ente/shared/components/EnteButton";
|
||||
import { apiOrigin } from "@ente/shared/network/api";
|
||||
import { CircularProgress, Typography, styled } from "@mui/material";
|
||||
import { CircularProgress, Stack, Typography, styled } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useState } from "react";
|
||||
@@ -46,22 +45,26 @@ const Header_ = styled("div")`
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
export const ConnectionDetails: React.FC = () => {
|
||||
const host = new URL(apiOrigin()).host;
|
||||
export const LoginFlowFormFooter: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const host = customAPIHost();
|
||||
|
||||
return (
|
||||
<ConnectionDetails_>
|
||||
<Typography variant="small" color="text.faint">
|
||||
{host}
|
||||
</Typography>
|
||||
</ConnectionDetails_>
|
||||
<FormPaperFooter>
|
||||
<Stack gap="16px" width="100%" textAlign="start">
|
||||
{children}
|
||||
|
||||
{host && (
|
||||
<Typography variant="small" color="text.faint">
|
||||
{host}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</FormPaperFooter>
|
||||
);
|
||||
};
|
||||
|
||||
const ConnectionDetails_ = styled("div")`
|
||||
margin-block-start: 1rem;
|
||||
`;
|
||||
|
||||
interface VerifyingPasskeyProps {
|
||||
/** ID of the current passkey verification session. */
|
||||
passkeySessionID: string;
|
||||
@@ -161,16 +164,16 @@ export const VerifyingPasskey: React.FC<VerifyingPasskeyProps> = ({
|
||||
</ButtonStack>
|
||||
</VerifyingPasskeyMiddle>
|
||||
|
||||
<FormPaperFooter style={{ justifyContent: "space-between" }}>
|
||||
<LinkButton onClick={handleRecover}>
|
||||
{t("RECOVER_ACCOUNT")}
|
||||
</LinkButton>
|
||||
<LinkButton onClick={logout}>
|
||||
{t("CHANGE_EMAIL")}
|
||||
</LinkButton>
|
||||
</FormPaperFooter>
|
||||
|
||||
{isDevBuild && <ConnectionDetails />}
|
||||
<LoginFlowFormFooter>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<LinkButton onClick={handleRecover}>
|
||||
{t("RECOVER_ACCOUNT")}
|
||||
</LinkButton>
|
||||
<LinkButton onClick={logout}>
|
||||
{t("CHANGE_EMAIL")}
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
</LoginFlowFormFooter>
|
||||
</FormPaper>
|
||||
</VerticallyCentered>
|
||||
);
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
/**
|
||||
* Return the origin (scheme, host, port triple) that should be used for making
|
||||
* API requests to museum.
|
||||
*
|
||||
* This defaults to "https://api.ente.io", Ente's own servers, but can be
|
||||
* overridden when self hosting or developing by setting the
|
||||
* `NEXT_PUBLIC_ENTE_ENDPOINT` environment variable.
|
||||
*/
|
||||
export const apiOrigin = () => customAPIOrigin() ?? "https://api.ente.io";
|
||||
|
||||
/**
|
||||
* Return the overridden API origin, if one is defined by setting the
|
||||
* `NEXT_PUBLIC_ENTE_ENDPOINT` environment variable.
|
||||
*
|
||||
* Otherwise return undefined.
|
||||
*/
|
||||
export const customAPIOrigin = () =>
|
||||
process.env.NEXT_PUBLIC_ENTE_ENDPOINT ?? undefined;
|
||||
|
||||
/** Deprecated, use {@link apiOrigin} instead. */
|
||||
export const getEndpoint = apiOrigin;
|
||||
|
||||
export const getUploadEndpoint = () => {
|
||||
const endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
|
||||
if (endpoint) {
|
||||
return endpoint;
|
||||
}
|
||||
return `https://uploader.ente.io`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the URL of the Ente Accounts app.
|
||||
*
|
||||
* Defaults to our production instance, "https://accounts.ente.io", but can be
|
||||
* overridden by setting the `NEXT_PUBLIC_ENTE_ACCOUNTS_URL` environment
|
||||
* variable.
|
||||
*/
|
||||
export const accountsAppURL = () =>
|
||||
process.env.NEXT_PUBLIC_ENTE_ACCOUNTS_URL ?? `https://accounts.ente.io`;
|
||||
|
||||
export const getAlbumsURL = () => {
|
||||
const albumsURL = process.env.NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT;
|
||||
if (albumsURL) {
|
||||
return albumsURL;
|
||||
}
|
||||
return `https://albums.ente.io`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the URL for the family dashboard which can be used to create or manage
|
||||
* family plans.
|
||||
*/
|
||||
export const getFamilyPortalURL = () => {
|
||||
const familyURL = process.env.NEXT_PUBLIC_ENTE_FAMILY_URL;
|
||||
if (familyURL) {
|
||||
return familyURL;
|
||||
}
|
||||
return `https://family.ente.io`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the URL for the host that handles payment related functionality.
|
||||
*/
|
||||
export const getPaymentsURL = () => {
|
||||
const paymentsURL = process.env.NEXT_PUBLIC_ENTE_PAYMENTS_URL;
|
||||
if (paymentsURL) {
|
||||
return paymentsURL;
|
||||
}
|
||||
return `https://payments.ente.io`;
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import log from "@/next/log";
|
||||
import { apiOrigin } from "@/next/origins";
|
||||
import { ApiError } from "../error";
|
||||
import { getToken } from "../storage/localStorage/helpers";
|
||||
import HTTPService from "./HTTPService";
|
||||
import { getEndpoint } from "./api";
|
||||
|
||||
class CastGateway {
|
||||
constructor() {}
|
||||
@@ -11,7 +11,7 @@ class CastGateway {
|
||||
let resp;
|
||||
try {
|
||||
resp = await HTTPService.get(
|
||||
`${getEndpoint()}/cast/cast-data/${code}`,
|
||||
`${apiOrigin()}/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(
|
||||
getEndpoint() + "/cast/revoke-all-tokens/",
|
||||
apiOrigin() + "/cast/revoke-all-tokens/",
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
@@ -42,7 +42,7 @@ class CastGateway {
|
||||
try {
|
||||
const token = getToken();
|
||||
resp = await HTTPService.get(
|
||||
`${getEndpoint()}/cast/device-info/${code}`,
|
||||
`${apiOrigin()}/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(
|
||||
getEndpoint() + "/cast/device-info/",
|
||||
apiOrigin() + "/cast/device-info/",
|
||||
{
|
||||
publicKey: publicKey,
|
||||
},
|
||||
@@ -76,7 +76,7 @@ class CastGateway {
|
||||
) {
|
||||
const token = getToken();
|
||||
await HTTPService.post(
|
||||
getEndpoint() + "/cast/cast-data/",
|
||||
apiOrigin() + "/cast/cast-data/",
|
||||
{
|
||||
deviceCode: `${code}`,
|
||||
encPayload: castPayload,
|
||||
|
||||
@@ -18,7 +18,6 @@ export enum LS_KEYS {
|
||||
COLLECTION_SORT_BY = "collectionSortBy",
|
||||
THEME = "theme",
|
||||
WAIT_TIME = "waitTime",
|
||||
API_ENDPOINT = "apiEndpoint",
|
||||
// Moved to the new wrapper @/next/local-storage
|
||||
// LOCALE = 'locale',
|
||||
MAP_ENABLED = "mapEnabled",
|
||||
|
||||
@@ -2611,10 +2611,10 @@ form-data@^4.0.0:
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
formik@^2.1.5:
|
||||
version "2.4.5"
|
||||
resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.5.tgz#f899b5b7a6f103a8fabb679823e8fafc7e0ee1b4"
|
||||
integrity sha512-Gxlht0TD3vVdzMDHwkiNZqJ7Mvg77xQNfmBRrNtvzcHZs72TJppSTDKHpImCMJZwcWPBJ8jSQQ95GJzXFf1nAQ==
|
||||
formik@^2.4:
|
||||
version "2.4.6"
|
||||
resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.6.tgz#4da75ca80f1a827ab35b08fd98d5a76e928c9686"
|
||||
integrity sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==
|
||||
dependencies:
|
||||
"@types/hoist-non-react-statics" "^3.3.1"
|
||||
deepmerge "^2.1.1"
|
||||
@@ -4375,7 +4375,16 @@ streamsearch@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
|
||||
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -4495,7 +4504,14 @@ string_decoder@~1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -4904,7 +4920,16 @@ which@^2.0.1:
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
||||
Reference in New Issue
Block a user