diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index fe42114..edb8833 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -134,3 +134,15 @@ model WidgetInstance { @@index([userId]) @@index([tenantId]) } + +model SearchProvider { + id String @id @default(uuid()) + userId String? + tenantId String? + name String + urlTemplate String + isDefault Boolean @default(false) + createdAt DateTime @default(now()) + + @@index([userId]) +} diff --git a/apps/api/src/dashboard/dashboard.controller.ts b/apps/api/src/dashboard/dashboard.controller.ts index 3df19f4..40d362b 100644 --- a/apps/api/src/dashboard/dashboard.controller.ts +++ b/apps/api/src/dashboard/dashboard.controller.ts @@ -12,6 +12,7 @@ import { } from '@nestjs/common'; import { Request } from 'express'; import { DashboardService } from './dashboard.service'; +import { CreateSearchProviderDto } from './dto/create-search-provider.dto'; import { CreateWidgetDto } from './dto/create-widget.dto'; import { SaveLayoutDto } from './dto/save-layout.dto'; import { UpdateWidgetConfigDto } from './dto/update-widget-config.dto'; @@ -30,6 +31,9 @@ import { UpdateWidgetConfigDto } from './dto/update-widget-config.dto'; * - POST /dashboard/widgets — create a new widget instance * - PATCH /dashboard/widgets/:id/config — update widget config * - DELETE /dashboard/widgets/:id — remove a widget instance + * - GET /dashboard/search-providers — list default + user's custom providers + * - POST /dashboard/search-providers — create a custom search provider + * - DELETE /dashboard/search-providers/:id — remove a custom provider */ @Controller('dashboard') export class DashboardController { @@ -92,4 +96,30 @@ export class DashboardController { const { userId } = this.extractContext(req); return this.dashboardService.removeWidget(id, userId); } + + // --- Search Providers (05-02, D-15) --- + + @Get('search-providers') + async getSearchProviders(@Req() req: Request) { + const { userId } = this.extractContext(req); + return this.dashboardService.getSearchProviders(userId); + } + + @Post('search-providers') + async addSearchProvider( + @Req() req: Request, + @Body() dto: CreateSearchProviderDto, + ) { + const { userId, tenantId } = this.extractContext(req); + return this.dashboardService.addSearchProvider(userId, tenantId, dto); + } + + @Delete('search-providers/:id') + async removeSearchProvider( + @Param('id') id: string, + @Req() req: Request, + ) { + const { userId } = this.extractContext(req); + return this.dashboardService.removeSearchProvider(id, userId); + } } diff --git a/apps/api/src/dashboard/dashboard.service.ts b/apps/api/src/dashboard/dashboard.service.ts index d642602..28deb24 100644 --- a/apps/api/src/dashboard/dashboard.service.ts +++ b/apps/api/src/dashboard/dashboard.service.ts @@ -4,10 +4,46 @@ import { } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; +import { CreateSearchProviderDto } from './dto/create-search-provider.dto'; import { CreateWidgetDto } from './dto/create-widget.dto'; import { SaveLayoutDto } from './dto/save-layout.dto'; import { UpdateWidgetConfigDto } from './dto/update-widget-config.dto'; +/** + * Default search providers (D-15). + * Returned as part of getSearchProviders even when no DB rows exist. + * userId null = global defaults — cannot be deleted by users. + */ +const DEFAULT_SEARCH_PROVIDERS = [ + { + id: 'google', + userId: null, + tenantId: null, + name: 'Google', + urlTemplate: 'https://www.google.com/search?q={query}', + isDefault: true, + createdAt: new Date('2024-01-01'), + }, + { + id: 'bing', + userId: null, + tenantId: null, + name: 'Bing', + urlTemplate: 'https://www.bing.com/search?q={query}', + isDefault: true, + createdAt: new Date('2024-01-01'), + }, + { + id: 'ddg', + userId: null, + tenantId: null, + name: 'DuckDuckGo', + urlTemplate: 'https://duckduckgo.com/?q={query}', + isDefault: true, + createdAt: new Date('2024-01-01'), + }, +]; + /** * Service managing per-user dashboard layouts and widget instances. * @@ -126,4 +162,59 @@ export class DashboardService { where: { id }, }); } + + // --- Search Providers (05-02, D-15) --- + + /** + * Returns the three default providers merged with any user-custom providers. + * Defaults are always returned even with an empty DB (no seed migration needed). + */ + async getSearchProviders(userId: string) { + const custom = await this.prisma.searchProvider.findMany({ + where: { userId }, + orderBy: { createdAt: 'asc' }, + }); + + return [...DEFAULT_SEARCH_PROVIDERS, ...custom]; + } + + /** + * Creates a user-custom search provider. + */ + async addSearchProvider( + userId: string, + tenantId: string, + dto: CreateSearchProviderDto, + ) { + return this.prisma.searchProvider.create({ + data: { + userId, + tenantId, + name: dto.name, + urlTemplate: dto.urlTemplate, + isDefault: false, + }, + }); + } + + /** + * Removes a user-custom search provider. + * Verifies ownership — default providers (userId null) cannot be deleted (T-05-07). + */ + async removeSearchProvider(id: string, userId: string) { + // Default providers have hardcoded IDs that won't exist in DB + const provider = await this.prisma.searchProvider.findUnique({ + where: { id }, + }); + + if (!provider || provider.userId !== userId) { + throw new NotFoundException( + `Search provider with id '${id}' not found`, + ); + } + + return this.prisma.searchProvider.delete({ + where: { id }, + }); + } } diff --git a/apps/api/src/dashboard/dto/create-search-provider.dto.ts b/apps/api/src/dashboard/dto/create-search-provider.dto.ts new file mode 100644 index 0000000..e0173ae --- /dev/null +++ b/apps/api/src/dashboard/dto/create-search-provider.dto.ts @@ -0,0 +1,18 @@ +import { IsNotEmpty, IsString, Matches } from 'class-validator'; + +/** + * DTO for creating a custom search provider (D-15). + * urlTemplate must contain {query} placeholder for the search term. + */ +export class CreateSearchProviderDto { + @IsString() + @IsNotEmpty() + name!: string; + + @IsString() + @IsNotEmpty() + @Matches(/\{query\}/, { + message: 'urlTemplate must contain {query}', + }) + urlTemplate!: string; +}