第 1 部分:在文件共享应用程序中使用 Kinde 和 Convex 掌握身份验证和基于角色的访问控制 (RBAC) 使用 Kinde 的 RBAC(或基于角色的访问控制)最后的想法用户和身份验证示例应用程序

2025-06-10

第一部分:在文件共享应用程序中使用 Kinde 和 Convex 实现主身份验证和基于角色的访问控制 (RBAC)

Kinde 的 RBAC(基于角色的访问控制)

最后的想法

用户和身份验证示例应用程序

Kinde 是一个身份验证和身份管理平台,可帮助开发人员实现安全的用户身份验证、基于角色的访问控制(RBAC)和社交登录(例如,Google、Facebook、GitHub)。

它提供无密码登录、SSO 和 webhook,用于实时用户数据同步,从而可以轻松管理 Web 应用程序中的用户访问和权限。

而 Convex 为您提供了功能齐全的后端,具有云功能、数据库、调度和同步引擎,可让您的前端和后端实时保持最新状态。

您将在本教程中学习什么

  • 在您的 Next.js 应用中设置 Kinde:了解如何集成 Kinde 进行用户身份验证。
  • 支持多种身份验证提供商:集成 Google、Facebook 和 GitHub 等社交登录。
  • 使用中间件创建受保护的路由:限制对未经身份验证的路由的访问。
  • 用户身份验证工作流程:无缝管理登录和注销。
  • 配置 Convex 进行用户数据管理:设置 Convex 来存储和管理用户数据。
  • 使用 webhook 同步身份验证事件:将用户身份验证和数据事件从 Kinde 同步到 Convex。
  • RBAC 和设置简介:如何根据角色限制对功能的访问,增强安全性。

先决条件

  • 您的计算机上安装了 Node.js。
  • 了解 TypeScript React 和 Next.js。
  • Kinde上的免费帐户用于管理身份验证。
  • Convex上的免费帐户,用于用户数据管理。

开始

设置启动器仓库

为了简化流程,我们创建了一个入门存储库,其中包括:

  • 一个预先配置的 Next.js 项目,带有 TypeScript 和 TailwindCSS。
  • Shadcn UI 组件已设置。
  • 基本应用程序路由器文件夹结构。
  • 预先安装必要的库。

克隆启动项目

首先,克隆启动存储库:

git clone https://github.com/sholajegede/nextjs-shadcn-starter.git
cd nextjs-shadcn-starter
Enter fullscreen mode Exit fullscreen mode

安装依赖项:

npm install
# or
yarn install
# or
bun install
Enter fullscreen mode Exit fullscreen mode

启动开发服务器:

npm run dev
# or
yarn dev
# or
bun run dev
Enter fullscreen mode Exit fullscreen mode

在浏览器中导航到http://localhost:3000。你应该会看到一个如下所示的简单界面:

图片描述

在 Next.js 项目中开始使用 Kinde 的步骤

1. 访问 Kinde 网站

  • 通过教程中的链接进入 Kinde 网站。
  • 如果您没有帐户,请单击“免费开始”创建一个。
  • 登录以访问仪表板。

2. 创建新项目

  • 在 Kinde 仪表板中,单击“添加业务”(在 Kinde 的术语中,项目被称为业务)。
  • 填写项目详细信息(例如,名称、地区、员工人数)。
  • 保存项目并访问入职流程。

3.配置项目

  • 选择将 Kinde 集成到现有项目中的选项。

图片描述

  • 选择适当的 SDK(在本例中为 Next.js)。

图片描述

  • 选择身份验证方法“Google 和 Facebook”

图片描述

4. 访问文档

5. 安装 Kinde SDK

  • 使用提供的安装命令:
npm i @kinde-oss/kinde-auth-nextjs
Enter fullscreen mode Exit fullscreen mode
  • 如果有必要,请重新启动开发服务器。

6. 将 Kinde 集成到代码库

  • 配置环境变量:
