From df8df7f7b77d5eda847e648fb09c7092b1fbf03f Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Fri, 12 Dec 2025 22:56:27 -0500 Subject: [PATCH] feat: migrate from Clerk to Auth0 authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create @superset/auth0 package with server/client exports - Update all apps (web, api, admin, marketing) to use Auth0 - Replace ClerkProvider with Auth0Provider in layouts - Update middleware to use auth0.middleware() - Replace Clerk env vars with Auth0 env vars in turbo.jsonc - Update GitHub workflows with Auth0 secrets - Rename users.clerkId to users.auth0Id in schema - Remove Clerk webhook handler - Remove sso-callback pages (Auth0 handles callbacks internally) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .env.example | 12 +-- .github/workflows/deploy-preview.yml | 27 +++--- .github/workflows/deploy-production.yml | 27 +++--- apps/admin/package.json | 2 +- .../AppSidebar/components/NavUser/NavUser.tsx | 14 ++- .../DeletedUsersTable/DeletedUsersTable.tsx | 2 +- apps/admin/src/app/layout.tsx | 8 +- apps/admin/src/env.ts | 10 +-- apps/admin/src/proxy.ts | 25 ++++-- apps/api/package.json | 3 +- apps/api/src/app/api/webhooks/clerk/route.ts | 89 ------------------- apps/api/src/env.ts | 4 +- apps/api/src/proxy.ts | 26 +++++- apps/api/src/trpc/context.ts | 4 +- apps/marketing/package.json | 2 +- .../app/components/CTAButtons/CTAButtons.tsx | 6 +- apps/marketing/src/app/layout.tsx | 8 +- apps/marketing/src/env.ts | 10 +-- apps/marketing/src/proxy.ts | 19 +++- apps/web/package.json | 2 +- .../(auth)/sign-in/[[...sign-in]]/page.tsx | 56 +++--------- .../(auth)/sign-up/[[...sign-up]]/page.tsx | 56 +++--------- .../src/app/(auth)/sso-callback/layout.tsx | 7 -- apps/web/src/app/(auth)/sso-callback/page.tsx | 5 -- apps/web/src/app/download/page.tsx | 5 +- apps/web/src/app/layout.tsx | 35 +++----- apps/web/src/app/page.tsx | 28 +++--- apps/web/src/env.ts | 8 +- apps/web/src/proxy.ts | 40 ++++++--- bun.lock | 64 +++++++------ packages/auth0/package.json | 31 +++++++ packages/auth0/src/client.ts | 3 + packages/auth0/src/server.ts | 29 ++++++ packages/auth0/src/types.ts | 30 +++++++ packages/auth0/tsconfig.json | 5 ++ packages/db/src/schema/schema.ts | 4 +- packages/trpc/package.json | 2 +- packages/trpc/src/router/admin.ts | 4 +- packages/trpc/src/router/organization.ts | 2 +- packages/trpc/src/router/task.ts | 2 +- packages/trpc/src/router/user.ts | 2 +- packages/trpc/src/trpc.ts | 35 ++++---- turbo.jsonc | 10 +-- 43 files changed, 366 insertions(+), 397 deletions(-) delete mode 100644 apps/api/src/app/api/webhooks/clerk/route.ts delete mode 100644 apps/web/src/app/(auth)/sso-callback/layout.tsx delete mode 100644 apps/web/src/app/(auth)/sso-callback/page.tsx create mode 100644 packages/auth0/package.json create mode 100644 packages/auth0/src/client.ts create mode 100644 packages/auth0/src/server.ts create mode 100644 packages/auth0/src/types.ts create mode 100644 packages/auth0/tsconfig.json diff --git a/.env.example b/.env.example index 279dd6e245b..d7a59a3b05c 100644 --- a/.env.example +++ b/.env.example @@ -8,10 +8,13 @@ NEON_ORG_ID=your_neon_org_id_here NEON_PROJECT_ID=your_neon_project_id_here NEON_BRANCH_ID=your_neon_branch_id_here -# Clerk Authentication -CLERK_SECRET_KEY=your_clerk_secret_key_here -CLERK_WEBHOOK_SECRET=your_clerk_webhook_secret_here -NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key_here +# Auth0 Authentication +# Generate AUTH0_SECRET with: openssl rand -hex 32 +AUTH0_SECRET=your_auth0_secret_here +AUTH0_BASE_URL=http://localhost:3000 +AUTH0_ISSUER_BASE_URL=https://YOUR_TENANT.auth0.com +AUTH0_CLIENT_ID=your_auth0_client_id_here +AUTH0_CLIENT_SECRET=your_auth0_client_secret_here # App URLs NEXT_PUBLIC_WEB_URL=http://localhost:3000 @@ -19,7 +22,6 @@ NEXT_PUBLIC_API_URL=http://localhost:3001 NEXT_PUBLIC_ADMIN_URL=http://localhost:3002 NEXT_PUBLIC_MARKETING_URL=http://localhost:3003 NEXT_PUBLIC_DOCS_URL=http://localhost:3004 -NEXT_PUBLIC_COOKIE_DOMAIN=localhost # Vercel Blob Storage BLOB_READ_WRITE_TOKEN=your_blob_token_here diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index a3252258e04..a376f73c2cb 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -121,8 +121,10 @@ jobs: BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }} NEXT_PUBLIC_WEB_URL: https://${{ env.WEB_ALIAS }} NEXT_PUBLIC_ADMIN_URL: https://${{ env.ADMIN_ALIAS }} - CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} - CLERK_WEBHOOK_SECRET: ${{ secrets.CLERK_WEBHOOK_SECRET }} + AUTH0_SECRET: ${{ secrets.AUTH0_SECRET }} + AUTH0_ISSUER_BASE_URL: ${{ secrets.AUTH0_ISSUER_BASE_URL }} + AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} + AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }} run: | vercel pull --yes --environment=preview --token=$VERCEL_TOKEN vercel build --token=$VERCEL_TOKEN @@ -193,9 +195,10 @@ jobs: NEXT_PUBLIC_API_URL: https://${{ env.API_ALIAS }} NEXT_PUBLIC_MARKETING_URL: https://${{ env.MARKETING_ALIAS }} NEXT_PUBLIC_DOCS_URL: https://${{ env.DOCS_ALIAS }} - CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} - NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }} + AUTH0_SECRET: ${{ secrets.AUTH0_SECRET }} + AUTH0_ISSUER_BASE_URL: ${{ secrets.AUTH0_ISSUER_BASE_URL }} + AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} + AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }} run: | vercel pull --yes --environment=preview --token=$VERCEL_TOKEN vercel build --token=$VERCEL_TOKEN @@ -251,9 +254,10 @@ jobs: VERCEL_PROJECT_ID: ${{ secrets.VERCEL_MARKETING_PROJECT_ID }} NEXT_PUBLIC_API_URL: https://${{ env.API_ALIAS }} NEXT_PUBLIC_WEB_URL: https://${{ env.WEB_ALIAS }} - CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} - NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }} + AUTH0_SECRET: ${{ secrets.AUTH0_SECRET }} + AUTH0_ISSUER_BASE_URL: ${{ secrets.AUTH0_ISSUER_BASE_URL }} + AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} + AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }} run: | vercel pull --yes --environment=preview --token=$VERCEL_TOKEN vercel build --token=$VERCEL_TOKEN @@ -322,9 +326,10 @@ jobs: DATABASE_URL_UNPOOLED: ${{ env.DATABASE_URL_UNPOOLED }} NEXT_PUBLIC_API_URL: https://${{ env.API_ALIAS }} NEXT_PUBLIC_WEB_URL: https://${{ env.WEB_ALIAS }} - CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} - NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }} + AUTH0_SECRET: ${{ secrets.AUTH0_SECRET }} + AUTH0_ISSUER_BASE_URL: ${{ secrets.AUTH0_ISSUER_BASE_URL }} + AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} + AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }} run: | vercel pull --yes --environment=preview --token=$VERCEL_TOKEN vercel build --token=$VERCEL_TOKEN diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 6e0986582a2..e566e732892 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -72,8 +72,10 @@ jobs: DATABASE_URL_UNPOOLED: ${{ secrets.DATABASE_URL_UNPOOLED }} NEXT_PUBLIC_WEB_URL: ${{ secrets.NEXT_PUBLIC_WEB_URL }} NEXT_PUBLIC_ADMIN_URL: ${{ secrets.NEXT_PUBLIC_ADMIN_URL }} - CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} - CLERK_WEBHOOK_SECRET: ${{ secrets.CLERK_WEBHOOK_SECRET }} + AUTH0_SECRET: ${{ secrets.AUTH0_SECRET }} + AUTH0_ISSUER_BASE_URL: ${{ secrets.AUTH0_ISSUER_BASE_URL }} + AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} + AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }} BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }} run: | vercel pull --yes --environment=production --token=$VERCEL_TOKEN @@ -117,9 +119,10 @@ jobs: NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} NEXT_PUBLIC_MARKETING_URL: ${{ secrets.NEXT_PUBLIC_MARKETING_URL }} NEXT_PUBLIC_DOCS_URL: ${{ secrets.NEXT_PUBLIC_DOCS_URL }} - CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} - NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }} + AUTH0_SECRET: ${{ secrets.AUTH0_SECRET }} + AUTH0_ISSUER_BASE_URL: ${{ secrets.AUTH0_ISSUER_BASE_URL }} + AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} + AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }} run: | vercel pull --yes --environment=production --token=$VERCEL_TOKEN vercel build --prod --token=$VERCEL_TOKEN @@ -159,9 +162,10 @@ jobs: VERCEL_PROJECT_ID: ${{ secrets.VERCEL_MARKETING_PROJECT_ID }} NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} NEXT_PUBLIC_WEB_URL: ${{ secrets.NEXT_PUBLIC_WEB_URL }} - CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} - NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }} + AUTH0_SECRET: ${{ secrets.AUTH0_SECRET }} + AUTH0_ISSUER_BASE_URL: ${{ secrets.AUTH0_ISSUER_BASE_URL }} + AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} + AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }} run: | vercel pull --yes --environment=production --token=$VERCEL_TOKEN vercel build --prod --token=$VERCEL_TOKEN @@ -203,9 +207,10 @@ jobs: DATABASE_URL_UNPOOLED: ${{ secrets.DATABASE_URL_UNPOOLED }} NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} NEXT_PUBLIC_WEB_URL: ${{ secrets.NEXT_PUBLIC_WEB_URL }} - CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} - NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }} + AUTH0_SECRET: ${{ secrets.AUTH0_SECRET }} + AUTH0_ISSUER_BASE_URL: ${{ secrets.AUTH0_ISSUER_BASE_URL }} + AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} + AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }} run: | vercel pull --yes --environment=production --token=$VERCEL_TOKEN vercel build --prod --token=$VERCEL_TOKEN diff --git a/apps/admin/package.json b/apps/admin/package.json index e7b9daf9551..5b04c18a0ed 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -11,7 +11,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@clerk/nextjs": "^6.36.2", + "@superset/auth0": "workspace:*", "@superset/db": "workspace:*", "@superset/queries": "workspace:*", "@superset/shared": "workspace:*", diff --git a/apps/admin/src/app/(dashboard)/components/AppSidebar/components/NavUser/NavUser.tsx b/apps/admin/src/app/(dashboard)/components/AppSidebar/components/NavUser/NavUser.tsx index 774cc5e0989..08098a904ee 100644 --- a/apps/admin/src/app/(dashboard)/components/AppSidebar/components/NavUser/NavUser.tsx +++ b/apps/admin/src/app/(dashboard)/components/AppSidebar/components/NavUser/NavUser.tsx @@ -1,6 +1,5 @@ "use client"; -import { useClerk } from "@clerk/nextjs"; import type { RouterOutputs } from "@superset/trpc"; import { Avatar, AvatarFallback, AvatarImage } from "@superset/ui/avatar"; import { @@ -26,15 +25,12 @@ import { LuSettings, } from "react-icons/lu"; -import { env } from "@/env"; - export interface NavUserProps { user: NonNullable; } export function NavUser({ user }: NavUserProps) { const { isMobile } = useSidebar(); - const { signOut } = useClerk(); const userInitials = user.name .split(" ") @@ -105,11 +101,11 @@ export function NavUser({ user }: NavUserProps) { - signOut({ redirectUrl: env.NEXT_PUBLIC_WEB_URL })} - > - - Log out + + + + Log out + diff --git a/apps/admin/src/app/(dashboard)/users/deleted/components/DeletedUsersTable/DeletedUsersTable.tsx b/apps/admin/src/app/(dashboard)/users/deleted/components/DeletedUsersTable/DeletedUsersTable.tsx index c137fbb1ab3..d5bc0cc3cb7 100644 --- a/apps/admin/src/app/(dashboard)/users/deleted/components/DeletedUsersTable/DeletedUsersTable.tsx +++ b/apps/admin/src/app/(dashboard)/users/deleted/components/DeletedUsersTable/DeletedUsersTable.tsx @@ -268,7 +268,7 @@ export function DeletedUsersTable() {
  • All user data
  • All associated records
  • -
  • Their Clerk account
  • +
  • Their Auth0 account

