发布于 2026-01-06 6 阅读
0

结合 NextJS、AWS Amplify 和 Stripe 构建餐饮应用程序(第 1 部分)DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

结合 NextJS、AWS Amplify 和 Stripe 构建餐饮应用(第一部分)

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

在构建包含众多复杂组件的复杂应用程序原型时,许多开发者选择AWS Amplify来进行托管和开发。然而,正如我们已经从许多客户那里看到的,Amplify 在生产环境中也表现出色。虽然有时可能需要扩展 Amplify 的功能,但在本系列文章中,我将通过构建一个无服务器餐饮应用程序来展示 Amplify 的诸多强大功能。

项目概述

我选择这个项目是因为它既足够实用,可以探索 AWS 的许多方面,又足够易于理解,可以写成一系列短文。

这个项目涉及诸多环节,仅举几例:

  • NextJS:我们选择的 React 框架。我们将使用此框架来销售我们的产品。
  • Stripe:为了处理支付并确保合规性,我们将与第三方集成,由其完成繁重的工作。
  • React Native (Expo):一个跨平台移动应用框架。送货司机需要分享他们的位置信息并与我们的云后端进行交互。
  • AppSync:一种托管的 GraphQL 服务,内置对 WebSocket 的支持。
  • 亚马逊位置服务:允许我们设置地理围栏、地图、跟踪司机,并在司机靠近客户家时发送通知。
  • AWS Amplify:面向专业前端开发人员的基础设施即代码解决方案。本项目将使用 Amplify CLI、Amplify 库和 Amplify 托管服务。

架构概述

本页顶部的架构图展示了我们将在接下来几篇文章中创建的完整流程。不过,在本文中,我们将重点介绍以下子集:

catering-phase-1.drawio.png

简而言之,当用户访问我们应用的首页时,我们会通过调用 REST 接口从 Stripe 控制面板获取产品信息。当用户点击某个产品时,我们会提交该产品的详细信息,从而创建一个结账会话,供用户完成支付。

如果付款成功,我们将触发一个 webhook,以便执行一些客户逻辑。在本例中,该逻辑是自动为我们的应用创建一个用户。新创建的用户可以查看他们的订单详情。

创造我们的产品

我们将从 Stripe 控制面板的“创建新产品”页面开始。本文不会详细介绍如何设置帐户,但进入控制面板后,请确保您处于测试模式

在产品页面,创建一些一次性产品。

image.png

在创建用于获取这些产品的 API 之前,我们需要前往Stripe 控制面板获取密钥。请记下这些密钥,或者保持页面打开状态。我们稍后会用到它们。

stripe-dashboard.png

添加我们的依赖项

如前所述,我们将使用 NextJS 为客户创建一个购买餐饮套餐的页面。首先,我们将创建一个新的 NextJS 应用。

npx create-next-app catering-with-amplify

进入项目目录后,我们将添加一些必要的依赖项:

npm i aws-amplify @aws-amplify/ui-react @stripe/stripe-js

  • aws-amplify:让我们的前端与即将创建的后端集成。
  • @aws-amplify/ui-react:预构建的 UI 组件
  • @stripe/stripe-js:允许我们将客户重定向到结账会话。

初始化后端

🗒️ 本系列项目使用Amplify CLI 6.3.1 版本npm i -g @aws-amplify/cli。运行命令即可安装最新版本。

虽然我们的用户需要一个前端来查看产品和结账,但我们的大部分时间将用于设置后端。

首先,我们将在终端中运行以下命令:

amplify init
Enter fullscreen mode Exit fullscreen mode

检测到我们的应用程序后,系统会弹出一系列问题。当被问及是否接受默认配置时,请选择“否”

虽然大多数默认设置都可以放心使用,但由于我们使用的是 NextJS,所以有些地方需要更改:

* What is the name of the source directory: [enter a period here]
* What is the name of the build directory: [enter .next here]
Enter fullscreen mode Exit fullscreen mode

