Add: Entra Cronjob, Cronjob-Verwaltung, Profilfotos, PDF-Auth-Fix, Tickets-Demo
This commit is contained in:
@@ -497,6 +497,18 @@ async function initializeDatabase() {
|
||||
created_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
// Entra Profil-Sync: Cronjob-Logs + Avatar-URL
|
||||
`CREATE TABLE IF NOT EXISTS cron_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
job_name TEXT NOT NULL,
|
||||
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
finished_at DATETIME,
|
||||
status TEXT DEFAULT 'running',
|
||||
message TEXT,
|
||||
details TEXT
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_cron_logs_job ON cron_logs(job_name, started_at)`,
|
||||
`ALTER TABLE users ADD COLUMN avatar_url TEXT`,
|
||||
];
|
||||
for (const migration of migrations) {
|
||||
try {
|
||||
|
||||
60
backend/src/routes/cron.routes.js
Normal file
60
backend/src/routes/cron.routes.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
const { requireSuperAdmin } = require('../middleware/roleCheck');
|
||||
const { asyncHandler } = require('../middleware/errorHandler');
|
||||
const Database = require('better-sqlite3');
|
||||
const DB_PATH = process.env.DATABASE_PATH || './database.sqlite';
|
||||
|
||||
router.use(authenticateToken);
|
||||
router.use(requireSuperAdmin);
|
||||
|
||||
// Alle Cronjob-Definitionen (statisch)
|
||||
const CRON_JOBS = [
|
||||
{ name: 'entra_profile_sync', label: 'Entra Profil-Sync', description: 'Synchronisiert Profildaten und Fotos aus Azure AD', schedule: 'Täglich 02:00 Uhr', icon: '👤', canRun: true },
|
||||
{ name: 'imap_ticket_import', label: 'E-Mail Ticket-Import', description: 'Importiert eingehende E-Mails als Tickets', schedule: 'Alle 30 Sekunden', icon: '📧', canRun: false },
|
||||
{ name: 'mdo_alert_poll', label: 'MDO Alert-Polling', description: 'Holt neue Security-Alerts aus Microsoft Defender', schedule: 'Alle 5 Minuten', icon: '🛡️', canRun: false },
|
||||
{ name: 'proxmox_sync', label: 'Proxmox Sync', description: 'Synchronisiert VM/CT-Status von Proxmox', schedule: 'Alle 5 Minuten', icon: '🖥️', canRun: false },
|
||||
{ name: 'monitoring_status', label: 'Monitoring Status', description: 'Aktualisiert Online/Offline-Status der Agents', schedule: 'Kontinuierlich', icon: '📡', canRun: false },
|
||||
{ name: 'health_history', label: 'Health-History', description: 'Zeichnet API- und DB-Status auf', schedule: 'Alle 5 Minuten', icon: '💚', canRun: false },
|
||||
{ name: 'escalation_check', label: 'Eskalations-Check', description: 'Prüft überfällige Tickets und sendet Eskalations-Mails', schedule: 'Stündlich', icon: '🚨', canRun: false },
|
||||
{ name: 'shares_cleanup', label: 'Shares-Cleanup', description: 'Löscht abgelaufene / verbrauchte sichere Links', schedule: 'Alle 15 Minuten', icon: '🔗', canRun: false },
|
||||
{ name: 'stock_threshold_check', label: 'Mindestbestand-Check', description: 'Prüft Lagerbestände und sendet Warnungen', schedule: 'Täglich 07:00 Uhr', icon: '📦', canRun: false },
|
||||
{ name: 'weekly_report', label: 'Wöchentlicher Bericht', description: 'Versendet den wöchentlichen Ticket-Report', schedule: 'Montag 08:00 Uhr', icon: '📊', canRun: false },
|
||||
];
|
||||
|
||||
// GET /api/cron — alle Jobs mit letztem Log
|
||||
router.get('/', asyncHandler(async (req, res) => {
|
||||
const db = new Database(DB_PATH);
|
||||
const jobs = CRON_JOBS.map(job => {
|
||||
const lastLog = db.prepare(`
|
||||
SELECT * FROM cron_logs WHERE job_name = ? ORDER BY started_at DESC LIMIT 1
|
||||
`).get(job.name);
|
||||
return { ...job, lastRun: lastLog || null };
|
||||
});
|
||||
db.close();
|
||||
res.json({ status: 'success', data: jobs });
|
||||
}));
|
||||
|
||||
// GET /api/cron/:name/logs — Logs für einen Job
|
||||
router.get('/:name/logs', asyncHandler(async (req, res) => {
|
||||
const db = new Database(DB_PATH);
|
||||
const logs = db.prepare(`
|
||||
SELECT * FROM cron_logs WHERE job_name = ? ORDER BY started_at DESC LIMIT 50
|
||||
`).all(req.params.name);
|
||||
db.close();
|
||||
res.json({ status: 'success', data: logs });
|
||||
}));
|
||||
|
||||
// POST /api/cron/:name/run — Job manuell auslösen
|
||||
router.post('/:name/run', asyncHandler(async (req, res) => {
|
||||
const { name } = req.params;
|
||||
if (name === 'entra_profile_sync') {
|
||||
const { syncEntraProfiles } = require('../services/entraSync.service');
|
||||
syncEntraProfiles().catch(console.error); // async, nicht awaiten
|
||||
return res.json({ status: 'success', message: 'Job gestartet' });
|
||||
}
|
||||
res.status(404).json({ status: 'error', message: 'Job nicht gefunden oder nicht manuell auslösbar' });
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -177,6 +177,8 @@ const tvRoutes = require('./routes/tv.routes');
|
||||
app.use('/api/tv', tvRoutes);
|
||||
const feedbackRoutes = require('./routes/feedback.routes');
|
||||
app.use('/api/feedback', feedbackRoutes);
|
||||
const cronRoutes = require('./routes/cron.routes');
|
||||
app.use('/api/cron', cronRoutes);
|
||||
|
||||
// Static file serving for uploads (PDFs)
|
||||
const path = require('path');
|
||||
@@ -415,6 +417,16 @@ async function startServer() {
|
||||
console.log('🖥️ Proxmox Monitoring gestartet (alle 5 Minuten)');
|
||||
}
|
||||
|
||||
// Entra Profil-Sync (täglich 02:00 Uhr)
|
||||
if (process.env.AZURE_TENANT_ID && process.env.AZURE_CLIENT_ID) {
|
||||
const { syncEntraProfiles } = require('./services/entraSync.service');
|
||||
cron.schedule('0 2 * * *', async () => {
|
||||
console.log('[Cron] Entra Profil-Sync gestartet');
|
||||
try { await syncEntraProfiles(); } catch (e) { console.error('[Cron] Entra Sync Fehler:', e.message); }
|
||||
}, { timezone: 'Europe/Berlin' });
|
||||
console.log('👤 Entra Profil-Sync konfiguriert (täglich 02:00 Uhr)');
|
||||
}
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log('═══════════════════════════════════════════');
|
||||
|
||||
99
backend/src/services/entraSync.service.js
Normal file
99
backend/src/services/entraSync.service.js
Normal file
@@ -0,0 +1,99 @@
|
||||
const { getClientToken } = require('./graph.service');
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const DB_PATH = process.env.DATABASE_PATH || './database.sqlite';
|
||||
|
||||
async function syncEntraProfiles() {
|
||||
const db = new Database(DB_PATH);
|
||||
const startedAt = new Date().toISOString();
|
||||
|
||||
// Log starten
|
||||
const logId = db.prepare(`
|
||||
INSERT INTO cron_logs (job_name, started_at, status) VALUES (?, ?, 'running')
|
||||
`).run('entra_profile_sync', startedAt).lastInsertRowid;
|
||||
|
||||
try {
|
||||
const token = await getClientToken();
|
||||
const users = db.prepare(`SELECT id, azure_id, username FROM users WHERE azure_id IS NOT NULL AND azure_id != ''`).all();
|
||||
|
||||
let synced = 0, errors = 0, photos = 0;
|
||||
|
||||
for (const user of users) {
|
||||
try {
|
||||
// Profildaten holen
|
||||
const profileRes = await fetch(
|
||||
`https://graph.microsoft.com/v1.0/users/${user.azure_id}?$select=jobTitle,department,mobilePhone,businessPhones,officeLocation,employeeHireDate,displayName,mail,givenName,surname`,
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
|
||||
if (profileRes.ok) {
|
||||
const p = await profileRes.json();
|
||||
const phone = p.mobilePhone || (p.businessPhones?.[0]) || null;
|
||||
|
||||
db.prepare(`
|
||||
UPDATE users SET
|
||||
position = COALESCE(?, position),
|
||||
department = COALESCE(?, department),
|
||||
phone = COALESCE(?, phone),
|
||||
location = COALESCE(?, location),
|
||||
joined_date = COALESCE(?, joined_date),
|
||||
first_name = COALESCE(?, first_name),
|
||||
last_name = COALESCE(?, last_name)
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
p.jobTitle || null,
|
||||
p.department || null,
|
||||
phone,
|
||||
p.officeLocation || null,
|
||||
p.employeeHireDate ? p.employeeHireDate.split('T')[0] : null,
|
||||
p.givenName || null,
|
||||
p.surname || null,
|
||||
user.id
|
||||
);
|
||||
synced++;
|
||||
}
|
||||
|
||||
// Foto holen
|
||||
const photoRes = await fetch(
|
||||
`https://graph.microsoft.com/v1.0/users/${user.azure_id}/photo/$value`,
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
|
||||
if (photoRes.ok) {
|
||||
const avatarDir = path.join(process.env.UPLOAD_PATH || path.join(__dirname, '../../uploads'), 'avatars');
|
||||
if (!fs.existsSync(avatarDir)) fs.mkdirSync(avatarDir, { recursive: true });
|
||||
|
||||
const buffer = Buffer.from(await photoRes.arrayBuffer());
|
||||
const filename = `${user.azure_id}.jpg`;
|
||||
fs.writeFileSync(path.join(avatarDir, filename), buffer);
|
||||
|
||||
db.prepare(`UPDATE users SET avatar_url = ? WHERE id = ?`)
|
||||
.run(`/uploads/avatars/${filename}`, user.id);
|
||||
photos++;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(`[Entra Sync] Fehler bei User ${user.username}:`, err.message);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
const message = `${synced} Profile synchronisiert, ${photos} Fotos gespeichert, ${errors} Fehler`;
|
||||
db.prepare(`UPDATE cron_logs SET status='success', finished_at=?, message=?, details=? WHERE id=?`)
|
||||
.run(new Date().toISOString(), message, JSON.stringify({ synced, photos, errors, total: users.length }), logId);
|
||||
|
||||
console.log(`[Entra Sync] ${message}`);
|
||||
return { synced, photos, errors };
|
||||
|
||||
} catch (err) {
|
||||
db.prepare(`UPDATE cron_logs SET status='error', finished_at=?, message=? WHERE id=?`)
|
||||
.run(new Date().toISOString(), err.message, logId);
|
||||
throw err;
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { syncEntraProfiles };
|
||||
Reference in New Issue
Block a user