mcpField
A
auth-setup

auth-setup

github.com/get-convex安全117208 次安装今天更新
代码仓库

关于 auth-setup

用于在 Convex 应用中实现身份验证与访问控制的技能,面向应用开发者,涵盖用户身份映射、认证提供方配置和受保护数据访问模式。

主要功能

  1. 01Convex 用户身份映射
  2. 02身份验证辅助函数
  3. 03OAuth 提供方配置
  4. 04所有者与团队访问模式
  5. 05公开与私有查询模式

适用场景

  1. 01为新的 Convex 应用添加身份验证
  2. 02将提供方身份映射到用户记录
  3. 03保护用户所有的数据
  4. 04在 React 或 Next.js 中配置 WorkOS 身份验证

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/get-convex/convex-agent-plugins --skill auth-setup

安装后在 Claude Code 中输入/auth-setup确认已加载。

详细介绍

Convex 认证设置

在 Convex 中实现安全认证,包括用户管理和访问控制。

适用场景

  • 首次设置认证
  • 实现用户管理(用户表、身份映射)
  • 创建认证辅助函数
  • 设置 OAuth 提供商(WorkOS、Auth0 等)

架构概览

Convex 认证主要包含两个部分:

  1. 客户端认证:使用提供商(WorkOS、Auth0、自定义 JWT)
  2. 后端身份:将认证提供商身份映射到你的用户表

架构设置

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

export default defineSchema({
  users: defineTable({
    // From auth provider identity
    tokenIdentifier: v.string(), // Unique per auth provider

    // User profile data
    name: v.string(),
    email: v.string(),
    pictureUrl: v.optional(v.string()),

    // Your app-specific fields
    role: v.union(
      v.literal("user"),
      v.literal("admin")
    ),

    createdAt: v.number(),
    updatedAt: v.optional(v.number()),
  })
    .index("by_token", ["tokenIdentifier"])
    .index("by_email", ["email"]),
});

核心辅助函数

获取当前用户
// convex/lib/auth.ts
import { QueryCtx, MutationCtx } from "./_generated/server";
import { Doc } from "./_generated/dataModel";

export async function getCurrentUser(
  ctx: QueryCtx | MutationCtx
): Promise<Doc<"users">> {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) {
    throw new Error("Not authenticated");
  }

  const user = await ctx.db
    .query("users")
    .withIndex("by_token", q =>
      q.eq("tokenIdentifier", identity.tokenIdentifier)
    )
    .unique();

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

  return user;
}

export async function getCurrentUserOrNull(
  ctx: QueryCtx | MutationCtx
): Promise<Doc<"users"> | null> {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) {
    return null;
  }

  return await ctx.db
    .query("users")
    .withIndex("by_token", q =>
      q.eq("tokenIdentifier", identity.tokenIdentifier)
    )
    .unique();
}
要求管理员权限
export async function requireAdmin(
  ctx: QueryCtx | MutationCtx
): Promise<Doc<"users">> {
  const user = await getCurrentUser(ctx);

  if (user.role !== "admin") {
    throw new Error("Admin access required");
  }

  return user;
}

用户创建/更新

首次登录时
// convex/users.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";

export const storeUser = mutation({
  args: {},
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) {
      throw new Error("Not authenticated");
    }

    // Check if user exists
    const existingUser = await ctx.db
      .query("users")
      .withIndex("by_token", q =>
        q.eq("tokenIdentifier", identity.tokenIdentifier)
      )
      .unique();

    if (existingUser) {
      // Update last seen or other fields
      await ctx.db.patch(existingUser._id, {
        updatedAt: Date.now(),
      });
      return existingUser._id;
    }

    // Create new user
    const userId = await ctx.db.insert("users", {
      tokenIdentifier: identity.tokenIdentifier,
      name: identity.name ?? "Anonymous",
      email: identity.email ?? "",
      pictureUrl: identity.pictureUrl,
      role: "user",
      createdAt: Date.now(),
    });

    return userId;
  },
});

访问控制模式

仅所有者可访问
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { getCurrentUser } from "./lib/auth";

export const updateProfile = mutation({
  args: {
    name: v.string(),
  },
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx);

    await ctx.db.patch(user._id, {
      name: args.name,
      updatedAt: Date.now(),
    });
  },
});
资源所有权
export const deleteTask = mutation({
  args: { taskId: v.id("tasks") },
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx);

    const task = await ctx.db.get(args.taskId);
    if (!task) {
      throw new Error("Task not found");
    }

    // Check ownership
    if (task.userId !== user._id) {
      throw new Error("You can only delete your own tasks");
    }

    await ctx.db.delete(args.taskId);
  },
});
基于团队的访问
// Schema includes membership table
export default defineSchema({
  teams: defineTable({
    name: v.string(),
    ownerId: v.id("users"),
  }),

  teamMembers: defineTable({
    teamId: v.id("teams"),
    userId: v.id("users"),
    role: v.union(v.literal("owner"), v.literal("member")),
  })
    .index("by_team", ["teamId"])
    .index("by_user", ["userId"])
    .index("by_team_and_user", ["teamId", "userId"]),
});

