使用Next.js和Auth0与Supabase

589 阅读20分钟

概述

在这篇文章中,我们将探索使用Next.jsAuth0Supabase来构建一个经典的Todo应用。每个用户只能看到自己的todos,所以我们需要实现认证、授权和数据库。

这篇文章将涵盖。

  • 配置Auth0、Next.js和Supabase,使其无缝地协同工作
  • 使用 nextjs-auth0库进行认证
  • 实现行级安全(RLS)和授权策略
  • 什么是JWT以及如何签署我们自己的JWT
  • 使用PostgreSQL函数从JWT中提取数值

Todo应用程序的最终版本的代码可以在这里找到。

前提条件

本文并不假设你对这些技术有任何经验。然而,你将需要安装Node.js来跟读。此外,计划拥有以下部分的管理服务(Auth0和Supabase)的账户。截至目前,这两项服务都是免费的,不需要信用卡。

堆栈

Next.js是一个React框架,使构建高效的网络应用超级容易。它还为我们提供了编写服务器端逻辑的能力--我们将需要确保我们的应用程序是安全的--而不需要维护我们自己的服务器。

Auth0是一个认证和授权解决方案,使管理用户和保护应用程序变得轻而易举。它是一个非常经过战斗考验的成熟的认证解决方案。

Supabase是一个开源的后台即服务,它使我们有可能在周末建立一个应用程序并扩展到数百万。它是一个方便的包装,围绕着一系列开源工具,实现数据库存储、文件存储、认证、授权和实时订阅。虽然这些都是伟大的功能,但本文将只使用数据库存储和授权。

"等等,如果Supabase处理授权,为什么我们要使用Auth0?"

Supabase的真正优势之一是没有厂商锁定。也许你已经有用户在Auth0中,你的公司对它有很多经验,或者你正在与使用它的其他应用程序进行交互。Supabase的任何组件都可以被换成类似的服务,并在任何地方托管。

因此,让我们就这样做吧!

Auth0

我们需要做的第一件事是在Auth0注册一个免费账户。一旦进入仪表板,我们需要为我们的项目创建一个新的Tenant

租户是一种将我们的用户和设置与我们在Auth0的其他应用程序隔离的方式。

在左上方点击你的账户名称,然后从下拉菜单中选择Create tenant

Create tenant from Auth0 dashboard

给你的租户一个独特的Domain ,设置离你最近的Region ,并将Environment Tag 设置为Development

Auth0 tenant settings

在一个生产应用中,你希望你的区域尽可能地靠近你的大多数用户。

接下来,我们要创建一个应用程序。从侧边栏菜单中选择Applications >Applications ,然后点击 + Create Application.我们要给它一个名字(可以与租户相同),并选择Regular Web Applications 。点击Create

Auth0 application settings

从应用程序的页面,你会被重定向到选择Settings 标签,并向下滚动到Application URIs 部分。

添加以下内容。

  • Allowed Callback URLs: http://localhost:3000/api/auth/callback
  • Allowed Logout URLs: http://localhost:3000

转到Advanced Settings >OAuth 并确认 JSON Web Token (JWT) Signature Algorithm被设置为 RS256并确认 OIDC Conformantenabled 。请务必保存您的更改。

真棒。我们现在有一个Auth0实例被配置来处理我们应用程序的认证。所以,让我们建立一个应用程序吧!

虽然我们可以为这个例子使用任何网络应用框架,但我将使用Next.js。它为我们提供了一个超级高效的React应用程序,并包括基于文件的路由,开箱即用。此外,它允许我们在用getStaticProps 和(当用户请求一个页面时)用getServerSideProps 函数构建我们的应用程序时,运行服务器端的逻辑。我们将需要在服务器端进行认证等工作,但我们不希望为另一个服务器进行设置、维护和付费,这很麻烦。

Next.js

创建Next.js应用程序的最快方法是使用 create-next-app包。

npx create-next-app supabase-auth0

的内容替换为 pages/index.js替换为。

// pages/index.js

import styles from "../styles/Home.module.css";

const Index = () => {
  return <div className={styles.container}>Working!</div>;
};

export default Index;

在开发模式下运行该项目。

npm run dev

并确认它在 http://localhost:3000.

认证

