[web] Use purpose to distinguish signup / login (#4421)
This commit is contained in:
@@ -144,11 +144,13 @@ export default function LandingPage() {
|
||||
</MobileBox>
|
||||
<DesktopBox>
|
||||
<SideBox>
|
||||
{showLogin ? (
|
||||
<Login {...{ signUp, host }} />
|
||||
) : (
|
||||
<SignUp {...{ router, login, host }} />
|
||||
)}
|
||||
<Box sx={{ maxWidth: "320px" }}>
|
||||
{showLogin ? (
|
||||
<Login {...{ signUp, host }} />
|
||||
) : (
|
||||
<SignUp {...{ router, login, host }} />
|
||||
)}
|
||||
</Box>
|
||||
</SideBox>
|
||||
</DesktopBox>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FormPaperFooter, FormPaperTitle } from "@/base/components/FormPaper";
|
||||
import { isMuseumHTTPError } from "@/base/http";
|
||||
import log from "@/base/log";
|
||||
import LinkButton from "@ente/shared/components/LinkButton";
|
||||
import SingleInputForm, {
|
||||
@@ -10,7 +11,7 @@ import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { PAGES } from "../constants/pages";
|
||||
import { getSRPAttributes } from "../services/srp-remote";
|
||||
import { sendOtt } from "../services/user";
|
||||
import { sendOTT } from "../services/user";
|
||||
|
||||
interface LoginProps {
|
||||
signUp: () => void;
|
||||
@@ -26,26 +27,30 @@ export const Login: React.FC<LoginProps> = ({ signUp, host }) => {
|
||||
setFieldError,
|
||||
) => {
|
||||
try {
|
||||
await setLSUser({ email });
|
||||
const srpAttributes = await getSRPAttributes(email);
|
||||
log.debug(() => ["srpAttributes", JSON.stringify(srpAttributes)]);
|
||||
if (!srpAttributes || srpAttributes.isEmailMFAEnabled) {
|
||||
await sendOtt(email);
|
||||
try {
|
||||
await sendOTT(email, "login");
|
||||
} catch (e) {
|
||||
if (
|
||||
await isMuseumHTTPError(e, 404, "USER_NOT_REGISTERED")
|
||||
) {
|
||||
setFieldError(t("email_not_registered"));
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
await setLSUser({ email });
|
||||
void router.push(PAGES.VERIFY);
|
||||
} else {
|
||||
await setLSUser({ email });
|
||||
setData(LS_KEYS.SRP_ATTRIBUTES, srpAttributes);
|
||||
void router.push(PAGES.CREDENTIALS);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setFieldError(
|
||||
`${t("generic_error_retry")} (reason:${e.message})`,
|
||||
);
|
||||
} else {
|
||||
setFieldError(
|
||||
`${t("generic_error_retry")} (reason:${JSON.stringify(e)})`,
|
||||
);
|
||||
}
|
||||
log.error("Login failed", e);
|
||||
setFieldError(t("generic_error"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FormPaperFooter, FormPaperTitle } from "@/base/components/FormPaper";
|
||||
import { LoadingButton } from "@/base/components/mui/LoadingButton";
|
||||
import { isMuseumHTTPError } from "@/base/http";
|
||||
import log from "@/base/log";
|
||||
import { LS_KEYS, setLSUser } from "@ente/shared//storage/localStorage";
|
||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
@@ -37,7 +38,7 @@ import { Trans } from "react-i18next";
|
||||
import * as Yup from "yup";
|
||||
import { PAGES } from "../constants/pages";
|
||||
import { generateKeyAndSRPAttributes } from "../services/srp";
|
||||
import { sendOtt } from "../services/user";
|
||||
import { sendOTT } from "../services/user";
|
||||
import { isWeakPassword } from "../utils/password";
|
||||
import { PasswordStrengthHint } from "./PasswordStrength";
|
||||
|
||||
@@ -81,15 +82,18 @@ export const SignUp: React.FC<SignUpProps> = ({ router, login, host }) => {
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
await setLSUser({ email });
|
||||
setLocalReferralSource(referral);
|
||||
await sendOtt(email);
|
||||
await sendOTT(email, "signup");
|
||||
await setLSUser({ email });
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : "";
|
||||
setFieldError(
|
||||
"confirm",
|
||||
`${t("generic_error_retry")} ${message}`,
|
||||
);
|
||||
log.error("Signup failed", e);
|
||||
if (
|
||||
await isMuseumHTTPError(e, 409, "USER_ALREADY_REGISTERED")
|
||||
) {
|
||||
setFieldError("email", t("email_already_registered"));
|
||||
} else {
|
||||
setFieldError("email", t("generic_error"));
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
FormPaperTitle,
|
||||
} from "@/base/components/FormPaper";
|
||||
import { LoadingButton } from "@/base/components/mui/LoadingButton";
|
||||
import { isHTTPErrorWithStatus } from "@/base/http";
|
||||
import log from "@/base/log";
|
||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import LinkButton from "@ente/shared/components/LinkButton";
|
||||
@@ -16,7 +17,7 @@ import { useEffect, useState } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import * as Yup from "yup";
|
||||
import { appHomeRoute } from "../services/redirect";
|
||||
import { changeEmail, sendOTTForEmailChange } from "../services/user";
|
||||
import { changeEmail, sendOTT } from "../services/user";
|
||||
import type { PageProps } from "../types/page";
|
||||
|
||||
const Page: React.FC<PageProps> = () => {
|
||||
@@ -60,7 +61,7 @@ const ChangeEmailForm: React.FC = () => {
|
||||
) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await sendOTTForEmailChange(email);
|
||||
await sendOTT(email, "change");
|
||||
setEmail(email);
|
||||
setShowOttInputVisibility(true);
|
||||
setShowMessage(true);
|
||||
@@ -71,7 +72,12 @@ const ChangeEmailForm: React.FC = () => {
|
||||
// }, 250);
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
setFieldError("email", t("email_already_taken"));
|
||||
setFieldError(
|
||||
"email",
|
||||
isHTTPErrorWithStatus(e, 403)
|
||||
? t("email_already_taken")
|
||||
: t("generic_error"),
|
||||
);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PAGES } from "@/accounts/constants/pages";
|
||||
import { sendOtt } from "@/accounts/services/user";
|
||||
import { sendOTT } from "@/accounts/services/user";
|
||||
import {
|
||||
FormPaper,
|
||||
FormPaperFooter,
|
||||
@@ -48,7 +48,7 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
return;
|
||||
}
|
||||
if (!user?.encryptedToken && !user?.token) {
|
||||
void sendOtt(user.email);
|
||||
void sendOTT(user.email, undefined);
|
||||
stashRedirect(PAGES.RECOVER);
|
||||
void router.push(PAGES.VERIFY);
|
||||
return;
|
||||
|
||||
@@ -42,7 +42,7 @@ import { configureSRP } from "../services/srp";
|
||||
import type { SRPAttributes, SRPSetupAttributes } from "../services/srp-remote";
|
||||
import { getSRPAttributes } from "../services/srp-remote";
|
||||
import type { UserVerificationResponse } from "../services/user";
|
||||
import { putAttributes, sendOtt, verifyOtt } from "../services/user";
|
||||
import { putAttributes, sendOTT, verifyOtt } from "../services/user";
|
||||
import type { PageProps } from "../types/page";
|
||||
|
||||
const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
@@ -170,7 +170,7 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
|
||||
const resendEmail = async () => {
|
||||
setResend(1);
|
||||
await sendOtt(email);
|
||||
await sendOTT(email, undefined);
|
||||
setResend(2);
|
||||
setTimeout(() => setResend(0), 3000);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { appName } from "@/base/app";
|
||||
import type { B64EncryptionResult } from "@/base/crypto/libsodium";
|
||||
import { authenticatedRequestHeaders, ensureOk } from "@/base/http";
|
||||
import {
|
||||
authenticatedRequestHeaders,
|
||||
ensureOk,
|
||||
publicRequestHeaders,
|
||||
} from "@/base/http";
|
||||
import { apiURL } from "@/base/origins";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
@@ -54,12 +57,31 @@ export interface RecoveryKey {
|
||||
recoveryKeyDecryptionNonce: string;
|
||||
}
|
||||
|
||||
export const sendOtt = async (email: string) => {
|
||||
return HTTPService.post(await apiURL("/users/ott"), {
|
||||
email,
|
||||
client: appName == "auth" ? "totp" : "web",
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Ask remote to send a OTP / OTT to the given email to verify that the user has
|
||||
* access to it. Subsequent the app will pass this OTT back via the
|
||||
* {@link verifyOTT} method.
|
||||
*
|
||||
* @param email The email to verify.
|
||||
*
|
||||
* @param purpose In which context is the email being verified. Remote applies
|
||||
* additional business rules depending on this. For example, passing the purpose
|
||||
* "login" ensures that the OTT is only sent to an already registered email.
|
||||
*
|
||||
* In cases where the purpose is ambiguous (e.g. we're not sure if it is an
|
||||
* existing login or a new signup), the purpose can be set to `undefined`.
|
||||
*/
|
||||
export const sendOTT = async (
|
||||
email: string,
|
||||
purpose: "change" | "signup" | "login" | undefined,
|
||||
) =>
|
||||
ensureOk(
|
||||
await fetch(await apiURL("/users/ott"), {
|
||||
method: "POST",
|
||||
headers: publicRequestHeaders(),
|
||||
body: JSON.stringify({ email, purpose }),
|
||||
}),
|
||||
);
|
||||
|
||||
export const verifyOtt = async (
|
||||
email: string,
|
||||
@@ -171,14 +193,6 @@ export const changeEmail = async (email: string, ott: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const sendOTTForEmailChange = async (email: string) => {
|
||||
await HTTPService.post(await apiURL("/users/ott"), {
|
||||
email,
|
||||
client: "web",
|
||||
purpose: "change",
|
||||
});
|
||||
};
|
||||
|
||||
export const setupTwoFactor = async () => {
|
||||
const resp = await HTTPService.post(
|
||||
await apiURL("/users/two-factor/setup"),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { retryAsyncOperation } from "@/utils/promise";
|
||||
import { z } from "zod";
|
||||
import { clientPackageName } from "./app";
|
||||
import { ensureAuthToken } from "./local-user";
|
||||
import log from "./log";
|
||||
|
||||
/**
|
||||
* Return headers that should be passed alongwith (almost) all authenticated
|
||||
@@ -101,6 +103,12 @@ export const ensureOk = (res: Response) => {
|
||||
if (!res.ok) throw new HTTPError(res);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if this is a HTTP error with the given {@link httpStatus}.
|
||||
*/
|
||||
export const isHTTPErrorWithStatus = (e: unknown, httpStatus: number) =>
|
||||
e instanceof HTTPError && e.res.status == httpStatus;
|
||||
|
||||
/**
|
||||
* Return true if this is a HTTP "client" error.
|
||||
*
|
||||
@@ -120,6 +128,36 @@ export const isHTTP4xxError = (e: unknown) =>
|
||||
export const isHTTP401Error = (e: unknown) =>
|
||||
e instanceof HTTPError && e.res.status == 401;
|
||||
|
||||
/**
|
||||
* Return `true` if this is an error because of a HTTP failure response returned
|
||||
* by museum with the given "code" and HTTP status.
|
||||
*
|
||||
* For some known set of errors, museum returns a payload of the form
|
||||
*
|
||||
* {"code":"USER_NOT_REGISTERED","message":"User is not registered"}
|
||||
*
|
||||
* where the code can be used to match a specific reason for the HTTP request
|
||||
* failing. This function can be used as a predicate to check both the HTTP
|
||||
* status code and the "code" within the payload.
|
||||
*/
|
||||
export const isMuseumHTTPError = async (
|
||||
e: unknown,
|
||||
httpStatus: number,
|
||||
code: string,
|
||||
) => {
|
||||
if (e instanceof HTTPError && e.res.status == httpStatus) {
|
||||
try {
|
||||
const payload = z
|
||||
.object({ code: z.string() })
|
||||
.parse(await e.res.json());
|
||||
return payload.code == code;
|
||||
} catch (e) {
|
||||
log.warn("Ignoring error when parsing error payload", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* A helper function to adapt {@link retryAsyncOperation} for HTTP fetches.
|
||||
*
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
"ENTER_EMAIL": "Enter email address",
|
||||
"EMAIL_ERROR": "Enter a valid email",
|
||||
"required": "required",
|
||||
"email_not_registered": "Email not registered",
|
||||
"email_already_registered": "Email already registered",
|
||||
"EMAIL_SENT": "Verification code sent to <a>{{email}}</a>",
|
||||
"CHECK_INBOX": "Please check your inbox (and spam) to complete verification",
|
||||
"ENTER_OTT": "Verification code",
|
||||
|
||||
Reference in New Issue
Block a user