之后,选择您要使用的 AWS 配置文件。

image.png

🎉 我们现在准备开始添加我们的 AWS 服务

添加我们的后端服务

添加身份验证

首先要添加的是身份验证会话服务。这项服务会设置 Amazon Cognito,以便用户在完成订单后可以登录。

amplify add auth

- Accept the default configuration
- Select **Email** as the sign in method
- Select "No, I am done."
Enter fullscreen mode Exit fullscreen mode

✅ 这太简单了!

接下来的服务是本文的主体部分。

添加 REST API

在许多框架中,创建 API 是通过添加一个文件来实现的,该文件随后会成为 API 路由。虽然这极大地提高了开发速度并简化了流程,但缺点是对该函数可以附加到哪些对象的控制较少。目前,我们希望该函数作为 API 路由,稍后我们将探讨如何在用户注册时自动触发无服务器函数。

Amplify 通过其 CLI 支持这种灵活性。要开始使用,请运行以下命令:

amplify add api

- Select REST
- **Give this a friendly name like "cateringapi"**
- Provide a path: `/catering`
- Create a new Lambda function
- Name the function `cateringfunc`
- NodeJS as the environment
- Select **Serverless ExpressJS function** as the template
- Select `y` for advanced settings
- Select `N` for all options **except** for configuring secret values, select `y`
- For the key of the secret, type `STRIPE_SECRET_KEY`
- For the value, grab the **secret key** (not the publishable key) from the Stripe Dashboard (make sure you're in **test mode**) and enter it.
- I'm done
- Select the option to edit the local lambda function now
Enter fullscreen mode Exit fullscreen mode

文件应该会在编辑器中打开。请花点时间浏览一下生成的内容。以下是一些重点:

  1. 顶部的评论显示,要检索我们创建的秘密值。
const { Parameters } = await (new aws.SSM())
  .getParameters({
    Names: ["STRIPE_SECRET_KEY"].map(secretName => process.env[secretName]),
    WithDecryption: true,
  })
  .promise();

Parameters will be of the form { Name: 'secretName', Value: 'secretValue', ... }[]
Enter fullscreen mode Exit fullscreen mode
  1. 我们所有的 CRUD 路由都是根据/catering我们提供的路径自动生成的。

image.png

现在我们已经有机会欣赏了生成的代码,请把这个文件里的所有内容都删除吧!(真的!)

添加业务逻辑

添加导入

以下是我们需要导入的软件包。唯一需要额外添加的是stripe软件包本身,我们稍后会添加。

const express = require('express')
const bodyParser = require('body-parser')
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware')
const Stripe = require('stripe')
const app = express()
const aws = require('aws-sdk')
Enter fullscreen mode Exit fullscreen mode

辅助方法和中间件

接下来,我们需要创建一个方法来获取我们的密钥。这段代码片段本质上是对原始代码的封装。

const fetchStripeSecret = async () => {
    const { Parameters } = await new aws.SSM()
        .getParameters({
            Names: ['STRIPE_SECRET_KEY'].map((secretName) => process.env[secretName]),
            WithDecryption: true,
        })
        .promise()

    return Parameters[0].Value
}
Enter fullscreen mode Exit fullscreen mode

我们的模板基于 ExpressJS,这是一个旨在服务器端运行的 NodeJS 框架。在 Express 中,中间件是一个核心概念。

以下代码会自动将我们的有效负载转换为 JSON,允许我们在无服务器函数中运行 express,允许 CORS(以便我们可以在前端访问我们的 API),最后,将我们的 stripe 密钥附加到我们的请求中,这样我们就不必将其添加到每个 API 路径中。

app.use(bodyParser.json())
app.use(awsServerlessExpressMiddleware.eventContext())

// Enable CORS for all methods
app.use(function (req, res, next) {
    res.header('Access-Control-Allow-Origin', '*')
    res.header('Access-Control-Allow-Headers', '*')
    next()
})

app.use(async (req, _, next) => {
    req.stripeSecretKey = await fetchStripeSecret()
    next()
})
Enter fullscreen mode Exit fullscreen mode

为我们的 API 路径添加逻辑

接下来,我们可以定义路由了!生成的代码会在路径前加上前缀/catering,但这并不意味着我们不能修改它们。下面的代码允许我们从 Stripe 获取创建的产品,并将它们返回到前端。

/****************************
 * get method to access stripe products *
 ****************************/

app.get('/catering/products', async function (req, res) {
    const stripe = new Stripe(req.stripeSecretKey)

    const productPriceData = await stripe.prices.list({
        expand: ['data.product'],
    })

    const productData = productPriceData.data.map(
        ({ product, unit_amount, id }) => ({
            name: product.name,
            description: product.description,
            price: unit_amount / 100,
            image: product.images[0],
            priceId: id,
        })
    )

    res.json(productData)
})
Enter fullscreen mode Exit fullscreen mode

当用户从前端点击我们的 Stripe 产品时,会发生以下情况:他们会向我们发送一个请求priceId和一个请求fulfillmentDate,我们会处理创建结账会话并将该会话发送到前端,以便他们可以重定向到相应页面。

🗒️ 请注意,在 `<head>`success_url和 `<body>`中cancel_url,需要分别创建 `<page>`/order/success和 ` <page>` 页面/order/canceled。我们稍后会创建这些页面。

app.post('/catering/checkout-sessions', async (req, res) => {
    const stripe = new Stripe(req.stripeSecretKey)

    try {
        // Create Checkout Sessions from body params.
        const session = await stripe.checkout.sessions.create({
            line_items: [
                {
                    price: req.body.priceId,
                    quantity: 1,
                },
            ],
            payment_method_types: ['card'],
            mode: 'payment',
            success_url: `${req.headers.origin}/order/success?session_id={CHECKOUT_SESSION_ID}`,
            cancel_url: `${req.headers.origin}/order/canceled`,
            metadata: {
                fulfillmentDate: req.body.fulfillmentDate, //new Date().toISOString()
            },
            shipping_address_collection: {
                allowed_countries: ['US'],
            },
        })

        res.status(200).json(session)
    } catch (err) {
        console.log(err)
        res.status(err.statusCode || 500).json(err.message)
    }
})
Enter fullscreen mode Exit fullscreen mode

最后一条路由更像是一个附加功能。在下一篇文章中,我们会将订单存储在数据库中,但现在,我们会提供一种机制,当订单完成后,customerSessionId前端会收到一个消息。我们会将该消息发送到这条路由,以便稍后查看客户详细信息。

app.get('/catering/checkout-sessions/:customerSessionId', async (req, res) => {
    const stripe = new Stripe(req.stripeSecretKey)

    const id = req.params.customerSessionId

    try {
        if (!id.startsWith('cs_')) {
            throw Error('Incorrect CheckoutSession ID.')
        }
        const checkoutSession = await stripe.checkout.sessions.retrieve(id)
        console.log(
            'the custoemr session',
            JSON.stringify(checkoutSession, null, 2)
        )
        res.json(checkoutSession)
    } catch (err) {
        res.status(404).json({ statusCode: 404, message: err.message })
    }
})
Enter fullscreen mode Exit fullscreen mode

此文件的最后一行代码只是导出我们创建的内容:

module.exports = app

请记住,我们需要stripe为我们的函数安装该软件包。

在项目根目录下,在终端中运行以下命令:

cd amplify/backend/function/cateringfunc/src && npm i stripe && cd ../../../../..
Enter fullscreen mode Exit fullscreen mode

它会进入 functions 目录,安装stripe软件包,然后返回主目录。

在部署后端并配置前端之前,我们还需要通过终端回答几个问题。它目前应该处于某种Press enter to continue状态。

按回车键并选择以下选项:

Restrict API access **N**
Do you want to add another path **N**
Enter fullscreen mode Exit fullscreen mode

完成上述步骤后,让我们保存更改并通过运行以下命令将后端推送到 AWS:

amplify push -y
Enter fullscreen mode Exit fullscreen mode

🗒️amplify push将显示后端更改并询问是否继续。添加此-y标记将自动接受。

在后端上线的同时,我们切换到前端。

搭建前端框架

😅 说实话,我本来打算添加一个 CSS 框架,但后来想想,如果有人想用这个项目,他们很可能会用自己的样式。所以我保持了样式简洁,专注于代码。不过最终版本使用了 ChakraUI 来实现样式 😎

与所有 Amplify 项目一样,我们需要将前端和后端连接起来。由于我们使用的是 NextJS,因此我们将通过更新_app.js文件来实现这一点,使其如下所示:

import '../styles/globals.css'
import Amplify from 'aws-amplify'
import config from '../aws-exports'

Amplify.configure(config)

function MyApp({ Component, pageProps }) {
    return <Component {...pageProps} />
}

export default MyApp
Enter fullscreen mode Exit fullscreen mode

之后,我们会getStripe.js在项目根目录添加一个名为 `stripe.conf` 的文件。这个文件确保我们使用同一个 Stripe 实例,并且只加载一次。

import { loadStripe } from '@stripe/stripe-js'

let stripePromise = null
const getStripe = () => {
    if (!stripePromise) {
        stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY)
    }
    return stripePromise
}

