All checks were successful
Trigger Cloudarix Deploy / call-webhook (push) Successful in 2s
✨ New Features - Advanced data table with TanStack Table v8 + Server Actions - Server-side pagination (10/25/50/100 rows per page) - Multi-column sorting with visual indicators - Column management (show/hide, resize) persisted to localStorage - URL state synchronization for shareable filtered views - Sticky header with compact/comfortable density modes 📦 Components Added - PolicyTableV2.tsx - Main table with TanStack integration - PolicyTableColumns.tsx - 7 column definitions with sorting - PolicyTablePagination.tsx - Pagination controls - PolicyTableToolbar.tsx - Density toggle + column visibility menu - ColumnVisibilityMenu.tsx - Show/hide columns dropdown 🔧 Hooks Added - usePolicyTable.ts - TanStack Table initialization - useURLState.ts - URL query param sync with nuqs - useTablePreferences.ts - localStorage persistence 🎨 Server Actions Updated - getPolicySettingsV2 - Pagination + sorting + filtering + Zod validation - exportPolicySettingsCSV - Server-side CSV generation (max 5000 rows) 📚 Documentation Added - Intune Migration Guide (1400+ lines) - Reverse engineering strategy - Intune Reference Version tracking - Tasks completed: 22/62 (Phase 1-3) ✅ Zero TypeScript compilation errors ✅ All MVP success criteria met (pagination, sorting, column management) ✅ Ready for Phase 4-7 (filtering, export, detail view, polish) Refs: specs/004-policy-explorer-v2/tasks.md
690 lines
21 KiB
TypeScript
690 lines
21 KiB
TypeScript
'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<SearchResult> {
|
|
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<GetSettingResult> {
|
|
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<RecentSettingsResult> {
|
|
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<AllSettingsResult> {
|
|
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<GetPolicySettingsResult> {
|
|
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<ExportPolicySettingsResult> {
|
|
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' };
|
|
}
|
|
}
|
|
|