- Build complete Next.js CRM for commercial real estate - Add authentication with JWT sessions and role-based access - Add GoHighLevel API integration for contacts, conversations, opportunities - Add AI-powered Control Center with tool calling - Add Setup page with onboarding checklist (/setup) - Add sidebar navigation with Setup menu item - Fix type errors in onboarding API, GHL services, and control center tools - Add Prisma schema with SQLite for local development - Add UI components with clay morphism design system Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
201 lines
5.2 KiB
TypeScript
201 lines
5.2 KiB
TypeScript
import { prisma } from '@/lib/db';
|
|
import { encrypt, decrypt, maskValue } from './encryption';
|
|
import { SystemSettings } from '@/types/admin';
|
|
|
|
// Keys that should be encrypted
|
|
const ENCRYPTED_KEYS = [
|
|
'ghlClientSecret',
|
|
'ghlAccessToken',
|
|
'ghlRefreshToken',
|
|
'ghlAgencyApiKey',
|
|
'ghlPrivateToken',
|
|
'ghlWebhookSecret',
|
|
'stripeSecretKey',
|
|
'stripeWebhookSecret',
|
|
'clickupApiKey',
|
|
'claudeApiKey',
|
|
'openaiApiKey',
|
|
'mcpServerUrl',
|
|
];
|
|
|
|
export class SettingsService {
|
|
// Get a single setting
|
|
async get(key: string): Promise<string | null> {
|
|
const setting = await prisma.systemSettings.findUnique({
|
|
where: { key },
|
|
});
|
|
|
|
if (!setting) return null;
|
|
|
|
return setting.isEncrypted ? decrypt(setting.value) : setting.value;
|
|
}
|
|
|
|
// Get all settings
|
|
async getAll(): Promise<SystemSettings> {
|
|
const settings = await prisma.systemSettings.findMany();
|
|
|
|
const result: Record<string, any> = {};
|
|
|
|
for (const setting of settings) {
|
|
result[setting.key] = setting.isEncrypted
|
|
? decrypt(setting.value)
|
|
: setting.value;
|
|
}
|
|
|
|
return result as SystemSettings;
|
|
}
|
|
|
|
// Get all settings with sensitive values masked (for display)
|
|
async getAllMasked(): Promise<Record<string, string>> {
|
|
const settings = await prisma.systemSettings.findMany();
|
|
|
|
const result: Record<string, string> = {};
|
|
|
|
for (const setting of settings) {
|
|
if (setting.isEncrypted) {
|
|
const decrypted = decrypt(setting.value);
|
|
result[setting.key] = maskValue(decrypted);
|
|
} else {
|
|
result[setting.key] = setting.value;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Set a single setting
|
|
async set(key: string, value: string, updatedBy?: string): Promise<void> {
|
|
const isEncrypted = ENCRYPTED_KEYS.includes(key);
|
|
const storedValue = isEncrypted ? encrypt(value) : value;
|
|
|
|
await prisma.systemSettings.upsert({
|
|
where: { key },
|
|
update: {
|
|
value: storedValue,
|
|
isEncrypted,
|
|
updatedBy,
|
|
updatedAt: new Date(),
|
|
},
|
|
create: {
|
|
key,
|
|
value: storedValue,
|
|
isEncrypted,
|
|
updatedBy,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Set multiple settings at once
|
|
async setMany(
|
|
settings: Partial<SystemSettings>,
|
|
updatedBy?: string
|
|
): Promise<void> {
|
|
const operations = Object.entries(settings).map(([key, value]) => {
|
|
if (value === undefined || value === null) return null;
|
|
|
|
const isEncrypted = ENCRYPTED_KEYS.includes(key);
|
|
const storedValue = isEncrypted ? encrypt(String(value)) : String(value);
|
|
|
|
return prisma.systemSettings.upsert({
|
|
where: { key },
|
|
update: {
|
|
value: storedValue,
|
|
isEncrypted,
|
|
updatedBy,
|
|
updatedAt: new Date(),
|
|
},
|
|
create: {
|
|
key,
|
|
value: storedValue,
|
|
isEncrypted,
|
|
updatedBy,
|
|
},
|
|
});
|
|
}).filter(Boolean);
|
|
|
|
await prisma.$transaction(operations as any);
|
|
}
|
|
|
|
// Delete a setting
|
|
async delete(key: string): Promise<void> {
|
|
await prisma.systemSettings.delete({
|
|
where: { key },
|
|
});
|
|
}
|
|
|
|
// Test GHL connection with current settings
|
|
async testGHLConnection(): Promise<{ success: boolean; error?: string; details?: any }> {
|
|
try {
|
|
const accessToken = await this.get('ghlAccessToken');
|
|
const locationId = await this.get('ghlLocationId');
|
|
|
|
if (!accessToken) {
|
|
return { success: false, error: 'Missing GHL Access Token. Please configure OAuth credentials.' };
|
|
}
|
|
|
|
if (!locationId) {
|
|
return { success: false, error: 'Missing GHL Location ID.' };
|
|
}
|
|
|
|
// Test v2 API connection by fetching location details
|
|
const response = await fetch(
|
|
`https://services.leadconnectorhq.com/locations/${locationId}`,
|
|
{
|
|
headers: {
|
|
'Authorization': `Bearer ${accessToken}`,
|
|
'Version': '2021-07-28',
|
|
'Accept': 'application/json',
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
return {
|
|
success: false,
|
|
error: `GHL API returned ${response.status}: ${response.statusText}`,
|
|
details: errorText
|
|
};
|
|
}
|
|
|
|
const data = await response.json();
|
|
return {
|
|
success: true,
|
|
details: { locationName: data.location?.name || data.name || 'Connected' }
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
};
|
|
}
|
|
}
|
|
|
|
// Test Stripe connection
|
|
async testStripeConnection(): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
const secretKey = await this.get('stripeSecretKey');
|
|
|
|
if (!secretKey) {
|
|
return { success: false, error: 'Missing Stripe secret key' };
|
|
}
|
|
|
|
// We'll implement actual Stripe test later
|
|
// For now, just check the key format
|
|
if (!secretKey.startsWith('sk_')) {
|
|
return { success: false, error: 'Invalid Stripe key format' };
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
export const settingsService = new SettingsService();
|