mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 18:18:29 +00:00
feat(v5): add Inertia app shell with Flux health
Adds v5 routing, middleware, home page rendering, team context sharing, project model/table support, and Flux health reporting. Installs Flux in container builds with role-aware s6 services and documents runtime roles.
This commit is contained in:
1 parent
02ab4b39ef
commit
dcd325dc44
41 files changed
+1705
-108
No files matched your search
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V5;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use App\Services\Flux\FluxHealth;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, FluxHealth $fluxHealth): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $request->user();
|
||||
$currentTeam = $request->attributes->get('v5.currentTeam');
|
||||
|
||||
return Inertia::render('Home', [
|
||||
'status' => 'v5-ready',
|
||||
'flux' => $fluxHealth->check(),
|
||||
'currentTeam' => $currentTeam instanceof Team ? [
|
||||
'id' => $currentTeam->id,
|
||||
'name' => $currentTeam->name,
|
||||
'description' => $currentTeam->description,
|
||||
'role' => $currentTeam->pivot?->role ?? $user->roleInTeam($currentTeam->id),
|
||||
'personal' => $currentTeam->personal_team,
|
||||
] : null,
|
||||
'teams' => $user->teams()
|
||||
->select('teams.id', 'teams.name', 'teams.description', 'teams.personal_team')
|
||||
->orderBy('teams.name')
|
||||
->get()
|
||||
->map(fn (Team $team) => [
|
||||
'id' => $team->id,
|
||||
'name' => $team->name,
|
||||
'description' => $team->description,
|
||||
'role' => $team->pivot->role,
|
||||
'personal' => $team->personal_team,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ use App\Http\Middleware\RedirectIfAuthenticated;
|
||||
use App\Http\Middleware\TrimStrings;
|
||||
use App\Http\Middleware\TrustHosts;
|
||||
use App\Http\Middleware\TrustProxies;
|
||||
use App\Http\Middleware\V5\EnsureCurrentTeam as V5EnsureCurrentTeam;
|
||||
use App\Http\Middleware\V5\HandleInertiaRequests as V5HandleInertiaRequests;
|
||||
use App\Http\Middleware\ValidateSignature;
|
||||
use App\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
|
||||
@@ -76,6 +78,22 @@ class Kernel extends HttpKernel
|
||||
|
||||
],
|
||||
|
||||
'v5.web' => [
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
V5HandleInertiaRequests::class,
|
||||
],
|
||||
|
||||
'v5.authenticated' => [
|
||||
'auth',
|
||||
'verified',
|
||||
V5EnsureCurrentTeam::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
||||
ThrottleRequests::class.':api',
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware\V5;
|
||||
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureCurrentTeam
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
/** @var User|null $user */
|
||||
$user = $request->user();
|
||||
|
||||
if (! $user) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$currentTeam = $this->resolveCurrentTeam($user);
|
||||
|
||||
if (! $currentTeam) {
|
||||
abort(403, 'No team available for this user.');
|
||||
}
|
||||
|
||||
session(['currentTeam' => $currentTeam]);
|
||||
$request->attributes->set('v5.currentTeam', $currentTeam);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
private function resolveCurrentTeam(User $user): ?Team
|
||||
{
|
||||
$sessionTeamId = data_get(session('currentTeam'), 'id');
|
||||
|
||||
if ($sessionTeamId) {
|
||||
$sessionTeam = $user->teams()
|
||||
->select('teams.id', 'teams.name', 'teams.description', 'teams.personal_team')
|
||||
->whereKey($sessionTeamId)
|
||||
->first();
|
||||
|
||||
if ($sessionTeam) {
|
||||
return $sessionTeam;
|
||||
}
|
||||
}
|
||||
|
||||
return $user->teams()
|
||||
->select('teams.id', 'teams.name', 'teams.description', 'teams.personal_team')
|
||||
->orderBy('teams.id')
|
||||
->first();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware\V5;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Middleware;
|
||||
|
||||
class HandleInertiaRequests extends Middleware
|
||||
{
|
||||
protected $rootView = 'v5.app';
|
||||
|
||||
public function share(Request $request): array
|
||||
{
|
||||
return [
|
||||
...parent::share($request),
|
||||
'auth' => [
|
||||
'user' => $request->user() ? [
|
||||
'id' => $request->user()->id,
|
||||
'name' => $request->user()->name,
|
||||
'email' => $request->user()->email,
|
||||
] : null,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\V5;
|
||||
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Project extends V5Model
|
||||
{
|
||||
protected $table = 'v5_projects';
|
||||
|
||||
protected $fillable = [
|
||||
'team_id',
|
||||
'created_by_user_id',
|
||||
'name',
|
||||
'description',
|
||||
];
|
||||
|
||||
public function team(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Team::class);
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by_user_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\V5;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
abstract class V5Model extends Model
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -34,6 +34,11 @@ class RouteServiceProvider extends ServiceProvider
|
||||
Route::prefix('webhooks')
|
||||
->group(base_path('routes/webhooks.php'));
|
||||
|
||||
Route::middleware('v5.web')
|
||||
->prefix('v5')
|
||||
->as('v5.')
|
||||
->group(base_path('routes/v5.php'));
|
||||
|
||||
Route::middleware('web')
|
||||
->group(base_path('routes/web.php'));
|
||||
});
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Flux;
|
||||
|
||||
class FluxHealth
|
||||
{
|
||||
/**
|
||||
* @return array{available: bool, label: string, message: string, socket: string|null}
|
||||
*/
|
||||
public function check(): array
|
||||
{
|
||||
$socketPath = config('flux.unix_socket_path');
|
||||
|
||||
if (! is_string($socketPath) || $socketPath === '') {
|
||||
return $this->unavailable(null, 'Flux socket is not configured.');
|
||||
}
|
||||
|
||||
if (! file_exists($socketPath)) {
|
||||
return $this->unavailable($socketPath, 'Flux socket was not found.');
|
||||
}
|
||||
|
||||
$timeout = (float) config('flux.health_timeout_seconds', 1.0);
|
||||
$stream = @stream_socket_client("unix://{$socketPath}", $errorCode, $errorMessage, $timeout);
|
||||
|
||||
if ($stream === false) {
|
||||
return $this->unavailable($socketPath, $errorMessage ?: "Could not connect to Flux socket ({$errorCode}).");
|
||||
}
|
||||
|
||||
stream_set_timeout($stream, (int) ceil($timeout));
|
||||
|
||||
fwrite($stream, "GET /v1/health HTTP/1.1\r\nHost: flux\r\nAccept: application/json\r\nConnection: close\r\n\r\n");
|
||||
$response = stream_get_contents($stream) ?: '';
|
||||
fclose($stream);
|
||||
|
||||
if (! str_starts_with($response, 'HTTP/1.1 200') && ! str_starts_with($response, 'HTTP/1.0 200')) {
|
||||
return $this->unavailable($socketPath, 'Flux health endpoint did not return HTTP 200.');
|
||||
}
|
||||
|
||||
$body = str_contains($response, "\r\n\r\n") ? substr($response, strpos($response, "\r\n\r\n") + 4) : '';
|
||||
$payload = json_decode($body, true);
|
||||
|
||||
if (! is_array($payload) || ($payload['ok'] ?? false) !== true) {
|
||||
return $this->unavailable($socketPath, 'Flux health endpoint returned an invalid response.');
|
||||
}
|
||||
|
||||
return [
|
||||
'available' => true,
|
||||
'label' => 'Running',
|
||||
'message' => 'Flux is running.',
|
||||
'socket' => $socketPath,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{available: false, label: string, message: string, socket: string|null}
|
||||
*/
|
||||
private function unavailable(?string $socketPath, string $message): array
|
||||
{
|
||||
return [
|
||||
'available' => false,
|
||||
'label' => 'Unavailable',
|
||||
'message' => $message,
|
||||
'socket' => $socketPath,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user