require('dotenv').config(); const express = require('express'); const cors = require('cors'); const { requireAuth } = require('./auth'); const { ensureDefaultAdmin } = require('./users'); const audit = require('./audit'); const secrets = require('./secrets'); const schedule = require('node-schedule'); const zonesRouter = require('./routes/zones'); const secretsRouter = require('./routes/secrets'); const ipamRouter = require('./routes/ipam'); const healthRouter = require('./routes/health'); const recordsRouter = require('./routes/records'); const settingsRouter = require('./routes/settings'); const authRouter = require('./routes/auth'); const usersRouter = require('./routes/users'); const db = require('./db'); // Create default admin user if none exist ensureDefaultAdmin(); const app = express(); app.use(cors()); app.use(express.json()); // ─── Public routes (no auth required) ──────────────────────────────────────── app.get('/api/ping', (req, res) => res.json({ ok: true })); app.use('/api/auth', authRouter); // ─── Protected routes (JWT required) ───────────────────────────────────────── app.use('/api/zones', requireAuth, zonesRouter); app.use('/api/records', requireAuth, recordsRouter); app.use('/api/settings', requireAuth, settingsRouter); app.use('/api/users', usersRouter); app.use('/api/secrets', secretsRouter); app.use('/api/ipam', ipamRouter); app.use('/api/health', healthRouter); // requireAuth applied inside router app.get('/api/audit', requireAuth, (req, res) => { const { limit, offset, user, action, provider, category } = req.query; res.json(audit.getEntries({ limit: Math.min(parseInt(limit) || 100, 500), offset: parseInt(offset) || 0, user, action, provider, category, })); }); app.get('/api/sync-status/:provider', requireAuth, (req, res) => { res.json(db.getProviderSyncStatus(req.params.provider)); }); app.get('/api/domains', requireAuth, (req, res) => { res.json(db.getAllZones()); }); app.get('/api/stats', requireAuth, (req, res) => { res.json(db.getStats()); }); app.get('/api/providers', requireAuth, (req, res) => { const providers = []; const disabled = new Set((process.env.DISABLED_PROVIDERS ?? '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean)); if (!disabled.has('cloudflare') && process.env.CLOUDFLARE_API_TOKEN) providers.push({ id: 'cloudflare', name: 'Cloudflare' }); if (!disabled.has('loopia') && process.env.LOOPIA_USER && process.env.LOOPIA_PASSWORD) providers.push({ id: 'loopia', name: 'Loopia' }); if (!disabled.has('pihole') && process.env.PIHOLE_URL && process.env.PIHOLE_PASSWORD) providers.push({ id: 'pihole', name: 'Pi-hole', url: process.env.PIHOLE_URL }); if (!disabled.has('azure') && process.env.AZURE_TENANT_ID && process.env.AZURE_CLIENT_ID && process.env.AZURE_CLIENT_SECRET && process.env.AZURE_SUBSCRIPTION_ID) providers.push({ id: 'azure', name: 'Azure DNS' }); if (!disabled.has('cpanel') && process.env.CPANEL_URL && process.env.CPANEL_USERNAME && process.env.CPANEL_API_TOKEN) providers.push({ id: 'cpanel', name: 'cPanel', url: process.env.CPANEL_URL }); res.json(providers); }); // ─── Daily secrets expiry check (runs at 08:00 every day) ──────────────────── const { notify } = require('./notify'); const LAST_CHECK_PATH = require('path').join(__dirname, '..', '.last-secret-check'); function getLastCheckDate() { try { return require('fs').readFileSync(LAST_CHECK_PATH, 'utf8').trim(); } catch { return null; } } function saveLastCheckDate(date) { require('fs').writeFileSync(LAST_CHECK_PATH, date, 'utf8'); } async function checkSecretExpiry() { const expiring = secrets.getExpiring(); if (expiring.length === 0) return; const lines = expiring.map(s => s.status === 'expired' ? `✕ EXPIRED — ${s.name}` : `⚠ ${s.daysLeft}d left — ${s.name}` ); await notify( `🦥 Sloth Manager — Secrets Alert`, `${expiring.length} secret${expiring.length !== 1 ? 's' : ''} need attention:\n\n${lines.join('\n')}` ); } async function checkSecretExpiryOnce() { const today = new Date().toDateString(); if (getLastCheckDate() === today) return; // already ran today, persisted to disk saveLastCheckDate(today); await checkSecretExpiry(); } // Run at startup only if not already checked today (persisted to disk), then every day at 08:00 checkSecretExpiryOnce(); schedule.scheduleJob('0 8 * * *', () => { saveLastCheckDate(''); // clear so the scheduled job always fires checkSecretExpiry(); }); const PORT = process.env.PORT || 3001; app.listen(PORT, () => console.log(`Sloth Manager backend running on port ${PORT}`));