【已过时】如何使用 Next.js 和 MongoDB 构建一个功能齐全的应用程序 第一部分:用户身份验证
这并未反映最近的重写版本nextjs-mongodb-app。请查看最新版本。
更新:为了兼容性,我已将 argon2 替换为 bcryptjs。
过去一个月,我一直在努力为我的下一个项目实现一个身份验证系统。这个项目隶属于一个我找到的组织,该组织致力于用代码解决社区问题。
该项目是一款旨在促进良好行为的在线游戏,因此需要为用户提供登录方式。
我使用的是React 框架Next.js和MongoDB作为数据库。
我之前用的是 Express 作为后端,搭建了一个基于 Next.js 的自定义服务器,并(偷懒地)使用 PassportJS 来处理身份验证。后来我意识到,这样我对 API 的控制力很弱。因此,我决定自己开发一套身份验证系统。
或许有人会反驳说我不应该重复发明轮子。但我渴望探索和挑战,所以我选择了这条路。
我在网上找到的大多数解决方案要么依赖第三方服务(例如 Google、Facebook、身份即服务),要么不够完善,没有充分考虑实际应用。因此,我决定自己开发一个。
下面提供了该项目的Github代码库和演示,供您参考。
演示(已修复)
关于nextjs-mongodb-app项目
nextjs-mongodb-app 是一个使用 Next.js 和 MongoDB 构建的完整应用程序。网上大多数教程要么不够完善,要么无法直接用于生产环境。本项目旨在解决这些问题。
该项目更进一步,尝试整合现实应用中的顶级功能,使其成为一款功能齐全的应用。
更多信息请访问Github仓库。
入门
典型的教程会告诉你该npm install next怎么做,但我相信你已经是一位经验丰富的开发者了。
我将首先着手搭建我的应用程序框架。
环境因素
本项目从数据库中检索变量process.env。您需要实现自己的策略。以下是一些可能的解决方案:
- https://github.com/zeit/next.js/tree/canary/examples/with-dotenv
- 在服务提供商中设置环境变量(例如 now.sh env 或 heroku config var)
本项目所需的环境变量包括:
- process.env.MONGODB_URI
请求库
本项目需要一个请求库来向 API 发送请求。您可以随意选择。以下是一些建议:
验证库
我使用验证器进行验证,但您也可以使用自己的库或编写自己的检查。
密码哈希库
密码必须经过哈希处理。就这么简单。市面上有很多不同的哈希库:
还有,请不要使用 MD5、SHA1 或 SHA256!
Mongoose……我是说 MongoDB
Mongoose不是MongoDB,它们完全是两码事。我看到很多教程把 Mongoose 和 MongoDB 混为一谈,这是大错特错的。
Mongoose是一个对象数据建模库。它通过在数据库中定义模式来确保数据的持久性和完整性,但我认为这违背了NoSQL的初衷。
NoSQL 的设计理念是摒弃验证和建模(即ACID原则)。这意味着它接受任何类型的数据,并且对数据结构的后续修改也更加灵活。另一方面,Mongoose则要求你为所有内容都定义一个模式。如果我将Age字段定义为 `null` Number,那么尝试将数据保存String到该字段就会产生错误。
请注意,使用 Mongoose 会显著降低性能,因为每次读取和写入操作都必须验证数据(这足以证明放弃的合理性ACID)。
如果您打算使用Mongoose,以下是我为其编写的 schema User:
const UserSchema = new mongoose.Schema({
email: {
type: String,
trim: true,
minlength: 1,
unique: true,
index: true,
},
emailVerified: {
type: Boolean,
},
password: {
type: String,
minlength: 6,
},
name: {
type: String,
},
});
export default mongoose.models.User || mongoose.model('User', UserSchema);
构建完整的身份验证系统。
响应模式
量两次,切一次
我必须强调这一点。我希望我的 API 响应能够保持一致性。因此,我制定了以下方案——一个适用于所有 API 的标准响应:
{
"status": "ok / error",
"message": "a user-readable message",
"data": "<payload object>"
}
欢迎您提出自己的观点。您可以参考这篇Stack Overflow 讨论,从中汲取一些灵感。
如果我没有提前做好规划,可能会遇到一些问题。想象一下,我的某个端点可能会这样响应:
{
"success": true,
"msg": "successful stuff",
}
……而另一人则回应道:
{
"error": false,
"text": "ok",
}
我需要编写两套不同的检查语句,并尝试使用两个不同的字段来显示消息:
if (response.success === true || response.error === false) {
alert(response.msg || response.text);
}
情况会很快恶化,尤其是当端点数量增加且缺乏文档记录时。
中间件
如果您有相关背景,可能对中间件这个术语比较熟悉ExpressJS。
在 Next.js 中使用中间件的方法是,将原始的handler(API 路由函数(req, res))作为参数,并返回一个handler具有附加功能的新中间件。
数据库中间件
我们需要一个中间件来处理数据库连接,因为我们不想在每个路由中都调用数据库。
创建middlewares/withDatabase.js:
import { MongoClient } from 'mongodb';
const client = new MongoClient(process.env.MONGODB_URI, { useNewUrlParser: true });
const withDatabase = handler => (req, res) => {
if (!client.isConnected()) {
return client.connect().then(() => {
req.db = client.db('nextjsmongodbapp');
return handler(req, res);
});
}
req.db = client.db('nextjsmongodbapp');
return handler(req, res);
};
export default withDatabase;
我的做法是,只有在客户端未连接的情况下才会尝试连接。
然后我将数据库req.db和客户端连接到服务器req.mongoClient。客户端稍后可以在我们的会话中间件中重复使用。
不建议将 MongoDB URI 或任何其他安全变量硬编码到代码中。我是通过以下方式获取的process.env:
会话中间件
会话是我们项目的核心要素之一。在 ExpressJS 中,我们可以使用 ` Session` 中间件。在 Next.js 中,可以使用`next-session`express-session中间件。您可以安装它,也可以使用/创建自己的中间件。
npm install next-session
注意:目前,next-session它没有任何原生会话存储,但通过设置可以与会话express-session存储兼容。storePromisifytrue
默认存储方式为MemoryStore,但尚未准备好用于生产环境。目前,由于我们已经在使用 MongoDB,因此可以使用connect-mongo :
创建middlewares/withSession.js:
import session from 'next-session';
import connectMongo from 'connect-mongo';
const MongoStore = connectMongo(session);
const withSession = handler => session.withSession(handler, {
store: new MongoStore({ url: process.env.MONGODB_URI }),
});
export default withSession;
MongoStore还需要session。我们的session中间件是next-session。
全局中间件
这就是我对中间件的处理方法,我将引用它next-session。
实际上,你并不需要在每个函数中都将 `session()` 函数包裹在处理程序周围。你可能会遇到这样的情况:某个 `session()` 函数的配置与其他函数不同。一种解决方案是创建一个全局中间件。
只需创建middlewares/withMiddleware.js并包含我们的其他中间件即可:
import withDatabase from './withDatabase';
import withSession from './withSession';
const middleware = handler => withDatabase(withSession(handler));
export default middleware;
需要注意的是,顺序很重要withDatabase(withSession(handler))。稍后我们会添加一个名为 `inside` 的中间件withAuthentication,它会使用我们的数据库。因此,我们需要确保withAuthentication`inside`withDatabase和 `inside`withSession都正确,否则数据库将无法准备就绪,会话也将不存在。
components/layout布局
这部分是可选的。你可以在这里引入你的 Header.jsx 文件,这样它就会出现在每个页面上,只要你用 . 包裹页面即可<Layout>。
我将使用 Next.js 添加一些样式styled-jsx。
import React from 'react';
export default ({ children }) => (
<>
<style jsx global>
{`
* {
box-sizing: border-box;
}
body {
color: #4a4a4a;
background-color: #f8f8f8;
}
input {
width: 100%;
margin-top: 1rem;
padding: 1rem;
border: none;
background-color: rgba(0, 0, 0, 0.05);
}
button {
color: #ecf0f1;
margin-top: 1rem;
background: #009688;
border: none;
padding: 1rem;
}
`}
</style>
{ children }
</>
);
用户注册
让我们从用户注册开始,因为我们至少需要一个用户才能进行操作。
构建注册 API
假设我们通过向服务器发送请求,输入用户POST姓名/api/users、电子邮件和密码来注册用户。
` <path>` 中的所有内容/api都是 API 路由,这是 Next.js 9 的新增功能。每个路由都必须导出一个接受两个参数的函数req。res以下是我们的路由内容users.js:
import isEmail from 'validator/lib/isEmail';
import * as argon2 from 'argon2';
import withMiddleware from '../../middlewares/withMiddleware';
const handler = (req, res) => {
if (req.method === 'POST') {
const { email, name, password } = req.body;
if (!isEmail(email)) {
return res.send({
status: 'error',
message: 'The email you entered is invalid.',
});
}
return req.db.collection('users').countDocuments({ email })
.then((count) => {
if (count) {
return Promise.reject(Error('The email has already been used.'));
}
return argon2.hash(password);
})
.then(hashedPassword => req.db.collection('users').insertOne({
email,
password: hashedPassword,
name,
}))
.then((user) => {
req.session.userId = user.insertedId;
res.status(201).send({
status: 'ok',
message: 'User signed up successfully',
});
})
.catch(error => res.send({
status: 'error',
message: error.toString(),
}));
}
return res.status(405).end();
};
export default withMiddleware(handler);
该函数验证电子邮件地址,对密码进行哈希处理,并将用户插入数据库。之后,我们将会userId话设置为新创建对象的 ID。
可以看到,我首先检查该方法是否可用,POST然后再继续执行。如果不可用,则返回状态码405 方法不允许。
如果用户已创建,我将其设置req.session.userId为已创建对象的 ID。稍后我会对此进行深入研究。
另请注意,在每次出错的情况下,我都会返回一个包含 Error 对象的拒绝。拒绝将在 `get_rejection()` 处捕获.catch(),然后我通过 `get_rejection()` 发送包含错误消息的响应.toString()(Error 是一个Error 对象,因此需要转换为字符串)。
pages/signup.jsx注册页面
在 中signup.jsx,我们将有以下内容:
import React, { useState } from 'react';
import axioswal from 'axioswal';
import Layout from '../components/layout';
import redirectTo from '../lib/redirectTo';
const SignupPage = () => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (event) => {
event.preventDefault();
axioswal
.post('/api/users', {
name,
email,
password,
})
.then((data) => {
if (data.status === 'ok') {
redirectTo('/');
}
});
};
return (
<Layout>
<div style={{ margin: '4rem' }}>
<h1>Sign up</h1>
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
placeholder="Your name"
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
<div>
<input
type="email"
placeholder="Email address"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</div>
<div>
<input
type="password"
placeholder="Create a password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
</div>
<button type="submit">
Sign up
</button>
</form>
</div>
</Layout>
);
};
export default SignupPage;
太棒了,让我们启动开发服务器,访问/signup并查看一下。尝试用一个有创意的虚构邮箱和一个很酷的密码注册。
另外,请尝试:
- 使用同一邮箱再次注册。
- 输入了无效的电子邮件地址。
它显示错误信息了吗?如果显示了,那就太好了!我们来回顾一下你刚才的操作。
如果您不熟悉 Hook,我使用的是 React State Hook useState。在每个文本框中,我将其值设置为其对应的state变量(name,,email)password,当它发生变化时(onChange),我调用方法(setName,,setEmail)setPassword并应用其新值(通过e.target.value)。
从图中onSubmit={handleSubmit}可以看出,提交(无论是通过按钮还是 Enter 键)都会调用函数handleSubmit,我们将在其中添加登录流程。event.preventDefault()阻止表单提交(到当前页面,这会导致页面重新渲染)。
handleSubmit接下来我应该做的preventDefault()是POST向服务器发出请求/api/users。之后,我还需要向用户显示错误或成功消息。
我使用我的程序axioswal,它发出 Axios 请求,处理响应,并根据响应显示sweetalert2对话框。
npm install axioswal
如果您使用的是其他请求库或想自行处理,请随意操作。
另外,请注意,如果用户注册成功,我会将其重定向到其他页面。我使用了一个定义在以下位置的函数lib/redirectTo.js:
import Router from 'next/router';
export default function redirectTo(destination, { res, status } = {}) {
if (res) {
res.writeHead(status || 302, { Location: destination });
res.end();
} else if (destination[0] === '/' && destination[1] !== '/') {
Router.push(destination);
} else {
window.location = destination;
}
}
这段代码取自 GitHub 上的一个代码片段,但我忘了它在哪了。如果你知道,请告诉我 :(
用户身份验证
现在我们已经有了一位用户。让我们尝试验证该用户的身份。实际上,我们在用户注册时就已经验证过其身份了:
req.session.userId = user.insertedId;
让我们看看如何在 中实现/login,我们向 发出POST请求/api/authenticate。
构建身份验证 API
让我们一起创造api/authenticate.js:
import * as argon2 from 'argon2';
import withMiddleware from '../../middlewares/withMiddleware';
const handler = (req, res) => {
if (req.method === 'POST') {
const { email, password } = req.body;
return req.db.collection('users').findOne({ email })
.then((user) => {
if (user) {
return argon2.verify(user.password, password)
.then((result) => {
if (result) return Promise.resolve(user);
return Promise.reject(Error('The password you entered is incorrect'));
});
}
return Promise.reject(Error('The email does not exist'));
})
.then((user) => {
req.session.userId = user._id;
return res.send({
status: 'ok',
message: `Welcome back, ${user.name}!`,
});
})
.catch(error => res.send({
status: 'error',
message: error.toString(),
}));
}
return res.status(405).end();
};
export default withMiddleware(handler);
逻辑很简单,我们首先调用 `req.db.collection.findOne` 来查找findOne给定的电子邮件email地址。如果电子邮件地址不存在,则拒绝并返回“该电子邮件地址不存在”。如果电子邮件地址存在,`req.db.collection.findOne` 将返回一个文档,我们称之为 `<email>` user。
然后我们尝试匹配密码。我调用一个函数argon2.verify(该函数会对接收到的密码进行哈希处理,并将其与哈希后的密码进行比较),以查看password从请求中获取的密码是否与数据库中的密码匹配。如果不匹配,argon2.verify()则返回 false。然后我们拒绝请求并提示“您输入的密码不正确”。
但实际上,我可能不希望出现两种不同的回复,分别表示邮箱地址或密码错误。这可能会让攻击者有机可乘。我们可以简单地将每个Error()对象的回复都改为“您的邮箱地址或密码错误”。
如果匹配,我们将设置userId为req.session类似Signup,并返回一条消息“欢迎回来”,其中包含用户名。
pages/login.jsx登录页面
以下是我们的代码pages/login.jsx:
import React, { useState } from 'react';
import axioswal from 'axioswal';
import Layout from '../components/layout';
import redirectTo from '../lib/redirectTo';
const LoginPage = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (event) => {
event.preventDefault();
axioswal
.post('/api/authenticate', {
email,
password,
})
.then((data) => {
if (data.status === 'ok') {
redirectTo('/');
}
});
};
return (
<Layout>
<div style={{ margin: '4rem' }}>
<h1>Log in</h1>
<form onSubmit={handleSubmit}>
<div>
<input
type="email"
placeholder="Email address"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</div>
<div>
<input
type="password"
placeholder="Create a password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
</div>
<button type="submit">
Log in
</button>
</form>
</div>
</Layout>
);
};
export default LoginPage;
仔细观察,你会发现我从我们的文件中复制了整个内容signup.jsx,删除了该name字段,并将POSTURL 更改为api/authenticate。
逻辑是一样的。如果用户登录成功,我们仍然会重定向用户。
启动服务器并访问以/login查看我们的结果。
成功了吗?如果成功了,那就太棒了!我们已经完成了项目的大部分工作。
使用会话来确定用户身份
回想一下,我们已经设置了一个 userId req.session。我们可以做的就是使用中间件在我们的 MongoDB 数据库中查找它。
req.user中间件
请继续创建middlewares/withAuthentication:
import { ObjectId } from 'mongodb';
const withAuthentication = handler => (req, res) => {
if (req.session.userId) {
console.log(req.session.userId);
return req.db.collection('users').findOne(ObjectId(req.session.userId))
.then((user) => {
console.log(user);
if (user) req.user = user;
return handler(req, res);
});
}
return handler(req, res);
};
export default withAuthentication;
将其纳入我们的withMiddleware.js:
const middleware = handler => withDatabase(withSession(withAuthentication(handler)));
我提到顺序很重要,因为我们需要req.session做好准备。
如果存在一个对象req.session.userId,我们会尝试查找其 ID(确保先将其转换为字符串ObjectId)。如果存在用户,我们会将其附加到该对象上req。为什么呢?因为我们可能会在其他端点中重用此user文档。由于每个端点都会经过withAuthentication该对象(可通过withMiddleware访问),我们只需引用该对象即可确定用户req.user。这也有助于我们避免重复代码。
会话端点,用于获取当前用户
我们来创建一个获取当前用户的端点。我会把它命名为 `user` /api/session,但你可以把它命名为任何名称,例如 `user`/api/user或 `user` /api/users/me。
在 中/api/session,输入以下内容:
import withMiddleware from '../../middlewares/withMiddleware';
const handler = (req, res) => {
if (req.method === 'GET') {
if (req.user) {
const { name, email } = req.user;
return res.status(200).send({
status: 'ok',
data: {
isLoggedIn: true,
user: { name, email },
},
});
}
return res.status(200).send({
status: 'ok',
data: {
isLoggedIn: false,
user: {},
},
});
}
return res.status(405).end();
};
export default withMiddleware(handler);
我认为,让该端点接受GET获取当前用户信息的请求是合适的。
如您所见,我首先通过检查是否存在来判断用户是否已登录req.user。
如果是这样,我会回复isLoggedIn: true用户姓名和电子邮件地址。
否则,我只需回复“isLoggedIn: false以及一个空的用户对象”。
用户上下文
我们需要从某个地方调用数据,并在 React 组件之间传递数据。我使用 React Context APIGET /api/session实现了这一点。
上下文提供了一种在组件树中传递数据的方法,而无需在每一层手动传递 props。
没错,正是我们需要的。
另一个类似的解决方案是——我相信你肯定听说过很多次了——Redux 。但是,我认为Redux有点过于复杂,越简单越好。
让我们一起创造components/UserContext.js:
import React, { createContext, useReducer, useEffect } from 'react';
import axios from 'axios';
const UserContext = createContext();
const reducer = (state, action) => {
switch (action.type) {
case 'set':
return action.data;
case 'clear':
return {
isLoggedIn: false,
user: {},
};
default:
throw new Error();
}
};
const UserContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, { isLoggedIn: false, user: {} });
const dispatchProxy = (action) => {
switch (action.type) {
case 'fetch':
return axios.get('/api/session')
.then(res => ({
isLoggedIn: res.data.data.isLoggedIn,
user: res.data.data.user,
}))
.then(({ isLoggedIn, user }) => {
dispatch({
type: 'set',
data: { isLoggedIn, user },
});
});
default:
return dispatch(action);
}
};
useEffect(() => {
dispatchProxy({ type: 'fetch' });
}, []);
return (
<UserContext.Provider value={{ state, dispatch: dispatchProxy }}>
{ children }
</UserContext.Provider>
);
};
const UserContextConsumer = UserContext.Consumer;
export { UserContext, UserContextProvider, UserContextConsumer };
这里内容相当丰富。我建议你先阅读 React 网站上关于Context 的介绍,然后再回到这里。
我们首先通过调用来创建一个上下文createContext()。
然后我使用 React Hook 创建一个Reducer(再次提醒,建议先阅读一些相关资料):
const [state, dispatch] = useReducer(reducer, { isLoggedIn: false, user: {} });
其中{ isLoggedIn: false, user: {} },默认值reducer如上所述:
const reducer = (state, action) => {
switch (action.type) {
case 'set':
return action.data;
case 'clear':
return {
isLoggedIn: false,
user: {},
};
default:
throw new Error();
}
};
返回的值将被设置为state。
例如,在该clear函数中,我只是返回了一个空的用户对象isLoggedIn: false。
事情变得有点奇怪set。
Reducer不支持异步函数,所以我需要用到一个小技巧。我有一个“代理”函数。所有 reducer 都会首先经过它dispatchProxy。
如果 reducer 需要异步调用,例如fetch dispatchProxy()执行异步操作,则会在操作完成后调用该方法dispatch()。具体来说,在获取数据完成后,它dispatchProxy()会调用该方法dispatch()并更新用户上下文。
如果 reducer 不包含异步调用,我只需将其转发到实际的 reducer dispatch()。
每次需要获取用户数据时(例如登录后、更改个人资料后等),我只需调用 `getUserData()` 方法dispatch(),该方法可以通过导入 `getUserData()` 模块来使用UserContext。另请注意useEffect(() => {}, []);:我希望在应用程序首次渲染时获取数据。
与访问类似,我们也可以通过导入的方式reducer来访问。导入的文件将包含我们需要的所有用户信息。stateUserContextstate
提供者
要在子组件上使用 Context,我们需要在高阶组件中提供一个ContextProvider 。
<Provider>
<Child1 />
<Child 2>
<Child 3 />
</Child 2>
</Provider>
根据上面的映射关系Child 1,可以访问上下文。上下文Child 2的格式如下:Child 3<Provider>
<MyContext.Provider value={/* some value */}>
如果你回顾一下UserContextProvider`in`的返回值UserContext.jsx,你会发现我们已经实现了这一点。因此,只需导入UserContextProvider那个高阶组件即可。
在 Next.js 中,` _app.jscomponentWill` 是我们能够访问的最高层组件。因此,我们将把我们的UserContextProvider组件导入到 `componentWill` 中。
创建pages/_app.jsx:
import React from 'react';
import App, { Container } from 'next/app';
import { UserContextProvider } from '../components/UserContext';
class MyApp extends App {
render() {
const { Component, pageProps } = this.props;
return (
<Container>
<UserContextProvider>
<Component {...pageProps} />
</UserContextProvider>
</Container>
);
}
}
export default MyApp;
访问stateUserContext
打开我们的表格pages/index.js并填写以下内容:
import React, { useContext } from 'react';
import Link from 'next/link';
import { UserContext } from '../components/UserContext';
import Layout from '../components/layout';
const IndexPage = () => {
const { state: { isLoggedIn, user: { name } } } = useContext(UserContext);
return (
<Layout>
<div>
<h1>
Hello,
{' '}
{(isLoggedIn ? name : 'stranger.')}
</h1>
{(!isLoggedIn ? (
<>
<Link href="/login"><div><button>Login</button></div></Link>
<Link href="/signup"><div><button>Sign up</button></div></Link>
</>
) : <button>Logout</button>)}
</div>
</Layout>
);
};
export default IndexPage;
如果const { state: { isLoggedIn, user: { name } } } = useContext(UserContext);我使用了一些 ES6 语法让您感到困惑,我深表歉意。我基本上是从.获取isLoggedIn和。user.namestateUserContext
我还编写了一个条件渲染。如果isLoggedIn === true,则渲染name。否则,渲染"stranger"。此外,在欢迎文本下方,如果isLoggedIn === false,则渲染“登录”和“注册”Logout两个链接。类似地,如果 ,则渲染按钮isLoggedIn === true。
派遣fetchUserContext
请记住,我们需要调度fetchreducer 才能使一切正常运行。
打开pages/login.jsx并pages/signup.jsx包含UserContext。这是的实现pages/login.jsx。尝试自己完成另一个。
axioswal
.post('/api/authenticate', {
email,
password,
})
.then((data) => {
if (data.status === 'ok') {
// Fetch the user data for UserContext here
dispatch({ type: 'fetch' });
redirectTo('/');
}
});
dispatch({ type: 'fetch' });将调用dispatchProxy(),该调用会发出GET请求并调用dispatch()以更新UserContext。
userId注销:从...删除session
让我们为“注销”按钮添加以下功能index.jsx:
import React, { useContext } from 'react';
import Link from 'next/link';
import axioswal from 'axioswal';
import { UserContext } from '../components/UserContext';
import Layout from '../components/layout';
const IndexPage = () => {
const { state: { isLoggedIn, user: { name } }, dispatch } = useContext(UserContext);
const handleLogout = (event) => {
event.preventDefault();
axioswal
.delete('/api/session')
.then((data) => {
if (data.status === 'ok') {
dispatch({ type: 'clear' });
}
});
};
return (
<Layout>
<div>
<h1>
Hello,
{' '}
{(isLoggedIn ? name : 'stranger.')}
</h1>
{(!isLoggedIn ? (
<>
<Link href="/login"><div><button>Login</button></div></Link>
<Link href="/signup"><div><button>Sign up</button></div></Link>
</>
) : <button onClick={handleLogout}>Logout</button>)}
</div>
</Layout>
);
};
export default IndexPage;
我dispatch从导入。此外,我为注销UserContext按钮分配了一个函数,该函数会向发出请求。如果请求成功,我们会调用reducer 来清空。DELETE/api/sessionclearUserContext
显然我们需要添加一个 DELETE 请求处理程序api/session.js:
import withMiddleware from '../../middlewares/withMiddleware';
const handler = (req, res) => {
if (req.method === 'GET') {
if (req.user) {
const { name, email } = req.user;
return res.status(200).send({
status: 'ok',
data: {
isLoggedIn: true,
user: { name, email },
},
});
}
return res.status(200).send({
status: 'ok',
data: {
isLoggedIn: false,
user: {},
},
});
}
if (req.method === 'DELETE') {
delete req.session.userId;
return res.status(200).send({
status: 'ok',
data: {
isLoggedIn: false,
message: 'You have been logged out.',
},
});
}
return res.status(405).end();
};
export default withMiddleware(handler);
就像我们userId登录时设置的会话一样。我只需要删除userId会话中的相应条目即可注销。
结论
好了,我们来运行一下应用并进行测试。这将是使用Next.js和MongoDB构建完整应用的第一步。
我希望这能成为你开发下一款优秀应用的模板。再次提醒,请查看这里的代码仓库。我接受功能请求。下次应该添加哪些功能?欢迎创建 issue 告诉我。
我不太擅长写作,所以如果文章有什么问题,请帮助我改进。
祝你下一个 Next.js + MongoDB 项目一切顺利!
文章来源:https://dev.to/hoangvvo/how-i-build-a-full-fledged-app-with-next-js-and-mongodb-part-1-user-authentication-3io9

