Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .github/workflows/deploy-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,11 @@ jobs:
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_API_PROJECT_ID }}
DATABASE_URL: ${{ env.DATABASE_URL }}
DATABASE_URL_UNPOOLED: ${{ env.DATABASE_URL_UNPOOLED }}
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 }}
MOCK_USER_ID: ${{ secrets.MOCK_USER_ID }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
CLERK_WEBHOOK_SECRET: ${{ secrets.CLERK_WEBHOOK_SECRET }}
Comment on lines +124 to +125
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use separate Clerk environment for preview deployments.

Preview deployments are currently using the same Clerk secrets as production (CLERK_SECRET_KEY, CLERK_WEBHOOK_SECRET, etc.). This creates several operational and security risks:

  1. Data isolation: User authentication and actions in preview environments could affect production Clerk data
  2. Cookie conflicts: NEXT_PUBLIC_COOKIE_DOMAIN may cause conflicts between preview and production environments
  3. Webhook confusion: Webhook events from preview deployments could trigger production workflows

Recommendation: Create a separate Clerk project for preview/development environments and configure separate secrets (e.g., CLERK_SECRET_KEY_PREVIEW, CLERK_WEBHOOK_SECRET_PREVIEW, etc.).

Also applies to: 195-197, 252-254, 323-325

🤖 Prompt for AI Agents
.github/workflows/deploy-preview.yml around lines 123-124 (also apply similar
changes at 195-197, 252-254, 323-325): workflow is currently injecting
production Clerk secrets into preview deploys; update the workflow to use
separate preview-specific secrets (for example CLERK_SECRET_KEY_PREVIEW,
CLERK_WEBHOOK_SECRET_PREVIEW, and any NEXT_PUBLIC_* preview vars) instead of the
production secrets, and ensure any cookie/domain env vars point to preview
domain; modify each env mapping to reference the new preview secret names and
add a note or conditional to fall back to production only for non-preview jobs.

run: |
vercel pull --yes --environment=preview --token=$VERCEL_TOKEN
vercel build --token=$VERCEL_TOKEN
Expand Down Expand Up @@ -191,7 +193,9 @@ 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 }}
MOCK_USER_ID: ${{ secrets.MOCK_USER_ID }}
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 }}
run: |
vercel pull --yes --environment=preview --token=$VERCEL_TOKEN
vercel build --token=$VERCEL_TOKEN
Expand Down Expand Up @@ -246,6 +250,10 @@ jobs:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
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 }}
run: |
vercel pull --yes --environment=preview --token=$VERCEL_TOKEN
vercel build --token=$VERCEL_TOKEN
Expand Down Expand Up @@ -314,7 +322,9 @@ jobs:
DATABASE_URL_UNPOOLED: ${{ env.DATABASE_URL_UNPOOLED }}
NEXT_PUBLIC_API_URL: https://${{ env.API_ALIAS }}
NEXT_PUBLIC_WEB_URL: https://${{ env.WEB_ALIAS }}
MOCK_USER_ID: ${{ secrets.MOCK_USER_ID }}
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 }}
run: |
vercel pull --yes --environment=preview --token=$VERCEL_TOKEN
vercel build --token=$VERCEL_TOKEN
Expand Down
14 changes: 11 additions & 3 deletions .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ 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 }}
MOCK_USER_ID: ${{ secrets.MOCK_USER_ID }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
CLERK_WEBHOOK_SECRET: ${{ secrets.CLERK_WEBHOOK_SECRET }}
run: |
vercel pull --yes --environment=production --token=$VERCEL_TOKEN
vercel build --prod --token=$VERCEL_TOKEN
Expand Down Expand Up @@ -115,7 +116,9 @@ 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 }}
MOCK_USER_ID: ${{ secrets.MOCK_USER_ID }}
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 }}
run: |
vercel pull --yes --environment=production --token=$VERCEL_TOKEN
vercel build --prod --token=$VERCEL_TOKEN
Expand Down Expand Up @@ -154,6 +157,9 @@ jobs:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_MARKETING_PROJECT_ID }}
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_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 }}
run: |
vercel pull --yes --environment=production --token=$VERCEL_TOKEN
vercel build --prod --token=$VERCEL_TOKEN
Expand Down Expand Up @@ -195,7 +201,9 @@ 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 }}
MOCK_USER_ID: ${{ secrets.MOCK_USER_ID }}
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 }}
run: |
vercel pull --yes --environment=production --token=$VERCEL_TOKEN
vercel build --prod --token=$VERCEL_TOKEN
Expand Down
5 changes: 3 additions & 2 deletions apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@clerk/nextjs": "^6.36.2",
"@superset/db": "workspace:*",
"@superset/queries": "workspace:*",
"@superset/shared": "workspace:*",
Expand All @@ -22,12 +23,12 @@
"@trpc/client": "^11.7.1",
"@trpc/server": "^11.7.1",
"@trpc/tanstack-react-query": "^11.7.1",
"geist": "^1.5.1",
"lucide-react": "^0.560.0",
"date-fns": "^4.1.0",
"next": "^16.0.10",
"next-themes": "^0.4.6",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-icons": "^5.5.0",
"server-only": "^0.0.1",
"superjson": "^2.2.5",
"zod": "^4.1.13"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import type { RouterOutputs } from "@superset/trpc";
import {
Collapsible,
CollapsibleContent,
Expand All @@ -18,19 +19,8 @@ import {
SidebarMenuItem,
SidebarRail,
} from "@superset/ui/sidebar";
import {
BarChart3,
Bot,
ChevronRight,
Database,
Home,
Settings,
Shield,
Users,
Webhook,
} from "lucide-react";
import { LuChevronRight, LuHouse, LuUsers } from "react-icons/lu";

import type { User } from "@/lib/auth/types";
import { AppSidebarHeader } from "./components/AppSidebarHeader";
import { NavUser } from "./components/NavUser";
import { SearchForm } from "./components/SearchForm";
Expand All @@ -42,7 +32,7 @@ const navigation = [
{
title: "Dashboard",
url: "/",
icon: Home,
icon: LuHouse,
},
],
},
Expand All @@ -52,75 +42,18 @@ const navigation = [
{
title: "All Users",
url: "/users",
icon: Users,
icon: LuUsers,
},
{
title: "Deleted Users",
url: "/users/deleted",
},
{
title: "Permissions",
url: "/users/permissions",
icon: Shield,
},
],
},
{
title: "Analytics",
items: [
{
title: "Overview",
url: "/analytics",
icon: BarChart3,
},
{
title: "User Activity",
url: "/analytics/activity",
},
{
title: "Performance",
url: "/analytics/performance",
},
],
},
{
title: "AI Lab",
items: [
{
title: "Plan Testing",
url: "/ai-lab",
icon: Bot,
},
{
title: "Model Config",
url: "/ai-lab/models",
},
],
},
{
title: "System",
items: [
{
title: "Database",
url: "/system/database",
icon: Database,
},
{
title: "Webhooks",
url: "/system/webhooks",
icon: Webhook,
},
{
title: "Settings",
url: "/settings",
icon: Settings,
},
],
},
];

export interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
user: User;
user: NonNullable<RouterOutputs["user"]["me"]>;
}

export function AppSidebar({ user, ...props }: AppSidebarProps) {
Expand All @@ -145,7 +78,7 @@ export function AppSidebar({ user, ...props }: AppSidebarProps) {
>
<CollapsibleTrigger>
{section.title}
<ChevronRight className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" />
<LuChevronRight className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" />
</CollapsibleTrigger>
</SidebarGroupLabel>
<CollapsibleContent>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use client";

import { useClerk } from "@clerk/nextjs";
import type { RouterOutputs } from "@superset/trpc";
import { Avatar, AvatarFallback, AvatarImage } from "@superset/ui/avatar";
import {
DropdownMenu,
Expand All @@ -17,22 +19,22 @@ import {
useSidebar,
} from "@superset/ui/sidebar";
import {
BadgeCheck,
Bell,
ChevronsUpDown,
LogOut,
Settings,
} from "lucide-react";
import { useSignOut } from "@/lib/auth/client";
import type { User } from "@/lib/auth/types";
LuBadgeCheck,
LuBell,
LuChevronsUpDown,
LuLogOut,
LuSettings,
} from "react-icons/lu";

import { env } from "@/env";

export interface NavUserProps {
user: User;
user: NonNullable<RouterOutputs["user"]["me"]>;
}

export function NavUser({ user }: NavUserProps) {
const { isMobile } = useSidebar();
const { signOut } = useSignOut();
const { signOut } = useClerk();

const userInitials = user.name
.split(" ")
Expand All @@ -49,7 +51,10 @@ export function NavUser({ user }: NavUserProps) {
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.imageUrl} alt={user.name} />
<AvatarImage
src={user.avatarUrl ?? undefined}
alt={user.name}
/>
<AvatarFallback className="rounded-lg">
{userInitials}
</AvatarFallback>
Expand All @@ -58,7 +63,7 @@ export function NavUser({ user }: NavUserProps) {
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
<LuChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
Expand All @@ -70,7 +75,10 @@ export function NavUser({ user }: NavUserProps) {
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.imageUrl} alt={user.name} />
<AvatarImage
src={user.avatarUrl ?? undefined}
alt={user.name}
/>
<AvatarFallback className="rounded-lg">
{userInitials}
</AvatarFallback>
Expand All @@ -84,21 +92,23 @@ export function NavUser({ user }: NavUserProps) {
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheck />
<LuBadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<Settings />
<LuSettings />
Settings
</DropdownMenuItem>
<DropdownMenuItem>
<Bell />
<LuBell />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={signOut}>
<LogOut />
<DropdownMenuItem
onClick={() => signOut({ redirectUrl: env.NEXT_PUBLIC_WEB_URL })}
>
<LuLogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
SidebarGroupContent,
SidebarInput,
} from "@superset/ui/sidebar";
import { Search } from "lucide-react";
import { LuSearch } from "react-icons/lu";

export function SearchForm({ ...props }: React.ComponentProps<"form">) {
return (
Expand All @@ -17,7 +17,7 @@ export function SearchForm({ ...props }: React.ComponentProps<"form">) {
Search
</Label>
<SidebarInput id="search" placeholder="Search..." className="pl-8" />
<Search className="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none" />
<LuSearch className="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none" />
</SidebarGroupContent>
</SidebarGroup>
</form>
Expand Down
10 changes: 4 additions & 6 deletions apps/admin/src/app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ import {
SidebarProvider,
SidebarTrigger,
} from "@superset/ui/sidebar";
import { redirect } from "next/navigation";

import { env } from "@/env";
import { currentUser } from "@/lib/auth/server";
import { api } from "@/trpc/server";

import { AppSidebar } from "./components/AppSidebar";

Expand All @@ -24,11 +22,11 @@ export default async function DashboardLayout({
}: {
children: React.ReactNode;
}) {
const user = await currentUser();
const trpc = await api();
const user = await trpc.user.me.query();

// Redirect unauthorized users to web app
if (!user) {
redirect(env.NEXT_PUBLIC_WEB_URL);
throw new Error("User not found");
}
Comment on lines +25 to 30
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid a generic throw here; redirect (or render an auth state) instead of 500.
throw new Error("User not found") in a layout will typically surface as an app error; consider redirect(...) to your sign-in entrypoint (or a dedicated unauthorized page) so “signed out / missing user” is a controlled flow.

🤖 Prompt for AI Agents
In apps/admin/src/app/(dashboard)/layout.tsx around lines 25 to 30, the code
currently throws a generic Error when the user is not found causing a 500;
instead handle the unauthenticated case by redirecting to your sign-in
entrypoint or rendering an explicit auth/unauthorized UI. Replace the throw with
a server-side redirect (using next/navigation's redirect) to the sign-in route
or return a layout that displays a controlled "not signed in" or "unauthorized"
state so missing user is a normal flow rather than an app error.


return (
Expand Down
Loading