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

使用 AdonisJS 构建 API(第二部分)

使用 AdonisJS 构建 API(第二部分)

这是关于使用 AdonisJS 创建 API 系列文章的第二篇。如果您还没有阅读第一篇,请点击此链接:使用 AdonisJS 构建 API

现在我们继续第二部分。在这里我们将学习如何:

  • 更新用户信息(PUT 请求)
  • 恢复用户密码
  • 使用 Adonis 发送电子邮件
  • 使用迁移工具更新表结构

更新用户信息

我们首先来创建控制器,允许用户更新他的信息,例如他的用户名和密码(在本应用程序中,用户将无法更新他的电子邮件)。

其背后的逻辑非常简单:

  • 用户将提交请求,包括他想要的新用户名、当前密码和想要的新密码。
  • 然后我们将在数据库中搜索该用户。
  • 然后我们检查当前提供的密码是否正确,然后用新提供的密码更新他的信息。

为了创建一个新的控制器,我们需要运行以下 Adonis 命令:

adonis make:controller UpdateUserInfo --type http
Enter fullscreen mode Exit fullscreen mode

现在我们可以打开文件app/controllers/http/UpdateUserInfoController.js并开始编写代码了:

让我们确保导入我们的User模型,我们还将使用 Adonis 的一个名为Hash.

出于安全考虑,哈希程序将负责对新提供的密码进行哈希处理。

'use stric'

const User = use('App/Models/User')
const Hash = use('Hash')
Enter fullscreen mode Exit fullscreen mode

我们的控制器只需要一个update方法,所以让我们在控制器内部UpdateUserInfoController首先创建这个方法:

