import { z } from 'zod' import { router, adminProcedure, superAdminProcedure, protectedProcedure } from '../trpc' import { getWhatsAppProvider, getWhatsAppProviderType } from '@/lib/whatsapp' export const settingsRouter = router({ /** * Get all settings by category */ getByCategory: adminProcedure .input(z.object({ category: z.string() })) .query(async ({ ctx, input }) => { const settings = await ctx.prisma.systemSettings.findMany({ where: { category: input.category as any }, orderBy: { key: 'asc' }, }) // Mask secret values for non-super-admins if (ctx.user.role !== 'SUPER_ADMIN') { return settings.map((s) => ({ ...s, value: s.isSecret ? '********' : s.value, })) } return settings }), /** * Get a single setting */ get: adminProcedure .input(z.object({ key: z.string() })) .query(async ({ ctx, input }) => { const setting = await ctx.prisma.systemSettings.findUnique({ where: { key: input.key }, }) if (!setting) return null // Mask secret values for non-super-admins if (setting.isSecret && ctx.user.role !== 'SUPER_ADMIN') { return { ...setting, value: '********' } } return setting }), /** * Get multiple settings by keys (for client-side caching) */ getMultiple: adminProcedure .input(z.object({ keys: z.array(z.string()) })) .query(async ({ ctx, input }) => { const settings = await ctx.prisma.systemSettings.findMany({ where: { key: { in: input.keys } }, }) // Mask secret values for non-super-admins if (ctx.user.role !== 'SUPER_ADMIN') { return settings.map((s) => ({ ...s, value: s.isSecret ? '********' : s.value, })) } return settings }), /** * Update a setting (super admin only for secrets) */ update: superAdminProcedure .input( z.object({ key: z.string(), value: z.string(), }) ) .mutation(async ({ ctx, input }) => { const setting = await ctx.prisma.systemSettings.update({ where: { key: input.key }, data: { value: input.value, updatedBy: ctx.user.id, }, }) // Audit log (don't log actual value for secrets) await ctx.prisma.auditLog.create({ data: { userId: ctx.user.id, action: 'UPDATE_SETTING', entityType: 'SystemSettings', entityId: setting.id, detailsJson: { key: input.key, isSecret: setting.isSecret, }, ipAddress: ctx.ip, userAgent: ctx.userAgent, }, }) return setting }), /** * Update multiple settings at once (upsert - creates if not exists) */ updateMultiple: superAdminProcedure .input( z.object({ settings: z.array( z.object({ key: z.string(), value: z.string(), category: z.enum(['AI', 'BRANDING', 'EMAIL', 'STORAGE', 'SECURITY', 'DEFAULTS', 'WHATSAPP']).optional(), }) ), }) ) .mutation(async ({ ctx, input }) => { // Infer category from key prefix if not provided const inferCategory = (key: string): 'AI' | 'BRANDING' | 'EMAIL' | 'STORAGE' | 'SECURITY' | 'DEFAULTS' | 'WHATSAPP' => { if (key.startsWith('openai') || key.startsWith('ai_')) return 'AI' if (key.startsWith('smtp_') || key.startsWith('email_')) return 'EMAIL' if (key.startsWith('storage_') || key.startsWith('local_storage') || key.startsWith('max_file') || key.startsWith('avatar_') || key.startsWith('allowed_file')) return 'STORAGE' if (key.startsWith('brand_') || key.startsWith('logo_') || key.startsWith('primary_') || key.startsWith('theme_')) return 'BRANDING' if (key.startsWith('whatsapp_')) return 'WHATSAPP' if (key.startsWith('security_') || key.startsWith('session_')) return 'SECURITY' return 'DEFAULTS' } const results = await Promise.all( input.settings.map((s) => ctx.prisma.systemSettings.upsert({ where: { key: s.key }, update: { value: s.value, updatedBy: ctx.user.id, }, create: { key: s.key, value: s.value, category: s.category || inferCategory(s.key), updatedBy: ctx.user.id, }, }) ) ) // Audit log await ctx.prisma.auditLog.create({ data: { userId: ctx.user.id, action: 'UPDATE_SETTINGS_BATCH', entityType: 'SystemSettings', detailsJson: { keys: input.settings.map((s) => s.key) }, ipAddress: ctx.ip, userAgent: ctx.userAgent, }, }) return results }), /** * Get all categories */ getCategories: adminProcedure.query(async ({ ctx }) => { const settings = await ctx.prisma.systemSettings.findMany({ select: { category: true }, distinct: ['category'], }) return settings.map((s) => s.category) }), /** * Test AI connection */ testAIConnection: superAdminProcedure.mutation(async ({ ctx }) => { const apiKeySetting = await ctx.prisma.systemSettings.findUnique({ where: { key: 'openai_api_key' }, }) if (!apiKeySetting?.value) { return { success: false, error: 'API key not configured' } } try { // Test OpenAI connection with a minimal request const response = await fetch('https://api.openai.com/v1/models', { headers: { Authorization: `Bearer ${apiKeySetting.value}`, }, }) if (response.ok) { return { success: true } } else { const error = await response.json() return { success: false, error: error.error?.message || 'Unknown error' } } } catch (error) { return { success: false, error: 'Connection failed' } } }), /** * Test email connection */ testEmailConnection: superAdminProcedure .input(z.object({ testEmail: z.string().email() })) .mutation(async ({ ctx, input }) => { try { const { sendTestEmail } = await import('@/lib/email') const success = await sendTestEmail(input.testEmail) return { success, error: success ? null : 'Failed to send test email' } } catch (error) { return { success: false, error: 'Email configuration error' } } }), /** * Get WhatsApp settings status */ getWhatsAppStatus: adminProcedure.query(async ({ ctx }) => { const [enabledSetting, providerSetting] = await Promise.all([ ctx.prisma.systemSettings.findUnique({ where: { key: 'whatsapp_enabled' }, }), ctx.prisma.systemSettings.findUnique({ where: { key: 'whatsapp_provider' }, }), ]) return { enabled: enabledSetting?.value === 'true', provider: providerSetting?.value || 'META', } }), /** * Test WhatsApp connection */ testWhatsAppConnection: superAdminProcedure.mutation(async () => { const provider = await getWhatsAppProvider() if (!provider) { return { success: false, error: 'WhatsApp not configured' } } const result = await provider.testConnection() const providerType = await getWhatsAppProviderType() return { ...result, provider: providerType, } }), /** * Send a test WhatsApp message */ sendTestWhatsApp: superAdminProcedure .input(z.object({ phoneNumber: z.string() })) .mutation(async ({ input }) => { const provider = await getWhatsAppProvider() if (!provider) { return { success: false, error: 'WhatsApp not configured' } } return provider.sendText( input.phoneNumber, 'This is a test message from MOPC Platform.' ) }), /** * Update user notification preferences */ updateNotificationPreferences: protectedProcedure .input( z.object({ phoneNumber: z.string().optional().nullable(), notificationPreference: z.enum(['EMAIL', 'WHATSAPP', 'BOTH', 'NONE']), whatsappOptIn: z.boolean(), }) ) .mutation(async ({ ctx, input }) => { const user = await ctx.prisma.user.update({ where: { id: ctx.user.id }, data: { phoneNumber: input.phoneNumber, notificationPreference: input.notificationPreference, whatsappOptIn: input.whatsappOptIn, }, }) // Audit log await ctx.prisma.auditLog.create({ data: { userId: ctx.user.id, action: 'UPDATE_NOTIFICATION_PREFERENCES', entityType: 'User', entityId: ctx.user.id, detailsJson: { notificationPreference: input.notificationPreference, whatsappOptIn: input.whatsappOptIn, }, ipAddress: ctx.ip, userAgent: ctx.userAgent, }, }) return user }), /** * Get notification statistics (admin only) */ getNotificationStats: adminProcedure .input( z.object({ startDate: z.string().datetime().optional(), endDate: z.string().datetime().optional(), }) ) .query(async ({ ctx, input }) => { const where: Record = {} if (input.startDate || input.endDate) { where.createdAt = {} if (input.startDate) { (where.createdAt as Record).gte = new Date(input.startDate) } if (input.endDate) { (where.createdAt as Record).lte = new Date(input.endDate) } } const [total, byChannel, byStatus, byType] = await Promise.all([ ctx.prisma.notificationLog.count({ where }), ctx.prisma.notificationLog.groupBy({ by: ['channel'], where, _count: true, }), ctx.prisma.notificationLog.groupBy({ by: ['status'], where, _count: true, }), ctx.prisma.notificationLog.groupBy({ by: ['type'], where, _count: true, }), ]) return { total, byChannel: Object.fromEntries( byChannel.map((r) => [r.channel, r._count]) ), byStatus: Object.fromEntries( byStatus.map((r) => [r.status, r._count]) ), byType: Object.fromEntries( byType.map((r) => [r.type, r._count]) ), } }), })