Jelajahi Sumber

complete login

Abhinav 1 tahun lalu
induk
melakukan
92e493c411

+ 2 - 2
apps/photos/src/pages/index.tsx

@@ -2,7 +2,7 @@ import React, { useContext, useEffect, useState } from 'react';
 import Carousel from 'react-bootstrap/Carousel';
 import { styled, Button, Typography, TypographyProps } from '@mui/material';
 import { AppContext } from './_app';
-import Login from 'components/Login';
+import Login from '@ente/accounts/components/Login';
 import { useRouter } from 'next/router';
 import { getData, LS_KEYS } from 'utils/storage/localStorage';
 import EnteSpinner from 'components/EnteSpinner';
@@ -245,7 +245,7 @@ export default function LandingPage() {
                     <DesktopBox>
                         <SideBox>
                             {showLogin ? (
-                                <Login signUp={signUp} />
+                                <Login signUp={signUp} appName={APPS.PHOTOS} />
                             ) : (
                                 <SignUp login={login} />
                             )}

+ 8 - 1
apps/photos/src/pages/login/index.tsx

@@ -2,9 +2,16 @@ import LoginPage from '@ente/accounts/pages/login';
 import { useRouter } from 'next/router';
 import { AppContext } from 'pages/_app';
 import { useContext } from 'react';
+import { APPS } from '@ente/shared/constants/apps';
 
 export default function Login() {
     const appContext = useContext(AppContext);
     const router = useRouter();
-    return <LoginPage appContext={appContext} router={router} />;
+    return (
+        <LoginPage
+            appContext={appContext}
+            router={router}
+            appName={APPS.PHOTOS}
+        />
+    );
 }

+ 22 - 13
apps/photos/src/components/Login.tsx → packages/accounts/components/Login.tsx

@@ -1,17 +1,20 @@
 import { useRouter } from 'next/router';
-import { getSRPAttributes, sendOtt } from 'services/userService';
-import { setData, LS_KEYS } from 'utils/storage/localStorage';
-import { PAGES } from 'constants/pages';
-import FormPaperTitle from './Form/FormPaper/Title';
-import FormPaperFooter from './Form/FormPaper/Footer';
-import LinkButton from './pages/gallery/LinkButton';
+import { getSRPAttributes, sendOtt } from '../services/user';
+import { setData, LS_KEYS } from '@ente/shared/storage/localStorage';
+import { PAGES } from '../constants/pages';
+import FormPaperTitle from '@ente/shared/components/Form/FormPaper/Title';
+import FormPaperFooter from '@ente/shared/components/Form/FormPaper/Footer';
+import LinkButton from '@ente/shared/components/LinkButton';
 import { t } from 'i18next';
-import { addLocalLog } from 'utils/logging';
+// import { addLocalLog } from 'utils/logging';
 import { Input } from '@mui/material';
-import SingleInputForm, { SingleInputFormProps } from './SingleInputForm';
+import SingleInputForm, {
+    SingleInputFormProps,
+} from '@ente/shared/components/SingleInputForm';
 
 interface LoginProps {
     signUp: () => void;
+    appName: string;
 }
 
 export default function Login(props: LoginProps) {
@@ -24,18 +27,24 @@ export default function Login(props: LoginProps) {
         try {
             setData(LS_KEYS.USER, { email });
             const srpAttributes = await getSRPAttributes(email);
-            addLocalLog(
-                () => ` srpAttributes: ${JSON.stringify(srpAttributes)}`
-            );
+            // addLocalLog(
+            //     () => ` srpAttributes: ${JSON.stringify(srpAttributes)}`
+            // );
             if (!srpAttributes || srpAttributes.isEmailMFAEnabled) {
-                await sendOtt(email);
+                await sendOtt(props.appName, email);
                 router.push(PAGES.VERIFY);
             } else {
                 setData(LS_KEYS.SRP_ATTRIBUTES, srpAttributes);
                 router.push(PAGES.CREDENTIALS);
             }
         } catch (e) {
-            setFieldError(`${t('UNKNOWN_ERROR')} (reason:${e.message})`);
+            if (e instanceof Error) {
+                setFieldError(`${t('UNKNOWN_ERROR')} (reason:${e.message})`);
+            } else {
+                setFieldError(
+                    `${t('UNKNOWN_ERROR')} (reason:${JSON.stringify(e)})`
+                );
+            }
         }
     };
 

+ 1 - 2
packages/accounts/package.json

@@ -3,10 +3,9 @@
     "version": "0.0.0",
     "scripts": {
         "lint": "eslint .",
-        "build": "yarn lint"
+        "build": "yarn lint && tsc"
     },
     "dependencies": {
-        "@ente/ui": "*",
         "@ente/shared": "*"
     },
     "devDependencies": {

+ 13 - 10
packages/accounts/pages/login.tsx

@@ -1,10 +1,10 @@
 import { useState, useEffect } from 'react';
-import EnteSpinner from '@ente/ui/components/EnteSpinner';
-// import Login from 'components/Login';
-import { VerticallyCentered } from '@ente/ui/components/Container';
+import EnteSpinner from '@ente/shared/components/EnteSpinner';
+import Login from '../components/Login';
+import { VerticallyCentered } from '@ente/shared/components/Container';
 import { getData, LS_KEYS } from '@ente/shared/storage/localStorage';
-import { PAGES } from 'constants/pages';
-import FormPaper from '@ente/ui/components/Form/FormPaper';
+import { PAGES } from '../constants/pages';
+import FormPaper from '@ente/shared/components/Form/FormPaper';
 import { NextRouter } from 'next/router';
 
 interface HomeProps {
@@ -12,9 +12,10 @@ interface HomeProps {
         showNavBar: (show: boolean) => void;
     };
     router: NextRouter;
+    appName: string;
 }
 
-export default function Home({ appContext, router }: HomeProps) {
+export default function Home({ appContext, router, appName }: HomeProps) {
     const [loading, setLoading] = useState(true);
 
     useEffect(() => {
@@ -28,9 +29,9 @@ export default function Home({ appContext, router }: HomeProps) {
         appContext?.showNavBar?.(true);
     }, []);
 
-    // const register = () => {
-    //     router.push(PAGES.SIGNUP);
-    // };
+    const register = () => {
+        router.push(PAGES.SIGNUP);
+    };
 
     return loading ? (
         <VerticallyCentered>
@@ -38,7 +39,9 @@ export default function Home({ appContext, router }: HomeProps) {
         </VerticallyCentered>
     ) : (
         <VerticallyCentered>
-            <FormPaper>{/* <Login signUp={register} /> */}</FormPaper>
+            <FormPaper>
+                <Login signUp={register} appName={appName} />
+            </FormPaper>
         </VerticallyCentered>
     );
 }

+ 757 - 0
packages/accounts/services/user.ts

@@ -0,0 +1,757 @@
+// import { PAGES } from 'constants/pages';
+import {
+    getEndpoint,
+    // getFamilyPortalURL,
+    // isDevDeployment,
+} from '@ente/shared/network/api';
+// import { clearKeys } from 'utils/storage/sessionStorage';
+// import router from 'next/router';
+// import { clearData, getData, LS_KEYS } from 'utils/storage/localStorage';
+// import localForage from 'utils/storage/localForage';
+// import { getToken } from 'utils/common/key';
+import HTTPService from '@ente/shared/network/HTTPService';
+// import {
+//     computeVerifierHelper,
+//     generateLoginSubKey,
+//     generateSRPClient,
+//     getRecoveryKey,
+// } from 'utils/crypto';
+// import { logError } from 'utils/sentry';
+// import { eventBus, Events } from './events';
+import {
+    // KeyAttributes,
+    // RecoveryKey,
+    // TwoFactorSecret,
+    // TwoFactorVerificationResponse,
+    // TwoFactorRecoveryResponse,
+    // UserDetails,
+    // DeleteChallengeResponse,
+    // GetRemoteStoreValueResponse,
+    // SetupSRPRequest,
+    // CreateSRPSessionResponse,
+    // UserVerificationResponse,
+    // GetFeatureFlagResponse,
+    // SetupSRPResponse,
+    // CompleteSRPSetupRequest,
+    // CompleteSRPSetupResponse,
+    // SRPSetupAttributes,
+    SRPAttributes,
+    // UpdateSRPAndKeysRequest,
+    // UpdateSRPAndKeysResponse,
+    GetSRPAttributesResponse,
+} from '../types/user';
+// import { ApiError, CustomError } from 'utils/error';
+// import isElectron from 'is-electron';
+// import safeStorageService from './electron/safeStorage';
+// import { deleteAllCache } from 'utils/storage/cache';
+// import { B64EncryptionResult } from 'types/crypto';
+// import { getLocalFamilyData, isPartOfFamily } from 'utils/user/family';
+// import { AxiosResponse, HttpStatusCode } from 'axios';
+// import { APPS, getAppName } from 'constants/apps';
+// import { addLocalLog } from 'utils/logging';
+// import { convertBase64ToBuffer, convertBufferToBase64 } from 'utils/user';
+// import { setLocalMapEnabled } from 'utils/storage';
+// import InMemoryStore, { MS_KEYS } from './InMemoryStore';
+
+const ENDPOINT = getEndpoint();
+
+// const HAS_SET_KEYS = 'hasSetKeys';
+
+export const sendOtt = (appName: string, email: string) => {
+    // const appName = getAppName();
+    return HTTPService.post(`${ENDPOINT}/users/ott`, {
+        email,
+        client: appName,
+    });
+};
+
+// export const getPublicKey = async (email: string) => {
+//     const token = getToken();
+
+//     const resp = await HTTPService.get(
+//         `${ENDPOINT}/users/public-key`,
+//         { email },
+//         {
+//             'X-Auth-Token': token,
+//         }
+//     );
+//     return resp.data.publicKey;
+// };
+
+// export const getPaymentToken = async () => {
+//     const token = getToken();
+
+//     const resp = await HTTPService.get(
+//         `${ENDPOINT}/users/payment-token`,
+//         null,
+//         {
+//             'X-Auth-Token': token,
+//         }
+//     );
+//     return resp.data['paymentToken'];
+// };
+
+// export const getFamiliesToken = async () => {
+//     try {
+//         const token = getToken();
+
+//         const resp = await HTTPService.get(
+//             `${ENDPOINT}/users/families-token`,
+//             null,
+//             {
+//                 'X-Auth-Token': token,
+//             }
+//         );
+//         return resp.data['familiesToken'];
+//     } catch (e) {
+//         logError(e, 'failed to get family token');
+//         throw e;
+//     }
+// };
+
+// export const getRoadmapRedirectURL = async () => {
+//     try {
+//         const token = getToken();
+
+//         const resp = await HTTPService.get(
+//             `${ENDPOINT}/users/roadmap/v2`,
+//             null,
+//             {
+//                 'X-Auth-Token': token,
+//             }
+//         );
+//         return resp.data['url'];
+//     } catch (e) {
+//         logError(e, 'failed to get roadmap url');
+//         throw e;
+//     }
+// };
+
+// export const verifyOtt = (email: string, ott: string) =>
+//     HTTPService.post(`${ENDPOINT}/users/verify-email`, { email, ott });
+
+// export const putAttributes = (token: string, keyAttributes: KeyAttributes) =>
+//     HTTPService.put(`${ENDPOINT}/users/attributes`, { keyAttributes }, null, {
+//         'X-Auth-Token': token,
+//     });
+
+// export const setRecoveryKey = (token: string, recoveryKey: RecoveryKey) =>
+//     HTTPService.put(`${ENDPOINT}/users/recovery-key`, recoveryKey, null, {
+//         'X-Auth-Token': token,
+//     });
+
+// export const logoutUser = async () => {
+//     try {
+//         try {
+//             // ignore server logout result as logoutUser can be triggered before sign up or on token expiry
+//             await _logout();
+//         } catch (e) {
+//             //ignore
+//         }
+//         try {
+//             InMemoryStore.clear();
+//         } catch (e) {
+//             logError(e, 'clear InMemoryStore failed');
+//         }
+//         try {
+//             clearKeys();
+//         } catch (e) {
+//             logError(e, 'clearKeys failed');
+//         }
+//         try {
+//             clearData();
+//         } catch (e) {
+//             logError(e, 'clearData failed');
+//         }
+//         try {
+//             await deleteAllCache();
+//         } catch (e) {
+//             logError(e, 'deleteAllCache failed');
+//         }
+//         try {
+//             await clearFiles();
+//         } catch (e) {
+//             logError(e, 'clearFiles failed');
+//         }
+//         if (isElectron()) {
+//             try {
+//                 safeStorageService.clearElectronStore();
+//             } catch (e) {
+//                 logError(e, 'clearElectronStore failed');
+//             }
+//         }
+//         try {
+//             eventBus.emit(Events.LOGOUT);
+//         } catch (e) {
+//             logError(e, 'Error in logout handlers');
+//         }
+//         router.push(PAGES.ROOT);
+//     } catch (e) {
+//         logError(e, 'logoutUser failed');
+//     }
+// };
+
+// export const clearFiles = async () => {
+//     await localForage.clear();
+// };
+
+// export const isTokenValid = async (token: string) => {
+//     try {
+//         const resp = await HTTPService.get(
+//             `${ENDPOINT}/users/session-validity/v2`,
+//             null,
+//             {
+//                 'X-Auth-Token': token,
+//             }
+//         );
+//         try {
+//             if (resp.data[HAS_SET_KEYS] === undefined) {
+//                 throw Error('resp.data.hasSetKey undefined');
+//             }
+//             if (!resp.data['hasSetKeys']) {
+//                 try {
+//                     await putAttributes(
+//                         token,
+//                         getData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES)
+//                     );
+//                 } catch (e) {
+//                     logError(e, 'put attribute failed');
+//                 }
+//             }
+//         } catch (e) {
+//             logError(e, 'hasSetKeys not set in session validity response');
+//         }
+//         return true;
+//     } catch (e) {
+//         logError(e, 'session-validity api call failed');
+//         if (
+//             e instanceof ApiError &&
+//             e.httpStatusCode === HttpStatusCode.Unauthorized
+//         ) {
+//             return false;
+//         } else {
+//             return true;
+//         }
+//     }
+// };
+
+// export const setupTwoFactor = async () => {
+//     const resp = await HTTPService.post(
+//         `${ENDPOINT}/users/two-factor/setup`,
+//         null,
+//         null,
+//         {
+//             'X-Auth-Token': getToken(),
+//         }
+//     );
+//     return resp.data as TwoFactorSecret;
+// };
+
+// export const enableTwoFactor = async (
+//     code: string,
+//     recoveryEncryptedTwoFactorSecret: B64EncryptionResult
+// ) => {
+//     await HTTPService.post(
+//         `${ENDPOINT}/users/two-factor/enable`,
+//         {
+//             code,
+//             encryptedTwoFactorSecret:
+//                 recoveryEncryptedTwoFactorSecret.encryptedData,
+//             twoFactorSecretDecryptionNonce:
+//                 recoveryEncryptedTwoFactorSecret.nonce,
+//         },
+//         null,
+//         {
+//             'X-Auth-Token': getToken(),
+//         }
+//     );
+// };
+
+// export const verifyTwoFactor = async (code: string, sessionID: string) => {
+//     const resp = await HTTPService.post(
+//         `${ENDPOINT}/users/two-factor/verify`,
+//         {
+//             code,
+//             sessionID,
+//         },
+//         null
+//     );
+//     return resp.data as TwoFactorVerificationResponse;
+// };
+
+// export const recoverTwoFactor = async (sessionID: string) => {
+//     const resp = await HTTPService.get(`${ENDPOINT}/users/two-factor/recover`, {
+//         sessionID,
+//     });
+//     return resp.data as TwoFactorRecoveryResponse;
+// };
+
+// export const removeTwoFactor = async (sessionID: string, secret: string) => {
+//     const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/remove`, {
+//         sessionID,
+//         secret,
+//     });
+//     return resp.data as TwoFactorVerificationResponse;
+// };
+
+// export const disableTwoFactor = async () => {
+//     await HTTPService.post(`${ENDPOINT}/users/two-factor/disable`, null, null, {
+//         'X-Auth-Token': getToken(),
+//     });
+// };
+
+// export const getTwoFactorStatus = async () => {
+//     const resp = await HTTPService.get(
+//         `${ENDPOINT}/users/two-factor/status`,
+//         null,
+//         {
+//             'X-Auth-Token': getToken(),
+//         }
+//     );
+//     return resp.data['status'];
+// };
+
+// export const _logout = async () => {
+//     if (!getToken()) return true;
+//     try {
+//         await HTTPService.post(`${ENDPOINT}/users/logout`, null, null, {
+//             'X-Auth-Token': getToken(),
+//         });
+//         return true;
+//     } catch (e) {
+//         logError(e, '/users/logout failed');
+//         return false;
+//     }
+// };
+
+// export const sendOTTForEmailChange = async (email: string) => {
+//     if (!getToken()) {
+//         return null;
+//     }
+//     await HTTPService.post(`${ENDPOINT}/users/ott`, {
+//         email,
+//         client: 'web',
+//         purpose: 'change',
+//     });
+// };
+
+// export const changeEmail = async (email: string, ott: string) => {
+//     if (!getToken()) {
+//         return null;
+//     }
+//     await HTTPService.post(
+//         `${ENDPOINT}/users/change-email`,
+//         {
+//             email,
+//             ott,
+//         },
+//         null,
+//         {
+//             'X-Auth-Token': getToken(),
+//         }
+//     );
+// };
+
+// export const getUserDetailsV2 = async (): Promise<UserDetails> => {
+//     try {
+//         const token = getToken();
+
+//         const resp = await HTTPService.get(
+//             `${ENDPOINT}/users/details/v2`,
+//             null,
+//             {
+//                 'X-Auth-Token': token,
+//             }
+//         );
+//         return resp.data;
+//     } catch (e) {
+//         logError(e, 'failed to get user details v2');
+//         throw e;
+//     }
+// };
+
+// export const getFamilyPortalRedirectURL = async () => {
+//     try {
+//         const jwtToken = await getFamiliesToken();
+//         const isFamilyCreated = isPartOfFamily(getLocalFamilyData());
+//         return `${getFamilyPortalURL()}?token=${jwtToken}&isFamilyCreated=${isFamilyCreated}&redirectURL=${
+//             window.location.origin
+//         }/gallery`;
+//     } catch (e) {
+//         logError(e, 'unable to generate to family portal URL');
+//         throw e;
+//     }
+// };
+
+// export const getAccountDeleteChallenge = async () => {
+//     try {
+//         const token = getToken();
+
+//         const resp = await HTTPService.get(
+//             `${ENDPOINT}/users/delete-challenge`,
+//             null,
+//             {
+//                 'X-Auth-Token': token,
+//             }
+//         );
+//         return resp.data as DeleteChallengeResponse;
+//     } catch (e) {
+//         logError(e, 'failed to get account delete challenge');
+//         throw e;
+//     }
+// };
+
+// export const deleteAccount = async (
+//     challenge: string,
+//     reason: string,
+//     feedback: string
+// ) => {
+//     try {
+//         const token = getToken();
+//         if (!token) {
+//             return;
+//         }
+
+//         await HTTPService.delete(
+//             `${ENDPOINT}/users/delete`,
+//             { challenge, reason, feedback },
+//             null,
+//             {
+//                 'X-Auth-Token': token,
+//             }
+//         );
+//     } catch (e) {
+//         logError(e, 'deleteAccount api call failed');
+//         throw e;
+//     }
+// };
+
+// // Ensure that the keys in local storage are not malformed by verifying that the
+// // recoveryKey can be decrypted with the masterKey.
+// // Note: This is not bullet-proof.
+// export const validateKey = async () => {
+//     try {
+//         await getRecoveryKey();
+//         return true;
+//     } catch (e) {
+//         await logoutUser();
+//         return false;
+//     }
+// };
+
+// export const getFaceSearchEnabledStatus = async () => {
+//     try {
+//         const token = getToken();
+//         const resp: AxiosResponse<GetRemoteStoreValueResponse> =
+//             await HTTPService.get(
+//                 `${ENDPOINT}/remote-store`,
+//                 {
+//                     key: 'faceSearchEnabled',
+//                     defaultValue: false,
+//                 },
+//                 {
+//                     'X-Auth-Token': token,
+//                 }
+//             );
+//         return resp.data.value === 'true';
+//     } catch (e) {
+//         logError(e, 'failed to get face search enabled status');
+//         throw e;
+//     }
+// };
+
+// export const updateFaceSearchEnabledStatus = async (newStatus: boolean) => {
+//     try {
+//         const token = getToken();
+//         await HTTPService.post(
+//             `${ENDPOINT}/remote-store/update`,
+//             {
+//                 key: 'faceSearchEnabled',
+//                 value: newStatus.toString(),
+//             },
+//             null,
+//             {
+//                 'X-Auth-Token': token,
+//             }
+//         );
+//     } catch (e) {
+//         logError(e, 'failed to update face search enabled status');
+//         throw e;
+//     }
+// };
+
+// export const syncMapEnabled = async () => {
+//     try {
+//         const status = await getMapEnabledStatus();
+//         setLocalMapEnabled(status);
+//     } catch (e) {
+//         logError(e, 'failed to sync map enabled status');
+//         throw e;
+//     }
+// };
+
+// export const getMapEnabledStatus = async () => {
+//     try {
+//         const token = getToken();
+//         const resp: AxiosResponse<GetRemoteStoreValueResponse> =
+//             await HTTPService.get(
+//                 `${ENDPOINT}/remote-store`,
+//                 {
+//                     key: 'mapEnabled',
+//                     defaultValue: false,
+//                 },
+//                 {
+//                     'X-Auth-Token': token,
+//                 }
+//             );
+//         return resp.data.value === 'true';
+//     } catch (e) {
+//         logError(e, 'failed to get map enabled status');
+//         throw e;
+//     }
+// };
+
+// export const updateMapEnabledStatus = async (newStatus: boolean) => {
+//     try {
+//         const token = getToken();
+//         await HTTPService.post(
+//             `${ENDPOINT}/remote-store/update`,
+//             {
+//                 key: 'mapEnabled',
+//                 value: newStatus.toString(),
+//             },
+//             null,
+//             {
+//                 'X-Auth-Token': token,
+//             }
+//         );
+//     } catch (e) {
+//         logError(e, 'failed to update map enabled status');
+//         throw e;
+//     }
+// };
+
+// export async function getDisableCFUploadProxyFlag(): Promise<boolean> {
+//     try {
+//         const disableCFUploadProxy =
+//             process.env.NEXT_PUBLIC_DISABLE_CF_UPLOAD_PROXY;
+//         if (isDevDeployment() && typeof disableCFUploadProxy !== 'undefined') {
+//             return disableCFUploadProxy === 'true';
+//         }
+//         const featureFlags = (
+//             await fetch('https://static.ente.io/feature_flags.json')
+//         ).json() as GetFeatureFlagResponse;
+//         return featureFlags.disableCFUploadProxy;
+//     } catch (e) {
+//         logError(e, 'failed to get feature flags');
+//         return false;
+//     }
+// }
+
+export const getSRPAttributes = async (
+    email: string
+): Promise<SRPAttributes | null> => {
+    try {
+        const resp = await HTTPService.get(`${ENDPOINT}/users/srp/attributes`, {
+            email,
+        });
+        return (resp.data as GetSRPAttributesResponse).attributes;
+    } catch (e) {
+        // logError(e, 'failed to get SRP attributes');
+        return null;
+    }
+};
+
+// export const configureSRP = async ({
+//     srpSalt,
+//     srpUserID,
+//     srpVerifier,
+//     loginSubKey,
+// }: SRPSetupAttributes) => {
+//     try {
+//         const srpConfigureInProgress = InMemoryStore.get(
+//             MS_KEYS.SRP_CONFIGURE_IN_PROGRESS
+//         );
+//         if (srpConfigureInProgress) {
+//             throw Error('SRP configure already in progress');
+//         }
+//         InMemoryStore.set(MS_KEYS.SRP_CONFIGURE_IN_PROGRESS, true);
+//         const srpClient = await generateSRPClient(
+//             srpSalt,
+//             srpUserID,
+//             loginSubKey
+//         );
+
+//         const srpA = convertBufferToBase64(srpClient.computeA());
+
+//         addLocalLog(() => `srp a: ${srpA}`);
+//         const token = getToken();
+//         const { setupID, srpB } = await startSRPSetup(token, {
+//             srpA,
+//             srpUserID,
+//             srpSalt,
+//             srpVerifier,
+//         });
+
+//         srpClient.setB(convertBase64ToBuffer(srpB));
+
+//         const srpM1 = convertBufferToBase64(srpClient.computeM1());
+
+//         const { srpM2 } = await completeSRPSetup(token, {
+//             srpM1,
+//             setupID,
+//         });
+
+//         srpClient.checkM2(convertBase64ToBuffer(srpM2));
+//     } catch (e) {
+//         logError(e, 'srp configure failed');
+//         throw e;
+//     } finally {
+//         InMemoryStore.set(MS_KEYS.SRP_CONFIGURE_IN_PROGRESS, false);
+//     }
+// };
+
+// export const startSRPSetup = async (
+//     token: string,
+//     setupSRPRequest: SetupSRPRequest
+// ): Promise<SetupSRPResponse> => {
+//     try {
+//         const resp = await HTTPService.post(
+//             `${ENDPOINT}/users/srp/setup`,
+//             setupSRPRequest,
+//             null,
+//             {
+//                 'X-Auth-Token': token,
+//             }
+//         );
+
+//         return resp.data as SetupSRPResponse;
+//     } catch (e) {
+//         logError(e, 'failed to post SRP attributes');
+//         throw e;
+//     }
+// };
+
+// export const completeSRPSetup = async (
+//     token: string,
+//     completeSRPSetupRequest: CompleteSRPSetupRequest
+// ) => {
+//     try {
+//         const resp = await HTTPService.post(
+//             `${ENDPOINT}/users/srp/complete`,
+//             completeSRPSetupRequest,
+//             null,
+//             {
+//                 'X-Auth-Token': token,
+//             }
+//         );
+//         return resp.data as CompleteSRPSetupResponse;
+//     } catch (e) {
+//         logError(e, 'failed to complete SRP setup');
+//         throw e;
+//     }
+// };
+
+// export const loginViaSRP = async (
+//     srpAttributes: SRPAttributes,
+//     kek: string
+// ): Promise<UserVerificationResponse> => {
+//     try {
+//         const loginSubKey = await generateLoginSubKey(kek);
+//         const srpClient = await generateSRPClient(
+//             srpAttributes.srpSalt,
+//             srpAttributes.srpUserID,
+//             loginSubKey
+//         );
+//         const srpVerifier = computeVerifierHelper(
+//             srpAttributes.srpSalt,
+//             srpAttributes.srpUserID,
+//             loginSubKey
+//         );
+//         addLocalLog(() => `srp verifier: ${srpVerifier}`);
+//         const srpA = srpClient.computeA();
+//         const { srpB, sessionID } = await createSRPSession(
+//             srpAttributes.srpUserID,
+//             convertBufferToBase64(srpA)
+//         );
+//         srpClient.setB(convertBase64ToBuffer(srpB));
+
+//         const m1 = srpClient.computeM1();
+//         addLocalLog(() => `srp m1: ${convertBufferToBase64(m1)}`);
+//         const { srpM2, ...rest } = await verifySRPSession(
+//             sessionID,
+//             srpAttributes.srpUserID,
+//             convertBufferToBase64(m1)
+//         );
+//         addLocalLog(() => `srp verify session successful,srpM2: ${srpM2}`);
+
+//         srpClient.checkM2(convertBase64ToBuffer(srpM2));
+
+//         addLocalLog(() => `srp server verify successful`);
+
+//         return rest;
+//     } catch (e) {
+//         logError(e, 'srp verify failed');
+//         throw e;
+//     }
+// };
+
+// export const createSRPSession = async (srpUserID: string, srpA: string) => {
+//     try {
+//         const resp = await HTTPService.post(
+//             `${ENDPOINT}/users/srp/create-session`,
+//             {
+//                 srpUserID,
+//                 srpA,
+//             }
+//         );
+//         return resp.data as CreateSRPSessionResponse;
+//     } catch (e) {
+//         logError(e, 'createSRPSession failed');
+//         throw e;
+//     }
+// };
+
+// export const verifySRPSession = async (
+//     sessionID: string,
+//     srpUserID: string,
+//     srpM1: string
+// ) => {
+//     try {
+//         const resp = await HTTPService.post(
+//             `${ENDPOINT}/users/srp/verify-session`,
+//             {
+//                 sessionID,
+//                 srpUserID,
+//                 srpM1,
+//             },
+//             null
+//         );
+//         return resp.data as UserVerificationResponse;
+//     } catch (e) {
+//         logError(e, 'verifySRPSession failed');
+//         if (
+//             e instanceof ApiError &&
+//             e.httpStatusCode === HttpStatusCode.Forbidden
+//         ) {
+//             throw Error(CustomError.INCORRECT_PASSWORD);
+//         } else {
+//             throw e;
+//         }
+//     }
+// };
+
+// export const updateSRPAndKeys = async (
+//     token: string,
+//     updateSRPAndKeyRequest: UpdateSRPAndKeysRequest
+// ): Promise<UpdateSRPAndKeysResponse> => {
+//     const resp = await HTTPService.post(
+//         `${ENDPOINT}/users/srp/update`,
+//         updateSRPAndKeyRequest,
+//         null,
+//         {
+//             'X-Auth-Token': token,
+//         }
+//     );
+//     return resp.data as UpdateSRPAndKeysResponse;
+// };

+ 6 - 1
packages/accounts/tsconfig.json

@@ -1,4 +1,9 @@
 {
     "extends": "../../tsconfig.base.json",
-    "compilerOptions": { "baseUrl": "." }
+    "include": [
+        "**/*.ts",
+        "**/*.tsx",
+        "**/*.js",
+        "../mui-config/mui-theme.d.ts"
+    ]
 }

+ 12 - 0
packages/accounts/types/user.ts

@@ -0,0 +1,12 @@
+export interface SRPAttributes {
+    srpUserID: string;
+    srpSalt: string;
+    memLimit: number;
+    opsLimit: number;
+    kekSalt: string;
+    isEmailMFAEnabled: boolean;
+}
+
+export interface GetSRPAttributesResponse {
+    attributes: SRPAttributes;
+}

+ 0 - 0
packages/shared/components/Container.ts → packages/shared/components/Container.tsx


+ 1 - 1
packages/shared/components/Form/FormPaper/Footer.tsx

@@ -1,6 +1,6 @@
 import { FC } from 'react';
 import { BoxProps, Divider } from '@mui/material';
-import { VerticallyCentered } from 'components/Container';
+import { VerticallyCentered } from '../../Container';
 
 const FormPaperFooter: FC<BoxProps> = ({ sx, style, ...props }) => {
     return (

+ 36 - 0
packages/shared/components/LinkButton.tsx

@@ -0,0 +1,36 @@
+import { ButtonProps, Link, LinkProps } from '@mui/material';
+import React, { FC } from 'react';
+
+export type LinkButtonProps = React.PropsWithChildren<{
+    onClick: () => void;
+    variant?: string;
+    style?: React.CSSProperties;
+}>;
+
+const LinkButton: FC<LinkProps<'button', { color?: ButtonProps['color'] }>> = ({
+    children,
+    sx,
+    color,
+    ...props
+}) => {
+    return (
+        <Link
+            component="button"
+            sx={{
+                color: 'text.base',
+                textDecoration: 'underline rgba(255, 255, 255, 0.4)',
+                paddingBottom: 0.5,
+                '&:hover': {
+                    color: `${color}.main`,
+                    textDecoration: `underline `,
+                    textDecorationColor: `${color}.main`,
+                },
+                ...sx,
+            }}
+            {...props}>
+            {children}
+        </Link>
+    );
+};
+
+export default LinkButton;

+ 179 - 0
packages/shared/components/SingleInputForm.tsx

@@ -0,0 +1,179 @@
+import React, { useMemo, useState } from 'react';
+import { Formik, FormikHelpers, FormikState } from 'formik';
+import * as Yup from 'yup';
+import SubmitButton from './SubmitButton';
+import TextField from '@mui/material/TextField';
+import ShowHidePassword from './Form/ShowHidePassword';
+import { FlexWrapper } from './Container';
+import { Button, FormHelperText } from '@mui/material';
+import { t } from 'i18next';
+
+interface formValues {
+    inputValue: string;
+}
+export interface SingleInputFormProps {
+    callback: (
+        inputValue: string,
+        setFieldError: (errorMessage: string) => void,
+        resetForm: (nextState?: Partial<FormikState<formValues>>) => void
+    ) => Promise<void>;
+    fieldType: 'text' | 'email' | 'password';
+    placeholder: string;
+    buttonText: string;
+    submitButtonProps?: any;
+    initialValue?: string;
+    secondaryButtonAction?: () => void;
+    disableAutoFocus?: boolean;
+    hiddenPreInput?: any;
+    caption?: any;
+    hiddenPostInput?: any;
+    autoComplete?: string;
+    blockButton?: boolean;
+    hiddenLabel?: boolean;
+    disableAutoComplete?: boolean;
+}
+
+export default function SingleInputForm(props: SingleInputFormProps) {
+    const { submitButtonProps } = props;
+    const { sx: buttonSx, ...restSubmitButtonProps } = submitButtonProps ?? {};
+
+    const [loading, SetLoading] = useState(false);
+    const [showPassword, setShowPassword] = useState(false);
+
+    const submitForm = async (
+        values: formValues,
+        { setFieldError, resetForm }: FormikHelpers<formValues>
+    ) => {
+        SetLoading(true);
+        await props.callback(
+            values.inputValue,
+            (message) => setFieldError('inputValue', message),
+            resetForm
+        );
+        SetLoading(false);
+    };
+
+    const handleClickShowPassword = () => {
+        setShowPassword(!showPassword);
+    };
+
+    const handleMouseDownPassword = (
+        event: React.MouseEvent<HTMLButtonElement>
+    ) => {
+        event.preventDefault();
+    };
+
+    const validationSchema = useMemo(() => {
+        switch (props.fieldType) {
+            case 'text':
+                return Yup.object().shape({
+                    inputValue: Yup.string().required(t('REQUIRED')),
+                });
+            case 'password':
+                return Yup.object().shape({
+                    inputValue: Yup.string().required(t('REQUIRED')),
+                });
+            case 'email':
+                return Yup.object().shape({
+                    inputValue: Yup.string()
+                        .email(t('EMAIL_ERROR'))
+                        .required(t('REQUIRED')),
+                });
+        }
+    }, [props.fieldType]);
+
+    return (
+        <Formik<formValues>
+            initialValues={{ inputValue: props.initialValue ?? '' }}
+            onSubmit={submitForm}
+            validationSchema={validationSchema}
+            validateOnChange={false}
+            validateOnBlur={false}>
+            {({ values, errors, handleChange, handleSubmit }) => (
+                <form noValidate onSubmit={handleSubmit}>
+                    {props.hiddenPreInput}
+                    <TextField
+                        hiddenLabel={props.hiddenLabel}
+                        variant="filled"
+                        fullWidth
+                        type={showPassword ? 'text' : props.fieldType}
+                        id={props.fieldType}
+                        name={props.fieldType}
+                        {...(props.hiddenLabel
+                            ? { placeholder: props.placeholder }
+                            : { label: props.placeholder })}
+                        value={values.inputValue}
+                        onChange={handleChange('inputValue')}
+                        error={Boolean(errors.inputValue)}
+                        helperText={errors.inputValue}
+                        disabled={loading}
+                        autoFocus={!props.disableAutoFocus}
+                        autoComplete={props.autoComplete}
+                        InputProps={{
+                            autoComplete:
+                                props.disableAutoComplete ||
+                                props.fieldType === 'password'
+                                    ? 'off'
+                                    : 'on',
+                            endAdornment: props.fieldType === 'password' && (
+                                <ShowHidePassword
+                                    showPassword={showPassword}
+                                    handleClickShowPassword={
+                                        handleClickShowPassword
+                                    }
+                                    handleMouseDownPassword={
+                                        handleMouseDownPassword
+                                    }
+                                />
+                            ),
+                        }}
+                    />
+                    <FormHelperText
+                        sx={{
+                            position: 'relative',
+                            top: errors.inputValue ? '-22px' : '0',
+                            float: 'right',
+                            padding: '0 8px',
+                        }}>
+                        {props.caption}
+                    </FormHelperText>
+                    {props.hiddenPostInput}
+                    <FlexWrapper
+                        justifyContent={'flex-end'}
+                        flexWrap={
+                            props.blockButton ? 'wrap-reverse' : 'nowrap'
+                        }>
+                        {props.secondaryButtonAction && (
+                            <Button
+                                onClick={props.secondaryButtonAction}
+                                size="large"
+                                color="secondary"
+                                sx={{
+                                    '&&&': {
+                                        mt: !props.blockButton ? 2 : 0.5,
+                                        mb: !props.blockButton ? 4 : 0,
+                                        mr: !props.blockButton ? 1 : 0,
+                                        ...buttonSx,
+                                    },
+                                }}
+                                {...restSubmitButtonProps}>
+                                {t('CANCEL')}
+                            </Button>
+                        )}
+                        <SubmitButton
+                            sx={{
+                                '&&&': {
+                                    mt: 2,
+                                    ...buttonSx,
+                                },
+                            }}
+                            buttonText={props.buttonText}
+                            loading={loading}
+                            {...restSubmitButtonProps}
+                        />
+                    </FlexWrapper>
+                </form>
+            )}
+        </Formik>
+    );
+}

+ 52 - 0
packages/shared/components/SubmitButton.tsx

@@ -0,0 +1,52 @@
+import Done from '@mui/icons-material/Done';
+import { Button, ButtonProps, CircularProgress } from '@mui/material';
+import { FC } from 'react';
+
+export interface SubmitButtonProps {
+    loading: boolean;
+    buttonText: string;
+
+    disabled?: boolean;
+    success?: boolean;
+}
+const SubmitButton: FC<ButtonProps<'button', SubmitButtonProps>> = ({
+    loading,
+    buttonText,
+    disabled,
+    success,
+    sx,
+    ...props
+}) => {
+    return (
+        <Button
+            size="large"
+            variant="contained"
+            color="accent"
+            type="submit"
+            disabled={disabled || loading || success}
+            sx={{
+                my: 4,
+                ...(loading
+                    ? {
+                          '&.Mui-disabled': {
+                              backgroundColor: (theme) =>
+                                  theme.colors.accent.A500,
+                              color: (theme) => theme.colors.text.base,
+                          },
+                      }
+                    : {}),
+                ...sx,
+            }}
+            {...props}>
+            {loading ? (
+                <CircularProgress size={20} />
+            ) : success ? (
+                <Done sx={{ fontSize: 20 }} />
+            ) : (
+                buttonText
+            )}
+        </Button>
+    );
+};
+
+export default SubmitButton;

+ 59 - 0
packages/shared/constants/apps.ts

@@ -0,0 +1,59 @@
+// import { PAGES } from 'constants/pages';
+// import { runningInBrowser } from 'utils/common';
+// import { getAlbumsURL, getAuthURL } from 'utils/common/apiUtil';
+
+export enum APPS {
+    PHOTOS = 'PHOTOS',
+    AUTH = 'AUTH',
+    ALBUMS = 'ALBUMS',
+}
+
+// export const ALLOWED_APP_PAGES = new Map([
+//     [APPS.ALBUMS, [PAGES.SHARED_ALBUMS, PAGES.ROOT]],
+//     [
+//         APPS.AUTH,
+//         [
+//             PAGES.ROOT,
+//             PAGES.LOGIN,
+//             PAGES.SIGNUP,
+//             PAGES.VERIFY,
+//             PAGES.CREDENTIALS,
+//             PAGES.RECOVER,
+//             PAGES.CHANGE_PASSWORD,
+//             PAGES.GENERATE,
+//             PAGES.AUTH,
+//             PAGES.TWO_FACTOR_VERIFY,
+//             PAGES.TWO_FACTOR_RECOVER,
+//         ],
+//     ],
+// ]);
+
+// export const CLIENT_PACKAGE_NAMES = new Map([
+//     [APPS.ALBUMS, 'io.ente.albums.web'],
+//     [APPS.PHOTOS, 'io.ente.photos.web'],
+//     [APPS.AUTH, 'io.ente.auth.web'],
+// ]);
+
+// export const getAppNameAndTitle = () => {
+//     if (!runningInBrowser()) {
+//         return {};
+//     }
+//     const currentURL = new URL(window.location.href);
+//     const albumsURL = new URL(getAlbumsURL());
+//     const authURL = new URL(getAuthURL());
+//     if (currentURL.origin === albumsURL.origin) {
+//         return { name: APPS.ALBUMS, title: 'ente Photos' };
+//     } else if (currentURL.origin === authURL.origin) {
+//         return { name: APPS.AUTH, title: 'ente Auth' };
+//     } else {
+//         return { name: APPS.PHOTOS, title: 'ente Photos' };
+//     }
+// };
+
+// export const getAppTitle = () => {
+//     return getAppNameAndTitle().title;
+// };
+
+// export const getAppName = () => {
+//     return getAppNameAndTitle().name;
+// };

+ 164 - 0
packages/shared/error/index.ts

@@ -0,0 +1,164 @@
+import { HttpStatusCode } from 'axios';
+
+export interface ApiErrorResponse {
+    code: string;
+    message: string;
+}
+
+export class ApiError extends Error {
+    httpStatusCode: number;
+    errCode: string;
+
+    constructor(message: string, errCode: string, httpStatus: number) {
+        super(message);
+        this.name = 'ApiError';
+        this.errCode = errCode;
+        this.httpStatusCode = httpStatus;
+    }
+}
+
+export function isApiErrorResponse(object: any): object is ApiErrorResponse {
+    return object && 'code' in object && 'message' in object;
+}
+
+export const CustomError = {
+    THUMBNAIL_GENERATION_FAILED: 'thumbnail generation failed',
+    VIDEO_PLAYBACK_FAILED: 'video playback failed',
+    ETAG_MISSING: 'no header/etag present in response body',
+    KEY_MISSING: 'encrypted key missing from localStorage',
+    FAILED_TO_LOAD_WEB_WORKER: 'failed to load web worker',
+    CHUNK_MORE_THAN_EXPECTED: 'chunks more than expected',
+    CHUNK_LESS_THAN_EXPECTED: 'chunks less than expected',
+    UNSUPPORTED_FILE_FORMAT: 'unsupported file format',
+    FILE_TOO_LARGE: 'file too large',
+    SUBSCRIPTION_EXPIRED: 'subscription expired',
+    STORAGE_QUOTA_EXCEEDED: 'storage quota exceeded',
+    SESSION_EXPIRED: 'session expired',
+    INVALID_MIME_TYPE: (type: string) => `invalid mime type -${type}`,
+    SIGNUP_FAILED: 'signup failed',
+    FAV_COLLECTION_MISSING: 'favorite collection missing',
+    INVALID_COLLECTION_OPERATION: 'invalid collection operation',
+    TO_MOVE_FILES_FROM_MULTIPLE_COLLECTIONS:
+        'to move files from multiple collections',
+    WAIT_TIME_EXCEEDED: 'operation wait time exceeded',
+    REQUEST_CANCELLED: 'request canceled',
+    REQUEST_FAILED: 'request failed',
+    TOKEN_EXPIRED: 'token expired',
+    TOKEN_MISSING: 'token missing',
+    TOO_MANY_REQUESTS: 'too many requests',
+    BAD_REQUEST: 'bad request',
+    SUBSCRIPTION_NEEDED: 'subscription not present',
+    NOT_FOUND: 'not found ',
+    NO_METADATA: 'no metadata',
+    TOO_LARGE_LIVE_PHOTO_ASSETS: 'too large live photo assets',
+    NOT_A_DATE: 'not a date',
+    NOT_A_LOCATION: 'not a location',
+    FILE_ID_NOT_FOUND: 'file with id not found',
+    WEAK_DEVICE: 'password decryption failed on the device',
+    INCORRECT_PASSWORD: 'incorrect password',
+    UPLOAD_CANCELLED: 'upload cancelled',
+    REQUEST_TIMEOUT: 'request taking too long',
+    HIDDEN_COLLECTION_SYNC_FILE_ATTEMPTED:
+        'hidden collection sync file attempted',
+    UNKNOWN_ERROR: 'Something went wrong, please try again',
+    TYPE_DETECTION_FAILED: (fileFormat: string) =>
+        `type detection failed ${fileFormat}`,
+    WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED:
+        'Windows native image processing is not supported',
+    NETWORK_ERROR: 'Network Error',
+    NOT_FILE_OWNER: 'not file owner',
+    UPDATE_EXPORTED_RECORD_FAILED: 'update file exported record failed',
+    EXPORT_STOPPED: 'export stopped',
+    NO_EXPORT_FOLDER_SELECTED: 'no export folder selected',
+    EXPORT_FOLDER_DOES_NOT_EXIST: 'export folder does not exist',
+    NO_INTERNET_CONNECTION: 'no internet connection',
+    AUTH_KEY_NOT_FOUND: 'auth key not found',
+    EXIF_DATA_NOT_FOUND: 'exif data not found',
+    SELECT_FOLDER_ABORTED: 'select folder aborted',
+    NON_MEDIA_FILE: 'non media file',
+    NOT_AVAILABLE_ON_WEB: 'not available on web',
+    UNSUPPORTED_RAW_FORMAT: 'unsupported raw format',
+    NON_PREVIEWABLE_FILE: 'non previewable file',
+    PROCESSING_FAILED: 'processing failed',
+    EXPORT_RECORD_JSON_PARSING_FAILED: 'export record json parsing failed',
+    TWO_FACTOR_ENABLED: 'two factor enabled',
+    CLIENT_ERROR: 'client error',
+    ServerError: 'server error',
+};
+
+export function handleUploadError(error: any): Error {
+    const parsedError = parseUploadErrorCodes(error);
+
+    // breaking errors
+    switch (parsedError.message) {
+        case CustomError.SUBSCRIPTION_EXPIRED:
+        case CustomError.STORAGE_QUOTA_EXCEEDED:
+        case CustomError.SESSION_EXPIRED:
+        case CustomError.UPLOAD_CANCELLED:
+            throw parsedError;
+    }
+    return parsedError;
+}
+
+export function errorWithContext(originalError: Error, context: string) {
+    const errorWithContext = new Error(context);
+    errorWithContext.stack =
+        errorWithContext.stack?.split('\n').slice(2, 4).join('\n') +
+        '\n' +
+        originalError.stack;
+    return errorWithContext;
+}
+
+export function parseUploadErrorCodes(error: any) {
+    let parsedMessage = null;
+    if (error instanceof ApiError) {
+        switch (error.httpStatusCode) {
+            case HttpStatusCode.PaymentRequired:
+                parsedMessage = CustomError.SUBSCRIPTION_EXPIRED;
+                break;
+            case HttpStatusCode.UpgradeRequired:
+                parsedMessage = CustomError.STORAGE_QUOTA_EXCEEDED;
+                break;
+            case HttpStatusCode.Unauthorized:
+                parsedMessage = CustomError.SESSION_EXPIRED;
+                break;
+            case HttpStatusCode.PayloadTooLarge:
+                parsedMessage = CustomError.FILE_TOO_LARGE;
+                break;
+            default:
+                parsedMessage = `${CustomError.UNKNOWN_ERROR} statusCode:${error.httpStatusCode}`;
+        }
+    } else {
+        parsedMessage = error.message;
+    }
+    return new Error(parsedMessage);
+}
+
+export const parseSharingErrorCodes = (error: any) => {
+    let parsedMessage = null;
+    if (error instanceof ApiError) {
+        switch (error.httpStatusCode) {
+            case HttpStatusCode.BadRequest:
+                parsedMessage = CustomError.BAD_REQUEST;
+                break;
+            case HttpStatusCode.PaymentRequired:
+                parsedMessage = CustomError.SUBSCRIPTION_NEEDED;
+                break;
+            case HttpStatusCode.NotFound:
+                parsedMessage = CustomError.NOT_FOUND;
+                break;
+            case HttpStatusCode.Unauthorized:
+            case HttpStatusCode.Gone:
+                parsedMessage = CustomError.TOKEN_EXPIRED;
+                break;
+            case HttpStatusCode.TooManyRequests:
+                parsedMessage = CustomError.TOO_MANY_REQUESTS;
+                break;
+            default:
+                parsedMessage = `${CustomError.UNKNOWN_ERROR} statusCode:${error.httpStatusCode}`;
+        }
+    } else {
+        parsedMessage = error.message;
+    }
+    return new Error(parsedMessage);
+};

+ 241 - 0
packages/shared/network/HTTPService.ts

@@ -0,0 +1,241 @@
+import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
+// import { addLogLine } from 'utils/logging';
+// import { logError } from 'utils/sentry';
+import { ApiError, CustomError, isApiErrorResponse } from '../error';
+
+interface IHTTPHeaders {
+    [headerKey: string]: any;
+}
+
+interface IQueryPrams {
+    [paramName: string]: any;
+}
+
+/**
+ * Service to manage all HTTP calls.
+ */
+class HTTPService {
+    constructor() {
+        axios.interceptors.response.use(
+            (response) => Promise.resolve(response),
+            (error) => {
+                // const config = error.config as AxiosRequestConfig;
+                if (error.response) {
+                    const response = error.response as AxiosResponse;
+                    let apiError: ApiError;
+                    // The request was made and the server responded with a status code
+                    // that falls out of the range of 2xx
+                    if (isApiErrorResponse(response.data)) {
+                        const responseData = response.data;
+                        // logError(error, 'HTTP Service Error', {
+                        //     url: config.url,
+                        //     method: config.method,
+                        //     xRequestId: response.headers['x-request-id'],
+                        //     httpStatus: response.status,
+                        //     errMessage: responseData.message,
+                        //     errCode: responseData.code,
+                        // });
+                        apiError = new ApiError(
+                            responseData.message,
+                            responseData.code,
+                            response.status
+                        );
+                    } else {
+                        if (response.status >= 400 && response.status < 500) {
+                            apiError = new ApiError(
+                                CustomError.CLIENT_ERROR,
+                                '',
+                                response.status
+                            );
+                        } else {
+                            apiError = new ApiError(
+                                CustomError.ServerError,
+                                '',
+                                response.status
+                            );
+                        }
+                    }
+                    // logError(apiError, 'HTTP Service Error', {
+                    //     url: config.url,
+                    //     method: config.method,
+                    //     cfRay: response.headers['cf-ray'],
+                    //     xRequestId: response.headers['x-request-id'],
+                    //     httpStatus: response.status,
+                    // });
+                    throw apiError;
+                } else if (error.request) {
+                    // The request was made but no response was received
+                    // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
+                    // http.ClientRequest in node.js
+                    // addLogLine(
+                    //     'request failed- no response',
+                    //     `url: ${config.url}`,
+                    //     `method: ${config.method}`
+                    // );
+                    return Promise.reject(error);
+                } else {
+                    // Something happened in setting up the request that triggered an Error
+                    // addLogLine(
+                    //     'request failed- axios error',
+                    //     `url: ${config.url}`,
+                    //     `method: ${config.method}`
+                    // );
+                    return Promise.reject(error);
+                }
+            }
+        );
+    }
+
+    /**
+     * header object to be append to all api calls.
+     */
+    private headers: IHTTPHeaders = {
+        'content-type': 'application/json',
+    };
+
+    /**
+     * Sets the headers to the given object.
+     */
+    public setHeaders(headers: IHTTPHeaders) {
+        this.headers = {
+            ...this.headers,
+            ...headers,
+        };
+    }
+
+    /**
+     * Adds a header to list of headers.
+     */
+    public appendHeader(key: string, value: string) {
+        this.headers = {
+            ...this.headers,
+            [key]: value,
+        };
+    }
+
+    /**
+     * Removes the given header.
+     */
+    public removeHeader(key: string) {
+        this.headers[key] = undefined;
+    }
+
+    /**
+     * Returns axios interceptors.
+     */
+    // eslint-disable-next-line class-methods-use-this
+    public getInterceptors() {
+        return axios.interceptors;
+    }
+
+    /**
+     * Generic HTTP request.
+     * This is done so that developer can use any functionality
+     * provided by axios. Here, only the set headers are spread
+     * over what was sent in config.
+     */
+    public async request(config: AxiosRequestConfig, customConfig?: any) {
+        // eslint-disable-next-line no-param-reassign
+        config.headers = {
+            ...this.headers,
+            ...config.headers,
+        };
+        if (customConfig?.cancel) {
+            config.cancelToken = new axios.CancelToken(
+                (c) => (customConfig.cancel.exec = c)
+            );
+        }
+        return await axios({ ...config, ...customConfig });
+    }
+
+    /**
+     * Get request.
+     */
+    public get(
+        url: string,
+        params?: IQueryPrams,
+        headers?: IHTTPHeaders,
+        customConfig?: any
+    ) {
+        return this.request(
+            {
+                headers,
+                method: 'GET',
+                params,
+                url,
+            },
+            customConfig
+        );
+    }
+
+    /**
+     * Post request
+     */
+    public post(
+        url: string,
+        data?: any,
+        params?: IQueryPrams,
+        headers?: IHTTPHeaders,
+        customConfig?: any
+    ) {
+        return this.request(
+            {
+                data,
+                headers,
+                method: 'POST',
+                params,
+                url,
+            },
+            customConfig
+        );
+    }
+
+    /**
+     * Put request
+     */
+    public put(
+        url: string,
+        data: any,
+        params?: IQueryPrams,
+        headers?: IHTTPHeaders,
+        customConfig?: any
+    ) {
+        return this.request(
+            {
+                data,
+                headers,
+                method: 'PUT',
+                params,
+                url,
+            },
+            customConfig
+        );
+    }
+
+    /**
+     * Delete request
+     */
+    public delete(
+        url: string,
+        data: any,
+        params?: IQueryPrams,
+        headers?: IHTTPHeaders,
+        customConfig?: any
+    ) {
+        return this.request(
+            {
+                data,
+                headers,
+                method: 'DELETE',
+                params,
+                url,
+            },
+            customConfig
+        );
+    }
+}
+
+// Creates a Singleton Service.
+// This will help me maintain common headers / functionality
+// at a central place.
+export default new HTTPService();

+ 113 - 0
packages/shared/network/api.ts

@@ -0,0 +1,113 @@
+import { getData, LS_KEYS } from '../storage/localStorage';
+
+export const getEndpoint = () => {
+    let endpoint = getData(LS_KEYS.API_ENDPOINT);
+    if (endpoint) {
+        return endpoint;
+    }
+    endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
+    if (isDevDeployment() && endpoint) {
+        return endpoint;
+    }
+    return 'https://api.ente.io';
+};
+
+export const getFileURL = (id: number) => {
+    const endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
+    if (isDevDeployment() && endpoint) {
+        return `${endpoint}/files/download/${id}`;
+    }
+    return `https://files.ente.io/?fileID=${id}`;
+};
+
+export const getPublicCollectionFileURL = (id: number) => {
+    const endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
+    if (isDevDeployment() && endpoint) {
+        return `${endpoint}/public-collection/files/download/${id}`;
+    }
+    return `https://public-albums.ente.io/download/?fileID=${id}`;
+};
+
+export const getThumbnailURL = (id: number) => {
+    const endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
+    if (isDevDeployment() && endpoint) {
+        return `${endpoint}/files/preview/${id}`;
+    }
+    return `https://thumbnails.ente.io/?fileID=${id}`;
+};
+
+export const getPublicCollectionThumbnailURL = (id: number) => {
+    const endpoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT;
+    if (isDevDeployment() && endpoint) {
+        return `${endpoint}/public-collection/files/preview/${id}`;
+    }
+    return `https://public-albums.ente.io/preview/?fileID=${id}`;
+};
+
+export const getUploadEndpoint = () => {
+    const endpoint = process.env.NEXT_PUBLIC_ENTE_UPLOAD_ENDPOINT;
+    if (isDevDeployment() && endpoint) {
+        return endpoint;
+    }
+    return `https://uploader.ente.io`;
+};
+
+export const getPaymentsURL = () => {
+    const paymentsURL = process.env.NEXT_PUBLIC_ENTE_PAYMENT_ENDPOINT;
+    if (isDevDeployment() && paymentsURL) {
+        return paymentsURL;
+    }
+    return `https://payments.ente.io`;
+};
+
+export const getAlbumsURL = () => {
+    const albumsURL = process.env.NEXT_PUBLIC_ENTE_ALBUM_ENDPOINT;
+    if (isDevDeployment() && albumsURL) {
+        return albumsURL;
+    }
+    return `https://albums.ente.io`;
+};
+
+// getFamilyPortalURL returns the endpoint for the family dashboard which can be used to
+// create or manage family.
+export const getFamilyPortalURL = () => {
+    const familyURL = process.env.NEXT_PUBLIC_ENTE_FAMILY_PORTAL_ENDPOINT;
+    if (isDevDeployment() && familyURL) {
+        return familyURL;
+    }
+    return `https://family.ente.io`;
+};
+
+// getAuthenticatorURL returns the endpoint for the authenticator which can be used to
+// view authenticator codes.
+export const getAuthURL = () => {
+    const authURL = process.env.NEXT_PUBLIC_ENTE_AUTH_ENDPOINT;
+    if (isDevDeployment() && authURL) {
+        return authURL;
+    }
+    return `https://auth.ente.io`;
+};
+
+export const getSentryTunnelURL = () => {
+    return `https://sentry-reporter.ente.io`;
+};
+
+/*
+It's a dev deployment (and should use the environment override for endpoints ) in three cases:
+1. when the URL opened is that of the staging web app, or
+2. when the URL opened is that of the staging album app, or
+3. if the app is running locally (hence node_env is development)
+4. if the app is running in test mode
+*/
+export const isDevDeployment = () => {
+    if (globalThis?.location) {
+        return (
+            process.env.NEXT_PUBLIC_ENTE_WEB_ENDPOINT ===
+                globalThis.location.origin ||
+            process.env.NEXT_PUBLIC_ENTE_ALBUM_ENDPOINT ===
+                globalThis.location.origin ||
+            process.env.NEXT_PUBLIC_IS_TEST_APP === 'true' ||
+            process.env.NODE_ENV === 'development'
+        );
+    }
+};

+ 2 - 1
packages/shared/package.json

@@ -10,7 +10,8 @@
         "@emotion/styled": "11.11.0",
         "@mui/material": "5.11.16",
         "react": "18.2.0",
-        "react-dom": "18.2.0"
+        "react-dom": "18.2.0",
+        "@mui/icons-material": "5.14.1"
     },
     "devDependencies": {
         "@ente/eslint-config": "*",

+ 0 - 3
packages/shared/tsconfig.json

@@ -1,8 +1,5 @@
 {
     "extends": "../../tsconfig.base.json",
-    "compilerOptions": {
-        "baseUrl": "."
-    },
     "include": [
         "**/*.ts",
         "**/*.tsx",

+ 1 - 1
yarn.lock

@@ -281,7 +281,7 @@
   resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.16.tgz"
   integrity sha512-GxRfZ/HquQ/1nUc9qQVGReP6oOMS8/3QjPJ+23a7TMrxl2wjlmXrMNn7tRa30vZcGcDgEG+J0aseefUN0AoawQ==
 
-"@mui/icons-material@^5.14.1":
+"@mui/icons-material@5.14.1", "@mui/icons-material@^5.14.1":
   version "5.14.1"
   resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.14.1.tgz#2f145c15047a0c7f01353ce620cb88276dadba9e"
   integrity sha512-xV/f26muQqtWzerzOIdGPrXoxp/OKaE2G2Wp9gnmG47mHua5Slup/tMc3fA4ZYUreGGrK6+tT81TEvt1Wsng8Q==