KINDE_CLIENT_ID=<your_kinde_client_id>
KINDE_CLIENT_SECRET=<your_kinde_client_secret>
KINDE_ISSUER_URL=https://<your_kinde_subdomain>.kinde.com
KINDE_SITE_URL=http://localhost:3000
KINDE_POST_LOGOUT_REDIRECT_URL=http://localhost:3000
KINDE_POST_LOGIN_REDIRECT_URL=http://localhost:3000/dashboard
Enter fullscreen mode Exit fullscreen mode
  • 将示例中的信息替换为您自己的信息。(i)在 Kinde 中,转到您的仪表板 > 应用程序 > [您的应用 > 查看详细信息 (ii) 向下滚动到“应用密钥”,您将在那里看到您的域、客户端 ID 和客户端密钥

7. 设置 Kinde Auth 路由处理程序
:在 Next.js 项目中创建以下文件:app/api/auth/[kindeAuth]/route.ts。在 route.ts 文件中输入以下代码:

import {handleAuth} from "@kinde-oss/kinde-auth-nextjs/server";
export const GET = handleAuth();
Enter fullscreen mode Exit fullscreen mode

这将处理 Next.js 应用中的 Kinde Auth 端点。

在 Next.js 项目中开始使用 Convex 的步骤

1. 安装 Convex
首先安装 convex 包,它提供了从 Next.js 应用程序使用 Convex 的便捷界面:

npm install convex
Enter fullscreen mode Exit fullscreen mode

2. 设置 Convex 开发部署
接下来,运行npx convex dev。这将提示您使用 GitHub 登录,创建项目,并保存生产和部署 URL。

它还将为convex/您创建一个文件夹,以便在其中编写后端 API 函数。dev然后,该命令将继续运行,以将您的函数与云中的开发部署同步。

作为此设置的一部分,Convex 会自动为您的部署设置一些环境变量,例如:

  • CONVEX_DEPLOYMENT:指定部署名称(例如dev:necessary-jackal-122)。
  • NEXT_PUBLIC_CONVEX_URL:您的 Convex 后端的面向公众的 URL(例如https://necessary-jackal-122.convex.cloud)。
npx convex dev
Enter fullscreen mode Exit fullscreen mode

对于 webhook(我们将在本教程后面介绍),您还需要添加:

NEXT_PUBLIC_CONVEX_HTTP_URL:用于处理 webhook 的 HTTP 端点(例如https://necessary-jackal-122.convex.site)。

3. 为你的项目创建 Schema。Schema
定义了数据库表的结构以及每个表包含的字段。为此,请进入你的convex文件夹并创建一个schema.ts文件。在其中复制并粘贴以下代码:

import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  users: defineTable({
    email: v.string(),
    kindeId: v.string(),
    username: v.optional(v.string()),
    imageUrl: v.optional(v.string()),
    imageStorageId: v.optional(v.id("_storage")),
    notificationType: v.optional(v.string()),
    communication_updates: v.optional(v.boolean()), // true or false : default = true
    marketing_updates: v.optional(v.boolean()), // true or false
    social_updates: v.optional(v.boolean()), // true or false
    security_updates: v.optional(v.boolean()), // true or false : default = true
    stripeId: v.optional(v.string()),
  }),
});
Enter fullscreen mode Exit fullscreen mode
  • emailkindeId:必填字段,用于存储用户的电子邮件和唯一标识符,该标识符将在身份验证后从 Kinde 获取。
  • usernameimageUrl:可选字段,用于在身份验证后存储来自 Kinde 的名字和个人资料图像/图片。
  • imageStorageId:链接到存储在 Convex 存储系统中的已上传图片。当用户在应用程序中更新个人资料图片时,这些图片会被存储。
  • notificationType并更新偏好设置(communication_updatesmarketing_updates等):布尔字段,让用户自定义他们的通知偏好设置。
  • stripeId:可选择存储用户的 Stripe ID 以进行支付集成。

4. 创建身份验证配置
convex文件夹中创建一个新文件,auth.config.ts其中包含用于验证访问令牌的服务器端配置。

KINDE_ISSUER_URL从您的文件中粘贴.env.local并将 applicationID 设置为convex

const authConfig = {
  providers: [
    {
      domain: process.env.KINDE_ISSUER_URL,
      applicationID: "convex",
    },
  ]
};

export default authConfig;
Enter fullscreen mode Exit fullscreen mode

5. 部署您的更改
运行npx convex dev以自动将您的配置同步到您的后端。

npx convex dev
Enter fullscreen mode Exit fullscreen mode

6. 配置您的ConvexKindeProvider
应用,以便集成 Convex 和 Kinde 身份验证,您需要一个自定义提供程序组件。请按照以下步骤创建并配置它:

  • 创建providers文件夹:在项目根目录下,创建一个名为 的文件夹providers。该文件夹用于存放自定义提供程序组件。
  • 创建ConvexKindeProvider.tsx文件:在providers文件夹内,创建一个名为 的新文件ConvexKindeProvider.tsx
  • 粘贴代码:将提供的代码复制并粘贴到此文件中。
"use client";

import { ReactNode, useEffect } from "react";
import { KindeProvider, useKindeAuth } from "@kinde-oss/kinde-auth-nextjs";
import { ConvexProvider, ConvexReactClient, AuthTokenFetcher } from "convex/react";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL as string);

const ConvexKindeProvider = ({ children }: { children: ReactNode }) => {
  const { getToken } = useKindeAuth();

  useEffect(() => {
    const fetchToken: AuthTokenFetcher = async () => {
      const token = await getToken();
      return token || null;
    };

    if (typeof getToken === "function") {
      convex.setAuth(fetchToken);
    }
  }, [getToken]);

  return (
    <KindeProvider
      domain={process.env.NEXT_PUBLIC_KINDE_DOMAIN as string}
      clientId={process.env.NEXT_PUBLIC_KINDE_CLIENT_ID as string}
      redirectUri={process.env.NEXT_PUBLIC_KINDE_REDIRECT_URI as string}
    >
      <ConvexProvider client={convex}>{children}</ConvexProvider>
    </KindeProvider>
  );
};

export default ConvexKindeProvider;
Enter fullscreen mode Exit fullscreen mode

代码功能:
ConvexKindeProvider组件将 Kinde 的身份验证与 Convex 的数据同步功能集成在一起,确保经过身份验证的用户会话得到安全管理。以下是其功能细分:

  • 导入所需的库:
    • KindeProvideruseKindeAuth:使用 Kinde 处理身份验证。
    • ConvexProviderConvexReactClient:管理 Convex 的实时数据库和同步功能。
  • 创建一个凸客户端:
    • ConvexReactClient使用您的 Convex 应用程序的 URL ( )进行初始化NEXT_PUBLIC_CONVEX_URL
  • 获取并设置身份验证令牌:
    • fetchToken使用 Kinde 定义一个函数getToken()来检索用户的身份验证令牌。
    • 该令牌被传递给 Convex 的setAuth方法来保护 API 请求。
  • 使用效果钩子:
    • 确保令牌获取逻辑在发生getToken更改时运行。
  • 使用提供者渲染子项:
    • 将应用程序的组件(children)包装在和中KindeProviderConvexProvider从而实现整个应用程序的身份验证和数据库访问。

