概述
在这篇文章中,我们将探索使用Next.js、Auth0和Supabase来构建一个经典的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
。
给你的租户一个独特的Domain
,设置离你最近的Region
,并将Environment Tag
设置为Development
。
在一个生产应用中,你希望你的区域尽可能地靠近你的大多数用户。
接下来,我们要创建一个应用程序。从侧边栏菜单中选择Applications
>Applications
,然后点击 + Create Application
.我们要给它一个名字(可以与租户相同),并选择Regular Web Applications
。点击Create
。
从应用程序的页面,你会被重定向到选择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 Conformant
是enabled
。请务必保存您的更改。
真棒。我们现在有一个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_ID
和AUTH0_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
组件被用来启用客户端路由,而不是需要从服务器上重新加载整个页面。
我们还可以添加处理loading
和error
状态的能力。
// 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区域在地理上接近的区域。
请确保你选择一个安全的密码,因为这也将适用于你的PostgreSQL数据库。
Supabase需要几分钟的时间在后台配置所有的位,但这个页面方便地显示了我们需要的所有值,以使我们的Next.js应用程序得到配置。
将这些值添加到 .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客户端中可用。所有其他的值都只能在getStaticProps
、getServerSideProps
和目录中的serverless函数中使用。pages/api/
目录中。将新的值添加到
.env.local
文件中添加新值需要重启Next.js开发服务器。
希望这能让你停顿足够长的时间,配置完成后,你的Supabase项目就可以开始了。
点击侧边栏菜单中的Table editor
图标,选择 + Create a new table
.
创建一个todo
表,并为content
,user_id
和is_complete
添加列。
content
将是我们的todo所显示的文本。user_id
将是拥有该todo的用户。is_complete
将标志着该todo是否已经完成。我们将默认值设置为false
,这是我们对一个新todo的假设。
现在让
Row Level Security
暂时禁用。我们以后会担心这个问题。
点击Insert row
,创建一些例子todos
。
我们可以让
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
道具是的情况下的一个退路。undefined
或null
.
我们的整个组件看起来应该是这样的。
// 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。
但是等等,我们看到了所有的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;
好吧,这个函数可能看起来有点吓人。让我们分解一下我们需要理解的部分。
- 我们正在创建一个名为
user_id
的新函数。 - 的部分只是一种命名空间的方式,因为它与auth有关--也称为模式,是POST中的惯例。
auth.
部分只是一种命名空间的方式,因为它与auth有关--也被称为模式,是Postgres中的一种惯例。 - 这个函数将返回一个
text
值。 - 该函数的主体是从
jwt
的userId
字段中获取数值,该字段是与request
一起出现的。 我们将在文章后面讨论JWTs。 - 如果没有
request.jwt.claim.userId
,我们只是返回一个空字符串 -''
.
没那么吓人!
请看这个关于Postgres函数的视频来了解更多。
让我们启用Row Level Security
,并使用我们的新函数来确保只有拥有todo
的用户可以看到它。
行级安全
因为Supabase只是一个PostgreSQL数据库,我们可以利用一个重要的功能--行级安全(RLS)。RLS允许我们在数据库中编写授权规则,这可以更有效,也更安全!
回到Supabase仪表板,在侧边面板中选择Authentication
>Policies
,然后点击 Enable RLS
为todo
表。
现在,如果我们刷新我们的应用程序,我们会看到空的状态信息。
我们的todos
去了哪里?
默认情况下,RLS会拒绝对所有行的访问。如果我们想让一个用户看到他们的todos
,我们需要写一个策略。
回到Supabase仪表板,单击New Policy
,然后单击. Create a policy from scratch
并填入以下内容。
这可能看起来有点不熟悉,所以让我们把它分解一下。
- 我们要给我们的策略一个
name
。这可以是任何东西。 - 我们声明我们想启用哪些操作。由于我们希望用户能够阅读自己的
todos
,我们选择了SELECT
. - 我们需要指定一个条件,可以是
true
或false
.如果它被评估为true
,该动作将被允许。否则,它将继续被拒绝。 - 我们调用我们的
auth.user_id()
函数来获取当前登录用户的id
,并将其与这个user_id
列的todo
。
我建议查看这个视频,以了解更多关于Supabase中的行级安全是多么的棒和强大。
点击Review
,看看正在为我们生成的SQL。
甚至不吓人。它只是把我们输入的字段格式化为正确的SQL语法。
当我们在这里时,让我们添加一个用于创建新todos
的策略。这将是一个 INSERT
行动。
虽然我们可能想允许用户执行所有的CRUD动作,但好的做法是指定单独的策略,而不是选择
ALL
.这使得它们在未来更容易扩展和删除。
我们的应用程序还不需要能够更新或删除todos
;因此,我们将不为这些操作创建策略。
良好的安全实践是,为应用程序的运行启用最小的权限。如果我们将来想启用这些操作,我们可以很容易地为它们编写策略。
让我们通过刷新Next.js应用程序,看看我们是否可以查看我们的todos了。
还是没有todos 🙁 我们是不是弄错了什么?
没有!我们只需要再加一点胶水,把Auth0给我们Next.js应用程序的JWT转换成Supabase所期望的格式。
什么是JWT?
要理解这个问题,我们必须首先理解什么是JWT。JWT将一个JSON对象编码成一个大字符串,我们可以用它来在不同的服务之间发送数据。
默认情况下,JWT中的数据并没有被加密或保密,只是被编码。
这是一个取自jwt.io的例子--一个处理JWTs的伟大工具。在左边,我们可以看到JWT值。在右边,我们可以看到每个部分在解码时代表什么。我们有一些关于JWT如何被编码的头信息,用户数据的有效载荷,以及可用于verify
该令牌的签名。
不要把秘密的东西放在JWT中!!
我们之所以能够信任JWT的认证,是因为我们可以用一个秘密值来sign
。这个值与有效载荷数据一起通过一个算法,另一边就会出现一个JWT字符串。我们可以使用签名秘密在我们的服务器上verify
JWT。这可以确保攻击者在传输过程中没有对我们的令牌进行修改。如果他们这样做了,JWT的值将是不同的,并且它将无法验证。
它被修改并通过verify
步骤的唯一方法是如果有人拥有你的签名秘密。这将是糟糕的。这就是为什么我们只能用我们的服务器来签署JWT--或者在Next.js的情况下,用getStaticProps
、getServerSideProps
,或者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);
}
},
});
在这个函数中,我们要。
- 从Auth0的会话中获取用户对象
- 创建一个新的有效载荷
- 使用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。
没有!没有
但我们已经非常接近了。如果我们看一下Supabase仪表板上的Table editor
。谁是拥有todos的用户?
NULL!!!。
所以我们只需要找出我们当前的user_id
,并将其添加到这些行中。
回到Auth0仪表板,点击侧边栏的User Management
>Users
,然后选择你的用户。
user_id
会显示在你的用户详情页面的顶部。
让我们复制这个值并把它作为todos
的user_id
。
刷新我们的Next.js应用程序。
Voilà!好了!!!
我们需要实现的最后一件事是添加一个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频道并查看我们的博客。
谢谢你的阅读!