与AWS Lambda,AWS SES和Google Recaptcha的无服务器混音应用程序联系表
#网络开发人员 #typescript #node #remix

介绍

这篇博客文章重新审视了我(显然)“查看这个新的React框架”系列。

具体来说,我将通过在“ Grunge Stack”上构建的Digital Canvas Development website上使用的联系表格实现。

整个网站,包括此处涵盖的细节,可在GitHub上找到:https://github.com/digital-canvas-dev/digitalcanvas.dev

项目概况

我的应用程序是一个简单的网站,它将充当我新业务的登陆页面。它主要由静态内容和联系表组成。

混音是一个完整的堆栈框架,可共定位服务器端和客户端代码。 Grunge堆栈带有许多功能和库,包括通过Architect(例如npx arc)部署AWS。许多AWS工具都有资格使用低流量站点的自由层。

本指南假定您已经部署了应用程序或已准备就绪。

要获得端到端的联系表,我将使用AWS简单电子邮件服务(SES)。为了防止垃圾邮件,我将使用Google recaptcha(v2)。

组件设置

初始形式看起来与官方Remix "blog" tutorial的形式非常相似。简而言之,我们从这样的东西开始:

import { Form, useActionData, useSubmit } from '@remix-run/react';
import { InputText } from 'my-component-library';

export const Contact = () => {
  const actionData = useActionData();

  const submit = useSubmit();

  const onSubmit = async (e) => {
    await submit(e.currentTarget);
  };

  return (
    <section>
      <Form method="POST" onSubmit={onSubmit}>
        <InputText
          name="name"
          label="Name"
          errorFeedback={actionData?.errors?.name ?? null}
        />
        {/* ... other fields ... */}
        <button type="submit">Send</button>
      </Form>
    </section>
  );
};

和我们的action的核心看起来像这样:

export const action = async ({ request }) => {
  const formData = await request.formData();

  // we'll pseudocode this away.
  // it will return an object of errors, if any.
  const errors = validate(formData);

  if (errors) {
    return errors;
  }

  // todo: avoid spam
  // todo: send the email

  return json({
    success: true,
    successMessage: 'Message sent! Expect to hear back soon.',
  });
};

您会注意到两个重要的todos,我们将以相反的顺序解决,以便我们可以首先解决更有趣的部分ð

使用AWS简单电子邮件服务(SES)发送电子邮件

要与SES集成,我拉了AWS SDK(v3):

npm i @aws-sdk/client-ses

@aws-sdk/client-ses是围绕AWS SES V3的包装器,其文档是here

**重要说明:您的lambda功能必须使用nodejs v18使用AWS V3 SDK。否则,您需要使用V2 SDK。

使用AWS与Grunge堆栈一起使用AWS的一件很酷的事情是,只要您已经遵循deployment stepsclient-ses库就可以使用.deploy script中设置的环境变量的AWS键。 。

现在(查看了许多接口和文档之后)action实现变得清晰:我们可以简单地创建一个sesclient的实例,然后使用它来发送电子邮件!

架构注:

我们可以将这些方法分解为一些逻辑(可重复使用,可测试的)部分。

以这种方式,我们可以将实例化在一个地方并稍后重复使用。

将所有内容放在一起,逻辑可能看起来像这样...

仅服务器的ses.server.ts文件:

import { ActionArgs, json } from '@remix-run/node';
import { SendEmailCommand, SendEmailCommandInput, SESClient } from '@aws-sdk/client-ses';

export const sendEmail = async (params: SendEmailCommandInput) => {

  const sesClient = new SESClient({
    region: 'us-east-1',
  });

  const command = new SendEmailCommand(params);

  return await sesClient.send(command);
};

和更新的操作:

export const action = async ({ request }: ActionArgs): Promise<{ success: true, successMessage: string; } | {
  success: false,
  errors: { form: string }
}> => {
  const formData = await request.formData();

  const requesterName = formData.get('name');

  const params = {
    Source: 'no-reply@...',
    Destination: {
      ToAddresses: ['...']
    },
    Message: {
      Subject: {
        Data: 'Form submission'
      },
      Body: {
        Html: {
          Data: `Someone submitted the contact form: Name ${requesterName}, Email: ...`
        }
      }
    }
  };

  const resp = await sendEmail(params);

  const sentError = resp.$metadata.httpStatusCode === 200 ? null : { form: 'Error sending email.' };

  if (sentError) {
    return json({
      success: false,
      errors: sentError
    });
  }

  return json({
    success: true,
    successMessage: 'Thank you for reaching out! Expect to hear back soon.'
  });
};

在这一点

验证域并设置完成后,您可以从开发环境发送测试电子邮件!

使用Google Recaptcha V2防止垃圾邮件