此设置的主要特点

  • 安全认证:
    • 检索并使用 Kinde 的身份验证令牌通过 Convex 验证用户身份。
  • 环境变量:
    • 使用环境变量(NEXT_PUBLIC_KINDE_DOMAINNEXT_PUBLIC_KINDE_CLIENT_ID等)来安全地配置 Kinde 和 Convex。
  • 可重复使用的组件:
    • 将所有身份验证和数据同步逻辑封装到单个组件中,简化整个应用程序的集成。

通过在您的应用程序中包含此提供程序,您可以确保 Kinde 的身份验证和 Convex 的实时数据功能之间的无缝集成。

7. 将 传递ConvexKindeProvider给根组件。layout.tsx
为了确保 Kinde 身份验证和 Convex 数据库同步在整个应用程序中均可访问,您需要使用 包装根布局ConvexKindeProvider。这样,所有子组件都可以继承这些功能,而无需进行其他设置。

您可以按照以下步骤操作:

  • 打开你的layout.tsx文件。
  • 将您的内容包装在里面,ConvexKindeProvider如下所示:
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import ConvexKindeProvider from "@/providers/ConvexKindeProvider";
import { TooltipProvider } from "@/components/ui/tooltip";

const geistSans = localFont({
  src: "../fonts/GeistVF.woff",
  variable: "--font-geist-sans",
  weight: "100 900",
});
const geistMono = localFont({
  src: "../fonts/GeistMonoVF.woff",
  variable: "--font-geist-mono",
  weight: "100 900",
});

export const calSans = localFont({
  src: "../fonts/CalSans-SemiBold.ttf",
  variable: "--font-calsans",
})

export const metadata: Metadata = {
  title: "File Share App",
  description: "The fastest and most secure way to share your files.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <ConvexKindeProvider>
      <html lang="en" suppressHydrationWarning>
        <body
          className={`${geistSans.variable} ${geistMono.variable} antialiased`}
        >
          <ThemeProvider
            attribute="class"
            defaultTheme="system"
            enableSystem
            disableTransitionOnChange
          >
            <TooltipProvider>
              {children}
            </TooltipProvider>
          </ThemeProvider>
        </body>
      </html>
    </ConvexKindeProvider>
  );
};
Enter fullscreen mode Exit fullscreen mode

为什么这很重要

  • 全局身份验证:包装根布局可确保应用中的所有组件都可以访问经过身份验证的会话。
  • 实时数据:Convex 的实时更新现在可在您的整个应用程序中使用。
  • 简化设置:将此逻辑集中在根部可减少重复代码并确保一致的行为。

通过遵循此步骤,您的应用程序将完全具备无缝处理身份验证和实时数据同步的功能。

设置中间件来保护路由

为了保护应用程序的路由并处理身份验证,您将使用中间件来重定向未经身份验证的用户并管理他们的访问。

步骤1:创建中间件文件
在根目录下(app目录外),创建一个名为的文件middleware.ts

第 2 步:定义中间件
下面是定义和配置中间件的完整代码:

import { withAuth } from "@kinde-oss/kinde-auth-nextjs/middleware";

export default withAuth({
  loginPage: "/api/auth/login", // Redirect unauthenticated users to this page
  isReturnToCurrentPage: true, // Redirect users back to their current page after logging in
});

export const config = {
  matcher: [
    /*
     * Protect all routes except:
     * - api (API routes)
     * - about, privacypolicy, termsofservice (public pages)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico, sitemap.xml, robots.txt (metadata files)
     */
    '/((?!api|about|privacypolicy|termsofservice|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|$).*)',
  ],
};
Enter fullscreen mode Exit fullscreen mode

中间件的作用

  • 限制访问:阻止未经身份验证的用户访问受保护的路由。诸如apiabout和静态文件之类的路由仍然可以访问。
  • 处理重定向:将未经身份验证的用户重定向到指定位置loginPage/api/auth/login在本例中)。
  • 将用户返回到其原始页面:使用isReturnToCurrentPage: true,用户成功登录后将返回到他们尝试访问的页面。
  • 灵活的匹配器配置:该matcher属性定义哪些路由应该绕过中间件(例如 API 和静态资源)。这确保只有相关的路由受到保护。

逻辑概述

  • 未经身份验证的用户:当用户访问受限路由时,中间件会拦截请求并将其重定向到登录页面。
  • 已验证用户:如果用户已经通过身份验证,中间件将允许不间断地访问请求的路由。
  • 登录后行为:登录后,用户会自动重定向到他们最初尝试访问的页面。

该中间件确保只有经过身份验证的用户才能访问应用程序的敏感部分,并通过智能重定向提供无缝的登录体验。

使用 Webhook 将经过身份验证的用户同步到数据库

  • Webhook 是由应用程序中的特定事件触发的 HTTP 回调(例如,user.created在 Kinde 中)。
  • Convex 如何使用 Webhook:Convex 通过 暴露 HTTP 端点httpRouter。这些端点接收 Webhook 负载,验证它们,并触发内部流程,例如变更或查询。
  • Convex 的 HTTPS URL:每个httpRouter路由(例如/kinde)都会生成一个唯一的 HTTPS 端点,例如https://<your-convex-app>.convex.site/kinde。您可以将此 URL 提供给 Kinde 以发送 webhook 事件。

在 Kinde 中设置 Webhook 配置

  • 要在 Kinde 中创建 Webhook,请转到您的 Kinde 仪表板并单击“设置”
    图片描述

  • 接下来,向下滚动直到看到Webhooks,然后单击它:

图片描述

  • 这里我已经有一个 Webhook 设置,但如果这是您第一次,只需单击添加 Webhook