让我们来整合 nextjs-auth0包。这是一个围绕Auth0 JS SDK的便捷包装,但专门为Next.js构建。

npm i @auth0/nextjs-auth0

创建一个新的文件夹在 pages/api/auth/并添加一个名为 [...auth0].js的文件,内容如下。

// pages/api/auth/[...auth0].js

import { handleAuth } from "@auth0/nextjs-auth0";

export default handleAuth();

[...auth0].js是一个全面的路由。这意味着,任何以 /api/auth0开头的网址都会加载这个组件 - /api/auth0, /api/auth0/login, /api/auth0/some/deeply/nested/url等等。

这是其中一个很棒的东西 nextjs-auth0免费提供给我们的呼叫 handleAuth()会自动创建一个方便的路线集合--比如说 /login/logout- 以及所有处理令牌和会话的必要逻辑。除了调用这个方法,没有任何额外的步骤需要。

pages/_app.js的内容。

// pages/_app.js

import React from "react";
import { UserProvider } from "@auth0/nextjs-auth0";

const App = ({ Component, pageProps }) => {
  return (
    <UserProvider>
      <Component {...pageProps} />
    </UserProvider>
  );
};

export default App;

在你的项目根目录下创建一个 .env.local文件,并添加。

AUTH0_SECRET=generate-this-below
AUTH0_BASE_URL=http://localhost:3000
AUTH0_ISSUER_BASE_URL=https://<name-of-your-tenant>.<region-you-selected>.auth0.com
AUTH0_CLIENT_ID=get-from-auth0-dashboard
AUTH0_CLIENT_SECRET=get-from-auth0-dashboard

请参阅Next.js的环境变量文档以了解更多。

生成一个安全的 AUTH0_SECRET通过运行:

node -e "console.log(crypto.randomBytes(32).toString('hex'))"

AUTH0_CLIENT_IDAUTH0_CLIENT_SECRET可以在 Applications > Settings > Basic Information在Auth0仪表板中。

你需要退出Next.js服务器,并在新的环境变量被添加到以下文件中时重新运行npm run dev 命令 .env.local文件中添加新的环境变量

让我们更新我们的 pages/index.js文件,增加签入和签出的功能。

// pages/index.js

import styles from "../styles/Home.module.css";
import { useUser } from "@auth0/nextjs-auth0";
import Link from "next/link";

const Index = () => {
  const { user } = useUser();

  return (
    <div className={styles.container}>
      {user ? (
        <p>
          Welcome {user.name}!{" "}
          <Link href="/api/auth/logout">
            <a>Logout</a>
          </Link>
        </p>
      ) : (
        <Link href="/api/auth/login">
          <a>Login</a>
        </Link>
      )}
    </div>
  );
};

export default Index;

我们正在使用 useUser()钩子来获取user 对象,如果他们已经登录。如果没有,我们将渲染一个链接到登录页面。

Next.js的Link 组件被用来启用客户端路由,而不是需要从服务器上重新加载整个页面。

我们还可以添加处理loadingerror 状态的能力。

// pages/index.js

import styles from "../styles/Home.module.css";
import { useUser } from "@auth0/nextjs-auth0";
import Link from "next/link";

const Index = () => {
  const { user, error, isLoading } = useUser();

  if (isLoading) return <div className={styles.container}>Loading...</div>;
  if (error) return <div className={styles.container}>{error.message}</div>;

  // rest of component
};

export default Index;

我们需要我们的用户登录后才能看到他们的todos ,或添加一个新的todo 。为了达到这个目的,我们将保护这个路由--要求用户已经登录。如果他们没有登录,我们也会自动将他们重定向到 /login路由,如果他们没有的话。

庆幸的是,该 nextjs-auth0库中的withPageAuthRequired 函数使之变得非常简单。我们可以告诉Next.js在渲染我们的页面之前在服务器上调用这个函数,方法是将其设置为getServerSideProps 函数。

将以下内容添加到 pages/index.js文件中。

// other imports
import { withPageAuthRequired } from "@auth0/nextjs-auth0";

// rest of component

export const getServerSideProps = withPageAuthRequired();

// other export

这个函数检查我们是否有用户登录,如果没有,则处理重定向他们到登录页面。如果我们有一个用户,它会自动将user 对象作为一个道具传递给我们的Index 组件。由于这是在我们的组件被渲染之前在服务器上发生的,我们不再需要处理加载、错误状态或用户是否已登录。这意味着我们可以大大简化我们的渲染逻辑。

