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
65 changes: 27 additions & 38 deletions apps/web/src/app/(dashboard)/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import { authClient } from "@superset/auth/client";
import { getInitials } from "@superset/shared/names";
import { Avatar, AvatarFallback, AvatarImage } from "@superset/ui/avatar";
import {
DropdownMenu,
Expand All @@ -15,7 +14,7 @@ import {
DropdownMenuTrigger,
} from "@superset/ui/dropdown-menu";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Check, LogOut } from "lucide-react";
import { Check, ChevronDown, LogOut } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
Expand All @@ -33,12 +32,13 @@ export function Header() {
);

const user = session?.user;
const initials = getInitials(user?.name, user?.email);
const activeOrganizationId = session?.session?.activeOrganizationId;
const activeOrganization = organizations?.find(
(org) => org.id === activeOrganizationId,
);

const displayName = activeOrganization?.name ?? "Organization";

const handleSignOut = async () => {
await authClient.signOut();
router.push("/sign-in");
Expand Down Expand Up @@ -66,15 +66,22 @@ export function Header() {
<DropdownMenuTrigger asChild>
<button
type="button"
className="cursor-pointer rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="flex cursor-pointer items-center gap-2 rounded-md border border-border/60 bg-secondary/50 px-3 py-1.5 transition-all duration-150 hover:border-border hover:bg-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
aria-label="Organization menu"
>
<Avatar className="size-8">
<Avatar className="size-5">
<AvatarImage
src={user?.image ?? undefined}
alt={user?.name ?? ""}
src={activeOrganization?.logo ?? undefined}
alt={displayName}
/>
<AvatarFallback className="text-xs">{initials}</AvatarFallback>
<AvatarFallback className="text-[10px]">
{displayName.charAt(0)}
</AvatarFallback>
</Avatar>
<span className="max-w-32 truncate text-sm font-medium">
{displayName}
</span>
<ChevronDown className="size-4 text-muted-foreground" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-56">
Expand All @@ -85,52 +92,34 @@ export function Header() {
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{organizations && organizations.length === 1 && (
<>
<div className="flex items-center px-2 py-1.5 text-sm">
<Avatar className="mr-2 size-4">
<AvatarFallback className="text-[8px]">
{activeOrganization?.name?.charAt(0) ?? "O"}
</AvatarFallback>
</Avatar>
<span className="truncate">
{activeOrganization?.name ?? "Organization"}
</span>
</div>
<DropdownMenuSeparator />
</>
)}
{organizations && organizations.length > 1 && (
<>
<DropdownMenuSub>
<DropdownMenuSubTrigger className="cursor-pointer">
<Avatar className="mr-2 size-4">
<AvatarFallback className="text-[8px]">
{activeOrganization?.name?.charAt(0) ?? "O"}
</AvatarFallback>
</Avatar>
<span className="truncate">
{activeOrganization?.name ?? "Organization"}
</span>
<span>Switch organization</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
Switch organization
{user?.email}
</DropdownMenuLabel>
{organizations.map((org) => (
<DropdownMenuItem
key={org.id}
className="cursor-pointer"
className="cursor-pointer gap-2"
onClick={() => handleSwitchOrganization(org.id)}
>
<Avatar className="mr-2 size-4">
<Avatar className="size-4">
<AvatarImage
src={org.logo ?? undefined}
alt={org.name ?? "Organization"}
/>
<AvatarFallback className="text-[8px]">
{org.name?.charAt(0) ?? "O"}
</AvatarFallback>
</Avatar>
<span className="flex-1 truncate">{org.name}</span>
{org.id === activeOrganizationId && (
<Check className="ml-2 size-4 text-primary" />
<Check className="size-4 text-primary" />
)}
</DropdownMenuItem>
))}
Expand All @@ -140,11 +129,11 @@ export function Header() {
</>
)}
<DropdownMenuItem
className="cursor-pointer"
className="cursor-pointer gap-2"
onClick={handleSignOut}
>
<LogOut className="mr-2 size-4" />
Logout
<LogOut className="size-4" />
<span>Log out</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Expand Down
11 changes: 9 additions & 2 deletions packages/trpc/src/router/user/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { db } from "@superset/db/client";
import { members, users } from "@superset/db/schema";
import { TRPCError, type TRPCRouterRecord } from "@trpc/server";
import { del, put } from "@vercel/blob";
import { eq } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
import { z } from "zod";

import { protectedProcedure } from "../../trpc";
Expand All @@ -11,8 +11,15 @@ export const userRouter = {
me: protectedProcedure.query(({ ctx }) => ctx.session.user),

myOrganization: protectedProcedure.query(async ({ ctx }) => {
const activeOrganizationId = ctx.session.session.activeOrganizationId;

const membership = await db.query.members.findFirst({
where: eq(members.userId, ctx.session.user.id),
where: activeOrganizationId
? and(
eq(members.userId, ctx.session.user.id),
eq(members.organizationId, activeOrganizationId),
)
: eq(members.userId, ctx.session.user.id),
Comment on lines +14 to +22
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

Guard against missing session payload before reading activeOrganizationId.

Line 14 assumes ctx.session.session is always defined; if it’s absent (e.g., session variants or partial payloads), this will throw and break myOrganization. The client-side usage already treats it as optional. Consider optional chaining to keep behavior aligned and fail-safe.

✅ Suggested fix
-		const activeOrganizationId = ctx.session.session.activeOrganizationId;
+		const activeOrganizationId = ctx.session.session?.activeOrganizationId;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const activeOrganizationId = ctx.session.session.activeOrganizationId;
const membership = await db.query.members.findFirst({
where: eq(members.userId, ctx.session.user.id),
where: activeOrganizationId
? and(
eq(members.userId, ctx.session.user.id),
eq(members.organizationId, activeOrganizationId),
)
: eq(members.userId, ctx.session.user.id),
const activeOrganizationId = ctx.session.session?.activeOrganizationId;
const membership = await db.query.members.findFirst({
where: activeOrganizationId
? and(
eq(members.userId, ctx.session.user.id),
eq(members.organizationId, activeOrganizationId),
)
: eq(members.userId, ctx.session.user.id),
🤖 Prompt for AI Agents
In `@packages/trpc/src/router/user/user.ts` around lines 14 - 22, The code reads
ctx.session.session.activeOrganizationId without guarding for missing session
payload; change to use optional chaining when extracting activeOrganizationId
(e.g., const activeOrganizationId = ctx.session?.session?.activeOrganizationId)
and ensure the conditional passed to db.query.members.findFirst still only
includes the organization filter when activeOrganizationId is defined so
members.userId and members.organizationId checks remain safe; update the logic
around db.query.members.findFirst (and any use in myOrganization) to handle
undefined activeOrganizationId gracefully.

with: {
organization: true,
},
Expand Down