Complete implementation of the admin authentication and authorization plan using a separate Clerk instance (App B) for cryptographic isolation from the storefront. Convex backend changes: - auth.config.ts: dual JWT provider (storefront + admin Clerk issuers) - http.ts: add /clerk-admin-webhook route with separate signing secret - users.ts: role-aware upsertFromClerk (optional role arg), store reads publicMetadata.role from JWT, assertSuperAdmin internal query - model/users.ts: add requireSuperAdmin helper - adminInvitations.ts: inviteAdmin action (super_admin gated, Clerk Backend SDK) Admin app (apps/admin): - Route groups: (auth) for sign-in, (dashboard) for gated pages - AdminUserSync, AdminAuthGate, AccessDenied, LoadingSkeleton components - useAdminAuth hook with loading/authorized/denied state machine - RequireRole component for super_admin-only UI sections - useStoreUserEffect hook for Clerk→Convex user sync - Sidebar shell with nav-main, nav-user, app-sidebar - clerkMiddleware with /sign-in excluded from auth.protect - ShadCN UI components (sidebar, dropdown, avatar, etc.) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
27 lines
713 B
TypeScript
27 lines
713 B
TypeScript
"use node";
|
|
|
|
import { v } from "convex/values";
|
|
import { action } from "./_generated/server";
|
|
import { internal } from "./_generated/api";
|
|
import { createClerkClient } from "@clerk/backend";
|
|
|
|
export const inviteAdmin = action({
|
|
args: {
|
|
email: v.string(),
|
|
role: v.union(v.literal("admin"), v.literal("super_admin")),
|
|
},
|
|
handler: async (ctx, { email, role }) => {
|
|
await ctx.runQuery(internal.users.assertSuperAdmin);
|
|
|
|
const clerk = createClerkClient({
|
|
secretKey: process.env.CLERK_ADMIN_SECRET_KEY!,
|
|
});
|
|
|
|
await clerk.invitations.createInvitation({
|
|
emailAddress: email,
|
|
redirectUrl: `${process.env.ADMIN_URL}/sign-in`,
|
|
publicMetadata: { role },
|
|
});
|
|
},
|
|
});
|