class UpdateUserInfoController {
  async update ({ request, response, params }) {
Enter fullscreen mode Exit fullscreen mode

基于我们的逻辑,我们执行以下操作:

  1. 让我们获取用户在请求中发送的新信息:

2.

   const id = params.id
       const { username, password, newPassword } = request
         .only(['username', 'password', 'newPassword'])
Enter fullscreen mode Exit fullscreen mode
  1. 现在在数据库中查找该用户(使用 id):
   const user = await User.findByOrFail('id', id)
Enter fullscreen mode Exit fullscreen mode
  1. 检查提供的密码是否与当前密码匹配:
   const passwordCheck = await Hash.verify(password, user.password)

   if (!passwordCheck) {
         return response
           .status(400)
           .send({ message: { error: 'Incorrect password provided' } })
       }
Enter fullscreen mode Exit fullscreen mode
  1. 密码验证完成后,我们就可以更新用户信息了:
   // updating user data
       user.username = username
       user.password = newPassword
Enter fullscreen mode Exit fullscreen mode
  1. 最后,我们只需要使用该.save()方法将数据持久化到数据库中即可。
   await user.save()
Enter fullscreen mode Exit fullscreen mode

UpdateUserInfoController.js现在应该看起来像这样:

'use strict'

const User = use('App/Models/User')
const Hash = use('Hash')

class UpdateUserInfoController {
  async update ({ request, response, params }) {
    const id = params.id
    const { username, password, newPassword } = request
      .only(['username', 'password', 'newPassword'])

    // looking for user in DB
    const user = await User.findByOrFail('id', id)

    // checking if old password informed is correct
    const passwordCheck = await Hash.verify(password, user.password)

    if (!passwordCheck) {
      return response
        .status(400)
        .send({ message: { error: 'Incorrect password provided' } })
    }

    // updating user data
    user.username = username
    user.password = newPassword

    // persisting new data (saving)
    await user.save()
  }
}

module.exports = UpdateUserInfoController

Enter fullscreen mode Exit fullscreen mode

完美!现在让我们测试一下控制器。前往start/routes.js文件

这里非常重要的一点是,我们的一些路由只能由已认证的用户访问,而 Adonis 的路由机制提供了一个完美的方法来处理这种情况,称为 `group` group()group您可以使用 `group` 方法,middleware并将一个数组作为参数传递给它,该数组包含在访问 `group` 方法内部的路由之前应该运行哪些中间件。

Route.group(() => {
  // updating username and password
  Route.put('users/:id', 'UpdateUserInfoController.update')
}).middleware(['auth'])
Enter fullscreen mode Exit fullscreen mode

在我们的例子中,我们只需要 Adonis 默认提供的身份验证方法。稍后我们将测试这条路由在用户未验证和已验证两种情况下的性能。

首先,让我们在未进行身份验证的情况下测试此路由:

这是我想为用户保存的新信息:

{
    "password": "123456",
    "newPassword": "123",
    "username": "DanSilva"
}
Enter fullscreen mode Exit fullscreen mode

这里我就不赘述如何使用 Insomnia 发送请求了,因为我在第一部分已经讲解过了。

如果我在未登录的情况下发送请求,将会收到 401 错误(未授权)。为了使此方法有效,我必须在请求中提供一个登录时获得的 JWT 令牌,因此请确保登录后再测试此路由。

登录后,复制请求返回的令牌。在 Insomnia 中创建一个新的 PUT 方法,在请求 URL 下方有一个名为“身份验证”的选项卡。在打开的下拉菜单中选择该选项卡Bearer Token,然后在令牌字段中粘贴您刚刚复制的令牌。

在再次发送请求之前,让我们查看数据库中的用户数据,以确保在我们的请求之后数据已更新。

用户信息

完美。现在我们发送请求。请确保您的 URL 符合以下结构。

base_url/users/YOUR_USER_ID_HEre

现在发送请求。如果请求成功,将返回 204 状态码,因为我们没有设置任何要返回的消息。

用户信息已更新

看到了吗?新用户信息已保存到我们的数据库中!

使用 AdonisJS 发送电子邮件

在继续创建控制器以请求密码恢复并使用此恢复设置新密码之前,让我们先来看看如何配置 Adonis 发送电子邮件。

该电子邮件服务提供商默认未安装,因此我们需要手动安装。为此,只需运行以下命令:

adonis install @adonisjs/mail
Enter fullscreen mode Exit fullscreen mode

现在我们需要在应用程序中注册新的提供商。我们的提供商数组位于 [此处应填写文件路径] start/app.js。打开该文件并找到名为 [此处应填写变量名] 的变量providers。该变量是一个数组,其中包含使 Adonis 正常工作所必需的所有提供商。只需将以下提供商添加到此数组的末尾即可:

'@adonisjs/mail/providers/MailProvider'
Enter fullscreen mode Exit fullscreen mode

我们还需要配置一些内容才能继续。我们需要一位客户来测试发送电子邮件的功能,而我们正好有一个非常适合这项任务的工具。

我们将使用Mailtrap。Mailtrap在其官网上是这样描述的:

Mailtrap 是一个模拟 SMTP服务器,用于测试、查看和共享从开发和测试环境发送的电子邮件,而不会向真实客户发送垃圾邮件。

创建帐户后,访问https://mailtrap.io/inboxes,您会看到一个页面,显示您没有任何收件箱。

邮件陷阱收件箱

点击“创建收件箱”即可创建一个新的收件箱。访问您的收件箱后,您将被直接重定向到 SMTP 设置选项卡。这是一个重要的选项卡,因为我们将使用此处显示的信息来完成 Adonis API 的配置。

Host:   smtp.mailtrap.io
Port:   25 or 465 or 2525
Username:   a218f0cd73b5a4
Password:   0a5b3c6c6acc17
Enter fullscreen mode Exit fullscreen mode

我们将把以上数据插入到我们的.env文件中,以便正确设置邮件服务:

MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=465
MAIL_USERNAME=a218f0cd73b5a4
MAIL_PASSWORD=0a5b3c6c6acc17
Enter fullscreen mode Exit fullscreen mode

请确保该数据MAIL_USERNAMEMAIL_PASSWORDmailtrap 提供给您的数据相符。

现在我们需要前往app/mail.js完成电子邮件设置。

由于我们将使用 SMTP 协议,因此文件连接会话的配置将保持不变。请确保您的配置与我的相同(当然,前提是您也使用 SMTP 协议):

connection: Env.get('MAIL_CONNECTION', 'smtp')
Enter fullscreen mode Exit fullscreen mode

现在找到该smtp物体,确保它看起来像这样:

smtp: {
    driver: 'smtp', // make sure here is as SMTP
    pool: true,
    // using Env (provided by Adonis) to retriev the .env variables
    port: Env.get('MAIL_PORT'),
    host: Env.get('MAIL_HOST'),
    secure: false,
    auth: {
      user: Env.get('MAIL_USERNAME'),
      pass: Env.get('MAIL_PASSWORD')
    },
    maxConnections: 5,
    maxMessages: 100,
    rateLimit: 10
  },
Enter fullscreen mode Exit fullscreen mode

太棒了,如果我们完成了所有这些步骤,就可以配置我们的应用程序来发送电子邮件了。实际上,这并不需要做太多工作。我们只需要三个步骤:

  1. 安装 Adonis 的邮件提供商
  2. 配置环境变量以使用我们所需的邮件服务。
  3. 我们已配置mail.js文件以从环境变量中获取信息。

请求恢复密码

我们先从密码找回开始。您知道,当您点击“忘记密码”后,通常需要提供您的电子邮件地址,然后您会收到一封包含密码找回链接的电子邮件吗?这就是我们现在要做的。

为此,我们需要检查请求的有效性,我的意思是,假设你发送了第一个请求,那么你有两天的时间点击发送给你的链接,否则它将失效。

为了方便演示,我将使用令牌(token)。因此,在开始之前,我们需要在数据库的用户表中添加一个令牌字段。由于在应用程序启动时,我们已经运行了创建用户表的迁移,所以我们需要运行一个新的迁移来更新表结构,以便添加令牌列。

要创建新的迁移,请运行以下命令:

adonis make:migration user --action select
Enter fullscreen mode Exit fullscreen mode

在继续之前,我们先来看一下这条命令的结构:

adonis make:migration MIGRATION_NAME --action ACTION_NAME(create, select)
Enter fullscreen mode Exit fullscreen mode
  • MIGRATION_NAME_HERE:迁移的名称。建议您在此处填写要更新的表的名称。
  • ACTION_NAME:您有两个选项:
    1. 创建:用于创建新表。
    2. 选择:当您想要更新现有表的结构时,请使用此选项。

现在选择新创建的迁移文件,该文件位于database/migrations

在你的迁移文件中,你会看到一个包含两个方法的类,分别是 `update`up和 `reverse` down。现在我们先关注 `update`up方法,因为它用于创建更新。`reverse`down方法则用于撤销你所做的更改。

up () {
    this.table('users', (table) => {
      // make alterations
    })
  }
Enter fullscreen mode Exit fullscreen mode

您可以看到它this.table()使用了两个参数。第一个参数是表名。这里的值会根据您的迁移文件名称自动设置,并且使用复数形式。如果您在创建迁移文件时将表名设置为 `<table_name>`,那么user_update第一个参数的值将为 ` <table_name> user_updates`,您可能会遇到一些错误,因为您没有 `<table_name>` 表。

第二个参数是运行所有更新的函数。

正如我之前所说,我们需要一个令牌字段,但我们也需要一个token_create_at字段来检查其有效性。

我们将创建 2 列,一列用于string存储我们的令牌,另一列timestamp用于存储我们的令牌创建时刻。

up () {
    this.table('users', (table) => {
      table.string('token') // token
      table.timestamp('token_created_at') // date when token was created
    })
  }
Enter fullscreen mode Exit fullscreen mode

只需运行我们的迁移程序即可:

adonis migration:run
Enter fullscreen mode Exit fullscreen mode

太好了,现在我们的用户表已经更新完毕,接下来我们将创建控制器。我把它命名为ForgotPassword……

adonis make:controller ForgotPassword --type http
Enter fullscreen mode Exit fullscreen mode

我们首先创建store()负责处理密码找回请求的方法。该方法将生成令牌并将电子邮件发送给用户。

首先,让我们从类中导入所有需要的模块:

'use strict'

const User = use('App/Models/User') // user model
const Mail = use('Mail') // Adonis' mail

const moment = require('moment') // moment (RUN NPM INSTALL MOMENT)
const crypto = require('crypto') // crypto
Enter fullscreen mode Exit fullscreen mode

我们需要获取用户的电子邮件地址,并在数据库中找到他:

// account request password recovery
const { email } = request.only(['email'])

// checking if email is registered
const user = await User.findByOrFail('email', email)
Enter fullscreen mode Exit fullscreen mode

之后我们将生成令牌。生成令牌将使用cryptoNodeJS 自带的原生功能。(您可以在这里找到更多关于加密的信息:https://nodejs.org/api/crypto.html

// generating token
const token = await crypto.randomBytes(10).toString('hex')
Enter fullscreen mode Exit fullscreen mode

生成令牌后,我们使用 .将其转换为字符串toString()

现在我们需要设置令牌的有效期。为此,我们需要存储令牌的创建时间:

user.token_created_at = new Date()
Enter fullscreen mode Exit fullscreen mode

然后,我们将令牌保存到数据库中,并持久化所有信息:

user.token = token

// persisting data (saving)
await user.save()
Enter fullscreen mode Exit fullscreen mode

完成以上步骤后,我们将向用户发送电子邮件:

await Mail.send('emails.recover', { user, token }, (message) => {
    message
        .from('support@danmiranda.io')
        .to(email)
})
Enter fullscreen mode Exit fullscreen mode

Mail.send()使用三个参数:

  1. 邮件模板(我们稍后会谈到)。
  2. 要发送到模板的变量
  3. 回调函数用于设置诸如发件人、收件人、主题、附件等信息……

首先我们来谈谈模板。由于我们创建的这个 Adonis 应用程序仅作为 API 使用,因此我们需要在提供程序列表中注册视图提供程序。start/app.js

'@adonisjs/framework/providers/ViewProvider'
Enter fullscreen mode Exit fullscreen mode

所有视图都必须保存在同一个目录中resources/views,因此请在项目根目录创建 `<views>` 文件夹resources,并在该文件夹内创建 `<views>` 文件views夹。现在,您可以在此文件夹中根据需要组织视图。例如,在我们的应用程序中,我会将电子邮件模板存储在 `<email>`emails文件夹中。到目前为止,您的文件夹结构可能如下所示:

├── resources
│   └── views
│       └── emails
│           └── recover.edge 
Enter fullscreen mode Exit fullscreen mode

recover.edge这是我们的模板文件。Edge 是 AdonisJS 官方的模板引擎。它的语法与 HTML 文件非常相似,因此使用此模板不会有任何学习成本。

你可以按照自己喜欢的方式在这个模板里编写文本,但我会把我正在使用的格式放在这里供你参考。

<h1>Password recovery request</h1>
<p>
  Hello {{ user.username }}, it seems someone requested a password recovery
  for your account registered with the email {{ user.email }}.
</p>

<p>
  If it was you, just click this
<a href="http://127.0.0.1:3333/users/forgotPassword/{{token}}/{{user.email}}">link</a>
</p>

<p>
  If it wasn't you then we recommend you to change your password. Someone may
  have stolen it. 🕵️‍🕵️‍🕵️‍🕵️‍🕵️‍🕵️‍🕵️
</p>
Enter fullscreen mode Exit fullscreen mode

这里最需要注意的是双括号的使用{{}}。您可以使用这种语法来访问传递给模板的变量。在上面的例子中,我们获取了用户的用户名、电子邮件地址和令牌。

现在让我们回顾一下我们的Mail.send()函数:

await Mail.send('emails.recover', { user, token }, (message) => {
    message
        .from('support@danmiranda.io')
        .to(email)
})
Enter fullscreen mode Exit fullscreen mode

正如我们之前所说,第一个参数是模板。由于 Adonis 会直接读取目录,resources/views我们只需要指定该views文件夹内的其余目录即可。因为我们首先创建了一个名为 `<template>` 的文件夹emails,然后将模板存储在其中,所以我们在第一个参数中指定它,语法类似于访问 JavaScript 对象的属性,在本例中为 `<template>` emails.recover

第二个参数,也就是我们的变量{ user, token }。这里我们将发送整个用户对象,所以不需要在这里传递很多变量。

最后,第三个参数是回调函数。在我们的示例中,我们只设置from()地址to()。如果您想查看其他可用选项,请点击此链接

目前,您的 store 方法必须如下所示:

async store ({ request }) {
    try {
      // account request password recovery
      const { email } = request.only(['email'])

      // checking if email is registered
      const user = await User.findByOrFail('email', email)

      // generating token
      const token = await crypto.randomBytes(10).toString('hex')

      // registering when token was created and saving token
      user.token_created_at = new Date()
      user.token = token

      // persisting data (saving)
      await user.save()

      await Mail.send('emails.recover', { user, token }, (message) => {
        message
          .from('support@danmiranda.io')
          .to(email)
      })

      return user
    } catch (err) {
      console.log(err)
    }
Enter fullscreen mode Exit fullscreen mode

让我们添加一条路由来处理这个请求。

Route.post('users/forgotPassword', 'ForgotPasswordController.store')
Enter fullscreen mode Exit fullscreen mode

当您测试请求时,我们的请求将返回我们的用户,因此您将能够看到生成的令牌:

{
  "id": 10,
  "username": "DanSilva",
  "email": "danmiranda@danmiranda.io",
  "password": "$2a$10$3p5Ci56Zc2h7i0nC7NrfFuuorTuS/7qdAPjudPBwDTzvYrZLbOa8i",
  "created_at": "2019-03-03 15:40:02",
  "updated_at": "2019-03-04 22:49:59",
  "token": "79ee3379e35eeabdbcca", // HERE IS THE TOKEN
  "token_created_at": "2019-03-05T01:49:59.958Z"
}
Enter fullscreen mode Exit fullscreen mode

另外,请查看您的邮件陷阱收件箱,您很可能会看到已发送的邮件。

邮件陷阱邮件

太棒了!我们已经完成了处理密码恢复请求的控制器创建。在下一节也是最后一节中,我们将创建一个方法,根据令牌以及令牌是否仍然有效,将密码更新为新密码。

更新和恢复密码

如果您查看电子邮件中的链接,您会看到类似这样的内容:

http://127.0.0.1:3333/users/forgotPassword/79ee3379e35eeabdbcca/danmiranda@danmiranda.io

它的结构基本如下:

base_url/users/forgotPassword/:token/:email

我们将使用此 URL 设置路由,该路由将触发控制器的方法。

控制器的更新方法将遵循以下逻辑:

  • 我们从 URL 请求中获取令牌和用户邮箱地址。
  • 我们获得了用户想要的新密码。
  • 在数据库中查找用户(使用电子邮件地址)
  • 检查从 URL 获取的令牌是否仍然与数据库中的令牌相同(这在用户请求新的密码恢复并尝试使用旧链接时很有用)。
  • 检查令牌是否仍然有效
  • 更新密码和重置令牌

那我们开始工作吧……

要获取 URL 中的参数,我们使用params来自请求上下文的信息。

async update ({ request, response, params }) {
    const tokenProvided = params.token // retrieving token in URL
    const emailRequesting = params.email // email requesting recovery
Enter fullscreen mode Exit fullscreen mode

现在用户想要的新密码

const { newPassword } = request.only(['newPassword'])
Enter fullscreen mode Exit fullscreen mode

我们来查找用户

const user = await User.findByOrFail('email', emailRequesting)
Enter fullscreen mode Exit fullscreen mode

现在我们来处理令牌,首先检查链接是否使用了旧令牌,然后检查当前令牌是否仍然有效。

// checking if token is still the same
// just to make sure that the user is not using an old link
// after requesting the password recovery again
const sameToken = tokenProvided === user.token

if (!sameToken) {
    return response
        .status(401)
        .send({ message: {
            error: 'Old token provided or token already used'
        } })
}

// checking if token is still valid (48 hour period)
const tokenExpired = moment()
.subtract(2, 'days')
.isAfter(user.token_created_at)

if (tokenExpired) {
    return response.status(401).send({ message: { error: 'Token expired' } })
}
Enter fullscreen mode Exit fullscreen mode

最后,在对提供的令牌进行所有检查并成功通过后,我们更新密码并重置令牌:

// saving new password
user.password = newPassword

// deleting current token
user.token = null
user.token_created_at = 0

// persisting data (saving)
await user.save()
Enter fullscreen mode Exit fullscreen mode

你的update()方法现在应该是这样的:

async update ({ request, response, params }) {
    const tokenProvided = params.token // retrieving token in URL
    const emailRequesting = params.email // email requesting recovery

    const { newPassword } = request.only(['newPassword'])

    // looking for user with the registered email
    const user = await User.findByOrFail('email', emailRequesting)

    // checking if token is still the same
    // just to make sure that the user is not using an old link
    // after requesting the password recovery again
    const sameToken = tokenProvided === user.token

    if (!sameToken) {
      return response
        .status(401)
        .send({ message: {
          error: 'Old token provided or token already used'
        } })
    }

    // checking if token is still valid (48 hour period)
    const tokenExpired = moment()
      .subtract(2, 'days')
      .isAfter(user.token_created_at)

    if (tokenExpired) {
      return response.status(401).send({ message: { error: 'Token expired' } })
    }

    // saving new password
    user.password = newPassword

    // deleting current token
    user.token = null
    user.token_created_at = 0

    // persisting data (saving)
    await user.save()
  }
Enter fullscreen mode Exit fullscreen mode

你的整个流程ForgotPassowrdController应该像这样:

'use strict'

const User = use('App/Models/User')
const Mail = use('Mail')

const moment = require('moment')
const crypto = require('crypto')

class ForgotPasswordController {
  /**
   * this method will store a new request made by the user
   * when he requires a password recover it'll generate a
   * token to allow him to reset his password
   */
  async store ({ request }) {
    try {
      // account request password recovery
      const { email } = request.only(['email'])

      // checking if email is registered
      const user = await User.findByOrFail('email', email)

      // generating token
      const token = await crypto.randomBytes(10).toString('hex')

      // registering when token was created and saving token
      user.token_created_at = new Date()
      user.token = token

      // persisting data (saving)
      await user.save()

      await Mail.send('emails.recover', { user, token }, (message) => {
        message
          .from('support@danmiranda.io')
          .to(email)
      })

      return user
    } catch (err) {
      console.log(err)
    }
  }

  async update ({ request, response, params }) {
    const tokenProvided = params.token // retrieving token in URL
    const emailRequesting = params.email // email requesting recovery

    const { newPassword } = request.only(['newPassword'])

    // looking for user with the registered email
    const user = await User.findByOrFail('email', emailRequesting)

    // checking if token is still the same
    // just to make sure that the user is not using an old link
    // after requesting the password recovery again
    const sameToken = tokenProvided === user.token

    if (!sameToken) {
      return response
        .status(401)
        .send({ message: {
          error: 'Old token provided or token already used'
        } })
    }

    // checking if token is still valid (48 hour period)
    const tokenExpired = moment()
      .subtract(2, 'days')
      .isAfter(user.token_created_at)

    if (tokenExpired) {
      return response.status(401).send({ message: { error: 'Token expired' } })
    }

    // saving new password
    user.password = newPassword

    // deleting current token
    user.token = null
    user.token_created_at = 0

    // persisting data (saving)
    await user.save()
  }
}

module.exports = ForgotPasswordController

Enter fullscreen mode Exit fullscreen mode

现在我们来测试最后一个方法。首先,按照我之前提到的结构添加路由:

base_url/users/forgotPassword/:token/:email并在我们的路由中添加 PUT 请求

Route.put('users/forgotPassword/:token/:email', 'ForgotPasswordController.update')
Enter fullscreen mode Exit fullscreen mode

首先,我将测试令牌编号错误的情况:

像这样的请求会返回 401 错误和以下 JSON 数据:

{
  "message": {
    "error": "Old token provided or token already used"
  }
}
Enter fullscreen mode Exit fullscreen mode

最后一个例子,在测试成功的例子之前,是一个无效令牌。为了测试这一点,我将手动修改数据库中令牌的生成日期,使其超过两天。

这样一来,我还会收到一个 401 错误和一个 JSON 数据,提示令牌已过期。

现在到了我们期待已久的测试环节。我再次修改了令牌的创建日期,使其符合两天的限制。我不会收到任何消息正文,只会收到204状态更新。在这个例子中,我将新密码设置为“12”。

如果我尝试使用旧密码“123456”登录,我会收到错误提示;但如果我尝试使用新密码“12”,一切应该就没问题了。

哇!这篇文章好长啊,我就先写到这里吧。下一篇会讲解用户创建新预约的步骤,我们下一篇见!

文章来源:https://dev.to/nilomiranda/building-an-api-with-adonisjs-part-2-3p94