ADD - open admin panel by command

This commit is contained in:
Leon.Schmidt
2026-08-20 20:10:31 +02:00
parent 22165d64df
commit 3a45aa159a
9 changed files with 155 additions and 3 deletions
+9 -2
View File
@@ -630,6 +630,8 @@ function loadUnlockedPhoneData(): void {
}
function completePhoneSetup(): void {
const requestedRoute = pendingUnlockRoute.value
pendingUnlockRoute.value = null
setupPreviewDismissed.value = true
setupAppearanceSelected.value = false
isLocked.value = false
@@ -637,7 +639,7 @@ function completePhoneSetup(): void {
passcodeVisible.value = false
passcodeRequired.value = false
controlCenterOpened.value = false
void router.replace('/')
void router.replace(requestedRoute ?? '/')
loadUnlockedPhoneData()
}
@@ -766,7 +768,12 @@ function onMessage(event: MessageEvent<AppMessage>): void {
isPhoneAppId(data.appId) &&
appStore.isInstalled(data.appId)
) {
void router.push(`/apps/${data.appId}`)
const requestedRoute = `/apps/${data.appId}`
if (setupRequired.value || isLocked.value) {
pendingUnlockRoute.value = requestedRoute
} else {
void router.push(requestedRoute)
}
} else {
console.error('[Navigation] Ignored an unavailable app target.')
}
@@ -29,4 +29,10 @@ describe('neutral phone navigation contract', () => {
expect(navigationSource).toContain('if not installed_apps[normalized_app_id] then')
expect(navigationSource).toContain('if current_app_id ~= normalized_app_id then')
})
it('defers command-driven app routes until setup or device unlock completes', () => {
expect(appSource).toContain('if (setupRequired.value || isLocked.value)')
expect(appSource).toContain('pendingUnlockRoute.value = requestedRoute')
expect(appSource).toContain("void router.replace(requestedRoute ?? '/')")
})
})
@@ -11,6 +11,14 @@ const server = readFileSync(
new URL('../../../../sky_phone/source/server/admin.lua', import.meta.url),
'utf8',
)
const phoneServer = readFileSync(
new URL('../../../../sky_phone/source/server/phone.lua', import.meta.url),
'utf8',
)
const phoneClient = readFileSync(
new URL('../../../../sky_phone/source/client/main.lua', import.meta.url),
'utf8',
)
const bridge = readFileSync(
new URL(
'../../../../sky_phone/source/client/nui_server_bridge.lua',
@@ -71,6 +79,24 @@ describe('admin command center contracts', () => {
expect(config).toContain('Config.AdminPanel = {')
})
it('opens directly from a server-authorized configurable command', () => {
expect(config).toContain('Command = "phoneadmin"')
expect(phoneServer).toContain(
'RegisterCommand(Config.AdminPanel.Command, function(command_source)',
)
expect(phoneServer).toContain(
'Bridge.Framework.HasAdminGroup(player_source, Config.AdminPanel.AdminGroups)',
)
expect(phoneServer).toContain(
'TriggerClientEvent("sky_phone:admin:launch", player_source)',
)
expect(phoneServer).toContain('if not sessions[player_source] then')
expect(phoneClient).toContain(
'RegisterNetEvent("sky_phone:admin:launch"',
)
expect(phoneClient).toContain('requested_app_id = "admin"')
})
it('validates ownership and app policy before remote device mutation', () => {
expect(server).toContain('find_owned_device(target_source, data.imei)')
expect(server).toContain('app_metadata(data.appId)')
+1
View File
@@ -63,6 +63,7 @@ Config.Security = {
Config.AdminPanel = {
Enabled = true,
Command = "phoneadmin",
AdminGroups = { "admin", "superadmin" },
MaximumPlayers = 128,
ReadRequestsPerMinute = 60,
+8
View File
@@ -1,5 +1,13 @@
Locales["de"] = {
CommandDescription = "Öffne dein Handy.",
AdminCommand = {
CommandDescription = "Öffne das geschützte Handy-Admin-Panel.",
Errors = {
disabled = "Das Handy-Admin-Panel ist deaktiviert.",
not_authorized = "Du hast keinen Zugriff auf das Handy-Admin-Panel.",
default = "Das Handy-Admin-Panel konnte nicht geöffnet werden.",
},
},
Controls = {
OpenPhone = "Handy öffnen",
},
+8
View File
@@ -1,5 +1,13 @@
Locales["en"] = {
CommandDescription = "Open your phone.",
AdminCommand = {
CommandDescription = "Open the protected phone admin panel.",
Errors = {
disabled = "The phone admin panel is disabled.",
not_authorized = "You do not have access to the phone admin panel.",
default = "The phone admin panel could not be opened.",
},
},
Controls = {
OpenPhone = "Open phone",
},
+51
View File
@@ -8,6 +8,7 @@ local equipped_phone_number = nil
local nui_generation = 0
local live_activity_active = false
local open_home_requested = false
local requested_app_id = nil
local function get_equipped_phone_number()
if not device_payload or not device_payload.device.sim then
@@ -60,6 +61,24 @@ end
SkyPhoneClient.GetState = get_phone_state
SkyPhoneClient.GetEquippedPhoneNumber = get_authoritative_phone_number
local function open_requested_app()
if not requested_app_id then
return
end
local app_id = requested_app_id
requested_app_id = nil
local opened, error_code = SkyPhoneNavigation.Open(app_id)
if not opened then
Bridge.Debug(
"warn",
"[sky_phone] Could not open requested app '%s': %s.",
app_id,
tostring(error_code)
)
end
end
Bridge.Debug("debug", "[sky_phone] Client script initialized.", { always = true })
local locale, locale_name = SkyPhoneLocales.Resolve(Config.Bridge.Locale)
@@ -95,6 +114,7 @@ local function close_phone(close_device_session)
local was_open = is_open
open_requested = false
open_without_focus = false
requested_app_id = nil
TriggerEvent("sky_phone:animation:phone", false)
is_open = false
if was_open then
@@ -266,9 +286,27 @@ RegisterNUICallback("ui:opened", function(data, cb)
end
SkyPhoneFocus.SetPhone(true, open_without_focus)
TriggerEvent("sky_phone:animation:phone", true)
open_requested_app()
cb({ success = true })
end)
RegisterNetEvent("sky_phone:admin:launch", function()
requested_app_id = "admin"
if is_open then
open_requested_app()
end
end)
RegisterNetEvent("sky_phone:admin:command-error", function(error_code)
local messages = locale.AdminCommand.Errors
Bridge.Framework.Notify(
"iFruit",
messages[error_code] or messages.default,
"error",
5000
)
end)
RegisterNUICallback("ui:input-focus", function(data, cb)
if type(data) ~= "table" or type(data.active) ~= "boolean" then
cb({ success = false, error = "invalid_request" })
@@ -336,9 +374,11 @@ RegisterNetEvent("sky_phone:device:invalidated", function()
update_equipped_phone_number(nil)
live_activity_active = false
open_home_requested = false
requested_app_id = nil
end)
RegisterNetEvent("sky_phone:device:error", function(error_code)
requested_app_id = nil
if not is_open and not open_requested then
open_without_focus = false
end
@@ -359,6 +399,13 @@ CreateThread(function()
if Config.TestData.Enabled then
TriggerEvent("chat:addSuggestion", "/" .. Config.TestData.Command, locale.TestData.CommandDescription)
end
if Config.AdminPanel.Enabled then
TriggerEvent(
"chat:addSuggestion",
"/" .. Config.AdminPanel.Command,
locale.AdminCommand.CommandDescription
)
end
end)
AddEventHandler("onResourceStop", function(resource_name)
@@ -369,6 +416,7 @@ AddEventHandler("onResourceStop", function(resource_name)
is_open = false
open_requested = false
open_without_focus = false
requested_app_id = nil
TriggerEvent("sky_phone:animation:reset")
SkyPhoneCalls.Reset()
@@ -381,4 +429,7 @@ AddEventHandler("onResourceStop", function(resource_name)
if Config.TestData.Enabled then
TriggerEvent("chat:removeSuggestion", "/" .. Config.TestData.Command)
end
if Config.AdminPanel.Enabled then
TriggerEvent("chat:removeSuggestion", "/" .. Config.AdminPanel.Command)
end
end)
+30
View File
@@ -2,6 +2,10 @@ local phone_open_handler
local pending_phone_opens = {}
local server_started = false
if type(Config.AdminPanel.Command) ~= "string" or Config.AdminPanel.Command == "" then
error("[sky_phone] Config.AdminPanel.Command must be a non-empty command name.")
end
local function flush_pending_phone_opens()
if not server_started or not phone_open_handler then
return
@@ -915,6 +919,32 @@ end
phone_open_handler = open_phone
flush_pending_phone_opens()
RegisterCommand(Config.AdminPanel.Command, function(command_source)
local player_source = tonumber(command_source)
if not player_source or player_source < 1 then
Bridge.Debug("warn", "[sky_phone] The admin panel command can only be used by a player.")
return
end
if not Config.AdminPanel.Enabled then
TriggerClientEvent("sky_phone:admin:command-error", player_source, "disabled")
return
end
if not Bridge.Framework.HasAdminGroup(player_source, Config.AdminPanel.AdminGroups) then
Bridge.Debug(
"warn",
"[sky_phone] Rejected admin panel command from source %s.",
tostring(player_source)
)
TriggerClientEvent("sky_phone:admin:command-error", player_source, "not_authorized")
return
end
TriggerClientEvent("sky_phone:admin:launch", player_source)
if not sessions[player_source] then
open_phone(player_source, nil)
end
end, false)
function SkyPhone.OpenDeviceForCall(source, imei)
local matches = find_device_slots(source, imei)
if not matches[1] then
+16 -1
View File
@@ -23,7 +23,11 @@ Bridge = {
},
Debug = function()
end,
Framework = {},
Framework = {
HasAdminGroup = function()
return true
end,
},
Inventory = {
GetResourceName = function()
return "test_inventory"
@@ -36,6 +40,11 @@ Bridge = {
}
Config = {
AdminPanel = {
AdminGroups = { "admin" },
Command = "phoneadmin",
Enabled = true,
},
Phone = {
DevelopmentCommand = false,
DeviceName = "Test Phone",
@@ -79,6 +88,12 @@ function AddEventHandler(_, callback)
assert(type(callback) == "function")
end
function RegisterCommand(name, callback, restricted)
assert(name == "phoneadmin")
assert(type(callback) == "function")
assert(restricted == false)
end
function TriggerClientEvent()
end