Part 1: registration, login and character creation written from scratch

Всё, что здесь лежит, написал ИИ-агент на живом сервере под запись — без ESX,
без QBCore, без скачанных с форума скриптов.

- resources/justrp — свой ресурс на Lua: регистрация, вход, создание персонажа, спавн
- resources/justrp/html — экраны NUI: кинематографичная загрузка, вход, редактор персонажа
- api/api.py — внутренний HTTP-API к MariaDB, пароли через scrypt
- server.cfg.example — пример конфига

Секреты (пароль БД, внутренний ключ API) вынесены в переменные окружения и convar.
Ключ Cfx.re в репозиторий не попадает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
RedlHosting
2026-08-06 03:28:58 +00:00
co-authored by Claude Opus 5
parent 968d047f87
commit 0144e83f05
8 changed files with 2975 additions and 3 deletions
+210
View File
@@ -0,0 +1,210 @@
-- JustRP — Server-side core
-- All DB operations go through the internal Python API on :8787
local API_BASE = "http://127.0.0.1:8787"
-- Должен совпадать с JUSTRP_API_SECRET у Python-API. Задаётся в server.cfg:
-- set justrp_api_key "ваш-ключ"
local API_KEY = GetConvar("justrp_api_key", "change-me")
-- Per-player session state
local Sessions = {}
-- ─── HTTP helper ────────────────────────────────────────────────────────────
local function apiRequest(method, path, data, cb)
PerformHttpRequest(
API_BASE .. path,
function(status, body, _headers)
local ok, result = pcall(json.decode, body or "{}")
if ok then
cb(status, result)
else
cb(status, { success = false, error = "parse error" })
end
end,
method,
data and json.encode(data) or "",
{
["Content-Type"] = "application/json",
["X-API-Key"] = API_KEY,
}
)
end
-- ─── Player connecting ───────────────────────────────────────────────────────
AddEventHandler("playerConnecting", function(name, setKickReason, deferrals)
local src = source
local license = GetPlayerIdentifierByType(src, "license") or
GetPlayerIdentifierByType(src, "license2") or
"license:" .. tostring(src)
deferrals.defer()
Wait(0)
deferrals.update("Checking account…")
apiRequest("GET", "/account?license=" .. license, nil, function(status, res)
if status == 200 and res.success then
if res.account.banned == 1 then
deferrals.done("You are banned from JustRP.\nReason: " .. (res.account.ban_reason or "No reason given"))
else
deferrals.done()
end
else
-- No account yet — let them through, they'll register in-game
deferrals.done()
end
end)
end)
-- ─── Client ready → start session ───────────────────────────────────────────
RegisterNetEvent("justrp:clientReady")
AddEventHandler("justrp:clientReady", function()
local src = source
local license = GetPlayerIdentifierByType(src, "license") or
GetPlayerIdentifierByType(src, "license2") or
"license:" .. tostring(src)
Sessions[src] = { license = license, accountId = nil, charId = nil }
TriggerClientEvent("justrp:startSession", src)
end)
-- ─── Auth events ────────────────────────────────────────────────────────────
RegisterNetEvent("justrp:auth:register")
AddEventHandler("justrp:auth:register", function(username, password)
local src = source
local ses = Sessions[src]
if not ses then return end
apiRequest("POST", "/auth/register", {
license = ses.license,
username = username,
password = password,
}, function(status, res)
if res.success then
ses.accountId = res.account_id
TriggerClientEvent("justrp:auth:ok", src, res.account_id, res.username)
else
TriggerClientEvent("justrp:auth:fail", src, res.error or "Unknown error")
end
end)
end)
RegisterNetEvent("justrp:auth:login")
AddEventHandler("justrp:auth:login", function(username, password)
local src = source
local ses = Sessions[src]
if not ses then return end
apiRequest("POST", "/auth/login", {
license = ses.license,
username = username,
password = password,
}, function(status, res)
if res.success then
ses.accountId = res.account_id
TriggerClientEvent("justrp:auth:ok", src, res.account_id, res.username)
else
TriggerClientEvent("justrp:auth:fail", src, res.error or "Unknown error")
end
end)
end)
-- ─── Character events ────────────────────────────────────────────────────────
RegisterNetEvent("justrp:chars:load")
AddEventHandler("justrp:chars:load", function()
local src = source
local ses = Sessions[src]
if not ses or not ses.accountId then
TriggerClientEvent("justrp:chars:list", src, {})
return
end
apiRequest("GET", "/characters?account_id=" .. ses.accountId, nil, function(status, res)
if res.success then
TriggerClientEvent("justrp:chars:list", src, res.characters or {})
else
TriggerClientEvent("justrp:chars:list", src, {})
end
end)
end)
RegisterNetEvent("justrp:char:create")
AddEventHandler("justrp:char:create", function(data)
local src = source
local ses = Sessions[src]
if not ses or not ses.accountId then return end
data.account_id = ses.accountId
apiRequest("POST", "/character/create", data, function(status, res)
if res.success then
ses.charId = res.character_id
TriggerClientEvent("justrp:char:created", src, res.character_id)
else
TriggerClientEvent("justrp:char:createFail", src, res.error or "Unknown error")
end
end)
end)
RegisterNetEvent("justrp:char:select")
AddEventHandler("justrp:char:select", function(charId)
local src = source
local ses = Sessions[src]
if not ses or not ses.accountId then return end
apiRequest("GET", "/character?id=" .. tostring(charId), nil, function(status, res)
if res.success and res.character then
local ch = res.character
if ch.account_id ~= ses.accountId then
return -- character doesn't belong to this account
end
ses.charId = charId
TriggerClientEvent("justrp:char:loaded", src, ch)
end
end)
end)
-- ─── Spawn ───────────────────────────────────────────────────────────────────
RegisterNetEvent("justrp:spawn:request")
AddEventHandler("justrp:spawn:request", function(spawnKey)
local src = source
local ses = Sessions[src]
if not ses or not ses.charId then return end
local spawns = {
legion = { x = -166.7, y = -928.9, z = 31.4, h = 120.0 },
vespucci = { x = -1380.0, y = -1520.0, z = 4.9, h = 75.0 },
sandy = { x = 1843.4, y = 3683.0, z = 34.3, h = 200.0 },
paleto = { x = -165.8, y = 6330.0, z = 31.5, h = 270.0 },
airport = { x = -1037.0, y = -2738.0, z = 20.0, h = 330.0 },
}
local pos = spawns[spawnKey] or spawns["legion"]
TriggerClientEvent("justrp:spawn:go", src, pos)
end)
-- ─── Auto-save position every 60s ────────────────────────────────────────────
RegisterNetEvent("justrp:savePosition")
AddEventHandler("justrp:savePosition", function(x, y, z, h)
local src = source
local ses = Sessions[src]
if not ses or not ses.charId then return end
apiRequest("POST", "/character/save", {
character_id = ses.charId,
last_x = x,
last_y = y,
last_z = z,
last_heading = h,
}, function() end)
end)
-- ─── Disconnect ──────────────────────────────────────────────────────────────
AddEventHandler("playerDropped", function()
local src = source
Sessions[src] = nil
end)
print("[JustRP] Server core loaded.")