图片描述

  • 接下来,为你的 webhook 命名并添加描述(可选),然后将其复制粘贴NEXT_PUBLIC_CONVEX_HTTP_URLEndpoint URL中。不要忘记添加/kinde

图片描述

  • 最后,选择您想要触发的事件。在本教程中,我们将选择user.createduser.deleted事件。

图片描述

您还可以选择更多:

图片描述

  • 现在,与使用 Webhook Secret 来验证 webhook 的 Clerk 不同,Kinde 会自动使用 JWT 签署 webhook 有效负载,我将向您展示其工作原理。

  • 您还可以使用webhook.site等工具来测试您的 webhook 并查看它发送的有效负载类型。

  • 当使用 Convex http url 进行测试时,请确保检查 Convex 的日志中是否有收到的事件。

图片描述

使用 Kinde Webhooks 进行 Convex 集成

在 Convex 等无服务器环境中集成 Webhook 和 JWT 验证时,使用正确的库来处理 JWT 至关重要。Convex 函数无法使用 Node.js 内置模块,jsonwebtoken因为它们依赖于加密和流等功能,而这些功能在无服务器设置中不可用。

为什么要使用jose而不是jsonwebtoken

  • 与无服务器环境的兼容性:jsonwebtoken依赖于无服务器环境中不支持的 Node.js 模块,而专jose为此类平台设计,无需原生 Node.js 依赖即可运行。
  • 浏览器兼容性:jsonwebtoken与仅在服务器端不同,它jose可以在浏览器和无服务器平台上运行,使其成为更加通用、轻量级的选择。

jsonwebtokenKinde Webhooks 的问题:在关于Kinde
与 Convex Webhooks 集成的教程中建议使用 Kinde 包进行 JWT 验证。然而,这种方法在使用 Convex 时存在问题,因为 Convex 不支持 Node.js 内置模块。此外,Kinde 不会在标头中发送授权令牌,这使得验证过程变得复杂。因此,我们需要采用不同的方法,使用而不是标准标头。jsonwebtokenAuthorizationcontent-typeAuthorization

以下是我们如何使用joseJWT 验证来实现这一点:
步骤 1:安装jose

  • 首先,安装jose用于验证 JWT 的库。
npm install jose
Enter fullscreen mode Exit fullscreen mode

第 2 步:导入必要的库:

import { httpRouter } from "convex/server";
import { internal } from "./_generated/api";
import { httpAction } from "./_generated/server";
import { jwtVerify, createRemoteJWKSet } from "jose";
Enter fullscreen mode Exit fullscreen mode
  • httpRouter:这是 Convex 的,用于设置 HTTP 路由,以便在 Convex 函数中处理 HTTP 请求。它将帮助定义你的无服务器应用如何处理请求,在本例中是 Webhook 事件。
  • internal:这是您的 Convex 内部 API,允许您运行查询和变更。您将使用它来与 Convex 数据库进行交互(例如,添加、更新或删除用户)。
  • httpAction:这是一个 Convex 函数包装器,用于定义处理 HTTP 请求的无服务器函数。它封装了响应请求的逻辑。
  • jwtVerifycreateRemoteJWKSet:这些是来自库的函数josejwtVerify用于根据一组密钥验证 JWT(JSON Web Token)的有效性,而createRemoteJWKSet允许您从 URL 检索一组公钥,可用于验证 JWT。

步骤 3:定义 Kinde Webhook 数据的类型

type KindeEventData = {
  user: {
    id: string;
    email: string;
    first_name?: string;
    last_name?: string | null;
    is_password_reset_requested: boolean;
    is_suspended: boolean;
    organizations: {
      code: string;
      permissions: string | null;
      roles: string | null;
    }[];
    phone?: string | null;
    username?: string | null;
    image_url?: string | null;
  };
};

type KindeEvent = {
  type: string;
  data: KindeEventData;
};
Enter fullscreen mode Exit fullscreen mode

在这里,我们定义来自 Kinde webhook 的事件数据结构的类型。

  • KindeEventData:代表 Kinde 发送的实际数据,包括idemailfirst_namelast_name等用户详细信息。
  • KindeEvent:webhook 事件的整体结构,包括一个type(例如user.createduser.deleted)和关联的data(类型为KindeEventData)。

步骤 4:设置 HTTP 路由器

const http = httpRouter();
Enter fullscreen mode Exit fullscreen mode

这一行初始化 HTTP 路由器。您可以在此处定义处理特定 HTTP 请求的路由。您可以将其视为设置一个端点,以便 Kinde 可以向该端点发送其 webhook 事件。

步骤 5:定义 Webhook 处理程序

const handleKindeWebhook = httpAction(async (ctx, request) => {
  const event = await validateKindeRequest(request);
  if (!event) {
    return new Response("Invalid request", { status: 400 });
  }

  switch (event.type) {
    case "user.created":
      await ctx.runMutation(internal.users.createUserKinde, {
        kindeId: event.data.user.id,
        email: event.data.user.email,
        username: event.data.user.first_name || ""
      });
      break;
    case "user.deleted":
      const userToDelete = await ctx.runQuery(internal.users.getUserKinde, {
        kindeId: event.data.user.id,
      });

      if (userToDelete) {
        await ctx.runMutation(internal.users.deleteUserKinde, {
          kindeId: event.data.user.id,
        });
      } else {
        console.warn(`No user found to delete with kindeId ${event.data.user.id}.`);
      }
      break;
    default:
      console.warn(`Unhandled event type: ${event.type}`);
  }

  return new Response(null, { status: 200 });
});
Enter fullscreen mode Exit fullscreen mode

