mirror of
https://github.com/baptisteArno/typebot.io.git
synced 2026-06-05 21:04:43 +08:00
⚡️ (auth) Attempt to fix issue with link openers in corporate setups (#1819)
Also add option to provide type the login code instead of link-only option Closes #1817
This commit is contained in:
parent
6ec8f71cac
commit
d3a869498e
@ -70,9 +70,10 @@
|
||||
"ky": "1.2.4",
|
||||
"micro-cors": "0.1.1",
|
||||
"next": "14.2.13",
|
||||
"next-auth": "4.22.1",
|
||||
"@typebot.io/transactional": "workspace:*",
|
||||
"next-auth": "4.24.8",
|
||||
"nextjs-cors": "2.1.2",
|
||||
"nodemailer": "6.9.8",
|
||||
"nodemailer": "6.9.15",
|
||||
"nprogress": "0.2.0",
|
||||
"openai": "4.52.7",
|
||||
"papaparse": "5.4.1",
|
||||
|
||||
@ -5,14 +5,18 @@ import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Button,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
HStack,
|
||||
type HTMLChakraProps,
|
||||
Input,
|
||||
PinInput,
|
||||
PinInputField,
|
||||
SlideFade,
|
||||
Spinner,
|
||||
Stack,
|
||||
Text,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import type { BuiltInProviderType } from "next-auth/providers/index";
|
||||
@ -44,7 +48,7 @@ export const SignInForm = ({
|
||||
const [isLoadingProviders, setIsLoadingProviders] = useState(true);
|
||||
|
||||
const [emailValue, setEmailValue] = useState(defaultEmail ?? "");
|
||||
const [isMagicLinkSent, setIsMagicLinkSent] = useState(false);
|
||||
const [isMagicCodeSent, setIsMagicCodeSent] = useState(false);
|
||||
|
||||
const { showToast } = useToast();
|
||||
const [providers, setProviders] =
|
||||
@ -84,7 +88,7 @@ export const SignInForm = ({
|
||||
|
||||
const handleEmailSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (isMagicLinkSent) return;
|
||||
if (isMagicCodeSent) return;
|
||||
setAuthLoading(true);
|
||||
try {
|
||||
const response = await signIn("email", {
|
||||
@ -112,7 +116,7 @@ export const SignInForm = ({
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setIsMagicLinkSent(true);
|
||||
setIsMagicCodeSent(true);
|
||||
}
|
||||
} catch (e) {
|
||||
showToast({
|
||||
@ -123,6 +127,18 @@ 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"}`,
|
||||
);
|
||||
window.location.assign(url.toString());
|
||||
};
|
||||
|
||||
if (isLoadingProviders) return <Spinner />;
|
||||
if (hasNoAuthProvider)
|
||||
return (
|
||||
@ -138,7 +154,7 @@ export const SignInForm = ({
|
||||
);
|
||||
return (
|
||||
<Stack spacing="4" w="330px">
|
||||
{!isMagicLinkSent && (
|
||||
{!isMagicCodeSent && (
|
||||
<>
|
||||
<SocialLoginButtons providers={providers} />
|
||||
{providers?.email && (
|
||||
@ -159,7 +175,7 @@ export const SignInForm = ({
|
||||
isLoading={
|
||||
["loading", "authenticated"].includes(status) || authLoading
|
||||
}
|
||||
isDisabled={isMagicLinkSent}
|
||||
isDisabled={isMagicCodeSent}
|
||||
>
|
||||
{t("auth.emailSubmitButton.label")}
|
||||
</Button>
|
||||
@ -171,8 +187,8 @@ export const SignInForm = ({
|
||||
{router.query.error && (
|
||||
<SignInError error={router.query.error.toString()} />
|
||||
)}
|
||||
<SlideFade offsetY="20px" in={isMagicLinkSent} unmountOnExit>
|
||||
<Flex>
|
||||
<SlideFade offsetY="20px" in={isMagicCodeSent} unmountOnExit>
|
||||
<Stack spacing={3}>
|
||||
<Alert status="success" w="100%">
|
||||
<HStack>
|
||||
<AlertIcon />
|
||||
@ -182,7 +198,20 @@ export const SignInForm = ({
|
||||
</Stack>
|
||||
</HStack>
|
||||
</Alert>
|
||||
</Flex>
|
||||
<FormControl as={VStack} spacing={0}>
|
||||
<FormLabel>Login code:</FormLabel>
|
||||
<HStack>
|
||||
<PinInput onComplete={checkCodeAndRedirect}>
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
</PinInput>
|
||||
</HStack>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
</SlideFade>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { sendMagicLinkEmail } from "@typebot.io/emails/emails/MagicLinkEmail";
|
||||
import { sendLoginCodeEmail } from "@typebot.io/transactional/emails/LoginCodeEmail";
|
||||
|
||||
type Props = {
|
||||
identifier: string;
|
||||
@ -7,9 +7,17 @@ type Props = {
|
||||
|
||||
export const sendVerificationRequest = async ({ identifier, url }: Props) => {
|
||||
try {
|
||||
await sendMagicLinkEmail({ url, to: identifier });
|
||||
const code = extractCodeFromUrl(url);
|
||||
if (!code) throw new Error("Could not extract code from url");
|
||||
await sendLoginCodeEmail({ url, code, to: identifier });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw new Error(`Magic link email could not be sent. See error above.`);
|
||||
}
|
||||
};
|
||||
|
||||
const extractCodeFromUrl = (url: string) => {
|
||||
const urlParts = url.split("?");
|
||||
const queryParams = new URLSearchParams(urlParts[1]);
|
||||
return queryParams.get("token");
|
||||
};
|
||||
|
||||
@ -73,7 +73,12 @@ if (env.NEXT_PUBLIC_SMTP_FROM && !env.SMTP_AUTH_DISABLED)
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
maxAge: 5 * 60,
|
||||
from: env.NEXT_PUBLIC_SMTP_FROM,
|
||||
generateVerificationToken() {
|
||||
const code = Math.floor(100000 + Math.random() * 900000); // random 6-digit code
|
||||
return code.toString();
|
||||
},
|
||||
sendVerificationRequest,
|
||||
}),
|
||||
);
|
||||
@ -234,6 +239,8 @@ export const getAuthOptions = ({
|
||||
});
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
// Ignore email link openers (common in enterprise setups)
|
||||
if (req.method === "HEAD") return res.status(200).end();
|
||||
const isMockingSession =
|
||||
req.method === "GET" &&
|
||||
req.url === "/api/auth/session" &&
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
"google-spreadsheet": "4.1.1",
|
||||
"next": "14.2.13",
|
||||
"nextjs-cors": "2.1.2",
|
||||
"nodemailer": "6.9.8",
|
||||
"nodemailer": "6.9.15",
|
||||
"openai": "4.52.7",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
"ky": "1.2.4",
|
||||
"libphonenumber-js": "1.10.37",
|
||||
"node-html-parser": "6.1.5",
|
||||
"nodemailer": "6.9.8",
|
||||
"nodemailer": "6.9.15",
|
||||
"openai": "4.52.7",
|
||||
"qs": "6.11.2",
|
||||
"stripe": "17.1.0"
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@faire/mjml-react": "3.3.0",
|
||||
"nodemailer": "6.9.8",
|
||||
"nodemailer": "6.9.15",
|
||||
"react": "18.2.0",
|
||||
"@typebot.io/lib": "workspace:*",
|
||||
"@typebot.io/env": "workspace:*"
|
||||
|
||||
147
packages/transactional/emails/LoginCodeEmail.tsx
Normal file
147
packages/transactional/emails/LoginCodeEmail.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Preview,
|
||||
Section,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import { render } from "@react-email/render";
|
||||
import { env } from "@typebot.io/env";
|
||||
import type { SendMailOptions } from "nodemailer";
|
||||
import type { ComponentProps } from "react";
|
||||
import { sendEmail } from "../helpers/sendEmail";
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export const LoginCodeEmail = ({ url, code }: Props) => (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>Your login code for Typebot</Preview>
|
||||
<Body style={main}>
|
||||
<Container style={container}>
|
||||
<Img
|
||||
src={`${env.NEXTAUTH_URL}/images/logo.png`}
|
||||
width="32"
|
||||
height="32"
|
||||
alt="Typebot's Logo"
|
||||
style={{
|
||||
margin: "24px 0",
|
||||
}}
|
||||
/>
|
||||
<Heading style={heading}>Your login code for Typebot</Heading>
|
||||
<Section style={buttonContainer}>
|
||||
<Button style={button} href={url}>
|
||||
Login to Typebot
|
||||
</Button>
|
||||
</Section>
|
||||
<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:
|
||||
</Text>
|
||||
<code style={codeStyle}>{code}</code>
|
||||
<Hr style={hr} />
|
||||
<Link href="https://typebot.io" style={reportLink}>
|
||||
Typebot
|
||||
</Link>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
|
||||
LoginCodeEmail.PreviewProps = {
|
||||
url: "https://typebot.io",
|
||||
code: "654778",
|
||||
} as Props;
|
||||
|
||||
export default LoginCodeEmail;
|
||||
|
||||
const logo = {
|
||||
borderRadius: 21,
|
||||
width: 42,
|
||||
height: 42,
|
||||
};
|
||||
|
||||
const main = {
|
||||
backgroundColor: "#ffffff",
|
||||
fontFamily:
|
||||
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
|
||||
};
|
||||
|
||||
const container = {
|
||||
margin: "0 auto",
|
||||
padding: "20px 0 48px",
|
||||
maxWidth: "560px",
|
||||
};
|
||||
|
||||
const heading = {
|
||||
fontSize: "24px",
|
||||
letterSpacing: "-0.5px",
|
||||
lineHeight: "1.3",
|
||||
fontWeight: "400",
|
||||
color: "#484848",
|
||||
padding: "17px 0 0",
|
||||
};
|
||||
|
||||
const paragraph = {
|
||||
margin: "0 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 = {
|
||||
fontSize: "14px",
|
||||
color: "#b4becc",
|
||||
};
|
||||
|
||||
const hr = {
|
||||
borderColor: "#dfe1e4",
|
||||
margin: "42px 0 26px",
|
||||
};
|
||||
|
||||
const codeStyle = {
|
||||
fontFamily: "monospace",
|
||||
fontWeight: "700",
|
||||
padding: "1px 4px",
|
||||
backgroundColor: "#dfe1e4",
|
||||
letterSpacing: "-0.3px",
|
||||
fontSize: "21px",
|
||||
borderRadius: "4px",
|
||||
color: "#3c4149",
|
||||
};
|
||||
|
||||
export const sendLoginCodeEmail = async ({
|
||||
to,
|
||||
...props
|
||||
}: Pick<SendMailOptions, "to"> & ComponentProps<typeof LoginCodeEmail>) =>
|
||||
sendEmail({
|
||||
to,
|
||||
subject: "Sign in to Typebot",
|
||||
html: await render(<LoginCodeEmail {...props} />),
|
||||
});
|
||||
20
packages/transactional/helpers/sendEmail.ts
Normal file
20
packages/transactional/helpers/sendEmail.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { env } from "@typebot.io/env";
|
||||
import { type SendMailOptions, createTransport } from "nodemailer";
|
||||
|
||||
export const sendEmail = (
|
||||
props: Pick<SendMailOptions, "to" | "html" | "subject">,
|
||||
) => {
|
||||
const transporter = createTransport({
|
||||
host: env.SMTP_HOST,
|
||||
port: env.SMTP_PORT,
|
||||
auth: {
|
||||
user: env.SMTP_USERNAME,
|
||||
pass: env.SMTP_PASSWORD,
|
||||
},
|
||||
});
|
||||
|
||||
return transporter.sendMail({
|
||||
from: env.NEXT_PUBLIC_SMTP_FROM,
|
||||
...props,
|
||||
});
|
||||
};
|
||||
@ -1,16 +1,17 @@
|
||||
{
|
||||
"name": "transactional",
|
||||
"name": "@typebot.io/transactional",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "SKIP_ENV_CHECK=true dotenv -e ./.env -e ../../.env -- email dev --port=3005 --dir=templates"
|
||||
"preview": "SKIP_ENV_CHECK=true dotenv -e ./.env -e ../../.env -- email dev --port=3005"
|
||||
},
|
||||
"exports": {
|
||||
"./*": "./src/*.ts"
|
||||
"./*": "./*.tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "0.0.14",
|
||||
"@react-email/components": "0.0.25",
|
||||
"@typebot.io/env": "workspace:*",
|
||||
"react-email": "2.0.0"
|
||||
"nodemailer": "6.9.15",
|
||||
"react-email": "3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typebot.io/tsconfig": "workspace:*",
|
||||
|
||||
@ -1,129 +0,0 @@
|
||||
import {
|
||||
Body,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Preview,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import { env } from "@typebot.io/env";
|
||||
|
||||
interface Props {
|
||||
magicLinkUrl: string;
|
||||
}
|
||||
|
||||
const imagesBaseUrl = `${env.NEXTAUTH_URL}/images`;
|
||||
|
||||
export const MagicLink = ({ magicLinkUrl }: Props) => (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>Log in with this magic link</Preview>
|
||||
<Body style={main}>
|
||||
<Container style={container}>
|
||||
<Img
|
||||
src={`${imagesBaseUrl}/logo.png`}
|
||||
width="32"
|
||||
height="32"
|
||||
alt="Typebot's Logo"
|
||||
style={{
|
||||
margin: "24px 0",
|
||||
}}
|
||||
/>
|
||||
<Heading style={heading}>Your magic link</Heading>
|
||||
<Link
|
||||
href={magicLinkUrl}
|
||||
target="_blank"
|
||||
style={{
|
||||
...clickLink,
|
||||
display: "block",
|
||||
marginBottom: "24px",
|
||||
}}
|
||||
>
|
||||
👉 Click here to sign in 👈
|
||||
</Link>
|
||||
<Text
|
||||
style={{
|
||||
...text,
|
||||
color: "#ababab",
|
||||
marginTop: "14px",
|
||||
marginBottom: "16px",
|
||||
}}
|
||||
>
|
||||
If you didn't try to login, you can safely ignore this email.
|
||||
</Text>
|
||||
<Text style={footer}>
|
||||
<Link
|
||||
href="https://notion.so"
|
||||
target="_blank"
|
||||
style={{ ...link, color: "#898989" }}
|
||||
>
|
||||
Typebot.io
|
||||
</Link>
|
||||
- Powering Conversations at Scale
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
|
||||
MagicLink.PreviewProps = {
|
||||
magicLinkUrl: "http://localhost:3000",
|
||||
} as Props;
|
||||
|
||||
export default MagicLink;
|
||||
|
||||
const main = {
|
||||
backgroundColor: "#ffffff",
|
||||
};
|
||||
|
||||
const container = {
|
||||
paddingLeft: "12px",
|
||||
paddingRight: "12px",
|
||||
margin: "0 auto",
|
||||
};
|
||||
|
||||
const clickLink = {
|
||||
color: "#2754C5",
|
||||
fontFamily:
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
||||
fontSize: "18px",
|
||||
};
|
||||
|
||||
const link = {
|
||||
color: "#2754C5",
|
||||
fontFamily:
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
||||
fontSize: "14px",
|
||||
textDecoration: "underline",
|
||||
};
|
||||
|
||||
const heading = {
|
||||
color: "#333",
|
||||
fontFamily:
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
||||
fontSize: "24px",
|
||||
fontWeight: "bold",
|
||||
margin: "32px 0",
|
||||
padding: "0",
|
||||
};
|
||||
|
||||
const text = {
|
||||
color: "#333",
|
||||
fontFamily:
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
||||
fontSize: "14px",
|
||||
margin: "24px 0",
|
||||
};
|
||||
|
||||
const footer = {
|
||||
color: "#898989",
|
||||
fontFamily:
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
||||
fontSize: "12px",
|
||||
lineHeight: "22px",
|
||||
marginTop: "12px",
|
||||
marginBottom: "24px",
|
||||
};
|
||||
@ -1,4 +1,7 @@
|
||||
{
|
||||
"extends": "@typebot.io/tsconfig/base.json",
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user