export default getStripe
Enter fullscreen mode Exit fullscreen mode

请注意,该getStripe文件使用了一个环境变量。该键的值可以在Stripe 控制面板中找到,它就是可发布的键

从那里复制值,并在项目根目录下创建一个名为.env.local.

粘贴以下内容(将值替换为您实际可发布的密钥):

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= YOUR_PUBLISHABLE_KEY_STARTS_WITH_PK
Enter fullscreen mode Exit fullscreen mode

创建我们的主页

我们的主页是验证 API 是否已设置好的第一个机会。请记住,它承担着两项职责:

  1. 获取我们的产品
  2. 用户选择商品后创建结账会话

把原有内容替换pages/index.js成以下内容:

import { useEffect, useState } from 'react'
import getStripe from '../getStripe'
import { API } from 'aws-amplify'

const Home = () => {
    const [products, setProducts] = useState([])

    useEffect(() => {
        API.get('cateringapi', '/catering/products').then((productData) =>
            setProducts(productData)
        )
    }, [])

    const handleProductClick = async (priceId) => {
        const stripe = await getStripe()
        const data = await API.post('cateringapi', '/catering/checkout-sessions', {
            body: { priceId, fulfillmentDate: new Date().toISOString() },
        })

        await stripe.redirectToCheckout({ sessionId: data.id })
    }

    return <main>view products</main>
}

