aether-framework/resources/[aether]/aether-bank/server/main.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

296 lines
11 KiB
Lua

-- =====================================================================
-- Aether Bank — Serverlogik
-- Alle Geldbewegungen und die PIN-Pruefung laufen ausschliesslich hier.
-- Die PIN verlaesst den Server niemals.
-- =====================================================================
local Core = exports['aether-core']
-- Wie viele Buchungen an die Oberflaeche geschickt werden
local MAX_BUCHUNGEN = 60
-- Hoechstbetrag je Einzelvorgang (Schutz vor absurden Eingaben)
local MAX_BETRAG = 10000000
-- Merkt sich, wer sich an einem Automaten bereits mit PIN angemeldet hat
local angemeldet = {}
-- ---------------------------------------------------------------------
-- Hilfsfunktionen
-- ---------------------------------------------------------------------
--- Liefert die Charakter-ID eines Spielers
--- @param quelle number Server-ID
--- @return number|nil
local function CharakterVon(quelle)
return Core:HoleCharakterId(quelle)
end
--- Prueft, ob ein Betrag eine gueltige, positive Ganzzahl ist
--- @param betrag any Eingabe
--- @return number|nil Bereinigter Betrag oder nil
local function PruefeBetrag(betrag)
if type(betrag) ~= 'number' then return nil end
betrag = math.floor(betrag)
if betrag < 1 or betrag > MAX_BETRAG then return nil end
return betrag
end
--- Ordnet einer Buchungsart Symbol und Kategorie zu (nur fuer die Anzeige)
--- @param art string Buchungsart aus der Datenbank
--- @return string Symbol, string Kategorie
local function Darstellung(art)
local tabelle = {
deposit = { '💵', 'Einzahlung' },
withdraw = { '🏧', 'Auszahlung' },
transfer = { '↗', 'Transfer' },
salary = { '💼', 'Einkommen' },
fine = { '⚖️', 'Strafe' },
}
local eintrag = tabelle[art] or { '•', 'Sonstiges' }
return eintrag[1], eintrag[2]
end
--- Laedt die letzten Buchungen eines Charakters fuer die Anzeige
--- @param charakterId number Charakter-ID
--- @return table Liste von Buchungen
local function HoleBuchungen(charakterId)
local zeilen = MySQL.query.await([[
SELECT type, amount, balance, label, created_at
FROM aether_transactions
WHERE `character` = ?
ORDER BY id DESC
LIMIT ?
]], { charakterId, MAX_BUCHUNGEN }) or {}
local liste = {}
for _, zeile in ipairs(zeilen) do
local icon, kategorie = Darstellung(zeile.type)
liste[#liste + 1] = {
titel = zeile.label ~= '' and zeile.label or kategorie,
betrag = zeile.amount,
saldo = zeile.balance,
zeit = tostring(zeile.created_at):sub(1, 16),
kat = kategorie,
icon = icon,
}
end
return liste
end
--- Baut den Datensatz, den die Oberflaeche anzeigt (ohne PIN!)
--- @param quelle number Server-ID
--- @return table|nil
local function BaueKonto(quelle)
local charakterId = CharakterVon(quelle)
if not charakterId then return nil end
local daten = Core:HoleSpielerInfo(quelle)
if not daten then return nil end
return {
inhaber = (daten.vorname or '') .. ' ' .. (daten.nachname or ''),
iban = daten.iban or '—',
saldo = daten.konto or 0,
bargeld = daten.bargeld or 0,
transaktionen = HoleBuchungen(charakterId),
}
end
--- Schickt den aktuellen Kontostand an die Oberflaeche
--- @param quelle number Server-ID
--- @param hinweis string|nil Rueckmeldung
local function SendeKonto(quelle, hinweis)
local konto = BaueKonto(quelle)
if not konto then return end
konto.hinweis = hinweis
TriggerClientEvent('aether:bank:konto', quelle, konto)
end
--- Lehnt eine Aktion mit Begruendung ab
local function Ablehnen(quelle, grund)
TriggerClientEvent('aether:bank:fehler', quelle, grund)
end
-- ---------------------------------------------------------------------
-- Automat oeffnen
-- ---------------------------------------------------------------------
RegisterNetEvent('aether:bank:oeffnen', function()
local quelle = source
if not CharakterVon(quelle) then return end
angemeldet[quelle] = false -- jede Sitzung beginnt mit PIN-Eingabe
TriggerClientEvent('aether:bank:zeige', quelle)
end)
-- ---------------------------------------------------------------------
-- PIN pruefen
-- ---------------------------------------------------------------------
RegisterNetEvent('aether:bank:pinPruefen', function(pin)
local quelle = source
local charakterId = CharakterVon(quelle)
if not charakterId or type(pin) ~= 'string' then return end
local gespeichert = MySQL.scalar.await(
'SELECT bank_pin FROM aether_characters WHERE id = ?', { charakterId })
if gespeichert ~= pin then
angemeldet[quelle] = false
TriggerClientEvent('aether:bank:pinErgebnis', quelle, false)
return
end
angemeldet[quelle] = true
TriggerClientEvent('aether:bank:pinErgebnis', quelle, true, BaueKonto(quelle))
end)
-- ---------------------------------------------------------------------
-- PIN aendern
-- ---------------------------------------------------------------------
RegisterNetEvent('aether:bank:pinAendern', function(alt, neu)
local quelle = source
local charakterId = CharakterVon(quelle)
if not charakterId then return end
if type(alt) ~= 'string' or type(neu) ~= 'string' then return end
-- Neue PIN muss genau vier Ziffern haben
if not neu:match('^%d%d%d%d$') then
Ablehnen(quelle, 'Die neue PIN muss aus vier Ziffern bestehen.')
return
end
local gespeichert = MySQL.scalar.await(
'SELECT bank_pin FROM aether_characters WHERE id = ?', { charakterId })
if gespeichert ~= alt then
Ablehnen(quelle, 'Die aktuelle PIN ist falsch.')
return
end
MySQL.update.await('UPDATE aether_characters SET bank_pin = ? WHERE id = ?', { neu, charakterId })
TriggerClientEvent('aether:bank:erfolg', quelle, 'PIN geändert', 'Deine neue PIN ist ab sofort gültig.')
end)
-- ---------------------------------------------------------------------
-- Auszahlen (Konto -> Bargeld)
-- ---------------------------------------------------------------------
RegisterNetEvent('aether:bank:auszahlen', function(betrag)
local quelle = source
if not angemeldet[quelle] then return end
betrag = PruefeBetrag(betrag)
if not betrag then Ablehnen(quelle, 'Ungültiger Betrag.') return end
-- Erst abbuchen (prueft die Deckung), dann als Bargeld gutschreiben
if not Core:EntferneGeld(quelle, 'konto', betrag, 'Barauszahlung Automat') then
Ablehnen(quelle, 'Nicht genügend Guthaben.')
return
end
Core:FuegeGeldHinzu(quelle, 'bargeld', betrag, 'Barauszahlung')
SendeKonto(quelle)
TriggerClientEvent('aether:bank:erfolg', quelle,
'Bitte entnehmen Sie Ihr Geld', betrag .. ' € wurden ausgezahlt.')
end)
-- ---------------------------------------------------------------------
-- Einzahlen (Bargeld -> Konto)
-- ---------------------------------------------------------------------
RegisterNetEvent('aether:bank:einzahlen', function(betrag)
local quelle = source
if not angemeldet[quelle] then return end
betrag = PruefeBetrag(betrag)
if not betrag then Ablehnen(quelle, 'Ungültiger Betrag.') return end
if not Core:EntferneGeld(quelle, 'bargeld', betrag, 'Bareinzahlung') then
Ablehnen(quelle, 'Du hast nicht so viel Bargeld dabei.')
return
end
Core:FuegeGeldHinzu(quelle, 'konto', betrag, 'Bareinzahlung Automat')
SendeKonto(quelle)
TriggerClientEvent('aether:bank:erfolg', quelle,
'Einzahlung erfolgreich', betrag .. ' € wurden gutgeschrieben.')
end)
-- ---------------------------------------------------------------------
-- Ueberweisen (Konto -> fremdes Konto, auch offline)
-- ---------------------------------------------------------------------
RegisterNetEvent('aether:bank:ueberweisen', function(ziel, betrag, zweck)
local quelle = source
if not angemeldet[quelle] then return end
local charakterId = CharakterVon(quelle)
if not charakterId then return end
betrag = PruefeBetrag(betrag)
if not betrag then Ablehnen(quelle, 'Ungültiger Betrag.') return end
if type(ziel) ~= 'string' or #ziel < 3 then
Ablehnen(quelle, 'Bitte eine gültige IBAN angeben.')
return
end
zweck = type(zweck) == 'string' and zweck:sub(1, 64) or ''
-- Empfaenger ueber die IBAN suchen
local empfaenger = MySQL.query.await([[
SELECT id, firstname, lastname, bank
FROM aether_characters
WHERE iban = ? AND deleted = 0
LIMIT 1
]], { ziel })
if not empfaenger or not empfaenger[1] then
Ablehnen(quelle, 'Kein Konto mit dieser IBAN gefunden.')
return
end
local ziel_ = empfaenger[1]
if ziel_.id == charakterId then
Ablehnen(quelle, 'Du kannst dir nicht selbst überweisen.')
return
end
local empfaengerName = ziel_.firstname .. ' ' .. ziel_.lastname
local verwendung = zweck ~= '' and (' (' .. zweck .. ')') or ''
-- Abbuchen (prueft die Deckung)
if not Core:EntferneGeld(quelle, 'konto', betrag,
'Überweisung an ' .. empfaengerName .. verwendung) then
Ablehnen(quelle, 'Nicht genügend Guthaben.')
return
end
-- Gutschrift: bei Online-Spielern ueber den Core, sonst direkt in der Datenbank
local zielQuelle = Core:HoleQuelleNachCharakter(ziel_.id)
if zielQuelle then
Core:FuegeGeldHinzu(zielQuelle, 'konto', betrag,
'Überweisung von Konto' .. verwendung)
TriggerClientEvent('aether:bank:benachrichtigung', zielQuelle,
('Du hast %d € erhalten.'):format(betrag))
else
local neuerStand = (ziel_.bank or 0) + betrag
MySQL.update.await('UPDATE aether_characters SET bank = ? WHERE id = ?',
{ neuerStand, ziel_.id })
MySQL.insert('INSERT INTO aether_transactions (`character`, type, amount, balance, label) VALUES (?, ?, ?, ?, ?)',
{ ziel_.id, 'transfer', betrag, neuerStand, 'Überweisung erhalten' .. verwendung })
end
Aether.Info(('Überweisung: Charakter #%d -> #%d, %d €'):format(charakterId, ziel_.id, betrag))
SendeKonto(quelle)
TriggerClientEvent('aether:bank:erfolg', quelle,
'Überweisung ausgeführt', ('%d € an %s gesendet.'):format(betrag, empfaengerName))
end)
-- ---------------------------------------------------------------------
-- Aufraeumen
-- ---------------------------------------------------------------------
RegisterNetEvent('aether:bank:schliessen', function()
angemeldet[source] = nil
end)
AddEventHandler('playerDropped', function()
angemeldet[source] = nil
end)