这就是我们的整个文件应该是的样子。

// pages/index.js

import styles from "../styles/Home.module.css";
import { withPageAuthRequired } from "@auth0/nextjs-auth0";
import Link from "next/link";

const Index = ({ user }) => {
  return (
    <div className={styles.container}>
      <p>
        Welcome {user.name}!{" "}
        <Link href="/api/auth/logout">
          <a>Logout</a>
        </Link>
      </p>
    </div>
  );
};

export const getServerSideProps = withPageAuthRequired();

export default Index;

非常干净

我们想在登陆页面上显示一个todos列表,但首先,我们需要一个地方来存储它们。

超级数据库

前往app.supabase.io,点击Sign In ,通过GitHub进行认证。这将创建一个免费的Supabase账户。在仪表板上,点击New project ,选择你的Organization

输入名字和密码,并选择一个与你选择的Auth0区域在地理上接近的区域。

New Supabase project settings

请确保你选择一个安全的密码,因为这也将适用于你的PostgreSQL数据库。

Supabase需要几分钟的时间在后台配置所有的位,但这个页面方便地显示了我们需要的所有值,以使我们的Next.js应用程序得到配置。

Supabase app URL and secrets

将这些值添加到 .env.local文件。

NEXT_PUBLIC_SUPABASE_URL=your-url
NEXT_PUBLIC_SUPABASE_KEY=your-anon-public-key
SUPABASE_SIGNING_SECRET=your-jwt-secret

在环境变量前加上 NEXT_PUBLIC_使其在Next.js客户端中可用。所有其他的值都只能在getStaticPropsgetServerSideProps 和目录中的serverless函数中使用。 pages/api/目录中。

将新的值添加到 .env.local文件中添加新值需要重启Next.js开发服务器。

希望这能让你停顿足够长的时间,配置完成后,你的Supabase项目就可以开始了。

点击侧边栏菜单中的Table editor 图标,选择 + Create a new table.

Create new table button

创建一个todo 表,并为content,user_idis_complete 添加列。

New table settings

  • content 将是我们的todo所显示的文本。
  • user_id 将是拥有该todo的用户。
  • is_complete 将标志着该todo是否已经完成。我们将默认值设置为 false,这是我们对一个新todo的假设。

现在让Row Level Security 暂时禁用。我们以后会担心这个问题。

点击Insert row ,创建一些例子todos

New row settings

我们可以让user_id ,默认值为 false``is_complete

让我们回到我们的Next.js应用程序并安装 supabase-js库。

npm i @supabase/supabase-js

创建一个名为utils 的新文件夹,并添加一个名为 supabase.js:

// utils/supabase.js

import { createClient } from "@supabase/supabase-js";

const getSupabase = () => {
  const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL,
    process.env.NEXT_PUBLIC_SUPABASE_KEY
  );

  return supabase;
};

export { getSupabase };

我们把它变成一个函数,因为我们以后还需要扩展它。

这个函数使用我们之前声明的环境变量来创建一个新的Supabase客户端。让我们使用我们的新客户端来获取todos ,在 pages/index.js.

我们可以向withPageAuthRequired 函数传递一个配置对象,并声明我们自己的getServerSideProps 函数,该函数只有在用户登录后才会运行。

// pages/index.js

export const getServerSideProps = withPageAuthRequired({
  async getServerSideProps() {
    const supabase = getSupabase();

    const { data: todos } = await supabase.from("todo").select("*");

    return {
      props: { todos },
    };
  },
});

我们需要记住导入getSupabase 这个函数。

import { getSupabase } from "../utils/supabase";

而现在我们可以遍历我们的todos ,并在我们的组件中显示它们。

const Index = ({ user, todos }) => {
  return (
    <div className={styles.container}>
      <p>
        Welcome {user.name}!{" "}
        <Link href="/api/auth/logout">
          <a>Logout</a>
        </Link>
      </p>
      {todos.map((todo) => (
        <p key={todo.id}>{todo.content}</p>
      ))}
    </div>
  );
};

我们还可以处理没有todos 的情况。

