godcrm/backend/routes/v3/system/settingsController.js
GOD CRM Release 065f2edd23
Some checks are pending
CI / Lint / Typecheck / Test / Build (push) Waiting to run
CI / PostgreSQL Integration Tests (push) Waiting to run
GOD CRM — public scrubbed snapshot (2026-08-30)
Refresh of the open-core distribution from the private tree.

Included since the previous snapshot:
- Mail module (ADR-158/159/160/169): composer, labels, scheduling,
  attachments, reply-tokens, IMAP/SMTP bridge + migrations 079-083
- Crawler-readable SSR for /blog and public spaces (ADR-190):
  blogSeo, publicDocsSeo, per-space SEO prefs, blog index/post pages
- Registration policy + referral/promo settings (ADR-183/188)
- Message translation + language detection (ADR-185)
- Reddit connector for the agent-tool surface

Excised from the public distribution (unchanged policy): infrastructure
topology and host config, internal ops scripts, DB cleanup snapshots,
business documents, throwaway debug scripts, and two private product
lines (SC-SIM simulator, personal one-off tools). Real host addresses
are replaced with placeholders; credential-shaped literals are redacted.

Frontend build verified green on this tree.
2026-08-30 15:13:28 +03:00

315 lines
9.8 KiB
JavaScript

