feat: system tools and admin enhancements
All checks were successful
CI/CD Pipeline / test (push) Successful in 3s
CI/CD Pipeline / deploy (push) Successful in 20s

This commit is contained in:
2026-01-31 20:12:09 +01:00
parent 64244567c0
commit 9c60a8944e
31 changed files with 3088 additions and 173 deletions

View File

@@ -0,0 +1,150 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\Process\Process;
class SystemStatusController extends Controller
{
public function __invoke(Request $request): JsonResponse
{
$user = $request->user();
if (!$user || !$user->roles()->where('name', 'ROLE_ADMIN')->exists()) {
return response()->json(['message' => 'Forbidden'], 403);
}
$phpDefaultPath = $this->resolveBinary('php');
$phpSelectedPath = PHP_BINARY ?: $phpDefaultPath;
$phpSelectedOk = (bool) $phpSelectedPath;
$phpSelectedVersion = PHP_VERSION;
$minVersions = $this->resolveMinVersions();
$composerPath = $this->resolveBinary('composer');
$nodePath = $this->resolveBinary('node');
$npmPath = $this->resolveBinary('npm');
$tarPath = $this->resolveBinary('tar');
$rsyncPath = $this->resolveBinary('rsync');
$procFunctions = [
'proc_open',
'proc_get_status',
'proc_close',
];
$disabledFunctions = array_filter(array_map('trim', explode(',', (string) ini_get('disable_functions'))));
$disabledLookup = array_fill_keys($disabledFunctions, true);
$procFunctionStatus = [];
foreach ($procFunctions as $function) {
$procFunctionStatus[$function] = function_exists($function) && !isset($disabledLookup[$function]);
}
return response()->json([
'php' => PHP_VERSION,
'php_default' => $phpDefaultPath,
'php_selected_path' => $phpSelectedPath,
'php_selected_ok' => $phpSelectedOk,
'php_selected_version' => $phpSelectedVersion,
'min_versions' => $minVersions,
'composer' => $composerPath,
'composer_version' => $this->resolveBinaryVersion($composerPath, ['--version']),
'node' => $nodePath,
'node_version' => $this->resolveBinaryVersion($nodePath, ['--version']),
'npm' => $npmPath,
'npm_version' => $this->resolveBinaryVersion($npmPath, ['--version']),
'tar' => $tarPath,
'tar_version' => $this->resolveBinaryVersion($tarPath, ['--version']),
'rsync' => $rsyncPath,
'rsync_version' => $this->resolveBinaryVersion($rsyncPath, ['--version']),
'proc_functions' => $procFunctionStatus,
'storage_writable' => is_writable(storage_path()),
'updates_writable' => is_writable(storage_path('app/updates')) || @mkdir(storage_path('app/updates'), 0755, true),
]);
}
private function resolveBinary(string $name): ?string
{
$process = new Process(['sh', '-lc', "command -v {$name}"]);
$process->setTimeout(5);
$process->run();
if (!$process->isSuccessful()) {
return null;
}
$output = trim($process->getOutput());
return $output !== '' ? $output : null;
}
private function resolvePhpVersion(string $path): ?string
{
$process = new Process([$path, '-r', 'echo PHP_VERSION;']);
$process->setTimeout(5);
$process->run();
if (!$process->isSuccessful()) {
return null;
}
$output = trim($process->getOutput());
return $output !== '' ? $output : null;
}
private function resolveBinaryVersion(?string $path, array $args): ?string
{
if (!$path) {
return null;
}
$process = new Process(array_merge([$path], $args));
$process->setTimeout(5);
$process->run();
if (!$process->isSuccessful()) {
return null;
}
$output = trim($process->getOutput());
if ($output === '') {
return null;
}
$line = strtok($output, "\n") ?: $output;
if (preg_match('/(\\d+\\.\\d+(?:\\.\\d+)?)/', $line, $matches)) {
return $matches[1];
}
return null;
}
private function resolveMinVersions(): array
{
$composerJson = $this->readJson(base_path('composer.json'));
$packageJson = $this->readJson(base_path('package.json'));
$php = $composerJson['require']['php'] ?? null;
$node = $packageJson['engines']['node'] ?? null;
$npm = $packageJson['engines']['npm'] ?? null;
$composer = $composerJson['require']['composer-runtime-api'] ?? null;
return [
'php' => is_string($php) ? $php : null,
'node' => is_string($node) ? $node : null,
'npm' => is_string($npm) ? $npm : null,
'composer' => is_string($composer) ? $composer : null,
];
}
private function readJson(string $path): array
{
if (!is_file($path)) {
return [];
}
$contents = file_get_contents($path);
if ($contents === false) {
return [];
}
$data = json_decode($contents, true);
return is_array($data) ? $data : [];
}
}