import { Router, Request, Response, NextFunction } from 'express'; import { UserRole } from '@prisma/client'; import { siteSettingsService } from './settings.service'; import { updateSiteSettingsSchema } from './settings.schemas'; import { validate } from '../../middleware/validate'; import { authenticate } from '../../middleware/auth.middleware'; import { requireRole } from '../../middleware/rbac.middleware'; import { emailService } from '../../services/email.service'; import { giteaClient } from '../../services/gitea.client'; import { gancioSettingsSyncService } from '../../services/gancio-settings-sync.service'; import { headerBuilderService } from '../docs/header-builder.service'; import { mkdocsConfigService } from '../docs/mkdocs-config.service'; import { logger } from '../../utils/logger'; const router = Router(); // GET /api/settings — public (needed by login page + public pages), strips SMTP credentials router.get( '/', async (_req: Request, res: Response, next: NextFunction) => { try { const settings = await siteSettingsService.getPublic(); res.json(settings); } catch (err) { next(err); } } ); // GET /api/settings/admin — SUPER_ADMIN only, returns full settings including SMTP router.get( '/admin', authenticate, requireRole(UserRole.SUPER_ADMIN), async (_req: Request, res: Response, next: NextFunction) => { try { const settings = await siteSettingsService.getEffective(); res.json(settings); } catch (err) { next(err); } } ); // POST /api/settings/email/test-connection — SUPER_ADMIN only router.post( '/email/test-connection', authenticate, requireRole(UserRole.SUPER_ADMIN), async (_req: Request, res: Response, next: NextFunction) => { try { const success = await emailService.testConnection(); res.json({ success, message: success ? 'SMTP connection verified' : 'SMTP connection failed' }); } catch (err) { next(err); } } ); // POST /api/settings/email/test-send — SUPER_ADMIN only router.post( '/email/test-send', authenticate, requireRole(UserRole.SUPER_ADMIN), async (req: Request, res: Response, next: NextFunction) => { try { const { to } = req.body as { to?: string }; const settings = await siteSettingsService.get(); const recipient = to || settings.testEmailRecipient || 'admin@cmlite.org'; const result = await emailService.sendEmail({ to: recipient, subject: 'Changemaker Lite — Test Email', html: `
This email confirms that your SMTP configuration is working correctly.
Sent at: ${new Date().toISOString()}
`, text: `SMTP Test Successful\n\nThis email confirms that your SMTP configuration is working correctly.\n\nSent at: ${new Date().toISOString()}`, }); res.json({ success: result.success, messageId: result.messageId, testMode: result.testMode, recipient, }); } catch (err) { next(err); } } ); // PUT /api/settings — SUPER_ADMIN only router.put( '/', authenticate, requireRole(UserRole.SUPER_ADMIN), validate(updateSiteSettingsSchema), async (req: Request, res: Response, next: NextFunction) => { try { const settings = await siteSettingsService.update(req.body); // If SMTP-related fields were updated, rebuild the transporter const smtpFields = ['smtpHost', 'smtpPort', 'smtpUser', 'smtpPass', 'smtpFromAddress', 'smtpActiveProvider', 'emailTestMode', 'testEmailRecipient']; const hasSmtpChanges = smtpFields.some((f) => f in req.body); if (hasSmtpChanges) { await emailService.rebuildTransporter(); } // If Gitea-related fields were updated, invalidate the config cache const giteaFields = ['enableDocsComments', 'giteaApiToken', 'giteaCommentsRepoOwner', 'giteaCommentsRepoName', 'giteaOauthClientId', 'giteaOauthClientSecret']; if (giteaFields.some((f) => f in req.body)) { giteaClient.clearConfigCache(); } // If Gancio-relevant fields were updated, sync to Gancio (fire-and-forget) if (gancioSettingsSyncService.hasGancioChanges(req.body)) { gancioSettingsSyncService.syncChanged(req.body).catch(() => {}); } // If navConfig or theme colors changed, trigger MkDocs header rebuild + docs build const headerTriggerFields = [ 'navConfig', 'publicHeaderGradient', 'publicColorBgBase', 'publicColorBgContainer', 'enableInfluence', 'enableMap', 'enableMediaFeatures', 'enablePayments', 'enableEvents', 'enableMeetingPlanner', 'enableTicketedEvents', 'enableSocial', 'enableMeet', 'enableLandingPages', ]; if (headerTriggerFields.some((f) => f in req.body)) { if ('navConfig' in req.body) { gancioSettingsSyncService.syncAll().catch(() => {}); } // Regenerate MkDocs header from navConfig, then trigger a docs build const navItems = (settings.navConfig as Record