From 64d3ad810bea43c285b22056ba73c9dabeafb7e0 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 26 Feb 2026 22:48:23 -0600 Subject: [PATCH] fix(api,web): add workspace context to widget endpoints and auto-detect workspace ID - Add WorkspaceGuard to all widget data endpoints in WidgetsController - Use RequestWithWorkspace type for proper type safety (no non-null assertions) - Auto-detect workspace ID from localStorage in apiRequest when not explicitly provided, fixing all API calls missing X-Workspace-Id header - Replace hardcoded "default-workspace-id" in credentials pages with useWorkspaceId() hook Co-Authored-By: Claude Opus 4.6 --- apps/api/src/widgets/widgets.controller.ts | 70 ++++++------------- .../settings/credentials/audit/page.tsx | 13 ++-- .../settings/credentials/page.tsx | 12 ++-- apps/web/src/lib/api/client.ts | 10 ++- 4 files changed, 43 insertions(+), 62 deletions(-) diff --git a/apps/api/src/widgets/widgets.controller.ts b/apps/api/src/widgets/widgets.controller.ts index c90c33d..fb4bd5a 100644 --- a/apps/api/src/widgets/widgets.controller.ts +++ b/apps/api/src/widgets/widgets.controller.ts @@ -1,22 +1,14 @@ -import { - Controller, - Get, - Post, - Body, - Param, - UseGuards, - Request, - UnauthorizedException, -} from "@nestjs/common"; +import { Controller, Get, Post, Body, Param, UseGuards, Request } from "@nestjs/common"; import { WidgetsService } from "./widgets.service"; import { WidgetDataService } from "./widget-data.service"; import { AuthGuard } from "../auth/guards/auth.guard"; +import { WorkspaceGuard } from "../common/guards/workspace.guard"; import type { StatCardQueryDto, ChartQueryDto, ListQueryDto, CalendarPreviewQueryDto } from "./dto"; -import type { AuthenticatedRequest } from "../common/types/user.types"; +import type { RequestWithWorkspace } from "../common/types/user.types"; /** * Controller for widget definition and data endpoints - * All endpoints require authentication + * All endpoints require authentication; data endpoints also require workspace context */ @Controller("widgets") @UseGuards(AuthGuard) @@ -51,12 +43,9 @@ export class WidgetsController { * Get stat card widget data */ @Post("data/stat-card") - async getStatCardData(@Request() req: AuthenticatedRequest, @Body() query: StatCardQueryDto) { - const workspaceId = req.user?.currentWorkspaceId ?? req.user?.workspaceId; - if (!workspaceId) { - throw new UnauthorizedException("Workspace ID required"); - } - return this.widgetDataService.getStatCardData(workspaceId, query); + @UseGuards(WorkspaceGuard) + async getStatCardData(@Request() req: RequestWithWorkspace, @Body() query: StatCardQueryDto) { + return this.widgetDataService.getStatCardData(req.workspace.id, query); } /** @@ -64,12 +53,9 @@ export class WidgetsController { * Get chart widget data */ @Post("data/chart") - async getChartData(@Request() req: AuthenticatedRequest, @Body() query: ChartQueryDto) { - const workspaceId = req.user?.currentWorkspaceId ?? req.user?.workspaceId; - if (!workspaceId) { - throw new UnauthorizedException("Workspace ID required"); - } - return this.widgetDataService.getChartData(workspaceId, query); + @UseGuards(WorkspaceGuard) + async getChartData(@Request() req: RequestWithWorkspace, @Body() query: ChartQueryDto) { + return this.widgetDataService.getChartData(req.workspace.id, query); } /** @@ -77,12 +63,9 @@ export class WidgetsController { * Get list widget data */ @Post("data/list") - async getListData(@Request() req: AuthenticatedRequest, @Body() query: ListQueryDto) { - const workspaceId = req.user?.currentWorkspaceId ?? req.user?.workspaceId; - if (!workspaceId) { - throw new UnauthorizedException("Workspace ID required"); - } - return this.widgetDataService.getListData(workspaceId, query); + @UseGuards(WorkspaceGuard) + async getListData(@Request() req: RequestWithWorkspace, @Body() query: ListQueryDto) { + return this.widgetDataService.getListData(req.workspace.id, query); } /** @@ -90,15 +73,12 @@ export class WidgetsController { * Get calendar preview widget data */ @Post("data/calendar-preview") + @UseGuards(WorkspaceGuard) async getCalendarPreviewData( - @Request() req: AuthenticatedRequest, + @Request() req: RequestWithWorkspace, @Body() query: CalendarPreviewQueryDto ) { - const workspaceId = req.user?.currentWorkspaceId ?? req.user?.workspaceId; - if (!workspaceId) { - throw new UnauthorizedException("Workspace ID required"); - } - return this.widgetDataService.getCalendarPreviewData(workspaceId, query); + return this.widgetDataService.getCalendarPreviewData(req.workspace.id, query); } /** @@ -106,12 +86,9 @@ export class WidgetsController { * Get active projects widget data */ @Post("data/active-projects") - async getActiveProjectsData(@Request() req: AuthenticatedRequest) { - const workspaceId = req.user?.currentWorkspaceId ?? req.user?.workspaceId; - if (!workspaceId) { - throw new UnauthorizedException("Workspace ID required"); - } - return this.widgetDataService.getActiveProjectsData(workspaceId); + @UseGuards(WorkspaceGuard) + async getActiveProjectsData(@Request() req: RequestWithWorkspace) { + return this.widgetDataService.getActiveProjectsData(req.workspace.id); } /** @@ -119,11 +96,8 @@ export class WidgetsController { * Get agent chains widget data (active agent sessions) */ @Post("data/agent-chains") - async getAgentChainsData(@Request() req: AuthenticatedRequest) { - const workspaceId = req.user?.currentWorkspaceId ?? req.user?.workspaceId; - if (!workspaceId) { - throw new UnauthorizedException("Workspace ID required"); - } - return this.widgetDataService.getAgentChainsData(workspaceId); + @UseGuards(WorkspaceGuard) + async getAgentChainsData(@Request() req: RequestWithWorkspace) { + return this.widgetDataService.getAgentChainsData(req.workspace.id); } } diff --git a/apps/web/src/app/(authenticated)/settings/credentials/audit/page.tsx b/apps/web/src/app/(authenticated)/settings/credentials/audit/page.tsx index 4eb77d4..271df01 100644 --- a/apps/web/src/app/(authenticated)/settings/credentials/audit/page.tsx +++ b/apps/web/src/app/(authenticated)/settings/credentials/audit/page.tsx @@ -14,6 +14,7 @@ import { SelectValue, } from "@/components/ui/select"; import { fetchCredentialAuditLog, type AuditLogEntry } from "@/lib/api/credentials"; +import { useWorkspaceId } from "@/lib/hooks"; const ACTIVITY_ACTIONS = [ { value: "CREDENTIAL_CREATED", label: "Created" }, @@ -39,17 +40,17 @@ export default function CredentialAuditPage(): React.ReactElement { const [filters, setFilters] = useState({}); const [hasFilters, setHasFilters] = useState(false); - // TODO: Get workspace ID from context/auth - const workspaceId = "default-workspace-id"; // Placeholder + const workspaceId = useWorkspaceId(); useEffect(() => { - void loadLogs(); - }, [page, filters]); + if (!workspaceId) return; + void loadLogs(workspaceId); + }, [workspaceId, page, filters]); - async function loadLogs(): Promise { + async function loadLogs(wsId: string): Promise { try { setIsLoading(true); - const response = await fetchCredentialAuditLog(workspaceId, { + const response = await fetchCredentialAuditLog(wsId, { ...filters, page, limit, diff --git a/apps/web/src/app/(authenticated)/settings/credentials/page.tsx b/apps/web/src/app/(authenticated)/settings/credentials/page.tsx index 5df0c66..73adb5a 100644 --- a/apps/web/src/app/(authenticated)/settings/credentials/page.tsx +++ b/apps/web/src/app/(authenticated)/settings/credentials/page.tsx @@ -6,22 +6,24 @@ import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { fetchCredentials, type Credential } from "@/lib/api/credentials"; +import { useWorkspaceId } from "@/lib/hooks"; export default function CredentialsPage(): React.ReactElement { const [credentials, setCredentials] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const workspaceId = "default-workspace-id"; + const workspaceId = useWorkspaceId(); useEffect(() => { - void loadCredentials(); - }, []); + if (!workspaceId) return; + void loadCredentials(workspaceId); + }, [workspaceId]); - async function loadCredentials(): Promise { + async function loadCredentials(wsId: string): Promise { try { setIsLoading(true); - const response = await fetchCredentials(workspaceId); + const response = await fetchCredentials(wsId); setCredentials(response.data); setError(null); } catch (err) { diff --git a/apps/web/src/lib/api/client.ts b/apps/web/src/lib/api/client.ts index 7bdd8fc..e407fcd 100644 --- a/apps/web/src/lib/api/client.ts +++ b/apps/web/src/lib/api/client.ts @@ -202,9 +202,13 @@ export async function apiRequest(endpoint: string, options: ApiRequestOptions ...baseHeaders, }; - // Add workspace ID header if provided (recommended over query string) - if (workspaceId) { - headers["X-Workspace-Id"] = workspaceId; + // Add workspace ID header — use explicit value, or auto-detect from localStorage + const resolvedWorkspaceId = + workspaceId ?? + (typeof window !== "undefined" ? localStorage.getItem("mosaic-workspace-id") : null) ?? + undefined; + if (resolvedWorkspaceId) { + headers["X-Workspace-Id"] = resolvedWorkspaceId; } // Add CSRF token for state-changing requests (POST, PUT, PATCH, DELETE) -- 2.49.1