'use server'; import { db, policySettings, type PolicySetting } from '@/lib/db'; import { getUserAuth } from '@/lib/auth/utils'; import { eq, ilike, or, desc, and, ne, isNotNull } from 'drizzle-orm'; import { env } from '@/lib/env.mjs'; import { syncQueue } from '@/lib/queue/syncQueue'; 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.', }; } }