'use server'; import { db, policySettings, type PolicySetting } from '@/lib/db'; import { getUserAuth } from '@/lib/auth/utils'; import { eq, ilike, or, desc, asc, and, ne, isNotNull, inArray, count, sql } from 'drizzle-orm'; import { env } from '@/lib/env.mjs'; import { syncQueue } from '@/lib/queue/syncQueue'; import { z } from 'zod'; import type { GetPolicySettingsParams, GetPolicySettingsResult, ExportPolicySettingsParams, ExportPolicySettingsResult, PaginationMeta, PolicySettingRow, } from '@/lib/types/policy-table'; export interface PolicySettingSearchResult { id: string; policyName: string; policyType: string; settingName: string; settingValue: string; lastSyncedAt: Date; } export interface SearchResult { success: boolean; data?: PolicySettingSearchResult[]; error?: string; totalCount?: number; } export interface GetSettingResult { success: boolean; data?: PolicySetting; error?: string; } export interface RecentSettingsResult { success: boolean; data?: PolicySettingSearchResult[]; error?: string; } export interface AllSettingsResult { success: boolean; data?: PolicySettingSearchResult[]; error?: string; totalCount?: number; } /** * Search policy settings by keyword across settingName and settingValue * * **Security**: This function enforces tenant isolation by: * 1. Validating user session via getUserAuth() * 2. Extracting tenantId from session * 3. Including explicit WHERE tenantId = ? in ALL queries * * @param searchTerm - Search query (min 2 characters) * @param limit - Maximum number of results (default 100, max 200) * @returns Search results filtered by user's tenant */ export async function searchPolicySettings( searchTerm: string, limit: number = 100 ): Promise { try { const { session } = await getUserAuth(); // T017: Explicit security check - must have authenticated session if (!session?.user) { return { success: false, error: 'Unauthorized' }; } // T017: Explicit security check - must have tenantId in session const tenantId = session.user.tenantId; if (!tenantId) { return { success: false, error: 'Tenant not found' }; } if (searchTerm.length < 2) { return { success: false, error: 'Search term too short (min 2 characters)' }; } // Limit search term length to prevent abuse const sanitizedSearchTerm = searchTerm.slice(0, 200); const searchPattern = `%${sanitizedSearchTerm}%`; // Enforce maximum limit const safeLimit = Math.min(Math.max(1, limit), 200); // Explicit WHERE clause filters by tenantId FIRST for security + null filtering const results = await db .select({ id: policySettings.id, policyName: policySettings.policyName, policyType: policySettings.policyType, settingName: policySettings.settingName, settingValue: policySettings.settingValue, lastSyncedAt: policySettings.lastSyncedAt, }) .from(policySettings) .where( and( eq(policySettings.tenantId, tenantId), // CRITICAL: Tenant isolation ne(policySettings.settingValue, 'null'), // Filter out string "null" ne(policySettings.settingValue, ''), // Filter out empty strings isNotNull(policySettings.settingValue), // Filter out NULL values or( ilike(policySettings.settingName, searchPattern), ilike(policySettings.settingValue, searchPattern) ) ) ) .orderBy(policySettings.settingName) .limit(safeLimit); return { success: true, data: results, totalCount: results.length, }; } catch (error) { console.error('Search failed:', error); return { success: false, error: 'Search failed' }; } } /** * Get a single policy setting by ID * * **Security**: Enforces tenant isolation with explicit WHERE tenantId filter * * @param id - Policy setting ID * @returns Policy setting if found and belongs to user's tenant */ export async function getPolicySettingById( id: string ): Promise { try { const { session } = await getUserAuth(); // T017: Explicit security check if (!session?.user) { return { success: false, error: 'Unauthorized' }; } // T017: Explicit security check const tenantId = session.user.tenantId; if (!tenantId) { return { success: false, error: 'Tenant not found' }; } // T017: Query filtered by tenantId FIRST for security const [result] = await db .select() .from(policySettings) .where( and( eq(policySettings.tenantId, tenantId), // CRITICAL: Tenant isolation eq(policySettings.id, id) ) ) .limit(1); if (!result) { return { success: false, error: 'Policy setting not found' }; } return { success: true, data: result, }; } catch (error) { console.error('Get policy setting failed:', error); return { success: false, error: 'Failed to fetch policy setting' }; } } /** * Get recent policy settings sorted by last sync date * * **Security**: Enforces tenant isolation with explicit WHERE tenantId filter * * @param limit - Maximum number of results (1-100, default 50) * @returns Recent policy settings for user's tenant */ export async function getRecentPolicySettings( limit: number = 50 ): Promise { try { const { session } = await getUserAuth(); // T017: Explicit security check if (!session?.user) { return { success: false, error: 'Unauthorized' }; } // T017: Explicit security check const tenantId = session.user.tenantId; if (!tenantId) { return { success: false, error: 'Tenant not found' }; } // Clamp limit between 1 and 100 const safeLimit = Math.max(1, Math.min(100, limit)); // T017: Query filtered by tenantId for security const results = await db .select({ id: policySettings.id, policyName: policySettings.policyName, policyType: policySettings.policyType, settingName: policySettings.settingName, settingValue: policySettings.settingValue, lastSyncedAt: policySettings.lastSyncedAt, }) .from(policySettings) .where(eq(policySettings.tenantId, tenantId)) // CRITICAL: Tenant isolation .orderBy(desc(policySettings.lastSyncedAt)) .limit(safeLimit); return { success: true, data: results, }; } catch (error) { console.error('Get recent settings failed:', error); return { success: false, error: 'Failed to fetch recent settings' }; } } /** * Get all policy settings for the current user's tenant * * **Security**: Enforces tenant isolation with explicit WHERE tenantId filter * * @returns All policy settings for user's tenant */ export async function getAllPolicySettings(): Promise { try { const { session } = await getUserAuth(); // T017: Explicit security check if (!session?.user) { return { success: false, error: 'Unauthorized' }; } // T017: Explicit security check const tenantId = session.user.tenantId; if (!tenantId) { return { success: false, error: 'Tenant not found' }; } // T017: Query filtered by tenantId for security const results = await db .select({ id: policySettings.id, policyName: policySettings.policyName, policyType: policySettings.policyType, settingName: policySettings.settingName, settingValue: policySettings.settingValue, lastSyncedAt: policySettings.lastSyncedAt, }) .from(policySettings) .where(eq(policySettings.tenantId, tenantId)) // CRITICAL: Tenant isolation .orderBy(desc(policySettings.lastSyncedAt)); return { success: true, data: results, totalCount: results.length, }; } catch (error) { console.error('Get all settings failed:', error); return { success: false, error: 'Failed to fetch settings' }; } } /** * TEMPORARY: Seed test data for the current user's tenant * This is a development helper to populate realistic policy settings */ export async function seedMyTenantData(): Promise<{ success: boolean; error?: string; message?: string; }> { try { const { session } = await getUserAuth(); if (!session?.user) { return { success: false, error: 'Unauthorized' }; } const tenantId = session.user.tenantId; if (!tenantId) { return { success: false, error: 'Tenant ID not found in session' }; } // Create 5 realistic policy settings for the user's tenant const seedData = [ { tenantId, policyName: 'Windows 10 Security Baseline', policyType: 'deviceConfiguration' as const, settingName: 'USB.BlockExternalDevices', settingValue: 'enabled', graphPolicyId: `seed-${tenantId}-policy-001`, }, { tenantId, policyName: 'BitLocker Compliance Policy', policyType: 'compliancePolicy' as const, settingName: 'BitLocker.RequireEncryption', settingValue: 'true', graphPolicyId: `seed-${tenantId}-policy-002`, }, { tenantId, policyName: 'Camera and Microphone Restrictions', policyType: 'deviceConfiguration' as const, settingName: 'Camera.DisableCamera', settingValue: 'false', graphPolicyId: `seed-${tenantId}-policy-003`, }, { tenantId, policyName: 'Windows Defender Configuration', policyType: 'endpointSecurity' as const, settingName: 'Defender.EnableRealTimeProtection', settingValue: 'enabled', graphPolicyId: `seed-${tenantId}-policy-004`, }, { tenantId, policyName: 'Windows Update for Business', policyType: 'windowsUpdateForBusiness' as const, settingName: 'WindowsUpdate.DeferFeatureUpdatesPeriodInDays', settingValue: '30', graphPolicyId: `seed-${tenantId}-policy-005`, }, ]; // Insert all seed data for (const data of seedData) { await db.insert(policySettings).values({ ...data, lastSyncedAt: new Date(), }); } return { success: true, message: `Successfully seeded 5 policy settings for tenant ${tenantId}`, }; } catch (error) { console.error('Seed data failed:', error); return { success: false, error: 'Failed to seed data' }; } } /** * Trigger manual policy sync via BullMQ worker * * **Security**: This function enforces tenant isolation by: * 1. Validating user session via getUserAuth() * 2. Extracting tenantId from session * 3. Enqueuing a job with only the authenticated user's tenantId * * @returns Success/error result with job ID */ export async function triggerPolicySync(): Promise<{ success: boolean; message?: string; error?: string; jobId?: string }> { try { const { session } = await getUserAuth(); if (!session?.user) { return { success: false, error: 'Not authenticated' }; } const tenantId = session.user.tenantId; if (!tenantId) { return { success: false, error: 'No tenant ID found in session' }; } // Enqueue sync job to BullMQ const job = await syncQueue.add('sync-tenant', { tenantId, source: 'manual_trigger', triggeredAt: new Date().toISOString(), triggeredBy: session.user.email || session.user.id, }); return { success: true, message: `Policy sync queued successfully (Job #${job.id})`, jobId: job.id, }; } catch (error) { console.error('Failed to trigger policy sync:', error); return { success: false, error: 'Failed to queue sync job. Please try again later.', }; } } /** * ======================================== * POLICY EXPLORER V2 - Advanced Data Table Server Actions * ======================================== */ /** * Zod schema for getPolicySettings input validation */ const GetPolicySettingsSchema = z.object({ page: z.number().int().min(0).default(0), pageSize: z.union([z.literal(10), z.literal(25), z.literal(50), z.literal(100)]).default(50), sortBy: z.enum(['settingName', 'policyName', 'policyType', 'lastSyncedAt']).optional(), sortDir: z.enum(['asc', 'desc']).default('asc'), policyTypes: z.array(z.string()).optional(), searchQuery: z.string().optional(), }); /** * Get policy settings with pagination, sorting, and filtering * * **Security**: Enforces tenant isolation with explicit WHERE tenantId filter * **Performance**: Uses composite index on (tenantId, policyType, settingName) * * @param params - Pagination, sorting, and filtering parameters * @returns Paginated policy settings with metadata */ export async function getPolicySettingsV2( params: GetPolicySettingsParams ): Promise { try { const { session } = await getUserAuth(); // Security check: Require authenticated session if (!session?.user) { return { success: false, error: 'Unauthorized' }; } // Security check: Require tenantId const tenantId = session.user.tenantId; if (!tenantId) { return { success: false, error: 'Tenant not found' }; } // Validate input parameters const validatedParams = GetPolicySettingsSchema.parse(params); const { page, pageSize, sortBy, sortDir, policyTypes, searchQuery } = validatedParams; // Build WHERE clause with tenant isolation const whereConditions = [ eq(policySettings.tenantId, tenantId), // CRITICAL: Tenant isolation ne(policySettings.settingValue, 'null'), // Filter out string "null" ne(policySettings.settingValue, ''), // Filter out empty strings isNotNull(policySettings.settingValue), // Filter out NULL values ]; // Add policy type filter if provided if (policyTypes && policyTypes.length > 0) { whereConditions.push(inArray(policySettings.policyType, policyTypes)); } // Add search query filter if provided if (searchQuery && searchQuery.trim().length >= 2) { const searchPattern = `%${searchQuery.trim().slice(0, 200)}%`; whereConditions.push( or( ilike(policySettings.settingName, searchPattern), ilike(policySettings.settingValue, searchPattern), ilike(policySettings.policyName, searchPattern) )! ); } // Build ORDER BY clause const orderByColumn = sortBy ? policySettings[sortBy as keyof typeof policySettings] : policySettings.settingName; const orderByDirection = sortDir === 'desc' ? desc : asc; // Execute count query for pagination metadata const [{ totalCount }] = await db .select({ totalCount: count() }) .from(policySettings) .where(and(...whereConditions)); // Calculate pagination metadata const pageCount = Math.ceil(totalCount / pageSize); const hasNextPage = page < pageCount - 1; const hasPreviousPage = page > 0; // Execute data query with pagination const data = await db .select({ id: policySettings.id, tenantId: policySettings.tenantId, policyName: policySettings.policyName, policyType: policySettings.policyType, settingName: policySettings.settingName, settingValue: policySettings.settingValue, graphPolicyId: policySettings.graphPolicyId, lastSyncedAt: policySettings.lastSyncedAt, createdAt: policySettings.createdAt, }) .from(policySettings) .where(and(...whereConditions)) .orderBy(orderByDirection(orderByColumn as any)) .limit(pageSize) .offset(page * pageSize); const meta: PaginationMeta = { totalCount, pageCount, currentPage: page, pageSize, hasNextPage, hasPreviousPage, }; return { success: true, data: data as PolicySettingRow[], meta, }; } catch (error) { console.error('getPolicySettingsV2 failed:', error); if (error instanceof z.ZodError) { return { success: false, error: 'Invalid parameters: ' + error.issues.map((e) => e.message).join(', ') }; } return { success: false, error: 'Failed to fetch policy settings' }; } } /** * Zod schema for exportPolicySettingsCSV input validation */ const ExportPolicySettingsSchema = z.object({ policyTypes: z.array(z.string()).optional(), searchQuery: z.string().optional(), sortBy: z.enum(['settingName', 'policyName', 'policyType', 'lastSyncedAt']).optional(), sortDir: z.enum(['asc', 'desc']).default('asc'), maxRows: z.number().int().min(1).max(5000).default(5000), }); /** * Helper function to escape CSV values * Handles commas, quotes, and newlines according to RFC 4180 */ function escapeCsvValue(value: string): string { // If value contains comma, quote, or newline, wrap in quotes and escape internal quotes if (value.includes(',') || value.includes('"') || value.includes('\n') || value.includes('\r')) { return `"${value.replace(/"/g, '""')}"`; } return value; } /** * Export policy settings as CSV (server-side, max 5000 rows) * * **Security**: Enforces tenant isolation with explicit WHERE tenantId filter * **Performance**: Limits export to 5000 rows to prevent memory issues * * @param params - Filtering and sorting parameters * @returns CSV content as string with filename */ export async function exportPolicySettingsCSV( params: ExportPolicySettingsParams = {} ): Promise { try { const { session } = await getUserAuth(); // Security check: Require authenticated session if (!session?.user) { return { success: false, error: 'Unauthorized' }; } // Security check: Require tenantId const tenantId = session.user.tenantId; if (!tenantId) { return { success: false, error: 'Tenant not found' }; } // Validate input parameters const validatedParams = ExportPolicySettingsSchema.parse(params); const { policyTypes, searchQuery, sortBy, sortDir, maxRows } = validatedParams; // Build WHERE clause with tenant isolation const whereConditions = [ eq(policySettings.tenantId, tenantId), // CRITICAL: Tenant isolation ne(policySettings.settingValue, 'null'), ne(policySettings.settingValue, ''), isNotNull(policySettings.settingValue), ]; // Add policy type filter if provided if (policyTypes && policyTypes.length > 0) { whereConditions.push(inArray(policySettings.policyType, policyTypes)); } // Add search query filter if provided if (searchQuery && searchQuery.trim().length >= 2) { const searchPattern = `%${searchQuery.trim().slice(0, 200)}%`; whereConditions.push( or( ilike(policySettings.settingName, searchPattern), ilike(policySettings.settingValue, searchPattern), ilike(policySettings.policyName, searchPattern) )! ); } // Build ORDER BY clause const orderByColumn = sortBy ? policySettings[sortBy as keyof typeof policySettings] : policySettings.settingName; const orderByDirection = sortDir === 'desc' ? desc : asc; // Fetch data (limited to maxRows) const data = await db .select({ id: policySettings.id, policyName: policySettings.policyName, policyType: policySettings.policyType, settingName: policySettings.settingName, settingValue: policySettings.settingValue, graphPolicyId: policySettings.graphPolicyId, lastSyncedAt: policySettings.lastSyncedAt, }) .from(policySettings) .where(and(...whereConditions)) .orderBy(orderByDirection(orderByColumn as any)) .limit(maxRows); // Generate CSV content // Add UTF-8 BOM for Excel compatibility const BOM = '\uFEFF'; // CSV header const headers = [ 'Policy Name', 'Policy Type', 'Setting Name', 'Setting Value', 'Graph Policy ID', 'Last Synced At', ]; const headerRow = headers.map(h => escapeCsvValue(h)).join(','); // CSV rows const dataRows = data.map(row => { return [ escapeCsvValue(row.policyName), escapeCsvValue(row.policyType), escapeCsvValue(row.settingName), escapeCsvValue(row.settingValue), escapeCsvValue(row.graphPolicyId), escapeCsvValue(row.lastSyncedAt.toISOString()), ].join(','); }); const csv = BOM + headerRow + '\n' + dataRows.join('\n'); // Generate filename with timestamp const timestamp = new Date().toISOString().slice(0, 10); // YYYY-MM-DD const filename = `policy-settings-${timestamp}.csv`; return { success: true, csv, filename, rowCount: data.length, }; } catch (error) { console.error('exportPolicySettingsCSV failed:', error); if (error instanceof z.ZodError) { return { success: false, error: 'Invalid parameters: ' + error.issues.map((e) => e.message).join(', ') }; } return { success: false, error: 'Failed to export policy settings' }; } }