Skip to content

Commit

Permalink
📝 (auth) Enhance email authentication flow with a redirect page
Browse files Browse the repository at this point in the history
Closes #1824
  • Loading branch information
baptisteArno committed Oct 7, 2024
1 parent c55655a commit 7b22e3f
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 72 deletions.
5 changes: 3 additions & 2 deletions apps/builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
"@giphy/js-fetch-api": "5.0.0",
"@giphy/react-components": "7.1.0",
"@googleapis/drive": "8.0.0",
"@typebot.io/trpc-openapi": "workspace:*",
"@paralleldrive/cuid2": "2.2.1",
"@sentry/nextjs": "7.77.0",
"@tanstack/react-query": "4.29.19",
Expand All @@ -41,6 +40,8 @@
"@typebot.io/env": "workspace:*",
"@typebot.io/nextjs": "workspace:*",
"@typebot.io/theme": "workspace:*",
"@typebot.io/transactional": "workspace:*",
"@typebot.io/trpc-openapi": "workspace:*",
"@typebot.io/typebot": "workspace:*",
"@typebot.io/whatsapp": "workspace:*",
"@udecode/plate-basic-marks": "30.5.3",
Expand Down Expand Up @@ -70,11 +71,11 @@
"ky": "1.2.4",
"micro-cors": "0.1.1",
"next": "14.2.13",
"@typebot.io/transactional": "workspace:*",
"next-auth": "4.24.8",
"nextjs-cors": "2.1.2",
"nodemailer": "6.9.15",
"nprogress": "0.2.0",
"nuqs": "^1.19.3",
"openai": "4.52.7",
"papaparse": "5.4.1",
"pexels": "^1.4.0",
Expand Down
6 changes: 5 additions & 1 deletion apps/builder/src/features/account/UserProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ export const UserProvider = ({ children }: { children: ReactNode }) => {
useEffect(() => {
if (!router.isReady) return;
if (status === "loading") return;
const isSignInPath = ["/signin", "/register"].includes(router.pathname);
const isSignInPath = [
"/signin",
"/register",
"/signin/email-redirect",
].includes(router.pathname);
const isPathPublicFriendly = /\/typebots\/.+\/(edit|theme|settings)/.test(
router.pathname,
);
Expand Down
42 changes: 42 additions & 0 deletions apps/builder/src/features/auth/components/EmailRedirectPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Seo } from "@/components/Seo";
import { Button, Heading, Tag, Text, VStack } from "@chakra-ui/react";
import { useQueryState } from "nuqs";
import { toast } from "sonner";
import { createEmailMagicLink } from "../helpers/createEmailMagicLink";

export const EmailRedirectPage = () => {
const [redirectPath] = useQueryState("redirectPath");
const [email] = useQueryState("email");
const [token] = useQueryState("token");

const redirectToMagicLink = () => {
if (!token || !email) {
toast.error("Missing token or email query params");
return;
}
window.location.assign(
createEmailMagicLink(token, email, redirectPath ?? undefined),
);
};

if (!email || !token) return null;

return (
<VStack spacing={4} h="100vh" justifyContent="center">
<Seo title={"Email auth confirmation"} />
<Heading
onClick={() => {
throw new Error("Sentry is working");
}}
>
Email authentication
</Heading>
<Text>
You are about to login with <Tag>{email}</Tag>
</Text>
<Button onClick={redirectToMagicLink} colorScheme="blue">
Continue
</Button>
</VStack>
);
};
3 changes: 3 additions & 0 deletions apps/builder/src/features/auth/components/SignInError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ export const SignInError = ({ error }: Props) => {
OAuthCreateAccount: t("auth.error.email"),
EmailCreateAccount: t("auth.error.default"),
Callback: t("auth.error.default"),
Verification:
"Your email authentication request is expired. Please sign in again.",
OAuthAccountNotLinked: t("auth.error.oauthNotLinked"),
default: t("auth.error.unknown"),
};

if (!errors[error]) return null;
return (
<Alert status="error" variant="solid" rounded="md">
Expand Down
31 changes: 13 additions & 18 deletions apps/builder/src/features/auth/components/SignInForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ import {
signIn,
useSession,
} from "next-auth/react";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useQueryState } from "nuqs";
import type { ChangeEvent, FormEvent } from "react";
import React, { useEffect, useState } from "react";
import { createEmailMagicLink } from "../helpers/createEmailMagicLink";
import { DividerWithText } from "./DividerWithText";
import { SignInError } from "./SignInError";
import { SocialLoginButtons } from "./SocialLoginButtons";
Expand All @@ -43,6 +45,8 @@ export const SignInForm = ({
}: Props & HTMLChakraProps<"form">) => {
const { t } = useTranslate();
const router = useRouter();
const [authError, setAuthError] = useQueryState("error");
const [redirectPath] = useQueryState("redirectPath");
const { status } = useSession();
const [authLoading, setAuthLoading] = useState(false);
const [isLoadingProviders, setIsLoadingProviders] = useState(true);
Expand All @@ -61,7 +65,6 @@ export const SignInForm = ({

useEffect(() => {
if (status === "authenticated") {
const redirectPath = router.query.redirectPath?.toString();
router.replace(redirectPath ? sanitizeUrl(redirectPath) : "/typebots");
return;
}
Expand All @@ -73,21 +76,21 @@ export const SignInForm = ({
}, [status, router]);

useEffect(() => {
if (!router.isReady) return;
if (router.query.error === "ip-banned") {
if (authError === "ip-banned") {
showToast({
status: "info",
description:
"Your account has suspicious activity and is being reviewed by our team. Feel free to contact us.",
});
}
}, [router.isReady, router.query.error, showToast]);
}, [authError, showToast]);

const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) =>
setEmailValue(e.target.value);

const handleEmailSubmit = async (e: FormEvent) => {
e.preventDefault();
setAuthError(null);
if (isMagicCodeSent) return;
setAuthLoading(true);
try {
Expand Down Expand Up @@ -127,16 +130,10 @@ export const SignInForm = ({
setAuthLoading(false);
};

const checkCodeAndRedirect = async (token: string) => {
const url = new URL(`${window.location.origin}/api/auth/callback/email`);
url.searchParams.set("token", token);
url.searchParams.set("email", emailValue);
const redirectPath = router.query.redirectPath?.toString();
url.searchParams.set(
"callbackUrl",
`${window.location.origin}${redirectPath ?? "/typebots"}`,
const redirectToMagicLink = (token: string) => {
window.location.assign(
createEmailMagicLink(token, emailValue, redirectPath ?? undefined),
);
window.location.assign(url.toString());
};

if (isLoadingProviders) return <Spinner />;
Expand Down Expand Up @@ -184,9 +181,6 @@ export const SignInForm = ({
)}
</>
)}
{router.query.error && (
<SignInError error={router.query.error.toString()} />
)}
<SlideFade offsetY="20px" in={isMagicCodeSent} unmountOnExit>
<Stack spacing={3}>
<Alert status="success" w="100%">
Expand All @@ -201,7 +195,7 @@ export const SignInForm = ({
<FormControl as={VStack} spacing={0}>
<FormLabel>Login code:</FormLabel>
<HStack>
<PinInput onComplete={checkCodeAndRedirect}>
<PinInput onComplete={redirectToMagicLink}>
<PinInputField />
<PinInputField />
<PinInputField />
Expand All @@ -213,6 +207,7 @@ export const SignInForm = ({
</FormControl>
</Stack>
</SlideFade>
{authError && <SignInError error={authError} />}
</Stack>
);
};
14 changes: 14 additions & 0 deletions apps/builder/src/features/auth/helpers/createEmailMagicLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const createEmailMagicLink = (
token: string,
email: string,
redirectPath?: string,
) => {
const url = new URL(`${window.location.origin}/api/auth/callback/email`);
url.searchParams.set("token", token);
url.searchParams.set("email", email);
url.searchParams.set(
"callbackUrl",
`${window.location.origin}${redirectPath ?? "/typebots"}`,
);
return url.toString();
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,17 @@ export const sendVerificationRequest = async ({ identifier, url }: Props) => {
try {
const code = extractCodeFromUrl(url);
if (!code) throw new Error("Could not extract code from url");
await sendLoginCodeEmail({ url, code, to: identifier });
const redirectEmailUrl = new URL("/signin/email-redirect", url);
redirectEmailUrl.searchParams.set("token", code);
redirectEmailUrl.searchParams.set("email", identifier);
const redirectPath = new URL(url).searchParams.get("redirectPath");
if (redirectPath)
redirectEmailUrl.searchParams.set("redirectPath", redirectPath);
await sendLoginCodeEmail({
url: redirectEmailUrl.toString(),
code,
to: identifier,
});
} catch (err) {
console.error(err);
throw new Error(`Magic link email could not be sent. See error above.`);
Expand Down
5 changes: 5 additions & 0 deletions apps/builder/src/pages/signin/email-redirect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { EmailRedirectPage } from "@/features/auth/components/EmailRedirectPage";

export default function Page() {
return <EmailRedirectPage />;
}
Binary file modified bun.lockb
Binary file not shown.
45 changes: 9 additions & 36 deletions packages/transactional/emails/LoginCodeEmail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,15 @@ export const LoginCodeEmail = ({ url, code }: Props) => (
}}
/>
<Heading style={heading}>Your login code for Typebot</Heading>
<Section style={buttonContainer}>
<Button style={button} href={url}>
Login to Typebot
</Button>
</Section>
<code style={codeStyle}>{code}</code>
<Text style={paragraph}>
This link and code will only be valid for the next 5 minutes. If the
link does not work, you can use the login verification code directly:
This code will only be valid for the next 5 minutes.
</Text>
<Text style={paragraph}>
You can also sign in by <Link href={url}>clicking here</Link>.
</Text>
<code style={codeStyle}>{code}</code>
<Hr style={hr} />
<Link href="https://typebot.io" style={reportLink}>
Typebot
</Link>
<Text style={footerText}>Typebot - Build faster, Chat smarter</Text>
</Container>
</Body>
</Html>
Expand All @@ -65,12 +60,6 @@ LoginCodeEmail.PreviewProps = {

export default LoginCodeEmail;

const logo = {
borderRadius: 21,
width: 42,
height: 42,
};

const main = {
backgroundColor: "#ffffff",
fontFamily:
Expand All @@ -93,31 +82,15 @@ const heading = {
};

const paragraph = {
margin: "0 0 15px",
margin: "15px 0 15px",
fontSize: "15px",
lineHeight: "1.4",
color: "#3c4149",
};

const buttonContainer = {
padding: "27px 0 27px",
};

const button = {
backgroundColor: "#0042DA",
borderRadius: "3px",
fontWeight: "600",
color: "#fff",
fontSize: "15px",
textDecoration: "none",
textAlign: "center" as const,
display: "block",
padding: "11px 23px",
};

const reportLink = {
const footerText = {
color: "#3c4149",
fontSize: "14px",
color: "#b4becc",
};

const hr = {
Expand Down
Loading

0 comments on commit 7b22e3f

Please sign in to comment.