export default Home
Enter fullscreen mode Exit fullscreen mode

虽然讨论 React 的工作原理不在本系列文章的讨论范围之内,但其中两个重要的部分是 `getProducts` useEffect()(在初始加载后获取我们的产品一次)和handleProductClick`createCheckoutSession` 函数。

这两个功能都通过流程调用了我们的 API API Name, Path Name。因此,在 CLI 中为这些服务提供一个易于理解的名称非常重要。

您可以随意设计产品样式,如果您只是想跟着教程操作,可以将“查看产品”文本替换为以下内容:

{products.map((product) => {
  return (
    <article
        style={{
          border: '1px solid black',
          margin: '20px',
          display: 'flex',
        }}
        key={product.priceId}
        onClick={() => handleProductClick(product.priceId)}
    >
        <div>
          <img
            src={product.image}
            alt={product.description}
            width="300px"
            height="300px"
          />
        </div>
        <div>
            <h2>{product.name}</h2>
            <h2>
              {new Intl.NumberFormat('en-US', {
                  style: 'currency',
                  currency: 'USD',
              }).format(product.price)}
            </h2>
            <p>{product.description}</p>
        </div>
    </article>
  )
})}
Enter fullscreen mode Exit fullscreen mode

在测试我们的流程之前,请记住,当客户被转至 Stripe 提交付款时,如果付款成功,他们将被转回成功页面;如果客户取消付款,则会被转回取消页面。