const Index = ({ user, todos }) => {
  return (
    <div className={styles.container}>
      <p>
        Welcome {user.name}!{" "}
        <Link href="/api/auth/logout">
          <a>Logout</a>
        </Link>
      </p>
      {todos?.length > 0 ? (
        todos.map((todo) => <p key={todo.id}>{todo.content}</p>)
      ) : (
        <p>You have completed all todos!</p>
      )}
    </div>
  );
};

todos?.length语句使用的是Optional Chaining。这是在todos 道具是的情况下的一个退路。 undefinednull.

我们的整个组件看起来应该是这样的。

// pages/index.js

import styles from "../styles/Home.module.css";
import { withPageAuthRequired } from "@auth0/nextjs-auth0";
import { getSupabase } from "../utils/supabase";
import Link from "next/link";

const Index = ({ user, todos }) => {
  return (
    <div className={styles.container}>
      <p>
        Welcome {user.name}!{" "}
        <Link href="/api/auth/logout">
          <a>Logout</a>
        </Link>
      </p>
      {todos?.length > 0 ? (
        todos.map((todo) => <p key={todo.id}>{todo.content}</p>)
      ) : (
        <p>You have completed all todos!</p>
      )}
    </div>
  );
};

export const getServerSideProps = withPageAuthRequired({
  async getServerSideProps() {
    const supabase = getSupabase();

    const { data: todos } = await supabase.from("todo").select("*");

    return {
      props: { todos },
    };
  },
});

export default Index;

太棒了!现在我们应该在Next.js应用程序中看到我们的todos。

List of todo items

但是等等,我们看到了所有的todos。

我们只想让用户看到他们的todos。为此,我们需要实现授权。让我们在Postgres中创建一个辅助函数,从请求的JWT中提取当前登录的用户。

Postgres函数

回到Supabase的仪表板,点击 SQL并选择New query 。这将创建一个题为 new sql snippet.添加以下SQL blob并点击 RUN.

create or replace function auth.user_id() returns text as $$
  select nullif(current_setting('request.jwt.claim.userId', true), '')::text;
$$ language sql stable;

好吧,这个函数可能看起来有点吓人。让我们分解一下我们需要理解的部分。

  1. 我们正在创建一个名为user_id 的新函数。
  2. 的部分只是一种命名空间的方式,因为它与auth有关--也称为模式,是POST中的惯例。 auth.部分只是一种命名空间的方式,因为它与auth有关--也被称为模式,是Postgres中的一种惯例。
  3. 这个函数将返回一个text 值。
  4. 该函数的主体是从jwtuserId 字段中获取数值,该字段是与request 一起出现的。 我们将在文章后面讨论JWTs。
  5. 如果没有 request.jwt.claim.userId,我们只是返回一个空字符串 - ''.

没那么吓人!

请看这个关于Postgres函数的视频来了解更多。

让我们启用Row Level Security ,并使用我们的新函数来确保只有拥有todo 的用户可以看到它。

行级安全

因为Supabase只是一个PostgreSQL数据库,我们可以利用一个重要的功能--行级安全(RLS)。RLS允许我们在数据库中编写授权规则,这可以更有效,也更安全!

回到Supabase仪表板,在侧边面板中选择Authentication >Policies ,然后点击 Enable RLStodo 表。

Supabase dashboard showing RLS enabled for todo table

现在,如果我们刷新我们的应用程序,我们会看到空的状态信息。

Empty todo list

我们的todos 去了哪里?

默认情况下,RLS会拒绝对所有行的访问。如果我们想让一个用户看到他们的todos ,我们需要写一个策略。

回到Supabase仪表板,单击New Policy ,然后单击. Create a policy from scratch并填入以下内容。

Policy settings for SELECT

这可能看起来有点不熟悉,所以让我们把它分解一下。

  1. 我们要给我们的策略一个name 。这可以是任何东西。
  2. 我们声明我们想启用哪些操作。由于我们希望用户能够阅读自己的todos ,我们选择了 SELECT.
  3. 我们需要指定一个条件,可以是 truefalse.如果它被评估为 true,该动作将被允许。否则,它将继续被拒绝。
  4. 我们调用我们的 auth.user_id()函数来获取当前登录用户的id ,并将其与这个user_id 列的todo

我建议查看这个视频,以了解更多关于Supabase中的行级安全是多么的棒和强大。

