使用 AdonisJS 构建 API(第二部分)
这是关于使用 AdonisJS 创建 API 系列文章的第二篇。如果您还没有阅读第一篇,请点击此链接:使用 AdonisJS 构建 API。
现在我们继续第二部分。在这里我们将学习如何:
- 更新用户信息(PUT 请求)
- 恢复用户密码
- 使用 Adonis 发送电子邮件
- 使用迁移工具更新表结构
更新用户信息
我们首先来创建控制器,允许用户更新他的信息,例如他的用户名和密码(在本应用程序中,用户将无法更新他的电子邮件)。
其背后的逻辑非常简单:
- 用户将提交请求,包括他想要的新用户名、当前密码和想要的新密码。
- 然后我们将在数据库中搜索该用户。
- 然后我们检查当前提供的密码是否正确,然后用新提供的密码更新他的信息。
为了创建一个新的控制器,我们需要运行以下 Adonis 命令:
adonis make:controller UpdateUserInfo --type http
现在我们可以打开文件app/controllers/http/UpdateUserInfoController.js并开始编写代码了:
让我们确保导入我们的User模型,我们还将使用 Adonis 的一个名为Hash.
出于安全考虑,哈希程序将负责对新提供的密码进行哈希处理。
'use stric'
const User = use('App/Models/User')
const Hash = use('Hash')
我们的控制器只需要一个update方法,所以让我们在控制器内部UpdateUserInfoController首先创建这个方法:
class UpdateUserInfoController {
async update ({ request, response, params }) {
基于我们的逻辑,我们执行以下操作:
- 让我们获取用户在请求中发送的新信息:
2.
const id = params.id
const { username, password, newPassword } = request
.only(['username', 'password', 'newPassword'])
- 现在在数据库中查找该用户(使用 id):
const user = await User.findByOrFail('id', id)
- 检查提供的密码是否与当前密码匹配:
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
- 最后,我们只需要使用该
.save()方法将数据持久化到数据库中即可。
await user.save()
你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
完美!现在让我们测试一下控制器。前往start/routes.js文件
这里非常重要的一点是,我们的一些路由只能由已认证的用户访问,而 Adonis 的路由机制提供了一个完美的方法来处理这种情况,称为 `group` group()。group您可以使用 `group` 方法,middleware并将一个数组作为参数传递给它,该数组包含在访问 `group` 方法内部的路由之前应该运行哪些中间件。
Route.group(() => {
// updating username and password
Route.put('users/:id', 'UpdateUserInfoController.update')
}).middleware(['auth'])
在我们的例子中,我们只需要 Adonis 默认提供的身份验证方法。稍后我们将测试这条路由在用户未验证和已验证两种情况下的性能。
首先,让我们在未进行身份验证的情况下测试此路由:
这是我想为用户保存的新信息:
{
"password": "123456",
"newPassword": "123",
"username": "DanSilva"
}
这里我就不赘述如何使用 Insomnia 发送请求了,因为我在第一部分已经讲解过了。
如果我在未登录的情况下发送请求,将会收到 401 错误(未授权)。为了使此方法有效,我必须在请求中提供一个登录时获得的 JWT 令牌,因此请确保登录后再测试此路由。
登录后,复制请求返回的令牌。在 Insomnia 中创建一个新的 PUT 方法,在请求 URL 下方有一个名为“身份验证”的选项卡。在打开的下拉菜单中选择该选项卡Bearer Token,然后在令牌字段中粘贴您刚刚复制的令牌。
在再次发送请求之前,让我们查看数据库中的用户数据,以确保在我们的请求之后数据已更新。
完美。现在我们发送请求。请确保您的 URL 符合以下结构。
base_url/users/YOUR_USER_ID_HEre
现在发送请求。如果请求成功,将返回 204 状态码,因为我们没有设置任何要返回的消息。
看到了吗?新用户信息已保存到我们的数据库中!
使用 AdonisJS 发送电子邮件
在继续创建控制器以请求密码恢复并使用此恢复设置新密码之前,让我们先来看看如何配置 Adonis 发送电子邮件。
该电子邮件服务提供商默认未安装,因此我们需要手动安装。为此,只需运行以下命令:
adonis install @adonisjs/mail
现在我们需要在应用程序中注册新的提供商。我们的提供商数组位于 [此处应填写文件路径] start/app.js。打开该文件并找到名为 [此处应填写变量名] 的变量providers。该变量是一个数组,其中包含使 Adonis 正常工作所必需的所有提供商。只需将以下提供商添加到此数组的末尾即可:
'@adonisjs/mail/providers/MailProvider'
我们还需要配置一些内容才能继续。我们需要一位客户来测试发送电子邮件的功能,而我们正好有一个非常适合这项任务的工具。
我们将使用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
我们将把以上数据插入到我们的.env文件中,以便正确设置邮件服务:
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=465
MAIL_USERNAME=a218f0cd73b5a4
MAIL_PASSWORD=0a5b3c6c6acc17
请确保该数据MAIL_USERNAME与MAIL_PASSWORDmailtrap 提供给您的数据相符。
现在我们需要前往app/mail.js完成电子邮件设置。
由于我们将使用 SMTP 协议,因此文件连接会话的配置将保持不变。请确保您的配置与我的相同(当然,前提是您也使用 SMTP 协议):
connection: Env.get('MAIL_CONNECTION', 'smtp')
现在找到该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
},
太棒了,如果我们完成了所有这些步骤,就可以配置我们的应用程序来发送电子邮件了。实际上,这并不需要做太多工作。我们只需要三个步骤:
- 安装 Adonis 的邮件提供商
- 配置环境变量以使用我们所需的邮件服务。
- 我们已配置
mail.js文件以从环境变量中获取信息。
请求恢复密码
我们先从密码找回开始。您知道,当您点击“忘记密码”后,通常需要提供您的电子邮件地址,然后您会收到一封包含密码找回链接的电子邮件吗?这就是我们现在要做的。
为此,我们需要检查请求的有效性,我的意思是,假设你发送了第一个请求,那么你有两天的时间点击发送给你的链接,否则它将失效。
为了方便演示,我将使用令牌(token)。因此,在开始之前,我们需要在数据库的用户表中添加一个令牌字段。由于在应用程序启动时,我们已经运行了创建用户表的迁移,所以我们需要运行一个新的迁移来更新表结构,以便添加令牌列。
要创建新的迁移,请运行以下命令:
adonis make:migration user --action select
在继续之前,我们先来看一下这条命令的结构:
adonis make:migration MIGRATION_NAME --action ACTION_NAME(create, select)
- MIGRATION_NAME_HERE:迁移的名称。建议您在此处填写要更新的表的名称。
- ACTION_NAME:您有两个选项:
- 创建:用于创建新表。
- 选择:当您想要更新现有表的结构时,请使用此选项。
现在选择新创建的迁移文件,该文件位于database/migrations
在你的迁移文件中,你会看到一个包含两个方法的类,分别是 `update`up和 `reverse` down。现在我们先关注 `update`up方法,因为它用于创建更新。`reverse`down方法则用于撤销你所做的更改。
up () {
this.table('users', (table) => {
// make alterations
})
}
您可以看到它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
})
}
只需运行我们的迁移程序即可:
adonis migration:run
太好了,现在我们的用户表已经更新完毕,接下来我们将创建控制器。我把它命名为ForgotPassword……
adonis make:controller ForgotPassword --type http
我们首先创建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
我们需要获取用户的电子邮件地址,并在数据库中找到他:
// account request password recovery
const { email } = request.only(['email'])
// checking if email is registered
const user = await User.findByOrFail('email', email)
之后我们将生成令牌。生成令牌将使用cryptoNodeJS 自带的原生功能。(您可以在这里找到更多关于加密的信息:https://nodejs.org/api/crypto.html)
// generating token
const token = await crypto.randomBytes(10).toString('hex')
生成令牌后,我们使用 .将其转换为字符串toString()。
现在我们需要设置令牌的有效期。为此,我们需要存储令牌的创建时间:
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)
})
Mail.send()使用三个参数:
- 邮件模板(我们稍后会谈到)。
- 要发送到模板的变量
- 回调函数用于设置诸如发件人、收件人、主题、附件等信息……
首先我们来谈谈模板。由于我们创建的这个 Adonis 应用程序仅作为 API 使用,因此我们需要在提供程序列表中注册视图提供程序。start/app.js
'@adonisjs/framework/providers/ViewProvider'
所有视图都必须保存在同一个目录中resources/views,因此请在项目根目录创建 `<views>` 文件夹resources,并在该文件夹内创建 `<views>` 文件views夹。现在,您可以在此文件夹中根据需要组织视图。例如,在我们的应用程序中,我会将电子邮件模板存储在 `<email>`emails文件夹中。到目前为止,您的文件夹结构可能如下所示:
├── resources
│ └── views
│ └── emails
│ └── recover.edge
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>
这里最需要注意的是双括号的使用{{}}。您可以使用这种语法来访问传递给模板的变量。在上面的例子中,我们获取了用户的用户名、电子邮件地址和令牌。
现在让我们回顾一下我们的Mail.send()函数:
await Mail.send('emails.recover', { user, token }, (message) => {
message
.from('support@danmiranda.io')
.to(email)
})
正如我们之前所说,第一个参数是模板。由于 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)
}
让我们添加一条路由来处理这个请求。
Route.post('users/forgotPassword', 'ForgotPasswordController.store')
当您测试请求时,我们的请求将返回我们的用户,因此您将能够看到生成的令牌:
{
"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"
}
另外,请查看您的邮件陷阱收件箱,您很可能会看到已发送的邮件。
太棒了!我们已经完成了处理密码恢复请求的控制器创建。在下一节也是最后一节中,我们将创建一个方法,根据令牌以及令牌是否仍然有效,将密码更新为新密码。
更新和恢复密码
如果您查看电子邮件中的链接,您会看到类似这样的内容:
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
现在用户想要的新密码
const { newPassword } = request.only(['newPassword'])
我们来查找用户
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()
你的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()
}
你的整个流程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
现在我们来测试最后一个方法。首先,按照我之前提到的结构添加路由:
base_url/users/forgotPassword/:token/:email并在我们的路由中添加 PUT 请求
Route.put('users/forgotPassword/:token/:email', 'ForgotPasswordController.update')
首先,我将测试令牌编号错误的情况:
像这样的请求会返回 401 错误和以下 JSON 数据:
{
"message": {
"error": "Old token provided or token already used"
}
}
最后一个例子,在测试成功的例子之前,是一个无效令牌。为了测试这一点,我将手动修改数据库中令牌的生成日期,使其超过两天。
这样一来,我还会收到一个 401 错误和一个 JSON 数据,提示令牌已过期。
现在到了我们期待已久的测试环节。我再次修改了令牌的创建日期,使其符合两天的限制。我不会收到任何消息正文,只会收到204状态更新。在这个例子中,我将新密码设置为“12”。
如果我尝试使用旧密码“123456”登录,我会收到错误提示;但如果我尝试使用新密码“12”,一切应该就没问题了。
哇!这篇文章好长啊,我就先写到这里吧。下一篇会讲解用户创建新预约的步骤,我们下一篇见!
文章来源:https://dev.to/nilomiranda/building-an-api-with-adonisjs-part-2-3p94



