Files
the-pet-loft/convex/adminInvitations.ts
ianshaloom a897089fdc feat(admin): implement admin auth & authorization system (Phases 0–6)
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>
2026-03-04 16:13:07 +03:00

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 },
});
},
});