在 pages 目录中,创建以下页面:

  1. success.js
  2. canceled.js
  3. customer-session/[orderId].js(我们稍后会再讨论这个问题)

对于取消页面,我们将保持极其简单,仅检查返回的查询参数,以确定用户是否通过 Stripe 成功取消:

import React, { useEffect } from 'react'

function OrderCancelPage() {
    useEffect(() => {
        const query = new URLSearchParams(window.location.search)

        if (query.get('canceled')) {
            console.log(
                'Order canceled -- continue to shop around and checkout when you’re ready.'
            )
        }
    }, [])
    return <div>Order Cancel Page</div>
}

export default OrderCancelPage
Enter fullscreen mode Exit fullscreen mode

成功页面将非常相似,只是我们还会让他们能够通过访问该customer-session/{orderId}页面来查看他们的订单。

import React, { useEffect, useState } from 'react'
import Link from 'next/link'
function OrderSuccessPage() {
    const [orderId, setOrderId] = useState('')
    useEffect(() => {
        // Check to see if this is a redirect back from Checkout
        const query = new URLSearchParams(window.location.search)

        if (query.get('session_id')) {
            setOrderId(query.get('session_id'))
        }
    }, [])

    return (
        <div>
            <h1>Order Success Page</h1>
            <Link href={`/customer-session/${orderId}`}>
                <a>View Order</a>
            </Link>
        </div>
    )
}

export default OrderSuccessPage
Enter fullscreen mode Exit fullscreen mode

在客户订单页面,添加以下几行代码:

import React from 'react'
import { withAuthenticator } from '@aws-amplify/ui-react'

function CustomerOrder() {
    return <div>Protected customer order page.</div>
}

export default withAuthenticator(CustomerOrder)
Enter fullscreen mode Exit fullscreen mode

此页面受保护,这意味着用户需要拥有帐户才能查看。我们稍后会深入探讨这意味着什么,但现在,让我们测试一下我们的流程!

启动应用程序

npm run dev
Enter fullscreen mode Exit fullscreen mode

访问网站localhost:3000。您的产品应该会显示出来,点击产品后,您应该会被引导至 Stripe 结账页面。

Stripe 结账会话

🗒️ 为了完成付款,您必须输入一系列42卡片信息。

付款完成后,请确保您已跳转到付款success页面。您也可以点击 Stripe 返回按钮,确认是否已返回该canceled页面。

竭尽全力

我们目前已经做了很多。我们现在拥有的成果很容易就能发展成一个不错的入门项目👀

但在本文中,我们将更进一步。具体来说,当客户购买产品时,我们会自动为他们创建一个帐户,以便他们查看订单。

我们的前端已经配置好可以处理这个流程:

当用户进入成功页面后,他们可以点击链接查看订单。点击链接后,他们会被重定向到pages/customer-session/{orderId}我们可以获取客户详细信息的页面。

因为这个问题已经解决了,所以我们将专注于后端开发。

创建 Stripe webhook

创建 webhook 分为两步:首先,我们需要向 Stripe 注册我们的端点;然后,我们需要在我们的 API 中创建相应的逻辑。

要开始监听结账付款完成的情况,我们首先要进入我们的aws-exports.js文件来获取我们的 API 端点。

此文件包含我们所有的密钥,并且理所当然地会自动添加到我们的数据库中.gitignore。从数组中获取端点值aws_cloud_logic_custom

REST API 端点