点击Review ,看看正在为我们生成的SQL。

Generated SQL for SELECT policy

甚至不吓人。它只是把我们输入的字段格式化为正确的SQL语法。

当我们在这里时,让我们添加一个用于创建新todos 的策略。这将是一个 INSERT行动。

Policy settings for INSERT

虽然我们可能想允许用户执行所有的CRUD动作,但好的做法是指定单独的策略,而不是选择 ALL.这使得它们在未来更容易扩展和删除。

我们的应用程序还不需要能够更新或删除todos ;因此,我们将不为这些操作创建策略。

良好的安全实践是,为应用程序的运行启用最小的权限。如果我们将来想启用这些操作,我们可以很容易地为它们编写策略。

让我们通过刷新Next.js应用程序,看看我们是否可以查看我们的todos了。

Empty todo list

还是没有todos 🙁 我们是不是弄错了什么?

没有!我们只需要再加一点胶水,把Auth0给我们Next.js应用程序的JWT转换成Supabase所期望的格式。

什么是JWT?

要理解这个问题,我们必须首先理解什么是JWT。JWT将一个JSON对象编码成一个大字符串,我们可以用它来在不同的服务之间发送数据。

默认情况下,JWT中的数据并没有被加密或保密,只是被编码。

Aditya Shukla写了一篇关于编码、加密和散列之间区别的好文章。请阅读它以了解更多。

Screenshot from JWT.io showing the encoded and decoded values of a JWT

这是一个取自jwt.io的例子--一个处理JWTs的伟大工具。在左边,我们可以看到JWT值。在右边,我们可以看到每个部分在解码时代表什么。我们有一些关于JWT如何被编码的头信息,用户数据的有效载荷,以及可用于verify 该令牌的签名。

不要把秘密的东西放在JWT中!!

我们之所以能够信任JWT的认证,是因为我们可以用一个秘密值来sign 。这个值与有效载荷数据一起通过一个算法,另一边就会出现一个JWT字符串。我们可以使用签名秘密在我们的服务器上verify JWT。这可以确保攻击者在传输过程中没有对我们的令牌进行修改。如果他们这样做了,JWT的值将是不同的,并且它将无法验证。

它被修改并通过verify 步骤的唯一方法是如果有人拥有你的签名秘密。这将是糟糕的。这就是为什么我们只能用我们的服务器来签署JWT--或者在Next.js的情况下,用getStaticPropsgetServerSideProps ,或者API路由在 pages/api目录中的API路由。

永远不要把签名的秘密暴露给客户端

好了,现在我们理解了JWT,那么问题出在哪里呢?

Auth0使用的签名秘密与Supabase的签名秘密不一致。虽然我们没有使用Supabase进行认证,但它仍然使用该秘密来验证我们每次请求数据时的JWT。这两个服务都没有使签名秘密的值可配置。

为了解决这个问题,我们可以从Auth0中抓取sub 属性--用户的ID,然后使用Supabase所期望的签名秘密sign 一个新的token。

签署JWT

在处理JWT时,我们希望使用一个有信誉的来源的、受信任的库。Auth0有一个非常广泛使用和信任的库,叫做jsonwebtoken

让我们来安装它。

npm i jsonwebtoken

我们可以使用另一个钩子,即 @auth0/nextjs-auth0库给我们的另一个钩子,在用户登录后运行一些逻辑。这被称为afterCallback ,可以作为配置传给我们的handleAuth 函数。让我们把这个文件的内容替换为 pages/api/auth/[...auth0].js文件的内容。

// pages/api/auth/[...auth0].js

import { handleAuth, handleCallback } from "@auth0/nextjs-auth0";

const afterCallback = async (req, res, session) => {
  // do some stuff
  // modify the session

  return session;
};

export default handleAuth({
  async callback(req, res) {
    try {
      await handleCallback(req, res, { afterCallback });
    } catch (error) {
      res.status(error.status || 500).end(error.message);
    }
  },
});

在这个函数中,我们要。

  1. 从Auth0的会话中获取用户对象
  2. 创建一个新的有效载荷
  3. 使用Supabase的签署秘密签署一个新的token

让我们扩展我们的afterCallback 函数来执行这个逻辑。

// pages/api/auth/[...auth0].js

// other imports
import jwt from "jsonwebtoken";

