aether-framework/resources/[aether]/aether-police/server/kern.lua
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark,
  Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI)
- aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource,
  an aether-core gekoppelt (Rechte über aether_users.admin_group), mit
  WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU)
- Prototypen admin/ vervollständigt
- SQL-Schema aether_admin.sql
- Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:55:32 +02:00

228 lines
8.1 KiB
Lua

-- =====================================================================
-- aether-police — Kern
--
-- Der Kern kennt keine Fachlogik. Er stellt bereit:
-- * eine Modul-Registry (Plugin-System)
-- * einen Ereignisbus zwischen den Modulen
-- * ein Handler-Register, an das die Oberfläche Anfragen schickt
--
-- Fachmodule registrieren sich selbst und können jederzeit ergänzt
-- werden, ohne dass der Kern angefasst werden muss.
-- =====================================================================
Polizei = Polizei or {}
Kern = {}
-- Registrierte Fachmodule: [id] = { id, name, icon, recht, reihenfolge, aktiv }
local module = {}
-- Handler, die die Oberfläche aufrufen darf: [modulId][aktion] = funktion
local handler = {}
-- Abonnenten des Ereignisbusses: [ereignis] = { funktion, ... }
local abonnenten = {}
-- ---------------------------------------------------------------------
-- Modul-Registry
-- ---------------------------------------------------------------------
--- Registriert ein Fachmodul.
--- @param beschreibung table {
--- id eindeutige Kennung
--- name Anzeigename in der Seitenleiste
--- icon Symbol
--- recht Recht, das zum Öffnen nötig ist (nil = für alle)
--- reihenfolge Sortierung in der Seitenleiste
--- aktiv Standardmäßig aktiv?
--- }
function Kern.RegistriereModul(beschreibung)
if type(beschreibung) ~= 'table' or not beschreibung.id then
Aether.Fehler('Modul ohne Kennung kann nicht registriert werden.')
return false
end
if module[beschreibung.id] then
Aether.Fehler(('Modul "%s" ist bereits registriert.'):format(beschreibung.id))
return false
end
module[beschreibung.id] = {
id = beschreibung.id,
name = beschreibung.name or beschreibung.id,
icon = beschreibung.icon or '▪',
recht = beschreibung.recht,
reihenfolge = beschreibung.reihenfolge or 100,
aktiv = beschreibung.aktiv ~= false,
beschreibung = beschreibung.beschreibung,
}
handler[beschreibung.id] = handler[beschreibung.id] or {}
Aether.Debug(('Polizei-Modul registriert: %s'):format(beschreibung.id))
return true
end
--- Registriert einen Handler, den die Oberfläche aufrufen kann.
--- @param modulId string Modul
--- @param aktion string Name der Aktion
--- @param recht string|nil Erforderliches Recht (nil = nur Modulrecht)
--- @param funktion function fun(quelle, beamter, daten): ergebnis
function Kern.RegistriereAktion(modulId, aktion, recht, funktion)
handler[modulId] = handler[modulId] or {}
handler[modulId][aktion] = { recht = recht, funktion = funktion }
end
--- Prüft das Öffnungsrecht eines Moduls.
--- `recht` darf eine Kennung oder eine Liste sein — bei einer Liste
--- genügt EIN Recht daraus. Das brauchen Module, die mehrere Bereiche
--- mit je eigenem Recht unter einem Dach zusammenfassen.
--- @param beamter table Beamtendaten
--- @param recht string|table|nil Erforderliches Recht
--- @return boolean
function Kern.DarfOeffnen(beamter, recht)
if not recht then return true end
if type(recht) == 'table' then
for _, einzeln in ipairs(recht) do
if Rechte.Hat(beamter, einzeln) then return true end
end
return false
end
return Rechte.Hat(beamter, recht)
end
--- Liefert alle Module, die ein Beamter sehen darf
--- @param beamter table Beamtendaten (mit Rechten)
--- @return table Liste
function Kern.SichtbareModule(beamter)
local liste = {}
for _, modul in pairs(module) do
if modul.aktiv and Kern.DarfOeffnen(beamter, modul.recht) then
liste[#liste + 1] = {
id = modul.id, name = modul.name, icon = modul.icon,
reihenfolge = modul.reihenfolge, beschreibung = modul.beschreibung,
}
end
end
table.sort(liste, function(a, b) return a.reihenfolge < b.reihenfolge end)
return liste
end
--- Liefert alle registrierten Module, auch die abgeschalteten.
--- Nur für die Administration gedacht.
--- @return table Liste
function Kern.AlleModule()
local liste = {}
for _, modul in pairs(module) do
-- Wie viele Aktionen hängen an diesem Modul?
local aktionen = 0
for _ in pairs(handler[modul.id] or {}) do aktionen = aktionen + 1 end
liste[#liste + 1] = {
id = modul.id, name = modul.name, icon = modul.icon,
recht = modul.recht, reihenfolge = modul.reihenfolge,
beschreibung = modul.beschreibung,
aktiv = modul.aktiv, aktionen = aktionen,
}
end
table.sort(liste, function(a, b) return a.reihenfolge < b.reihenfolge end)
return liste
end
--- Gibt es dieses Modul?
--- @param modulId string Modul
--- @return boolean
function Kern.KenntModul(modulId)
return module[modulId] ~= nil
end
--- Schaltet ein Modul an oder aus (Administration)
--- @param modulId string Modul
--- @param aktiv boolean Neuer Zustand
function Kern.SetzeModulAktiv(modulId, aktiv)
if module[modulId] then
module[modulId].aktiv = aktiv and true or false
end
end
--- Führt eine Aktion aus — inklusive Rechteprüfung.
--- Wird von der Oberfläche über ein einziges Netz-Event aufgerufen.
--- @param quelle number Server-ID
--- @param beamter table Beamtendaten
--- @param modulId string Modul
--- @param aktion string Aktion
--- @param daten table Nutzdaten
--- @return any Ergebnis, string|nil Fehler
function Kern.FuehreAus(quelle, beamter, modulId, aktion, daten)
local modul = module[modulId]
if not modul or not modul.aktiv then
return nil, 'Dieses Modul ist nicht verfügbar.'
end
-- Recht auf das Modul selbst
if not Kern.DarfOeffnen(beamter, modul.recht) then
return nil, 'Dir fehlt die Berechtigung für diesen Bereich.'
end
local eintrag = handler[modulId] and handler[modulId][aktion]
if not eintrag then
return nil, 'Unbekannte Aktion.'
end
-- Zusätzliches Recht auf die einzelne Aktion
if eintrag.recht and not Rechte.Hat(beamter, eintrag.recht) then
return nil, 'Dir fehlt die Berechtigung für diese Aktion.'
end
local erfolg, ergebnis, fehler = pcall(eintrag.funktion, quelle, beamter, daten or {})
if not erfolg then
Aether.Fehler(('Fehler in %s/%s: %s'):format(modulId, aktion, tostring(ergebnis)))
return nil, 'Interner Fehler im Modul.'
end
return ergebnis, fehler
end
-- ---------------------------------------------------------------------
-- Ereignisbus zwischen den Modulen
-- ---------------------------------------------------------------------
--- Abonniert ein internes Ereignis
--- @param ereignis string Name
--- @param funktion function Rückruf
function Kern.Abonniere(ereignis, funktion)
abonnenten[ereignis] = abonnenten[ereignis] or {}
table.insert(abonnenten[ereignis], funktion)
end
--- Löst ein internes Ereignis aus.
--- Fehler eines Abonnenten dürfen die anderen nicht mitreißen.
--- @param ereignis string Name
--- @param ... any Nutzdaten
function Kern.Loese(ereignis, ...)
for _, funktion in ipairs(abonnenten[ereignis] or {}) do
local erfolg, fehler = pcall(funktion, ...)
if not erfolg then
Aether.Fehler(('Abonnent von "%s" schlug fehl: %s'):format(ereignis, tostring(fehler)))
end
end
-- Zusätzlich nach außen geben, damit fremde Ressourcen andocken können
TriggerEvent('aether:police:' .. ereignis, ...)
end
-- ---------------------------------------------------------------------
-- Hilfsfunktionen für Fachmodule
-- ---------------------------------------------------------------------
--- Erzeugt eine fortlaufende Kennung, z. B. AZ-2026-0042
--- @param praefix string Vorsilbe
--- @param tabelle string Tabellenname
--- @param spalte string Spalte mit der Kennung
--- @return string
function Kern.ErzeugeKennung(praefix, tabelle, spalte)
local jahr = os.date('%Y')
local anzahl = MySQL.scalar.await(
('SELECT COUNT(*) FROM %s WHERE %s LIKE ?'):format(tabelle, spalte),
{ praefix .. '-' .. jahr .. '-%' }) or 0
return ('%s-%s-%04d'):format(praefix, jahr, anzahl + 1)
end