// Helper to check team access
async function requireTeamAccess(
  ctx: MutationCtx,
  teamId: Id<"teams">
): Promise<{ user: Doc<"users">, membership: Doc<"teamMembers"> }> {
  const user = await getCurrentUser(ctx);

  const membership = await ctx.db
    .query("teamMembers")
    .withIndex("by_team_and_user", q =>
      q.eq("teamId", teamId).eq("userId", user._id)
    )
    .unique();

  if (!membership) {
    throw new Error("You don't have access to this team");
  }

  return { user, membership };
}

// Use in functions
export const createProject = mutation({
  args: {
    teamId: v.id("teams"),
    name: v.string(),
  },
  handler: async (ctx, args) => {
    await requireTeamAccess(ctx, args.teamId);

    return await ctx.db.insert("projects", {
      teamId: args.teamId,
      name: args.name,
    });
  },
});

公开与私有查询

公开查询(无需认证)
export const listPublicPosts = query({
  args: {},
  handler: async (ctx) => {
    // No auth check - anyone can read
    return await ctx.db
      .query("posts")
      .withIndex("by_published", q => q.eq("published", true))
      .collect();
  },
});
私有查询(需要认证)
export const getMyPosts = query({
  args: {},
  handler: async (ctx) => {
    const user = await getCurrentUser(ctx);

    return await ctx.db
      .query("posts")
      .withIndex("by_user", q => q.eq("userId", user._id))
      .collect();
  },
});
混合查询(认证可选)
export const getPosts = query({
  args: {},
  handler: async (ctx) => {
    const user = await getCurrentUserOrNull(ctx);

    if (user) {
      // Show all posts including drafts for this user
      return await ctx.db
        .query("posts")
        .withIndex("by_user", q => q.eq("userId", user._id))
        .collect();
    } else {
      // Show only public posts for anonymous users
      return await ctx.db
        .query("posts")
        .withIndex("by_published", q => q.eq("published", true))
        .collect();
    }
  },
});

使用 WorkOS 设置客户端

WorkOS AuthKit 提供完整的认证解决方案,设置步骤极少。

React/Vite 设置
npm install @workos-inc/authkit-react
// src/main.tsx
import { AuthKitProvider, useAuth } from "@workos-inc/authkit-react";
import { ConvexReactClient } from "convex/react";
import { ConvexProvider } from "convex/react";

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);

// Configure Convex to use WorkOS auth
convex.setAuth(useAuth);

function App() {
  return (
    <AuthKitProvider clientId={import.meta.env.VITE_WORKOS_CLIENT_ID}>
      <ConvexProvider client={convex}>
        <YourApp />
      </ConvexProvider>
    </AuthKitProvider>
  );
}
Next.js 设置
npm install @workos-inc/authkit-nextjs
// app/layout.tsx
import { AuthKitProvider } from "@workos-inc/authkit-nextjs";
import { ConvexClientProvider } from "./ConvexClientProvider";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <AuthKitProvider>
          <ConvexClientProvider>
            {children}
          </ConvexClientProvider>
        </AuthKitProvider>
      </body>
    </html>
  );
}
// app/ConvexClientProvider.tsx
"use client";

import { ConvexReactClient } from "convex/react";
import { ConvexProvider } from "convex/react";
import { useAuth } from "@workos-inc/authkit-nextjs";

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

export function ConvexClientProvider({ children }: { children: React.ReactNode }) {
  const { getToken } = useAuth();

  convex.setAuth(async () => {
    return await getToken();
  });

  return <ConvexProvider client={convex}>{children}</ConvexProvider>;
}
环境变量
# .env.local (React/Vite)
VITE_CONVEX_URL=https://your-deployment.convex.cloud
VITE_WORKOS_CLIENT_ID=your_workos_client_id

# .env.local (Next.js)
NEXT_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud
NEXT_PUBLIC_WORKOS_CLIENT_ID=your_workos_client_id
WORKOS_API_KEY=your_workos_api_key
WORKOS_COOKIE_PASSWORD=generate_a_random_32_character_string
登录时调用 storeUser
// In your app after user signs in
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
import { useEffect } from "react";
import { useAuth } from "@workos-inc/authkit-react";

function YourApp() {
  const { user } = useAuth();
  const storeUser = useMutation(api.users.storeUser);

  useEffect(() => {
    if (user) {
      storeUser();
    }
  }, [user, storeUser]);

  // ... rest of your app
}
其他认证提供商

如需使用其他提供商,请参阅 Convex 认证文档,了解:

  • 自定义 JWT
  • Auth0
  • 其他 OAuth 提供商

检查清单

  • 用户表包含 tokenIdentifier 索引
  • getCurrentUser 辅助函数
  • 用于首次登录的 storeUser mutation
  • 在所有受保护函数中检查认证状态
  • 检查资源访问权限
  • 清晰的错误消息(“未认证”“未授权”)
  • 已配置客户端认证提供商(WorkOS、Auth0 等)

评论

支持 Markdown