使用 Express js 和 Fauna 构建用户登录系统
在本文中,我将向您展示如何使用节点框架 express.js 和 Fauna 构建用户登录系统。
什么是动物区系?
Fauna 是一个全球云数据库,旨在与 Jamstack 和现代无服务器架构集成。Fauna 是一个灵活、开发者友好的事务型数据库,以安全可扩展的云 API 形式提供,并支持原生 GraphQL。
Fauna 是一个 NoSQL 无服务器数据库,因此您不必担心数据库配置、扩展、分片、复制或正确性。
让我们直接开始构建我们的用户登录系统!
先决条件
为了充分利用本文,您需要在笔记本电脑上安装以下软件。
- Node.js
- 可以访问一个包管理器,例如 npm 或 yarn
- 访问 Fauna 仪表板
- 对 Node.js、Express 和 Handlebars.js 或视图引擎有基本的了解。
关于应用程序
在这个应用程序中,我们将有六条路线:
- 注册路线:在此路线中,使用必要的凭据(例如电子邮件、用户名和密码)创建新用户,然后用户登录其帐户并显示其仪表板页面。
- 登录路由:在此路由中,用户通过提供注册信息登录。如果登录成功,则显示用户信息中心页面;如果登录失败,则不显示。系统会根据登录失败的原因,向用户显示必要的错误消息。
- 仪表板路线:在此路线中,成功注册或登录后,将向用户显示一个定制的仪表板页面,欢迎用户访问他们的页面。
- 退出路线:这是将用户退出其帐户的路线。
- 删除账户路由:在我们的应用中,用户可以删除已创建的账户。如果删除成功,该用户的账户将从我们的 Fauna 数据库中删除。
- 确认令牌路由:此路由允许用户在成功重定向到仪表板页面之前确认其电子邮件地址。
在创建路线之前,我们需要按照以下步骤创建用于应用程序的 Fauna 数据库。
步骤 1:建立我们的动物数据库
要开始使用我们的应用程序,我们需要在 Fauna 仪表板中为该应用程序创建数据库。
您可以在此处创建 Fauna 帐户。
在您的仪表板中,单击“创建数据库”按钮,为您的数据库提供一个名称,然后单击创建。
第 2 步:生成 Fauna API 密钥
Fauna 密钥将 Fauna 连接到数据库独有的应用程序或脚本。
我们需要创建一个 Fauna API 密钥,以便将 Fauna 数据库连接到我们的应用。为此,请转到屏幕左侧的安全设置。
点击上图中的“保存”按钮后,系统会生成一个新的 API 密钥。复制该 API 密钥并将其保存在安全的地方,因为您无法再次在仪表盘中访问该密钥。
步骤 3:创建 Fauna 集合
我们需要创建一个将在代码中使用的 Fauna 集合。
集合只是将具有相同或相似用途的文档(行)分组。集合的作用类似于传统 SQL 数据库中的表。
在我们的应用中,我们只有一个用户集合。用户集合用于存储用户数据。
要创建集合,请单击您创建的数据库,单击“新建集合”,输入您选择的集合名称,然后单击保存。
您可以创建任意数量的集合名称以供您在应用中使用。
步骤 4:创建动物群索引
索引用于快速查找数据,无需每次访问数据库集合时都搜索数据库集合中的所有文档。可以使用数据库集合中的一个或多个字段创建索引。要创建 Fauna 索引,请点击仪表板左侧的“索引”部分。
在我们的应用程序中,我们将只创建一个索引,即 user_by_email 索引。
我们将使用 user_by_email 索引来获取指定邮箱地址的用户数据。此索引必须是唯一的,以确保集合中不存在重复的邮箱地址。
创建项目并安装依赖项
首先,我们需要在 npm 中初始化您的项目;在您的终端中输入以下内容:
npm init
这将提示一些问题,您可以适当地回答这些问题,完成后,将为您创建一个 package.json 文件。
接下来,我们需要安装所需的依赖项。在终端中输入以下内容:
npm install express faunadb dotenv express-handlebars
构建应用程序
- routes 文件夹中存放着用于定义路线的 routes.js 文件。
- 我们会将页面创建在 views 文件夹中,在本例中即为 handlebars。
- app.js 文件是我们将设置服务器的地方。
- configure.js 文件是我们设置应用程序中间件的地方。
- 在 artwork.js 文件中,我们将 Fauna 数据库连接到我们的应用程序,并定义用于创建用户、登录用户的函数以及我们将在路线中使用的一些其他函数。
- sendMail.js 文件是用户创建帐户后我们将使用 nodemailer 发送确认电子邮件来验证用户的地方。
构建我们的应用程序
- 配置和运行服务器:在您的 app.js 中,写入以下代码来设置您的服务器。
var express = require('express'),
config = require('./configure'),
path = require("path"),
app = express();
app = config(app);
app.set("port", process.env.PORT || 5000);
app.set("views", path.join(__dirname, "views"));
var server = app.listen(app.get("port"), function () {
console.log("Server up: http://localhost:" + app.get("port"));
});
- 在您的配置文件(即 configure.js)中,编写以下代码来配置您的中间件功能。
var createError = require('http-errors');
routes = require('./routes/routes')
express = require('express'),
session = require('express-session'),
path = require('path'),
cookieParser = require('cookie-parser'),
logger = require('morgan'),
dotenv = require('dotenv').config(),
flash = require('connect-flash'),
exphbs = require('express-handlebars'),
relativeTime = require('dayjs/plugin/relativeTime'),
dayjs = require('dayjs');
module.exports = function (app) {
dayjs.extend(relativeTime);
app.engine('.hbs', exphbs.create({
defaultlayout: 'main',
layoutsDir: path.join(__dirname, './views/layouts'),
partialsDir: path.join(__dirname, './views/partials'),
helpers: { timeago: () => dayjs(new Date().toString()).fromNow()},
extname: '.hbs',
}).engine);
app.set('view engine', 'hbs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(flash());
app.use(session({
secret: process.env.SECRET,
resave: true,
saveUninitialized: true,
maxAge: 600
}))
app.use(function(req,res,next){
app.locals.isLoggedIn = req.session.user ? true : false
next();
})
app.use(routes)
app.use('/public/', express.static(path.join(__dirname, './public')));
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
return app;
};
- 在路由文件夹中创建一个 .env 文件并填充以下内容:
NODE_LOGIN_FAUNA_KEY=’your generated fauna API key’
SECRET=’your app secret key’
EMAIL=’your email’
PASSWORD=’your email password’
您在此处输入的电子邮件将用于向新用户发送确认电子邮件,因此请确保它有效且可用。
创建我们的 Fauna 辅助函数
在 Fauna 中,您需要创建用户、登录用户、更新用户验证状态(我们将使用该状态来判断用户是否已验证)以及删除用户。Fauna 提供了辅助函数来帮助完成这些操作。请将以下内容粘贴到您的代码中以完成这些操作:
var dotenv = require('dotenv').config(),
faunadb = require('faunadb'),
bcrypt = require('bcrypt'),
q = faunadb.query;
let Client = new faunadb.Client({ secret: process.env.NODE_LOGIN_FAUNA_KEY });
exports.createUser = async (email, username, password) => {
password = bcrypt.hashSync(password, bcrypt.genSaltSync(10)) // generates a hash for the password
let data
try {
data= await Client.query(
q.Create(
q.Collection('Users'),
{
data: {email, username, password, isVerified: false}
}
)
)
if (data.username === 'BadRequest') return // if there's an error in the data creation it should return null
} catch (error) {
console.log(error)
return
}
const user = data.data
user.id = data.ref.value.id // attaches the ref id as the user id in the client, it will be easy to fetch and you can guarantee that it's unique
return user
}
exports.getUserByEmail = async (email) => {
try {
const user = await Client.query(
q.Get(
q.Match(
q.Index('user_by_email'),
email
)
)
)
return user.data
} catch {
return // return null if there is any error.
}
}
exports.loginUser = async (email, password) => {
try {
let userData = await Client.query(
q.Get(
q.Match(q.Index('user_by_email'), email.trim())
)
)
userData.data.id = userData.ref.value.id
if (bcrypt.compareSync(password, userData.data.password)) return userData.data
else return
} catch (error) {
return
}
}
exports.updateUser = (userId) => {
const user = Client.query(
q.Update(
q.Ref(q.Collection('Users'), userId),
{
data: {
isVerified: true
}
}
)
)
.then((result) => result.data)
.catch((err) => console.log(err.message))
}
exports.deleteUser = (userId) => {
const user = Client.query(
q.Delete(
q.Ref(q.Collection('Users'), userId)
)
)
.then((result) => console.log(result))
.catch((err) => console.log(err.message))
}
上面我们创建了五个 Fauna 辅助函数,分别是:
- createUser:它接受电子邮件、用户名和密码,使用 bcrypt 为密码生成哈希值,将用户信息保存为 false,并将 isVerified 设置为 false,直到用户确认帐户,然后将 isVerified 设置为 true
- getUserByEmail:它使用我们之前创建的索引通过电子邮件检索用户。
- loginUser:使用电子邮件和密码登录用户。
- updateUser:它更新用户的信息,在这种情况下,更新用户的验证状态。
- deleteUser:从 Fauna 数据库中删除用户。
定义路线
为了定义我们之前讨论过的应用程序的所有可能的路线,请在 routes 文件夹中创建一个 routes.js 文件,并输入以下内容:
var express = require('express'),
hbs = require('express-handlebars'),
router = express.Router(),
auth = require('../fauna'),
{sendMail} = require('../sendMail'),
dotenv = require('dotenv').config(),
jwt = require('jsonwebtoken');
router.get('/', (req, res) => {
return res.render('index');
});
// Sign Up Routes
router.get('/signup/', (req, res) => {
return res.render('auth/signup')
})
router.post('/signup/', async (req, res) => {
try {
const {username, email, password, confirm_password} = req.body
if (password !== confirm_password) {
return res.render('auth/signup', {
error: 'Passwords do not match'
})
}
const user = await auth.createUser(email, username, password)
let token = jwt.sign(user, process.env.SECRET, {expiresIn: 600})
if (user) {
req.session.user = user
// Send verification mail for confirmation of account using Nodemailer
sendMail(email, `Hi ${username}!,\nTo verify your account, please click on the link below and signin again. \nhttp://${req.headers.host}/confirm/${token}`, 'Verify your account')
req.session.save((err) => {console.log(err)})
return res.redirect('/dashboard/')
}
}
catch (error){
return res.render('auth/signup', {
error: error.message
})
}
return res.render('auth/signup', {
error: 'Username or Email is chosen'
})
})
// Sign In Routes
router.get('/signin/', function(req, res) {
return res.render('auth/signin');
});
router.post('/signin/', async (req, res) => {
try {
const {email, password} = req.body
const user = await auth.loginUser(email, password)
if (user) {
req.session.user = user
req.session.save((err) => console.log(err))
return res.redirect('/dashboard/')
}
}
catch (error){
return res.render('auth/signin', {
error: 'Invalid Email or Password'
})
}
return res.render('auth/signin', {
error: 'Invalid Email or Password'
})
});
// Dashboard Routes
router.get('/dashboard/', async (req, res) => {
try {
if (req.session.user) {
const user = req.session.user
return res.render('dashboard', {user})
}
}
catch (error){
return res.render('dashboard', {
error: error.message
})
}
return res.redirect('/')
});
// Sign Out Routes
router.get('/signout/', (req, res) => {
req.session.destroy((err) => console.log(err))
return res.redirect('/signin/')
})
// Delete Account Route
router.delete('/delete-account/', async (req, res) => {
if (req.session.user) {
auth.deleteUser(req.session.user.id)
req.session.destroy();
return res.status(200).json({success: 'Data Deleted Successfully' })
} else {
return res.status(400).json({error: 'Not Successfully Deleted'})
}
})
// confirm token and update user verification status
router.get('/confirm/:token', (req, res) => {
const token = req.params.token
jwt.verify(token, process.env.SECRET, (err, decoded) => {
try {
if (err) {
return res.render('auth/signup', {
error: 'Invalid Token'
})
}
user = auth.updateUser(decoded.id, {isVerified: true})
if (user) {
req.session.user = user
return res.redirect('/dashboard')
}
} catch (error) {
return res.render('auth/signup', {
error: 'Invalid Token'
})
}
})
})
module.exports = router;
在仪表板路由中,我们在登录后添加了用户会话,以便在用户尚未退出的情况下轻松登录一段时间。
在退出路由中,用户会话被删除,并且用户被重定向回主页。
在删除路由中,使用我们在 fogagních.js 文件中创建的 deleteUser 函数从 Fauna 数据库中删除用户。
在确认路由中,我们使用 jsonwebtoken 生成了一个唯一的 token,并使用 nodemailer 发送一封包含该 token 的重定向链接的邮件,该链接会重定向到仪表盘页面并确认用户的邮箱地址。之后,用户的 isVerified 状态将被设置为 true。
发送邮件
我一直在提到发送邮件,但为了真正发送邮件,我们需要一个辅助函数,用于在用户创建帐户后发送邮件。我们需要创建一个 sendMail.js 文件。在下面输入以下内容:
var config = require('./configure'),
express = require('express'),
router = express.Router(),
nodemailer = require('nodemailer');
exports.sendMail = async (to, html, subject) => {
var transporter = nodemailer.createTransport({
service: 'gmail',
port:465,
auth: {
user: process.env.EMAIL,
pass: process.env.PASSWORD
}
});
var mailOptions = {
from: process.env.EMAIL,
to: to,
subject: subject || 'Confirmation of Account',
html: html
};
transporter.sendMail(mailOptions, function(error, info){
if (error) {
console.log(error);
return {error: error.message}
} else {
console.log('Email sent: ' + info.response);
return {success: info.response}
}
});
transporter.close()
}
测试我们的应用程序
正如我之前所说,我们的前端是用 Handlebars 构建的。你可以选择任何你想使用的视图引擎。让我们测试一下我们构建的路线:
- 注册路线
我们使用我们的凭证(电子邮件、用户名和密码)进行注册,它会重定向到仪表板页面,并显示欢迎消息,但提示用户应检查他/她的电子邮件以获取验证说明。
让我们确认一下数据库中是否已经创建了该用户
然后我们会确认我们的电子邮件是否已发送。
附言:为了使 nodemailer 能够使用您提供的电子邮件发送邮件,您必须将 Gmail 设置配置为“允许安全性较低的应用程序”并启用Recaptcha。
- 登录路线
我们将点击发送到邮件的链接并检查它是否重定向到登录页面。
我们将再次登录并看到经过验证的用户的新欢迎消息。
- 退出路线
我们将点击退出按钮并退出帐户。
- 删除路线
我们再次登录并测试删除帐户功能。该用户将从 Fauna 数据库中彻底删除。
最后,我们将从数据库中确认该用户是否已被删除。
正如我们上面看到的,我们创建的唯一用户已被删除。
结论
本文构建了一个 Web 应用程序,用于登录和注销用户,使用了 Fauna 和 Expressjs 两种激动人心的技术。该项目的源代码可在Github上获取。如果您喜欢这篇文章,请分享给有需要的朋友。 如有任何疑问,可以通过Twitter联系我。
与“与动物一起写作”计划相关而撰写。
鏂囩珷鏉ユ簮锛�https://dev.to/sodiq123/building-a-user-login-system-with-express-js-and-fauna-4p32