Plugins base (#613)

* wip

* wip

* cleanup

* notification channels

* phpstan

* services

* remove server types

* refactoring

* refactoring
This commit is contained in:
Saeed Vaziry
2025-06-14 14:35:18 +02:00
committed by GitHub
parent adc0653d15
commit 131b828807
311 changed files with 3976 additions and 2660 deletions

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Knuckles\Scribe\Attributes\Endpoint;
use Knuckles\Scribe\Attributes\Group;
use Knuckles\Scribe\Attributes\Unauthenticated;
@ -14,7 +15,7 @@ class HealthController extends Controller
#[Get('api/health', name: 'api.health')]
#[Unauthenticated]
#[Endpoint(title: 'health-check')]
public function __invoke(): \Illuminate\Http\JsonResponse
public function __invoke(): JsonResponse
{
return response()->json([
'success' => true,

View File

@ -5,10 +5,6 @@
use App\Actions\Server\CreateServer;
use App\Actions\Server\RebootServer;
use App\Actions\Server\Update;
use App\Enums\Database;
use App\Enums\PHP;
use App\Enums\ServerProvider;
use App\Enums\Webserver;
use App\Http\Controllers\Controller;
use App\Http\Resources\ServerResource;
use App\Models\Project;
@ -45,16 +41,16 @@ public function index(Project $project): ResourceCollection
#[Post('/', name: 'api.projects.servers.create', middleware: 'ability:write')]
#[Endpoint(title: 'create', description: 'Create a new server.')]
#[BodyParam(name: 'provider', description: 'The server provider type', required: true)]
#[BodyParam(name: 'server_provider', description: 'If the provider is not custom, the ID of the server provider profile', enum: [ServerProvider::CUSTOM, ServerProvider::HETZNER, ServerProvider::DIGITALOCEAN, ServerProvider::LINODE, ServerProvider::VULTR])]
#[BodyParam(name: 'server_provider', description: 'If the provider is not custom, the ID of the server provider profile')]
#[BodyParam(name: 'region', description: 'Provider region if the provider is not custom')]
#[BodyParam(name: 'plan', description: 'Provider plan if the provider is not custom')]
#[BodyParam(name: 'ip', description: 'SSH IP address if the provider is custom')]
#[BodyParam(name: 'port', description: 'SSH Port if the provider is custom')]
#[BodyParam(name: 'name', description: 'The name of the server.', required: true)]
#[BodyParam(name: 'os', description: 'The os of the server', required: true)]
#[BodyParam(name: 'webserver', description: 'Web server', required: true, enum: [Webserver::NONE, Webserver::NGINX, Webserver::CADDY])]
#[BodyParam(name: 'database', description: 'Database', required: true, enum: [Database::NONE, Database::MYSQL57, Database::MYSQL80, Database::MARIADB103, Database::MARIADB104, Database::MARIADB103, Database::POSTGRESQL12, Database::POSTGRESQL13, Database::POSTGRESQL14, Database::POSTGRESQL15, Database::POSTGRESQL16], )]
#[BodyParam(name: 'php', description: 'PHP version', required: true, enum: [PHP::V70, PHP::V71, PHP::V72, PHP::V73, PHP::V74, PHP::V80, PHP::V81, PHP::V82, PHP::V83])]
#[BodyParam(name: 'webserver', description: 'Web server', required: true)]
#[BodyParam(name: 'database', description: 'Database', required: true)]
#[BodyParam(name: 'php', description: 'PHP version', required: true)]
#[ResponseFromApiResource(ServerResource::class, Server::class)]
public function create(Request $request, Project $project): ServerResource
{

View File

@ -9,13 +9,13 @@
use App\Actions\Site\UpdateEnv;
use App\Actions\Site\UpdateLoadBalancer;
use App\Enums\LoadBalancerMethod;
use App\Enums\SiteType;
use App\Exceptions\DeploymentScriptIsEmptyException;
use App\Http\Controllers\Controller;
use App\Http\Resources\SiteResource;
use App\Models\Project;
use App\Models\Server;
use App\Models\Site;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Knuckles\Scribe\Attributes\BodyParam;
@ -49,7 +49,7 @@ public function index(Project $project, Server $server): ResourceCollection
#[Post('/', name: 'api.projects.servers.sites.create', middleware: 'ability:write')]
#[Endpoint(title: 'create', description: 'Create a new site.')]
#[BodyParam(name: 'type', required: true, enum: [SiteType::PHP, SiteType::PHP_BLANK, SiteType::PHPMYADMIN, SiteType::LARAVEL, SiteType::WORDPRESS, SiteType::LOAD_BALANCER])]
#[BodyParam(name: 'type', required: true)]
#[BodyParam(name: 'domain', required: true)]
#[BodyParam(name: 'aliases', type: 'array')]
#[BodyParam(name: 'php_version', description: 'One of the installed PHP Versions', required: true, example: '7.4')]
@ -172,7 +172,7 @@ public function updateDeploymentScript(Request $request, Project $project, Serve
#[Get('{site}/deployment-script', name: 'api.projects.servers.sites.deployment-script.show', middleware: 'ability:read')]
#[Endpoint(title: 'deployment-script', description: 'Get site deployment script content')]
#[Response(status: 200)]
public function showDeploymentScript(Project $project, Server $server, Site $site): \Illuminate\Http\JsonResponse
public function showDeploymentScript(Project $project, Server $server, Site $site): JsonResponse
{
$this->authorize('view', [$site, $server]);
@ -190,7 +190,7 @@ public function showDeploymentScript(Project $project, Server $server, Site $sit
'env' => 'APP_NAME=Laravel\nAPP_ENV=production',
],
], status: 200)]
public function showEnv(Project $project, Server $server, Site $site): \Illuminate\Http\JsonResponse
public function showEnv(Project $project, Server $server, Site $site): JsonResponse
{
$this->authorize('view', [$site, $server]);

View File

@ -42,7 +42,7 @@ public function index(Project $project): ResourceCollection
#[Post('/', name: 'api.projects.source-controls.create', middleware: 'ability:write')]
#[Endpoint(title: 'create')]
#[BodyParam(name: 'provider', description: 'The provider', required: true, enum: [\App\Enums\SourceControl::GITLAB, \App\Enums\SourceControl::GITHUB, \App\Enums\SourceControl::BITBUCKET])]
#[BodyParam(name: 'provider', description: 'The provider', required: true)]
#[BodyParam(name: 'name', description: 'The name of the storage provider.', required: true)]
#[BodyParam(name: 'token', description: 'The token if provider requires api token')]
#[BodyParam(name: 'url', description: 'The URL if the provider is Gitlab and it is self-hosted')]
@ -61,7 +61,7 @@ public function create(Request $request, Project $project): SourceControlResourc
#[Get('{sourceControl}', name: 'api.projects.source-controls.show', middleware: 'ability:read')]
#[Endpoint(title: 'show')]
#[ResponseFromApiResource(SourceControlResource::class, SourceControl::class)]
public function show(Project $project, SourceControl $sourceControl): \App\Http\Resources\SourceControlResource
public function show(Project $project, SourceControl $sourceControl): SourceControlResource
{
$this->authorize('view', $sourceControl);
@ -79,7 +79,7 @@ public function show(Project $project, SourceControl $sourceControl): \App\Http\
#[BodyParam(name: 'password', description: 'The password if the provider is Bitbucket')]
#[BodyParam(name: 'global', description: 'Accessible in all projects', enum: [true, false])]
#[ResponseFromApiResource(SourceControlResource::class, SourceControl::class)]
public function update(Request $request, Project $project, SourceControl $sourceControl): \App\Http\Resources\SourceControlResource
public function update(Request $request, Project $project, SourceControl $sourceControl): SourceControlResource
{
$this->authorize('update', $sourceControl);

View File

@ -63,7 +63,9 @@ public function enableForceSSL(Server $server, Site $site): RedirectResponse
$site->force_ssl = true;
$site->save();
$site->webserver()->updateVHost($site);
$site->webserver()->updateVHost($site, regenerate: [
'force-ssl',
]);
return back()
->with('success', 'Force SSL enabled successfully.');
@ -76,7 +78,9 @@ public function disableForceSSL(Server $server, Site $site): RedirectResponse
$site->force_ssl = false;
$site->save();
$site->webserver()->updateVHost($site);
$site->webserver()->updateVHost($site, regenerate: [
'force-ssl',
]);
return back()
->with('success', 'Force SSL disabled successfully.');

View File

@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers;
use App\Models\Server;
use App\Models\Site;
use App\SiteFeatures\ActionInterface;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
#[Prefix('/servers/{server}/sites/{site}/features')]
#[Middleware(['auth', 'has-project'])]
class SiteFeatureController extends Controller
{
#[Get('/', name: 'site-features')]
public function index(Server $server, Site $site): Response
{
$this->authorize('view', [$site, $server]);
return Inertia::render('site-features/index', [
'features' => $site->features(),
]);
}
#[Post('/{feature}/{action}', name: 'site-features.action')]
public function action(Request $request, Server $server, Site $site, string $feature, string $action): RedirectResponse
{
$this->authorize('update', [$site, $server]);
$handler = config('site.types.'.$site->type.'.features.'.$feature.'.actions.'.$action.'.handler');
if ($handler && class_exists($handler)) {
/** @var ActionInterface $actionHandler */
$actionHandler = new $handler($site);
$actionHandler->handle($request);
}
return back();
}
}

View File

@ -2,7 +2,37 @@
namespace App\Http;
use App\Http\Middleware\Authenticate;
use App\Http\Middleware\CanSeeProjectMiddleware;
use App\Http\Middleware\EncryptCookies;
use App\Http\Middleware\HandleAppearance;
use App\Http\Middleware\HandleInertiaRequests;
use App\Http\Middleware\HasProjectMiddleware;
use App\Http\Middleware\MustBeAdminMiddleware;
use App\Http\Middleware\PreventRequestsDuringMaintenance;
use App\Http\Middleware\RedirectIfAuthenticated;
use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\TrustProxies;
use App\Http\Middleware\ValidateSignature;
use App\Http\Middleware\VerifyCsrfToken;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Auth\Middleware\EnsureEmailIsVerified;
use Illuminate\Auth\Middleware\RequirePassword;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
use Illuminate\Http\Middleware\HandleCors;
use Illuminate\Http\Middleware\SetCacheHeaders;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
use Laravel\Sanctum\Http\Middleware\CheckAbilities;
use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;
class Kernel extends HttpKernel
{
@ -15,12 +45,12 @@ class Kernel extends HttpKernel
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
TrustProxies::class,
HandleCors::class,
PreventRequestsDuringMaintenance::class,
ValidatePostSize::class,
TrimStrings::class,
ConvertEmptyStringsToNull::class,
];
/**
@ -30,20 +60,20 @@ class Kernel extends HttpKernel
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\HandleInertiaRequests::class,
\App\Http\Middleware\HandleAppearance::class,
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
HandleInertiaRequests::class,
HandleAppearance::class,
AddLinkHeadersForPreloadedAssets::class,
],
'api' => [
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
ThrottleRequests::class.':api',
SubstituteBindings::class,
],
];
@ -55,20 +85,20 @@ class Kernel extends HttpKernel
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
'has-project' => \App\Http\Middleware\HasProjectMiddleware::class,
'can-see-project' => \App\Http\Middleware\CanSeeProjectMiddleware::class,
'must-be-admin' => \App\Http\Middleware\MustBeAdminMiddleware::class,
'auth' => Authenticate::class,
'auth.basic' => AuthenticateWithBasicAuth::class,
'auth.session' => AuthenticateSession::class,
'cache.headers' => SetCacheHeaders::class,
'can' => Authorize::class,
'guest' => RedirectIfAuthenticated::class,
'password.confirm' => RequirePassword::class,
'signed' => ValidateSignature::class,
'throttle' => ThrottleRequests::class,
'verified' => EnsureEmailIsVerified::class,
'abilities' => CheckAbilities::class,
'ability' => CheckForAnyAbility::class,
'has-project' => HasProjectMiddleware::class,
'can-see-project' => CanSeeProjectMiddleware::class,
'must-be-admin' => MustBeAdminMiddleware::class,
];
}

View File

@ -65,7 +65,7 @@ public function share(Request $request): array
$sites = SiteResource::collection($server->sites);
}
$data['serverSites'] = $sites;
$data['server_sites'] = $sites;
if ($request->route('site')) {
$data['site'] = SiteResource::make($request->route('site'));
@ -82,15 +82,37 @@ public function share(Request $request): array
'projects' => $user?->allProjects()->get(),
'currentProject' => $user?->currentProject,
],
'publicKeyText' => __('servers.create.public_key_text', ['public_key' => get_public_key_content()]),
'projectServers' => $servers,
'configs' => config('core'),
'public_key_text' => __('servers.create.public_key_text', ['public_key' => get_public_key_content()]),
'project_servers' => $servers,
'configs' => [
'operating_systems' => config('core.operating_systems'),
'colors' => config('core.colors'),
'cronjob_intervals' => config('core.cronjob_intervals'),
'metrics_periods' => config('core.metrics_periods'),
'site' => [
'types' => config('site.types'),
],
'source_control' => [
'providers' => config('source-control.providers'),
],
'server_provider' => [
'providers' => config('server-provider.providers'),
],
'storage_provider' => [
'providers' => config('storage-provider.providers'),
],
'notification_channel' => [
'providers' => config('notification-channel.providers'),
],
'service' => [
'services' => config('service.services'),
],
],
'ziggy' => fn (): array => [
...(new Ziggy)->toArray(),
'location' => $request->url(),
],
'csrf_token' => csrf_token(),
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
'flash' => [
'success' => fn () => $request->session()->get('success'),
'error' => fn () => $request->session()->get('error'),

View File

@ -13,7 +13,7 @@ class RedirectIfAuthenticated
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{

View File

@ -27,8 +27,6 @@ public function toArray(Request $request): array
'local_ip' => $this->local_ip,
'port' => $this->port,
'os' => $this->os,
'type' => $this->type,
'type_data' => $this->type_data,
'provider' => $this->provider,
'provider_data' => $this->provider_data,
'public_key' => $this->public_key,

View File

@ -21,11 +21,10 @@ public function toArray(Request $request): array
'source_control_id' => $this->source_control_id,
'type' => $this->type,
'type_data' => $this->type_data,
'features' => $this->type()->supportedFeatures(),
'domain' => $this->domain,
'aliases' => $this->aliases,
'web_directory' => $this->web_directory,
'webserver' => $this->webserver()->name(),
'webserver' => $this->webserver()->id(),
'path' => $this->path,
'php_version' => $this->php_version,
'repository' => $this->repository,

View File

@ -17,6 +17,7 @@ public function toArray(Request $request): array
return [
'id' => $this->id,
'server_id' => $this->server_id,
'name' => $this->name,
'command' => $this->command,
'user' => $this->user,
'auto_start' => $this->auto_start,