diff --git a/backend/src/controllers/asset.controller.js b/backend/src/controllers/asset.controller.js index 1501b80..31f5678 100644 --- a/backend/src/controllers/asset.controller.js +++ b/backend/src/controllers/asset.controller.js @@ -177,6 +177,20 @@ class AssetController { req.ip ); + // ANL-Nummer automatisch vergeben wenn noch keine vorhanden + try { + const Database = require('better-sqlite3'); + const db = new Database(process.env.DATABASE_PATH || './database.sqlite'); + const asset = db.prepare('SELECT inventory_number FROM assets WHERE id=?').get(parseInt(id)); + if (!asset?.inventory_number) { + const maxRow = db.prepare("SELECT MAX(CAST(SUBSTR(inventory_number, 5) AS INTEGER)) as mx FROM assets WHERE inventory_number LIKE 'ANL-%'").get(); + const next = (maxRow?.mx || 0) + 1; + const anl = `ANL-${String(next).padStart(4, '0')}`; + db.prepare('UPDATE assets SET inventory_number=? WHERE id=?').run(anl, parseInt(id)); + } + db.close(); + } catch (_) {} + res.json({ status: 'success', data: assignment diff --git a/backend/src/services/entraSync.service.js b/backend/src/services/entraSync.service.js index 6e40b4a..2e03950 100644 --- a/backend/src/services/entraSync.service.js +++ b/backend/src/services/entraSync.service.js @@ -80,12 +80,164 @@ async function syncEntraProfiles() { } } - const message = `${synced} Profile synchronisiert, ${photos} Fotos gespeichert, ${errors} Fehler`; + // ── Schritt 2: Neue Entra-User → IT Nexus anlegen ────────────────────── + let newUsers = 0; + try { + const allEntraUsersRes = await fetch( + 'https://graph.microsoft.com/v1.0/users?$select=id,displayName,givenName,surname,mail,userPrincipalName,jobTitle,department,mobilePhone,businessPhones,officeLocation,employeeHireDate&$top=999', + { headers: { Authorization: `Bearer ${token}` } } + ); + const allEntraUsers = await allEntraUsersRes.json(); + + for (const eu of (allEntraUsers.value || [])) { + if (!eu.mail && !eu.userPrincipalName) continue; + const email = (eu.mail || eu.userPrincipalName).toLowerCase().trim(); + + // Prüfe ob User schon existiert (per azure_id oder email) + const existing = db.prepare('SELECT id FROM users WHERE azure_id=? OR email=?').get(eu.id, email); + if (existing) { + // Falls azure_id noch nicht gesetzt, verknüpfen + if (!db.prepare('SELECT azure_id FROM users WHERE id=?').get(existing.id)?.azure_id) { + db.prepare('UPDATE users SET azure_id=? WHERE id=?').run(eu.id, existing.id); + } + continue; + } + + // Neuen User anlegen + const benutzerRole = db.prepare("SELECT id FROM roles WHERE name='benutzer'").get(); + if (!benutzerRole) continue; + + let username = email.split('@')[0].replace(/[^a-z0-9._-]/gi, '').toLowerCase(); + let suffix = 1; + while (db.prepare('SELECT id FROM users WHERE username=?').get(username)) { + username = `${email.split('@')[0].replace(/[^a-z0-9._-]/gi, '').toLowerCase()}${suffix++}`; + } + + db.prepare(`INSERT INTO users (username, email, password_hash, role_id, first_name, last_name, is_active, must_change_password, azure_id, department, position, phone, location, joined_date) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run( + username, email, 'AZURE_SSO_NO_PASSWORD', benutzerRole.id, + eu.givenName || null, eu.surname || null, 1, 0, eu.id, + eu.department || null, eu.jobTitle || null, + eu.mobilePhone || (eu.businessPhones?.[0]) || null, + eu.officeLocation || null, + eu.employeeHireDate ? eu.employeeHireDate.split('T')[0] : null + ); + newUsers++; + } + console.log(`[Entra Sync] Schritt 2: ${newUsers} neue User angelegt`); + } catch (err) { + console.error('[Entra Sync] Schritt 2 (neue User) fehlgeschlagen:', err.message); + errors++; + } + + // ── Schritt 3: Intune-Geräte → Assets sync ────────────────────────────── + let newAssets = 0, updatedAssets = 0; + try { + const intuneRes = await fetch( + 'https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?$select=id,deviceName,serialNumber,model,manufacturer,operatingSystem,osVersion,userDisplayName,userPrincipalName,lastSyncDateTime,managementState,complianceState&$top=999', + { headers: { Authorization: `Bearer ${token}` } } + ); + const intuneDevices = await intuneRes.json(); + + for (const device of (intuneDevices.value || [])) { + if (!device.serialNumber || device.serialNumber === 'unknown') continue; + + // Asset-Typ bestimmen + let assetType = 'sonstiges'; + const name = (device.deviceName || '').toLowerCase(); + const os = (device.operatingSystem || '').toLowerCase(); + if (os.includes('windows') && (name.includes('nb') || name.includes('laptop') || name.includes('book'))) assetType = 'Notebook'; + else if (os.includes('windows')) assetType = 'Notebook'; // Default Windows = Notebook + + // Benutzer finden + let assignedUserId = null; + if (device.userPrincipalName) { + const assignedUser = db.prepare('SELECT id FROM users WHERE email=? OR username=?') + .get(device.userPrincipalName.toLowerCase(), device.userPrincipalName.split('@')[0].toLowerCase()); + assignedUserId = assignedUser?.id || null; + } + + const existingAsset = db.prepare('SELECT id, status FROM assets WHERE serial_number=? OR name=?').get(device.serialNumber, device.deviceName); + + if (existingAsset) { + // Bestehend updaten (OS, letzter Sync) + db.prepare('UPDATE assets SET os=?, last_agent_sync=?, updated_at=CURRENT_TIMESTAMP WHERE id=?') + .run(device.operatingSystem ? `${device.operatingSystem} ${device.osVersion || ''}`.trim() : null, device.lastSyncDateTime, existingAsset.id); + updatedAssets++; + } else { + // Neues Asset anlegen + db.prepare(`INSERT INTO assets (name, type, serial_number, model, manufacturer, os, status, department, assigned_to_user_id, last_agent_sync, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)`).run( + device.deviceName || device.serialNumber, + assetType, + device.serialNumber, + device.model || null, + device.manufacturer || null, + device.operatingSystem ? `${device.operatingSystem} ${device.osVersion || ''}`.trim() : null, + assignedUserId ? 'zugewiesen' : 'verfuegbar', + 'IT', + assignedUserId, + device.lastSyncDateTime + ); + newAssets++; + } + } + console.log(`[Entra Sync] Schritt 3: ${newAssets} neue Assets, ${updatedAssets} aktualisiert`); + } catch (err) { + console.error('[Entra Sync] Schritt 3 (Intune Assets) fehlgeschlagen:', err.message); + errors++; + } + + // ── Schritt 4: Entra-Geräte (Hybrid Join + Azure AD Join) → Assets sync ── + try { + const entraDevRes = await fetch( + 'https://graph.microsoft.com/v1.0/devices?$select=id,displayName,operatingSystem,operatingSystemVersion,manufacturer,model,deviceId,approximateLastSignInDateTime,registrationDateTime,trustType&$top=999', + { headers: { Authorization: `Bearer ${token}` } } + ); + const entraDevices = await entraDevRes.json(); + + for (const device of (entraDevices.value || [])) { + if (!device.displayName) continue; + // Nur Windows-Geräte (Hybrid/Azure AD joined) + const os = (device.operatingSystem || '').toLowerCase(); + if (!os.includes('windows')) continue; + + // Prüfe ob Asset bereits existiert (per Name) + const existingByName = db.prepare('SELECT id FROM assets WHERE name=?').get(device.displayName); + if (existingByName) { + // OS updaten falls nötig + db.prepare('UPDATE assets SET os=?, updated_at=CURRENT_TIMESTAMP WHERE id=?') + .run(device.operatingSystem ? `${device.operatingSystem} ${device.operatingSystemVersion || ''}`.trim() : null, existingByName.id); + updatedAssets++; + continue; + } + + // Typ bestimmen + let assetType = 'Notebook'; + const name = device.displayName.toLowerCase(); + if (name.includes('srv') || name.includes('server')) assetType = 'Other'; + + db.prepare(`INSERT INTO assets (name, type, model, manufacturer, os, status, department, created_at, updated_at) VALUES (?,?,?,?,?,?,?,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)`).run( + device.displayName, + assetType, + device.model || null, + device.manufacturer || null, + device.operatingSystem ? `${device.operatingSystem} ${device.operatingSystemVersion || ''}`.trim() : null, + 'verfuegbar', + 'IT' + ); + newAssets++; + } + console.log(`[Entra Sync] Schritt 4: Entra-Geräte sync abgeschlossen`); + } catch (err) { + console.error('[Entra Sync] Schritt 4 (Entra Devices) fehlgeschlagen:', err.message); + errors++; + } + + const message = `${synced} Profile sync, ${photos} Fotos, ${newUsers} neue User, ${newAssets} neue Assets, ${updatedAssets} Assets aktualisiert, ${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); + .run(new Date().toISOString(), message, JSON.stringify({ synced, photos, newUsers, newAssets, updatedAssets, errors, total: users.length }), logId); console.log(`[Entra Sync] ${message}`); - return { synced, photos, errors }; + return { synced, photos, newUsers, newAssets, updatedAssets, errors }; } catch (err) { db.prepare(`UPDATE cron_logs SET status='error', finished_at=?, message=? WHERE id=?`) diff --git a/frontend/src/pages/AssetsPage.jsx b/frontend/src/pages/AssetsPage.jsx index 07f7ef3..5916dd0 100644 --- a/frontend/src/pages/AssetsPage.jsx +++ b/frontend/src/pages/AssetsPage.jsx @@ -18,6 +18,7 @@ const API = process.env.REACT_APP_API_URL || '/api'; const TYPE_ICONS = { notebook: '💻', + server: '🖧', monitor: '🖥️', headset: '🎧', drucker: '🖨️', @@ -182,6 +183,7 @@ const EditModal = ({ asset, onClose, onSaved }) => {