"use client"; import { useEffect, useState } from "react"; import { useSession } from "next-auth/react"; interface Device { id: string; name: string; isActive: boolean; } interface MqttCredential { id: number; device_id: string; mqtt_username: string; mqtt_password_hash: string; enabled: number; created_at: string; updated_at: string; device_name: string; mqtt_password?: string; // Nur bei Erstellung/Regenerierung vorhanden } interface AclRule { id: number; device_id: string; topic_pattern: string; permission: 'read' | 'write' | 'readwrite'; created_at: string; } interface SyncStatus { pending_changes: number; last_sync_at: string | null; last_sync_status: string; } export default function MqttPage() { const { data: session } = useSession(); const userRole = (session?.user as any)?.role; const isAdmin = userRole === 'ADMIN'; const [devices, setDevices] = useState([]); const [credentials, setCredentials] = useState([]); const [aclRules, setAclRules] = useState>({}); const [syncStatus, setSyncStatus] = useState(null); const [loading, setLoading] = useState(true); const [syncing, setSyncing] = useState(false); // Modal States const [showAddModal, setShowAddModal] = useState(false); const [showPasswordModal, setShowPasswordModal] = useState(false); const [showAclModal, setShowAclModal] = useState(false); const [selectedDevice, setSelectedDevice] = useState(null); const [generatedPassword, setGeneratedPassword] = useState(""); // Form States const [addFormData, setAddFormData] = useState({ device_id: "", auto_generate: true, }); const [aclFormData, setAclFormData] = useState({ device_id: "", topic_pattern: "", permission: "readwrite" as 'read' | 'write' | 'readwrite', mqtt_username: "", // Needed for correct topic pattern }); useEffect(() => { if (isAdmin) { fetchAll(); } }, [isAdmin]); const fetchAll = async () => { try { await Promise.all([ fetchDevices(), fetchCredentials(), fetchSyncStatus(), ]); } finally { setLoading(false); } }; const fetchDevices = async () => { try { const response = await fetch("/api/devices"); if (!response.ok) throw new Error("Failed to fetch devices"); const data = await response.json(); setDevices(data.devices || []); } catch (err) { console.error("Failed to fetch devices", err); } }; const fetchCredentials = async () => { try { const response = await fetch("/api/mqtt/credentials"); if (!response.ok) throw new Error("Failed to fetch credentials"); const data = await response.json(); setCredentials(data); // Lade ACL Regeln für alle Devices mit Credentials for (const cred of data) { await fetchAclRules(cred.device_id); } } catch (err) { console.error("Failed to fetch credentials", err); } }; const fetchAclRules = async (deviceId: string) => { try { const response = await fetch(`/api/mqtt/acl?device_id=${deviceId}`); if (!response.ok) throw new Error("Failed to fetch ACL rules"); const rules = await response.json(); setAclRules(prev => ({ ...prev, [deviceId]: rules })); } catch (err) { console.error("Failed to fetch ACL rules", err); } }; const fetchSyncStatus = async () => { try { const response = await fetch("/api/mqtt/sync"); if (!response.ok) throw new Error("Failed to fetch sync status"); const status = await response.json(); setSyncStatus(status); } catch (err) { console.error("Failed to fetch sync status", err); } }; const handleAddCredentials = async (e: React.FormEvent) => { e.preventDefault(); try { const response = await fetch("/api/mqtt/credentials", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(addFormData), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "Failed to create credentials"); } const newCred = await response.json(); // Zeige generiertes Passwort an if (newCred.mqtt_password) { setGeneratedPassword(`Username: ${newCred.mqtt_username}\nPassword: ${newCred.mqtt_password}`); setSelectedDevice(addFormData.device_id); setShowPasswordModal(true); } await fetchCredentials(); await fetchSyncStatus(); setShowAddModal(false); setAddFormData({ device_id: "", auto_generate: true }); } catch (err: any) { alert(err.message); } }; const handleDeleteCredentials = async (deviceId: string) => { if (!confirm("MQTT Credentials für dieses Device löschen?")) return; try { const response = await fetch(`/api/mqtt/credentials/${deviceId}`, { method: "DELETE", }); if (!response.ok) throw new Error("Failed to delete credentials"); await fetchCredentials(); await fetchSyncStatus(); } catch (err: any) { alert(err.message); } }; const handleRegeneratePassword = async (deviceId: string) => { if (!confirm("Passwort neu generieren? Das alte Passwort wird ungültig.")) return; try { const response = await fetch(`/api/mqtt/credentials/${deviceId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ regenerate_password: true }), }); if (!response.ok) throw new Error("Failed to regenerate password"); const updated = await response.json(); if (updated.mqtt_password) { setGeneratedPassword(`Username: ${updated.mqtt_username}\nPassword: ${updated.mqtt_password}`); setSelectedDevice(deviceId); setShowPasswordModal(true); } await fetchCredentials(); await fetchSyncStatus(); } catch (err: any) { alert(err.message); } }; const handleToggleEnabled = async (deviceId: string, enabled: boolean) => { try { const response = await fetch(`/api/mqtt/credentials/${deviceId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enabled }), }); if (!response.ok) throw new Error("Failed to update credentials"); await fetchCredentials(); await fetchSyncStatus(); } catch (err: any) { alert(err.message); } }; const handleAddAclRule = async (e: React.FormEvent) => { e.preventDefault(); try { const response = await fetch("/api/mqtt/acl", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(aclFormData), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "Failed to create ACL rule"); } await fetchAclRules(aclFormData.device_id); await fetchSyncStatus(); setShowAclModal(false); setAclFormData({ device_id: "", topic_pattern: "", permission: "readwrite", mqtt_username: "" }); } catch (err: any) { alert(err.message); } }; const handleDeleteAclRule = async (ruleId: number, deviceId: string) => { if (!confirm("ACL Regel löschen?")) return; try { const response = await fetch(`/api/mqtt/acl/${ruleId}`, { method: "DELETE", }); if (!response.ok) throw new Error("Failed to delete ACL rule"); await fetchAclRules(deviceId); await fetchSyncStatus(); } catch (err: any) { alert(err.message); } }; const handleSync = async () => { setSyncing(true); try { const response = await fetch("/api/mqtt/sync", { method: "POST", }); const result = await response.json(); if (result.success) { alert(result.message); } else { alert(`Sync fehlgeschlagen: ${result.message}`); } await fetchSyncStatus(); } catch (err: any) { alert("Sync fehlgeschlagen: " + err.message); } finally { setSyncing(false); } }; const handleSendCredentialsEmail = async () => { if (!selectedDevice) return; // Parse username and password from generatedPassword string const lines = generatedPassword.split('\n'); const usernameLine = lines.find(l => l.startsWith('Username:')); const passwordLine = lines.find(l => l.startsWith('Password:')); if (!usernameLine || !passwordLine) { alert('Fehler beim Extrahieren der Credentials'); return; } const mqttUsername = usernameLine.replace('Username:', '').trim(); const mqttPassword = passwordLine.replace('Password:', '').trim(); try { const response = await fetch('/api/mqtt/send-credentials', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ deviceId: selectedDevice, mqttUsername, mqttPassword, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to send email'); } const result = await response.json(); alert(result.message); } catch (err: any) { alert('Email senden fehlgeschlagen: ' + err.message); } }; if (!isAdmin) { return
Keine Berechtigung
; } if (loading) { return
Lade...
; } const devicesWithoutCredentials = devices.filter( d => d.isActive && !credentials.find(c => c.device_id === d.id) ); return (

MQTT Provisioning

{syncStatus && syncStatus.pending_changes > 0 && ( {syncStatus.pending_changes} ausstehende Änderungen )}
{syncStatus && (

Sync Status

Status: {syncStatus.last_sync_status}

{syncStatus.last_sync_at && (

Letzter Sync: {new Date(syncStatus.last_sync_at).toLocaleString('de-DE')}

)}
)}
{credentials.map(cred => { const deviceRules = aclRules[cred.device_id] || []; return (

{cred.device_name}

Device ID: {cred.device_id}

Username: {cred.mqtt_username}

Erstellt: {new Date(cred.created_at).toLocaleString('de-DE')}

ACL Regeln:

{deviceRules.length > 0 ? ( {deviceRules.map(rule => ( ))}
Topic Pattern Berechtigung Erstellt
{rule.topic_pattern} {rule.permission} {new Date(rule.created_at).toLocaleString('de-DE')}
) : (

Keine ACL Regeln definiert

)}
); })} {credentials.length === 0 && (
Noch keine Devices provisioniert
)}
{/* Add Credentials Modal */} {showAddModal && (

Device Provisionieren

)} {/* Password Display Modal */} {showPasswordModal && (

MQTT Credentials

{generatedPassword}

⚠️ Speichere diese Credentials! Das Passwort kann nicht nochmal angezeigt werden.

)} {/* Add ACL Rule Modal */} {showAclModal && (

ACL Regel Hinzufügen

setAclFormData({ ...aclFormData, topic_pattern: e.target.value })} className="w-full px-3 py-2 border rounded-md" placeholder={`owntracks/${aclFormData.mqtt_username || ''}/#`} required />

Format: owntracks/<Username>/# (z.B. owntracks/device_12_4397af93/#)

)}
); }