From 14120df29ef1ba54c39977fe42f9385a7c4b8585 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Mon, 5 Jan 2026 23:58:21 -0800 Subject: [PATCH 01/11] WIP --- .github/workflows/deploy-preview.yml | 30 +- .github/workflows/deploy-production.yml | 30 +- apps/admin/package.json | 3 +- .../AppSidebar/components/NavUser/NavUser.tsx | 28 +- .../LeaderboardTable/LeaderboardTable.tsx | 4 +- apps/admin/src/app/(dashboard)/layout.tsx | 19 +- .../components/UsersTable/UsersTable.tsx | 10 +- .../DeletedUsersTable/DeletedUsersTable.tsx | 296 ---- .../components/DeletedUsersTable/index.ts | 1 - .../app/(dashboard)/users/deleted/page.tsx | 16 - apps/admin/src/app/layout.tsx | 33 +- .../PostHogUserIdentifier.tsx | 12 +- apps/admin/src/env.ts | 4 - apps/admin/src/proxy.ts | 38 +- apps/admin/src/trpc/react.tsx | 12 +- apps/api/next.config.ts | 2 - apps/api/package.json | 4 +- apps/api/src/app/api/auth/[...all]/route.ts | 4 + .../src/app/api/auth/desktop/connect/route.ts | 45 + .../src/app/api/auth/desktop/github/route.ts | 207 --- .../src/app/api/auth/desktop/google/route.ts | 173 -- .../src/app/api/auth/desktop/refresh/route.ts | 42 - apps/api/src/app/api/auth/desktop/tokens.ts | 81 - .../src/app/api/electric/[...path]/route.ts | 42 +- .../src/app/api/electric/[...path]/utils.ts | 73 +- .../api/integrations/linear/connect/route.ts | 26 +- apps/api/src/env.ts | 2 - apps/api/src/lib/auth.ts | 61 - apps/api/src/proxy.ts | 17 +- apps/api/src/trpc/context.ts | 8 +- apps/marketing/package.json | 2 +- .../app/components/CTAButtons/CTAButtons.tsx | 7 +- apps/marketing/src/app/layout.tsx | 44 +- apps/marketing/src/env.ts | 6 - apps/marketing/src/proxy.ts | 10 - packages/auth/package.json | 34 + packages/auth/src/client.ts | 9 + packages/auth/src/env.ts | 27 + packages/auth/src/index.ts | 104 ++ packages/auth/tsconfig.json | 5 + .../db/drizzle/0005_back_up_user_tables.sql | 51 + .../drizzle/0006_add_better_auth_tables.sql | 179 ++ .../drizzle/0007_add_created_at_default.sql | 2 + packages/db/drizzle/meta/0005_snapshot.json | 1194 +++++++++++++ packages/db/drizzle/meta/0006_snapshot.json | 1523 ++++++++++++++++ packages/db/drizzle/meta/0007_snapshot.json | 1525 +++++++++++++++++ packages/db/package.json | 4 + packages/db/src/env.ts | 23 +- packages/db/src/schema/auth.ts | 147 ++ packages/db/src/schema/index.ts | 1 + packages/db/src/schema/relations.ts | 65 +- packages/db/src/schema/schema.ts | 78 +- packages/db/src/utils/sql.ts | 10 +- 53 files changed, 5115 insertions(+), 1258 deletions(-) delete mode 100644 apps/admin/src/app/(dashboard)/users/deleted/components/DeletedUsersTable/DeletedUsersTable.tsx delete mode 100644 apps/admin/src/app/(dashboard)/users/deleted/components/DeletedUsersTable/index.ts delete mode 100644 apps/admin/src/app/(dashboard)/users/deleted/page.tsx create mode 100644 apps/api/src/app/api/auth/[...all]/route.ts create mode 100644 apps/api/src/app/api/auth/desktop/connect/route.ts delete mode 100644 apps/api/src/app/api/auth/desktop/github/route.ts delete mode 100644 apps/api/src/app/api/auth/desktop/google/route.ts delete mode 100644 apps/api/src/app/api/auth/desktop/refresh/route.ts delete mode 100644 apps/api/src/app/api/auth/desktop/tokens.ts delete mode 100644 apps/api/src/lib/auth.ts delete mode 100644 apps/marketing/src/proxy.ts create mode 100644 packages/auth/package.json create mode 100644 packages/auth/src/client.ts create mode 100644 packages/auth/src/env.ts create mode 100644 packages/auth/src/index.ts create mode 100644 packages/auth/tsconfig.json create mode 100644 packages/db/drizzle/0005_back_up_user_tables.sql create mode 100644 packages/db/drizzle/0006_add_better_auth_tables.sql create mode 100644 packages/db/drizzle/0007_add_created_at_default.sql create mode 100644 packages/db/drizzle/meta/0005_snapshot.json create mode 100644 packages/db/drizzle/meta/0006_snapshot.json create mode 100644 packages/db/drizzle/meta/0007_snapshot.json create mode 100644 packages/db/src/schema/auth.ts diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index 3152585e6..f8350265f 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -170,10 +170,7 @@ jobs: NEXT_PUBLIC_API_URL: https://${{ env.API_ALIAS }} 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 }} - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} - DESKTOP_AUTH_SECRET: ${{ secrets.DESKTOP_AUTH_SECRET }} + BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }} GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} GH_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }} @@ -197,16 +194,13 @@ jobs: vercel pull --yes --environment=preview --token=$VERCEL_TOKEN vercel build --token=$VERCEL_TOKEN VERCEL_URL=$(vercel deploy --prebuilt --token=$VERCEL_TOKEN \ - --env CLERK_SECRET_KEY=$CLERK_SECRET_KEY \ - --env CLERK_WEBHOOK_SECRET=$CLERK_WEBHOOK_SECRET \ --env DATABASE_URL=$DATABASE_URL \ --env DATABASE_URL_UNPOOLED=$DATABASE_URL_UNPOOLED \ --env BLOB_READ_WRITE_TOKEN=$BLOB_READ_WRITE_TOKEN \ --env NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \ --env NEXT_PUBLIC_WEB_URL=$NEXT_PUBLIC_WEB_URL \ --env NEXT_PUBLIC_ADMIN_URL=$NEXT_PUBLIC_ADMIN_URL \ - --env NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY \ - --env DESKTOP_AUTH_SECRET=$DESKTOP_AUTH_SECRET \ + --env BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET \ --env GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID \ --env GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET \ --env GH_CLIENT_ID=$GH_CLIENT_ID \ @@ -292,12 +286,10 @@ jobs: NEXT_PUBLIC_WEB_URL: https://${{ env.WEB_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 }} + BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }} NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }} NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} - DESKTOP_AUTH_SECRET: ${{ secrets.DESKTOP_AUTH_SECRET }} NEXT_PUBLIC_SENTRY_DSN_WEB: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN_WEB }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} NEXT_PUBLIC_SENTRY_ENVIRONMENT: ${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} @@ -305,15 +297,13 @@ jobs: vercel pull --yes --environment=preview --token=$VERCEL_TOKEN vercel build --token=$VERCEL_TOKEN VERCEL_URL=$(vercel deploy --prebuilt --token=$VERCEL_TOKEN \ - --env CLERK_SECRET_KEY=$CLERK_SECRET_KEY \ --env DATABASE_URL=$DATABASE_URL \ --env DATABASE_URL_UNPOOLED=$DATABASE_URL_UNPOOLED \ - --env DESKTOP_AUTH_SECRET=$DESKTOP_AUTH_SECRET \ + --env BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET \ --env NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \ --env NEXT_PUBLIC_WEB_URL=$NEXT_PUBLIC_WEB_URL \ --env NEXT_PUBLIC_MARKETING_URL=$NEXT_PUBLIC_MARKETING_URL \ --env NEXT_PUBLIC_DOCS_URL=$NEXT_PUBLIC_DOCS_URL \ - --env NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY \ --env NEXT_PUBLIC_COOKIE_DOMAIN=$NEXT_PUBLIC_COOKIE_DOMAIN \ --env NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY \ --env NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST \ @@ -370,8 +360,7 @@ 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 }} + BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }} NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }} NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} @@ -382,10 +371,9 @@ jobs: vercel pull --yes --environment=preview --token=$VERCEL_TOKEN vercel build --token=$VERCEL_TOKEN VERCEL_URL=$(vercel deploy --prebuilt --token=$VERCEL_TOKEN \ - --env CLERK_SECRET_KEY=$CLERK_SECRET_KEY \ + --env BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET \ --env NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \ --env NEXT_PUBLIC_WEB_URL=$NEXT_PUBLIC_WEB_URL \ - --env NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY \ --env NEXT_PUBLIC_COOKIE_DOMAIN=$NEXT_PUBLIC_COOKIE_DOMAIN \ --env NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY \ --env NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST \ @@ -455,8 +443,7 @@ 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 }} + BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }} NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }} NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} @@ -469,12 +456,11 @@ jobs: vercel pull --yes --environment=preview --token=$VERCEL_TOKEN vercel build --token=$VERCEL_TOKEN VERCEL_URL=$(vercel deploy --prebuilt --token=$VERCEL_TOKEN \ - --env CLERK_SECRET_KEY=$CLERK_SECRET_KEY \ --env DATABASE_URL=$DATABASE_URL \ --env DATABASE_URL_UNPOOLED=$DATABASE_URL_UNPOOLED \ + --env BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET \ --env NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \ --env NEXT_PUBLIC_WEB_URL=$NEXT_PUBLIC_WEB_URL \ - --env NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY \ --env NEXT_PUBLIC_COOKIE_DOMAIN=$NEXT_PUBLIC_COOKIE_DOMAIN \ --env NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY \ --env NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST \ diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 47e542c92..150188a05 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -73,11 +73,8 @@ jobs: NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} 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 }} + BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }} BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }} - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} - DESKTOP_AUTH_SECRET: ${{ secrets.DESKTOP_AUTH_SECRET }} GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} GH_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }} @@ -101,16 +98,13 @@ jobs: vercel pull --yes --environment=production --token=$VERCEL_TOKEN vercel build --prod --token=$VERCEL_TOKEN vercel deploy --prod --prebuilt --token=$VERCEL_TOKEN \ - --env CLERK_SECRET_KEY=$CLERK_SECRET_KEY \ - --env CLERK_WEBHOOK_SECRET=$CLERK_WEBHOOK_SECRET \ --env DATABASE_URL=$DATABASE_URL \ --env DATABASE_URL_UNPOOLED=$DATABASE_URL_UNPOOLED \ --env BLOB_READ_WRITE_TOKEN=$BLOB_READ_WRITE_TOKEN \ --env NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \ --env NEXT_PUBLIC_WEB_URL=$NEXT_PUBLIC_WEB_URL \ --env NEXT_PUBLIC_ADMIN_URL=$NEXT_PUBLIC_ADMIN_URL \ - --env NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY \ - --env DESKTOP_AUTH_SECRET=$DESKTOP_AUTH_SECRET \ + --env BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET \ --env GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID \ --env GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET \ --env GH_CLIENT_ID=$GH_CLIENT_ID \ @@ -168,12 +162,10 @@ jobs: NEXT_PUBLIC_WEB_URL: ${{ secrets.NEXT_PUBLIC_WEB_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 }} + BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }} NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }} NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} - DESKTOP_AUTH_SECRET: ${{ secrets.DESKTOP_AUTH_SECRET }} NEXT_PUBLIC_SENTRY_DSN_WEB: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN_WEB }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} NEXT_PUBLIC_SENTRY_ENVIRONMENT: ${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} @@ -181,15 +173,13 @@ jobs: vercel pull --yes --environment=production --token=$VERCEL_TOKEN vercel build --prod --token=$VERCEL_TOKEN vercel deploy --prod --prebuilt --token=$VERCEL_TOKEN \ - --env CLERK_SECRET_KEY=$CLERK_SECRET_KEY \ --env DATABASE_URL=$DATABASE_URL \ --env DATABASE_URL_UNPOOLED=$DATABASE_URL_UNPOOLED \ - --env DESKTOP_AUTH_SECRET=$DESKTOP_AUTH_SECRET \ + --env BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET \ --env NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \ --env NEXT_PUBLIC_WEB_URL=$NEXT_PUBLIC_WEB_URL \ --env NEXT_PUBLIC_MARKETING_URL=$NEXT_PUBLIC_MARKETING_URL \ --env NEXT_PUBLIC_DOCS_URL=$NEXT_PUBLIC_DOCS_URL \ - --env NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY \ --env NEXT_PUBLIC_COOKIE_DOMAIN=$NEXT_PUBLIC_COOKIE_DOMAIN \ --env NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY \ --env NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST \ @@ -230,8 +220,7 @@ 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 }} + BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }} NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }} NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} @@ -242,10 +231,9 @@ jobs: vercel pull --yes --environment=production --token=$VERCEL_TOKEN vercel build --prod --token=$VERCEL_TOKEN vercel deploy --prod --prebuilt --token=$VERCEL_TOKEN \ - --env CLERK_SECRET_KEY=$CLERK_SECRET_KEY \ + --env BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET \ --env NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \ --env NEXT_PUBLIC_WEB_URL=$NEXT_PUBLIC_WEB_URL \ - --env NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY \ --env NEXT_PUBLIC_COOKIE_DOMAIN=$NEXT_PUBLIC_COOKIE_DOMAIN \ --env NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY \ --env NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST \ @@ -288,8 +276,7 @@ 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 }} + BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }} NEXT_PUBLIC_COOKIE_DOMAIN: ${{ secrets.NEXT_PUBLIC_COOKIE_DOMAIN }} NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} @@ -302,12 +289,11 @@ jobs: vercel pull --yes --environment=production --token=$VERCEL_TOKEN vercel build --prod --token=$VERCEL_TOKEN vercel deploy --prod --prebuilt --token=$VERCEL_TOKEN \ - --env CLERK_SECRET_KEY=$CLERK_SECRET_KEY \ --env DATABASE_URL=$DATABASE_URL \ --env DATABASE_URL_UNPOOLED=$DATABASE_URL_UNPOOLED \ + --env BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET \ --env NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \ --env NEXT_PUBLIC_WEB_URL=$NEXT_PUBLIC_WEB_URL \ - --env NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY \ --env NEXT_PUBLIC_COOKIE_DOMAIN=$NEXT_PUBLIC_COOKIE_DOMAIN \ --env NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY \ --env NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST \ diff --git a/apps/admin/package.json b/apps/admin/package.json index c22d46c49..51757481b 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -11,8 +11,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@clerk/nextjs": "^6.36.2", "@sentry/nextjs": "^10.32.1", + "@superset/auth": "workspace:*", "@superset/db": "workspace:*", "@superset/shared": "workspace:*", "@superset/trpc": "workspace:*", @@ -23,6 +23,7 @@ "@trpc/client": "^11.7.1", "@trpc/server": "^11.7.1", "@trpc/tanstack-react-query": "^11.7.1", + "better-auth": "^1.4.9", "date-fns": "^4.1.0", "drizzle-orm": "0.45.1", "import-in-the-middle": "2.0.1", 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 774cc5e09..83bb8abd3 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,6 @@ "use client"; -import { useClerk } from "@clerk/nextjs"; +import { authClient } from "@superset/auth/client"; import type { RouterOutputs } from "@superset/trpc"; import { Avatar, AvatarFallback, AvatarImage } from "@superset/ui/avatar"; import { @@ -25,7 +25,6 @@ import { LuLogOut, LuSettings, } from "react-icons/lu"; - import { env } from "@/env"; export interface NavUserProps { @@ -34,13 +33,22 @@ export interface NavUserProps { export function NavUser({ user }: NavUserProps) { const { isMobile } = useSidebar(); - const { signOut } = useClerk(); const userInitials = user.name .split(" ") .map((name) => name[0]) .join(""); + const handleSignOut = async () => { + await authClient.signOut({ + fetchOptions: { + onSuccess: () => { + window.location.href = env.NEXT_PUBLIC_WEB_URL; + }, + }, + }); + }; + return ( @@ -51,10 +59,7 @@ export function NavUser({ user }: NavUserProps) { className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" > - + {userInitials} @@ -75,10 +80,7 @@ export function NavUser({ user }: NavUserProps) {
- + {userInitials} @@ -105,9 +107,7 @@ export function NavUser({ user }: NavUserProps) { - signOut({ redirectUrl: env.NEXT_PUBLIC_WEB_URL })} - > + Log out diff --git a/apps/admin/src/app/(dashboard)/components/LeaderboardTable/LeaderboardTable.tsx b/apps/admin/src/app/(dashboard)/components/LeaderboardTable/LeaderboardTable.tsx index 6c7bd5b63..b24cba6e3 100644 --- a/apps/admin/src/app/(dashboard)/components/LeaderboardTable/LeaderboardTable.tsx +++ b/apps/admin/src/app/(dashboard)/components/LeaderboardTable/LeaderboardTable.tsx @@ -23,7 +23,7 @@ interface LeaderboardEntry { userId: string; name: string; email: string; - avatarUrl: string | null; + image: string | null; count: number; } @@ -97,7 +97,7 @@ export function LeaderboardTable({
- + {entry.name .split(" ") diff --git a/apps/admin/src/app/(dashboard)/layout.tsx b/apps/admin/src/app/(dashboard)/layout.tsx index cdf7dd2d5..e7b05fe6a 100644 --- a/apps/admin/src/app/(dashboard)/layout.tsx +++ b/apps/admin/src/app/(dashboard)/layout.tsx @@ -1,3 +1,5 @@ +import { auth } from "@superset/auth"; +import { COMPANY } from "@superset/shared/constants"; import { Breadcrumb, BreadcrumbItem, @@ -12,7 +14,10 @@ import { SidebarProvider, SidebarTrigger, } from "@superset/ui/sidebar"; +import { headers } from "next/headers"; +import { redirect } from "next/navigation"; +import { env } from "@/env"; import { api } from "@/trpc/server"; import { AppSidebar } from "./components/AppSidebar"; @@ -22,11 +27,23 @@ export default async function DashboardLayout({ }: { children: React.ReactNode; }) { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session?.user) { + redirect(env.NEXT_PUBLIC_WEB_URL); + } + + if (!session.user.email?.endsWith(COMPANY.EMAIL_DOMAIN)) { + redirect(env.NEXT_PUBLIC_WEB_URL); + } + const trpc = await api(); const user = await trpc.user.me.query(); if (!user) { - throw new Error("User not found"); + redirect(env.NEXT_PUBLIC_WEB_URL); } return ( diff --git a/apps/admin/src/app/(dashboard)/users/components/UsersTable/UsersTable.tsx b/apps/admin/src/app/(dashboard)/users/components/UsersTable/UsersTable.tsx index 3ccac15f6..0883614d7 100644 --- a/apps/admin/src/app/(dashboard)/users/components/UsersTable/UsersTable.tsx +++ b/apps/admin/src/app/(dashboard)/users/components/UsersTable/UsersTable.tsx @@ -45,7 +45,7 @@ export function UsersTable() { const trpc = useTRPC(); const queryClient = useQueryClient(); const { data, isLoading, error } = useQuery( - trpc.admin.listActiveUsers.queryOptions(), + trpc.admin.listUsers.queryOptions(), ); const [userToDelete, setUserToDelete] = useState<{ @@ -55,12 +55,12 @@ export function UsersTable() { } | null>(null); const deleteMutation = useMutation( - trpc.admin.permanentlyDeleteUser.mutationOptions({ + trpc.admin.deleteUser.mutationOptions({ onSuccess: () => { queryClient.invalidateQueries({ - queryKey: trpc.admin.listActiveUsers.queryKey(), + queryKey: trpc.admin.listUsers.queryKey(), }); - toast.success(`${userToDelete?.name} has been permanently deleted`); + toast.success(`${userToDelete?.name} has been deleted`); setUserToDelete(null); }, onError: (error) => { @@ -152,7 +152,7 @@ export function UsersTable() {
- + {user.name .split(" ") 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 deleted file mode 100644 index c137fbb1a..000000000 --- a/apps/admin/src/app/(dashboard)/users/deleted/components/DeletedUsersTable/DeletedUsersTable.tsx +++ /dev/null @@ -1,296 +0,0 @@ -"use client"; - -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@superset/ui/alert-dialog"; -import { Avatar, AvatarFallback, AvatarImage } from "@superset/ui/avatar"; -import { Button } from "@superset/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@superset/ui/card"; -import { toast } from "@superset/ui/sonner"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@superset/ui/table"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { formatDistanceToNow } from "date-fns"; -import { useState } from "react"; -import { LuLoaderCircle, LuRotateCcw, LuTrash2, LuUserX } from "react-icons/lu"; - -import { useTRPC } from "@/trpc/react"; - -export function DeletedUsersTable() { - const trpc = useTRPC(); - const queryClient = useQueryClient(); - const { data, isLoading, error } = useQuery( - trpc.admin.listDeletedUsers.queryOptions(), - ); - - const [userToDelete, setUserToDelete] = useState<{ - id: string; - email: string; - name: string; - } | null>(null); - - const restoreMutation = useMutation( - trpc.admin.restoreUser.mutationOptions({ - onSuccess: (_, _variables) => { - queryClient.invalidateQueries({ - queryKey: trpc.admin.listActiveUsers.queryKey(), - }); - queryClient.invalidateQueries({ - queryKey: trpc.admin.listDeletedUsers.queryKey(), - }); - toast.success("User restored successfully"); - }, - onError: (error) => { - toast.error(`Failed to restore user: ${error.message}`); - }, - }), - ); - - const permanentDeleteMutation = useMutation( - trpc.admin.permanentlyDeleteUser.mutationOptions({ - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: trpc.admin.listDeletedUsers.queryKey(), - }); - toast.success(`${userToDelete?.name} has been permanently deleted`); - setUserToDelete(null); - }, - onError: (error) => { - toast.error(`Failed to delete user: ${error.message}`); - }, - }), - ); - - const handlePermanentDelete = () => { - if (!userToDelete) return; - permanentDeleteMutation.mutate({ userId: userToDelete.id }); - }; - - if (isLoading) { - return ( - - - - - - ); - } - - if (error) { - return ( - - -
- -
-

Failed to load deleted users

-

- {error.message || "An error occurred while fetching deleted users"} -

-
-
- ); - } - - if (!data || data.length === 0) { - return ( - - - -

No deleted users

-

- Users that are soft-deleted will appear here -

-
-
- ); - } - - return ( - <> - - - Deleted Users - - {data.length} user{data.length !== 1 ? "s" : ""} queued for deletion - - - - - - - User - Email - Deleted - Actions - - - - {data.map((user) => { - const deletedAt = user.deletedAt - ? new Date(user.deletedAt) - : null; - const daysSinceDeleted = deletedAt - ? Math.floor( - (Date.now() - deletedAt.getTime()) / - (1000 * 60 * 60 * 24), - ) - : 0; - const isOverdue = daysSinceDeleted > 30; - - return ( - - -
- - - - {user.name - .split(" ") - .map((n) => n[0]) - .join("") - .toUpperCase() - .slice(0, 2)} - - - {user.name} -
-
- {user.email} - -
-
- {deletedAt - ? formatDistanceToNow(deletedAt, { - addSuffix: true, - }) - : "-"} -
- {deletedAt && ( -
- {daysSinceDeleted} day - {daysSinceDeleted !== 1 ? "s" : ""} ago - {isOverdue && " (overdue)"} -
- )} -
-
- -
- - -
-
-
- ); - })} -
-
-
-
- - !open && setUserToDelete(null)} - > - - - Permanently delete user? - -
-

- This will permanently delete{" "} - {userToDelete?.name} ({userToDelete?.email}) - and all their data including: -

-
    -
  • All user data
  • -
  • All associated records
  • -
  • Their Clerk account
  • -
-

- This action cannot be undone. -

-
-
-
- - Cancel - - {permanentDeleteMutation.isPending ? ( - - ) : null} - Delete Permanently - - -
-
- - ); -} diff --git a/apps/admin/src/app/(dashboard)/users/deleted/components/DeletedUsersTable/index.ts b/apps/admin/src/app/(dashboard)/users/deleted/components/DeletedUsersTable/index.ts deleted file mode 100644 index 3b5567553..000000000 --- a/apps/admin/src/app/(dashboard)/users/deleted/components/DeletedUsersTable/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DeletedUsersTable } from "./DeletedUsersTable"; diff --git a/apps/admin/src/app/(dashboard)/users/deleted/page.tsx b/apps/admin/src/app/(dashboard)/users/deleted/page.tsx deleted file mode 100644 index 9d188ff24..000000000 --- a/apps/admin/src/app/(dashboard)/users/deleted/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { DeletedUsersTable } from "./components/DeletedUsersTable"; - -export default function DeletedUsersPage() { - return ( -
-
-

Deleted Users

-

- Manage users queued for deletion. Users can be restored or permanently - deleted. -

-
- -
- ); -} diff --git a/apps/admin/src/app/layout.tsx b/apps/admin/src/app/layout.tsx index 2280d3658..5be8712d8 100644 --- a/apps/admin/src/app/layout.tsx +++ b/apps/admin/src/app/layout.tsx @@ -1,11 +1,8 @@ -import { ClerkProvider } from "@clerk/nextjs"; 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,21 +37,19 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - - - - - {children} - - - - - + + + + {children} + + + + ); } diff --git a/apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx b/apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx index 68f00c993..e70937d4b 100644 --- a/apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx +++ b/apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx @@ -1,27 +1,27 @@ "use client"; -import { useUser } from "@clerk/nextjs"; +import { authClient } from "@superset/auth/client"; import { useQuery } from "@tanstack/react-query"; import posthog from "posthog-js"; import { useEffect } from "react"; -import { useTRPC } from "../../trpc/react"; +import { useTRPC } from "@/trpc/react"; export function PostHogUserIdentifier() { - const { isSignedIn } = useUser(); + const { data: session } = authClient.useSession(); const trpc = useTRPC(); const { data: user } = useQuery({ ...trpc.user.me.queryOptions(), - enabled: isSignedIn, + enabled: !!session?.user, }); useEffect(() => { if (user) { posthog.identify(user.id, { email: user.email, name: user.name }); - } else if (isSignedIn === false) { + } else if (!session?.user) { posthog.reset(); } - }, [user, isSignedIn]); + }, [user, session?.user]); return null; } diff --git a/apps/admin/src/env.ts b/apps/admin/src/env.ts index ba90201c8..5dce7ad6f 100644 --- a/apps/admin/src/env.ts +++ b/apps/admin/src/env.ts @@ -13,14 +13,12 @@ export const env = createEnv({ server: { DATABASE_URL: z.string().url(), DATABASE_URL_UNPOOLED: z.string().url(), - CLERK_SECRET_KEY: z.string(), SENTRY_AUTH_TOKEN: z.string().optional(), }, 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(), NEXT_PUBLIC_POSTHOG_KEY: z.string(), NEXT_PUBLIC_POSTHOG_HOST: z.string().url(), @@ -34,8 +32,6 @@ export const env = createEnv({ 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, NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, diff --git a/apps/admin/src/proxy.ts b/apps/admin/src/proxy.ts index 087e49b26..566f2fc8a 100644 --- a/apps/admin/src/proxy.ts +++ b/apps/admin/src/proxy.ts @@ -1,47 +1,29 @@ -import { clerkMiddleware } from "@clerk/nextjs/server"; -import { db } from "@superset/db/client"; -import { users } from "@superset/db/schema"; +import { auth } from "@superset/auth"; import { COMPANY } from "@superset/shared/constants"; -import { eq } from "drizzle-orm"; +import { headers } from "next/headers"; import { NextResponse } from "next/server"; import { env } from "./env"; -const PUBLIC_ROUTES = ["/ingest", "/monitoring"]; - -function isPublicRoute(pathname: string): boolean { - return PUBLIC_ROUTES.some( - (route) => pathname === route || pathname.startsWith(`${route}/`), - ); -} - -export default clerkMiddleware(async (auth, req) => { - const { pathname } = req.nextUrl; - - if (isPublicRoute(pathname)) { - return NextResponse.next(); - } - - const { userId: clerkId } = await auth(); +export default async function proxy() { + const session = await auth.api.getSession({ + headers: await headers(), + }); - if (!clerkId) { + if (!session?.user) { return NextResponse.redirect(new URL(env.NEXT_PUBLIC_WEB_URL)); } - const user = await db.query.users.findFirst({ - where: eq(users.clerkId, clerkId), - }); - - if (!user?.email.endsWith(COMPANY.EMAIL_DOMAIN)) { + if (!session.user.email.endsWith(COMPANY.EMAIL_DOMAIN)) { return NextResponse.redirect(new URL(env.NEXT_PUBLIC_WEB_URL)); } return NextResponse.next(); -}); +} export const config = { matcher: [ - "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", + "/((?!_next|ingest|monitoring|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", "/(api|trpc)(.*)", ], }; diff --git a/apps/admin/src/trpc/react.tsx b/apps/admin/src/trpc/react.tsx index 95db3b924..b654b409f 100644 --- a/apps/admin/src/trpc/react.tsx +++ b/apps/admin/src/trpc/react.tsx @@ -1,6 +1,5 @@ "use client"; -import { useAuth } from "@clerk/nextjs"; import type { AppRouter } from "@superset/trpc"; import type { QueryClient } from "@tanstack/react-query"; import { QueryClientProvider } from "@tanstack/react-query"; @@ -33,7 +32,6 @@ export type UseTRPC = typeof useTRPC; export function TRPCReactProvider(props: { children: React.ReactNode }) { const queryClient = getQueryClient(); - const { getToken } = useAuth(); const [trpcClient] = useState(() => createTRPCClient({ @@ -46,13 +44,17 @@ export function TRPCReactProvider(props: { children: React.ReactNode }) { httpBatchStreamLink({ transformer: SuperJSON, url: `${env.NEXT_PUBLIC_API_URL}/api/trpc`, - async headers() { - const token = await getToken(); + headers() { return { "x-trpc-source": "nextjs-react", - ...(token ? { Authorization: `Bearer ${token}` } : {}), }; }, + fetch(url, options) { + return fetch(url, { + ...options, + credentials: "include", + }); + }, }), ], }), diff --git a/apps/api/next.config.ts b/apps/api/next.config.ts index 9943f4e41..8de5ba6fc 100644 --- a/apps/api/next.config.ts +++ b/apps/api/next.config.ts @@ -3,7 +3,6 @@ import { withSentryConfig } from "@sentry/nextjs"; import { config as dotenvConfig } from "dotenv"; import type { NextConfig } from "next"; -// Load .env from monorepo root during development if (process.env.NODE_ENV !== "production") { dotenvConfig({ path: join(process.cwd(), "../../.env"), override: true }); } @@ -11,7 +10,6 @@ if (process.env.NODE_ENV !== "production") { const config: NextConfig = { reactCompiler: true, typescript: { ignoreBuildErrors: true }, - // CORS is handled dynamically in the route handlers }; export default withSentryConfig(config, { diff --git a/apps/api/package.json b/apps/api/package.json index 62ccda6d7..023421e13 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -11,11 +11,10 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@clerk/backend": "^2.27.0", - "@clerk/nextjs": "^6.36.2", "@electric-sql/client": "^1.3.1", "@linear/sdk": "^68.1.0", "@sentry/nextjs": "^10.32.1", + "@superset/auth": "workspace:*", "@superset/db": "workspace:*", "@superset/shared": "workspace:*", "@superset/trpc": "workspace:*", @@ -23,6 +22,7 @@ "@trpc/server": "^11.7.1", "@upstash/qstash": "^2.8.4", "@vercel/blob": "^2.0.0", + "better-auth": "^1.4.9", "drizzle-orm": "0.45.1", "import-in-the-middle": "2.0.1", "jose": "^6.1.3", diff --git a/apps/api/src/app/api/auth/[...all]/route.ts b/apps/api/src/app/api/auth/[...all]/route.ts new file mode 100644 index 000000000..6b68abd83 --- /dev/null +++ b/apps/api/src/app/api/auth/[...all]/route.ts @@ -0,0 +1,4 @@ +import { auth } from "@superset/auth"; +import { toNextJsHandler } from "better-auth/next-js"; + +export const { GET, POST } = toNextJsHandler(auth); diff --git a/apps/api/src/app/api/auth/desktop/connect/route.ts b/apps/api/src/app/api/auth/desktop/connect/route.ts new file mode 100644 index 000000000..86deb03a7 --- /dev/null +++ b/apps/api/src/app/api/auth/desktop/connect/route.ts @@ -0,0 +1,45 @@ +import { auth } from "@superset/auth"; +import { NextResponse } from "next/server"; + +import { env } from "@/env"; + +export async function GET(request: Request) { + const url = new URL(request.url); + const provider = url.searchParams.get("provider"); + const state = url.searchParams.get("state"); + + if (!provider || !state) { + return new Response("Missing provider or state", { status: 400 }); + } + + if (provider !== "google" && provider !== "github") { + return new Response("Invalid provider", { status: 400 }); + } + + const successUrl = new URL(`${env.NEXT_PUBLIC_WEB_URL}/auth/desktop/success`); + successUrl.searchParams.set("desktop_state", state); + + const result = await auth.api.signInSocial({ + body: { + provider, + callbackURL: successUrl.toString(), + }, + asResponse: true, + }); + + const cookies = result.headers.getSetCookie(); + const body = (await result.json()) as { url?: string; redirect?: boolean }; + + if (!body.url) { + return new Response(`Failed to initiate OAuth: ${JSON.stringify(body)}`, { + status: 500, + }); + } + + const response = NextResponse.redirect(body.url); + for (const cookie of cookies) { + response.headers.append("set-cookie", cookie); + } + + return response; +} diff --git a/apps/api/src/app/api/auth/desktop/github/route.ts b/apps/api/src/app/api/auth/desktop/github/route.ts deleted file mode 100644 index 4f95b0968..000000000 --- a/apps/api/src/app/api/auth/desktop/github/route.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { clerkClient } from "@clerk/nextjs/server"; -import { env } from "@/env"; -import { generateTokens } from "../tokens"; - -/** - * GitHub OAuth token response - */ -interface GitHubTokenResponse { - access_token: string; - token_type: string; - scope: string; -} - -/** - * GitHub user response - */ -interface GitHubUser { - id: number; - login: string; - name: string | null; - email: string | null; - avatar_url: string; -} - -/** - * GitHub email response - */ -interface GitHubEmail { - email: string; - primary: boolean; - verified: boolean; - visibility: string | null; -} - -/** - * Exchange GitHub auth code for tokens and create desktop session - * - * POST /api/auth/desktop/github - * Body: { code: string, redirectUri: string } - * Returns: { accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt } - */ -export async function POST(request: Request) { - try { - const body = await request.json(); - const { code, redirectUri } = body as { - code: string; - redirectUri: string; - }; - - if (!code || !redirectUri) { - return Response.json( - { error: "Missing code or redirectUri" }, - { status: 400 }, - ); - } - - // Exchange code for access token with GitHub - const tokenResponse = await fetch( - "https://github.com/login/oauth/access_token", - { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify({ - client_id: env.GH_CLIENT_ID, - client_secret: env.GH_CLIENT_SECRET, - code, - redirect_uri: redirectUri, - }), - }, - ); - - if (!tokenResponse.ok) { - const errorData = await tokenResponse.json().catch(() => ({})); - console.error("[auth/github] Token exchange failed:", errorData); - return Response.json({ error: "Token exchange failed" }, { status: 400 }); - } - - const tokenData: GitHubTokenResponse = await tokenResponse.json(); - - if (!tokenData.access_token) { - console.error("[auth/github] No access token in response:", tokenData); - return Response.json( - { error: "No access token received" }, - { status: 400 }, - ); - } - - // Fetch user info from GitHub - const userResponse = await fetch("https://api.github.com/user", { - headers: { - Authorization: `Bearer ${tokenData.access_token}`, - Accept: "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - }, - }); - - if (!userResponse.ok) { - console.error("[auth/github] Failed to fetch user info"); - return Response.json( - { error: "Failed to fetch user info" }, - { status: 400 }, - ); - } - - const githubUser: GitHubUser = await userResponse.json(); - - // Always fetch verified email from /user/emails endpoint - // Never trust githubUser.email as it could be unverified - const emailsResponse = await fetch("https://api.github.com/user/emails", { - headers: { - Authorization: `Bearer ${tokenData.access_token}`, - Accept: "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - }, - }); - - if (!emailsResponse.ok) { - console.error("[auth/github] Failed to fetch user emails"); - return Response.json( - { error: "Failed to fetch user emails" }, - { status: 400 }, - ); - } - - const emails: GitHubEmail[] = await emailsResponse.json(); - // Only trust verified emails - prefer primary+verified, fallback to any verified - const primaryVerifiedEmail = emails.find((e) => e.primary && e.verified); - const anyVerifiedEmail = emails.find((e) => e.verified); - const email = - primaryVerifiedEmail?.email || anyVerifiedEmail?.email || null; - - if (!email) { - return Response.json( - { error: "No verified email found on GitHub account" }, - { status: 400 }, - ); - } - - // Parse name into first/last - const nameParts = (githubUser.name || "").split(" "); - const firstName = nameParts[0] || undefined; - const lastName = nameParts.slice(1).join(" ") || undefined; - - // Find or create user in Clerk - const clerk = await clerkClient(); - const existingUsers = await clerk.users.getUserList({ - emailAddress: [email], - }); - - let userId: string; - const existingUser = existingUsers.data[0]; - - if (existingUser) { - userId = existingUser.id; - console.log("[auth/github] Found existing user:", userId); - } else { - // Create new user - try { - const newUser = await clerk.users.createUser({ - emailAddress: [email], - firstName, - lastName, - skipPasswordRequirement: true, - }); - userId = newUser.id; - console.log("[auth/github] Created new user:", userId); - - // Mark the email as verified since GitHub already verified it - const emailId = newUser.emailAddresses[0]?.id; - if (emailId) { - await clerk.emailAddresses.updateEmailAddress(emailId, { - verified: true, - }); - console.log("[auth/github] Marked email as verified"); - } - } catch (clerkError: unknown) { - // Log and return detailed Clerk error - const errorDetails = - clerkError && typeof clerkError === "object" && "errors" in clerkError - ? (clerkError as { errors: unknown[] }).errors - : clerkError; - console.error( - "[auth/github] Clerk createUser failed:", - JSON.stringify(errorDetails, null, 2), - ); - return Response.json( - { - error: "Failed to create user account", - details: errorDetails, - }, - { status: 400 }, - ); - } - } - - // Generate access and refresh tokens - const tokens = await generateTokens(userId, email); - - return Response.json(tokens); - } catch (error) { - console.error("[auth/github] Error:", error); - return Response.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/apps/api/src/app/api/auth/desktop/google/route.ts b/apps/api/src/app/api/auth/desktop/google/route.ts deleted file mode 100644 index 622408b47..000000000 --- a/apps/api/src/app/api/auth/desktop/google/route.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { clerkClient } from "@clerk/nextjs/server"; -import { createRemoteJWKSet, jwtVerify } from "jose"; -import { env } from "@/env"; -import { generateTokens } from "../tokens"; - -/** - * Google OAuth token response - */ -interface GoogleTokenResponse { - access_token: string; - expires_in: number; - token_type: string; - scope: string; - id_token: string; - refresh_token?: string; -} - -/** - * Google ID token payload (verified) - */ -interface GoogleIdTokenPayload { - iss: string; - azp: string; - aud: string; - sub: string; - email: string; - email_verified: boolean; - name?: string; - picture?: string; - given_name?: string; - family_name?: string; - iat: number; - exp: number; -} - -// Google's JWKS endpoint - jose handles caching internally -const GOOGLE_JWKS = createRemoteJWKSet( - new URL("https://www.googleapis.com/oauth2/v3/certs"), -); - -/** - * Exchange Google auth code for tokens and create desktop session - * - * POST /api/auth/desktop/google - * Body: { code: string, redirectUri: string } - * Returns: { accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt } - */ -export async function POST(request: Request) { - try { - const body = await request.json(); - const { code, redirectUri } = body as { - code: string; - redirectUri: string; - }; - - if (!code || !redirectUri) { - return Response.json( - { error: "Missing code or redirectUri" }, - { status: 400 }, - ); - } - - // Exchange code for tokens with Google - const tokenResponse = await fetch("https://oauth2.googleapis.com/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - code, - client_id: env.GOOGLE_CLIENT_ID, - client_secret: env.GOOGLE_CLIENT_SECRET, - redirect_uri: redirectUri, - grant_type: "authorization_code", - }), - }); - - if (!tokenResponse.ok) { - const errorData = await tokenResponse.json().catch(() => ({})); - console.error("[auth/google] Token exchange failed:", errorData); - return Response.json( - { error: errorData.error_description || "Token exchange failed" }, - { status: 400 }, - ); - } - - const googleTokens: GoogleTokenResponse = await tokenResponse.json(); - - // Verify the ID token signature and claims using Google's JWKS - let payload: GoogleIdTokenPayload; - try { - const { payload: verifiedPayload } = await jwtVerify( - googleTokens.id_token, - GOOGLE_JWKS, - { - issuer: ["https://accounts.google.com", "accounts.google.com"], - audience: env.GOOGLE_CLIENT_ID, - }, - ); - payload = verifiedPayload as unknown as GoogleIdTokenPayload; - } catch (jwtError) { - console.error("[auth/google] JWT verification failed:", jwtError); - return Response.json( - { error: "Invalid or expired ID token" }, - { status: 401 }, - ); - } - - if (!payload.email_verified) { - return Response.json({ error: "Email not verified" }, { status: 400 }); - } - - // Find or create user in Clerk - const clerk = await clerkClient(); - const existingUsers = await clerk.users.getUserList({ - emailAddress: [payload.email], - }); - - let userId: string; - const existingUser = existingUsers.data[0]; - - if (existingUser) { - userId = existingUser.id; - console.log("[auth/google] Found existing user:", userId); - } else { - // Create new user - try { - const newUser = await clerk.users.createUser({ - emailAddress: [payload.email], - firstName: payload.given_name, - lastName: payload.family_name, - skipPasswordRequirement: true, - }); - userId = newUser.id; - console.log("[auth/google] Created new user:", userId); - - // Mark the email as verified since Google already verified it - const emailId = newUser.emailAddresses[0]?.id; - if (emailId) { - await clerk.emailAddresses.updateEmailAddress(emailId, { - verified: true, - }); - console.log("[auth/google] Marked email as verified"); - } - } catch (clerkError: unknown) { - // Log and return detailed Clerk error - const errorDetails = - clerkError && typeof clerkError === "object" && "errors" in clerkError - ? (clerkError as { errors: unknown[] }).errors - : clerkError; - console.error( - "[auth/google] Clerk createUser failed:", - JSON.stringify(errorDetails, null, 2), - ); - return Response.json( - { - error: "Failed to create user account", - details: errorDetails, - }, - { status: 400 }, - ); - } - } - - // Generate access and refresh tokens - const tokens = await generateTokens(userId, payload.email); - - return Response.json(tokens); - } catch (error) { - console.error("[auth/google] Error:", error); - return Response.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/apps/api/src/app/api/auth/desktop/refresh/route.ts b/apps/api/src/app/api/auth/desktop/refresh/route.ts deleted file mode 100644 index 808cc500a..000000000 --- a/apps/api/src/app/api/auth/desktop/refresh/route.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { generateTokens, verifyRefreshToken } from "../tokens"; - -/** - * Refresh access token using a valid refresh token - * - * POST /api/auth/desktop/refresh - * Body: { refreshToken: string } - * Returns: { accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt } - * - * This endpoint allows the desktop app to get new tokens without - * requiring the user to re-authenticate through Google OAuth. - */ -export async function POST(request: Request) { - try { - const body = await request.json(); - const { refreshToken } = body as { refreshToken: string }; - - if (!refreshToken) { - return Response.json({ error: "Missing refresh token" }, { status: 400 }); - } - - // Verify the refresh token - const tokenData = await verifyRefreshToken(refreshToken); - - if (!tokenData) { - return Response.json( - { error: "Invalid or expired refresh token" }, - { status: 401 }, - ); - } - - // Generate new tokens (rotate both access and refresh tokens) - const tokens = await generateTokens(tokenData.userId, tokenData.email); - - console.log("[auth/refresh] Tokens refreshed for user:", tokenData.userId); - - return Response.json(tokens); - } catch (error) { - console.error("[auth/refresh] Error:", error); - return Response.json({ error: "Internal server error" }, { status: 500 }); - } -} diff --git a/apps/api/src/app/api/auth/desktop/tokens.ts b/apps/api/src/app/api/auth/desktop/tokens.ts deleted file mode 100644 index f3aff4065..000000000 --- a/apps/api/src/app/api/auth/desktop/tokens.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { jwtVerify, SignJWT } from "jose"; -import { env } from "@/env"; - -// Token expiration times -export const ACCESS_TOKEN_EXPIRY = 60 * 60 * 1000; // 1 hour -export const REFRESH_TOKEN_EXPIRY = 30 * 24 * 60 * 60 * 1000; // 30 days - -/** - * Get the secret key for signing/verifying tokens - */ -export function getSecretKey(): Uint8Array { - return new TextEncoder().encode(env.DESKTOP_AUTH_SECRET); -} - -/** - * Generate access and refresh tokens for a user - */ -export async function generateTokens(userId: string, email: string) { - const secretKey = getSecretKey(); - const now = Date.now(); - const accessTokenExpiresAt = now + ACCESS_TOKEN_EXPIRY; - const refreshTokenExpiresAt = now + REFRESH_TOKEN_EXPIRY; - - // Access token - short-lived, used for API calls - const accessToken = await new SignJWT({ - sub: userId, - email, - type: "access", - }) - .setProtectedHeader({ alg: "HS256" }) - .setIssuedAt() - .setExpirationTime(Math.floor(accessTokenExpiresAt / 1000)) - .setIssuer("superset-desktop") - .sign(secretKey); - - // Refresh token - long-lived, used to get new access tokens - const refreshToken = await new SignJWT({ - sub: userId, - email, - type: "refresh", - }) - .setProtectedHeader({ alg: "HS256" }) - .setIssuedAt() - .setExpirationTime(Math.floor(refreshTokenExpiresAt / 1000)) - .setIssuer("superset-desktop") - .sign(secretKey); - - return { - accessToken, - accessTokenExpiresAt, - refreshToken, - refreshTokenExpiresAt, - }; -} - -/** - * Verify a refresh token and return its payload - */ -export async function verifyRefreshToken(token: string): Promise<{ - userId: string; - email: string; -} | null> { - try { - const secretKey = getSecretKey(); - const { payload } = await jwtVerify(token, secretKey, { - issuer: "superset-desktop", - }); - - // Ensure it's a refresh token - if (payload.type !== "refresh") { - return null; - } - - return { - userId: payload.sub as string, - email: payload.email as string, - }; - } catch { - return null; - } -} diff --git a/apps/api/src/app/api/electric/[...path]/route.ts b/apps/api/src/app/api/electric/[...path]/route.ts index 90c9cb64e..3617a72d1 100644 --- a/apps/api/src/app/api/electric/[...path]/route.ts +++ b/apps/api/src/app/api/electric/[...path]/route.ts @@ -1,46 +1,22 @@ import { ELECTRIC_PROTOCOL_QUERY_PARAMS } from "@electric-sql/client"; -import { db } from "@superset/db/client"; -import { organizationMembers, users } from "@superset/db/schema"; -import { and, eq } from "drizzle-orm"; +import { auth } from "@superset/auth"; import { env } from "@/env"; -import { authenticateRequest } from "@/lib/auth"; import { buildWhereClause } from "./utils"; -/** - * Electric SQL Proxy - * - * Forwards shape requests to Electric with organization-based filtering. - * @see https://electric-sql.com/docs/guides/auth#proxy-auth - */ export async function GET(request: Request): Promise { - const clerkUserId = await authenticateRequest(request); - if (!clerkUserId) { - return new Response("Unauthorized", { status: 401 }); - } - - const user = await db.query.users.findFirst({ - where: eq(users.clerkId, clerkUserId), + const sessionData = await auth.api.getSession({ + headers: request.headers, }); - if (!user) { - return new Response("User not found", { status: 401 }); + if (!sessionData?.user) { + return new Response("Unauthorized", { status: 401 }); } - const url = new URL(request.url); - const organizationId = url.searchParams.get("organizationId"); + const organizationId = sessionData.session.activeOrganizationId; if (!organizationId) { - return new Response("Missing organizationId parameter", { status: 400 }); - } - - const membership = await db.query.organizationMembers.findFirst({ - where: and( - eq(organizationMembers.userId, user.id), - eq(organizationMembers.organizationId, organizationId), - ), - }); - if (!membership) { - return new Response("Not a member of this organization", { status: 403 }); + return new Response("No active organization", { status: 400 }); } + const url = new URL(request.url); const originUrl = new URL(env.ELECTRIC_URL); originUrl.searchParams.set("secret", env.ELECTRIC_SECRET); @@ -68,8 +44,6 @@ export async function GET(request: Request): Promise { const response = await fetch(originUrl.toString()); - // Forward headers, but remove content-encoding/length per Electric docs - // (these can cause issues when proxying compressed responses) const headers = new Headers(); response.headers.forEach((value, key) => { const lower = key.toLowerCase(); diff --git a/apps/api/src/app/api/electric/[...path]/utils.ts b/apps/api/src/app/api/electric/[...path]/utils.ts index efd8ca2ff..ba2b1d05f 100644 --- a/apps/api/src/app/api/electric/[...path]/utils.ts +++ b/apps/api/src/app/api/electric/[...path]/utils.ts @@ -1,7 +1,8 @@ import { db } from "@superset/db/client"; import { - organizationMembers, + members, organizations, + repositories, tasks, users, } from "@superset/db/schema"; @@ -11,9 +12,10 @@ import { QueryBuilder } from "drizzle-orm/pg-core"; export type AllowedTable = | "tasks" - | "organization_members" - | "organizations" - | "users"; + | "repositories" + | "auth.members" + | "auth.organizations" + | "auth.users"; interface WhereClause { fragment: string; @@ -40,25 +42,62 @@ export async function buildWhereClause( case "tasks": return build(tasks, tasks.organizationId, organizationId); - case "organization_members": - return build( - organizationMembers, - organizationMembers.organizationId, - organizationId, - ); + case "repositories": + return build(repositories, repositories.organizationId, organizationId); + + case "auth.members": + return build(members, members.organizationId, organizationId); + + case "auth.organizations": { + const userMemberships = await db.query.members.findMany({ + where: eq(members.organizationId, organizationId), + columns: { userId: true }, + }); + + if (userMemberships.length === 0) { + return { fragment: "1 = 0", params: [] }; + } + + const userId = userMemberships[0]?.userId; + if (!userId) { + return { fragment: "1 = 0", params: [] }; + } + + const allUserMemberships = await db.query.members.findMany({ + where: eq(members.userId, userId), + columns: { organizationId: true }, + }); - case "organizations": - return build(organizations, organizations.id, organizationId); + if (allUserMemberships.length === 0) { + return { fragment: "1 = 0", params: [] }; + } + + const orgIds = [ + ...new Set(allUserMemberships.map((m) => m.organizationId)), + ]; + const whereExpr = inArray( + sql`${sql.identifier(organizations.id.name)}`, + orgIds, + ); + const qb = new QueryBuilder(); + const { sql: query, params } = qb + .select() + .from(organizations) + .where(whereExpr) + .toSQL(); + const fragment = query.replace(/^select .* from .* where\s+/i, ""); + return { fragment, params }; + } - case "users": { - const members = await db.query.organizationMembers.findMany({ - where: eq(organizationMembers.organizationId, organizationId), + case "auth.users": { + const orgMembers = await db.query.members.findMany({ + where: eq(members.organizationId, organizationId), columns: { userId: true }, }); - if (members.length === 0) { + if (orgMembers.length === 0) { return { fragment: "1 = 0", params: [] }; } - const userIds = [...new Set(members.map((m) => m.userId))]; + const userIds = [...new Set(orgMembers.map((m) => m.userId))]; const whereExpr = inArray(sql`${sql.identifier(users.id.name)}`, userIds); const qb = new QueryBuilder(); const { sql: query, params } = qb diff --git a/apps/api/src/app/api/integrations/linear/connect/route.ts b/apps/api/src/app/api/integrations/linear/connect/route.ts index 7a76e51a0..b359cb37c 100644 --- a/apps/api/src/app/api/integrations/linear/connect/route.ts +++ b/apps/api/src/app/api/integrations/linear/connect/route.ts @@ -1,13 +1,15 @@ -import { auth } from "@clerk/nextjs/server"; +import { auth } from "@superset/auth"; import { db } from "@superset/db/client"; -import { organizationMembers, users } from "@superset/db/schema"; +import { members } from "@superset/db/schema"; import { and, eq } from "drizzle-orm"; import { env } from "@/env"; export async function GET(request: Request) { - const { userId: clerkUserId } = await auth(); + const session = await auth.api.getSession({ + headers: request.headers, + }); - if (!clerkUserId) { + if (!session?.user) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } @@ -21,18 +23,10 @@ export async function GET(request: Request) { ); } - const user = await db.query.users.findFirst({ - where: eq(users.clerkId, clerkUserId), - }); - - if (!user) { - return Response.json({ error: "User not found" }, { status: 404 }); - } - - const membership = await db.query.organizationMembers.findFirst({ + const membership = await db.query.members.findFirst({ where: and( - eq(organizationMembers.organizationId, organizationId), - eq(organizationMembers.userId, user.id), + eq(members.organizationId, organizationId), + eq(members.userId, session.user.id), ), }); @@ -44,7 +38,7 @@ export async function GET(request: Request) { } const state = Buffer.from( - JSON.stringify({ organizationId, userId: user.id }), + JSON.stringify({ organizationId, userId: session.user.id }), ).toString("base64url"); const linearAuthUrl = new URL("https://linear.app/oauth/authorize"); diff --git a/apps/api/src/env.ts b/apps/api/src/env.ts index a1cfec766..710ea1a1b 100644 --- a/apps/api/src/env.ts +++ b/apps/api/src/env.ts @@ -10,11 +10,9 @@ export const env = createEnv({ server: { DATABASE_URL: z.string(), DATABASE_URL_UNPOOLED: z.string(), - CLERK_SECRET_KEY: z.string(), ELECTRIC_URL: z.string().url(), ELECTRIC_SECRET: z.string().min(16), BLOB_READ_WRITE_TOKEN: z.string(), - DESKTOP_AUTH_SECRET: z.string().min(32), GOOGLE_CLIENT_ID: z.string().min(1), GOOGLE_CLIENT_SECRET: z.string().min(1), GH_CLIENT_ID: z.string().min(1), diff --git a/apps/api/src/lib/auth.ts b/apps/api/src/lib/auth.ts deleted file mode 100644 index e883f106c..000000000 --- a/apps/api/src/lib/auth.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { clerkClient } from "@clerk/nextjs/server"; -import { jwtVerify } from "jose"; -import { env } from "@/env"; - -/** - * Verify desktop JWT access token - * Only accepts access tokens (type: "access"), not refresh tokens - */ -async function verifyDesktopToken(token: string): Promise { - try { - const secretKey = new TextEncoder().encode(env.DESKTOP_AUTH_SECRET); - const { payload } = await jwtVerify(token, secretKey, { - issuer: "superset-desktop", - }); - - if (payload.type !== "access") { - return null; - } - - if (typeof payload.sub !== "string") { - return null; - } - - return payload.sub; - } catch { - return null; - } -} - -/** - * Authenticate a request and return the Clerk user ID - * - * Supports: - * 1. Clerk session token (from web app) - * 2. Desktop JWT token (from desktop app) - * - * Returns null if not authenticated. - */ -export async function authenticateRequest( - request: Request, -): Promise { - // Try Clerk auth first - const client = await clerkClient(); - const { isAuthenticated, toAuth } = await client.authenticateRequest(request); - - if (isAuthenticated) { - const auth = toAuth(); - if (auth.userId) { - return auth.userId; - } - } - - // Fall back to desktop JWT - const authHeader = request.headers.get("authorization"); - if (authHeader?.startsWith("Bearer ")) { - const token = authHeader.slice(7); - return verifyDesktopToken(token); - } - - return null; -} diff --git a/apps/api/src/proxy.ts b/apps/api/src/proxy.ts index 9d58e4e26..11ccda86f 100644 --- a/apps/api/src/proxy.ts +++ b/apps/api/src/proxy.ts @@ -1,5 +1,4 @@ -import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; -import { NextResponse } from "next/server"; +import { type NextRequest, NextResponse } from "next/server"; import { env } from "./env"; @@ -8,7 +7,6 @@ const allowedOrigins = [ env.NEXT_PUBLIC_ADMIN_URL, env.NODE_ENV === "development" && "http://localhost:5927", ].filter(Boolean); -const isPublicRoute = createRouteMatcher(["/ingest(.*)", "/monitoring(.*)"]); function getCorsHeaders(origin: string | null) { const isAllowed = origin && allowedOrigins.includes(origin); @@ -21,12 +19,7 @@ function getCorsHeaders(origin: string | null) { }; } -export default clerkMiddleware(async (_auth, req) => { - // Allow Sentry and PostHog routes without CORS processing - if (isPublicRoute(req)) { - return NextResponse.next(); - } - +export default function proxy(req: NextRequest) { const origin = req.headers.get("origin"); const corsHeaders = getCorsHeaders(origin); @@ -41,13 +34,11 @@ export default clerkMiddleware(async (_auth, req) => { response.headers.set(key, value); } return response; -}); +} export const config = { matcher: [ - // Skip Next.js internals and static files - "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", - // Always run for API routes + "/((?!_next|ingest|monitoring|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", "/(api|trpc)(.*)", ], }; diff --git a/apps/api/src/trpc/context.ts b/apps/api/src/trpc/context.ts index 98bc13e3b..ccafe79f2 100644 --- a/apps/api/src/trpc/context.ts +++ b/apps/api/src/trpc/context.ts @@ -1,5 +1,5 @@ +import { auth } from "@superset/auth"; import { createTRPCContext } from "@superset/trpc"; -import { authenticateRequest } from "@/lib/auth"; export const createContext = async ({ req, @@ -7,6 +7,8 @@ export const createContext = async ({ req: Request; resHeaders: Headers; }) => { - const userId = await authenticateRequest(req); - return createTRPCContext({ userId }); + const session = await auth.api.getSession({ + headers: req.headers, + }); + return createTRPCContext({ session }); }; diff --git a/apps/marketing/package.json b/apps/marketing/package.json index 52fab968e..af17cd237 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -11,10 +11,10 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@clerk/nextjs": "^6.36.2", "@react-three/drei": "^10.7.6", "@react-three/fiber": "^9.4.0", "@sentry/nextjs": "^10.32.1", + "@superset/auth": "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 53f41fa7d..2c0c76855 100644 --- a/apps/marketing/src/app/components/CTAButtons/CTAButtons.tsx +++ b/apps/marketing/src/app/components/CTAButtons/CTAButtons.tsx @@ -1,13 +1,14 @@ -import { auth } from "@clerk/nextjs/server"; +import { auth } from "@superset/auth"; import { DOWNLOAD_URL_MAC_ARM64 } from "@superset/shared/constants"; import { Download } from "lucide-react"; +import { headers } from "next/headers"; import { env } from "@/env"; export async function CTAButtons() { - const { userId } = await auth(); + const session = await auth.api.getSession({ headers: await headers() }); - if (userId) { + if (session) { return ( <> ) { return ( - - - -