有了端点信息,我们将再次前往 Stripe 控制面板创建一个新的 webhook:https://dashboard.stripe.com/test/webhooks/create

Stripe Webhook 创建

🚨请务必将其添加/payment-webhook到 API 端点的末尾。

输入详细信息后,点击+ selected events按钮。

选择checkout.session.completed事件并点击add events

选择 Stripe Webhook 事件

创建好 webhook 后,我们需要获取签名密钥。

🚨这不是仪表盘右上角的秘密。

显示 Stripe Webhook 秘密位置

将密码复制到剪贴板。

创建 webhook 端点

这部分与我们之前介绍的路由有很多相似之处。我们将首先通过命令行界面 (CLI) 向现有 API 添加一条新路径。

amplify update api
Enter fullscreen mode Exit fullscreen mode

接下来,我们将选择以下选项:

- REST
- cateringapi
- Add another path
- /payment-webhook
- Create a new Lambda function
- cateringwebhookfunc
- NodeJS
- Serverless ExpressJS function
Enter fullscreen mode Exit fullscreen mode

当被问及是否要配置高级设置时,请y选择以下选项:

- Access other resources: y
- Press spacebar on auth and hit enter
- Press spacebar on create and read and hit enter
- Select 'n' for recurring schedule, lambda layers, and environment variables
- Select 'y' for wanting to configure environment secrets
Enter fullscreen mode Exit fullscreen mode

对于密钥名称,请输入STRIPE_SECRET_WEBHOOK。对于值,请输入从 Stripe 控制面板复制的 webhook 签名密钥。

当被问及你想做什么时,说“添加一个秘密”

对于密钥名称,请输入STRIPE_SECRET_KEY。对于密钥值,请输入 Stripe 控制面板首页上的密钥值:https://dashboard.stripe.com/test/dashboard

🗒️ 我们已经为第一个函数输入了这个密钥。但是,目前还没有办法在函数之间共享密钥。幸运的是,团队已经在努力尽快实现这一点!

选择I'm done,然后n现在想要编辑本地函数。

要限制 API 访问权限,请选择nn添加另一个路径。

🤔 “我们刚刚创建了一个可以从用户池中创建和读取用户的函数,为什么不限制对它的访问呢?” 问得好!我们没有使用 IAM 权限来锁定 API 路径,而是使用了 Stripe webhook secret。我们会检查请求头,确保它与我们存储在 AWS 上的 webhook secret 匹配。

添加我们的端点逻辑

在代码编辑器中,我们将导航到amplify/backend/function/paymentwebhookfunc/src/app.js

与之前一样,Amplify 在提供示例代码方面做得非常出色。请注意,由于我们已允许create访问readCognito 连接池,因此系统会显示可用于访问该连接池的环境变量。

/* Amplify Params - DO NOT EDIT
    AUTH_AMPLIFYEXAMPLE20C9CA19_USERPOOLID
    ENV
    REGION
Amplify Params - DO NOT EDIT */
Enter fullscreen mode Exit fullscreen mode

稍后会详细介绍。

现在,我们先来删除除已提供的环境变量之外的所有内容。

现在几乎是空白的,让我们从添加导入和密钥获取器开始:

const aws = require('aws-sdk')
const express = require('express')
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware')
const { createCognitoUser } = require('./createCognitoUser')
const Stripe = require('stripe')
const app = express()

const fetchSecrets = async (key) => {
    const { Parameters } = await new aws.SSM()
        .getParameters({
            Names: [key].map((secretName) => process.env[secretName]),
            WithDecryption: true,
        })
        .promise()

    return Parameters[0].Value
}
Enter fullscreen mode Exit fullscreen mode

没什么特别的,我们导入的模块和上次一样,只是增加了一个createCognitoUser模块。稍后我们会完善这部分内容。

接下来,我们引入 Stripe 库,以便可以使用 Stripe API。