const afterCallback = async (req, res, session) => {
  const payload = {
    userId: session.user.sub,
    exp: Math.floor(Date.now() / 1000) + 60 * 60,
  };

  session.user.accessToken = jwt.sign(
    payload,
    process.env.SUPABASE_SIGNING_SECRET
  );

  return session;
};

sub 字段代表Auth0对这个用户的唯一ID。由于这是我们实际需要告诉Supabase谁是我们的用户的唯一值,我们可以创建一个只包含这个值的新payload。

只给事物处理任务所需的最小数量的数据和权限,是很好的安全实践。

此外,我们正在为我们的令牌设置一个过期时间--1小时。这意味着,如果有人掌握了我们的令牌,他们就不能无限期地访问我们的数据库。

最后,我们要用该有效载荷签署一个新的令牌,使用 SUPABASE_SIGNING_SECRET.

真棒!我们的整个文件应该是这样的我们的整个文件看起来应该是这样的。

// pages/api/auth/[...auth0].js

import { handleAuth, handleCallback } from "@auth0/nextjs-auth0";
import jwt from "jsonwebtoken";

const afterCallback = async (req, res, session) => {
  const payload = {
    userId: session.user.sub,
    exp: Math.floor(Date.now() / 1000) + 60 * 60,
  };

  session.user.accessToken = jwt.sign(
    payload,
    process.env.SUPABASE_SIGNING_SECRET
  );

  return session;
};

export default handleAuth({
  async callback(req, res) {
    try {
      await handleCallback(req, res, { afterCallback });
    } catch (error) {
      res.status(error.status || 500).end(error.message);
    }
  },
});

让我们在getSupabase 中扩展我们的函数 utils/supabase.js来接受一个可选的access_token 参数。

// utils/supabase.js

import { createClient } from "@supabase/supabase-js";

const getSupabase = (access_token) => {
  const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL,
    process.env.NEXT_PUBLIC_SUPABASE_KEY
  );

  if (access_token) {
    supabase.auth.session = () => ({
      access_token,
    });
  }

  return supabase;
};

export { getSupabase };

如果我们给这个函数传递一个access_token ,它将把它附加到Supabase会话上,当我们从Supabase请求数据时,Supabase会话将随行。

让我们在getServerSideProps 中扩展我们的函数 pages/index.js以从会话的用户那里获得accessToken ,并将其传递给getSupabase 函数。

// pages/index.js

export const getServerSideProps = withPageAuthRequired({
  async getServerSideProps({ req, res }) {
    const {
      user: { accessToken },
    } = await getSession(req, res);

    const supabase = getSupabase(accessToken);

    const { data: todos } = await supabase.from("todo").select("*");

    return {
      props: { todos },
    };
  },
});

我们还需要记住将getSession 加入到import from @auth0/nextjs-auth0:

import { withPageAuthRequired, getSession } from "@auth0/nextjs-auth0";

我们的整个组件看起来应该是这样的。

// pages/index.js

import styles from "../styles/Home.module.css";
import { withPageAuthRequired, getSession } from "@auth0/nextjs-auth0";
import { getSupabase } from "../utils/supabase";
import Link from "next/link";

const Index = ({ user, todos }) => {
  return (
    <div className={styles.container}>
      <p>
        Welcome {user.name}!{" "}
        <Link href="/api/auth/logout">
          <a>Logout</a>
        </Link>
      </p>
      {todos?.length > 0 ? (
        todos.map((todo) => <p key={todo.id}>{todo.content}</p>)
      ) : (
        <p>You have completed all todos!</p>
      )}
    </div>
  );
};

export const getServerSideProps = withPageAuthRequired({
  async getServerSideProps({ req, res }) {
    const {
      user: { accessToken },
    } = await getSession(req, res);

    const supabase = getSupabase(accessToken);

    const { data: todos } = await supabase.from("todo").select("*");

    return {
      props: { todos },
    };
  },
});

export default Index;

由于Auth0的afterCallback 函数在用户登录后运行,我们需要通过点击登陆页面上的Logout 链接或手动导航至 http://localhost:3000/api/auth/logout.

这将签出我们,并自动将我们重定向到Auth0的签入页面。

重新登录后,我们应该把我们的新JWT连接到我们Auth0会话的用户。

现在,当我们刷新我们的应用程序时,我们应该最终看到todos。

