first commit
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
-- d4rk_tablet — Bridge Client
|
||||
-- Tablet (Web-iframe) öffnen, Auth-Token an die NUI übergeben, Waypoint setzen.
|
||||
|
||||
local isOpen = false
|
||||
|
||||
local function closeTablet()
|
||||
if not isOpen then return end
|
||||
isOpen = false
|
||||
SetNuiFocus(false, false)
|
||||
SendNUIMessage({ action = 'close' })
|
||||
end
|
||||
|
||||
local function openTablet()
|
||||
if isOpen then return end
|
||||
-- Token vom Server holen (Server-zu-Server zum Backend)
|
||||
local auth = lib.callback.await('d4rk_tablet:getAuth', false)
|
||||
if not auth or not auth.token then
|
||||
lib.notify({ title = 'MDT', description = 'Kein Zugriff (kein Behörden-Job?).', type = 'error' })
|
||||
return
|
||||
end
|
||||
isOpen = true
|
||||
SetNuiFocus(true, true)
|
||||
SendNUIMessage({
|
||||
action = 'open',
|
||||
webUrl = Config.WebUrl,
|
||||
token = auth.token,
|
||||
user = auth.user,
|
||||
})
|
||||
end
|
||||
|
||||
RegisterCommand(Config.OpenCommand, openTablet, false)
|
||||
RegisterKeyMapping(Config.OpenCommand, 'MDT öffnen', 'keyboard', '')
|
||||
|
||||
-- Aus der Web-App (via NUI-Shell) kommende Aktionen
|
||||
RegisterNUICallback('close', function(_, cb)
|
||||
closeTablet()
|
||||
cb({ ok = true })
|
||||
end)
|
||||
|
||||
RegisterNUICallback('setWaypoint', function(data, cb)
|
||||
if data and data.x and data.y then
|
||||
SetNewWaypoint(data.x + 0.0, data.y + 0.0)
|
||||
lib.notify({ title = 'MDT', description = 'Wegpunkt gesetzt.', type = 'success' })
|
||||
end
|
||||
cb({ ok = true })
|
||||
end)
|
||||
|
||||
-- ESC schließt das Tablet
|
||||
CreateThread(function()
|
||||
while true do
|
||||
if isOpen then
|
||||
if IsControlJustReleased(0, 322) then -- ESC
|
||||
closeTablet()
|
||||
end
|
||||
Wait(0)
|
||||
else
|
||||
Wait(250)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('onResourceStop', function(resource)
|
||||
if resource == GetCurrentResourceName() and isOpen then
|
||||
SetNuiFocus(false, false)
|
||||
end
|
||||
end)
|
||||
@@ -0,0 +1,29 @@
|
||||
Config = {}
|
||||
|
||||
-- Basis-URL des MDT-Backends (Server-zu-Server)
|
||||
Config.BackendUrl = GetConvar('mdt:backendUrl', 'http://127.0.0.1:3000')
|
||||
|
||||
-- Shared Secret zwischen Bridge (Server) und Backend.
|
||||
-- MUSS BRIDGE_HMAC_SECRET in der Backend-.env entsprechen. In server.cfg setzen:
|
||||
-- set mdt:bridgeSecret "dein-langes-geheimnis"
|
||||
Config.Secret = GetConvar('mdt:bridgeSecret', 'dev-insecure-bridge-secret')
|
||||
|
||||
-- Gehostete Web-App, die im NUI-iframe geladen wird
|
||||
Config.WebUrl = GetConvar('mdt:webUrl', 'http://localhost:5190')
|
||||
|
||||
-- Command zum Öffnen des Tablets
|
||||
Config.OpenCommand = GetConvar('mdt:openCommand', 'mdt')
|
||||
|
||||
-- Jobs, die das MDT öffnen dürfen → Basis-Department (police/ems/fire)
|
||||
Config.JobDepartments = {
|
||||
police = 'police',
|
||||
bcso = 'police',
|
||||
sheriff = 'police',
|
||||
doj = 'police',
|
||||
ambulance = 'ems',
|
||||
ems = 'ems',
|
||||
fire = 'fire',
|
||||
}
|
||||
|
||||
-- Intervall (ms) für den server-seitigen Positions-Push (nur im Dienst)
|
||||
Config.PositionInterval = 5000
|
||||
@@ -0,0 +1,36 @@
|
||||
fx_version 'cerulean'
|
||||
game 'gta5'
|
||||
|
||||
author 'D4rkst3r'
|
||||
description 'd4rk_tablet — FiveM Bridge (Token, NUI-Öffner, Live-Sync zum MDT-Backend)'
|
||||
version '1.0.0'
|
||||
lua54 'yes'
|
||||
|
||||
dependencies {
|
||||
'qbx_core',
|
||||
'ox_lib',
|
||||
}
|
||||
|
||||
shared_scripts {
|
||||
'@ox_lib/init.lua',
|
||||
'config.lua',
|
||||
}
|
||||
|
||||
client_scripts {
|
||||
'client/main.lua',
|
||||
}
|
||||
|
||||
server_scripts {
|
||||
'server/main.lua',
|
||||
}
|
||||
|
||||
ui_page 'nui/index.html'
|
||||
|
||||
files {
|
||||
'nui/index.html',
|
||||
'nui/app.js',
|
||||
'nui/style.css',
|
||||
}
|
||||
|
||||
-- HINWEIS: Die MDT-Logik lebt im Backend/Web. Diese Resource ist bewusst minimal:
|
||||
-- Token holen · NUI (Web-iframe) öffnen · Positionen/Dienst pushen · Waypoint setzen.
|
||||
@@ -0,0 +1,55 @@
|
||||
// d4rk_tablet — NUI-Shell
|
||||
// Relayt zwischen FiveM (SendNUIMessage / NUI-Callbacks) und der Web-App im iframe (postMessage).
|
||||
|
||||
const wrap = document.getElementById('wrap');
|
||||
const iframe = document.getElementById('mdt');
|
||||
|
||||
let pendingAuth = null; // { token, user } bis das iframe "mdt:ready" meldet
|
||||
|
||||
function resourceName() {
|
||||
return typeof GetParentResourceName === 'function' ? GetParentResourceName() : 'd4rk_tablet';
|
||||
}
|
||||
function nui(endpoint, body) {
|
||||
return fetch(`https://${resourceName()}/${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body || {}),
|
||||
}).catch(() => {});
|
||||
}
|
||||
function sendAuth() {
|
||||
if (pendingAuth && iframe.contentWindow) {
|
||||
iframe.contentWindow.postMessage(
|
||||
{ action: 'auth', token: pendingAuth.token, user: pendingAuth.user },
|
||||
'*',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Token nachschieben, sobald das iframe fertig geladen ist
|
||||
iframe.addEventListener('load', sendAuth);
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
const data = event.data || {};
|
||||
|
||||
// 1) Nachrichten aus der Web-App (iframe)
|
||||
if (event.source === iframe.contentWindow) {
|
||||
if (data.action === 'mdt:ready') sendAuth();
|
||||
else if (data.action === 'mdt:close') nui('close');
|
||||
else if (data.action === 'officer:setWaypoint') nui('setWaypoint', { x: data.x, y: data.y });
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Nachrichten aus FiveM (SendNUIMessage)
|
||||
if (data.action === 'open') {
|
||||
pendingAuth = { token: data.token, user: data.user };
|
||||
const target = data.webUrl;
|
||||
if (iframe.src !== target) {
|
||||
iframe.src = target; // 'load' → sendAuth()
|
||||
} else {
|
||||
sendAuth(); // bereits geladen (Reopen)
|
||||
}
|
||||
wrap.classList.remove('hidden');
|
||||
} else if (data.action === 'close') {
|
||||
wrap.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>d4rk_tablet</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Dünne Shell: bettet die gehostete MDT-Web-App als iframe ein und relayed Nachrichten. -->
|
||||
<div id="wrap" class="hidden">
|
||||
<iframe id="mdt" title="MDT" allow="clipboard-read; clipboard-write"></iframe>
|
||||
</div>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#wrap {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#wrap.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#mdt {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
-- d4rk_tablet — Bridge Server
|
||||
-- Token holen (Shared-Secret), Dienst + Positionen an /bridge/* pushen, Dispatch-Export.
|
||||
|
||||
local onDuty = {} -- [src] = { citizenid, name, callsign, department }
|
||||
|
||||
--- HTTP POST an das Backend (mit Shared-Secret-Header).
|
||||
local function post(path, body, cb)
|
||||
PerformHttpRequest(Config.BackendUrl .. path, function(status, res)
|
||||
if cb then cb(status, res) end
|
||||
end, 'POST', json.encode(body), {
|
||||
['Content-Type'] = 'application/json',
|
||||
['x-bridge-secret'] = Config.Secret,
|
||||
})
|
||||
end
|
||||
|
||||
--- Baut den Auth-Body aus den QBox-Charakterdaten.
|
||||
local function authBody(player)
|
||||
local pd = player.PlayerData
|
||||
local job = pd.job or {}
|
||||
local grade = job.grade or {}
|
||||
return {
|
||||
license = pd.license,
|
||||
citizenid = pd.citizenid,
|
||||
name = ('%s %s'):format(pd.charinfo.firstname, pd.charinfo.lastname),
|
||||
job = job.name,
|
||||
gradeLevel = grade.level or 0,
|
||||
callsign = pd.metadata and pd.metadata.callsign or nil,
|
||||
}
|
||||
end
|
||||
|
||||
-- ── Token holen (Client öffnet Tablet) ──
|
||||
lib.callback.register('d4rk_tablet:getAuth', function(source)
|
||||
local player = exports.qbx_core:GetPlayer(source)
|
||||
if not player then return nil end
|
||||
if not Config.JobDepartments[player.PlayerData.job.name] then return nil end -- kein Behörden-Job
|
||||
|
||||
local p = promise.new()
|
||||
post('/auth/fivem', authBody(player), function(status, res)
|
||||
if status == 200 and res then
|
||||
local ok, data = pcall(json.decode, res)
|
||||
if ok and data and data.token then
|
||||
p:resolve({ token = data.token, user = data.user })
|
||||
return
|
||||
end
|
||||
end
|
||||
p:resolve(nil)
|
||||
end)
|
||||
return Citizen.Await(p)
|
||||
end)
|
||||
|
||||
-- ── Dienststatus ──
|
||||
local function setDuty(src, isOn)
|
||||
local player = exports.qbx_core:GetPlayer(src)
|
||||
if not player then return end
|
||||
local jobName = player.PlayerData.job.name
|
||||
local department = Config.JobDepartments[jobName]
|
||||
if not department then return end
|
||||
|
||||
if isOn then
|
||||
onDuty[src] = {
|
||||
citizenid = player.PlayerData.citizenid,
|
||||
name = ('%s %s'):format(player.PlayerData.charinfo.firstname, player.PlayerData.charinfo.lastname),
|
||||
callsign = player.PlayerData.metadata and player.PlayerData.metadata.callsign or nil,
|
||||
department = department,
|
||||
}
|
||||
end
|
||||
|
||||
local o = onDuty[src] or {
|
||||
citizenid = player.PlayerData.citizenid,
|
||||
name = ('%s %s'):format(player.PlayerData.charinfo.firstname, player.PlayerData.charinfo.lastname),
|
||||
department = department,
|
||||
}
|
||||
post('/bridge/officer/duty', {
|
||||
citizenid = o.citizenid,
|
||||
name = o.name,
|
||||
callsign = o.callsign,
|
||||
department = o.department,
|
||||
onDuty = isOn,
|
||||
})
|
||||
if not isOn then onDuty[src] = nil end
|
||||
end
|
||||
|
||||
-- QBox-Dienstwechsel (best effort) + Fallback-Command
|
||||
RegisterNetEvent('QBCore:Server:SetDuty', function(src, isOn)
|
||||
setDuty(src, isOn == true)
|
||||
end)
|
||||
lib.addCommand('mdtduty', { help = 'MDT: Dienststatus umschalten' }, function(source)
|
||||
setDuty(source, onDuty[source] == nil)
|
||||
end)
|
||||
|
||||
AddEventHandler('playerDropped', function()
|
||||
local src = source
|
||||
if onDuty[src] then setDuty(src, false) end
|
||||
end)
|
||||
|
||||
-- ── Server-seitiger Positions-Push (vertraut Client nicht) ──
|
||||
CreateThread(function()
|
||||
while true do
|
||||
Wait(Config.PositionInterval)
|
||||
for src, info in pairs(onDuty) do
|
||||
local ped = GetPlayerPed(src)
|
||||
if ped and ped ~= 0 then
|
||||
local c = GetEntityCoords(ped)
|
||||
post('/bridge/officer/position', {
|
||||
citizenid = info.citizenid,
|
||||
coords = { x = c.x, y = c.y, z = c.z },
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- ── Positionsziel an einen Officer senden (von der Leitstelle via Web → Socket → hier) ──
|
||||
RegisterNetEvent('d4rk_tablet:relayWaypoint', function() end) -- Platzhalter M8 (Socket→Bridge)
|
||||
|
||||
-- ── Dispatch-Export für andere Resourcen ──
|
||||
-- exports.d4rk_tablet:createDispatch({ code='10-90', title='Banküberfall', department='police', priority='high', location='...', coords={x=,y=,z=} })
|
||||
exports('createDispatch', function(data)
|
||||
post('/bridge/dispatch', data)
|
||||
end)
|
||||
Reference in New Issue
Block a user