// system/settingsController.js — Rate limits, system info, settings, SMTP
import express from 'express';
import CryptoJS from 'crypto-js';
import { dbGet, dbRun, dbAll } from '../../../database/connection.js';
import { apiLogger } from '../../../utils/logger.js';
import SMTPService from '../../../services/SMTPService.js';
import { success, badRequest, serverError } from '../../../utils/response.js';
import { ownerOnly } from './helpers.js';
import { isPromoEnabled, PROMO_FLAG_KEY } from '../../../services/starter-pack/StarterPackService.js';
const router = express.Router();
/**
* GET /api/v3/system/promo-settings — ADR-183-A D6 (owner only).
* Read the promo kill-switch. Default ON when the row is absent (fail-open).
*/
router.get('/promo-settings', ownerOnly, async (req, res) => {
try {
return success(res, { promo_enabled: await isPromoEnabled() });
} catch (error) {
apiLogger.error('Error reading promo settings:', error);
return serverError(res, error.message);
}
});
/**
* PUT /api/v3/system/promo-settings — ADR-183-A D6 (owner only).
* Flip the promo kill-switch. Body: { promo_enabled: boolean }. Stored in
* _app_settings (JSONB), the same store StarterPackService reads.
*/
router.put('/promo-settings', ownerOnly, async (req, res) => {
try {
const { promo_enabled } = req.body;
if (typeof promo_enabled !== 'boolean') {
return badRequest(res, 'promo_enabled must be a boolean', 'VALIDATION_ERROR');
}
await dbRun(
`INSERT INTO _app_settings (key, value, updated_by, updated_at)
VALUES (?, ?::jsonb, ?, NOW())
ON CONFLICT (key) DO UPDATE
SET value = EXCLUDED.value,
updated_by = EXCLUDED.updated_by,
updated_at = NOW()`,
[PROMO_FLAG_KEY, JSON.stringify(promo_enabled), req.user.id]
);
apiLogger.info({ userId: req.user.id, promo_enabled }, 'Promo settings updated');
return success(res, { promo_enabled });
} catch (error) {
apiLogger.error('Error updating promo settings:', error);
return serverError(res, error.message);
}
});
// In-memory storage for verification codes
const verificationCodes = new Map();
/**
* GET /api/v3/system/rate-limit-config
* Get current rate limit configuration
*/
router.get('/rate-limit-config', ownerOnly, async (req, res) => {
try {
// Get from system_settings table or return defaults
const setting = await dbGet(
"SELECT value FROM system_settings WHERE key = 'rate_limit_config'"
);
const config = setting?.value
? (typeof setting.value === 'string' ? JSON.parse(setting.value) : setting.value)
: {
auth_max_attempts: 10,
auth_window_minutes: 15,
global_max_requests: 1000,
global_window_minutes: 15
};
return success(res, config);
} catch (error) {
apiLogger.error('Error fetching rate limit config:', error);
return serverError(res, error.message);
}
});
/**
* PUT /api/v3/system/rate-limit-config
* Update rate limit configuration
*/
router.put('/rate-limit-config', ownerOnly, async (req, res) => {
try {
const { auth_max_attempts, auth_window_minutes, global_max_requests, global_window_minutes } = req.body;
// Validate
if (auth_max_attempts < 1 || auth_max_attempts > 100) {
return badRequest(res, 'auth_max_attempts must be between 1 and 100', 'VALIDATION_ERROR');
}
const config = {
auth_max_attempts: parseInt(auth_max_attempts) || 10,
auth_window_minutes: parseInt(auth_window_minutes) || 15,
global_max_requests: parseInt(global_max_requests) || 1000,
global_window_minutes: parseInt(global_window_minutes) || 15
};
// Ensure system_settings table exists
await dbRun(`
CREATE TABLE IF NOT EXISTS system_settings (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Upsert setting
await dbRun(
`INSERT INTO system_settings (key, value, updated_at)
VALUES ('rate_limit_config', ?, CURRENT_TIMESTAMP)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP`,
[JSON.stringify(config)]
);
apiLogger.info({ userId: req.user.id, config }, 'Rate limit config updated');
return success(res, { ...config, message: 'Rate limit configuration updated. Restart server to apply changes.' });
} catch (error) {
apiLogger.error('Error updating rate limit config:', error);
return serverError(res, error.message);
}
});
/**
* GET /api/v3/system/info
* Get system info (owner only)
*/
router.get('/info', ownerOnly, async (req, res) => {
try {
const userCount = await dbGet('SELECT COUNT(*) as count FROM users');
const spaceCount = await dbGet('SELECT COUNT(*) as count FROM spaces');
const projectCount = await dbGet('SELECT COUNT(*) as count FROM projects');
return success(res, {
version: process.env.npm_package_version || '0.003.000',
node_version: process.version,
environment: process.env.NODE_ENV || 'development',
database: 'PostgreSQL',
stats: {
users: userCount?.count || 0,
spaces: spaceCount?.count || 0,
projects: projectCount?.count || 0
}
});
} catch (error) {
apiLogger.error('Error fetching system info:', error);
return serverError(res, error.message);
}
});
/**
* GET /api/v3/system/settings
* Get system settings (non-sensitive only)
*/
router.get('/settings', ownerOnly, async (req, res) => {
try {
const settings = await dbAll(
'SELECT key, value FROM system_settings WHERE key NOT LIKE ?',
['%password%']
);
const settingsObj = {};
for (const setting of settings) {
settingsObj[setting.key] = setting.value;
}
return success(res, settingsObj);
} catch (error) {
apiLogger.error('Error fetching system settings:', error);
return serverError(res, error.message);
}
});
/**
* POST /api/v3/system/smtp-settings
* Save SMTP configuration and send verification email
*/
router.post('/smtp-settings', ownerOnly, async (req, res) => {
try {
const { host, port, user, password, from } = req.body;
// Validate
if (!host || !port || !user || !password || !from) {
return badRequest(res, 'All SMTP fields are required', 'VALIDATION_ERROR');
}
const smtpConfig = { host, port: parseInt(port), user, password, from };
// Validate SMTP config
try {
SMTPService.validate(smtpConfig);
} catch (validationError) {
return badRequest(res, validationError.message, 'VALIDATION_ERROR');
}
// Generate verification code
const code = SMTPService.generateVerificationCode();
// Send test email
const emailResult = await SMTPService.sendTestEmail(
smtpConfig,
req.user.email,
code
);
if (!emailResult.success) {
return badRequest(res, 'Failed to send test email: ' + emailResult.error, 'SMTP_SEND_FAILED');
}
// Store verification code (10 minutes)
verificationCodes.set(req.user.id, {
code,
config: smtpConfig,
expiresAt: Date.now() + 10 * 60 * 1000
});
// Auto-cleanup
setTimeout(() => verificationCodes.delete(req.user.id), 10 * 60 * 1000);
apiLogger.info({ userId: req.user.id, host }, 'SMTP verification email sent');
return success(res, { message: 'Verification code sent to ' + req.user.email });
} catch (error) {
apiLogger.error('Error saving SMTP settings:', error);
return serverError(res, error.message);
}
});
/**
* POST /api/v3/system/smtp-verify
* Verify code and save SMTP configuration
*/
router.post('/smtp-verify', ownerOnly, async (req, res) => {
try {
const { code } = req.body;
if (!code) {
return badRequest(res, 'Verification code is required', 'CODE_REQUIRED');
}
const verificationData = verificationCodes.get(req.user.id);
if (!verificationData) {
return badRequest(res, 'Verification code expired or not found', 'CODE_EXPIRED');
}
if (Date.now() > verificationData.expiresAt) {
verificationCodes.delete(req.user.id);
return badRequest(res, 'Verification code has expired', 'CODE_EXPIRED');
}
if (code !== verificationData.code) {
return badRequest(res, 'Invalid verification code', 'INVALID_CODE');
}
// Encrypt password
const encryptionKey = process.env.ENCRYPTION_KEY || process.env.JWT_SECRET;
const encryptedPassword = CryptoJS.AES.encrypt(
verificationData.config.password,
encryptionKey
).toString();
// Save SMTP config
const smtpConfigJson = JSON.stringify({
host: verificationData.config.host,
port: verificationData.config.port,
user: verificationData.config.user,
password: encryptedPassword,
from: verificationData.config.from
});
// Ensure table exists
await dbRun(`
CREATE TABLE IF NOT EXISTS system_settings (
key TEXT PRIMARY KEY,
value TEXT,
encrypted INTEGER DEFAULT 0,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await dbRun(
`INSERT INTO system_settings (key, value, encrypted, updated_at)
VALUES ('smtp_config', ?, 1, CURRENT_TIMESTAMP)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP`,
[smtpConfigJson]
);
await dbRun(
`INSERT INTO system_settings (key, value, encrypted, updated_at)
VALUES ('smtp_configured', 'true', 0, CURRENT_TIMESTAMP)
ON CONFLICT(key) DO UPDATE SET value = 'true', updated_at = CURRENT_TIMESTAMP`
);
verificationCodes.delete(req.user.id);
apiLogger.info({ userId: req.user.id }, 'SMTP configured successfully');
return success(res, { message: 'SMTP configured successfully' });
} catch (error) {
apiLogger.error('Error verifying SMTP:', error);
return serverError(res, error.message);
}
});
export default router;