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

【已过时】如何使用 Next.js 和 MongoDB 构建一个功能齐全的应用程序 第一部分:用户身份验证

【已过时】如何使用 Next.js 和 MongoDB 构建一个功能齐全的应用程序 第一部分:用户身份验证

这并未反映最近的重写版本nextjs-mongodb-app。请查看最新版本

更新:为了兼容性,我已将 argon2 替换为 bcryptjs。

过去一个月,我一直在努力为我的下一个项目实现一个身份验证系统。这个项目隶属于一个我找到的组织,该组织致力于用代码解决社区问题。

该项目是一款旨在促进良好行为的在线游戏,因此需要为用户提供登录方式。

我使用的是React 框架Next.jsMongoDB作为数据库。

我之前用的是 Express 作为后端,搭建了一个基于 Next.js 的自定义服务器,并(偷懒地)使用 PassportJS 来处理身份验证。后来我意识到,这样我对 API 的控制力很弱。因此,我决定自己开发一套身份验证系统。

或许有人会反驳说我不应该重复发明轮子。但我渴望探索和挑战,所以我选择了这条路。

我在网上找到的大多数解决方案要么依赖第三方服务(例如 Google、Facebook、身份即服务),要么不够完善,没有充分考虑实际应用。因此,我决定自己开发一个。

下面提供了该项目的Github代码库和演示,供您参考。

Github 仓库

演示(已修复)

关于nextjs-mongodb-app项目

nextjs-mongodb-app 是一个使用 Next.js 和 MongoDB 构建的完整应用程序。网上大多数教程要么不够完善,要么无法直接用于生产环境。本项目旨在解决这些问题。

该项目更进一步,尝试整合现实应用中的顶级功能,使其成为一款功能齐全的应用。

更多信息请访问Github仓库

入门

典型的教程会告诉你该npm install next怎么做,但我相信你已经是一位经验丰富的开发者了。

我将首先着手搭建我的应用程序框架。

环境因素

本项目从数据库中检索变量process.env。您需要实现自己的策略。以下是一些可能的解决方案:

本项目所需的环境变量包括:

  • 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);
Enter fullscreen mode Exit fullscreen mode

构建完整的身份验证系统。

响应模式

量两次,切一次

我必须强调这一点。我希望我的 API 响应能够保持一致性。因此,我制定了以下方案——一个适用于所有 API 的标准响应:

{
  "status": "ok / error",
  "message": "a user-readable message",
  "data": "<payload object>"
}
Enter fullscreen mode Exit fullscreen mode

欢迎您提出自己的观点。您可以参考这篇Stack Overflow 讨论,从中汲取一些灵感。

如果我没有提前做好规划,可能会遇到一些问题。想象一下,我的某个端点可能会这样响应:

{
  "success": true,
  "msg": "successful stuff",
}
Enter fullscreen mode Exit fullscreen mode

……而另一人则回应道:

{
  "error": false,
  "text": "ok",  
}
Enter fullscreen mode Exit fullscreen mode

我需要编写两套不同的检查语句,并尝试使用两个不同的字段来显示消息:

if (response.success === true || response.error === false) {
  alert(response.msg || response.text);
}
Enter fullscreen mode Exit fullscreen mode

情况会很快恶化,尤其是当端点数量增加且缺乏文档记录时。

中间件

如果您有相关背景,可能对中间件这个术语比较熟悉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;
Enter fullscreen mode Exit fullscreen mode

我的做法是,只有在客户端未连接的情况下才会尝试连接。

然后我将数据库req.db和客户端连接到服务器req.mongoClient。客户端稍后可以在我们的会话中间件中重复使用。

不建议将 MongoDB URI 或任何其他安全变量硬编码到代码中。我是通过以下方式获取的process.env

会话中间件

会话是我们项目的核心要素之一。在 ExpressJS 中,我们可以使用 ` Session` 中间件。在 Next.js 中,可以使用`next-session`express-session中间件。您可以安装它,也可以使用/创建自己的中间件。

npm install next-session
Enter fullscreen mode Exit fullscreen mode

注意:目前,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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

需要注意的是,顺序很重要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 }
  </>
);
Enter fullscreen mode Exit fullscreen mode

用户注册

让我们从用户注册开始,因为我们至少需要一个用户才能进行操作。

构建注册 API

假设我们通过向服务器发送请求,输入用户POST姓名/api/users、电子邮件和密码来注册用户。

` <path>` 中的所有内容/api都是 API 路由,这是 Next.js 9 的新增功能。每个路由都必须导出一个接受两个参数的函数reqres以下是我们的路由内容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);
Enter fullscreen mode Exit fullscreen mode

该函数验证电子邮件地址,对密码进行哈希处理,并将用户插入数据库。之后,我们将会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;
Enter fullscreen mode Exit fullscreen mode

太棒了,让我们启动开发服务器,访问/signup并查看一下。尝试用一个有创意的虚构邮箱和一个很酷的密码注册。

使用 Next.js 和 MongoDB 构建的完整应用程序:注册 1

另外,请尝试:

  • 使用同一邮箱再次注册。
  • 输入了无效的电子邮件地址。

它显示错误信息了吗?如果显示了,那就太好了!我们来回顾一下你刚才的操作。