This action cannot be undone. diff --git a/apps/admin/src/app/layout.tsx b/apps/admin/src/app/layout.tsx index 398e8320152..c3f664cc532 100644 --- a/apps/admin/src/app/layout.tsx +++ b/apps/admin/src/app/layout.tsx @@ -1,11 +1,9 @@ -import { ClerkProvider } from "@clerk/nextjs"; +import { Auth0Provider } from "@superset/auth0/client"; import { Toaster } from "@superset/ui/sonner"; import { cn } from "@superset/ui/utils"; import type { Metadata, Viewport } from "next"; import { IBM_Plex_Mono, Inter } from "next/font/google"; -import { env } from "@/env"; - import "./globals.css"; import { Providers } from "./providers"; @@ -40,7 +38,7 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - + - + ); } diff --git a/apps/admin/src/env.ts b/apps/admin/src/env.ts index 8c8857439c1..92489ddfe15 100644 --- a/apps/admin/src/env.ts +++ b/apps/admin/src/env.ts @@ -13,23 +13,21 @@ export const env = createEnv({ server: { DATABASE_URL: z.string().url(), DATABASE_URL_UNPOOLED: z.string().url(), - CLERK_SECRET_KEY: z.string(), + AUTH0_SECRET: z.string(), + AUTH0_ISSUER_BASE_URL: z.string().url(), + AUTH0_CLIENT_ID: z.string(), + AUTH0_CLIENT_SECRET: z.string(), }, client: { NEXT_PUBLIC_API_URL: z.string().url(), NEXT_PUBLIC_WEB_URL: z.string().url(), - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string(), - NEXT_PUBLIC_COOKIE_DOMAIN: z.string(), }, experimental__runtimeEnv: { NODE_ENV: process.env.NODE_ENV, NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, NEXT_PUBLIC_WEB_URL: process.env.NEXT_PUBLIC_WEB_URL, - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: - process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, - NEXT_PUBLIC_COOKIE_DOMAIN: process.env.NEXT_PUBLIC_COOKIE_DOMAIN, }, skipValidation: !!process.env.SKIP_ENV_VALIDATION, diff --git a/apps/admin/src/proxy.ts b/apps/admin/src/proxy.ts index c3b5bfb88a6..c565b63e110 100644 --- a/apps/admin/src/proxy.ts +++ b/apps/admin/src/proxy.ts @@ -1,12 +1,13 @@ -import { clerkMiddleware } from "@clerk/nextjs/server"; +import { auth0 } from "@superset/auth0/server"; import { db, eq } from "@superset/db"; import { users } from "@superset/db/schema"; import { COMPANY } from "@superset/shared/constants"; +import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { env } from "./env"; -const PUBLIC_ROUTES = ["/ingest", "/monitoring"]; +const PUBLIC_ROUTES = ["/api/auth", "/ingest", "/monitoring"]; function isPublicRoute(pathname: string): boolean { return PUBLIC_ROUTES.some( @@ -14,21 +15,31 @@ function isPublicRoute(pathname: string): boolean { ); } -export default clerkMiddleware(async (auth, req) => { +export default async function middleware(req: NextRequest) { const { pathname } = req.nextUrl; + // Run Auth0 middleware first (handles /api/auth/* routes) + const authResponse = await auth0.middleware(req); + + // If Auth0 handled the request, return its response + if (pathname.startsWith("/api/auth")) { + return authResponse; + } + if (isPublicRoute(pathname)) { return NextResponse.next(); } - const { userId: clerkId } = await auth(); + // Get session to check authentication + const session = await auth0.getSession(); - if (!clerkId) { + if (!session?.user) { return NextResponse.redirect(new URL(env.NEXT_PUBLIC_WEB_URL)); } + const auth0Id = session.user.sub; const user = await db.query.users.findFirst({ - where: eq(users.clerkId, clerkId), + where: eq(users.auth0Id, auth0Id), }); if (!user?.email.endsWith(COMPANY.EMAIL_DOMAIN)) { @@ -36,7 +47,7 @@ export default clerkMiddleware(async (auth, req) => { } return NextResponse.next(); -}); +} export const config = { matcher: [ diff --git a/apps/api/package.json b/apps/api/package.json index 752c87ef094..cc544d0b1d9 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -11,8 +11,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@clerk/backend": "^2.27.0", - "@clerk/nextjs": "^6.36.2", + "@superset/auth0": "workspace:*", "@superset/db": "workspace:*", "@superset/shared": "workspace:*", "@superset/trpc": "workspace:*", diff --git a/apps/api/src/app/api/webhooks/clerk/route.ts b/apps/api/src/app/api/webhooks/clerk/route.ts deleted file mode 100644 index 1fe958bec9d..00000000000 --- a/apps/api/src/app/api/webhooks/clerk/route.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { verifyWebhook } from "@clerk/backend/webhooks"; -import { db } from "@superset/db/client"; -import { users } from "@superset/db/schema"; -import { put } from "@vercel/blob"; -import { eq } from "drizzle-orm"; - -import { env } from "../../../../env"; - -async function uploadAvatar( - imageUrl: string | undefined, - userId: string, -): Promise { - if (!imageUrl) return null; - - try { - const response = await fetch(imageUrl); - if (!response.ok) return null; - - const blob = await response.blob(); - const { url } = await put(`users/${userId}/avatar.png`, blob, { - access: "public", - token: env.BLOB_READ_WRITE_TOKEN, - }); - return url; - } catch { - return null; - } -} - -export async function POST(req: Request) { - try { - const evt = await verifyWebhook(req, { - signingSecret: env.CLERK_WEBHOOK_SECRET, - }); - - if (evt.type === "user.created" || evt.type === "user.updated") { - const clerkUser = evt.data; - const primaryEmail = clerkUser.email_addresses.find( - (email) => email.id === clerkUser.primary_email_address_id, - )?.email_address; - - if (!primaryEmail) { - return new Response("No primary email", { status: 200 }); - } - - const name = - [clerkUser.first_name, clerkUser.last_name].filter(Boolean).join(" ") || - primaryEmail.split("@")[0] || - "User"; - - // Insert/update user first to get the internal UUID - const [user] = await db - .insert(users) - .values({ - clerkId: clerkUser.id, - email: primaryEmail, - name, - }) - .onConflictDoUpdate({ - target: users.clerkId, - set: { - email: primaryEmail, - name, - }, - }) - .returning({ id: users.id }); - - // Upload avatar using internal UUID, then update user - if (user) { - const avatarUrl = await uploadAvatar(clerkUser.image_url, user.id); - if (avatarUrl) { - await db - .update(users) - .set({ avatarUrl }) - .where(eq(users.id, user.id)); - } - } - } - - if (evt.type === "user.deleted" && evt.data.id) { - await db.delete(users).where(eq(users.clerkId, evt.data.id)); - } - - return new Response("Success", { status: 200 }); - } catch (err) { - console.error("Webhook verification failed:", err); - return new Response("Webhook verification failed", { status: 400 }); - } -} diff --git a/apps/api/src/env.ts b/apps/api/src/env.ts index c6119fcd638..3e7fb3bffa9 100644 --- a/apps/api/src/env.ts +++ b/apps/api/src/env.ts @@ -5,8 +5,8 @@ export const env = createEnv({ server: { DATABASE_URL: z.string(), DATABASE_URL_UNPOOLED: z.string(), - CLERK_SECRET_KEY: z.string(), - CLERK_WEBHOOK_SECRET: z.string(), + // Auth0 env vars are read directly by @auth0/nextjs-auth0: + // AUTH0_SECRET, AUTH0_BASE_URL, AUTH0_ISSUER_BASE_URL, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET BLOB_READ_WRITE_TOKEN: z.string(), }, client: { diff --git a/apps/api/src/proxy.ts b/apps/api/src/proxy.ts index 480353c355a..429cc26b8ca 100644 --- a/apps/api/src/proxy.ts +++ b/apps/api/src/proxy.ts @@ -1,4 +1,5 @@ -import { clerkMiddleware } from "@clerk/nextjs/server"; +import { auth0 } from "@superset/auth0/server"; +import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { env } from "./env"; @@ -16,7 +17,7 @@ function getCorsHeaders(origin: string | null) { }; } -export default clerkMiddleware(async (_auth, req) => { +export default async function middleware(req: NextRequest) { const origin = req.headers.get("origin"); const corsHeaders = getCorsHeaders(origin); @@ -25,13 +26,32 @@ export default clerkMiddleware(async (_auth, req) => { return new NextResponse(null, { status: 204, headers: corsHeaders }); } + // Run Auth0 middleware for auth routes + const authResponse = await auth0.middleware(req); + + // If Auth0 handled the request (auth routes), return its response with CORS + if ( + authResponse.status !== 200 || + req.nextUrl.pathname.startsWith("/api/auth") + ) { + // Clone headers and add CORS + const headers = new Headers(authResponse.headers); + for (const [key, value] of Object.entries(corsHeaders)) { + headers.set(key, value); + } + return new NextResponse(authResponse.body, { + status: authResponse.status, + headers, + }); + } + // Add CORS headers to all responses const response = NextResponse.next(); for (const [key, value] of Object.entries(corsHeaders)) { response.headers.set(key, value); } return response; -}); +} export const config = { matcher: [ diff --git a/apps/api/src/trpc/context.ts b/apps/api/src/trpc/context.ts index fc5a0c5e06a..b1812995d6a 100644 --- a/apps/api/src/trpc/context.ts +++ b/apps/api/src/trpc/context.ts @@ -1,7 +1,7 @@ -import { auth } from "@clerk/nextjs/server"; +import { getSession } from "@superset/auth0/server"; import { createTRPCContext } from "@superset/trpc"; export const createContext = async () => { - const session = await auth(); + const session = await getSession(); return createTRPCContext({ session }); }; diff --git a/apps/marketing/package.json b/apps/marketing/package.json index dff4a25fbb2..c9c70864552 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -11,9 +11,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@clerk/nextjs": "^6.36.2", "@react-three/drei": "^10.7.6", "@react-three/fiber": "^9.4.0", + "@superset/auth0": "workspace:*", "@superset/shared": "workspace:*", "@superset/ui": "workspace:*", "@t3-oss/env-nextjs": "^0.13.8", diff --git a/apps/marketing/src/app/components/CTAButtons/CTAButtons.tsx b/apps/marketing/src/app/components/CTAButtons/CTAButtons.tsx index 8fedcd6f515..565ae05621e 100644 --- a/apps/marketing/src/app/components/CTAButtons/CTAButtons.tsx +++ b/apps/marketing/src/app/components/CTAButtons/CTAButtons.tsx @@ -1,13 +1,13 @@ -import { auth } from "@clerk/nextjs/server"; +import { auth0 } from "@superset/auth0/server"; import { DOWNLOAD_URL_MAC_ARM64 } from "@superset/shared/constants"; import { Download } from "lucide-react"; import { env } from "@/env"; export async function CTAButtons() { - const { userId } = await auth(); + const session = await auth0.getSession(); - if (userId) { + if (session?.user) { return (

) { return ( - + - + ); } diff --git a/apps/marketing/src/env.ts b/apps/marketing/src/env.ts index 01e0458ce21..d5448f80963 100644 --- a/apps/marketing/src/env.ts +++ b/apps/marketing/src/env.ts @@ -10,21 +10,19 @@ export const env = createEnv({ .default("development"), }, server: { - CLERK_SECRET_KEY: z.string(), + AUTH0_SECRET: z.string(), + AUTH0_ISSUER_BASE_URL: z.string().url(), + AUTH0_CLIENT_ID: z.string(), + AUTH0_CLIENT_SECRET: z.string(), }, client: { NEXT_PUBLIC_API_URL: z.string().url(), NEXT_PUBLIC_WEB_URL: z.string().url(), - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string(), - NEXT_PUBLIC_COOKIE_DOMAIN: z.string(), }, experimental__runtimeEnv: { NODE_ENV: process.env.NODE_ENV, NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, NEXT_PUBLIC_WEB_URL: process.env.NEXT_PUBLIC_WEB_URL, - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: - process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, - NEXT_PUBLIC_COOKIE_DOMAIN: process.env.NEXT_PUBLIC_COOKIE_DOMAIN, }, skipValidation: !!process.env.SKIP_ENV_VALIDATION, }); diff --git a/apps/marketing/src/proxy.ts b/apps/marketing/src/proxy.ts index 29446eb2ba3..bf7eebd1ec1 100644 --- a/apps/marketing/src/proxy.ts +++ b/apps/marketing/src/proxy.ts @@ -1,6 +1,21 @@ -import { clerkMiddleware } from "@clerk/nextjs/server"; +import { auth0 } from "@superset/auth0/server"; +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; -export default clerkMiddleware(); +export default async function middleware(req: NextRequest) { + const { pathname } = req.nextUrl; + + // Run Auth0 middleware first (handles /api/auth/* routes) + const authResponse = await auth0.middleware(req); + + // If Auth0 handled the request, return its response + if (pathname.startsWith("/api/auth")) { + return authResponse; + } + + // Marketing site is public, just continue + return NextResponse.next(); +} export const config = { matcher: [ diff --git a/apps/web/package.json b/apps/web/package.json index 0009aa5f798..1dc930e5a53 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,7 +11,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@clerk/nextjs": "^6.36.2", + "@superset/auth0": "workspace:*", "@superset/db": "workspace:*", "@superset/queries": "workspace:*", "@superset/shared": "workspace:*", diff --git a/apps/web/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx b/apps/web/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx index a64aa38a558..93335a0b1e8 100644 --- a/apps/web/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +++ b/apps/web/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx @@ -1,38 +1,12 @@ "use client"; -import { useSignIn } from "@clerk/nextjs"; import { Button } from "@superset/ui/button"; import Image from "next/image"; import Link from "next/link"; -import { useState } from "react"; import { env } from "@/env"; export default function SignInPage() { - const { signIn, isLoaded } = useSignIn(); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const signInWithGoogle = async () => { - if (!isLoaded) return; - - setIsLoading(true); - setError(null); - - try { - await signIn.authenticateWithRedirect({ - strategy: "oauth_google", - redirectUrl: "/sso-callback", - redirectUrlComplete: "/", - }); - } catch (err) { - console.error("Sign in failed:", err); - setError("Failed to sign in. Please try again."); - } finally { - setIsLoading(false); - } - }; - return (
@@ -42,24 +16,18 @@ export default function SignInPage() {

- {error && ( -

{error}

- )} - +
+ +

By clicking continue, you agree to our{" "} (null); - - const signUpWithGoogle = async () => { - if (!isLoaded) return; - - setIsLoading(true); - setError(null); - - try { - await signUp.authenticateWithRedirect({ - strategy: "oauth_google", - redirectUrl: "/sso-callback", - redirectUrlComplete: "/", - }); - } catch (err) { - console.error("Sign up failed:", err); - setError("Failed to sign up. Please try again."); - } finally { - setIsLoading(false); - } - }; - return (

@@ -44,24 +18,18 @@ export default function SignUpPage() {

- {error && ( -

{error}

- )} - +
+ +

By clicking continue, you agree to our{" "} {children}; -} diff --git a/apps/web/src/app/(auth)/sso-callback/page.tsx b/apps/web/src/app/(auth)/sso-callback/page.tsx deleted file mode 100644 index 033b8ecc87f..00000000000 --- a/apps/web/src/app/(auth)/sso-callback/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { AuthenticateWithRedirectCallback } from "@clerk/nextjs"; - -export default function SSOCallbackPage() { - return ; -} diff --git a/apps/web/src/app/download/page.tsx b/apps/web/src/app/download/page.tsx index b535588835c..d7f87a0fb60 100644 --- a/apps/web/src/app/download/page.tsx +++ b/apps/web/src/app/download/page.tsx @@ -1,6 +1,5 @@ "use client"; -import { SignOutButton } from "@clerk/nextjs"; import { Button } from "@superset/ui/button"; import { Card, @@ -15,11 +14,11 @@ export default function DownloadPage() { return (

diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 4bbd4d7ecbd..9e36336bde2 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,11 +1,9 @@ -import { ClerkProvider } from "@clerk/nextjs"; +import { Auth0Provider } from "@superset/auth0/client"; import { Toaster } from "@superset/ui/sonner"; import { cn } from "@superset/ui/utils"; import type { Metadata, Viewport } from "next"; import { IBM_Plex_Mono, Inter } from "next/font/google"; -import { env } from "@/env"; - import "./globals.css"; import { Providers } from "./providers"; @@ -40,28 +38,21 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - - - + + + {children} - - - + + + ); } diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index fda413943be..636438ce89a 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { SignOutButton, useUser } from "@clerk/nextjs"; +import { useUser } from "@superset/auth0/client"; import { COMPANY, DOWNLOAD_URL_MAC_ARM64 } from "@superset/shared/constants"; import { getInitials } from "@superset/shared/names"; import { Avatar, AvatarFallback, AvatarImage } from "@superset/ui/avatar"; @@ -22,10 +22,10 @@ import { env } from "@/env"; export default function HomePage() { const { user } = useUser(); - const initials = getInitials( - user?.fullName, - user?.primaryEmailAddress?.emailAddress, - ); + const initials = getInitials(user?.name, user?.email); + + // Extract first name from full name + const firstName = user?.name?.split(" ")[0]; return (
@@ -45,11 +45,11 @@ export default function HomePage() { type="button" className="flex cursor-pointer items-center gap-2 rounded-md border bg-background px-3 py-1.5 shadow-xs outline-none transition-all hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:border-input dark:bg-input/30 dark:hover:bg-input/50" > - {user?.firstName && ( - Hello, {user.firstName}! + {firstName && ( + Hello, {firstName}! )} - + {initials} @@ -57,21 +57,19 @@ export default function HomePage() {
- {user?.fullName && ( -

{user.fullName}

+ {user?.name && ( +

{user.name}

)} -

- {user?.primaryEmailAddress?.emailAddress} -

+

{user?.email}

- + Sign out - +
diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts index d9db3a4f590..69a9b863197 100644 --- a/apps/web/src/env.ts +++ b/apps/web/src/env.ts @@ -13,15 +13,14 @@ export const env = createEnv({ server: { DATABASE_URL: z.string().url(), DATABASE_URL_UNPOOLED: z.string().url(), - CLERK_SECRET_KEY: z.string(), + // Auth0 env vars are read directly by @auth0/nextjs-auth0: + // AUTH0_SECRET, AUTH0_BASE_URL, AUTH0_ISSUER_BASE_URL, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET }, client: { NEXT_PUBLIC_API_URL: z.string().url(), NEXT_PUBLIC_MARKETING_URL: z.string().url(), NEXT_PUBLIC_DOCS_URL: z.string().url(), - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string(), - NEXT_PUBLIC_COOKIE_DOMAIN: z.string(), }, experimental__runtimeEnv: { @@ -29,9 +28,6 @@ export const env = createEnv({ NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, NEXT_PUBLIC_MARKETING_URL: process.env.NEXT_PUBLIC_MARKETING_URL, NEXT_PUBLIC_DOCS_URL: process.env.NEXT_PUBLIC_DOCS_URL, - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: - process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, - NEXT_PUBLIC_COOKIE_DOMAIN: process.env.NEXT_PUBLIC_COOKIE_DOMAIN, }, skipValidation: !!process.env.SKIP_ENV_VALIDATION, diff --git a/apps/web/src/proxy.ts b/apps/web/src/proxy.ts index 95f3f694348..e1ffeff1ec0 100644 --- a/apps/web/src/proxy.ts +++ b/apps/web/src/proxy.ts @@ -1,31 +1,45 @@ -import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; +import { auth0 } from "@superset/auth0/server"; +import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; -const isPublicRoute = createRouteMatcher([ - "/sign-in(.*)", - "/sign-up(.*)", - "/sso-callback(.*)", -]); +const publicPaths = ["/api/auth", "/sign-in", "/sign-up"]; -export default clerkMiddleware(async (auth, req) => { - const { userId } = await auth(); +function isPublicPath(pathname: string): boolean { + return publicPaths.some( + (path) => pathname === path || pathname.startsWith(`${path}/`), + ); +} + +export default async function middleware(req: NextRequest) { + const { pathname } = req.nextUrl; + + // Run Auth0 middleware first (handles /api/auth/* routes) + const authResponse = await auth0.middleware(req); + + // If Auth0 handled the request, return its response + if (pathname.startsWith("/api/auth")) { + return authResponse; + } + + // Get session to check authentication + const session = await auth0.getSession(); + const isAuthenticated = !!session?.user; // Redirect authenticated users away from auth pages if ( - userId && - (req.nextUrl.pathname.startsWith("/sign-in") || - req.nextUrl.pathname.startsWith("/sign-up")) + isAuthenticated && + (pathname.startsWith("/sign-in") || pathname.startsWith("/sign-up")) ) { return NextResponse.redirect(new URL("/", req.url)); } // Redirect unauthenticated users to sign-in - if (!userId && !isPublicRoute(req)) { + if (!isAuthenticated && !isPublicPath(pathname)) { return NextResponse.redirect(new URL("/sign-in", req.url)); } return NextResponse.next(); -}); +} export const config = { matcher: [ diff --git a/bun.lock b/bun.lock index d879954c47e..2f9ca230be5 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,7 @@ "name": "@superset/admin", "version": "0.1.0", "dependencies": { - "@clerk/nextjs": "^6.36.2", + "@superset/auth0": "workspace:*", "@superset/db": "workspace:*", "@superset/queries": "workspace:*", "@superset/shared": "workspace:*", @@ -51,8 +51,7 @@ "name": "@superset/api", "version": "0.1.0", "dependencies": { - "@clerk/backend": "^2.27.0", - "@clerk/nextjs": "^6.36.2", + "@superset/auth0": "workspace:*", "@superset/db": "workspace:*", "@superset/shared": "workspace:*", "@superset/trpc": "workspace:*", @@ -234,9 +233,9 @@ "name": "@superset/marketing", "version": "0.1.0", "dependencies": { - "@clerk/nextjs": "^6.36.2", "@react-three/drei": "^10.7.6", "@react-three/fiber": "^9.4.0", + "@superset/auth0": "workspace:*", "@superset/shared": "workspace:*", "@superset/ui": "workspace:*", "@t3-oss/env-nextjs": "^0.13.8", @@ -269,7 +268,7 @@ "name": "@superset/web", "version": "0.1.0", "dependencies": { - "@clerk/nextjs": "^6.36.2", + "@superset/auth0": "workspace:*", "@superset/db": "workspace:*", "@superset/queries": "workspace:*", "@superset/shared": "workspace:*", @@ -303,6 +302,17 @@ "typescript": "^5.9.3", }, }, + "packages/auth0": { + "name": "@superset/auth0", + "version": "0.1.0", + "dependencies": { + "@auth0/nextjs-auth0": "^4.13.1", + }, + "devDependencies": { + "@superset/typescript": "workspace:*", + "typescript": "^5.9.3", + }, + }, "packages/db": { "name": "@superset/db", "version": "0.1.0", @@ -354,7 +364,7 @@ "name": "@superset/trpc", "version": "0.1.0", "dependencies": { - "@clerk/backend": "^2.27.0", + "@superset/auth0": "workspace:*", "@superset/db": "workspace:*", "@superset/shared": "workspace:*", "@t3-oss/env-core": "^0.13.8", @@ -459,6 +469,8 @@ "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], + "@auth0/nextjs-auth0": ["@auth0/nextjs-auth0@4.13.3", "", { "dependencies": { "@edge-runtime/cookies": "^5.0.1", "@panva/hkdf": "^1.2.1", "jose": "^6.0.11", "oauth4webapi": "^3.8.2", "openid-client": "^6.8.0", "swr": "^2.2.5" }, "peerDependencies": { "next": "^14.2.35 || ~15.0.7 || ~15.1.11 || ~15.2.8 || ~15.3.8 || ~15.4.10 || ~15.5.9 || ^16.0.10", "react": "^18.0.0 || ~19.0.1 || ~19.1.2 || ^19.2.1", "react-dom": "^18.0.0 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-RG3wwOY/ALG4pQc1wCXdqaAkoIW4EEygcDnV+a4epm1GNlmai+Q9xQUAuDN89grBjN77+m2kXmI/W0mMpZ5IBA=="], + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], @@ -531,16 +543,6 @@ "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="], - "@clerk/backend": ["@clerk/backend@2.27.0", "", { "dependencies": { "@clerk/shared": "^3.39.0", "@clerk/types": "^4.101.6", "cookie": "1.0.2", "standardwebhooks": "^1.0.0", "tslib": "2.8.1" } }, "sha512-e3xdLV/dAp40eFg2eo5tPs/wF942WfJoQA0NLlKHUPYiBD1K+DD8WcX5Fv/kvXUlpoTGiMalexHz42rpx9TJBQ=="], - - "@clerk/clerk-react": ["@clerk/clerk-react@5.58.1", "", { "dependencies": { "@clerk/shared": "^3.39.0", "tslib": "2.8.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" } }, "sha512-jN6mfuqwZakm99CKRQlTahMZEa8qLOpr3Z4lG6XtyJfIcyADleOdRxuOXYoN9sV8ZhNfDDAs+eKjUeusIzPpbg=="], - - "@clerk/nextjs": ["@clerk/nextjs@6.36.2", "", { "dependencies": { "@clerk/backend": "^2.27.0", "@clerk/clerk-react": "^5.58.1", "@clerk/shared": "^3.39.0", "@clerk/types": "^4.101.6", "server-only": "0.0.1", "tslib": "2.8.1" }, "peerDependencies": { "next": "^13.5.7 || ^14.2.25 || ^15.2.3 || ^16", "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" } }, "sha512-1ovNi+Xxjq1ZGg8gp++5cp8FYiwkUX5OB5KH7t0bCFkvHTBu48Fdbucmw38AhEqwLnNg9St4GNSKZmyGvVwh6Q=="], - - "@clerk/shared": ["@clerk/shared@3.39.0", "", { "dependencies": { "csstype": "3.1.3", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", "std-env": "^3.9.0", "swr": "2.3.4" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-9kqqXGMPAdMQ7SXo5ZwUhbzbLLQeLp/1jdb8FQS5qlhmL0S0bAYKcyDjcmMB8xZPXNc7vZJRT72QHsGSlUAJxw=="], - - "@clerk/types": ["@clerk/types@4.101.6", "", { "dependencies": { "@clerk/shared": "^3.39.0" } }, "sha512-Ah6R65loy5Aq1jBpWo3x01IOlYJfgVn4LVAkSXKVQDtehn8w1bT2uJ7BTa2zH72A8F4K2HivkmMQa+v/37tLFA=="], - "@code-inspector/core": ["@code-inspector/core@1.3.2", "", { "dependencies": { "@vue/compiler-dom": "^3.5.13", "chalk": "^4.1.1", "dotenv": "^16.1.4", "launch-ide": "1.3.0", "portfinder": "^1.0.28" } }, "sha512-6bwCW7AbX/gYaoiZPFqkHAP5f/C0nXj29AixqBUNl1sRrsWFPHHfiaWH8WPmjx9IXI/nln109GvMiFHUpm2Mvg=="], "@code-inspector/esbuild": ["@code-inspector/esbuild@1.3.2", "", { "dependencies": { "@code-inspector/core": "1.3.2" } }, "sha512-AlcMO+pfwvh+m9L7bOeonTjgeJKaUsfEJxJIjyqUZMjk+F+ujhxFUKtZZU7RUvuQLr4e/flPdN+yxvWKfymr+g=="], @@ -569,6 +571,8 @@ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@edge-runtime/cookies": ["@edge-runtime/cookies@5.0.2", "", {}, "sha512-Sd8LcWpZk/SWEeKGE8LT6gMm5MGfX/wm+GPnh1eBEtCpya3vYqn37wYknwAHw92ONoyyREl1hJwxV/Qx2DWNOg=="], + "@electron/asar": ["@electron/asar@3.2.18", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg=="], "@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="], @@ -821,6 +825,8 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], @@ -1023,8 +1029,6 @@ "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], - "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], - "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], @@ -1033,6 +1037,8 @@ "@superset/api": ["@superset/api@workspace:apps/api"], + "@superset/auth0": ["@superset/auth0@workspace:packages/auth0"], + "@superset/cli": ["@superset/cli@workspace:apps/cli"], "@superset/db": ["@superset/db@workspace:packages/db"], @@ -1561,7 +1567,7 @@ "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], - "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], @@ -1849,8 +1855,6 @@ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "fault": ["fault@1.0.4", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA=="], @@ -1925,8 +1929,6 @@ "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], - "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], @@ -2125,9 +2127,9 @@ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], - "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], + "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -2469,6 +2471,8 @@ "npm-to-yarn": ["npm-to-yarn@3.0.1", "", {}, "sha512-tt6PvKu4WyzPwWUzy/hvPFqn+uwXO0K1ZHka8az3NnrhWJDmSqI8ncWq0fkL0k/lmmi5tAC11FXwXuh0rFbt1A=="], + "oauth4webapi": ["oauth4webapi@3.8.3", "", {}, "sha512-pQ5BsX3QRTgnt5HxgHwgunIRaDXBdkT23tf8dfzmtTIL2LTpdmxgbpbBm0VgFWAIDlezQvQCTgnVIUmHupXHxw=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], @@ -2487,6 +2491,8 @@ "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], + "openid-client": ["openid-client@6.8.1", "", { "dependencies": { "jose": "^6.1.0", "oauth4webapi": "^3.8.2" } }, "sha512-VoYT6enBo6Vj2j3Q5Ec0AezS+9YGzQo1f5Xc42lreMGlfP4ljiXPKVDvCADh+XHCV/bqPu/wWSiCVXbJKvrODw=="], + "opentype.js": ["opentype.js@0.8.0", "", { "dependencies": { "tiny-inflate": "^1.0.2" }, "bin": { "ot": "./bin/ot" } }, "sha512-FQHR4oGP+a0m/f6yHoRpBOIbn/5ZWxKd4D/djHVJu8+KpBTYrJda0b7mLcgDEMWXE9xBCJm+qb0yv6FcvPjukg=="], "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], @@ -2907,8 +2913,6 @@ "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], - "standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="], - "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], @@ -2919,8 +2923,6 @@ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], - "steno": ["steno@4.0.2", "", {}, "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A=="], "streamdown": ["streamdown@1.6.10", "", { "dependencies": { "clsx": "^2.1.1", "hast": "^1.0.0", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "katex": "^0.16.22", "lucide-react": "^0.542.0", "marked": "^16.2.1", "mermaid": "^11.11.0", "rehype-harden": "^1.1.6", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-cjk-friendly": "^1.2.3", "remark-cjk-friendly-gfm-strikethrough": "^1.2.3", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.0.1", "shiki": "^3.12.2", "tailwind-merge": "^3.3.1", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-B4Y3Z/qiXl1Dc+LzAB5c52Cd1QGRiFjaDwP+ERoj1JtCykdRDM8X6HwQnn3YkpkSk0x3R7S/6LrGe1nQiElHQQ=="], @@ -3213,8 +3215,6 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@clerk/shared/csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - "@code-inspector/core/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@code-inspector/core/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], @@ -3385,8 +3385,6 @@ "execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], "filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], diff --git a/packages/auth0/package.json b/packages/auth0/package.json new file mode 100644 index 00000000000..09dc48fe16a --- /dev/null +++ b/packages/auth0/package.json @@ -0,0 +1,31 @@ +{ + "name": "@superset/auth0", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + "./server": { + "types": "./src/server.ts", + "default": "./src/server.ts" + }, + "./client": { + "types": "./src/client.ts", + "default": "./src/client.ts" + }, + "./types": { + "types": "./src/types.ts", + "default": "./src/types.ts" + } + }, + "scripts": { + "clean": "git clean -xdf .cache .turbo dist node_modules", + "typecheck": "tsc --noEmit --emitDeclarationOnly false" + }, + "dependencies": { + "@auth0/nextjs-auth0": "^4.13.1" + }, + "devDependencies": { + "@superset/typescript": "workspace:*", + "typescript": "^5.9.3" + } +} diff --git a/packages/auth0/src/client.ts b/packages/auth0/src/client.ts new file mode 100644 index 00000000000..e5514ff4bf6 --- /dev/null +++ b/packages/auth0/src/client.ts @@ -0,0 +1,3 @@ +"use client"; + +export { Auth0Provider, useUser } from "@auth0/nextjs-auth0"; diff --git a/packages/auth0/src/server.ts b/packages/auth0/src/server.ts new file mode 100644 index 00000000000..8362c2de052 --- /dev/null +++ b/packages/auth0/src/server.ts @@ -0,0 +1,29 @@ +import { Auth0Client } from "@auth0/nextjs-auth0/server"; + +import type { AppSession } from "./types"; + +/** + * Auth0 client instance for server-side operations. + * + * Configuration is loaded from environment variables: + * - AUTH0_SECRET + * - AUTH0_BASE_URL + * - AUTH0_ISSUER_BASE_URL + * - AUTH0_CLIENT_ID + * - AUTH0_CLIENT_SECRET + */ +export const auth0 = new Auth0Client(); + +/** + * Get the current session from Auth0. + * Returns null if user is not authenticated. + */ +export async function getSession(): Promise { + try { + return await auth0.getSession(); + } catch { + return null; + } +} + +export { Auth0Client }; diff --git a/packages/auth0/src/types.ts b/packages/auth0/src/types.ts new file mode 100644 index 00000000000..21092d414b8 --- /dev/null +++ b/packages/auth0/src/types.ts @@ -0,0 +1,30 @@ +import type { auth0 } from "./server"; + +/** + * Session type inferred from Auth0 SDK + */ +type Auth0Session = Awaited>; + +/** + * Session type that can be null when user is not authenticated + */ +export type AppSession = Auth0Session; + +/** + * Session type when user is authenticated (non-null) + */ +export type SignedInSession = NonNullable; + +/** + * Type guard to check if session is authenticated + */ +export function isSignedIn(session: AppSession): session is SignedInSession { + return session?.user?.sub != null; +} + +/** + * Get the Auth0 user ID from a session + */ +export function getUserId(session: SignedInSession): string { + return session.user.sub; +} diff --git a/packages/auth0/tsconfig.json b/packages/auth0/tsconfig.json new file mode 100644 index 00000000000..840bf8c9aaf --- /dev/null +++ b/packages/auth0/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@superset/typescript/internal-package.json", + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/db/src/schema/schema.ts b/packages/db/src/schema/schema.ts index 90b3a09d9b6..a87bb6ae28a 100644 --- a/packages/db/src/schema/schema.ts +++ b/packages/db/src/schema/schema.ts @@ -16,7 +16,7 @@ export const users = pgTable( "users", { id: uuid().primaryKey().defaultRandom(), - clerkId: text("clerk_id").notNull().unique(), + auth0Id: text("auth0_id").notNull().unique(), name: text().notNull(), email: text().notNull().unique(), avatarUrl: text("avatar_url"), @@ -29,7 +29,7 @@ export const users = pgTable( }, (table) => [ index("users_email_idx").on(table.email), - index("users_clerk_id_idx").on(table.clerkId), + index("users_auth0_id_idx").on(table.auth0Id), index("users_deleted_at_idx").on(table.deletedAt), ], ); diff --git a/packages/trpc/package.json b/packages/trpc/package.json index bceef6e42bf..1a75855afa3 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -14,7 +14,7 @@ "typecheck": "tsc --noEmit --emitDeclarationOnly false" }, "dependencies": { - "@clerk/backend": "^2.27.0", + "@superset/auth0": "workspace:*", "@superset/db": "workspace:*", "@superset/shared": "workspace:*", "@t3-oss/env-core": "^0.13.8", diff --git a/packages/trpc/src/router/admin.ts b/packages/trpc/src/router/admin.ts index 7d615dcaba9..df081576d5e 100644 --- a/packages/trpc/src/router/admin.ts +++ b/packages/trpc/src/router/admin.ts @@ -43,11 +43,11 @@ export const adminRouter = { permanentlyDeleteUser: adminProcedure .input(z.object({ userId: z.string().uuid() })) .mutation(async () => { - // TODO: Implement Clerk user deletion, avatar cleanup, etc. + // TODO: Implement Auth0 user deletion, avatar cleanup, etc. throw new TRPCError({ code: "NOT_IMPLEMENTED", message: - "Permanent deletion not yet implemented - requires Clerk cleanup", + "Permanent deletion not yet implemented - requires Auth0 cleanup", }); }), } satisfies TRPCRouterRecord; diff --git a/packages/trpc/src/router/organization.ts b/packages/trpc/src/router/organization.ts index ef3731ea7ea..13f43c57514 100644 --- a/packages/trpc/src/router/organization.ts +++ b/packages/trpc/src/router/organization.ts @@ -58,7 +58,7 @@ export const organizationRouter = { ) .mutation(async ({ ctx, input }) => { const user = await db.query.users.findFirst({ - where: eq(users.clerkId, ctx.session.userId), + where: eq(users.auth0Id, ctx.session.user.sub), }); const [organization] = await db diff --git a/packages/trpc/src/router/task.ts b/packages/trpc/src/router/task.ts index d1d9d7c730a..7eeac02466a 100644 --- a/packages/trpc/src/router/task.ts +++ b/packages/trpc/src/router/task.ts @@ -73,7 +73,7 @@ export const taskRouter = { ) .mutation(async ({ ctx, input }) => { const user = await db.query.users.findFirst({ - where: eq(users.clerkId, ctx.session.userId), + where: eq(users.auth0Id, ctx.session.user.sub), }); if (!user) throw new Error("User not found"); diff --git a/packages/trpc/src/router/user.ts b/packages/trpc/src/router/user.ts index 46f704974ee..42d4e84e2ce 100644 --- a/packages/trpc/src/router/user.ts +++ b/packages/trpc/src/router/user.ts @@ -8,7 +8,7 @@ import { protectedProcedure } from "../trpc"; export const userRouter = { me: protectedProcedure.query(async ({ ctx }) => { return db.query.users.findFirst({ - where: eq(users.clerkId, ctx.session.userId), + where: eq(users.auth0Id, ctx.session.user.sub), }); }), } satisfies TRPCRouterRecord; diff --git a/packages/trpc/src/trpc.ts b/packages/trpc/src/trpc.ts index 3ef87c1600b..87019afecf1 100644 --- a/packages/trpc/src/trpc.ts +++ b/packages/trpc/src/trpc.ts @@ -1,4 +1,5 @@ -import type { SessionAuthObject } from "@clerk/backend"; +import type { AppSession, SignedInSession } from "@superset/auth0/types"; +import { isSignedIn } from "@superset/auth0/types"; import { db } from "@superset/db/client"; import { users } from "@superset/db/schema"; import { COMPANY } from "@superset/shared/constants"; @@ -7,29 +8,21 @@ import { eq } from "drizzle-orm"; import superjson from "superjson"; import { ZodError } from "zod"; -// SignedInAuthObject isn't exported from @clerk/backend main entry, -// so we extract it from the SessionAuthObject union -type SignedInAuthObject = Extract; - /** * tRPC Context * - * We use SessionAuthObject from @clerk/backend (not @clerk/nextjs) because: - * - The API is hosted on Next.js with clerkMiddleware handling auth - * - Expo/Desktop clients send Bearer tokens to this API - * - clerkMiddleware handles both cookie auth (web) and Bearer tokens (mobile/desktop) - * - @clerk/backend types work across all clients, while @clerk/nextjs would - * cause dependency issues in Expo/Desktop which don't have Next.js + * We use AppSession from @superset/auth0 which wraps Auth0's Session type. + * - AppSession = Session | null (null when not authenticated) + * - SignedInSession = Session with non-null user * - * SessionAuthObject = SignedInAuthObject | SignedOutAuthObject - * Public procedures may be called by unauthenticated users (SignedOutAuthObject) + * Public procedures may be called by unauthenticated users (null session) */ export type TRPCContext = { - session: SessionAuthObject; + session: AppSession; }; export const createTRPCContext = (opts: { - session: SessionAuthObject; + session: AppSession; }): TRPCContext => { return { session: opts.session }; }; @@ -55,7 +48,7 @@ export const createCallerFactory = t.createCallerFactory; export const publicProcedure = t.procedure; export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => { - if (!ctx.session.userId) { + if (!isSignedIn(ctx.session)) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Not authenticated. Please sign in.", @@ -64,16 +57,18 @@ export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => { return next({ ctx: { - // Cast needed because TypeScript doesn't propagate type narrowing through next() - // After the userId check above, we know session is SignedInAuthObject - session: ctx.session as SignedInAuthObject, + // After the isSignedIn check above, we know session is SignedInSession + session: ctx.session as SignedInSession, }, }); }); export const adminProcedure = protectedProcedure.use(async ({ ctx, next }) => { + // Auth0 user ID is in session.user.sub + const auth0Id = ctx.session.user.sub; + const user = await db.query.users.findFirst({ - where: eq(users.clerkId, ctx.session.userId), + where: eq(users.auth0Id, auth0Id), }); if (!user) { diff --git a/turbo.jsonc b/turbo.jsonc index 311ba4417ce..9ee4fa1f844 100644 --- a/turbo.jsonc +++ b/turbo.jsonc @@ -4,12 +4,12 @@ "globalEnv": [ "DATABASE_URL", "DATABASE_URL_UNPOOLED", - "CLERK_SECRET_KEY", - "CLERK_WEBHOOK_SECRET", + "AUTH0_SECRET", + "AUTH0_BASE_URL", + "AUTH0_ISSUER_BASE_URL", + "AUTH0_CLIENT_ID", + "AUTH0_CLIENT_SECRET", "BLOB_READ_WRITE_TOKEN", - "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", - "NEXT_PUBLIC_CLERK_SIGN_IN_URL", - "NEXT_PUBLIC_CLERK_SIGN_UP_URL", "NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_WEB_URL", "NEXT_PUBLIC_MARKETING_URL",