MOPC-App/src/server/routers/settings.ts

377 lines
10 KiB
TypeScript
Raw Normal View History

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<string, unknown> = {}
if (input.startDate || input.endDate) {
where.createdAt = {}
if (input.startDate) {
(where.createdAt as Record<string, Date>).gte = new Date(input.startDate)
}
if (input.endDate) {
(where.createdAt as Record<string, Date>).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])
),
}
}),
})