Empty todo list

没有!没有

但我们已经非常接近了。如果我们看一下Supabase仪表板上的Table editor 。谁是拥有todos的用户?

User ID null in Supabase Table Editor

NULL!!!。

所以我们只需要找出我们当前的user_id ,并将其添加到这些行中。

回到Auth0仪表板,点击侧边栏的User Management >Users ,然后选择你的用户。

List of users in Auth0 dashboard

user_id 会显示在你的用户详情页面的顶部。

User ID in Auth0 dashboard

让我们复制这个值并把它作为todosuser_id

User ID set to Auth0 user

刷新我们的Next.js应用程序。

Voilà!好了!!!

List of todo items

我们需要实现的最后一件事是添加一个todo 的功能。

让我们把表单逻辑添加到我们的 pages/index.js组件。

// pages/index.js

// other imports
import { useState } from "react";

const Index = ({ user, todos }) => {
  const [content, setContent] = useState("");
  const [allTodos, setAllTodos] = useState([...todos]);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const supabase = getSupabase(user.accessToken);
    const resp = await supabase
      .from("todo")
      .insert({ content, user_id: user.sub });

    setAllTodos([...todos, resp.data[0]]);
    setContent("");
  };

  return (
    <div className={styles.container}>
      <p>
        Welcome {user.name}!{" "}
        <Link href="/api/auth/logout">
          <a>Logout</a>
        </Link>
      </p>
      <form onSubmit={handleSubmit}>
        <input onChange={(e) => setContent(e.target.value)} value={content} />
        <button>Add</button>
      </form>
      {allTodos?.length > 0 ? (
        allTodos.map((todo) => <p key={todo.id}>{todo.content}</p>)
      ) : (
        <p>You have completed all todos!</p>
      )}
    </div>
  );
};

// exports

我们的最终组件应该是这样的。

// pages/index.js

import styles from "../styles/Home.module.css";
import { withPageAuthRequired, getSession } from "@auth0/nextjs-auth0";
import { getSupabase } from "../utils/supabase";
import Link from "next/link";
import { useState } from "react";

const Index = ({ user, todos }) => {
  const [content, setContent] = useState("");
  const [allTodos, setAllTodos] = useState([...todos]);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const supabase = getSupabase(user.accessToken);
    const resp = await supabase
      .from("todo")
      .insert({ content, user_id: user.sub });

    setAllTodos([...todos, resp.data[0]]);
    setContent("");
  };

  return (
    <div className={styles.container}>
      <p>
        Welcome {user.name}!{" "}
        <Link href="/api/auth/logout">
          <a>Logout</a>
        </Link>
      </p>
      <form onSubmit={handleSubmit}>
        <input onChange={(e) => setContent(e.target.value)} value={content} />
        <button>Add</button>
      </form>
      {allTodos?.length > 0 ? (
        allTodos.map((todo) => <p key={todo.id}>{todo.content}</p>)
      ) : (
        <p>You have completed all todos!</p>
      )}
    </div>
  );
};

export const getServerSideProps = withPageAuthRequired({
  async getServerSideProps({ req, res }) {
    const {
      user: { accessToken },
    } = await getSession(req, res);

    const supabase = getSupabase(accessToken);

    const { data: todos } = await supabase.from("todo").select("*");

    return {
      props: { todos },
    };
  },
});

export default Index;

我们现在有一个输入字段在我们的todos 。当我们为我们的todo输入content ,并点击add ,一个新的todo 将被插入到我们的DB。我们还将user_id 列设置为我们的值。 user.sub所以我们知道todo 是属于谁的。

我们将引入allTodos ,这样我们就可以在插入一个新的todo 后调用setAllTodos 。它将触发部分重新渲染,显示新插入的todo ,而不需要全页面刷新。

棒极了!我们现在有了一个Next.js应用程序,使用Auth0进行所有的认证,并使用Supabase进行授权的行级策略。我们了解了JWTs和如何签署我们自己的JWTs,以及编写一个Postgres函数来查询数据库中JWT的值。

如果你喜欢这篇文章,请在Twitter上关注我订阅我的YouTube频道查看我的博客

关于Supabase的一切,请关注我们的Twitter订阅我们的YouTube频道并查看我们的博客

谢谢你的阅读!