Files
sloth-manager/backend/src/index.js
T
2026-07-13 23:54:59 +02:00

156 lines
6.1 KiB
JavaScript

require('dotenv').config();
const express = require('express');
const cors = require('cors');
const { requireAuth } = require('./auth');
const { ensureDefaultAdmin } = require('./users');
const audit = require('./audit');
const settings = require('./settings');
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 diag = require('./diag');
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);
app.get('/api/diag', requireAuth, (req, res) => {
const { provider, ok, limit, offset } = req.query;
res.json(diag.getEntries({
provider,
ok: ok !== undefined ? ok === 'true' : undefined,
limit: Math.min(parseInt(limit) || 100, 200),
offset: parseInt(offset) || 0,
}));
});
app.delete('/api/diag', requireAuth, (req, res) => {
diag.clearLog();
res.json({ success: true });
}); // 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 });
if (!disabled.has('technitium') && process.env.TECHNITIUM_URL && process.env.TECHNITIUM_TOKEN) providers.push({ id: 'technitium', name: 'Technitium', url: process.env.TECHNITIUM_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();
}
// Build cron string from HH:MM setting
function getSecretCron() {
const time = settings.get().notifications?.secret_check_time ?? '08:00';
const [h, m] = time.split(':').map(Number);
return `${m ?? 0} ${h ?? 8} * * *`;
}
let secretJob = null;
function scheduleSecretCheck() {
if (secretJob) secretJob.cancel();
const notif = settings.get().notifications ?? {};
const tz = notif.timezone || 'UTC';
secretJob = schedule.scheduleJob({ rule: getSecretCron(), tz }, () => {
saveLastCheckDate('');
const { notifications } = settings.get();
if (notifications?.secret_check !== false) checkSecretExpiry();
});
console.log(`Secret check scheduled at ${notif.secret_check_time ?? '08:00'} (${tz})`);
}
// Run at startup only if not already checked today, then on schedule
checkSecretExpiryOnce();
scheduleSecretCheck();
module.exports = { scheduleSecretCheck };
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => console.log(`Sloth Manager backend running on port ${PORT}`));