最后,我们还有一种略有不同的方法来获取密钥值。由于我们现在有两个密钥(一个 Stripe 密钥和一个 Webhook 密钥),此方法接收密钥名称并返回它。

接下来是一些中间件。这和上一个 API 模块相同。

app.use(awsServerlessExpressMiddleware.eventContext())

// Enable CORS for all methods
app.use(function (req, res, next) {
    res.header('Access-Control-Allow-Origin', '*')
    res.header('Access-Control-Allow-Headers', '*')
    next()
})
Enter fullscreen mode Exit fullscreen mode

最后一部分是文件的核心内容。在这里,我们将创建路由,并从请求头中获取密钥和 Stripe 值。获取到这些信息后,我们将try验证签名值是否与我们从 Stripe 复制的值一致。

如果是,我们会检查事件类型是否正确checkout.session.completed,获取客户的电子邮件,并使用该电子邮件在我们的用户池中创建一个用户。

/****************************
 * post method to capture successful payment *
 ****************************/

app.post(
    '/payment-webhook',
    express.raw({ type: 'application/json' }),
    async function (req, res) {
        const stripeSecretKey = await fetchSecrets('STRIPE_SECRET_KEY')
        const stripeWebhookSecret = await fetchSecrets('STRIPE_SECRET_WEBHOOK')
        const stripe = new Stripe(stripeSecretKey)
        const sig = req.headers['stripe-signature']
        let event

        try {
            event = stripe.webhooks.constructEvent(req.body, sig, stripeWebhookSecret)
        } catch (err) {
            res.status(400).send(`Webhook Error: ${err.message}`)
            return
        }

        switch (event.type) {
            case 'checkout.session.completed':
                const paymentIntent = event.data.object
                const { email } = paymentIntent.customer_details

                const user = await createCognitoUser({
                    UserPoolId: process.env.AUTH_AMPLIFYEXAMPLE20C9CA19_USERPOOLID,
                    Username: email,
                })

                break
            default:
                console.log(`Unhandled event type ${event.type}`)
        }

        // Return a 200 response to acknowledge receipt of the event
        res.send()
    }
)

app.listen(3000, function () {
    console.log('App started')
})
module.exports = app
Enter fullscreen mode Exit fullscreen mode

有两点值得一提:

  1. express.raw({ type: 'application/json' })这个路由级别的中间件告诉 Express 不要修改传入的 JSON 数据(不要尝试解析它)。有效负载必须保持未修改状态,否则 Stripe 会拒绝该请求。
  2. process.env.AUTH_AMPLIFYEXAMPLE20C9CA19_USERPOOLID请务必将其替换为您实际的环境变量。

自动创建用户

在我们的app.js文件中,我们导入了一个名为 的函数createCognitoUser。当调用该函数时,我们会传递用户池 ID 和客户的电子邮件地址。

让我们创建这个文件(在与 相同的目录中app.js),并添加我们的业务逻辑。

这部分代码相对简单。我们首先使用用户池 ID 和客户的邮箱地址尝试创建一个新用户。如果创建失败,我们会检查是否因为该用户已存在而导致失败。无论哪种情况,我们都会返回该用户或抛出一个错误。

const aws = require('aws-sdk')

const cognito = new aws.CognitoIdentityServiceProvider({
    apiVersion: '2016-04-18',
})

const createCognitoUser = async ({ UserPoolId, Username }) => {
    try {
        const user = await cognito
            .adminCreateUser({
                UserPoolId,
                Username,
                DesiredDeliveryMediums: ['EMAIL'],
                UserAttributes: [
                    {
                        Name: 'email',
                        Value: Username, //email they used to pay
                    },
                ],
            })
            .promise()

        return user
    } catch (e) {
        //check if user already exists
        if (e.code === 'UsernameExistsException') {
            const user = await cognito
                .adminGetUser({
                    UserPoolId,
                    Username,
                })
                .promise()

            return user
        }

        throw Error('application error', e)
    }
}
module.exports.createCognitoUser = createCognitoUser

