172 lines
6.6 KiB
TypeScript
172 lines
6.6 KiB
TypeScript
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: `<h2>SMTP Test Successful</h2><p>This email confirms that your SMTP configuration is working correctly.</p><p>Sent at: ${new Date().toISOString()}</p>`,
|
|
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<string, unknown> | null)?.items as unknown[] | undefined;
|
|
if (navItems?.length) {
|
|
headerBuilderService.regenerateFromNavConfig(
|
|
navItems as any[],
|
|
{
|
|
publicHeaderGradient: settings.publicHeaderGradient ?? undefined,
|
|
publicColorBgBase: settings.publicColorBgBase ?? undefined,
|
|
publicColorBgContainer: settings.publicColorBgContainer ?? undefined,
|
|
enableInfluence: settings.enableInfluence,
|
|
enableMap: settings.enableMap,
|
|
enableMediaFeatures: settings.enableMediaFeatures,
|
|
enablePayments: settings.enablePayments,
|
|
enableEvents: settings.enableEvents,
|
|
enableMeetingPlanner: settings.enableMeetingPlanner,
|
|
enableTicketedEvents: settings.enableTicketedEvents,
|
|
enableSocial: settings.enableSocial,
|
|
enableMeet: settings.enableMeet,
|
|
enableLandingPages: settings.enableLandingPages,
|
|
},
|
|
).then(() => {
|
|
// Fire-and-forget docs build so the static site picks up the new header
|
|
mkdocsConfigService.triggerBuild()
|
|
.then((result) => {
|
|
if (result.success) {
|
|
logger.info(`MkDocs rebuild after header change completed in ${result.duration}ms`);
|
|
} else {
|
|
logger.warn(`MkDocs rebuild after header change failed: ${result.output}`);
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}).catch(() => {});
|
|
}
|
|
}
|
|
|
|
res.json(settings);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
}
|
|
);
|
|
|
|
export { router as siteSettingsRouter };
|