This commit is contained in:
Saeed Vaziry
2025-06-04 08:08:20 +02:00
parent efacadba10
commit c3f69f3247
114 changed files with 4032 additions and 765 deletions

View File

@ -123,9 +123,19 @@ public static function rules(array $input): array
'min:1',
'max:65535',
],
'source' => [
'nullable',
'ip',
],
'mask' => [
'nullable',
'numeric',
'min:1',
'max:32',
],
];
if (! ($input['source_any'] ?? false)) {
if (isset($input['source_any']) && $input['source_any'] === false) {
$rules['source'] = ['required', 'ip'];
$rules['mask'] = ['required', 'numeric', 'min:1', 'max:32'];
}

View File

@ -7,6 +7,7 @@
use App\Models\Service;
use App\Models\Site;
use App\SSH\Services\Webserver\Webserver;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class CreateRedirect
@ -16,6 +17,8 @@ class CreateRedirect
*/
public function create(Site $site, array $input): Redirect
{
Validator::make($input, self::rules($site))->validate();
$redirect = new Redirect;
$redirect->site_id = $site->id;

View File

@ -9,6 +9,7 @@
use App\Models\Site;
use App\Models\Ssl;
use App\SSH\Services\Webserver\Webserver;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
@ -21,6 +22,8 @@ class CreateSSL
*/
public function create(Site $site, array $input): void
{
Validator::make($input, self::rules($input))->validate();
$site->ssls()
->where('type', $input['type'])
->where('status', SslStatus::FAILED)

View File

@ -0,0 +1,15 @@
<?php
namespace App\Actions\SSL;
use App\Models\Ssl;
class DeactivateSSL
{
public function deactivate(Ssl $ssl): void
{
$ssl->is_active = false;
$ssl->save();
$ssl->site->webserver()->updateVHost($ssl->site);
}
}

View File

@ -4,6 +4,7 @@
use App\Models\Command;
use App\Models\Site;
use Illuminate\Support\Facades\Validator;
class CreateCommand
{
@ -12,6 +13,8 @@ class CreateCommand
*/
public function create(Site $site, array $input): Command
{
Validator::make($input, self::rules())->validate();
$script = new Command([
'site_id' => $site->id,
'name' => $input['name'],

View File

@ -7,14 +7,20 @@
use App\Models\Site;
use App\SSH\Services\PHP\PHP;
use App\SSH\Services\Webserver\Webserver;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class DeleteSite
{
/**
* @param array<string, mixed> $input
*
* @throws SSHError
*/
public function delete(Site $site): void
public function delete(Site $site, array $input): void
{
$this->validate($site, $input);
/** @var Service $service */
$service = $site->server->webserver();
@ -35,4 +41,14 @@ public function delete(Site $site): void
$site->delete();
}
private function validate(Site $site, array $input): void
{
Validator::make($input, [
'domain' => [
'required',
Rule::in($site->domain),
],
])->validate();
}
}

View File

@ -4,7 +4,6 @@
use App\Enums\DeploymentStatus;
use App\Exceptions\DeploymentScriptIsEmptyException;
use App\Exceptions\SSHError;
use App\Facades\Notifier;
use App\Models\Deployment;
use App\Models\ServerLog;
@ -15,7 +14,6 @@ class Deploy
{
/**
* @throws DeploymentScriptIsEmptyException
* @throws SSHError
*/
public function run(Site $site): Deployment
{
@ -32,6 +30,11 @@ public function run(Site $site): Deployment
'deployment_script_id' => $site->deploymentScript->id,
'status' => DeploymentStatus::DEPLOYING,
]);
$log = ServerLog::newLog($site->server, 'deploy-'.strtotime('now'))
->forSite($site);
$log->save();
$deployment->log_id = $log->id;
$deployment->save();
$lastCommit = $site->sourceControl?->provider()?->getLastCommit($site->repository, $site->branch);
if ($lastCommit) {
$deployment->commit_id = $lastCommit['commit_id'];
@ -39,12 +42,7 @@ public function run(Site $site): Deployment
}
$deployment->save();
dispatch(function () use ($site, $deployment): void {
$log = ServerLog::newLog($site->server, 'deploy-'.strtotime('now'))
->forSite($site);
$log->save();
$deployment->log_id = $log->id;
$deployment->save();
dispatch(function () use ($site, $deployment, $log): void {
$site->server->os()->runScript(
path: $site->path,
script: $site->deploymentScript->content,

View File

@ -3,6 +3,7 @@
namespace App\Actions\Site;
use App\Models\Command;
use Illuminate\Support\Facades\Validator;
class EditCommand
{
@ -11,6 +12,8 @@ class EditCommand
*/
public function edit(Command $command, array $input): Command
{
Validator::make($input, self::rules())->validate();
$command->name = $input['name'];
$command->command = $input['command'];
$command->save();

View File

@ -7,6 +7,7 @@
use App\Models\CommandExecution;
use App\Models\ServerLog;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
class ExecuteCommand
{
@ -15,19 +16,31 @@ class ExecuteCommand
*/
public function execute(Command $command, User $user, array $input): CommandExecution
{
Validator::make($input, self::rules($command))->validate();
$variables = [];
foreach ($command->getVariables() as $variable) {
if (array_key_exists($variable, $input)) {
$variables[$variable] = $input[$variable] ?? '';
}
}
$execution = new CommandExecution([
'command_id' => $command->id,
'server_id' => $command->site->server_id,
'user_id' => $user->id,
'variables' => $input['variables'] ?? [],
'variables' => $variables,
'status' => CommandExecutionStatus::EXECUTING,
]);
$execution->save();
dispatch(function () use ($execution, $command): void {
$log = ServerLog::newLog($execution->server, 'command-'.$command->id.'-'.strtotime('now'));
$log->save();
$execution->server_log_id = $log->id;
$execution->save();
dispatch(function () use ($execution, $command, $log): void {
$content = $execution->getContent();
$log = ServerLog::newLog($execution->server, 'command-'.$command->id.'-'.strtotime('now'));
$log->save();
$execution->server_log_id = $log->id;
$execution->save();
$execution->server->os()->runScript(
@ -48,18 +61,19 @@ public function execute(Command $command, User $user, array $input): CommandExec
}
/**
* @param array<string, mixed> $input
* @return array<string, string|array<int, mixed>>
*/
public static function rules(array $input): array
public static function rules(Command $command): array
{
return [
'variables' => 'array',
'variables.*' => [
$rules = [];
foreach ($command->getVariables() as $variable) {
$rules[$variable] = [
'required',
'string',
'max:255',
],
];
];
}
return $rules;
}
}

View File

@ -5,6 +5,7 @@
use App\Exceptions\SSHError;
use App\Models\Site;
use App\SSH\Git\Git;
use Illuminate\Support\Facades\Validator;
class UpdateBranch
{
@ -15,6 +16,8 @@ class UpdateBranch
*/
public function update(Site $site, array $input): void
{
Validator::make($input, self::rules())->validate();
$site->branch = $input['branch'];
app(Git::class)->fetchOrigin($site);
app(Git::class)->checkout($site);

View File

@ -4,6 +4,7 @@
use App\Models\DeploymentScript;
use App\Models\Site;
use Illuminate\Support\Facades\Validator;
class UpdateDeploymentScript
{
@ -12,6 +13,8 @@ class UpdateDeploymentScript
*/
public function update(Site $site, array $input): void
{
Validator::make($input, self::rules())->validate();
/** @var DeploymentScript $script */
$script = $site->deploymentScript;
$script->content = $input['script'];

View File

@ -4,6 +4,7 @@
use App\Exceptions\SSHError;
use App\Models\Site;
use Illuminate\Support\Facades\Validator;
class UpdateEnv
{
@ -14,10 +15,14 @@ class UpdateEnv
*/
public function update(Site $site, array $input): void
{
$site->server->os()->editFileAs(
Validator::make($input, [
'env' => ['required', 'string'],
])->validate();
$site->server->os()->write(
$site->path.'/.env',
$site->user,
trim((string) $input['env']),
$site->user,
);
}
}

View File

@ -5,6 +5,7 @@
use App\Enums\LoadBalancerMethod;
use App\Models\LoadBalancerServer;
use App\Models\Site;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class UpdateLoadBalancer
@ -14,6 +15,8 @@ class UpdateLoadBalancer
*/
public function update(Site $site, array $input): void
{
Validator::make($input, self::rules($site))->validate();
$site->loadBalancerServers()->delete();
foreach ($input['servers'] as $server) {

View File

@ -4,10 +4,23 @@
use App\Exceptions\SSHError;
use App\Models\Site;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class UpdatePHPVersion
{
/**
* @param array<string, mixed> $input
*
* @throws SSHError
*/
public function update(Site $site, array $input): void
{
Validator::make($input, self::rules($site))->validate();
$site->changePHPVersion($input['version']);
}
/**
* @return array<string, array<string>>
*/
@ -22,14 +35,4 @@ public static function rules(Site $site): array
],
];
}
/**
* @param array<string, mixed> $input
*
* @throws SSHError
*/
public function update(Site $site, array $input): void
{
$site->changePHPVersion($input['version']);
}
}

View File

@ -6,6 +6,7 @@
use App\Exceptions\RepositoryPermissionDenied;
use App\Exceptions\SourceControlIsNotConnected;
use App\Models\Site;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
@ -18,6 +19,8 @@ class UpdateSourceControl
*/
public function update(Site $site, array $input): void
{
Validator::make($input, self::rules())->validate();
$site->source_control_id = $input['source_control'];
try {
if ($site->sourceControl) {

View File

@ -3,5 +3,20 @@
namespace App\Exceptions;
use Exception;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class DeploymentScriptIsEmptyException extends Exception {}
class DeploymentScriptIsEmptyException extends Exception
{
public function render(Request $request): RedirectResponse
{
if ($request->header('X-Inertia')) {
return back()->with('error', 'Cannot deploy an empty deployment script.');
}
throw ValidationException::withMessages([
'deployment_script' => 'Deployment script cannot be empty.',
]);
}
}

View File

@ -3,8 +3,17 @@
namespace App\Exceptions;
use Exception;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class FailedToDeployGitHook extends Exception
{
//
public function render(Request $request): ?RedirectResponse
{
if ($request->header('X-Inertia')) {
return back()->with('error', 'Failed to deploy git hook.');
}
return null;
}
}

View File

@ -3,8 +3,17 @@
namespace App\Exceptions;
use Exception;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class FailedToDestroyGitHook extends Exception
{
//
public function render(Request $request): ?RedirectResponse
{
if ($request->header('X-Inertia')) {
return back()->with('error', 'Failed to destroy git hook.');
}
return null;
}
}

View File

@ -3,5 +3,17 @@
namespace App\Exceptions;
use Exception;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class SourceControlIsNotConnected extends Exception {}
class SourceControlIsNotConnected extends Exception
{
public function render(Request $request): ?RedirectResponse
{
if ($request->header('X-Inertia')) {
return back()->with('error', 'Source control is not connected.');
}
return null;
}
}

View File

@ -0,0 +1,136 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Site\Deploy;
use App\Actions\Site\UpdateDeploymentScript;
use App\Actions\Site\UpdateEnv;
use App\Actions\Site\UpdateLoadBalancer;
use App\Exceptions\DeploymentScriptIsEmptyException;
use App\Exceptions\FailedToDestroyGitHook;
use App\Exceptions\SourceControlIsNotConnected;
use App\Exceptions\SSHError;
use App\Http\Resources\DeploymentResource;
use App\Http\Resources\LoadBalancerServerResource;
use App\Models\Server;
use App\Models\Site;
use Illuminate\Http\JsonResponse;
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;
use Spatie\RouteAttributes\Attributes\Put;
#[Prefix('/servers/{server}/sites/{site}')]
#[Middleware(['auth', 'has-project'])]
class ApplicationController extends Controller
{
#[Get('/', name: 'application')]
public function index(Server $server, Site $site): Response
{
$this->authorize('view', [$site, $server]);
return Inertia::render('application/index', [
'deployments' => DeploymentResource::collection($site->deployments()->latest()->simplePaginate(config('web.pagination_size'))),
'deploymentScript' => $site->deploymentScript?->content,
'loadBalancerServers' => LoadBalancerServerResource::collection($site->loadBalancerServers)
]);
}
#[Put('/deployment-script', name: 'application.update-deployment-script')]
public function updateDeploymentScript(Request $request, Server $server, Site $site): RedirectResponse
{
$this->authorize('update', [$site, $server]);
app(UpdateDeploymentScript::class)->update($site, $request->input());
return back()->with('success', 'Deployment script updated successfully.');
}
/**
* @throws DeploymentScriptIsEmptyException
*/
#[Post('/deploy', name: 'application.deploy')]
public function deploy(Server $server, Site $site): RedirectResponse
{
$this->authorize('update', [$site, $server]);
app(Deploy::class)->run($site);
return back()->with('info', 'Deployment started, please wait...');
}
#[Get('/env', name: 'application.env')]
public function env(Server $server, Site $site): JsonResponse
{
$this->authorize('view', [$site, $server]);
$env = $site->getEnv();
return response()->json([
'env' => $env,
]);
}
/**
* @throws SSHError
*/
#[Put('/env', name: 'application.update-env')]
public function updateEnv(Request $request, Server $server, Site $site): RedirectResponse
{
$this->authorize('update', [$site, $server]);
app(UpdateEnv::class)->update($site, $request->input());
return back()->with('success', '.env file updated successfully.');
}
/**
* @throws SourceControlIsNotConnected
*/
#[Post('/enable-auto-deployment', name: 'application.enable-auto-deployment')]
public function enableAutoDeployment(Server $server, Site $site): RedirectResponse
{
$this->authorize('update', [$site, $server]);
if (! $site->sourceControl) {
return back()->with('error', 'Cannot find source control for this site.');
}
$site->enableAutoDeployment();
return back()->with('success', 'Auto deployment enabled successfully.');
}
/**
* @throws SourceControlIsNotConnected
* @throws FailedToDestroyGitHook
*/
#[Post('/disable-auto-deployment', name: 'application.disable-auto-deployment')]
public function disableAutoDeployment(Server $server, Site $site): RedirectResponse
{
$this->authorize('update', [$site, $server]);
if (! $site->sourceControl) {
return back()->with('error', 'Cannot find source control for this site.');
}
$site->disableAutoDeployment();
return back()->with('success', 'Auto deployment disabled successfully.');
}
#[Post('/load-balancer', name: 'application.update-load-balancer')]
public function updateLoadBalancer(Request $request, Server $server, Site $site): RedirectResponse
{
$this->authorize('update', [$site, $server]);
app(UpdateLoadBalancer::class)->update($site, $request->input());
return back()->with('success', 'Load balancer updated successfully.');
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Site\CreateCommand;
use App\Actions\Site\EditCommand;
use App\Actions\Site\ExecuteCommand;
use App\Http\Resources\CommandExecutionResource;
use App\Http\Resources\CommandResource;
use App\Models\Command;
use App\Models\Server;
use App\Models\Site;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
use Spatie\RouteAttributes\Attributes\Delete;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;
use Spatie\RouteAttributes\Attributes\Put;
#[Prefix('/servers/{server}/sites/{site}/commands')]
#[Middleware(['auth', 'has-project'])]
class CommandController extends Controller
{
#[Get('/', name: 'commands')]
public function index(Server $server, Site $site): Response
{
$this->authorize('viewAny', [Command::class, $site, $server]);
return Inertia::render('commands/index', [
'commands' => CommandResource::collection($site->commands()->latest()->simplePaginate(config('web.pagination_size'))),
]);
}
#[Post('/', name: 'commands.store')]
public function store(Request $request, Server $server, Site $site): RedirectResponse
{
$this->authorize('create', [Command::class, $site, $server]);
app(CreateCommand::class)->create($site, $request->input());
return back()
->with('success', 'Command created successfully.');
}
#[Get('/{command}', name: 'commands.show')]
public function show(Server $server, Site $site, Command $command): Response
{
$this->authorize('view', [$command, $site, $server]);
return Inertia::render('commands/show', [
'command' => new CommandResource($command),
'executions' => CommandExecutionResource::collection($command->executions()->latest()->simplePaginate(config('web.pagination_size'))),
]);
}
#[Put('/{command}', name: 'commands.update')]
public function update(Request $request, Server $server, Site $site, Command $command): RedirectResponse
{
$this->authorize('update', [$command, $site, $server]);
app(EditCommand::class)->edit($command, $request->input());
return back()
->with('success', 'Command updated successfully.');
}
#[Delete('/{command}', name: 'commands.destroy')]
public function destroy(Server $server, Site $site, Command $command): RedirectResponse
{
$this->authorize('delete', [$command, $site, $server]);
$command->delete();
return back()
->with('success', 'Command deleted successfully.');
}
#[Post('/{command}/execute', name: 'commands.execute')]
public function execute(Request $request, Server $server, Site $site, Command $command): RedirectResponse
{
$this->authorize('update', [$site, $server]);
app(ExecuteCommand::class)->execute($command, user(), $request->input());
return redirect()->route('commands.show', ['server' => $server, 'site' => $site, 'command' => $command])
->with('info', 'Command is being executed.');
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Redirect\CreateRedirect;
use App\Actions\Redirect\DeleteRedirect;
use App\Http\Resources\RedirectResource;
use App\Models\Redirect;
use App\Models\Server;
use App\Models\Site;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
use Spatie\RouteAttributes\Attributes\Delete;
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}/redirects')]
#[Middleware(['auth', 'has-project'])]
class RedirectController extends Controller
{
#[Get('/', name: 'redirects')]
public function index(Server $server, Site $site): Response
{
$this->authorize('viewAny', [Redirect::class, $site, $server]);
return Inertia::render('redirects/index', [
'redirects' => RedirectResource::collection($site->redirects()->latest()->simplePaginate(config('web.pagination_size'))),
]);
}
#[Post('/', name: 'redirects.store')]
public function store(Request $request, Server $server, Site $site): RedirectResponse
{
$this->authorize('create', [Redirect::class, $site, $server]);
app(CreateRedirect::class)->create($site, $request->input());
return back()
->with('info', 'Creating the redirect');
}
#[Delete('/{redirect}', name: 'redirects.destroy')]
public function destroy(Server $server, Site $site, Redirect $redirect): RedirectResponse
{
$this->authorize('delete', [$redirect, $site, $server]);
app(DeleteRedirect::class)->delete($site, $redirect);
return back()
->with('info', 'Deleting the redirect');
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace App\Http\Controllers;
use App\Actions\SSL\ActivateSSL;
use App\Actions\SSL\CreateSSL;
use App\Actions\SSL\DeactivateSSL;
use App\Actions\SSL\DeleteSSL;
use App\Http\Resources\SslResource;
use App\Models\Server;
use App\Models\Site;
use App\Models\Ssl;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
use Spatie\RouteAttributes\Attributes\Delete;
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}/ssl')]
#[Middleware(['auth', 'has-project'])]
class SSLController extends Controller
{
#[Get('/', name: 'ssls')]
public function index(Server $server, Site $site): Response
{
$this->authorize('viewAny', [Ssl::class, $site, $server]);
return Inertia::render('ssls/index', [
'ssls' => SslResource::collection($site->ssls()->latest()->simplePaginate(config('web.pagination_size'))),
]);
}
#[Post('/', name: 'ssls.store')]
public function store(Request $request, Server $server, Site $site): RedirectResponse
{
$this->authorize('create', [Ssl::class, $site, $server]);
app(CreateSSL::class)->create($site, $request->input());
return back()
->with('info', 'Setting up SSL.');
}
#[Delete('/{ssl}', name: 'ssls.destroy')]
public function destroy(Server $server, Site $site, Ssl $ssl): RedirectResponse
{
$this->authorize('delete', [$ssl, $site, $server]);
app(DeleteSSL::class)->delete($ssl);
return back()
->with('success', 'SSL deleted successfully.');
}
#[Post('/enable-force-ssl', name: 'ssls.enable-force-ssl')]
public function enableForceSSL(Server $server, Site $site): RedirectResponse
{
$this->authorize('update', [$site, $server]);
$site->force_ssl = true;
$site->save();
$site->webserver()->updateVHost($site);
return back()
->with('success', 'Force SSL enabled successfully.');
}
#[Post('/disable-force-ssl', name: 'ssls.disable-force-ssl')]
public function disableForceSSL(Server $server, Site $site): RedirectResponse
{
$this->authorize('update', [$site, $server]);
$site->force_ssl = false;
$site->save();
$site->webserver()->updateVHost($site);
return back()
->with('success', 'Force SSL disabled successfully.');
}
#[Post('/{ssl}/activate', name: 'ssls.activate')]
public function activate(Server $server, Site $site, Ssl $ssl): RedirectResponse
{
$this->authorize('update', [$ssl, $site, $server]);
app(ActivateSSL::class)->activate($ssl);
return back()
->with('success', 'SSL activated successfully.');
}
#[Post('/{ssl}/deactivate', name: 'ssls.deactivate')]
public function deactivate(Server $server, Site $site, Ssl $ssl): RedirectResponse
{
$this->authorize('update', [$ssl, $site, $server]);
app(DeactivateSSL::class)->deactivate($ssl);
return back()
->with('success', 'SSL deactivated successfully.');
}
}

View File

@ -3,8 +3,6 @@
namespace App\Http\Controllers;
use App\Actions\Site\CreateSite;
use App\Actions\Site\DeleteSite;
use App\Exceptions\SSHError;
use App\Http\Resources\ServerLogResource;
use App\Http\Resources\SiteResource;
use App\Models\Server;
@ -14,7 +12,6 @@
use Illuminate\Support\Facades\URL;
use Inertia\Inertia;
use Inertia\Response;
use Spatie\RouteAttributes\Attributes\Delete;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post;
@ -43,17 +40,6 @@ public function server(Server $server): Response
]);
}
#[Get('/servers/{server}/sites/{site}', name: 'sites.show')]
public function show(Server $server, Site $site): Response
{
$this->authorize('view', [$site, $server]);
return Inertia::render('sites/show', [
'site' => SiteResource::make($site),
'logs' => ServerLogResource::collection($site->logs()->latest()->simplePaginate(config('web.pagination_size'), pageName: 'logsPage')),
]);
}
/**
* @throws Throwable
*/
@ -64,7 +50,7 @@ public function store(Request $request, Server $server): RedirectResponse
$site = app(CreateSite::class)->create($server, $request->all());
return redirect()->route('sites.show', ['server' => $server, 'site' => $site])
return redirect()->route('application', ['server' => $server, 'site' => $site])
->with('info', 'Installing site, please wait...');
}
@ -79,26 +65,22 @@ public function switch(Server $server, Site $site): RedirectResponse
if ($previousRoute->hasParameter('site')) {
if (count($previousRoute->parameters()) > 2) {
return redirect()->route('sites.show', ['server' => $server->id, 'site' => $site->id]);
return redirect()->route('application', ['server' => $server->id, 'site' => $site->id]);
}
return redirect()->route($previousRoute->getName(), ['server' => $server, 'site' => $site->id]);
}
return redirect()->route('sites.show', ['server' => $server->id, 'site' => $site->id]);
return redirect()->route('application', ['server' => $server->id, 'site' => $site->id]);
}
/**
* @throws SSHError
*/
#[Delete('/servers/{server}/sites/{site}', name: 'sites.destroy')]
public function destroy(Server $server, Site $site): RedirectResponse
#[Get('/servers/{server}/sites/{site}/logs', name: 'sites.logs')]
public function logs(Server $server, Site $site): Response
{
$this->authorize('delete', [$site, $server]);
$this->authorize('view', [$site, $server]);
app(DeleteSite::class)->delete($site);
return redirect()->route('sites', ['server' => $server])
->with('success', 'Site deleted successfully.');
return Inertia::render('sites/logs', [
'logs' => ServerLogResource::collection($site->logs()->latest()->simplePaginate(config('web.pagination_size'))),
]);
}
}

View File

@ -0,0 +1,110 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Site\DeleteSite;
use App\Actions\Site\UpdateBranch;
use App\Actions\Site\UpdatePHPVersion;
use App\Actions\Site\UpdateSourceControl;
use App\Exceptions\SSHError;
use App\Http\Resources\SourceControlResource;
use App\Models\Server;
use App\Models\Site;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
use Spatie\RouteAttributes\Attributes\Delete;
use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Patch;
use Spatie\RouteAttributes\Attributes\Prefix;
use Spatie\RouteAttributes\Attributes\Put;
#[Prefix('/servers/{server}/sites/{site}/settings')]
#[Middleware(['auth', 'has-project'])]
class SiteSettingController extends Controller
{
#[Get('/', name: 'site-settings')]
public function index(Server $server, Site $site): Response
{
return Inertia::render('site-settings/index', [
'sourceControl' => $site->sourceControl ? SourceControlResource::make($site->sourceControl) : null,
]);
}
/**
* @throws SSHError
*/
#[Patch('/branch', name: 'site-settings.update-branch')]
public function updateBranch(Request $request, Server $server, Site $site): RedirectResponse
{
$this->authorize('update', [$site, $server]);
app(UpdateBranch::class)->update($site, $request->input());
return back()->with('success', 'Branch updated successfully.');
}
#[Patch('/source-control', name: 'site-settings.update-source-control')]
public function updateSourceControl(Request $request, Server $server, Site $site): RedirectResponse
{
$this->authorize('update', [$site, $server]);
app(UpdateSourceControl::class)->update($site, $request->input());
return back()->with('success', 'Source control updated successfully.');
}
/**
* @throws SSHError
*/
#[Patch('/php-version', name: 'site-settings.update-php-version')]
public function updatePHPVersion(Request $request, Server $server, Site $site): RedirectResponse
{
$this->authorize('update', [$site, $server]);
app(UpdatePHPVersion::class)->update($site, $request->input());
return back()->with('success', 'PHP version updated successfully.');
}
#[Get('/vhost', name: 'site-settings.vhost')]
public function vhost(Server $server, Site $site): JsonResponse
{
$this->authorize('update', [$site, $server]);
return response()->json([
'vhost' => $site->webserver()->getVHost($site),
]);
}
#[Put('/vhost', name: 'site-settings.update-vhost')]
public function updateVhost(Request $request, Server $server, Site $site): RedirectResponse
{
$this->authorize('update', [$site, $server]);
$this->validate($request, [
'vhost' => 'required|string',
]);
$site->webserver()->updateVHost($site, $request->input('vhost'));
return back()->with('success', 'VHost updated successfully.');
}
/**
* @throws SSHError
*/
#[Delete('/', name: 'site-settings.destroy')]
public function destroy(Request $request, Server $server, Site $site): RedirectResponse
{
$this->authorize('delete', [$site, $server]);
app(DeleteSite::class)->delete($site, $request->input());
return redirect()->route('sites', ['server' => $server])
->with('success', 'Site deleted successfully.');
}
}

View File

@ -39,7 +39,7 @@ public function index(Server $server): Response
]);
}
#[Get('/sites/{site}/workers', name: 'sites.workers')]
#[Get('/sites/{site}/workers', name: 'workers.site')]
public function site(Server $server, Site $site): Response
{
$this->authorize('viewAny', [Worker::class, $server, $site]);

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Resources;
use App\Models\CommandExecution;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin CommandExecution */
class CommandExecutionResource extends JsonResource
{
/**
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'command_id' => $this->command_id,
'server_id' => $this->server_id,
'user_id' => $this->user_id,
'server_log_id' => $this->server_log_id,
'log' => ServerLogResource::make($this->serverLog),
'variables' => $this->variables,
'status' => $this->status,
'status_color' => CommandExecution::$statusColors[$this->status] ?? 'gray',
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Resources;
use App\Models\Command;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin Command */
class CommandResource extends JsonResource
{
/**
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'server_id' => $this->site->server_id,
'site_id' => $this->site_id,
'name' => $this->name,
'command' => $this->command,
'variables' => $this->getVariables(),
'updated_at' => $this->updated_at,
'created_at' => $this->created_at,
];
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Resources;
use App\Models\Deployment;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin Deployment */
class DeploymentResource extends JsonResource
{
/**
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'site_id' => $this->site_id,
'deployment_script_id' => $this->deployment_script_id,
'log_id' => $this->log_id,
'log' => new ServerLogResource($this->log),
'commit_id' => $this->commit_id,
'commit_id_short' => $this->commit_id_short,
'commit_data' => $this->commit_data,
'status' => $this->status,
'status_color' => Deployment::$statusColors[$this->status] ?? 'gray',
'updated_at' => $this->updated_at,
'created_at' => $this->created_at,
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Resources;
use App\Models\LoadBalancerServer;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin LoadBalancerServer */
class LoadBalancerServerResource extends JsonResource
{
/**
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'load_balancer_id' => $this->load_balancer_id,
'ip' => $this->ip,
'port' => $this->port,
'weight' => $this->weight,
'backup' => $this->backup,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@ -16,11 +16,13 @@ public function toArray(Request $request): array
{
return [
'id' => $this->id,
'server_id' => $this->site->server_id,
'site_id' => $this->site_id,
'mode' => $this->mode,
'from' => $this->from,
'to' => $this->to,
'mode' => $this->mode,
'status' => $this->status,
'status_color' => Redirect::$statusColors[$this->status] ?? 'gray',
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];

View File

@ -17,6 +17,7 @@ public function toArray(Request $request): array
return [
'id' => $this->id,
'project_id' => $this->project_id,
'services' => $this->services()->pluck('name', 'type'),
'user_id' => $this->user_id,
'provider_id' => $this->provider_id,
'name' => $this->name,

View File

@ -21,18 +21,22 @@ 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(),
'path' => $this->path,
'php_version' => $this->php_version,
'repository' => $this->repository,
'branch' => $this->branch,
'status' => $this->status,
'status_color' => Site::$statusColors[$this->status] ?? 'default',
'auto_deploy' => $this->isAutoDeployment(),
'port' => $this->port,
'user' => $this->user,
'url' => $this->getUrl(),
'force_ssl' => $this->force_ssl,
'progress' => $this->progress,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Resources;
use App\Models\Ssl;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin Ssl */
class SslResource extends JsonResource
{
/**
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'server_id' => $this->site->server_id,
'site_id' => $this->site_id,
'is_active' => $this->is_active,
'type' => $this->type,
'status' => $this->status,
'log' => $this->log_id ? ServerLogResource::make($this->log) : null,
'status_color' => Ssl::$statusColors[$this->status] ?? 'secondary',
'expires_at' => $this->expires_at,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

View File

@ -3,6 +3,7 @@
namespace App\Models;
use Carbon\Carbon;
use Database\Factories\CommandFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
@ -22,7 +23,7 @@
*/
class Command extends AbstractModel
{
/** @use HasFactory<\Database\Factories\CommandFactory> */
/** @use HasFactory<CommandFactory> */
use HasFactory;
protected $fillable = [

View File

@ -4,6 +4,7 @@
use App\Enums\CommandExecutionStatus;
use Carbon\Carbon;
use Database\Factories\CommandExecutionFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -18,13 +19,13 @@
* @property Carbon $created_at
* @property Carbon $updated_at
* @property Command $command
* @property ?ServerLog $serverLog
* @property ServerLog $serverLog
* @property Server $server
* @property ?User $user
*/
class CommandExecution extends AbstractModel
{
/** @use HasFactory<\Database\Factories\CommandExecutionFactory> */
/** @use HasFactory<CommandExecutionFactory> */
use HasFactory;
protected $fillable = [

View File

@ -3,6 +3,7 @@
namespace App\Models;
use App\Enums\DeploymentStatus;
use Database\Factories\DeploymentFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -20,7 +21,7 @@
*/
class Deployment extends AbstractModel
{
/** @use HasFactory<\Database\Factories\DeploymentFactory> */
/** @use HasFactory<DeploymentFactory> */
use HasFactory;
protected $fillable = [

View File

@ -2,6 +2,7 @@
namespace App\Models;
use Database\Factories\DeploymentScriptFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -13,7 +14,7 @@
*/
class DeploymentScript extends AbstractModel
{
/** @use HasFactory<\Database\Factories\DeploymentScriptFactory> */
/** @use HasFactory<DeploymentScriptFactory> */
use HasFactory;
protected static function boot(): void

View File

@ -2,6 +2,7 @@
namespace App\Models;
use Database\Factories\LoadBalancerServerFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -15,7 +16,7 @@
*/
class LoadBalancerServer extends AbstractModel
{
/** @use HasFactory<\Database\Factories\LoadBalancerServerFactory> */
/** @use HasFactory<LoadBalancerServerFactory> */
use HasFactory;
protected $fillable = [

View File

@ -4,6 +4,7 @@
use App\Enums\SslStatus;
use Carbon\Carbon;
use Database\Factories\SslFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
@ -28,7 +29,7 @@
*/
class Ssl extends AbstractModel
{
/** @use HasFactory<\Database\Factories\SslFactory> */
/** @use HasFactory<SslFactory> */
use HasFactory;
protected $fillable = [

View File

@ -2,7 +2,6 @@
namespace App\Policies;
use App\Enums\SiteFeature;
use App\Models\Command;
use App\Models\Server;
use App\Models\Site;
@ -17,7 +16,6 @@ public function viewAny(User $user, Site $site, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$server->isReady() &&
$site->hasFeature(SiteFeature::COMMANDS) &&
$site->isReady();
}
@ -27,7 +25,6 @@ public function view(User $user, Command $command, Site $site, Server $server):
$site->server_id === $server->id &&
$server->isReady() &&
$site->isReady() &&
$site->hasFeature(SiteFeature::COMMANDS) &&
$command->site_id === $site->id;
}
@ -35,7 +32,6 @@ public function create(User $user, Site $site, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$server->isReady() &&
$site->hasFeature(SiteFeature::COMMANDS) &&
$site->isReady();
}
@ -45,7 +41,6 @@ public function update(User $user, Command $command, Site $site, Server $server)
$site->server_id === $server->id &&
$server->isReady() &&
$site->isReady() &&
$site->hasFeature(SiteFeature::COMMANDS) &&
$command->site_id === $site->id;
}
@ -55,7 +50,6 @@ public function delete(User $user, Command $command, Site $site, Server $server)
$site->server_id === $server->id &&
$server->isReady() &&
$site->isReady() &&
$site->hasFeature(SiteFeature::COMMANDS) &&
$command->site_id === $site->id;
}
}

View File

@ -2,34 +2,40 @@
namespace App\Policies;
use App\Models\Redirect;
use App\Models\Server;
use App\Models\Site;
use App\Models\User;
class RedirectPolicy
{
public function view(User $user, Site $site, Server $server): bool
public function viewAny(User $user, Site $site, Server $server): bool
{
if ($user->isAdmin()) {
return true;
}
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$server->isReady() &&
$site->isReady();
}
return $site->server->project->users->contains($user);
public function view(User $user, Redirect $redirect, Site $site, Server $server): bool
{
return ($user->isAdmin() || $site->server->project->users->contains($user))
&& $site->server_id === $server->id
&& $site->server->isReady()
&& $redirect->site_id === $site->id;
}
public function create(User $user, Site $site, Server $server): bool
{
return ($user->isAdmin() || $site->server->project->users->contains($user))
&& $site->server_id === $server->id
&& $site->server->isReady()
&& $site->server->webserver();
&& $site->server->isReady();
}
public function delete(User $user, Site $site, Server $server): bool
public function delete(User $user, Redirect $redirect, Site $site, Server $server): bool
{
return ($user->isAdmin() || $site->server->project->users->contains($user))
&& $site->server_id === $server->id
&& $site->server->isReady()
&& $site->server->webserver();
&& $server->isReady()
&& $redirect->site_id === $site->id;
}
}

View File

@ -2,7 +2,6 @@
namespace App\Policies;
use App\Enums\SiteFeature;
use App\Models\Server;
use App\Models\Site;
use App\Models\Ssl;
@ -17,7 +16,6 @@ public function viewAny(User $user, Site $site, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$server->isReady() &&
$site->hasFeature(SiteFeature::SSL) &&
$site->isReady();
}
@ -27,7 +25,6 @@ public function view(User $user, Ssl $ssl, Site $site, Server $server): bool
$site->server_id === $server->id &&
$server->isReady() &&
$site->isReady() &&
$site->hasFeature(SiteFeature::SSL) &&
$ssl->site_id === $site->id;
}
@ -35,18 +32,16 @@ public function create(User $user, Site $site, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$server->isReady() &&
$site->hasFeature(SiteFeature::SSL) &&
$site->isReady();
}
public function update(User $user, Ssl $ssl, Site $site, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$site->server_id === $server->id &&
$server->isReady() &&
$site->isReady() &&
$site->hasFeature(SiteFeature::SSL) &&
$ssl->site_id === $site->id;
$site->server_id === $server->id &&
$server->isReady() &&
$site->isReady() &&
$ssl->site_id === $site->id;
}
public function delete(User $user, Ssl $ssl, Site $site, Server $server): bool
@ -55,7 +50,6 @@ public function delete(User $user, Ssl $ssl, Site $site, Server $server): bool
$site->server_id === $server->id &&
$server->isReady() &&
$site->isReady() &&
$site->hasFeature(SiteFeature::SSL) &&
$ssl->site_id === $site->id;
}
}

View File

@ -2,7 +2,6 @@
namespace App\Policies;
use App\Enums\SiteFeature;
use App\Models\Server;
use App\Models\Site;
use App\Models\User;
@ -17,70 +16,37 @@ public function viewAny(User $user, Server $server, ?Site $site = null): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$server->isReady() &&
(
! $site instanceof Site ||
(
$site->hasFeature(SiteFeature::WORKERS) &&
$site->isReady()
)
);
$server->processManager();
}
public function view(User $user, Worker $worker, Server $server, ?Site $site = null): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$server->isReady() &&
(
! $site instanceof Site ||
(
$site->server_id === $server->id &&
$site->hasFeature(SiteFeature::WORKERS) &&
$site->isReady() &&
$worker->site_id === $site->id
)
);
$worker->server_id === $server->id &&
$server->processManager();
}
public function create(User $user, Server $server, ?Site $site = null): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$server->isReady() &&
(
! $site instanceof Site ||
(
$site->hasFeature(SiteFeature::WORKERS) &&
$site->isReady()
)
);
$server->processManager();
}
public function update(User $user, Worker $worker, Server $server, ?Site $site = null): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$server->isReady() &&
(
! $site instanceof Site ||
(
$site->server_id === $server->id &&
$site->hasFeature(SiteFeature::WORKERS) &&
$site->isReady() &&
$worker->site_id === $site->id
)
);
$worker->server_id === $server->id &&
$server->processManager();
}
public function delete(User $user, Worker $worker, Server $server, ?Site $site = null): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$server->isReady() &&
(
! $site instanceof Site ||
(
$site->server_id === $server->id &&
$site->hasFeature(SiteFeature::WORKERS) &&
$site->isReady() &&
$worker->site_id === $site->id
)
);
$worker->server_id === $server->id &&
$server->processManager();
}
}

View File

@ -10,6 +10,11 @@
class Caddy extends AbstractWebserver
{
public function name(): string
{
return \App\Enums\Webserver::CADDY;
}
/**
* @throws SSHError
*/

View File

@ -10,6 +10,11 @@
class Nginx extends AbstractWebserver
{
public function name(): string
{
return \App\Enums\Webserver::NGINX;
}
/**
* @throws SSHError
*/

View File

@ -8,6 +8,8 @@
interface Webserver extends ServiceInterface
{
public function name(): string;
public function createVHost(Site $site): void;
public function updateVHost(Site $site, ?string $vhost = null): void;