Enter fullscreen mode Exit fullscreen mode

🗒️ 请记住,我们最初添加身份验证功能时,曾说过用户将通过邮箱登录。这就是为什么我们使用邮箱地址作为用户名的原因。

现在,当用户注册时,他们会收到一封包含临时密码的电子邮件,当他们尝试使用该密码登录时,系统会自动提示他们将其更改为其他密码,询问他们是否要确认,然后带他们进入一个受保护的页面!

测试我们的应用程序

在尝试之前,我们需要确保考虑到一些事项。

  1. 如果您尚未安装,请确保安装stripe此函数所需的软件包。即使它属于同一个 API,它仍然是一个拥有自身一组参数的函数node_modules
cd amplify/backend/function/cateringwebhookfunc/src && npm i stripe && cd ../../../../..
Enter fullscreen mode Exit fullscreen mode
  1. 这与我们之前输入重复密钥有关。目前,Amplify 只会尝试查找第一个创建的函数的密钥。我们可以通过扩展 webhook 函数的权限来解决这个问题:

src在我们的目录文件夹paymentwebhookfunc,有一个名为custom-policies.json.

打开此文件并粘贴以下内容:

[
    {
        "Action": ["ssm:GetParameters"],
        "Resource": ["arn:aws:ssm:YOUR_REGION:YOUR_AWS_ACT_NUMBER:*"]
    }
]
Enter fullscreen mode Exit fullscreen mode

如果您不确定自己的地区,请查看文件aws_project_region中的值aws-exports.js

不过,要获取账号,您需要登录 AWS 控制台,然后从右上角找到它。

如果你知道如何抓住它,那就去做吧;如果不知道,那就跟着做:

在终端中运行amplify console并选择Amplify Console

登录后,您可以在右上角的下拉菜单中找到您的账号。

acctNumber.png

🗒️再次声明,这只是暂时的,一旦对凭证的支持得到改善,我将删除帖子中的这部分内容。

完成这些步骤后,让我们运行命令amplify push -y将本地更改与云端同步。

测试我们的流程

请确保您的应用正在运行localhost:3000

npm run dev
Enter fullscreen mode Exit fullscreen mode

像以前一样购买产品,购买成功后,您应该会被带到相应的success页面。

请查看您的电子邮件,您会收到一封注册确认邮件,其中包含临时密码。

返回应用程序后,点击链接查看订单,并使用您的电子邮件和临时密码登录。

接下来,系统会提示您更改密码,您还可以选择验证您的电子邮件。

🎉完成以上步骤后,您将被引导至订单页面🎉

额外学分挑战

如前所述,函数无需绑定到 API。例如,Cognito 允许我们将其绑定到身份验证事件。

既然用户在付款时已经需要提供邮箱地址,那么让他们再验证邮箱地址似乎就显得多余了。如果能在用户注册后、确认之前自动验证他们的邮箱地址,那就太好了!

跑步amplify update auth就能做到这一点!

以下是用于访问触发器的 CLI 选项:

预注册 Cognito 触发选项

这里有一个链接,指向一段用于自动验证电子邮件的代码片段。


这是系列文章的第一部分!

在这篇文章中,我们创建了一个 REST API,并允许我们的应用接受 Stripe 付款。我们还添加了一个安全的 Webhook,用于自动为我们的应用创建用户。用户创建完成后,即可登录查看订单详情。

这仅仅是冰山一角。

接下来,我们将深入探讨如何将 Expo、Amazon Location Service 和 AWS AppSync 结合起来,打造一个能够响应地理位置更新和订单状态变化的实时地理位置配送应用程序!

如果您有任何意见、建议或想法,欢迎留言告诉我!

文章来源:https://dev.to/focusotter/combining-nextjs-aws-amplify-and-stripe-to-build-a-fitting-app-part-1-3731