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 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FilterState>({});
|
||||
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<void> {
|
||||
async function loadLogs(wsId: string): Promise<void> {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetchCredentialAuditLog(workspaceId, {
|
||||
const response = await fetchCredentialAuditLog(wsId, {
|
||||
...filters,
|
||||
page,
|
||||
limit,
|
||||
|
||||
@@ -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<Credential[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const workspaceId = "default-workspace-id";
|
||||
const workspaceId = useWorkspaceId();
|
||||
|
||||
useEffect(() => {
|
||||
void loadCredentials();
|
||||
}, []);
|
||||
if (!workspaceId) return;
|
||||
void loadCredentials(workspaceId);
|
||||
}, [workspaceId]);
|
||||
|
||||
async function loadCredentials(): Promise<void> {
|
||||
async function loadCredentials(wsId: string): Promise<void> {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetchCredentials(workspaceId);
|
||||
const response = await fetchCredentials(wsId);
|
||||
setCredentials(response.data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
|
||||
@@ -202,9 +202,13 @@ export async function apiRequest<T>(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)
|
||||
|
||||
Reference in New Issue
Block a user