const express = require('express'); const rateLimit = require('express-rate-limit'); const { ChannelType } = require('discord.js'); const { CONFIG } = require('../config'); const { applyConfigUpdates, readAllConfig } = require('../services/configPersistence'); const { logSystem } = require('../services/debugLog'); const router = express.Router(); const internalLimiter = rateLimit({ windowMs: 60 * 1000, max: 10, standardHeaders: true, legacyHeaders: false, message: { error: 'Too many requests, please try again later.' } }); router.use(internalLimiter); // Middleware: verify internal secret router.use((req, res, next) => { const secret = req.headers['x-internal-secret']; if (!CONFIG.INTERNAL_API_SECRET || secret !== CONFIG.INTERNAL_API_SECRET) { return res.status(401).json({ error: 'Unauthorized' }); } next(); }); // GET /config — return all current .env values (redacted secrets) router.get('/config', (req, res) => { const map = readAllConfig(); const obj = {}; const REDACTED = ['DISCORD_TOKEN', 'REFRESH_TOKEN', 'GOOGLE_CLIENT_SECRET', 'MONGODB_URI', 'INTERNAL_API_SECRET', 'SETTINGS_ADMIN_PASSWORD']; for (const [k, v] of map) { obj[k] = REDACTED.includes(k) ? '••••••••' : v; } res.json(obj); }); // POST /config — apply config updates (allowlisted keys only) const ALLOWED_CONFIG_KEYS = new Set([ // Ticket settings 'TICKET_CATEGORY_ID', 'TICKET_CATEGORY_NAME', 'TICKET_T2_CATEGORY_NAME', 'TICKET_T3_CATEGORY_NAME', 'EMAIL_TICKET_OVERFLOW_CATEGORY_IDS', 'DISCORD_TICKET_CATEGORY_ID', 'DISCORD_TICKET_OVERFLOW_CATEGORY_IDS', 'DISCORD_THREAD_CHANNEL_ID', 'EMAIL_THREAD_CHANNEL_ID', 'THREAD_PARENT_CHANNEL', 'USE_THREADS', // Escalation categories 'EMAIL_ESCALATED2_CHANNEL_ID', 'DISCORD_ESCALATED2_CHANNEL_ID', 'EMAIL_ESCALATED3_CHANNEL_ID', 'DISCORD_ESCALATED3_CHANNEL_ID', // Roles and staff 'ROLE_ID_TO_PING', 'ROLE_TO_PING_ID', 'ADDITIONAL_STAFF_ROLES', 'BLACKLISTED_ROLES', 'STAFF_IDS', 'ADMIN_ID', 'STAFF_EMOJIS', 'CLAIMER_EMOJI_FALLBACK', // Channel IDs 'TRANSCRIPT_CHANNEL_ID', 'LOGGING_CHANNEL_ID', 'DEBUGGING_CHANNEL_ID', 'BACKUP_EXPORT_CHANNEL_ID', 'ACCOUNT_INFO_CHANNEL_ID', 'DISCORD_CHANNEL_ID', 'GMAIL_LOG_CHANNEL_ID', 'AUTOMATION_LOG_CHANNEL_ID', 'RENAME_LOG_CHANNEL_ID', 'SECURITY_LOG_CHANNEL_ID', 'SYSTEM_LOG_CHANNEL_ID', 'ALL_STAFF_CHANNEL_ID', 'ALL_STAFF_CHAT_ALERT_CHANNEL_ID', 'STAFF_NOTIFICATION_CATEGORY_ID', // Pattern channel IDs 'USER_PATTERNS_CHANNEL_ID', 'GAME_PATTERNS_CHANNEL_ID', 'TAG_PATTERNS_CHANNEL_ID', 'ESCALATION_PATTERNS_CHANNEL_ID', 'STAFF_PATTERNS_CHANNEL_ID', 'COMBINED_PATTERNS_CHANNEL_ID', // Messages and labels 'ESCALATION_MESSAGE', 'TICKET_CLOSE_SUBJECT_PREFIX', 'TICKET_CLOSE_MESSAGE', 'TICKET_CLOSE_SIGNATURE', 'DISCORD_CLOSE_MESSAGE', 'DISCORD_TRANSCRIPT_MESSAGE', 'DISCORD_AUTO_CLOSE_MESSAGE', 'AUTO_CLOSE_MESSAGE', 'TICKET_WELCOME_MESSAGE', 'TICKET_CLAIMED_MESSAGE', 'TICKET_UNCLAIMED_MESSAGE', 'REMINDER_MESSAGE', 'BUTTON_LABEL_CLOSE', 'BUTTON_LABEL_CLAIM', 'BUTTON_LABEL_UNCLAIM', 'BUTTON_EMOJI_CLOSE', 'BUTTON_EMOJI_CLAIM', 'BUTTON_EMOJI_UNCLAIM', // Branding 'LOGO_URL', 'SUPPORT_NAME', 'EMAIL_SIGNATURE', 'GAME_LIST', // Toggles 'AUTO_CLOSE_ENABLED', 'AUTO_CLOSE_AFTER_HOURS', 'AUTO_UNCLAIM_ENABLED', 'AUTO_UNCLAIM_AFTER_HOURS', 'CLAIM_TIMEOUT_ENABLED', 'CLAIM_TIMEOUT_HOURS', 'ALLOW_CLAIM_OVERWRITE', 'REMINDER_ENABLED', 'REMINDER_AFTER_HOURS', 'PRIORITY_ENABLED', 'DEFAULT_PRIORITY', 'STAFF_THREAD_ENABLED', 'STAFF_THREAD_NAME', 'STAFF_THREAD_AUTO_ADD_ROLE', 'STAFF_THREAD_ROLE_ID', 'PIN_INITIAL_MESSAGE_ENABLED', 'PIN_ESCALATION_MESSAGE_ENABLED', 'PIN_SUPPRESS_SYSTEM_MESSAGE', 'STAFF_DND_COUNTS_AS_AVAILABLE', // Limits and thresholds 'GLOBAL_TICKET_LIMIT', 'TICKET_LIMIT_PER_CATEGORY', 'RATE_LIMIT_TICKETS_PER_USER', 'RATE_LIMIT_WINDOW_MINUTES', 'FORCE_CLOSE_TIMER_SECONDS', 'GMAIL_POLL_INTERVAL_SECONDS', // Embed colors 'EMBED_COLOR_OPEN', 'EMBED_COLOR_CLOSED', 'EMBED_COLOR_CLAIMED', 'EMBED_COLOR_ESCALATED', 'EMBED_COLOR_INFO', 'PRIORITY_HIGH_EMOJI', 'PRIORITY_MEDIUM_EMOJI', 'PRIORITY_LOW_EMOJI', // Pattern thresholds 'PATTERN_USER_TICKET_THRESHOLD', 'PATTERN_GAME_TICKET_THRESHOLD', 'PATTERN_STAFF_STALE_PING_THRESHOLD', 'PATTERN_ESCALATION_THRESHOLD', 'PATTERN_RAPID_CLOSE_SECONDS', 'PATTERN_UNCLAIMED_HOURS', 'PATTERN_CHECK_INTERVAL_MINUTES', // Surge settings 'SURGE_ROLE_ID', 'SURGE_TICKET_COUNT', 'SURGE_TICKET_WINDOW_MINUTES', 'SURGE_GAME_TICKET_COUNT', 'SURGE_GAME_TICKET_WINDOW_MINUTES', 'SURGE_STALE_COUNT', 'SURGE_STALE_HOURS', 'SURGE_NEEDS_RESPONSE_COUNT', 'SURGE_NEEDS_RESPONSE_HOURS', 'SURGE_UNCLAIMED_COUNT', 'SURGE_UNCLAIMED_MINUTES', 'SURGE_TIER3_UNCLAIMED_MINUTES', 'SURGE_COOLDOWN_MINUTES', 'SURGE_NO_STAFF_COOLDOWN_MINUTES', 'SURGE_NO_STAFF_OPEN_TICKET_THRESHOLD', // Chat alerts 'CHAT_ALERT_CHANNEL_IDS', 'CHAT_ALERT_MESSAGE_COUNT', 'CHAT_ALERT_HOURS_WITHOUT_RESPONSE', 'CHAT_ALERT_COOLDOWN_MINUTES', // Notification thresholds 'NOTIFICATION_THRESHOLDS_JSON', 'UNCLAIMED_REMINDER_THRESHOLDS' ]); router.post('/config', express.json(), async (req, res) => { const updates = req.body; if (!updates || typeof updates !== 'object' || Array.isArray(updates)) { return res.status(400).json({ error: 'Invalid body' }); } const rejected = Object.keys(updates).filter(k => !ALLOWED_CONFIG_KEYS.has(k)); if (rejected.length > 0) { return res.status(400).json({ error: `Disallowed config keys: ${rejected.join(', ')}` }); } const result = applyConfigUpdates(updates); await logSystem('Config updated via settings UI', [ { name: 'Keys updated', value: result.applied.join(', ') || 'none', inline: false }, { name: 'Errors', value: result.errors.join(', ') || 'none', inline: false } ]).catch(() => {}); res.json(result); }); // GET /discord/guild — return guild info for smart dropdowns router.get('/discord/guild', async (req, res) => { try { const client = require('../api/bosscordClient').getBot(); if (!client) return res.status(503).json({ error: 'Bot not ready' }); const guild = client.guilds.cache.get(CONFIG.DISCORD_GUILD_ID); if (!guild) return res.status(404).json({ error: 'Guild not found' }); await guild.members.fetch().catch(() => {}); const CHANNEL_TYPES = [ ChannelType.GuildText, ChannelType.GuildCategory, ChannelType.GuildAnnouncement, ChannelType.GuildForum ]; const channels = guild.channels.cache .filter(c => CHANNEL_TYPES.includes(c.type)) .map(c => ({ id: c.id, name: c.name, type: c.type, parentId: c.parentId })) .sort((a, b) => a.name.localeCompare(b.name)); const roles = guild.roles.cache .filter(r => !r.managed && r.id !== guild.id) .map(r => ({ id: r.id, name: r.name, color: r.hexColor })) .sort((a, b) => b.position - a.position); const members = guild.members.cache .filter(m => !m.user.bot) .map(m => ({ id: m.id, username: m.user.username, displayName: m.displayName, avatar: m.user.displayAvatarURL({ size: 32 }) })) .sort((a, b) => a.displayName.localeCompare(b.displayName)); const categories = guild.channels.cache .filter(c => c.type === ChannelType.GuildCategory) .map(c => ({ id: c.id, name: c.name })) .sort((a, b) => a.name.localeCompare(b.name)); res.json({ channels, roles, members, categories }); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /restart — restart the bot process let scheduledRestart = null; router.post('/restart', express.json(), (req, res) => { const { mode, scheduledFor } = req.body; if (mode === 'immediate') { res.json({ ok: true, mode }); setTimeout(() => { console.log('[restart] Restarting bot process...'); process.exit(0); // Docker/systemd will restart }, 1500); return; } if (mode === 'scheduled' && scheduledFor) { const delay = new Date(scheduledFor).getTime() - Date.now(); if (delay <= 0) return res.status(400).json({ error: 'Scheduled time is in the past' }); if (scheduledRestart) clearTimeout(scheduledRestart); scheduledRestart = setTimeout(() => { console.log('[restart] Scheduled restart firing...'); process.exit(0); }, delay); res.json({ ok: true, mode, scheduledFor, delayMs: delay }); return; } if (mode === 'cancel_scheduled') { if (scheduledRestart) { clearTimeout(scheduledRestart); scheduledRestart = null; } res.json({ ok: true, cancelled: true }); return; } if (mode === 'pending') { res.json({ ok: true, mode: 'pending', note: 'Restart required on next manual restart' }); return; } res.status(400).json({ error: 'Invalid mode' }); }); router.get('/restart/status', (req, res) => { res.json({ scheduledRestart: !!scheduledRestart }); }); module.exports = router;