diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx
index ef0eea75..6e495e9b 100644
--- a/src/app/(dashboard)/layout.tsx
+++ b/src/app/(dashboard)/layout.tsx
@@ -5,7 +5,7 @@ import Link from "next/link";
import { useRouter } from "next/navigation";
import { signOut, useSession } from "next-auth/react";
import { useQuery } from "@tanstack/react-query";
-import { LogOut, User } from "lucide-react";
+import { LogOut, ShieldAlert, User } from "lucide-react";
import { useTRPC } from "@/trpc/client";
import { AppSidebar } from "@/components/app-sidebar";
@@ -51,6 +51,10 @@ export default function DashboardLayout({
return "U";
})();
+ const teamsQuery = useQuery(trpc.team.list.queryOptions());
+ const teams = teamsQuery.data ?? [];
+ const isTeamless = teamsQuery.isSuccess && teams.length === 0;
+
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
// Force password change dialog when mustChangePassword is set
useEffect(() => {
@@ -68,6 +72,64 @@ export default function DashboardLayout({
}
}, [me, router]);
+ if (isTeamless) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
{userName ?? "User"}
+ {userEmail && (
+
{userEmail}
+ )}
+
+
+
+ signOut({ callbackUrl: "/login" })}>
+
+ Sign out
+
+
+
+
+
+
+
+
+
+
+
No Team Assigned
+
+ Your account is active but you haven't been assigned to a team yet. Contact your administrator to get access.
+
+ {(userName || userEmail) && (
+
+ Signed in as {userName || userEmail}
+
+ )}
+
+
+
+
+
+ );
+ }
+
return (
diff --git a/src/app/(dashboard)/settings/page.tsx b/src/app/(dashboard)/settings/page.tsx
index f0b76672..8f793368 100644
--- a/src/app/(dashboard)/settings/page.tsx
+++ b/src/app/(dashboard)/settings/page.tsx
@@ -467,7 +467,32 @@ function AuthSettings() {
testOidcMutation.mutate({ issuer });
};
- const [teamMappings, setTeamMappings] = useState>([]);
+ const [teamMappings, setTeamMappings] = useState>([]);
+
+ function mergeMappings(
+ flat: Array<{ group: string; teamId: string; role: string }>
+ ): Array<{ group: string; teamIds: string[]; role: "VIEWER" | "EDITOR" | "ADMIN" }> {
+ const map = new Map();
+ for (const m of flat) {
+ const key = `${m.group}::${m.role}`;
+ const existing = map.get(key);
+ if (existing) {
+ existing.teamIds.push(m.teamId);
+ } else {
+ map.set(key, { group: m.group, teamIds: [m.teamId], role: m.role as "VIEWER" | "EDITOR" | "ADMIN" });
+ }
+ }
+ return [...map.values()];
+ }
+
+ function flattenMappings(
+ grouped: Array<{ group: string; teamIds: string[]; role: "VIEWER" | "EDITOR" | "ADMIN" }>
+ ): Array<{ group: string; teamId: string; role: "VIEWER" | "EDITOR" | "ADMIN" }> {
+ return grouped.flatMap((row) =>
+ row.teamIds.map((teamId) => ({ group: row.group, teamId, role: row.role }))
+ );
+ }
+
const [defaultTeamId, setDefaultTeamId] = useState("");
const [defaultRole, setDefaultRole] = useState<"VIEWER" | "EDITOR" | "ADMIN">("VIEWER");
const [groupSyncEnabled, setGroupSyncEnabled] = useState(false);
@@ -483,7 +508,9 @@ function AuthSettings() {
setGroupSyncEnabled(settings.oidcGroupSyncEnabled ?? false);
setGroupsScope(settings.oidcGroupsScope ?? "");
setGroupsClaim(settings.oidcGroupsClaim ?? "groups");
- setTeamMappings((settings.oidcTeamMappings ?? []) as Array<{group: string; teamId: string; role: "VIEWER" | "EDITOR" | "ADMIN"}>);
+ setTeamMappings(
+ mergeMappings((settings.oidcTeamMappings ?? []) as Array<{group: string; teamId: string; role: string}>)
+ );
setDefaultTeamId(settings.oidcDefaultTeamId ?? "");
}, [settings, isDirty]);
@@ -504,7 +531,7 @@ function AuthSettings() {
function addMapping() {
markDirty();
- setTeamMappings([...teamMappings, { group: "", teamId: "", role: "VIEWER" }]);
+ setTeamMappings([...teamMappings, { group: "", teamIds: [], role: "VIEWER" }]);
}
function removeMapping(index: number) {
@@ -512,10 +539,17 @@ function AuthSettings() {
setTeamMappings(teamMappings.filter((_, i) => i !== index));
}
- function updateMapping(index: number, field: keyof typeof teamMappings[number], value: string) {
+ function updateMapping(index: number, field: "group" | "role", value: string) {
+ markDirty();
+ setTeamMappings(teamMappings.map((m, i) =>
+ i === index ? { ...m, [field]: value } : m
+ ));
+ }
+
+ function updateMappingTeams(index: number, teamIds: string[]) {
markDirty();
setTeamMappings(teamMappings.map((m, i) =>
- i === index ? { ...m, [field]: value } as typeof m : m
+ i === index ? { ...m, teamIds } : m
));
}
@@ -688,7 +722,7 @@ function AuthSettings() {