156 lines
6.1 KiB
JavaScript
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}`));
|