如果您不熟悉 Hook,我使用的是 React State Hook useState。在每个文本框中,我将其值设置为其对应的state变量(name,,emailpassword,当它发生变化时(onChange),我调用方法(setName,,setEmailsetPassword并应用其新值(通过e.target.value)。

从图中onSubmit={handleSubmit}可以看出,提交(无论是通过按钮还是 Enter 键)都会调用函数handleSubmit,我们将在其中添加登录流程。event.preventDefault()阻止表单提交(到当前页面,这会导致页面重新渲染)。

handleSubmit接下来我应该做的preventDefault()POST向服务器发出请求/api/users。之后,我还需要向用户显示错误成功消息。

我使用我的程序axioswal,它发出 Axios 请求,处理响应,并根据响应显示sweetalert2对话框。

npm install axioswal
Enter fullscreen mode Exit fullscreen mode

如果您使用的是其他请求库或想自行处理,请随意操作。

另外,请注意,如果用户注册成功,我会将其重定向到其他页面。我使用了一个定义在以下位置的函数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;
  }
}
Enter fullscreen mode Exit fullscreen mode

这段代码取自 GitHub 上的一个代码片段,但我忘了它在哪了。如果你知道,请告诉我 :(

用户身份验证

现在我们已经有了一位用户。让我们尝试验证该用户的身份。实际上,我们在用户注册时就已经验证过其身份了:

req.session.userId = user.insertedId;
Enter fullscreen mode Exit fullscreen mode

让我们看看如何在 中实现/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);
Enter fullscreen mode Exit fullscreen mode

逻辑很简单,我们首先调用 `req.db.collection.findOne` 来查找findOne给定的电子邮件email地址。如果电子邮件地址不存在,则拒绝并返回“该电子邮件地址不存在”。如果电子邮件地址存在,`req.db.collection.findOne` 将返回一个文档,我们称之为 `<email>` user

然后我们尝试匹配密码。我调用一个函数argon2.verify(该函数会对接收到的密码进行哈希处理,并将其与哈希后的密码进行比较),以查看password从请求中获取的密码是否与数据库中的密码匹配。如果不匹配,argon2.verify()则返回 false。然后我们拒绝请求并提示“您输入的密码不正确”。

但实际上,我可能不希望出现两种不同的回复,分别表示邮箱地址或密码错误。这可能会让攻击者有机可乘。我们可以简单地将每个Error()对象的回复都改为“您的邮箱地址或密码错误”。

如果匹配,我们将设置userIdreq.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;
Enter fullscreen mode Exit fullscreen mode

仔细观察,你会发现我从我们的文件中复制了整个内容signup.jsx,删除了该name字段,并将POSTURL 更改为api/authenticate

逻辑是一样的。如果用户登录成功,我们仍然会重定向用户。

启动服务器并访问以/login查看我们的结果。

成功了吗?如果成功了,那就太棒了!我们已经完成了项目的大部分工作。

使用会话来确定用户身份

回想一下,我们已经设置了一个 userId req.session。我们可以做的就是使用中间件在我们的 MongoDB 数据库中查找它。

37fk3o

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;
Enter fullscreen mode Exit fullscreen mode

将其纳入我们的withMiddleware.js

const middleware = handler => withDatabase(withSession(withAuthentication(handler)));
Enter fullscreen mode Exit fullscreen mode

我提到顺序很重要,因为我们需要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);
Enter fullscreen mode Exit fullscreen mode

我认为,让该端点接受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 };
Enter fullscreen mode Exit fullscreen mode

这里内容相当丰富。我建议你先阅读 React 网站上关于Context 的介绍,然后再回到这里。

我们首先通过调用来创建一个上下文createContext()

然后我使用 React Hook 创建一个Reducer(再次提醒,建议先阅读一些相关资料):

const [state, dispatch] = useReducer(reducer, { isLoggedIn: false, user: {} });
Enter fullscreen mode Exit fullscreen mode

其中{ 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();
  }
};
Enter fullscreen mode Exit fullscreen mode

返回的值将被设置为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>
Enter fullscreen mode Exit fullscreen mode

根据上面的映射关系Child 1可以访问上下文。上下文Child 2格式如下:Child 3<Provider>

<MyContext.Provider value={/* some value */}>
Enter fullscreen mode Exit fullscreen mode

如果你回顾一下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;
Enter fullscreen mode Exit fullscreen mode
访问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;
Enter fullscreen mode Exit fullscreen mode

如果const { state: { isLoggedIn, user: { name } } } = useContext(UserContext);我使用了一些 ES6 语法让您感到困惑,我深表歉意。我基本上是.获取isLoggedInuser.namestateUserContext

我还编写了一个条件渲染。如果isLoggedIn === true,则渲染name。否则,渲染"stranger"。此外,在欢迎文本下方,如果isLoggedIn === false,则渲染“登录”“注册”Logout两个链接。类似地,如果 ,则渲染按钮isLoggedIn === true

派遣fetchUserContext

请记住,我们需要调度fetchreducer 才能使一切正常运行。

打开pages/login.jsxpages/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('/');
        }
      });
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

就像我们userId登录时设置的会话一样。我只需要删除userId会话中的相应条目即可注销。

结论

好了,我们来运行一下应用并进行测试。这将是使用Next.jsMongoDB构建完整应用的第一步

我希望这能成为你开发下一款优秀应用的模板。再次提醒,请查看这里的代码仓库。我接受功能请求。下次应该添加哪些功能?欢迎创建 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