first commit
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
local open = false
|
||||
|
||||
local function setOpen(state)
|
||||
open = state
|
||||
SetNuiFocus(state, state)
|
||||
SendNUIMessage({ action = state and 'open' or 'close' })
|
||||
if state then
|
||||
DevXTriggerCallback('admin:bootstrap', {}, function(result)
|
||||
if not result.ok then
|
||||
open = false
|
||||
SetNuiFocus(false, false)
|
||||
SendNUIMessage({ action = 'close' })
|
||||
exports.devx_core:Notify(result.error or 'Kein Adminzugriff.')
|
||||
return
|
||||
end
|
||||
SendNUIMessage({ action = 'state', players = result.players, weapons = result.weapons })
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
RegisterCommand('admin', function()
|
||||
setOpen(not open)
|
||||
end, false)
|
||||
|
||||
RegisterNUICallback('close', function(_, cb)
|
||||
setOpen(false)
|
||||
cb({ ok = true })
|
||||
end)
|
||||
|
||||
RegisterNUICallback('action', function(data, cb)
|
||||
DevXTriggerCallback('admin:action', data, function(result)
|
||||
if result.players then SendNUIMessage({ action = 'players', players = result.players }) end
|
||||
cb(result)
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('devx_admin:client:giveWeapon', function(weapon, ammo)
|
||||
GiveWeaponToPed(PlayerPedId(), GetHashKey(weapon), tonumber(ammo) or 250, false, true)
|
||||
exports.devx_core:Notify(('Adminwaffe erhalten: %s'):format(weapon))
|
||||
end)
|
||||
|
||||
RegisterNetEvent('devx_admin:client:heal', function()
|
||||
local ped = PlayerPedId()
|
||||
SetEntityHealth(ped, GetEntityMaxHealth(ped))
|
||||
SetPedArmour(ped, 100)
|
||||
ClearPedBloodDamage(ped)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('devx_admin:client:freeze', function(state)
|
||||
FreezeEntityPosition(PlayerPedId(), state == true)
|
||||
exports.devx_core:Notify(state and 'Du wurdest eingefroren.' or 'Du wurdest freigegeben.')
|
||||
end)
|
||||
|
||||
RegisterNetEvent('devx_admin:client:teleport', function(coords)
|
||||
local ped = PlayerPedId()
|
||||
SetEntityCoordsNoOffset(ped, coords.x, coords.y, coords.z, false, false, false)
|
||||
end)
|
||||
|
||||
AddEventHandler('onResourceStop', function(resourceName)
|
||||
if resourceName == GetCurrentResourceName() then SetNuiFocus(false, false) end
|
||||
end)
|
||||
@@ -0,0 +1,14 @@
|
||||
fx_version 'cerulean'
|
||||
game 'gta5'
|
||||
|
||||
author 'Colin-Joel / DevX Studios'
|
||||
description 'DevX RP ACE-secured administration interface'
|
||||
version '0.4.3'
|
||||
lua54 'yes'
|
||||
|
||||
shared_script '@devx_core/shared/config.lua'
|
||||
ui_page 'web/index.html'
|
||||
files { 'web/index.html', 'web/style.css', 'web/app.js' }
|
||||
client_scripts { '@devx_core/client/callbacks.lua', 'client/main.lua' }
|
||||
server_scripts { '@oxmysql/lib/MySQL.lua', '@devx_core/server/callbacks.lua', 'server/main.lua' }
|
||||
dependencies { 'devx_core', 'devx_banking' }
|
||||
@@ -0,0 +1,147 @@
|
||||
local frozenPlayers = {}
|
||||
|
||||
local function isAdmin(source)
|
||||
return source == 0 or IsPlayerAceAllowed(source, 'devx.admin')
|
||||
end
|
||||
|
||||
local function cleanReason(value)
|
||||
if type(value) ~= 'string' then return 'Keine Begründung angegeben.' end
|
||||
value = value:gsub('^%s+', ''):gsub('%s+$', '')
|
||||
if value == '' then return 'Keine Begründung angegeben.' end
|
||||
return value:sub(1, 255)
|
||||
end
|
||||
|
||||
local function playerRows()
|
||||
local rows = {}
|
||||
for _, id in ipairs(GetPlayers()) do
|
||||
local source = tonumber(id)
|
||||
local player = exports.devx_core:GetPlayer(source)
|
||||
rows[#rows + 1] = {
|
||||
source = source,
|
||||
connectionName = GetPlayerName(source) or ('Spieler %s'):format(source),
|
||||
fullName = player and player.fullName or 'Kein Charakter geladen',
|
||||
characterId = player and player.characterId or nil,
|
||||
phoneNumber = player and player.phoneNumber or nil,
|
||||
cash = player and player.cash or 0,
|
||||
bank = player and player.bank or 0,
|
||||
rpXp = player and player.rpXp or 0,
|
||||
frozen = frozenPlayers[source] == true
|
||||
}
|
||||
end
|
||||
table.sort(rows, function(a, b) return a.source < b.source end)
|
||||
return rows
|
||||
end
|
||||
|
||||
DevXRegisterCallback('admin:bootstrap', function(source)
|
||||
if not isAdmin(source) then return { ok = false, error = 'Keine Administratorberechtigung.' } end
|
||||
|
||||
local weapons = {}
|
||||
for name, label in pairs(DevXConfig.AdminWeapons or {}) do
|
||||
weapons[#weapons + 1] = { name = name, label = label }
|
||||
end
|
||||
table.sort(weapons, function(a, b) return a.label < b.label end)
|
||||
|
||||
return { ok = true, players = playerRows(), weapons = weapons }
|
||||
end)
|
||||
|
||||
DevXRegisterCallback('admin:action', function(source, payload)
|
||||
if not isAdmin(source) then return { ok = false, error = 'Keine Administratorberechtigung.' } end
|
||||
|
||||
local action = tostring(payload.action or '')
|
||||
local target = tonumber(payload.target)
|
||||
local targetPlayer = target and exports.devx_core:GetPlayer(target) or nil
|
||||
|
||||
if action == 'refresh' then return { ok = true, players = playerRows() } end
|
||||
if not target or not GetPlayerName(target) then return { ok = false, error = 'Spieler ist nicht mehr online.' } end
|
||||
|
||||
if action == 'setcash' then
|
||||
local amount = math.floor(tonumber(payload.amount) or -1)
|
||||
if amount < 0 or amount > DevXConfig.MaxMoneyOperation or not targetPlayer then
|
||||
return { ok = false, error = 'Ungültiger Bargeldbetrag.' }
|
||||
end
|
||||
if not exports.devx_core:SetCash(target, amount, ('Admin %s'):format(source)) then
|
||||
return { ok = false, error = 'Bargeld konnte nicht geändert werden.' }
|
||||
end
|
||||
|
||||
elseif action == 'setbank' then
|
||||
local amount = math.floor(tonumber(payload.amount) or -1)
|
||||
if amount < 0 or amount > DevXConfig.MaxMoneyOperation or not targetPlayer then
|
||||
return { ok = false, error = 'Ungültiger Bankbetrag.' }
|
||||
end
|
||||
local success, errorMessage = exports.devx_banking:SetPlayerBalance(target, amount, ('Admin %s'):format(source))
|
||||
if not success then return { ok = false, error = errorMessage } end
|
||||
|
||||
elseif action == 'giveweapon' then
|
||||
local weapon = tostring(payload.weapon or ''):upper()
|
||||
if not DevXConfig.AdminWeapons[weapon] then return { ok = false, error = 'Waffe ist nicht freigegeben.' } end
|
||||
TriggerClientEvent('devx_admin:client:giveWeapon', target, weapon, 250)
|
||||
|
||||
elseif action == 'heal' then
|
||||
TriggerClientEvent('devx_admin:client:heal', target)
|
||||
|
||||
elseif action == 'freeze' then
|
||||
frozenPlayers[target] = not frozenPlayers[target]
|
||||
TriggerClientEvent('devx_admin:client:freeze', target, frozenPlayers[target])
|
||||
|
||||
elseif action == 'goto' then
|
||||
local ped = GetPlayerPed(target)
|
||||
if ped <= 0 then return { ok = false, error = 'Zielposition ist nicht verfügbar.' } end
|
||||
local coords = GetEntityCoords(ped)
|
||||
TriggerClientEvent('devx_admin:client:teleport', source, { x = coords.x, y = coords.y, z = coords.z + 1.0 })
|
||||
|
||||
elseif action == 'bring' then
|
||||
local ped = GetPlayerPed(source)
|
||||
if ped <= 0 then return { ok = false, error = 'Adminposition ist nicht verfügbar.' } end
|
||||
local coords = GetEntityCoords(ped)
|
||||
TriggerClientEvent('devx_admin:client:teleport', target, { x = coords.x, y = coords.y, z = coords.z + 1.0 })
|
||||
|
||||
elseif action == 'setphone' then
|
||||
local number = tostring(payload.phoneNumber or ''):gsub('%s+', '')
|
||||
if not targetPlayer or not number:match('^%d%d%d%-%d%d%d%d+$') or #number > 16 then
|
||||
return { ok = false, error = 'Telefonnummer muss wie 555-1234 aussehen.' }
|
||||
end
|
||||
local duplicate = MySQL.scalar.await('SELECT id FROM devx_characters WHERE phone_number = ? AND id <> ? LIMIT 1', {
|
||||
number, targetPlayer.characterId
|
||||
})
|
||||
if duplicate then return { ok = false, error = 'Telefonnummer ist bereits vergeben.' } end
|
||||
MySQL.update.await('UPDATE devx_characters SET phone_number = ? WHERE id = ?', { number, targetPlayer.characterId })
|
||||
targetPlayer.phoneNumber = number
|
||||
TriggerClientEvent('devx_core:client:playerData', target, targetPlayer)
|
||||
|
||||
elseif action == 'kick' then
|
||||
local reason = cleanReason(payload.reason)
|
||||
DropPlayer(target, ('Du wurdest von einem Administrator getrennt.\nGrund: %s'):format(reason))
|
||||
|
||||
elseif action == 'ban' then
|
||||
local reason = cleanReason(payload.reason)
|
||||
local minutes = math.max(0, math.floor(tonumber(payload.minutes) or 0))
|
||||
local license = exports.devx_core:GetLicense(target)
|
||||
if not license then return { ok = false, error = 'Spielerlizenz konnte nicht ermittelt werden.' } end
|
||||
|
||||
local expiresAt = nil
|
||||
if minutes > 0 then
|
||||
expiresAt = os.date('!%Y-%m-%d %H:%M:%S', os.time() + minutes * 60)
|
||||
end
|
||||
|
||||
MySQL.insert.await([[
|
||||
INSERT INTO devx_bans (license, player_name, reason, banned_by, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
]], {
|
||||
license,
|
||||
targetPlayer and targetPlayer.fullName or GetPlayerName(target),
|
||||
reason,
|
||||
GetPlayerName(source) or ('Admin %s'):format(source),
|
||||
expiresAt
|
||||
})
|
||||
|
||||
DropPlayer(target, ('Du wurdest von DevX-City gesperrt.\nGrund: %s'):format(reason))
|
||||
else
|
||||
return { ok = false, error = 'Unbekannte Adminaktion.' }
|
||||
end
|
||||
|
||||
return { ok = true, players = playerRows() }
|
||||
end)
|
||||
|
||||
AddEventHandler('playerDropped', function()
|
||||
frozenPlayers[source] = nil
|
||||
end)
|
||||
@@ -0,0 +1,21 @@
|
||||
const resource=typeof GetParentResourceName==='function'?GetParentResourceName():'devx_admin';
|
||||
const app=document.querySelector('#app'),playersEl=document.querySelector('#players'),editor=document.querySelector('#editor'),errorEl=document.querySelector('#error');
|
||||
let players=[],weapons=[],selectedId=null;
|
||||
async function post(event,data={}){const r=await fetch(`https://${resource}/${event}`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});return r.json()}
|
||||
function esc(v){return String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}
|
||||
function error(t=''){errorEl.textContent=t;errorEl.classList.toggle('hidden',!t)}
|
||||
function selected(){return players.find(p=>Number(p.source)===Number(selectedId))}
|
||||
function renderPlayers(){const q=document.querySelector('#search').value.toLowerCase();playersEl.innerHTML='';players.filter(p=>`${p.source} ${p.fullName} ${p.connectionName}`.toLowerCase().includes(q)).forEach(p=>{const b=document.createElement('button');b.className=`player ${Number(p.source)===Number(selectedId)?'active':''}`;b.innerHTML=`<span>${p.source}</span><div><strong>${esc(p.fullName)}</strong><small>${esc(p.connectionName)}</small></div>`;b.onclick=()=>{selectedId=p.source;renderPlayers();renderEditor()};playersEl.appendChild(b)})}
|
||||
function action(action,data={}){error('');return post('action',{action,target:selectedId,...data}).then(r=>{if(!r.ok)error(r.error||'Aktion fehlgeschlagen.');if(r.players){players=r.players;renderPlayers();renderEditor()}return r})}
|
||||
function renderEditor(){const p=selected();if(!p){editor.className='editor empty';editor.innerHTML='<div><h2>Spieler auswählen</h2><p>Wähle links einen Online-Spieler aus.</p></div>';return}editor.className='editor';editor.innerHTML=`
|
||||
<div class="editor-header"><div><h2>${esc(p.fullName)}</h2><p>Server-ID ${p.source} · Charakter ${p.characterId??'–'} · ${esc(p.phoneNumber||'keine Nummer')}</p></div><div class="badges"><span class="badge">${p.cash} $ bar</span><span class="badge">${p.bank} $ Bank</span><span class="badge">${p.rpXp} XP</span></div></div>
|
||||
<div class="grid">
|
||||
<div class="card"><h3>Geld bearbeiten</h3><div class="form-row"><input id="cash" type="number" min="0" value="${p.cash}"><button class="primary" data-act="setcash">Bargeld setzen</button></div><div class="form-row"><input id="bank" type="number" min="0" value="${p.bank}"><button class="primary" data-act="setbank">Bank setzen</button></div></div>
|
||||
<div class="card"><h3>Identität</h3><div class="form-row"><input id="phone" value="${esc(p.phoneNumber||'')}"><button class="primary" data-act="setphone">Nummer setzen</button></div></div>
|
||||
<div class="card"><h3>Waffe geben</h3><div class="form-row"><select id="weapon">${weapons.map(w=>`<option value="${w.name}">${esc(w.label)}</option>`).join('')}</select><button class="primary" data-act="giveweapon">Geben</button></div></div>
|
||||
<div class="card"><h3>Spieleraktionen</h3><div class="button-grid"><button data-act="heal">Heilen</button><button data-act="freeze">${p.frozen?'Freigeben':'Einfrieren'}</button><button data-act="goto">Zu Spieler</button><button data-act="bring">Herholen</button></div></div>
|
||||
<div class="card punish"><h3>Verwarnung / Trennung</h3><div class="punish-fields"><input id="reason" class="full-input" placeholder="Begründung"><input id="minutes" class="full-input" type="number" min="0" placeholder="Minuten (0 = permanent)"></div><div class="button-grid"><button class="danger" data-act="kick">Kicken</button><button class="danger" data-act="ban">Bannen</button></div></div>
|
||||
</div>`;
|
||||
editor.querySelectorAll('[data-act]').forEach(b=>b.onclick=()=>{const a=b.dataset.act;if(a==='setcash')action(a,{amount:document.querySelector('#cash').value});else if(a==='setbank')action(a,{amount:document.querySelector('#bank').value});else if(a==='setphone')action(a,{phoneNumber:document.querySelector('#phone').value});else if(a==='giveweapon')action(a,{weapon:document.querySelector('#weapon').value});else if(a==='kick'||a==='ban')action(a,{reason:document.querySelector('#reason').value,minutes:document.querySelector('#minutes').value});else action(a)})}
|
||||
document.querySelector('#search').oninput=renderPlayers;document.querySelector('#refresh').onclick=()=>action('refresh');document.querySelector('#close').onclick=()=>post('close');document.addEventListener('keyup',e=>{if(e.key==='Escape')post('close')});
|
||||
window.addEventListener('message',e=>{const d=e.data||{};if(d.action==='open')app.classList.remove('hidden');if(d.action==='close')app.classList.add('hidden');if(d.action==='state'){players=d.players||[];weapons=d.weapons||[];selectedId=players[0]?.source??null;renderPlayers();renderEditor()}if(d.action==='players'){players=d.players||[];renderPlayers();renderEditor()}});
|
||||
@@ -0,0 +1,11 @@
|
||||
<!doctype html><html lang="de"><head><meta charset="utf-8"><link rel="stylesheet" href="style.css"></head>
|
||||
<body><main id="app" class="hidden">
|
||||
<section class="admin-shell">
|
||||
<header><div><small>DEVX-CITY</small><h1>Administration</h1></div><div class="header-actions"><button id="refresh">Aktualisieren</button><button id="close">×</button></div></header>
|
||||
<div id="error" class="error hidden"></div>
|
||||
<div class="layout">
|
||||
<aside><input id="search" placeholder="Spieler suchen"><div id="players" class="players"></div></aside>
|
||||
<section id="editor" class="editor empty"><h2>Spieler auswählen</h2><p>Wähle links einen Online-Spieler aus.</p></section>
|
||||
</div>
|
||||
</section>
|
||||
</main><script src="app.js"></script></body></html>
|
||||
@@ -0,0 +1,5 @@
|
||||
:root{font-family:Inter,system-ui,sans-serif;color:#f6f7fb;--line:rgba(255,255,255,.1);--muted:#929baa;--accent:#5e82f4}
|
||||
*{box-sizing:border-box}body{margin:0;background:transparent;overflow:hidden}.hidden{display:none!important}button,input,select{font:inherit}
|
||||
#app{position:fixed;inset:0;display:grid;place-items:center;background:rgba(2,4,8,.62)}
|
||||
.admin-shell{width:min(1180px,94vw);height:min(760px,90vh);padding:25px;border:1px solid var(--line);border-radius:16px;background:#0d1119;box-shadow:0 35px 110px rgba(0,0,0,.62)}
|
||||
header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}header small{color:#84a2ff;letter-spacing:.2em;font-weight:850}h1{margin:4px 0 0}.header-actions{display:flex;gap:8px}button{border:0;border-radius:8px;padding:10px 13px;color:white;background:rgba(255,255,255,.08);cursor:pointer}#close{font-size:22px}.layout{height:calc(100% - 78px);display:grid;grid-template-columns:320px 1fr;gap:18px}aside,.editor{border:1px solid var(--line);border-radius:12px;background:#111721;padding:15px;overflow:auto}#search{width:100%;padding:11px;border:1px solid var(--line);border-radius:8px;background:#090d13;color:white;outline:0}.players{display:grid;gap:7px;margin-top:12px}.player{width:100%;display:grid;grid-template-columns:42px 1fr;gap:10px;text-align:left;padding:11px;background:rgba(255,255,255,.04);border:1px solid transparent}.player.active{border-color:#6689f4;background:rgba(72,106,215,.18)}.player span{display:grid;place-items:center;width:38px;height:38px;border-radius:9px;background:#27375d;font-weight:850}.player strong{display:block}.player small{color:var(--muted)}.editor.empty{display:grid;place-items:center;text-align:center;color:var(--muted)}.editor-header{display:flex;justify-content:space-between;align-items:flex-start}.editor-header h2{margin:0}.editor-header p{margin:5px 0;color:var(--muted)}.badges{display:flex;gap:7px}.badge{padding:7px 9px;border-radius:7px;background:rgba(255,255,255,.06);font-size:11px}.grid{display:grid;grid-template-columns:repeat(2,1fr);gap:13px;margin-top:20px}.card{padding:15px;border:1px solid var(--line);border-radius:10px;background:rgba(255,255,255,.025)}.card h3{margin:0 0 12px;font-size:14px}.form-row{display:grid;grid-template-columns:1fr auto;gap:7px;margin-top:8px}.form-row input,.form-row select,.full-input{width:100%;padding:10px;border:1px solid var(--line);border-radius:8px;background:#090d13;color:white}.primary{background:#557af0}.danger{background:rgba(197,62,81,.23);color:#ffabb5}.button-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:7px}.error{margin-bottom:12px;padding:10px;border-radius:8px;background:rgba(184,48,67,.25);color:#ffadb7}.punish{grid-column:1/-1}.punish-fields{display:grid;grid-template-columns:1fr 140px;gap:8px;margin-bottom:8px}
|
||||
Reference in New Issue
Block a user