但是,此时部署是有风险的。即使您只向自己的电子邮件地址发送电子邮件,恶意演员也可能会尝试不断提交您的表格,以限制您或收取您的AWS账单。

代替(或除)预防站点垃圾邮件的垃圾邮件,我们将确保通过添加复选框验证码提交表格。我决定不使用v3,因为坦率地说,我不喜欢右下角的“被recaptcha”徽章。

首先,您需要创建一个recaptcha here。填写标签,选择Challenge (v2)"I'm not a robot" Checkbox,然后添加码头名称(验证码)将打开(当您在这里时,最好为localhost创建一个,如果您使用的(如果您使用它)为登台环境创建另一个名称)。

example of a new recaptcha being created

您会出现“站点密钥”和“秘密密钥”,您可以添加到1).env文件,以及2)GitHub repo设置。

我分别命名为CAPTCHA_SITE_KEYCAPTCHA_SECRET

example of captcha values being added to github environment secrets and variables settings page

网站键将链接表单要recaptcha的站点,并且秘密密钥将仅使用服务器端来验证浏览器中recaptcha生成的值。

接下来,我们将使用npm i react-google-recaptcha安装koude13 library

在页面上导入它:

import ReCAPTCHA from 'react-google-recaptcha';

并将其渲染在组件中:

export const Contact = () => {
+ const [recaptchaValue, setRecaptchaValue] = useState<string | null>(null);
+ const recaptchaRef = useRef<ReCAPTCHA>(null);

  const actionData = useActionData();

  const submit = useSubmit();

  const onSubmit = async (e) => {
    await submit(e.currentTarget);
+   setRecaptchaValue(null);
+   recaptchaRef?.current?.reset();
  };

+ const handleRecaptchaChange = (value: string | null) => {
+   setRecaptchaValue(value);
+ };

  return (
    <section>
      <Form method='POST' onSubmit={onSubmit}>
        <InputText
          name='name'
          label='Name'
          errorFeedback={actionData?.errors?.name ?? null}
        />
        {/* ... other fields ... */}
+       <input type='hidden' name='recaptchaValue' value={recaptchaValue} />
+       <ReCAPTCHA
+         ref={recaptchaRef}
+         onChange={handleRecaptchaChange}
+       />
        <button type='submit'>Send</button>
      </Form>
    </section>
  );
};

为此,组件需要访问CAPTCHA_SITE_KEY,因此我们可以使用加载程序:

export const loader = async (): Promise<TypedResponse<{ ENV: Pick<Globals, 'CAPTCHA_SITE_KEY'> }>> => {
  return json<{
    ENV: Pick<Globals, 'CAPTCHA_SITE_KEY'>;
  }>({
    ENV: {
      CAPTCHA_SITE_KEY: `${process.env.CAPTCHA_SITE_KEY}`,
    }
  });
};

并使用useLoaderData的组件访问数据:

  const data = useLoaderData<{ ENV: Pick<Globals, 'CAPTCHA_SITE_KEY'> }>();

最后(对于组件),我们将填写真正的sitekey Prop:

<ReCAPTCHA
  ref={recaptchaRef}
  onChange={handleRecaptchaChange}
+ sitekey={data.ENV.CAPTCHA_SITE_KEY}
/>

我们需要再次更新操作,以验证recaptcha:

export const action = async ({ request }: ActionArgs): Promise<{ success: true, successMessage: string; } | {
  success: false,
  errors: { form: string }
}> => {
  const formData = await request.formData();

+ const recaptchaValue = formData.get('recaptchaValue');
+
+ const captchaResponse = await validateCaptcha(recaptchaValue);
+
+ if (!captchaResponse.success) {
+   return json({
+     success: false,
+     errors: {
+       recaptchaValue: 'Invalid ReCAPTCHA response.',
+     },
+   });
+ }

  const requesterName = formData.get('name');

  // params, etc...
};

我们可以创建validateCaptcha函数并将其放入captcha.server.ts文件中:

const ReCaptchaURL = 'https://www.google.com/recaptcha/api/siteverify';
export const validateCaptcha = async (
  recaptchaValue: FormDataEntryValue | null
) => {
  const captchaResponse = await fetch(ReCaptchaURL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: `secret=${process.env.CAPTCHA_SECRET}&response=${recaptchaValue}`,
  });

  return await captchaResponse.json();
};

CAPTCHA_SECRET将从您的构建或.env文件中提取,没有什么可做的!

一个完整的实现,带有更多字段和一些样式可能看起来像这样(至少是我的写作,我确实可以!):

screenshot of digitalcanvas.dev contact form

谢谢您的阅读!

我在建立解决方案后发现了this article的道具,这给了我一些改进的想法。