[web] Enable Typescript's strict mode for auth's code (#1865)

This commit is contained in:
Manav Rathi 2024-05-26 08:35:11 +05:30 committed by GitHub
commit 582eb9e1ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 528 additions and 446 deletions

View file

@ -6,12 +6,9 @@ import { accountLogout } from "@ente/accounts/services/logout";
import { APPS, APP_TITLES } from "@ente/shared/apps/constants";
import { Overlay } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import type {
DialogBoxAttributesV2,
SetDialogBoxAttributesV2,
} from "@ente/shared/components/DialogBoxV2/types";
import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import AppNavbar from "@ente/shared/components/Navbar/app";
import { AppNavbar } from "@ente/shared/components/Navbar/app";
import { useLocalState } from "@ente/shared/hooks/useLocalState";
import HTTPService from "@ente/shared/network/HTTPService";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
@ -28,7 +25,7 @@ import "styles/global.css";
interface AppContextProps {
isMobile: boolean;
showNavBar: (show: boolean) => void;
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
setDialogBoxAttributesV2: (attrs: DialogBoxAttributesV2) => void;
logout: () => void;
}
@ -39,8 +36,9 @@ export default function App({ Component, pageProps }: AppProps) {
const [showNavbar, setShowNavBar] = useState(false);
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] =
useState<DialogBoxAttributesV2>();
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
DialogBoxAttributesV2 | undefined
>();
const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
@ -106,8 +104,7 @@ export default function App({ Component, pageProps }: AppProps) {
value={{
isMobile,
showNavBar,
setDialogBoxAttributesV2:
setDialogBoxAttributesV2 as any,
setDialogBoxAttributesV2,
logout,
}}
>

View file

@ -12,20 +12,16 @@ import {
} from "@ente/shared/apps/constants";
import { Overlay } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import type {
DialogBoxAttributesV2,
SetDialogBoxAttributesV2,
} from "@ente/shared/components/DialogBoxV2/types";
import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { MessageContainer } from "@ente/shared/components/MessageContainer";
import AppNavbar from "@ente/shared/components/Navbar/app";
import { AppNavbar } from "@ente/shared/components/Navbar/app";
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import { useLocalState } from "@ente/shared/hooks/useLocalState";
import HTTPService from "@ente/shared/network/HTTPService";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import { getTheme } from "@ente/shared/themes";
import { THEME_COLOR } from "@ente/shared/themes/constants";
import type { SetTheme } from "@ente/shared/themes/types";
import type { User } from "@ente/shared/user/types";
import { CssBaseline, useMediaQuery } from "@mui/material";
import { ThemeProvider } from "@mui/material/styles";
@ -33,7 +29,7 @@ import { t } from "i18next";
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
import { createContext, useEffect, useRef, useState } from "react";
import LoadingBar from "react-top-loading-bar";
import LoadingBar, { type LoadingBarRef } from "react-top-loading-bar";
import "../../public/css/global.css";
type AppContextType = {
@ -42,13 +38,13 @@ type AppContextType = {
finishLoading: () => void;
isMobile: boolean;
themeColor: THEME_COLOR;
setThemeColor: SetTheme;
setThemeColor: (themeColor: THEME_COLOR) => void;
somethingWentWrong: () => void;
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
setDialogBoxAttributesV2: (attrs: DialogBoxAttributesV2) => void;
logout: () => void;
};
export const AppContext = createContext<AppContextType>(null);
export const AppContext = createContext<AppContextType | undefined>(undefined);
export default function App({ Component, pageProps }: AppProps) {
const router = useRouter();
@ -58,10 +54,11 @@ export default function App({ Component, pageProps }: AppProps) {
typeof window !== "undefined" && !window.navigator.onLine,
);
const [showNavbar, setShowNavBar] = useState(false);
const isLoadingBarRunning = useRef(false);
const loadingBar = useRef(null);
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] =
useState<DialogBoxAttributesV2>();
const isLoadingBarRunning = useRef<boolean>(false);
const loadingBar = useRef<LoadingBarRef>(null);
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
DialogBoxAttributesV2 | undefined
>();
const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
const isMobile = useMediaQuery("(max-width:428px)");
const [themeColor, setThemeColor] = useLocalState(
@ -134,9 +131,10 @@ export default function App({ Component, pageProps }: AppProps) {
void accountLogout().then(() => router.push(PAGES.ROOT));
};
// TODO: Refactor this to have a fallback
const title = isI18nReady
? t("TITLE", { context: APPS.AUTH })
: APP_TITLES.get(APPS.AUTH);
: APP_TITLES.get(APPS.AUTH) ?? "";
return (
<>

View file

@ -1,3 +1,4 @@
import { ensure } from "@/utils/ensure";
import {
HorizontalFlex,
VerticallyCentered,
@ -12,7 +13,7 @@ import { CustomError } from "@ente/shared/error";
import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore";
import LogoutOutlined from "@mui/icons-material/LogoutOutlined";
import MoreHoriz from "@mui/icons-material/MoreHoriz";
import { Button, ButtonBase, Snackbar, TextField } from "@mui/material";
import { Button, ButtonBase, Snackbar, TextField, styled } from "@mui/material";
import { t } from "i18next";
import { useRouter } from "next/router";
import { AppContext } from "pages/_app";
@ -20,20 +21,22 @@ import React, { useContext, useEffect, useState } from "react";
import { generateOTPs, type Code } from "services/code";
import { getAuthCodes } from "services/remote";
const AuthenticatorCodesPage = () => {
const appContext = useContext(AppContext);
const Page: React.FC = () => {
const appContext = ensure(useContext(AppContext));
const router = useRouter();
const [codes, setCodes] = useState([]);
const [codes, setCodes] = useState<Code[]>([]);
const [hasFetched, setHasFetched] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
useEffect(() => {
const fetchCodes = async () => {
try {
const res = await getAuthCodes();
setCodes(res);
} catch (err) {
if (err.message === CustomError.KEY_MISSING) {
setCodes(await getAuthCodes());
} catch (e) {
if (
e instanceof Error &&
e.message == CustomError.KEY_MISSING
) {
InMemoryStore.set(MS_KEYS.REDIRECT_URL, PAGES.AUTH);
router.push(PAGES.ROOT);
} else {
@ -55,11 +58,9 @@ const AuthenticatorCodesPage = () => {
if (!hasFetched) {
return (
<>
<VerticallyCentered>
<EnteSpinner></EnteSpinner>
</VerticallyCentered>
</>
<VerticallyCentered>
<EnteSpinner />
</VerticallyCentered>
);
}
@ -77,7 +78,7 @@ const AuthenticatorCodesPage = () => {
}}
>
<div style={{ marginBottom: "1rem" }} />
{filteredCodes.length === 0 && searchTerm.length === 0 ? (
{filteredCodes.length == 0 && searchTerm.length == 0 ? (
<></>
) : (
<TextField
@ -101,7 +102,7 @@ const AuthenticatorCodesPage = () => {
justifyContent: "center",
}}
>
{filteredCodes.length === 0 ? (
{filteredCodes.length == 0 ? (
<div
style={{
alignItems: "center",
@ -110,10 +111,10 @@ const AuthenticatorCodesPage = () => {
marginTop: "32px",
}}
>
{searchTerm.length !== 0 ? (
{searchTerm.length > 0 ? (
<p>{t("NO_RESULTS")}</p>
) : (
<div />
<></>
)}
</div>
) : (
@ -122,18 +123,16 @@ const AuthenticatorCodesPage = () => {
))
)}
</div>
<div style={{ marginBottom: "2rem" }} />
<Footer />
<div style={{ marginBottom: "4rem" }} />
</div>
</>
);
};
export default AuthenticatorCodesPage;
export default Page;
const AuthNavbar: React.FC = () => {
const { isMobile, logout } = useContext(AppContext);
const { isMobile, logout } = ensure(useContext(AppContext));
return (
<NavbarBase isMobile={isMobile}>
@ -158,11 +157,11 @@ const AuthNavbar: React.FC = () => {
);
};
interface CodeDisplay {
interface CodeDisplayProps {
code: Code;
}
const CodeDisplay: React.FC<CodeDisplay> = ({ code }) => {
const CodeDisplay: React.FC<CodeDisplayProps> = ({ code }) => {
const [otp, setOTP] = useState("");
const [nextOTP, setNextOTP] = useState("");
const [errorMessage, setErrorMessage] = useState("");
@ -393,14 +392,7 @@ const UnparseableCode: React.FC<UnparseableCodeProps> = ({
const Footer: React.FC = () => {
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
<Footer_>
<p>{t("AUTH_DOWNLOAD_MOBILE_APP")}</p>
<a
href="https://github.com/ente-io/ente/tree/main/auth#-download"
@ -408,6 +400,15 @@ const Footer: React.FC = () => {
>
<Button color="accent">{t("DOWNLOAD")}</Button>
</a>
</div>
</Footer_>
);
};
const Footer_ = styled("div")`
margin-block-start: 2rem;
margin-block-end: 4rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
`;

View file

@ -1,9 +1,12 @@
import { ensure } from "@/utils/ensure";
import ChangeEmailPage from "@ente/accounts/pages/change-email";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
import React, { useContext } from "react";
export default function ChangeEmail() {
const appContext = useContext(AppContext);
const Page: React.FC = () => {
const appContext = ensure(useContext(AppContext));
return <ChangeEmailPage appContext={appContext} appName={APPS.AUTH} />;
}
};
export default Page;

View file

@ -1,9 +1,12 @@
import { ensure } from "@/utils/ensure";
import ChangePasswordPage from "@ente/accounts/pages/change-password";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
import React, { useContext } from "react";
export default function ChangePassword() {
const appContext = useContext(AppContext);
const Page: React.FC = () => {
const appContext = ensure(useContext(AppContext));
return <ChangePasswordPage appContext={appContext} appName={APPS.AUTH} />;
}
};
export default Page;

View file

@ -1,9 +1,12 @@
import { ensure } from "@/utils/ensure";
import CredentialPage from "@ente/accounts/pages/credentials";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
import React, { useContext } from "react";
export default function Credential() {
const appContext = useContext(AppContext);
const Page: React.FC = () => {
const appContext = ensure(useContext(AppContext));
return <CredentialPage appContext={appContext} appName={APPS.AUTH} />;
}
};
export default Page;

View file

@ -1,9 +1,12 @@
import { ensure } from "@/utils/ensure";
import GeneratePage from "@ente/accounts/pages/generate";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
import React, { useContext } from "react";
export default function Generate() {
const appContext = useContext(AppContext);
const Page: React.FC = () => {
const appContext = ensure(useContext(AppContext));
return <GeneratePage appContext={appContext} appName={APPS.AUTH} />;
}
};
export default Page;

View file

@ -1,8 +1,8 @@
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import { useRouter } from "next/router";
import { useEffect } from "react";
import React, { useEffect } from "react";
const IndexPage = () => {
const Page: React.FC = () => {
const router = useRouter();
useEffect(() => {
router.push(PAGES.LOGIN);
@ -11,4 +11,4 @@ const IndexPage = () => {
return <></>;
};
export default IndexPage;
export default Page;

View file

@ -1,9 +1,12 @@
import { ensure } from "@/utils/ensure";
import LoginPage from "@ente/accounts/pages/login";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
import React, { useContext } from "react";
export default function Login() {
const appContext = useContext(AppContext);
const Page: React.FC = () => {
const appContext = ensure(useContext(AppContext));
return <LoginPage appContext={appContext} appName={APPS.AUTH} />;
}
};
export default Page;

View file

@ -1,11 +1,3 @@
import PasskeysFinishPage from "@ente/accounts/pages/passkeys/finish";
import Page from "@ente/accounts/pages/passkeys/finish";
const PasskeysFinish = () => {
return (
<>
<PasskeysFinishPage />
</>
);
};
export default PasskeysFinish;
export default Page;

View file

@ -1,9 +1,12 @@
import { ensure } from "@/utils/ensure";
import RecoverPage from "@ente/accounts/pages/recover";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
import React, { useContext } from "react";
export default function Recover() {
const appContext = useContext(AppContext);
const Page: React.FC = () => {
const appContext = ensure(useContext(AppContext));
return <RecoverPage appContext={appContext} appName={APPS.AUTH} />;
}
};
export default Page;

View file

@ -1,9 +1,12 @@
import { ensure } from "@/utils/ensure";
import SignupPage from "@ente/accounts/pages/signup";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
import React, { useContext } from "react";
export default function Sigup() {
const appContext = useContext(AppContext);
const Page: React.FC = () => {
const appContext = ensure(useContext(AppContext));
return <SignupPage appContext={appContext} appName={APPS.AUTH} />;
}
};
export default Page;

View file

@ -1,9 +1,12 @@
import { ensure } from "@/utils/ensure";
import TwoFactorRecoverPage from "@ente/accounts/pages/two-factor/recover";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
import React, { useContext } from "react";
export default function TwoFactorRecover() {
const appContext = useContext(AppContext);
const Page: React.FC = () => {
const appContext = ensure(useContext(AppContext));
return <TwoFactorRecoverPage appContext={appContext} appName={APPS.AUTH} />;
}
};
export default Page;

View file

@ -1,9 +1,12 @@
import { ensure } from "@/utils/ensure";
import TwoFactorSetupPage from "@ente/accounts/pages/two-factor/setup";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
import React, { useContext } from "react";
export default function TwoFactorSetup() {
const appContext = useContext(AppContext);
const Page: React.FC = () => {
const appContext = ensure(useContext(AppContext));
return <TwoFactorSetupPage appContext={appContext} appName={APPS.AUTH} />;
}
};
export default Page;

View file

@ -1,9 +1,12 @@
import { ensure } from "@/utils/ensure";
import TwoFactorVerifyPage from "@ente/accounts/pages/two-factor/verify";
import { APPS } from "@ente/shared/apps/constants";
import { useContext } from "react";
import React, { useContext } from "react";
import { AppContext } from "../_app";
export default function TwoFactorVerify() {
const appContext = useContext(AppContext);
const Page: React.FC = () => {
const appContext = ensure(useContext(AppContext));
return <TwoFactorVerifyPage appContext={appContext} appName={APPS.AUTH} />;
}
};
export default Page;

View file

@ -1,9 +1,12 @@
import { ensure } from "@/utils/ensure";
import VerifyPage from "@ente/accounts/pages/verify";
import { APPS } from "@ente/shared/apps/constants";
import { AppContext } from "pages/_app";
import { useContext } from "react";
import React, { useContext } from "react";
export default function Verify() {
const appContext = useContext(AppContext);
const Page: React.FC = () => {
const appContext = ensure(useContext(AppContext));
return <VerifyPage appContext={appContext} appName={APPS.AUTH} />;
}
};
export default Page;

View file

@ -9,7 +9,7 @@ import { Steam } from "./steam";
*/
export interface Code {
/** A unique id for the corresponding "auth entity" in our system. */
id?: String;
id: string;
/** The type of the code. */
type: "totp" | "hotp" | "steam";
/** The user's account or email for which this code is used. */
@ -146,8 +146,8 @@ const parseIssuer = (url: URL, path: string): string => {
let p = decodeURIComponent(path);
if (p.startsWith("/")) p = p.slice(1);
if (p.includes(":")) p = p.split(":")[0];
else if (p.includes("-")) p = p.split("-")[0];
if (p.includes(":")) p = ensure(p.split(":")[0]);
else if (p.includes("-")) p = ensure(p.split("-")[0]);
return p;
};

View file

@ -26,6 +26,9 @@ export const getAuthCodes = async (): Promise<Code[]> => {
authEntity
.filter((f) => !f.isDeleted)
.map(async (entity) => {
if (!entity.id) return undefined;
if (!entity.encryptedData) return undefined;
if (!entity.header) return undefined;
try {
const decryptedCode =
await cryptoWorker.decryptMetadata(
@ -36,14 +39,12 @@ export const getAuthCodes = async (): Promise<Code[]> => {
return codeFromURIString(entity.id, decryptedCode);
} catch (e) {
log.error(`Failed to parse codeID ${entity.id}`, e);
return null;
return undefined;
}
}),
);
// Remove null and undefined values
const filteredAuthCodes = authCodes.filter(
(f) => f !== null && f !== undefined,
);
// Remove undefined values
const filteredAuthCodes = authCodes.filter((f): f is Code => !!f);
filteredAuthCodes.sort((a, b) => {
if (a.issuer && b.issuer) {
return a.issuer.localeCompare(b.issuer);
@ -58,7 +59,7 @@ export const getAuthCodes = async (): Promise<Code[]> => {
});
return filteredAuthCodes;
} catch (e) {
if (e.message !== CustomError.AUTH_KEY_NOT_FOUND) {
if (e instanceof Error && e.message != CustomError.AUTH_KEY_NOT_FOUND) {
log.error("get authenticator entities failed", e);
}
throw e;
@ -92,7 +93,7 @@ export const getAuthKey = async (): Promise<AuthKey> => {
} catch (e) {
if (
e instanceof ApiError &&
e.httpStatusCode === HttpStatusCode.NotFound
e.httpStatusCode == HttpStatusCode.NotFound
) {
throw Error(CustomError.AUTH_KEY_NOT_FOUND);
} else {

View file

@ -3,23 +3,18 @@
"include": [
"src",
"next-env.d.ts",
"../../packages/next/global-electron.d.ts",
"../../packages/shared/themes/mui-theme.d.ts"
],
// Temporarily disable some things to get the existing code to compile
// without warnings.
"compilerOptions": {
/* Set the base directory from which to resolve bare module names */
"baseUrl": "./src",
"jsxImportSource": "@emotion/react",
"strict": false,
/* Stricter than strict */
"noImplicitReturns": false,
"noUnusedParameters": false,
"noUnusedLocals": false,
"noFallthroughCasesInSwitch": false,
/* e.g. makes array indexing returns undefined */
/* This is hard to enforce in certain cases where we do a lot of array
indexing, e.g. image/ML ops, and TS doesn't currently have a way to
disable this for blocks of code. */
"noUncheckedIndexedAccess": false,
/* MUI doesn't play great with exactOptionalPropertyTypes currently. */
"exactOptionalPropertyTypes": false
}
}

View file

@ -157,9 +157,9 @@ const UserDetailsSection: React.FC<UserDetailsSectionProps> = ({
}) => {
const galleryContext = useContext(GalleryContext);
const [userDetails, setUserDetails] = useLocalState<UserDetails>(
LS_KEYS.USER_DETAILS,
);
const [userDetails, setUserDetails] = useLocalState<
UserDetails | undefined
>(LS_KEYS.USER_DETAILS, undefined);
const [memberSubscriptionManageView, setMemberSubscriptionManageView] =
useState(false);
@ -198,6 +198,7 @@ const UserDetailsSection: React.FC<UserDetailsSectionProps> = ({
openMemberSubscriptionManage();
} else {
if (
userDetails &&
hasStripeSubscription(userDetails.subscription) &&
isSubscriptionPastDue(userDetails.subscription)
) {
@ -529,7 +530,7 @@ const UtilitySection: React.FC<UtilitySectionProps> = ({ closeSidebar }) => {
});
const toggleTheme = () => {
setThemeColor((themeColor) =>
setThemeColor(
themeColor === THEME_COLOR.DARK
? THEME_COLOR.LIGHT
: THEME_COLOR.DARK,

View file

@ -22,7 +22,7 @@ export const CollectionMappingChoiceModal: React.FC<
return (
<Dialog open={open} onClose={handleClose}>
<DialogTitleWithCloseButton onClose={handleClose}>
<DialogTitleWithCloseButton onClose={onClose}>
{t("MULTI_FOLDER_UPLOAD")}
</DialogTitleWithCloseButton>
<DialogContent>

View file

@ -119,12 +119,11 @@ export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
onClose={onClose}
PaperProps={{ sx: { height: "448px", maxWidth: "414px" } }}
>
<DialogTitleWithCloseButton
onClose={onClose}
sx={{ "&&&": { padding: "32px 16px 16px 24px" } }}
>
{t("WATCHED_FOLDERS")}
</DialogTitleWithCloseButton>
<Title_>
<DialogTitleWithCloseButton onClose={onClose}>
{t("WATCHED_FOLDERS")}
</DialogTitleWithCloseButton>
</Title_>
<DialogContent sx={{ flex: 1 }}>
<Stack spacing={1} p={1.5} height={"100%"}>
<WatchList {...{ watches, removeWatch }} />
@ -149,6 +148,10 @@ export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
);
};
const Title_ = styled("div")`
padding: 32px 16px 16px 24px;
`;
interface WatchList {
watches: FolderWatch[];
removeWatch: (watch: FolderWatch) => void;

View file

@ -18,13 +18,10 @@ import {
SetDialogBoxAttributes,
} from "@ente/shared/components/DialogBox/types";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import type {
DialogBoxAttributesV2,
SetDialogBoxAttributesV2,
} from "@ente/shared/components/DialogBoxV2/types";
import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { MessageContainer } from "@ente/shared/components/MessageContainer";
import AppNavbar from "@ente/shared/components/Navbar/app";
import { AppNavbar } from "@ente/shared/components/Navbar/app";
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import { useLocalState } from "@ente/shared/hooks/useLocalState";
import HTTPService from "@ente/shared/network/HTTPService";
@ -36,7 +33,6 @@ import {
} from "@ente/shared/storage/localStorage/helpers";
import { getTheme } from "@ente/shared/themes";
import { THEME_COLOR } from "@ente/shared/themes/constants";
import type { SetTheme } from "@ente/shared/themes/types";
import type { User } from "@ente/shared/user/types";
import ArrowForward from "@mui/icons-material/ArrowForward";
import { CssBaseline, useMediaQuery } from "@mui/material";
@ -95,9 +91,9 @@ type AppContextType = {
setWatchFolderFiles: (files: FileList) => void;
isMobile: boolean;
themeColor: THEME_COLOR;
setThemeColor: SetTheme;
setThemeColor: (themeColor: THEME_COLOR) => void;
somethingWentWrong: () => void;
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
setDialogBoxAttributesV2: (attrs: DialogBoxAttributesV2) => void;
isCFProxyDisabled: boolean;
setIsCFProxyDisabled: (disabled: boolean) => void;
logout: () => void;
@ -119,8 +115,9 @@ export default function App({ Component, pageProps }: AppProps) {
const isLoadingBarRunning = useRef(false);
const loadingBar = useRef(null);
const [dialogMessage, setDialogMessage] = useState<DialogBoxAttributes>();
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] =
useState<DialogBoxAttributesV2>();
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
DialogBoxAttributesV2 | undefined
>();
useState<DialogBoxAttributes>(null);
const [messageDialogView, setMessageDialogView] = useState(false);
const [dialogBoxV2View, setDialogBoxV2View] = useState(false);

View file

@ -25,81 +25,6 @@ import { useContext, useEffect, useState } from "react";
import { Trans } from "react-i18next";
import { AppContext } from "./_app";
const Container = styled("div")`
display: flex;
flex: 1;
align-items: center;
justify-content: center;
background-color: #000;
@media (max-width: 1024px) {
flex-direction: column;
}
`;
const SlideContainer = styled("div")`
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
@media (max-width: 1024px) {
flex-grow: 0;
}
`;
const DesktopBox = styled("div")`
flex: 1;
height: 100%;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
background-color: #242424;
@media (max-width: 1024px) {
display: none;
}
`;
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;
min-width: 320px;
`;
const TextContainer = (props: TypographyProps) => (
<Typography color={"text.muted"} mt={2} mb={3} {...props} />
);
const FeatureText = (props: TypographyProps) => (
<Typography variant="h3" mt={4} {...props} />
);
const Img = styled("img")`
height: 250px;
object-fit: contain;
@media (max-width: 400px) {
height: 180px;
}
`;
export default function LandingPage() {
const router = useRouter();
const appContext = useContext(AppContext);
@ -189,7 +114,9 @@ export default function LandingPage() {
) : (
<>
<SlideContainer>
<EnteLogo height={24} sx={{ mb: 8 }} />
<Logo_>
<EnteLogo height={24} />
</Logo_>
<Slideshow />
</SlideContainer>
<MobileBox>
@ -223,6 +150,68 @@ export default function LandingPage() {
);
}
const Container = styled("div")`
display: flex;
flex: 1;
align-items: center;
justify-content: center;
background-color: #000;
@media (max-width: 1024px) {
flex-direction: column;
}
`;
const SlideContainer = styled("div")`
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
@media (max-width: 1024px) {
flex-grow: 0;
}
`;
const Logo_ = styled("div")`
margin-block-end: 64px;
`;
const DesktopBox = styled("div")`
flex: 1;
height: 100%;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
background-color: #242424;
@media (max-width: 1024px) {
display: none;
}
`;
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;
min-width: 320px;
`;
const Slideshow: React.FC = () => {
return (
<CarouselProvider
@ -276,6 +265,23 @@ const Slideshow: React.FC = () => {
);
};
const TextContainer = (props: TypographyProps) => (
<Typography color={"text.muted"} mt={2} mb={3} {...props} />
);
const FeatureText = (props: TypographyProps) => (
<Typography variant="h3" mt={4} {...props} />
);
const Img = styled("img")`
height: 250px;
object-fit: contain;
@media (max-width: 400px) {
height: 180px;
}
`;
const CustomDotGroup = styled(DotGroup)`
margin-block-start: 2px;
margin-block-end: 24px;

View file

@ -127,7 +127,7 @@ export const updateSRPAndKeys = async (
const resp = await HTTPService.post(
`${ENDPOINT}/users/srp/update`,
updateSRPAndKeyRequest,
null,
undefined,
{
"X-Auth-Token": token,
},

View file

@ -66,14 +66,10 @@ export const logout = async () => {
};
export const verifyTwoFactor = async (code: string, sessionID: string) => {
const resp = await HTTPService.post(
`${ENDPOINT}/users/two-factor/verify`,
{
code,
sessionID,
},
null,
);
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/verify`, {
code,
sessionID,
});
return resp.data as UserVerificationResponse;
};
@ -108,7 +104,7 @@ export const changeEmail = async (email: string, ott: string) => {
email,
ott,
},
null,
undefined,
{
"X-Auth-Token": getToken(),
},
@ -127,7 +123,7 @@ export const setupTwoFactor = async () => {
const resp = await HTTPService.post(
`${ENDPOINT}/users/two-factor/setup`,
null,
null,
undefined,
{
"X-Auth-Token": getToken(),
},
@ -148,7 +144,7 @@ export const enableTwoFactor = async (
twoFactorSecretDecryptionNonce:
recoveryEncryptedTwoFactorSecret.nonce,
},
null,
undefined,
{
"X-Auth-Token": getToken(),
},
@ -156,12 +152,17 @@ export const enableTwoFactor = async (
};
export const setRecoveryKey = (token: string, recoveryKey: RecoveryKey) =>
HTTPService.put(`${ENDPOINT}/users/recovery-key`, recoveryKey, null, {
HTTPService.put(`${ENDPOINT}/users/recovery-key`, recoveryKey, undefined, {
"X-Auth-Token": token,
});
export const disableTwoFactor = async () => {
await HTTPService.post(`${ENDPOINT}/users/two-factor/disable`, null, null, {
"X-Auth-Token": getToken(),
});
await HTTPService.post(
`${ENDPOINT}/users/two-factor/disable`,
null,
undefined,
{
"X-Auth-Token": getToken(),
},
);
};

View file

@ -1,3 +1,4 @@
import { ensure } from "@/utils/ensure";
import { wait } from "@/utils/promise";
import { changeEmail, sendOTTForEmailChange } from "@ente/accounts/api/user";
import { APP_HOMES } from "@ente/shared/apps/constants";
@ -11,7 +12,7 @@ import { Alert, Box, TextField } from "@mui/material";
import { Formik, type FormikHelpers } from "formik";
import { t } from "i18next";
import { useRouter } from "next/router";
import { useRef, useState } from "react";
import { useState } from "react";
import { Trans } from "react-i18next";
import * as Yup from "yup";
@ -23,8 +24,7 @@ interface formValues {
function ChangeEmailForm({ appName }: PageProps) {
const [loading, setLoading] = useState(false);
const [ottInputVisible, setShowOttInputVisibility] = useState(false);
const ottInputRef = useRef(null);
const [email, setEmail] = useState(null);
const [email, setEmail] = useState<string | null>(null);
const [showMessage, setShowMessage] = useState(false);
const [success, setSuccess] = useState(false);
@ -40,9 +40,11 @@ function ChangeEmailForm({ appName }: PageProps) {
setEmail(email);
setShowOttInputVisibility(true);
setShowMessage(true);
setTimeout(() => {
ottInputRef.current?.focus();
}, 250);
// TODO: What was this meant to focus on? The ref referred to an
// Form element that was removed. Is this still needed.
// setTimeout(() => {
// ottInputRef.current?.focus();
// }, 250);
} catch (e) {
setFieldError("email", t("EMAIl_ALREADY_OWNED"));
}
@ -55,7 +57,7 @@ function ChangeEmailForm({ appName }: PageProps) {
) => {
try {
setLoading(true);
await changeEmail(email, ott);
await changeEmail(email, ensure(ott));
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), email });
setLoading(false);
setSuccess(true);
@ -68,18 +70,27 @@ function ChangeEmailForm({ appName }: PageProps) {
};
const goToApp = () => {
router.push(APP_HOMES.get(appName));
// TODO: Refactor the type of APP_HOMES to not require the ??
router.push(APP_HOMES.get(appName) ?? "/");
};
return (
<Formik<formValues>
initialValues={{ email: "" }}
validationSchema={Yup.object().shape({
email: Yup.string()
.email(t("EMAIL_ERROR"))
.required(t("REQUIRED")),
ott: ottInputVisible && Yup.string().required(t("REQUIRED")),
})}
validationSchema={
ottInputVisible
? Yup.object().shape({
email: Yup.string()
.email(t("EMAIL_ERROR"))
.required(t("REQUIRED")),
ott: Yup.string().required(t("REQUIRED")),
})
: Yup.object().shape({
email: Yup.string()
.email(t("EMAIL_ERROR"))
.required(t("REQUIRED")),
})
}
validateOnChange={false}
validateOnBlur={false}
onSubmit={!ottInputVisible ? requestOTT : requestEmailChange}
@ -148,7 +159,9 @@ function ChangeEmailForm({ appName }: PageProps) {
<FormPaperFooter
style={{
justifyContent: ottInputVisible && "space-between",
justifyContent: ottInputVisible
? "space-between"
: "normal",
}}
>
{ottInputVisible && (

View file

@ -58,7 +58,8 @@ function SetPasswordForm(props: SetPasswordFormProps) {
setFieldError("confirm", t("PASSPHRASE_MATCH_ERROR"));
}
} catch (e) {
setFieldError("confirm", `${t("UNKNOWN_ERROR")} ${e.message}`);
const message = e instanceof Error ? e.message : "";
setFieldError("confirm", `${t("UNKNOWN_ERROR")} ${message}`);
} finally {
setLoading(false);
}

View file

@ -84,7 +84,8 @@ export default function SignUp({ router, appName, login }: SignUpProps) {
setLocalReferralSource(referral);
await sendOtt(appName, email);
} catch (e) {
setFieldError("confirm", `${t("UNKNOWN_ERROR")} ${e.message}`);
const message = e instanceof Error ? e.message : "";
setFieldError("confirm", `${t("UNKNOWN_ERROR")} ${message}`);
throw e;
}
try {

View file

@ -26,7 +26,7 @@ export type VerifyTwoFactorCallback = (
export default function VerifyTwoFactor(props: Props) {
const [waiting, setWaiting] = useState(false);
const otpInputRef = useRef(null);
const otpInputRef = useRef<OtpInput>(null);
const [success, setSuccess] = useState(false);
const markSuccessful = async () => {
@ -47,7 +47,8 @@ export default function VerifyTwoFactor(props: Props) {
for (let i = 0; i < 6; i++) {
otpInputRef.current?.focusPrevInput();
}
setFieldError("otp", `${t("UNKNOWN_ERROR")} ${e.message}`);
const message = e instanceof Error ? e.message : "";
setFieldError("otp", `${t("UNKNOWN_ERROR")} ${message}`);
}
setWaiting(false);
};

View file

@ -6,7 +6,7 @@ import { t } from "i18next";
import LinkButton from "@ente/shared/components/LinkButton";
interface Iprops {
twoFactorSecret: TwoFactorSecret;
twoFactorSecret?: TwoFactorSecret;
changeToQRMode: () => void;
}
export default function SetupManualMode({
@ -16,7 +16,7 @@ export default function SetupManualMode({
return (
<>
<Typography>{t("TWO_FACTOR_MANUAL_CODE_INSTRUCTION")}</Typography>
<CodeBlock code={twoFactorSecret?.secretCode} my={2} />
<CodeBlock code={twoFactorSecret?.secretCode ?? ""} my={2} />
<LinkButton onClick={changeToQRMode}>
{t("SCAN_QR_CODE")}
</LinkButton>

View file

@ -7,7 +7,7 @@ import { Typography } from "@mui/material";
import { LoadingQRCode, QRCode } from "../styledComponents";
interface Iprops {
twoFactorSecret: TwoFactorSecret;
twoFactorSecret?: TwoFactorSecret;
changeToManualMode: () => void;
}

View file

@ -6,7 +6,7 @@ import { VerticallyCentered } from "@ente/shared/components/Container";
import { useState } from "react";
interface Iprops {
twoFactorSecret: TwoFactorSecret;
twoFactorSecret?: TwoFactorSecret;
}
export function TwoFactorSetup({ twoFactorSecret }: Iprops) {
const [setupMode, setSetupMode] = useState<SetupMode>(SetupMode.QR_CODE);

View file

@ -1,3 +1,4 @@
import { ensure } from "@/utils/ensure";
import { startSRPSetup, updateSRPAndKeys } from "@ente/accounts/api/srp";
import SetPasswordForm, {
type SetPasswordFormProps,
@ -91,7 +92,7 @@ export default function ChangePassword({ appName }: PageProps) {
const srpA = convertBufferToBase64(srpClient.computeA());
const { setupID, srpB } = await startSRPSetup(token, {
const { setupID, srpB } = await startSRPSetup(ensure(token), {
srpUserID,
srpSalt,
srpVerifier,
@ -102,7 +103,7 @@ export default function ChangePassword({ appName }: PageProps) {
const srpM1 = convertBufferToBase64(srpClient.computeM1());
await updateSRPAndKeys(token, {
await updateSRPAndKeys(ensure(token), {
setupID,
srpM1,
updatedKeyAttr: updatedKey,
@ -121,15 +122,17 @@ export default function ChangePassword({ appName }: PageProps) {
const redirectToAppHome = () => {
setData(LS_KEYS.SHOW_BACK_BUTTON, { value: true });
router.push(APP_HOMES.get(appName));
// TODO: Refactor the type of APP_HOMES to not require the ??
router.push(APP_HOMES.get(appName) ?? "/");
};
// TODO: Handle the case where user is not loaded yet.
return (
<VerticallyCentered>
<FormPaper>
<FormPaperTitle>{t("CHANGE_PASSWORD")}</FormPaperTitle>
<SetPasswordForm
userEmail={user?.email}
userEmail={user?.email ?? ""}
callback={onSubmit}
buttonText={t("CHANGE_PASSWORD")}
/>

View file

@ -1,5 +1,6 @@
import { isDevBuild } from "@/next/env";
import log from "@/next/log";
import { ensure } from "@/utils/ensure";
import { APP_HOMES } from "@ente/shared/apps/constants";
import type { PageProps } from "@ente/shared/apps/types";
import { VerticallyCentered } from "@ente/shared/components/Container";
@ -86,7 +87,8 @@ export default function Credentials({ appContext, appName }: PageProps) {
}
const token = getToken();
if (key && token) {
router.push(APP_HOMES.get(appName));
// TODO: Refactor the type of APP_HOMES to not require the ??
router.push(APP_HOMES.get(appName) ?? "/");
return;
}
const kekEncryptedAttributes: B64EncryptionResult = getKey(
@ -148,7 +150,7 @@ export default function Credentials({ appContext, appName }: PageProps) {
id,
twoFactorSessionID,
passkeySessionID,
} = await loginViaSRP(srpAttributes, kek);
} = await loginViaSRP(ensure(srpAttributes), kek);
setIsFirstLogin(true);
if (passkeySessionID) {
const sessionKeyAttributes =
@ -168,7 +170,7 @@ export default function Credentials({ appContext, appName }: PageProps) {
window.location.href = `${getAccountsURL()}/passkeys/flow?passkeySessionID=${passkeySessionID}&redirect=${
window.location.origin
}/passkeys/finish`;
return;
return undefined;
} else if (twoFactorSessionID) {
const sessionKeyAttributes =
await cryptoWorker.generateKeyAndEncryptToB64(kek);
@ -193,11 +195,15 @@ export default function Credentials({ appContext, appName }: PageProps) {
id,
isTwoFactorEnabled: false,
});
setData(LS_KEYS.KEY_ATTRIBUTES, keyAttributes);
if (keyAttributes)
setData(LS_KEYS.KEY_ATTRIBUTES, keyAttributes);
return keyAttributes;
}
} catch (e) {
if (e.message !== CustomError.TWO_FACTOR_ENABLED) {
if (
e instanceof Error &&
e.message != CustomError.TWO_FACTOR_ENABLED
) {
log.error("getKeyAttributes failed", e);
}
throw e;
@ -221,10 +227,10 @@ export default function Credentials({ appContext, appName }: PageProps) {
await saveKeyInSessionStore(SESSION_KEYS.ENCRYPTION_KEY, key);
await decryptAndStoreToken(keyAttributes, key);
try {
let srpAttributes: SRPAttributes = getData(
let srpAttributes: SRPAttributes | null = getData(
LS_KEYS.SRP_ATTRIBUTES,
);
if (!srpAttributes) {
if (!srpAttributes && user) {
srpAttributes = await getSRPAttributes(user.email);
if (srpAttributes) {
setData(LS_KEYS.SRP_ATTRIBUTES, srpAttributes);
@ -258,10 +264,12 @@ export default function Credentials({ appContext, appName }: PageProps) {
);
}
// TODO: Handle the case when user is not present, or exclude that
// possibility using types.
return (
<VerticallyCentered>
<FormPaper style={{ minWidth: "320px" }}>
<Header>{user.email}</Header>
<Header>{user?.email ?? ""}</Header>
<VerifyMasterPasswordForm
buttonText={t("VERIFY_PASSPHRASE")}

View file

@ -1,18 +1,12 @@
import log from "@/next/log";
import { ensure } from "@/utils/ensure";
import { putAttributes } from "@ente/accounts/api/user";
import SetPasswordForm, {
type SetPasswordFormProps,
} from "@ente/accounts/components/SetPasswordForm";
import { PAGES } from "@ente/accounts/constants/pages";
import { configureSRP } from "@ente/accounts/services/srp";
import { generateKeyAndSRPAttributes } from "@ente/accounts/utils/srp";
import {
generateAndSaveIntermediateKeyAttributes,
saveKeyInSessionStore,
} from "@ente/shared/crypto/helpers";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import { SESSION_KEYS, getKey } from "@ente/shared/storage/sessionStorage";
import { t } from "i18next";
import { useEffect, useState } from "react";
import SetPasswordForm from "@ente/accounts/components/SetPasswordForm";
import { PAGES } from "@ente/accounts/constants/pages";
import { APP_HOMES } from "@ente/shared/apps/constants";
import type { PageProps } from "@ente/shared/apps/types";
import { VerticallyCentered } from "@ente/shared/components/Container";
@ -22,12 +16,20 @@ import FormPaperFooter from "@ente/shared/components/Form/FormPaper/Footer";
import FormTitle from "@ente/shared/components/Form/FormPaper/Title";
import LinkButton from "@ente/shared/components/LinkButton";
import RecoveryKey from "@ente/shared/components/RecoveryKey";
import {
generateAndSaveIntermediateKeyAttributes,
saveKeyInSessionStore,
} from "@ente/shared/crypto/helpers";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import {
justSignedUp,
setJustSignedUp,
} from "@ente/shared/storage/localStorage/helpers";
import { SESSION_KEYS, getKey } from "@ente/shared/storage/sessionStorage";
import type { KeyAttributes, User } from "@ente/shared/user/types";
import { t } from "i18next";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
export default function Generate({ appContext, appName }: PageProps) {
const { logout } = appContext;
@ -54,7 +56,8 @@ export default function Generate({ appContext, appName }: PageProps) {
setRecoveryModalView(true);
setLoading(false);
} else {
router.push(APP_HOMES.get(appName));
// TODO: Refactor the type of APP_HOMES to not require the ??
router.push(APP_HOMES.get(appName) ?? "/");
}
} else if (keyAttributes?.encryptedKey) {
router.push(PAGES.CREDENTIALS);
@ -67,12 +70,16 @@ export default function Generate({ appContext, appName }: PageProps) {
appContext.showNavBar(true);
}, []);
const onSubmit = async (passphrase, setFieldError) => {
const onSubmit: SetPasswordFormProps["callback"] = async (
passphrase,
setFieldError,
) => {
try {
const { keyAttributes, masterKey, srpSetupAttributes } =
await generateKeyAndSRPAttributes(passphrase);
await putAttributes(token, keyAttributes);
// TODO: Refactor the code to not require this ensure
await putAttributes(ensure(token), keyAttributes);
await configureSRP(srpSetupAttributes);
await generateAndSaveIntermediateKeyAttributes(
passphrase,
@ -90,7 +97,7 @@ export default function Generate({ appContext, appName }: PageProps) {
return (
<>
{loading ? (
{loading || !user ? (
<VerticallyCentered>
<EnteSpinner />
</VerticallyCentered>
@ -100,16 +107,18 @@ export default function Generate({ appContext, appName }: PageProps) {
show={recoverModalView}
onHide={() => {
setRecoveryModalView(false);
router.push(APP_HOMES.get(appName));
// TODO: Refactor the type of APP_HOMES to not require the ??
router.push(APP_HOMES.get(appName) ?? "/");
}}
somethingWentWrong={() => null}
/* TODO: Why is this error being ignored */
somethingWentWrong={() => {}}
/>
) : (
<VerticallyCentered>
<FormPaper>
<FormTitle>{t("SET_PASSPHRASE")}</FormTitle>
<SetPasswordForm
userEmail={user?.email}
userEmail={user.email}
callback={onSubmit}
buttonText={t("SET_PASSPHRASE")}
/>

View file

@ -1,4 +1,5 @@
import log from "@/next/log";
import { ensure } from "@/utils/ensure";
import { sendOtt } from "@ente/accounts/api/user";
import { PAGES } from "@ente/accounts/constants/pages";
import { APP_HOMES } from "@ente/shared/apps/constants";
@ -29,7 +30,9 @@ const bip39 = require("bip39");
bip39.setDefaultWordlist("english");
export default function Recover({ appContext, appName }: PageProps) {
const [keyAttributes, setKeyAttributes] = useState<KeyAttributes>();
const [keyAttributes, setKeyAttributes] = useState<
KeyAttributes | undefined
>();
const router = useRouter();
@ -50,7 +53,8 @@ export default function Recover({ appContext, appName }: PageProps) {
if (!keyAttributes) {
router.push(PAGES.GENERATE);
} else if (key) {
router.push(APP_HOMES.get(appName));
// TODO: Refactor the type of APP_HOMES to not require the ??
router.push(APP_HOMES.get(appName) ?? "/");
} else {
setKeyAttributes(keyAttributes);
}
@ -76,13 +80,14 @@ export default function Recover({ appContext, appName }: PageProps) {
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
}
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const keyAttr = ensure(keyAttributes);
const masterKey = await cryptoWorker.decryptB64(
keyAttributes.masterKeyEncryptedWithRecoveryKey,
keyAttributes.masterKeyDecryptionNonce,
keyAttr.masterKeyEncryptedWithRecoveryKey,
keyAttr.masterKeyDecryptionNonce,
await cryptoWorker.fromHex(recoveryKey),
);
await saveKeyInSessionStore(SESSION_KEYS.ENCRYPTION_KEY, masterKey);
await decryptAndStoreToken(keyAttributes, masterKey);
await decryptAndStoreToken(keyAttr, masterKey);
setData(LS_KEYS.SHOW_BACK_BUTTON, { value: false });
router.push(PAGES.CHANGE_PASSWORD);

View file

@ -1,4 +1,5 @@
import log from "@/next/log";
import { ensure } from "@/utils/ensure";
import { recoverTwoFactor, removeTwoFactor } from "@ente/accounts/api/user";
import { PAGES } from "@ente/accounts/constants/pages";
import { TwoFactorType } from "@ente/accounts/constants/twofactor";
@ -35,8 +36,8 @@ export default function Recover({
const { logout } = appContext;
const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] =
useState<B64EncryptionResult>(null);
const [sessionID, setSessionID] = useState(null);
useState<Omit<B64EncryptionResult, "key"> | null>(null);
const [sessionID, setSessionID] = useState<string | null>(null);
const [doesHaveEncryptedRecoveryKey, setDoesHaveEncryptedRecoveryKey] =
useState(false);
@ -70,7 +71,6 @@ export default function Recover({
setEncryptedTwoFactorSecret({
encryptedData: resp.encryptedSecret,
nonce: resp.secretDecryptionNonce,
key: null,
});
}
} catch (e) {
@ -111,13 +111,14 @@ export default function Recover({
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
}
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const { encryptedData, nonce } = ensure(encryptedTwoFactorSecret);
const twoFactorSecret = await cryptoWorker.decryptB64(
encryptedTwoFactorSecret.encryptedData,
encryptedTwoFactorSecret.nonce,
encryptedData,
nonce,
await cryptoWorker.fromHex(recoveryKey),
);
const resp = await removeTwoFactor(
sessionID,
ensure(sessionID),
twoFactorSecret,
twoFactorType,
);

View file

@ -1,4 +1,5 @@
import log from "@/next/log";
import { ensure } from "@/utils/ensure";
import { enableTwoFactor, setupTwoFactor } from "@ente/accounts/api/user";
import VerifyTwoFactor, {
type VerifyTwoFactorCallback,
@ -23,8 +24,9 @@ export enum SetupMode {
}
export default function SetupTwoFactor({ appName }: PageProps) {
const [twoFactorSecret, setTwoFactorSecret] =
useState<TwoFactorSecret>(null);
const [twoFactorSecret, setTwoFactorSecret] = useState<
TwoFactorSecret | undefined
>();
const router = useRouter();
@ -48,7 +50,7 @@ export default function SetupTwoFactor({ appName }: PageProps) {
markSuccessful,
) => {
const recoveryEncryptedTwoFactorSecret = await encryptWithRecoveryKey(
twoFactorSecret.secretCode,
ensure(twoFactorSecret).secretCode,
);
await enableTwoFactor(otp, recoveryEncryptedTwoFactorSecret);
await markSuccessful();
@ -56,7 +58,8 @@ export default function SetupTwoFactor({ appName }: PageProps) {
...getData(LS_KEYS.USER),
isTwoFactorEnabled: true,
});
router.push(APP_HOMES.get(appName));
// TODO: Refactor the type of APP_HOMES to not require the ??
router.push(APP_HOMES.get(appName) ?? "/");
};
return (

View file

@ -4,6 +4,7 @@ import VerifyTwoFactor, {
} from "@ente/accounts/components/two-factor/VerifyForm";
import { PAGES } from "@ente/accounts/constants/pages";
import { ensure } from "@/utils/ensure";
import type { PageProps } from "@ente/shared/apps/types";
import { VerticallyCentered } from "@ente/shared/components/Container";
import FormPaper from "@ente/shared/components/Form/FormPaper";
@ -55,7 +56,7 @@ export const TwoFactorVerify: React.FC<PageProps> = ({
encryptedToken,
id,
});
setData(LS_KEYS.KEY_ATTRIBUTES, keyAttributes);
setData(LS_KEYS.KEY_ATTRIBUTES, ensure(keyAttributes));
const redirectURL = InMemoryStore.get(MS_KEYS.REDIRECT_URL);
InMemoryStore.delete(MS_KEYS.REDIRECT_URL);
router.push(redirectURL ?? PAGES.CREDENTIALS);

View file

@ -1,3 +1,4 @@
import { ensure } from "@/utils/ensure";
import type { UserVerificationResponse } from "@ente/accounts/types/user";
import type { PageProps } from "@ente/shared/apps/types";
import { VerticallyCentered } from "@ente/shared/components/Container";
@ -110,7 +111,7 @@ export default function VerifyPage({ appContext, appName }: PageProps) {
} else {
if (getData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES)) {
await putAttributes(
token,
ensure(token),
getData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES),
);
}

View file

@ -3,6 +3,12 @@
"extends": "@/build-config/tsconfig-typecheck.json",
"compilerOptions": {
/* Also indicate expectation of a WebWorker runtime */
"lib": ["ESnext", "DOM", "DOM.Iterable", "WebWorker"]
}
"lib": ["ESnext", "DOM", "DOM.Iterable", "WebWorker"],
/* Next.js insists on adding these. Sigh. */
"allowJs": true,
"incremental": true
},
/* Next.js insists on adding this, even though we don't need it. */
"exclude": ["node_modules"]
}

View file

@ -67,8 +67,10 @@
"noUnusedParameters": true,
"noUnusedLocals": true,
"noFallthroughCasesInSwitch": true,
/* e.g. makes array indexing returns undefined */
/* e.g. makes array indexing returns undefined. */
"noUncheckedIndexedAccess": true,
/* Treat optional (?) properties and properties where undefined is a
valid value separately */
"exactOptionalPropertyTypes": true
}
}

View file

@ -1,12 +1,12 @@
import { TwoFactorType } from "@ente/accounts/constants/twofactor";
import type { SetDialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
import { APPS } from "./constants";
export interface PageProps {
appContext: {
showNavBar: (show: boolean) => void;
isMobile: boolean;
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
setDialogBoxAttributesV2: (attrs: DialogBoxAttributesV2) => void;
logout: () => void;
};
appName: APPS;

View file

@ -6,7 +6,7 @@ import CopyButton from "./CopyButton";
import { CodeWrapper, CopyButtonWrapper, Wrapper } from "./styledComponents";
type Iprops = React.PropsWithChildren<{
code: string;
code: string | null;
wordBreak?: "normal" | "break-all" | "keep-all" | "break-word";
}>;

View file

@ -6,12 +6,17 @@ import {
Typography,
type DialogProps,
} from "@mui/material";
import React from "react";
const DialogTitleWithCloseButton = (props) => {
const { children, onClose, ...other } = props;
interface DialogTitleWithCloseButtonProps {
onClose: () => void;
}
const DialogTitleWithCloseButton: React.FC<
React.PropsWithChildren<DialogTitleWithCloseButtonProps>
> = ({ children, onClose }) => {
return (
<DialogTitle {...other}>
<DialogTitle>
<SpaceBetweenFlex>
<Typography variant="h3" fontWeight={"bold"}>
{children}

View file

@ -54,9 +54,7 @@ export default function DialogBox({
{attributes.title && (
<DialogTitleWithCloseButton
onClose={
titleCloseButton &&
!attributes.nonClosable &&
handleClose
titleCloseButton && !attributes.nonClosable && onClose
}
>
{attributes.title}

View file

@ -15,7 +15,7 @@ import type { DialogBoxAttributesV2 } from "./types";
type IProps = React.PropsWithChildren<
Omit<DialogProps, "onClose"> & {
onClose: () => void;
attributes: DialogBoxAttributesV2;
attributes?: DialogBoxAttributesV2;
}
>;
@ -96,7 +96,9 @@ export default function DialogBoxV2({
size="large"
color={attributes.proceed?.variant}
onClick={async () => {
await attributes.proceed.action(setLoading);
await attributes.proceed?.action(
setLoading,
);
onClose();
}}
@ -110,7 +112,7 @@ export default function DialogBoxV2({
size="large"
color={attributes.close?.variant ?? "secondary"}
onClick={() => {
attributes.close.action &&
attributes.close?.action &&
attributes.close?.action();
onClose();
}}

View file

@ -39,7 +39,3 @@ export interface DialogBoxAttributesV2 {
}[];
buttonDirection?: "row" | "column";
}
export type SetDialogBoxAttributesV2 = React.Dispatch<
React.SetStateAction<DialogBoxAttributesV2>
>;

View file

@ -1,10 +1,6 @@
import { ensure } from "@/utils/ensure";
import Done from "@mui/icons-material/Done";
import {
Button,
CircularProgress,
type ButtonProps,
type PaletteColor,
} from "@mui/material";
import { Button, CircularProgress, type ButtonProps } from "@mui/material";
interface Iprops extends ButtonProps {
loading?: boolean;
@ -26,11 +22,10 @@ export default function EnteButton({
...sx,
...((loading || success) && {
"&.Mui-disabled": (theme) => ({
backgroundColor: (
theme.palette[props.color] as PaletteColor
).main,
color: (theme.palette[props.color] as PaletteColor)
.contrastText,
// TODO: Refactor to not need this ensure.
backgroundColor:
theme.palette[ensure(props.color)].main,
color: theme.palette[ensure(props.color)].contrastText,
}),
}),
}}

View file

@ -1,13 +1,23 @@
import { styled } from "@mui/material";
import React from "react";
interface EnteLogoProps {
/**
* The height of the logo image.
*
* Default: 18
*/
height?: number;
}
export const EnteLogo: React.FC<EnteLogoProps> = ({ height }) => {
return (
<LogoImage height={height ?? 18} alt="logo" src="/images/ente.svg" />
);
};
const LogoImage = styled("img")`
margin: 3px 0;
pointer-events: none;
vertical-align: middle;
`;
export function EnteLogo(props) {
return (
<LogoImage height={18} alt="logo" src="/images/ente.svg" {...props} />
);
}

View file

@ -1,8 +1,13 @@
import React from "react";
import { CenteredFlex } from "../../components/Container";
import { EnteLogo } from "../EnteLogo";
import NavbarBase from "./base";
export default function AppNavbar({ isMobile }) {
interface AppNavbarProps {
isMobile: boolean;
}
export const AppNavbar: React.FC<AppNavbarProps> = ({ isMobile }) => {
return (
<NavbarBase isMobile={isMobile}>
<CenteredFlex>
@ -10,4 +15,4 @@ export default function AppNavbar({ isMobile }) {
</CenteredFlex>
</NavbarBase>
);
}
};

View file

@ -1,5 +1,5 @@
import { createContext } from "react";
export const OverflowMenuContext = createContext({
close: () => null,
close: () => {},
});

View file

@ -1,5 +1,5 @@
import { IconButton, styled, type PaperProps } from "@mui/material";
import Menu from "@mui/material/Menu";
import Menu, { type MenuProps } from "@mui/material/Menu";
import React, { useState } from "react";
import { OverflowMenuContext } from "./context";
@ -31,7 +31,9 @@ export default function OverflowMenu({
triggerButtonProps,
menuPaperProps,
}: Iprops) {
const [sortByEl, setSortByEl] = useState(null);
const [sortByEl, setSortByEl] = useState<MenuProps["anchorEl"] | null>(
null,
);
const handleClose = () => setSortByEl(null);
return (
<OverflowMenuContext.Provider value={{ close: handleClose }}>

View file

@ -1,3 +1,4 @@
import { ensure } from "@/utils/ensure";
import type { PageProps } from "@ente/shared/apps/types";
import CodeBlock from "@ente/shared/components/CodeBlock";
import DialogTitleWithCloseButton from "@ente/shared/components/DialogBox/TitleWithCloseButton";
@ -28,7 +29,7 @@ interface Props {
}
function RecoveryKey({ somethingWentWrong, appContext, ...props }: Props) {
const [recoveryKey, setRecoveryKey] = useState(null);
const [recoveryKey, setRecoveryKey] = useState<string | null>(null);
useEffect(() => {
if (!props.show) {
@ -47,7 +48,7 @@ function RecoveryKey({ somethingWentWrong, appContext, ...props }: Props) {
}, [props.show]);
function onSaveClick() {
downloadAsFile(RECOVERY_KEY_FILE_NAME, recoveryKey);
downloadAsFile(RECOVERY_KEY_FILE_NAME, ensure(recoveryKey));
props.onHide();
}

View file

@ -2,10 +2,12 @@ import { THEME_COLOR } from "@ente/shared/themes/constants";
import DarkModeIcon from "@mui/icons-material/DarkMode";
import LightModeIcon from "@mui/icons-material/LightMode";
import { ToggleButton, ToggleButtonGroup } from "@mui/material";
interface Iprops {
themeColor: THEME_COLOR;
setThemeColor: (theme: THEME_COLOR) => void;
setThemeColor: (themeColor: THEME_COLOR) => void;
}
export default function ThemeSwitcher({ themeColor, setThemeColor }: Iprops) {
const handleChange = (event, themeColor: THEME_COLOR) => {
if (themeColor !== null) {

View file

@ -10,8 +10,8 @@ import { CustomError } from "../error";
import type { KeyAttributes, User } from "../user/types";
export interface VerifyMasterPasswordFormProps {
user: User;
keyAttributes: KeyAttributes;
user: User | undefined;
keyAttributes: KeyAttributes | undefined;
callback: (
key: string,
kek: string,
@ -20,7 +20,7 @@ export interface VerifyMasterPasswordFormProps {
) => void;
buttonText: string;
submitButtonProps?: ButtonProps;
getKeyAttributes?: (kek: string) => Promise<KeyAttributes>;
getKeyAttributes?: (kek: string) => Promise<KeyAttributes | undefined>;
srpAttributes?: SRPAttributes;
}
@ -48,14 +48,15 @@ export default function VerifyMasterPasswordForm({
srpAttributes.opsLimit,
srpAttributes.memLimit,
);
} else {
} else if (keyAttributes) {
kek = await cryptoWorker.deriveKey(
passphrase,
keyAttributes.kekSalt,
keyAttributes.opsLimit,
keyAttributes.memLimit,
);
}
} else
throw new Error("Both SRP and key attributes are missing");
} catch (e) {
log.error("failed to derive key", e);
throw Error(CustomError.WEAK_DEVICE);

View file

@ -116,7 +116,6 @@ export async function encryptWithRecoveryKey(key: string) {
}
export const getRecoveryKey = async () => {
let recoveryKey: string = null;
try {
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
@ -126,6 +125,7 @@ export const getRecoveryKey = async () => {
recoveryKeyDecryptionNonce,
} = keyAttributes;
const masterKey = await getActualKey();
let recoveryKey: string;
if (recoveryKeyEncryptedWithMasterKey) {
recoveryKey = await cryptoWorker.decryptB64(
recoveryKeyEncryptedWithMasterKey,

View file

@ -308,6 +308,7 @@ export async function deriveSensitiveKey(passphrase: string, salt: string) {
memLimit /= 2;
}
}
throw new Error("Failed to derive key: Memory limit exceeded");
}
export async function deriveInteractiveKey(passphrase: string, salt: string) {

View file

@ -1,11 +1,10 @@
import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
import type { Dispatch, SetStateAction } from "react";
import { useEffect, useState } from "react";
export function useLocalState<T>(
key: LS_KEYS,
initialValue?: T,
): [T, Dispatch<SetStateAction<T>>] {
initialValue: T,
): [T, (newValue: T) => void] {
const [value, setValue] = useState<T>(initialValue);
useEffect(() => {

View file

@ -29,19 +29,10 @@ export enum LS_KEYS {
CLIENT_PACKAGE = "clientPackage",
}
export const setData = (key: LS_KEYS, value: object) => {
if (typeof localStorage === "undefined") {
return null;
}
export const setData = (key: LS_KEYS, value: object) =>
localStorage.setItem(key, JSON.stringify(value));
};
export const removeData = (key: LS_KEYS) => {
if (typeof localStorage === "undefined") {
return null;
}
localStorage.removeItem(key);
};
export const removeData = (key: LS_KEYS) => localStorage.removeItem(key);
export const getData = (key: LS_KEYS) => {
try {
@ -60,9 +51,4 @@ export const getData = (key: LS_KEYS) => {
}
};
export const clearData = () => {
if (typeof localStorage === "undefined") {
return null;
}
localStorage.clear();
};
export const clearData = () => localStorage.clear();

View file

@ -3,31 +3,14 @@ export enum SESSION_KEYS {
KEY_ENCRYPTION_KEY = "keyEncryptionKey",
}
export const setKey = (key: SESSION_KEYS, value: object) => {
if (typeof sessionStorage === "undefined") {
return null;
}
export const setKey = (key: SESSION_KEYS, value: object) =>
sessionStorage.setItem(key, JSON.stringify(value));
};
export const getKey = (key: SESSION_KEYS) => {
if (typeof sessionStorage === "undefined") {
return null;
}
const value = sessionStorage.getItem(key);
return value && JSON.parse(value);
};
export const removeKey = (key: SESSION_KEYS) => {
if (typeof sessionStorage === "undefined") {
return null;
}
sessionStorage.removeItem(key);
};
export const removeKey = (key: SESSION_KEYS) => sessionStorage.removeItem(key);
export const clearKeys = () => {
if (typeof sessionStorage === "undefined") {
return null;
}
sessionStorage.clear();
};
export const clearKeys = () => sessionStorage.clear();

View file

@ -33,7 +33,7 @@ export const getComponents = (
styleOverrides: {
root: {
".MuiBackdrop-root": {
backgroundColor: colors.backdrop.faint,
backgroundColor: colors.backdrop?.faint,
},
},
},
@ -42,10 +42,10 @@ export const getComponents = (
styleOverrides: {
root: {
".MuiBackdrop-root": {
backgroundColor: colors.backdrop.faint,
backgroundColor: colors.backdrop?.faint,
},
"& .MuiDialog-paper": {
filter: getDropShadowStyle(colors.shadows.float),
filter: getDropShadowStyle(colors.shadows?.float),
},
"& .MuiDialogTitle-root": {
padding: "16px",
@ -72,14 +72,14 @@ export const getComponents = (
},
MuiLink: {
defaultProps: {
color: colors.accent.A500,
color: colors.accent?.A500,
underline: "none",
},
styleOverrides: {
root: {
"&:hover": {
underline: "always",
color: colors.accent.A500,
color: colors.accent?.A500,
},
},
},
@ -95,8 +95,8 @@ export const getComponents = (
borderRadius: "4px",
textTransform: "none",
fontWeight: "bold",
fontSize: typography.body.fontSize,
lineHeight: typography.body.lineHeight,
fontSize: typography.body?.fontSize,
lineHeight: typography.body?.lineHeight,
},
startIcon: {
marginRight: "12px",
@ -191,8 +191,8 @@ export const getComponents = (
},
});
const getDropShadowStyle = (shadows: Shadow[]) => {
return shadows
const getDropShadowStyle = (shadows: Shadow[] | undefined) => {
return (shadows ?? [])
.map(
(shadow) =>
`drop-shadow(${shadow.x}px ${shadow.y}px ${shadow.blur}px ${shadow.color})`,
@ -200,20 +200,29 @@ const getDropShadowStyle = (shadows: Shadow[]) => {
.join(" ");
};
function getIconColor(ownerState, colors: ThemeColorsOptions) {
interface IconColorableOwnerState {
color?: string;
disabled?: boolean;
}
function getIconColor(
ownerState: IconColorableOwnerState,
colors: ThemeColorsOptions,
) {
switch (ownerState.color) {
case "primary":
return {
color: colors.stroke.base,
color: colors.stroke?.base,
};
case "secondary":
return {
color: colors.stroke.muted,
color: colors.stroke?.muted,
};
}
if (ownerState.disabled) {
return {
color: colors.stroke.faint,
color: colors.stroke?.faint,
};
}
return {};
}

View file

@ -70,6 +70,7 @@ declare module "@mui/material/Button" {
success: false;
info: false;
warning: false;
inherit: false;
}
}
declare module "@mui/material/Checkbox" {

View file

@ -1,3 +1,4 @@
import { ensure } from "@/utils/ensure";
import type { PaletteOptions, ThemeColorsOptions } from "@mui/material";
import { THEME_COLOR } from "../constants";
@ -20,38 +21,39 @@ export const getPalletteOptions = (
): PaletteOptions => {
return {
primary: {
main: colors.fill.base,
dark: colors.fill.basePressed,
// TODO: Refactor this code to not require this ensure
main: ensure(colors.fill?.base),
dark: colors.fill?.basePressed,
contrastText:
themeColor === "dark" ? colors.black.base : colors.white.base,
themeColor === "dark" ? colors.black?.base : colors.white?.base,
},
secondary: {
main: colors.fill.faint,
dark: colors.fill.faintPressed,
contrastText: colors.text.base,
main: ensure(colors.fill?.faint),
dark: colors.fill?.faintPressed,
contrastText: colors.text?.base,
},
accent: {
main: colors.accent.A500,
dark: colors.accent.A700,
contrastText: colors.white.base,
main: ensure(colors.accent?.A500),
dark: colors.accent?.A700,
contrastText: colors.white?.base,
},
critical: {
main: colors.danger.A700,
dark: colors.danger.A800,
contrastText: colors.white.base,
main: ensure(colors.danger?.A700),
dark: colors.danger?.A800,
contrastText: colors.white?.base,
},
background: {
default: colors.background.base,
paper: colors.background.elevated,
default: colors.background?.base,
paper: colors.background?.elevated,
},
text: {
primary: colors.text.base,
secondary: colors.text.muted,
disabled: colors.text.faint,
base: colors.text.base,
muted: colors.text.muted,
faint: colors.text.faint,
primary: colors.text?.base,
secondary: colors.text?.muted,
disabled: colors.text?.faint,
base: colors.text?.base,
muted: colors.text?.muted,
faint: colors.text?.faint,
},
divider: colors.stroke.faint,
divider: colors.stroke?.faint,
};
};

View file

@ -1,3 +0,0 @@
import { THEME_COLOR } from "./constants";
export type SetTheme = React.Dispatch<React.SetStateAction<THEME_COLOR>>;

View file

@ -17,6 +17,7 @@
"**/*.tsx",
"**/*.js",
"themes/mui-theme.d.ts",
"../next/log-web.ts"
"../next/log-web.ts",
"../next/global-electron.d.ts"
]
}

View file

@ -26,6 +26,12 @@ export function isPromise<T>(obj: T | Promise<T>): obj is Promise<T> {
export async function retryAsyncFunction<T>(
request: (abort?: () => void) => Promise<T>,
waitTimeBeforeNextTry?: number[],
// Need to use @ts-ignore since this same file is currently included with
// varying tsconfigs, and the error is only surfaced in the stricter ones of
// them.
//
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TSC fails to detect that the exit of the loop is unreachable
): Promise<T> {
if (!waitTimeBeforeNextTry) waitTimeBeforeNextTry = [2000, 5000, 10000];