此函数负责处理 Kinde webhook 请求。它:

  • 使用函数验证传入的请求validateKindeRequest
  • 如果验证失败,它会以400状态代码(错误请求)进行响应。
  • 如果请求有效,它会检查事件类型(例如user.createduser.deleted)并执行适当的操作:
    • 对于user.created事件,它调用createUserKinde变异将新用户添加到 Convex 数据库。
    • 对于user.deleted事件,它会尝试从数据库中获取用户,如果找到则删除它们。
  • 如果事件类型未得到处理,则会记录警告。

步骤6:创建JWT验证函数

async function validateKindeRequest(request: Request): Promise<KindeEvent | null> {
  try {
    if (request.headers.get("content-type") !== "application/jwt") {
      console.error("Invalid Content-Type. Expected application/jwt");
      return null;
    }

    const token = await request.text(); // JWT is sent as raw text in the body.
    const JWKS_URL = `${process.env.KINDE_ISSUER_URL}/.well-known/jwks.json`;
    const JWKS = createRemoteJWKSet(new URL(JWKS_URL));

    const { payload } = await jwtVerify(token, JWKS);

    if (
      typeof payload === "object" &&
      payload !== null &&
      "type" in payload &&
      "data" in payload
    ) {
      return {
        type: payload.type as string,
        data: payload.data as KindeEventData,
      };
    } else {
      console.error("Payload does not match the expected structure");
      return null;
    }
  } catch (error) {
    console.error("JWT verification failed", error);
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

此函数负责验证传入请求中的 JWT。它的作用如下:

  • 内容类型检查:确保Content-Typeapplication/jwt。如果不是,则函数记录错误并返回null
  • 提取 JWT:使用 直接从请求主体中提取 JWT 令牌request.text()。这与许多通常在标头中传递 JWT 的标准设置不同Authorization
  • 获取 JWK 集:从 Kinde 的 URL ( ) 获取 JSON Web 密钥集 (JWKS) process.env.KINDE_ISSUER_URL。JWKS 是一组用于验证 JWT 签名的公钥。
  • 验证 JWT:使用josejwtVerify函数根据 JWKS 验证 JWT。如果验证成功,它会检查有效负载是否包含预期的typedata字段。
  • 返回有效事件:如果 JWT 有效,则返回解析后的事件及其类型和数据。否则,记录错误并返回null

步骤 7:注册路线

http.route({
  path: "/kinde",
  method: "POST",
  handler: handleKindeWebhook,
});
Enter fullscreen mode Exit fullscreen mode

这将注册/kinde用于处理POST请求的端点。当 Kinde 向此端点发送 webhook 时,该handleKindeWebhook函数将被执行。

步骤 8:导出路由器

export default http;
Enter fullscreen mode Exit fullscreen mode

最后,HTTP 路由器被导出以供您的 Convex 应用程序使用。

您的代码中会出现一些错误,这是因为我们还没有写出在调用事件后向凸集添加新用户的函数。

为此,请users.ts在 Convex 文件夹中创建一个文件。然后将以下代码复制并粘贴到其中:

import { ConvexError, v } from "convex/values";
import { internalMutation, internalQuery, mutation, query } from "./_generated/server";

export const createUserKinde = internalMutation({
  args: {
    kindeId: v.string(),
    email: v.string(),
    username: v.optional(v.string()),
    imageUrl: v.optional(v.string()),
    imageStorageId: v.optional(v.id("_storage")),
    notificationType: v.optional(v.string()),
    communication_updates: v.optional(v.boolean()), // true or false : default = true
    marketing_updates: v.optional(v.boolean()), // true or false
    social_updates: v.optional(v.boolean()), // true or false
    security_updates: v.optional(v.boolean()), // true or false : default = true
    stripeId: v.optional(v.string())
  },
  handler: async (ctx, args) => {
    try {
      const newUserId = await ctx.db.insert("users", {
        kindeId: args.kindeId,
        email: args.email,
        username: args.username || "",
        imageUrl: args.imageUrl,
        imageStorageId: args.imageStorageId,
        notificationType: "all",
        communication_updates: true,
        marketing_updates: false,
        social_updates: false,
        security_updates: true,
        stripeId: args.stripeId || ""
      });
      const updatedUser = await ctx.db.get(newUserId);

      return updatedUser;
    } catch (error) {
      console.error("Error creating user:", error);
      throw new ConvexError("Failed to create user.");
    }
  }
});

export const getUserKinde = internalQuery({
  args: { kindeId: v.optional(v.string()) },
  handler: async (ctx, args) => {
    const user = await ctx.db
      .query("users")
      .filter((q) => q.eq(q.field("kindeId"), args.kindeId))
      .unique();

    if (!user) {
      throw new ConvexError("User not found");
    }

    return user;
  },
});

export const updateUserKinde = internalMutation({
  args: {
    kindeId: v.string(),
    imageUrl: v.optional(v.string()),
    email: v.optional(v.string()),
    username: v.optional(v.string()),
    stripeId: v.optional(v.string())
  },
  handler: async (ctx, args) => {
    const user = await ctx.db
      .query("users")
      .filter((q) => q.eq(q.field("kindeId"), args.kindeId))
      .unique();

    if (!user) {
      throw new ConvexError("User not found");
    }

    const updateFields = {
      ...(args.kindeId !== undefined && { kindeId: args.kindeId }),
      ...(args.imageUrl !== undefined && { imageUrl: args.imageUrl }),
      ...(args.email !== undefined && { email: args.email }),
      ...(args.username !== undefined && { username: args.username }),
      ...(args.stripeId !== undefined && { stripeId: args.stripeId })
    };

    await ctx.db.patch(user._id, updateFields);
    return user._id;
  },
});

export const deleteUserKinde = internalMutation({
  args: { kindeId: v.string() },
  async handler(ctx, args) {
    const user = await ctx.db
      .query("users")
      .filter((q) => q.eq(q.field("kindeId"), args.kindeId))
      .unique();

    if (!user) {
      throw new ConvexError("User not found");
    }

    await ctx.db.delete(user._id);
  },
});

export const updateUser = mutation({
  args: {
    userId: v.id("users"),
    email: v.optional(v.string()),
    username: v.optional(v.string()),
    imageUrl: v.optional(v.string()),
    imageStorageId: v.optional(v.id("_storage")),
    notificationType: v.optional(v.string()),
    communication_updates: v.optional(v.boolean()), // true or false : default = true
    marketing_updates: v.optional(v.boolean()), // true or false
    social_updates: v.optional(v.boolean()), // true or false
    security_updates: v.optional(v.boolean()), // true or false : default = true
    stripeId: v.optional(v.string())
  },
  handler: async (ctx, args) => {
    const user = await ctx.db
      .query("users")
      .filter((q) => q.eq(q.field("_id"), args.userId))
      .unique();

    if (!user) {
      throw new ConvexError("User not found");
    }

    const updateFields = {
      ...(args.imageUrl !== undefined && { imageUrl: args.imageUrl }),
      ...(args.imageStorageId !== undefined && { imageStorageId: args.imageStorageId }),
      ...(args.notificationType !== undefined && { notificationType: args.notificationType }),
      ...(args.communication_updates !== undefined && { communication_updates: args.communication_updates }),
      ...(args.marketing_updates !== undefined && { marketing_updates: args.marketing_updates }),
      ...(args.social_updates !== undefined && { social_updates: args.social_updates }),
      ...(args.security_updates !== undefined && { security_updates: args.security_updates }),
      ...(args.email !== undefined && { email: args.email }),
      ...(args.username !== undefined && { username: args.username }),
      ...(args.stripeId !== undefined && { stripeId: args.stripeId })
    };

    await ctx.db.patch(args.userId, updateFields);
    return args.userId;
  },
});

export const getUserByKindeId = query({
  args: { kindeId: v.string() },
  handler: async (ctx, args) => {
    const user = await ctx.db
      .query("users")
      .filter((q) => q.eq(q.field("kindeId"), args.kindeId))
      .unique();

    if (!user) {
      throw new ConvexError("User not found");
    }

    return user;
  },
});

export const getUserByEmail = query({
  args: { email: v.string() },
  handler: async (ctx, args) => {
    const user = await ctx.db
      .query("users")
      .filter((q) => q.eq(q.field("email"), args.email))
      .unique();

    if (!user) {
      throw new ConvexError("User not found");
    }

    return user;
  },
});

export const getUserByConvexId = query({
  args: { userId: v.string() },
  handler: async (ctx, args) => {
    const user = await ctx.db
      .query("users")
      .filter((q) => q.eq(q.field("_id"), args.userId))
      .unique();

    if (!user) {
      throw new ConvexError("User not found");
    }

    return user;
  }
});

export const deleteAndUpdateImage = mutation({
  args: {
    userId: v.id("users"),
    oldImageStorageId: v.id('_storage'),
    newImageUrl: v.string(),
    newImageStorageId: v.id("_storage")
  },
  handler: async (ctx, args) => {
    await ctx.storage.delete(args.oldImageStorageId);

    const user = await ctx.db
      .query("users")
      .filter((q) => q.eq(q.field("_id"), args.userId))
      .unique();

    if (!user) {
      throw new ConvexError("User not found");
    }

    const updateProfileImage = {
      ...(args.newImageUrl !== undefined && { imageUrl: args.newImageUrl }),
      ...(args.newImageStorageId !== undefined && { imageStorageId: args.newImageStorageId })
    };

    await ctx.db.patch(args.userId, updateProfileImage);
  },
});

export const saveNewProfileImage = mutation({
  args: {
    userId: v.id("users"),
    newImageUrl: v.string(),
    newImageStorageId: v.id("_storage")
  },
  handler: async (ctx, args) => {
    const user = await ctx.db
      .query("users")
      .filter((q) => q.eq(q.field("_id"), args.userId))
      .unique();

    if (!user) {
      throw new ConvexError("User not found");
    }

    const updateProfileImage = {
      ...(args.newImageUrl !== undefined && { imageUrl: args.newImageUrl }),
      ...(args.newImageStorageId !== undefined && { imageStorageId: args.newImageStorageId })
    };

    await ctx.db.patch(args.userId, updateProfileImage);
  },
});

export const deleteUser = mutation({
  args: {
    userId: v.id("users"),
  },
  handler: async (ctx, args) => {
    const user = await ctx.db.get(args.userId);

    if (!user) {
      throw new ConvexError("User not found");
    }

    return await ctx.db.delete(args.userId);
  },
});

export const getUrl = mutation({
  args: {
    storageId: v.id("_storage"),
  },
  handler: async (ctx, args) => {
    return await ctx.storage.getUrl(args.storageId);
  },
});

export const getAllUsers = query({
  handler: async (ctx) => {
    return await ctx.db.query('users').order('desc').collect()
  },
});
Enter fullscreen mode Exit fullscreen mode

现在保存并运行命令:

npx convex dev
Enter fullscreen mode Exit fullscreen mode

这将重新验证您输入的凸代码库并推送它。

图片描述

测试登录和注销流程

要测试应用程序的登录和注销功能,请按照以下步骤操作:

  • 导航到app目录并打开page.tsx文件。
  • 找到并双击Hero组件文件。
  • 用下面的代码片段替换现有代码。
"use client";

import { Button } from "@/components/ui/button";
import { ArrowRight, FileUp, LogIn, LogOut } from "lucide-react";
import Link from "next/link";
import {
  RegisterLink,
  LoginLink,
  LogoutLink,
} from "@kinde-oss/kinde-auth-nextjs/components";
import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs";

export async function Hero() {
  const { user } = useKindeBrowserClient();

  return (
    <div className="relative isolate pt-14">
      <div className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80">
        <div className="relative left-[calc(50%-11rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 rotate-[30deg] bg-gradient-to-tr from-primary to-secondary opacity-30 sm:left-[calc(50%-30rem)] sm:w-[72.1875rem]" />
      </div>

      <div className="py-24 sm:py-32 lg:pb-40">
        <div className="mx-auto max-w-7xl px-6 lg:px-8">
          <div className="mx-auto max-w-2xl text-center">
            <h1 className="text-4xl font-bold tracking-tight sm:text-6xl">
              Share Files Securely with Anyone, Anywhere
            </h1>
            <p className="mt-6 text-lg leading-8 text-muted-foreground">
              The fastest and most secure way to share your files. No signup
              required for basic sharing. Enterprise-grade encryption for all
              your files.
            </p>

            {user ? (
              <div className="mt-10 flex items-center justify-center gap-x-6">
                <Button size="lg" variant="outline" asChild>
                  <Link href="/dashboard">
                    <FileUp className="mr-2 h-4 w-4" />
                    Upload Now
                  </Link>
                </Button>
                <LogoutLink>
                  <Button size="lg" variant="outline">
                    <LogOut className="mr-2 h-4 w-4" />
                    Sign in
                  </Button>
                </LogoutLink>
              </div>
            ) : (
              <div className="mt-10 flex items-center justify-center gap-x-6">
                <RegisterLink>
                  <Button size="lg">
                    Get Started
                    <ArrowRight className="ml-2 h-4 w-4" />
                  </Button>
                </RegisterLink>
                <LoginLink>
                  <Button size="lg" variant="outline">
                    <LogIn className="mr-2 h-4 w-4" />
                    Sign in
                  </Button>
                </LoginLink>
              </div>
            )}
          </div>
        </div>
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

此代码会动态检查用户的身份验证状态,并为已登录和已注销的用户呈现相应的操作。测试时,请监控日志和usersConvex 中的表格以验证行为。

从 Convex 获取用户数据

要将 Convex 中的用户数据提取到您的应用程序中,请按照以下步骤操作:

  • 导航到app目录并找到root文件夹。
  • 打开dashboard文件夹,然后访问page.tsx文件。
  • 用下面提供的代码片段替换文件中的现有代码。
"use client";

import { AppSidebar } from "@/components/app-sidebar"
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"
import {
  SidebarInset,
  SidebarProvider,
  SidebarTrigger,
} from "@/components/ui/sidebar"
import { api } from "@/convex/_generated/api"
import { useQuery } from "convex/react"
import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";

export default function Page() {
  const { user, isAuthenticated, getPermissions, getPermission } = useKindeBrowserClient();

  const profile = useQuery(api.users.getUserByKindeId, {
    kindeId: user?.id as string
  });

  return (
    <SidebarProvider>
      <AppSidebar />
      <SidebarInset>
        <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
          <div className="flex items-center gap-2 px-4">
            <SidebarTrigger className="-ml-1" />
            <Separator orientation="vertical" className="mr-2 h-4" />
            <Breadcrumb>
              <BreadcrumbList>
                <BreadcrumbItem className="hidden md:block">
                  <BreadcrumbLink href="#">
                    Building Your Application
                  </BreadcrumbLink>
                </BreadcrumbItem>
                <BreadcrumbSeparator className="hidden md:block" />
                <BreadcrumbItem>
                  <BreadcrumbPage>Data Fetching {profile?.username}</BreadcrumbPage>
                </BreadcrumbItem>
              </BreadcrumbList>
            </Breadcrumb>
          </div>
        </header>
        <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
          <div className="grid auto-rows-min gap-4 md:grid-cols-3">
            <div className="aspect-video rounded-xl bg-muted/50" />
            <div className="aspect-video rounded-xl bg-muted/50" />
            <div className="aspect-video rounded-xl bg-muted/50" />
          </div>
          <div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
        </div>
      </SidebarInset>
    </SidebarProvider>
  )
}
Enter fullscreen mode Exit fullscreen mode

主要特点

  • useQueryHook:使用用户的 Kinde ID 从 Convex 数据库获取用户数据。
  • Kinde 集成:用于useKindeBrowserClient管理用户身份验证和权限。
  • profile?.username动态渲染:在面包屑导航中显示从 Convex 获取的用户名( )。

Kinde 的 RBAC(基于角色的访问控制)

基于角色的访问控制 (RBAC) 是一种关键的授权机制,它根据用户在组织中的角色确定其访问级别。通过为角色分配特定权限,RBAC 确保用户只能执行其被授权的操作。本教程的这一部分将指导您使用 Kinde 为我们的应用程序实现 RBAC。

在我们的场景中,该应用程序允许:

  • 会员可以上传文件但不能删除文件。
  • 管理员可以删除组织内自己的文件和其他人的文件。

通过遵循本指南,您将学会有效地管理前端(隐藏 UI 元素)和后端(强制执行限制)的角色和权限。

步骤 1:在 Kinde 中设置权限。
权限定义了用户可以执行的操作。让我们从创建delete:file权限开始。

  • 访问 Kinde 仪表板
    • 登录您的 Kinde 仪表板。
    • 导航到侧栏中的“设置”并选择“用户管理”。

图片描述

  • 添加权限
    • 单击用户管理部分中的权限。
    • 单击“添加权限”。

图片描述

  • 添加权限的详细信息然后保存
    • 名称:(Delete File或任何描述性名称)。
    • 描述:留空或添加可选上下文。
    • 钥匙:delete:file

图片描述

步骤 2:为用户创建角色。
角色将权限分组,并根据用户的职责分配给他们。让我们创建一个包含该delete:file权限的管理员角色。

  • 添加角色
    • 转到仪表板上的“用户”部分。
    • 单击一个用户。

图片描述

  • 接下来,点击权限

图片描述

  • 单击“添加角色”。
    • 姓名:admin。
    • 关键词:管理员。
    • 权限:选择delete:file
  • 保存角色。

图片描述

图片描述

步骤 3:为用户分配角色
现在已创建管理员角色,请将其分配给特定用户。

  • 将角色分配给用户
    • 返回用户部分。
    • 选择您想要成为管理员的用户。
    • 将管理员角色的开关切换为“活动”。
    • 保存更改

图片描述

步骤 4:验证权限
要确认用户的权限:

  • 返回“用户”部分并选择用户。
  • 查看“权限”选项卡以查看delete:file权限现在是否链接到他们的角色。

图片描述

最后的想法

本教程提供了将 Kinde 和 Convex 集成到 Next.js 应用程序中的详细指南,以构建一个安全、可扩展且用户友好的文件共享平台,该平台具有无密码身份验证、实时数据同步和基于角色的访问控制 (RBAC) 等功能。

亮点:

1. 使用 Kinde 进行身份验证:

  • 无缝集成无密码登录并支持社交登录(Google 和 Facebook)。
  • 使用 Kinde 的中间件来保护路由并管理安全的用户会话。
  • 配置 webhook 以实时与 Convex 同步用户身份验证事件。

2. 带有 Convex 的后端:

  • 利用 Convex 的实时数据库和无服务器架构来管理用户数据。
  • 定义模式、查询和变异以动态存储和检索用户信息。
  • 利用 Convex 的 HTTP 路由进行 webhook 集成并自动执行用户数据更新。

3.基于角色的访问控制(RBAC):

  • 使用 Kinde 设置 RBAC 以根据角色限制用户操作。
  • 直接在 Kinde 仪表板中创建权限(例如,删除:文件)和角色(例如,管理员)。

4.实际实施:

  • 开发功能丰富的用户界面,通过动态用户状态处理提供登录和注销体验。
  • 使用 webhook 和 JWT 验证来安全地同步身份验证事件。
  • 测试登录/注销流程并从 Convex 获取用户数据以获取个性化仪表板。

5.开发人员友好设置:

  • 利用预先配置的 Next.js 入门存储库,其中包含 TypeScript、TailwindCSS 和 Shadcn UI 组件。
  • 有关设置 Kinde 和 Convex 的全面分步说明,确保无缝集成。

访问完整的代码库

想要探索完整的实现?请查看 GitHub 上的完整实现代码库。您可以随意克隆、实验并根据自己的需求进行调整。欢迎贡献代码并点赞!

GitHub 徽标 sholajegede / kinde-凸-启动器

Kinde 和 Convex Starter App 是一个完全预配置的 Next.js 应用程序,旨在帮助您使用 Kinde 和 Convex 快速集成安全身份验证、基于角色的访问控制 (RBAC) 和用户数据管理。

用户和身份验证示例应用程序

此示例演示如何向基本文件共享应用程序添加用户和身份验证。它使用Kinde进行身份验证。

用户最初会看到“开始”“登录”按钮。用户注册后,其信息将通过 Webhook 发送并持久保存到usersConvex 的表中。用户还可以使用无密码身份验证、社交登录和基于角色的访问控制进行注册和登录。

🛠️ 先决条件

开始之前,请确保您已准备好以下内容:

  • 您的计算机上安装了 Node.js。
  • 熟悉 TypeScript、React 和 Next.js。
  • KindeConvex上的免费帐户

🔧 入门

  1. 克隆存储库
git clone https://github.com/sholajegede/kinde-convex-starter.git
cd kinde-convex-starter
Enter fullscreen mode Exit fullscreen mode
  1. 安装依赖项
npm install
# or
yarn install
# or
bun install
Enter fullscreen mode Exit fullscreen mode
  1. 运行开发服务器
npm run dev
# or
yarn dev
# or
bun run dev
Enter fullscreen mode Exit fullscreen mode

使用浏览器打开http://localhost:3000即可查看...

下一步是什么?

在第二部分中,我们将重点介绍如何在生产就绪的文件共享应用程序中实现 RBAC。您将学习如何配置高级角色和权限、应用精细的访问控制,以及如何在 API 端点和前端工作流中强制执行 RBAC。这将确保在实际用例中实现安全且可扩展的授权。

通过本教程,您将能够使用Kinde构建具有企业级安全性、实时数据更新和精确访问控制机制的应用程序。第二部分将把这一基础提升到一个新的高度!🚀

我们很乐意听到您的声音!

有任何想法、疑问或建议吗?请在下方评论区留言,或直接在GitHub上联系我。您的反馈有助于改进,并确保我们涵盖您成功所需的一切。告诉我们您的想法——让我们携手共进!

链接:https://dev.to/sholajegede/part-1-master-authentication-and-role-based-access-control-rbac-with-kinde-and-convex-in-a-h3c
PREV
第二部分:在文件共享应用程序中使用 Kinde 和 Convex 实现主身份验证和基于角色的访问控制 (RBAC)
NEXT
NextRaise:利用人工智能代理 GenAI LIVE 简化初创企业的融资流程!| 2025 年 6 月 4 日