diff --git a/apps/api/src/knowledge/dto/entry-query.dto.ts b/apps/api/src/knowledge/dto/entry-query.dto.ts index 5a5f97b..c455838 100644 --- a/apps/api/src/knowledge/dto/entry-query.dto.ts +++ b/apps/api/src/knowledge/dto/entry-query.dto.ts @@ -1,6 +1,6 @@ -import { IsOptional, IsEnum, IsString, IsInt, Min, Max } from "class-validator"; +import { IsOptional, IsEnum, IsString, IsInt, IsIn, Min, Max } from "class-validator"; import { Type } from "class-transformer"; -import { EntryStatus } from "@prisma/client"; +import { EntryStatus, Visibility } from "@prisma/client"; /** * DTO for querying knowledge entries (list endpoint) @@ -10,10 +10,28 @@ export class EntryQueryDto { @IsEnum(EntryStatus, { message: "status must be a valid EntryStatus" }) status?: EntryStatus; + @IsOptional() + @IsEnum(Visibility, { message: "visibility must be a valid Visibility" }) + visibility?: Visibility; + @IsOptional() @IsString({ message: "tag must be a string" }) tag?: string; + @IsOptional() + @IsString({ message: "search must be a string" }) + search?: string; + + @IsOptional() + @IsIn(["updatedAt", "createdAt", "title"], { + message: "sortBy must be updatedAt, createdAt, or title", + }) + sortBy?: "updatedAt" | "createdAt" | "title"; + + @IsOptional() + @IsIn(["asc", "desc"], { message: "sortOrder must be asc or desc" }) + sortOrder?: "asc" | "desc"; + @IsOptional() @Type(() => Number) @IsInt({ message: "page must be an integer" }) diff --git a/apps/api/src/knowledge/knowledge.service.ts b/apps/api/src/knowledge/knowledge.service.ts index f004d91..e1ef04c 100644 --- a/apps/api/src/knowledge/knowledge.service.ts +++ b/apps/api/src/knowledge/knowledge.service.ts @@ -48,6 +48,10 @@ export class KnowledgeService { where.status = query.status; } + if (query.visibility) { + where.visibility = query.visibility; + } + if (query.tag) { where.tags = { some: { @@ -58,6 +62,20 @@ export class KnowledgeService { }; } + if (query.search) { + where.OR = [ + { title: { contains: query.search, mode: "insensitive" } }, + { content: { contains: query.search, mode: "insensitive" } }, + ]; + } + + // Build orderBy + const sortField = query.sortBy ?? "updatedAt"; + const sortDirection = query.sortOrder ?? "desc"; + const orderBy: Prisma.KnowledgeEntryOrderByWithRelationInput = { + [sortField]: sortDirection, + }; + // Get total count const total = await this.prisma.knowledgeEntry.count({ where }); @@ -71,9 +89,7 @@ export class KnowledgeService { }, }, }, - orderBy: { - updatedAt: "desc", - }, + orderBy, skip, take: limit, }); 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)