tenantpilot/lib/actions/policySettings.ts
Ahmed Darrazi 2592b89bc6
All checks were successful
Trigger Cloudarix Deploy / call-webhook (push) Successful in 1s
feat: Add settings overview page with getAllPolicySettings
- Add new /settings-overview route displaying all policy settings in table
- Implement getAllPolicySettings() server action with tenant isolation
- Add 'All Settings' navigation item with Database icon
- Use date-fns for relative time display (lastSyncedAt)
- Server-side rendering for optimal performance
2025-12-07 01:50:34 +01:00

404 lines
11 KiB
TypeScript

'use server';
import { db, policySettings, type PolicySetting } from '@/lib/db';
import { getUserAuth } from '@/lib/auth/utils';
import { eq, ilike, or, desc, and } from 'drizzle-orm';
import { env } from '@/lib/env.mjs';
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)
* @returns Search results filtered by user's tenant
*/
export async function searchPolicySettings(
searchTerm: string
): 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}%`;
// T017: Explicit WHERE clause filters by tenantId FIRST 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(
and(
eq(policySettings.tenantId, tenantId), // CRITICAL: Tenant isolation
or(
ilike(policySettings.settingName, searchPattern),
ilike(policySettings.settingValue, searchPattern)
)
)
)
.orderBy(policySettings.settingName)
.limit(100);
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 20)
* @returns Recent policy settings for user's tenant
*/
export async function getRecentPolicySettings(
limit: number = 20
): 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 n8n webhook
*
* **Security**: This function enforces tenant isolation by:
* 1. Validating user session via getUserAuth()
* 2. Extracting tenantId from session
* 3. Sending only the authenticated user's tenantId to n8n
*
* @returns Success/error result
*/
export async function triggerPolicySync(): Promise<{ success: boolean; message?: string; error?: 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' };
}
const webhookUrl = env.N8N_SYNC_WEBHOOK_URL;
if (!webhookUrl) {
return { success: false, error: 'Sync webhook not configured' };
}
// Trigger n8n workflow
const response = await fetch(webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
tenantId,
source: 'manual_trigger',
triggeredAt: new Date().toISOString(),
}),
});
if (!response.ok) {
throw new Error(`Webhook responded with status ${response.status}`);
}
return {
success: true,
message: 'Policy sync triggered successfully',
};
} catch (error) {
console.error('Failed to trigger policy sync:', error);
return {
success: false,
error: 'Failed to trigger sync. Please try again later.',
};
}
}