Compare commits

..

No commits in common. "1.x" and "1.6.2" have entirely different histories.
1.x ... 1.6.2

230 changed files with 2177 additions and 7231 deletions

View File

@ -1,11 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Feature request
url: https://github.com/vitodeploy/vito/discussions/new?category=ideas
about: Share ideas for new features
- name: Support
url: https://github.com/vitodeploy/vito/discussions/new?category=q-a
about: Ask the community for help
- name: Discord
url: https://discord.gg/uZeeHZZnm5
about: Join the community

View File

@ -0,0 +1,12 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature
assignees: ''
---
To request a feature or suggest an idea please add it to the feedback boards
https://vitodeploy.featurebase.app/

View File

@ -34,7 +34,8 @@ ## Useful Links
- [Documentation](https://vitodeploy.com) - [Documentation](https://vitodeploy.com)
- [Install on Server](https://vitodeploy.com/introduction/installation.html#install-on-vps-recommended) - [Install on Server](https://vitodeploy.com/introduction/installation.html#install-on-vps-recommended)
- [Install via Docker](https://vitodeploy.com/introduction/installation.html#install-via-docker) - [Install via Docker](https://vitodeploy.com/introduction/installation.html#install-via-docker)
- [Roadmap](https://github.com/orgs/vitodeploy/projects/5) - [Feedbacks](https://vitodeploy.featurebase.app)
- [Roadmap](https://vitodeploy.featurebase.app/roadmap)
- [Video Demo](https://youtu.be/AbmUOBDOc28) - [Video Demo](https://youtu.be/AbmUOBDOc28)
- [Discord](https://discord.gg/uZeeHZZnm5) - [Discord](https://discord.gg/uZeeHZZnm5)
- [Contribution](/CONTRIBUTING.md) - [Contribution](/CONTRIBUTING.md)

View File

@ -22,9 +22,7 @@ public function create(Server $server, array $input): Database
'server_id' => $server->id, 'server_id' => $server->id,
'name' => $input['name'], 'name' => $input['name'],
]); ]);
/** @var \App\SSH\Services\Database\Database */ $server->database()->handler()->create($database->name);
$databaseHandler = $server->database()->handler();
$databaseHandler->create($database->name);
$database->status = DatabaseStatus::READY; $database->status = DatabaseStatus::READY;
$database->save(); $database->save();

View File

@ -102,7 +102,7 @@ private function getInterval(array $input): Expression
)->diffInHours(); )->diffInHours();
} }
if (abs($periodInHours) <= 1) { if ($periodInHours <= 1) {
return DB::raw("strftime('%Y-%m-%d %H:%M:00', created_at) as date_interval"); return DB::raw("strftime('%Y-%m-%d %H:%M:00', created_at) as date_interval");
} }

View File

@ -19,7 +19,6 @@ public function add(User $user, array $input): void
'user_id' => $user->id, 'user_id' => $user->id,
'provider' => $input['provider'], 'provider' => $input['provider'],
'label' => $input['label'], 'label' => $input['label'],
'project_id' => isset($input['global']) && $input['global'] ? null : $user->current_project_id,
]); ]);
$this->validateType($channel, $input); $this->validateType($channel, $input);
$channel->data = $channel->provider()->createData($input); $channel->data = $channel->provider()->createData($input);

View File

@ -1,34 +0,0 @@
<?php
namespace App\Actions\NotificationChannels;
use App\Models\NotificationChannel;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class EditChannel
{
public function edit(NotificationChannel $notificationChannel, User $user, array $input): void
{
$this->validate($input);
$notificationChannel->label = $input['label'];
$notificationChannel->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
$notificationChannel->save();
}
/**
* @throws ValidationException
*/
private function validate(array $input): void
{
$rules = [
'label' => [
'required',
],
];
Validator::make($input, $rules)->validate();
}
}

View File

@ -2,11 +2,8 @@
namespace App\Actions\PHP; namespace App\Actions\PHP;
use App\Enums\PHPIniType;
use App\Models\Server; use App\Models\Server;
use App\SSH\Services\PHP\PHP; use App\SSH\Services\PHP\PHP;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
class GetPHPIni class GetPHPIni
@ -21,7 +18,7 @@ public function getIni(Server $server, array $input): string
/** @var PHP $handler */ /** @var PHP $handler */
$handler = $php->handler(); $handler = $php->handler();
return $handler->getPHPIni($input['type']); return $handler->getPHPIni();
} catch (\Throwable $e) { } catch (\Throwable $e) {
throw ValidationException::withMessages( throw ValidationException::withMessages(
['ini' => $e->getMessage()] ['ini' => $e->getMessage()]
@ -31,13 +28,6 @@ public function getIni(Server $server, array $input): string
public function validate(Server $server, array $input): void public function validate(Server $server, array $input): void
{ {
Validator::make($input, [
'type' => [
'required',
Rule::in([PHPIniType::CLI, PHPIniType::FPM]),
],
])->validate();
if (! isset($input['version']) || ! in_array($input['version'], $server->installedPHPVersions())) { if (! isset($input['version']) || ! in_array($input['version'], $server->installedPHPVersions())) {
throw ValidationException::withMessages( throw ValidationException::withMessages(
['version' => __('This version is not installed')] ['version' => __('This version is not installed')]

View File

@ -2,13 +2,10 @@
namespace App\Actions\PHP; namespace App\Actions\PHP;
use App\Enums\PHPIniType;
use App\Models\Server; use App\Models\Server;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Throwable; use Throwable;
@ -25,19 +22,19 @@ public function update(Server $server, array $input): void
$tmpName = Str::random(10).strtotime('now'); $tmpName = Str::random(10).strtotime('now');
try { try {
/** @var FilesystemAdapter $storageDisk */ /** @var \Illuminate\Filesystem\FilesystemAdapter $storageDisk */
$storageDisk = Storage::disk('local'); $storageDisk = Storage::disk('local');
$storageDisk->put($tmpName, $input['ini']); $storageDisk->put($tmpName, $input['ini']);
$service->server->ssh('root')->upload( $service->server->ssh('root')->upload(
$storageDisk->path($tmpName), $storageDisk->path($tmpName),
sprintf('/etc/php/%s/%s/php.ini', $service->version, $input['type']) "/etc/php/$service->version/cli/php.ini"
); );
$this->deleteTempFile($tmpName); $this->deleteTempFile($tmpName);
} catch (Throwable) { } catch (Throwable) {
$this->deleteTempFile($tmpName); $this->deleteTempFile($tmpName);
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'ini' => __("Couldn't update php.ini (:type) file!", ['type' => $input['type']]), 'ini' => __("Couldn't update php.ini file!"),
]); ]);
} }
@ -59,10 +56,6 @@ public function validate(Server $server, array $input): void
'string', 'string',
], ],
'version' => 'required|string', 'version' => 'required|string',
'type' => [
'required',
Rule::in([PHPIniType::CLI, PHPIniType::FPM]),
],
])->validate(); ])->validate();
if (! in_array($input['version'], $server->installedPHPVersions())) { if (! in_array($input['version'], $server->installedPHPVersions())) {

View File

@ -1,32 +0,0 @@
<?php
namespace App\Actions\Script;
use App\Models\Script;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
class CreateScript
{
public function create(User $user, array $input): Script
{
$this->validate($input);
$script = new Script([
'user_id' => $user->id,
'name' => $input['name'],
'content' => $input['content'],
]);
$script->save();
return $script;
}
private function validate(array $input): void
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
])->validate();
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace App\Actions\Script;
use App\Models\Script;
use Illuminate\Support\Facades\Validator;
class EditScript
{
public function edit(Script $script, array $input): Script
{
$this->validate($input);
$script->name = $input['name'];
$script->content = $input['content'];
$script->save();
return $script;
}
private function validate(array $input): void
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
])->validate();
}
}

View File

@ -1,62 +0,0 @@
<?php
namespace App\Actions\Script;
use App\Enums\ScriptExecutionStatus;
use App\Models\Script;
use App\Models\ScriptExecution;
use App\Models\Server;
use App\Models\ServerLog;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class ExecuteScript
{
public function execute(Script $script, Server $server, array $input): ScriptExecution
{
$this->validate($server, $input);
$execution = new ScriptExecution([
'script_id' => $script->id,
'user' => $input['user'],
'variables' => $input['variables'] ?? [],
'status' => ScriptExecutionStatus::EXECUTING,
]);
$execution->save();
dispatch(function () use ($execution, $server, $script) {
$content = $execution->getContent();
$log = ServerLog::make($server, 'script-'.$script->id.'-'.strtotime('now'));
$log->save();
$execution->server_log_id = $log->id;
$execution->save();
$server->os()->runScript('~/', $content, $log, $execution->user);
$execution->status = ScriptExecutionStatus::COMPLETED;
$execution->save();
})->catch(function () use ($execution) {
$execution->status = ScriptExecutionStatus::FAILED;
$execution->save();
})->onConnection('ssh');
return $execution;
}
private function validate(Server $server, array $input): void
{
Validator::make($input, [
'user' => [
'required',
Rule::in([
'root',
$server->ssh_user,
]),
],
'variables' => 'array',
'variables.*' => [
'required',
'string',
'max:255',
],
])->validate();
}
}

View File

@ -38,7 +38,6 @@ public function create(User $user, array $input): ServerProvider
$serverProvider->profile = $input['name']; $serverProvider->profile = $input['name'];
$serverProvider->provider = $input['provider']; $serverProvider->provider = $input['provider'];
$serverProvider->credentials = $provider->credentialData($input); $serverProvider->credentials = $provider->credentialData($input);
$serverProvider->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
$serverProvider->save(); $serverProvider->save();
return $serverProvider; return $serverProvider;

View File

@ -1,34 +0,0 @@
<?php
namespace App\Actions\ServerProvider;
use App\Models\ServerProvider;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class EditServerProvider
{
public function edit(ServerProvider $serverProvider, User $user, array $input): void
{
$this->validate($input);
$serverProvider->profile = $input['name'];
$serverProvider->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
$serverProvider->save();
}
/**
* @throws ValidationException
*/
private function validate(array $input): void
{
$rules = [
'name' => [
'required',
],
];
Validator::make($input, $rules)->validate();
}
}

View File

@ -44,12 +44,7 @@ public function run(Site $site): Deployment
$log->save(); $log->save();
$deployment->log_id = $log->id; $deployment->log_id = $log->id;
$deployment->save(); $deployment->save();
$site->server->os()->runScript( $site->server->os()->runScript($site->path, $site->deploymentScript->content, $log);
path: $site->path,
script: $site->deploymentScript->content,
serverLog: $log,
variables: $site->environmentVariables($deployment)
);
$deployment->status = DeploymentStatus::FINISHED; $deployment->status = DeploymentStatus::FINISHED;
$deployment->save(); $deployment->save();
})->catch(function () use ($deployment) { })->catch(function () use ($deployment) {

View File

@ -2,14 +2,10 @@
namespace App\Actions\Site; namespace App\Actions\Site;
use App\Exceptions\SSHUploadFailed;
use App\Models\Site; use App\Models\Site;
class UpdateEnv class UpdateEnv
{ {
/**
* @throws SSHUploadFailed
*/
public function update(Site $site, array $input): void public function update(Site $site, array $input): void
{ {
$site->server->os()->editFile( $site->server->os()->editFile(

View File

@ -14,7 +14,7 @@ public function edit(SourceControl $sourceControl, User $user, array $input): vo
$this->validate($input); $this->validate($input);
$sourceControl->profile = $input['name']; $sourceControl->profile = $input['name'];
$sourceControl->url = $input['url'] ?? null; $sourceControl->url = isset($input['url']) ? $input['url'] : null;
$sourceControl->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id; $sourceControl->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
$this->validateProvider($sourceControl, $input); $this->validateProvider($sourceControl, $input);

View File

@ -21,7 +21,6 @@ public function create(User $user, array $input): void
'user_id' => $user->id, 'user_id' => $user->id,
'provider' => $input['provider'], 'provider' => $input['provider'],
'profile' => $input['name'], 'profile' => $input['name'],
'project_id' => isset($input['global']) && $input['global'] ? null : $user->current_project_id,
]); ]);
$this->validateProvider($input, $storageProvider->provider()->validationRules()); $this->validateProvider($input, $storageProvider->provider()->validationRules());

View File

@ -1,34 +0,0 @@
<?php
namespace App\Actions\StorageProvider;
use App\Models\StorageProvider;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class EditStorageProvider
{
public function edit(StorageProvider $storageProvider, User $user, array $input): void
{
$this->validate($input);
$storageProvider->profile = $input['name'];
$storageProvider->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
$storageProvider->save();
}
/**
* @throws ValidationException
*/
private function validate(array $input): void
{
$rules = [
'name' => [
'required',
],
];
Validator::make($input, $rules)->validate();
}
}

View File

@ -1,58 +0,0 @@
<?php
namespace App\Actions\Tag;
use App\Models\Server;
use App\Models\Site;
use App\Models\Tag;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class AttachTag
{
public function attach(User $user, array $input): Tag
{
$this->validate($input);
/** @var Server|Site $taggable */
$taggable = $input['taggable_type']::findOrFail($input['taggable_id']);
$tag = Tag::query()->where('name', $input['name'])->first();
if ($tag) {
if (! $taggable->tags->contains($tag->id)) {
$taggable->tags()->attach($tag->id);
}
return $tag;
}
$tag = new Tag([
'project_id' => $user->currentProject->id,
'name' => $input['name'],
'color' => config('core.tag_colors')[array_rand(config('core.tag_colors'))],
]);
$tag->save();
$taggable->tags()->attach($tag->id);
return $tag;
}
private function validate(array $input): void
{
Validator::make($input, [
'name' => [
'required',
],
'taggable_id' => [
'required',
'integer',
],
'taggable_type' => [
'required',
Rule::in(config('core.taggable_types')),
],
])->validate();
}
}

View File

@ -1,49 +0,0 @@
<?php
namespace App\Actions\Tag;
use App\Models\Tag;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class CreateTag
{
public function create(User $user, array $input): Tag
{
$this->validate($input);
$tag = Tag::query()
->where('project_id', $user->current_project_id)
->where('name', $input['name'])
->first();
if ($tag) {
throw ValidationException::withMessages([
'name' => ['Tag with this name already exists.'],
]);
}
$tag = new Tag([
'project_id' => $user->currentProject->id,
'name' => $input['name'],
'color' => $input['color'],
]);
$tag->save();
return $tag;
}
private function validate(array $input): void
{
Validator::make($input, [
'name' => [
'required',
],
'color' => [
'required',
Rule::in(config('core.tag_colors')),
],
])->validate();
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace App\Actions\Tag;
use App\Models\Tag;
use Illuminate\Support\Facades\DB;
class DeleteTag
{
public function delete(Tag $tag): void
{
DB::table('taggables')->where('tag_id', $tag->id)->delete();
$tag->delete();
}
}

View File

@ -1,36 +0,0 @@
<?php
namespace App\Actions\Tag;
use App\Models\Server;
use App\Models\Site;
use App\Models\Tag;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class DetachTag
{
public function detach(Tag $tag, array $input): void
{
$this->validate($input);
/** @var Server|Site $taggable */
$taggable = $input['taggable_type']::findOrFail($input['taggable_id']);
$taggable->tags()->detach($tag->id);
}
private function validate(array $input): void
{
Validator::make($input, [
'taggable_id' => [
'required',
'integer',
],
'taggable_type' => [
'required',
Rule::in(config('core.taggable_types')),
],
])->validate();
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Actions\Tag;
use App\Models\Tag;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class EditTag
{
public function edit(Tag $tag, array $input): void
{
$this->validate($input);
$tag->name = $input['name'];
$tag->color = $input['color'];
$tag->save();
}
/**
* @throws ValidationException
*/
private function validate(array $input): void
{
$rules = [
'name' => [
'required',
],
'color' => [
'required',
Rule::in(config('core.tag_colors')),
],
];
Validator::make($input, $rules)->validate();
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace App\Enums;
final class PHPIniType
{
const CLI = 'cli';
const FPM = 'fpm';
}

View File

@ -1,12 +0,0 @@
<?php
namespace App\Enums;
final class ScriptExecutionStatus
{
const EXECUTING = 'executing';
const COMPLETED = 'completed';
const FAILED = 'failed';
}

View File

@ -9,8 +9,4 @@ final class StorageProvider
const FTP = 'ftp'; const FTP = 'ftp';
const LOCAL = 'local'; const LOCAL = 'local';
const S3 = 's3';
const WASABI = 'wasabi';
} }

View File

@ -4,4 +4,6 @@
use Exception; use Exception;
class DeploymentScriptIsEmptyException extends Exception {} class DeploymentScriptIsEmptyException extends Exception
{
}

View File

@ -1,8 +0,0 @@
<?php
namespace App\Exceptions;
class SSHUploadFailed extends SSHError
{
//
}

View File

@ -4,4 +4,6 @@
use Exception; use Exception;
class SourceControlIsNotConnected extends Exception {} class SourceControlIsNotConnected extends Exception
{
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Facades;
use App\Support\Testing\FTPFake;
use FTP\Connection;
use Illuminate\Support\Facades\Facade;
/**
* @method static bool|Connection connect(string $host, string $port, bool $ssl = false)
* @method static bool login(string $username, string $password, bool|Connection $connection)
* @method static void close(bool|Connection $connection)
* @method static bool passive(bool|Connection $connection, bool $passive)
* @method static bool delete(bool|Connection $connection, string $path)
* @method static void assertConnected(string $host)
*/
class FTP extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'ftp';
}
public static function fake(): FTPFake
{
static::swap($fake = new FTPFake());
return $fake;
}
}

View File

@ -16,8 +16,6 @@
* @method static string exec(string $command, string $log = '', int $siteId = null, ?bool $stream = false) * @method static string exec(string $command, string $log = '', int $siteId = null, ?bool $stream = false)
* @method static string assertExecuted(array|string $commands) * @method static string assertExecuted(array|string $commands)
* @method static string assertExecutedContains(string $command) * @method static string assertExecutedContains(string $command)
* @method static string assertFileUploaded(string $toPath, ?string $content = null)
* @method static string getUploadedLocalPath()
* @method static disconnect() * @method static disconnect()
*/ */
class SSH extends FacadeAlias class SSH extends FacadeAlias

View File

@ -1,37 +0,0 @@
<?php
namespace App\Helpers;
use FTP\Connection;
class FTP
{
public function connect(string $host, string $port, bool $ssl = false): bool|Connection
{
if ($ssl) {
return ftp_ssl_connect($host, $port, 5);
}
return ftp_connect($host, $port, 5);
}
public function login(string $username, string $password, bool|Connection $connection): bool
{
return ftp_login($connection, $username, $password);
}
public function close(bool|Connection $connection): void
{
ftp_close($connection);
}
public function passive(bool|Connection $connection, bool $passive): bool
{
return ftp_pasv($connection, $passive);
}
public function delete(bool|Connection $connection, string $path): bool
{
return ftp_delete($connection, $path);
}
}

View File

@ -10,7 +10,6 @@
use App\Exceptions\RepositoryNotFound; use App\Exceptions\RepositoryNotFound;
use App\Exceptions\RepositoryPermissionDenied; use App\Exceptions\RepositoryPermissionDenied;
use App\Exceptions\SourceControlIsNotConnected; use App\Exceptions\SourceControlIsNotConnected;
use App\Exceptions\SSHUploadFailed;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse; use App\Helpers\HtmxResponse;
use App\Models\Deployment; use App\Models\Deployment;
@ -82,12 +81,9 @@ public function updateEnv(Server $server, Site $site, Request $request): Redirec
{ {
$this->authorize('manage', $server); $this->authorize('manage', $server);
try {
app(UpdateEnv::class)->update($site, $request->input()); app(UpdateEnv::class)->update($site, $request->input());
Toast::success('Env updated!'); Toast::success('Env updated!');
} catch (SSHUploadFailed) {
Toast::error('Failed to update .env file!');
}
return back(); return back();
} }

View File

@ -81,7 +81,7 @@ public function updateIni(Server $server, Request $request): RedirectResponse
app(UpdatePHPIni::class)->update($server, $request->input()); app(UpdatePHPIni::class)->update($server, $request->input());
Toast::success(__('PHP ini (:type) updated!', ['type' => $request->input('type')])); Toast::success('PHP ini updated!');
return back()->with([ return back()->with([
'ini' => $request->input('ini'), 'ini' => $request->input('ini'),

View File

@ -29,7 +29,7 @@ public function store(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server); $this->authorize('manage', $server);
/** @var SshKey $key */ /** @var \App\Models\SshKey $key */
$key = app(CreateSshKey::class)->create( $key = app(CreateSshKey::class)->create(
$request->user(), $request->user(),
$request->input() $request->input()

View File

@ -1,111 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Script\CreateScript;
use App\Actions\Script\EditScript;
use App\Actions\Script\ExecuteScript;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Models\Script;
use App\Models\ScriptExecution;
use App\Models\Server;
use App\Models\User;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class ScriptController extends Controller
{
public function index(Request $request): View
{
$this->authorize('viewAny', Script::class);
/** @var User $user */
$user = auth()->user();
$data = [
'scripts' => $user->scripts,
];
if ($request->has('edit')) {
$data['editScript'] = $user->scripts()->findOrFail($request->input('edit'));
}
if ($request->has('execute')) {
$data['executeScript'] = $user->scripts()->findOrFail($request->input('execute'));
}
return view('scripts.index', $data);
}
public function show(Script $script): View
{
$this->authorize('view', $script);
return view('scripts.show', [
'script' => $script,
'executions' => $script->executions()->latest()->paginate(20),
]);
}
public function store(Request $request): HtmxResponse
{
$this->authorize('create', Script::class);
/** @var User $user */
$user = auth()->user();
app(CreateScript::class)->create($user, $request->input());
Toast::success('Script created.');
return htmx()->redirect(route('scripts.index'));
}
public function edit(Request $request, Script $script): HtmxResponse
{
$this->authorize('update', $script);
app(EditScript::class)->edit($script, $request->input());
Toast::success('Script updated.');
return htmx()->redirect(route('scripts.index'));
}
public function execute(Script $script, Request $request): HtmxResponse
{
$this->validate($request, [
'server' => 'required|exists:servers,id',
]);
$server = Server::findOrFail($request->input('server'));
$this->authorize('execute', [$script, $server]);
app(ExecuteScript::class)->execute($script, $server, $request->input());
Toast::success('Executing the script...');
return htmx()->redirect(route('scripts.show', $script));
}
public function delete(Script $script): RedirectResponse
{
$this->authorize('delete', $script);
$script->delete();
Toast::success('Script deleted.');
return redirect()->route('scripts.index');
}
public function log(Script $script, ScriptExecution $execution): RedirectResponse
{
$this->authorize('view', $script);
return back()->with('content', $execution->serverLog?->getContent());
}
}

View File

@ -35,9 +35,7 @@ public function create(Request $request): View
$this->authorize('create', [Server::class, $user->currentProject]); $this->authorize('create', [Server::class, $user->currentProject]);
$provider = $request->query('provider', old('provider', \App\Enums\ServerProvider::CUSTOM)); $provider = $request->query('provider', old('provider', \App\Enums\ServerProvider::CUSTOM));
$serverProviders = ServerProvider::getByProjectId(auth()->user()->current_project_id) $serverProviders = ServerProvider::query()->where('provider', $provider)->get();
->where('provider', $provider)
->get();
return view('servers.create', [ return view('servers.create', [
'serverProviders' => $serverProviders, 'serverProviders' => $serverProviders,

View File

@ -3,7 +3,6 @@
namespace App\Http\Controllers\Settings; namespace App\Http\Controllers\Settings;
use App\Actions\NotificationChannels\AddChannel; use App\Actions\NotificationChannels\AddChannel;
use App\Actions\NotificationChannels\EditChannel;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse; use App\Helpers\HtmxResponse;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
@ -14,17 +13,11 @@
class NotificationChannelController extends Controller class NotificationChannelController extends Controller
{ {
public function index(Request $request): View public function index(): View
{ {
$data = [ return view('settings.notification-channels.index', [
'channels' => NotificationChannel::getByProjectId(auth()->user()->current_project_id)->get(), 'channels' => NotificationChannel::query()->latest()->get(),
]; ]);
if ($request->has('edit')) {
$data['editChannel'] = NotificationChannel::find($request->input('edit'));
}
return view('settings.notification-channels.index', $data);
} }
public function add(Request $request): HtmxResponse public function add(Request $request): HtmxResponse
@ -39,19 +32,6 @@ public function add(Request $request): HtmxResponse
return htmx()->redirect(route('settings.notification-channels')); return htmx()->redirect(route('settings.notification-channels'));
} }
public function update(NotificationChannel $notificationChannel, Request $request): HtmxResponse
{
app(EditChannel::class)->edit(
$notificationChannel,
$request->user(),
$request->input(),
);
Toast::success('Channel updated.');
return htmx()->redirect(route('settings.notification-channels'));
}
public function delete(int $id): RedirectResponse public function delete(int $id): RedirectResponse
{ {
$channel = NotificationChannel::query()->findOrFail($id); $channel = NotificationChannel::query()->findOrFail($id);

View File

@ -68,7 +68,7 @@ public function delete(Project $project): RedirectResponse
return back(); return back();
} }
public function switch(Request $request, $projectId): RedirectResponse public function switch($projectId): RedirectResponse
{ {
/** @var User $user */ /** @var User $user */
$user = auth()->user(); $user = auth()->user();
@ -81,11 +81,6 @@ public function switch(Request $request, $projectId): RedirectResponse
$user->current_project_id = $project->id; $user->current_project_id = $project->id;
$user->save(); $user->save();
// check if the referer is settings/*
if (str_contains($request->headers->get('referer'), 'settings')) {
return redirect()->to($request->headers->get('referer'));
}
return redirect()->route('servers'); return redirect()->route('servers');
} }
} }

View File

@ -4,7 +4,6 @@
use App\Actions\ServerProvider\CreateServerProvider; use App\Actions\ServerProvider\CreateServerProvider;
use App\Actions\ServerProvider\DeleteServerProvider; use App\Actions\ServerProvider\DeleteServerProvider;
use App\Actions\ServerProvider\EditServerProvider;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse; use App\Helpers\HtmxResponse;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
@ -15,17 +14,11 @@
class ServerProviderController extends Controller class ServerProviderController extends Controller
{ {
public function index(Request $request): View public function index(): View
{ {
$data = [ return view('settings.server-providers.index', [
'providers' => ServerProvider::getByProjectId(auth()->user()->current_project_id)->get(), 'providers' => auth()->user()->serverProviders,
]; ]);
if ($request->has('edit')) {
$data['editProvider'] = ServerProvider::find($request->input('edit'));
}
return view('settings.server-providers.index', $data);
} }
public function connect(Request $request): HtmxResponse public function connect(Request $request): HtmxResponse
@ -40,19 +33,6 @@ public function connect(Request $request): HtmxResponse
return htmx()->redirect(route('settings.server-providers')); return htmx()->redirect(route('settings.server-providers'));
} }
public function update(ServerProvider $serverProvider, Request $request): HtmxResponse
{
app(EditServerProvider::class)->edit(
$serverProvider,
$request->user(),
$request->input(),
);
Toast::success('Provider updated.');
return htmx()->redirect(route('settings.server-providers'));
}
public function delete(ServerProvider $serverProvider): RedirectResponse public function delete(ServerProvider $serverProvider): RedirectResponse
{ {
try { try {

View File

@ -18,7 +18,7 @@ class SourceControlController extends Controller
public function index(Request $request): View public function index(Request $request): View
{ {
$data = [ $data = [
'sourceControls' => SourceControl::getByProjectId(auth()->user()->current_project_id)->get(), 'sourceControls' => SourceControl::getByCurrentProject(),
]; ];
if ($request->has('edit')) { if ($request->has('edit')) {

View File

@ -4,7 +4,6 @@
use App\Actions\StorageProvider\CreateStorageProvider; use App\Actions\StorageProvider\CreateStorageProvider;
use App\Actions\StorageProvider\DeleteStorageProvider; use App\Actions\StorageProvider\DeleteStorageProvider;
use App\Actions\StorageProvider\EditStorageProvider;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse; use App\Helpers\HtmxResponse;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
@ -15,17 +14,11 @@
class StorageProviderController extends Controller class StorageProviderController extends Controller
{ {
public function index(Request $request): View public function index(): View
{ {
$data = [ return view('settings.storage-providers.index', [
'providers' => StorageProvider::getByProjectId(auth()->user()->current_project_id)->get(), 'providers' => auth()->user()->storageProviders,
]; ]);
if ($request->has('edit')) {
$data['editProvider'] = StorageProvider::find($request->input('edit'));
}
return view('settings.storage-providers.index', $data);
} }
public function connect(Request $request): HtmxResponse public function connect(Request $request): HtmxResponse
@ -40,19 +33,6 @@ public function connect(Request $request): HtmxResponse
return htmx()->redirect(route('settings.storage-providers')); return htmx()->redirect(route('settings.storage-providers'));
} }
public function update(StorageProvider $storageProvider, Request $request): HtmxResponse
{
app(EditStorageProvider::class)->edit(
$storageProvider,
$request->user(),
$request->input(),
);
Toast::success('Provider updated.');
return htmx()->redirect(route('settings.storage-providers'));
}
public function delete(StorageProvider $storageProvider): RedirectResponse public function delete(StorageProvider $storageProvider): RedirectResponse
{ {
try { try {

View File

@ -1,90 +0,0 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Actions\Tag\AttachTag;
use App\Actions\Tag\CreateTag;
use App\Actions\Tag\DeleteTag;
use App\Actions\Tag\DetachTag;
use App\Actions\Tag\EditTag;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Http\Controllers\Controller;
use App\Models\Tag;
use App\Models\User;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class TagController extends Controller
{
public function index(Request $request): View
{
$data = [
'tags' => Tag::getByProjectId(auth()->user()->current_project_id)->get(),
];
if ($request->has('edit')) {
$data['editTag'] = Tag::find($request->input('edit'));
}
return view('settings.tags.index', $data);
}
public function create(Request $request): HtmxResponse
{
/** @var User $user */
$user = $request->user();
app(CreateTag::class)->create(
$user,
$request->input(),
);
Toast::success('Tag created.');
return htmx()->redirect(route('settings.tags'));
}
public function update(Tag $tag, Request $request): HtmxResponse
{
app(EditTag::class)->edit(
$tag,
$request->input(),
);
Toast::success('Tag updated.');
return htmx()->redirect(route('settings.tags'));
}
public function attach(Request $request): RedirectResponse
{
/** @var User $user */
$user = $request->user();
app(AttachTag::class)->attach($user, $request->input());
return back()->with([
'status' => 'tag-created',
]);
}
public function detach(Request $request, Tag $tag): RedirectResponse
{
app(DetachTag::class)->detach($tag, $request->input());
return back()->with([
'status' => 'tag-detached',
]);
}
public function delete(Tag $tag): RedirectResponse
{
app(DeleteTag::class)->delete($tag);
Toast::success('Tag deleted.');
return back();
}
}

View File

@ -3,9 +3,7 @@
namespace App\Models; namespace App\Models;
use App\Notifications\NotificationInterface; use App\Notifications\NotificationInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
/** /**
@ -14,7 +12,6 @@
* @property array data * @property array data
* @property string label * @property string label
* @property bool connected * @property bool connected
* @property int $project_id
*/ */
class NotificationChannel extends AbstractModel class NotificationChannel extends AbstractModel
{ {
@ -27,7 +24,6 @@ class NotificationChannel extends AbstractModel
'data', 'data',
'connected', 'connected',
'is_default', 'is_default',
'project_id',
]; ];
protected $casts = [ protected $casts = [
@ -51,16 +47,4 @@ public static function notifyAll(NotificationInterface $notification): void
$channel->notify($notification); $channel->notify($notification);
} }
} }
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public static function getByProjectId(int $projectId): Builder
{
return self::query()
->where('project_id', $projectId)
->orWhereNull('project_id');
}
} }

View File

@ -65,9 +65,4 @@ public function sourceControls(): HasMany
{ {
return $this->hasMany(SourceControl::class); return $this->hasMany(SourceControl::class);
} }
public function tags(): HasMany
{
return $this->hasMany(Tag::class);
}
} }

View File

@ -1,66 +0,0 @@
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Collection;
/**
* @property int $id
* @property int $user_id
* @property string $name
* @property string $content
* @property Carbon $created_at
* @property Carbon $updated_at
* @property Collection<ScriptExecution> $executions
* @property ?ScriptExecution $lastExecution
*/
class Script extends AbstractModel
{
use HasFactory;
protected $fillable = [
'user_id',
'name',
'content',
];
public static function boot(): void
{
parent::boot();
static::deleting(function (Script $script) {
$script->executions()->delete();
});
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function getVariables(): array
{
$variables = [];
preg_match_all('/\${(.*?)}/', $this->content, $matches);
foreach ($matches[1] as $match) {
$variables[] = $match;
}
return array_unique($variables);
}
public function executions(): HasMany
{
return $this->hasMany(ScriptExecution::class);
}
public function lastExecution(): HasOne
{
return $this->hasOne(ScriptExecution::class)->latest();
}
}

View File

@ -1,61 +0,0 @@
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property int $script_id
* @property int $server_log_id
* @property string $user
* @property array $variables
* @property string $status
* @property Carbon $created_at
* @property Carbon $updated_at
* @property Script $script
* @property ?ServerLog $serverLog
*/
class ScriptExecution extends Model
{
use HasFactory;
protected $fillable = [
'script_id',
'server_log_id',
'user',
'variables',
'status',
];
protected $casts = [
'script_id' => 'integer',
'server_log_id' => 'integer',
'variables' => 'array',
];
public function script(): BelongsTo
{
return $this->belongsTo(Script::class);
}
public function getContent(): string
{
$content = $this->script->content;
foreach ($this->variables as $variable => $value) {
if (is_string($value) && ! empty($value)) {
$content = str_replace('${'.$variable.'}', $value, $content);
}
}
return $content;
}
public function serverLog(): BelongsTo
{
return $this->belongsTo(ServerLog::class);
}
}

View File

@ -14,7 +14,6 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@ -215,11 +214,6 @@ public function sshKeys(): BelongsToMany
->withTimestamps(); ->withTimestamps();
} }
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function getSshUser(): string public function getSshUser(): string
{ {
if ($this->ssh_user) { if ($this->ssh_user) {

View File

@ -2,7 +2,6 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
@ -14,7 +13,6 @@
* @property array $credentials * @property array $credentials
* @property bool $connected * @property bool $connected
* @property User $user * @property User $user
* @property ?int $project_id
*/ */
class ServerProvider extends AbstractModel class ServerProvider extends AbstractModel
{ {
@ -26,14 +24,12 @@ class ServerProvider extends AbstractModel
'provider', 'provider',
'credentials', 'credentials',
'connected', 'connected',
'project_id',
]; ];
protected $casts = [ protected $casts = [
'user_id' => 'integer', 'user_id' => 'integer',
'credentials' => 'encrypted:array', 'credentials' => 'encrypted:array',
'connected' => 'boolean', 'connected' => 'boolean',
'project_id' => 'integer',
]; ];
public function user(): BelongsTo public function user(): BelongsTo
@ -50,16 +46,4 @@ public function servers(): HasMany
{ {
return $this->hasMany(Server::class, 'provider_id'); return $this->hasMany(Server::class, 'provider_id');
} }
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public static function getByProjectId(int $projectId): Builder
{
return self::query()
->where('project_id', $projectId)
->orWhereNull('project_id');
}
} }

View File

@ -11,7 +11,6 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Support\Str; use Illuminate\Support\Str;
/** /**
@ -127,11 +126,6 @@ public function ssls(): HasMany
return $this->hasMany(Ssl::class); return $this->hasMany(Ssl::class);
} }
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
/** /**
* @throws SourceControlIsNotConnected * @throws SourceControlIsNotConnected
*/ */
@ -289,17 +283,4 @@ public function hasSSL(): bool
{ {
return $this->ssls->isNotEmpty(); return $this->ssls->isNotEmpty();
} }
public function environmentVariables(?Deployment $deployment = null): array
{
return [
'SITE_PATH' => $this->path,
'DOMAIN' => $this->domain,
'BRANCH' => $this->branch ?? '',
'REPOSITORY' => $this->repository ?? '',
'COMMIT_ID' => $deployment?->commit_id ?? '',
'PHP_VERSION' => $this->php_version,
'PHP_PATH' => '/usr/bin/php'.$this->php_version,
];
}
} }

View File

@ -3,7 +3,7 @@
namespace App\Models; namespace App\Models;
use App\SourceControlProviders\SourceControlProvider; use App\SourceControlProviders\SourceControlProvider;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
@ -57,10 +57,10 @@ public function project(): BelongsTo
return $this->belongsTo(Project::class); return $this->belongsTo(Project::class);
} }
public static function getByProjectId(int $projectId): Builder public static function getByCurrentProject(): Collection
{ {
return self::query() return self::query()
->where('project_id', $projectId) ->where('project_id', auth()->user()->current_project_id)
->orWhereNull('project_id'); ->orWhereNull('project_id')->get();
} }
} }

View File

@ -2,7 +2,6 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
@ -13,7 +12,6 @@
* @property string $provider * @property string $provider
* @property array $credentials * @property array $credentials
* @property User $user * @property User $user
* @property int $project_id
*/ */
class StorageProvider extends AbstractModel class StorageProvider extends AbstractModel
{ {
@ -24,13 +22,11 @@ class StorageProvider extends AbstractModel
'profile', 'profile',
'provider', 'provider',
'credentials', 'credentials',
'project_id',
]; ];
protected $casts = [ protected $casts = [
'user_id' => 'integer', 'user_id' => 'integer',
'credentials' => 'encrypted:array', 'credentials' => 'encrypted:array',
'project_id' => 'integer',
]; ];
public function user(): BelongsTo public function user(): BelongsTo
@ -49,16 +45,4 @@ public function backups(): HasMany
{ {
return $this->hasMany(Backup::class, 'storage_id'); return $this->hasMany(Backup::class, 'storage_id');
} }
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public static function getByProjectId(int $projectId): Builder
{
return self::query()
->where('project_id', $projectId)
->orWhereNull('project_id');
}
} }

View File

@ -1,55 +0,0 @@
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
/**
* @property int $id
* @property int $project_id
* @property string $name
* @property string $color
* @property Carbon $created_at
* @property Carbon $updated_at
*/
class Tag extends Model
{
use HasFactory;
protected $fillable = [
'project_id',
'name',
'color',
];
protected $casts = [
'project_id' => 'int',
];
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function servers(): MorphToMany
{
return $this->morphedByMany(Server::class, 'taggable');
}
public function sites(): MorphToMany
{
return $this->morphedByMany(Site::class, 'taggable');
}
public static function getByProjectId(int $projectId): Builder
{
return self::query()
->where('project_id', $projectId)
->orWhereNull('project_id');
}
}

View File

@ -3,7 +3,6 @@
namespace App\Models; namespace App\Models;
use App\Enums\UserRole; use App\Enums\UserRole;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
@ -106,6 +105,24 @@ public function storageProvider(string $provider): HasOne
return $this->hasOne(StorageProvider::class)->where('provider', $provider); return $this->hasOne(StorageProvider::class)->where('provider', $provider);
} }
public function connectedStorageProviders(): HasMany
{
return $this->storageProviders()->where('connected', true);
}
public function connectedSourceControls(): array
{
$connectedSourceControls = [];
$sourceControls = $this->sourceControls()
->where('connected', 1)
->get(['provider']);
foreach ($sourceControls as $sourceControl) {
$connectedSourceControls[] = $sourceControl->provider;
}
return $connectedSourceControls;
}
public function projects(): BelongsToMany public function projects(): BelongsToMany
{ {
return $this->belongsToMany(Project::class, 'user_project')->withTimestamps(); return $this->belongsToMany(Project::class, 'user_project')->withTimestamps();
@ -116,6 +133,11 @@ public function currentProject(): HasOne
return $this->HasOne(Project::class, 'id', 'current_project_id'); return $this->HasOne(Project::class, 'id', 'current_project_id');
} }
public function isMemberOfProject(Project $project): bool
{
return $project->user_id === $this->id;
}
public function createDefaultProject(): Project public function createDefaultProject(): Project
{ {
$project = $this->projects()->first(); $project = $this->projects()->first();
@ -138,18 +160,4 @@ public function isAdmin(): bool
{ {
return $this->role === UserRole::ADMIN; return $this->role === UserRole::ADMIN;
} }
public function scripts(): HasMany
{
return $this->hasMany(Script::class);
}
public function allServers(): Builder
{
return Server::query()->whereHas('project', function (Builder $query) {
$query->whereHas('users', function ($query) {
$query->where('user_id', $this->id);
});
});
}
} }

View File

@ -7,5 +7,7 @@
abstract class AbstractNotificationChannel implements NotificationChannelInterface abstract class AbstractNotificationChannel implements NotificationChannelInterface
{ {
public function __construct(protected NotificationChannel $notificationChannel) {} public function __construct(protected NotificationChannel $notificationChannel)
{
}
} }

View File

@ -56,7 +56,7 @@ private function checkConnection(string $subject, string $text): bool
'content' => '*'.$subject.'*'."\n".$text, 'content' => '*'.$subject.'*'."\n".$text,
]); ]);
return $connect->successful(); return $connect->ok();
} }
public function send(object $notifiable, NotificationInterface $notification): void public function send(object $notifiable, NotificationInterface $notification): void

View File

@ -26,6 +26,6 @@ public function toEmail(object $notifiable): MailMessage
return (new MailMessage) return (new MailMessage)
->subject(__('Server disconnected!')) ->subject(__('Server disconnected!'))
->line("We've disconnected from your server [".$this->server->name.'].') ->line("We've disconnected from your server [".$this->server->name.'].')
->line('Please check your server is online and make sure that has our public keys in it'); ->line('Please check your sever is online and make sure that has our public keys in it');
} }
} }

View File

@ -7,7 +7,9 @@
class SiteInstallationFailed extends AbstractNotification class SiteInstallationFailed extends AbstractNotification
{ {
public function __construct(protected Site $site) {} public function __construct(protected Site $site)
{
}
public function rawText(): string public function rawText(): string
{ {

View File

@ -7,7 +7,9 @@
class SiteInstallationSucceed extends AbstractNotification class SiteInstallationSucceed extends AbstractNotification
{ {
public function __construct(protected Site $site) {} public function __construct(protected Site $site)
{
}
public function rawText(): string public function rawText(): string
{ {

View File

@ -7,7 +7,9 @@
class SourceControlDisconnected extends AbstractNotification class SourceControlDisconnected extends AbstractNotification
{ {
public function __construct(protected SourceControl $sourceControl) {} public function __construct(protected SourceControl $sourceControl)
{
}
public function rawText(): string public function rawText(): string
{ {

View File

@ -1,43 +0,0 @@
<?php
namespace App\Policies;
use App\Models\Script;
use App\Models\Server;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class ScriptPolicy
{
use HandlesAuthorization;
public function viewAny(User $user): bool
{
return true;
}
public function view(User $user, Script $script): bool
{
return $user->id === $script->user_id;
}
public function create(User $user): bool
{
return true;
}
public function update(User $user, Script $script): bool
{
return $user->id === $script->user_id;
}
public function execute(User $user, Script $script, Server $server): bool
{
return $user->id === $script->user_id && $server->project->users->contains($user);
}
public function delete(User $user, Script $script): bool
{
return $user->id === $script->user_id;
}
}

View File

@ -2,7 +2,6 @@
namespace App\Providers; namespace App\Providers;
use App\Helpers\FTP;
use App\Helpers\Notifier; use App\Helpers\Notifier;
use App\Helpers\SSH; use App\Helpers\SSH;
use App\Helpers\Toast; use App\Helpers\Toast;
@ -37,8 +36,5 @@ public function boot(): void
$this->app->bind('toast', function () { $this->app->bind('toast', function () {
return new Toast; return new Toast;
}); });
$this->app->bind('ftp', function () {
return new FTP;
});
} }
} }

View File

@ -6,7 +6,9 @@
class Cron class Cron
{ {
public function __construct(protected Server $server) {} public function __construct(protected Server $server)
{
}
public function update(string $user, string $cron): void public function update(string $user, string $cron): void
{ {

View File

@ -1,20 +0,0 @@
<?php
namespace App\SSH;
trait HasS3Storage
{
private function prepareS3Path(string $path, string $prefix = ''): string
{
$path = trim($path);
$path = ltrim($path, '/');
$path = preg_replace('/[^a-zA-Z0-9\-_\.\/]/', '_', $path);
$path = preg_replace('/\/+/', '/', $path);
if ($prefix) {
$path = trim($prefix, '/').'/'.$path;
}
return $path;
}
}

View File

@ -2,20 +2,17 @@
namespace App\SSH\OS; namespace App\SSH\OS;
use App\Exceptions\SSHUploadFailed;
use App\Models\Server; use App\Models\Server;
use App\Models\ServerLog; use App\Models\ServerLog;
use App\SSH\HasScripts; use App\SSH\HasScripts;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Throwable;
class OS class OS
{ {
use HasScripts; use HasScripts;
public function __construct(protected Server $server) {} public function __construct(protected Server $server)
{
}
public function installDependencies(): void public function installDependencies(): void
{ {
@ -114,25 +111,14 @@ public function reboot(): void
); );
} }
/**
* @throws SSHUploadFailed
*/
public function editFile(string $path, ?string $content = null): void public function editFile(string $path, ?string $content = null): void
{ {
$tmpName = Str::random(10).strtotime('now'); $this->server->ssh()->exec(
try { $this->getScript('edit-file.sh', [
/** @var FilesystemAdapter $storageDisk */ 'path' => $path,
$storageDisk = Storage::disk('local'); 'content' => $content ?? '',
$storageDisk->put($tmpName, $content); ]),
$this->server->ssh()->upload(
$storageDisk->path($tmpName),
$path
); );
} catch (Throwable) {
throw new SSHUploadFailed();
} finally {
$this->deleteTempFile($tmpName);
}
} }
public function readFile(string $path): string public function readFile(string $path): string
@ -154,23 +140,19 @@ public function tail(string $path, int $lines): string
); );
} }
public function runScript(string $path, string $script, ?ServerLog $serverLog, ?string $user = null, ?array $variables = []): ServerLog public function runScript(string $path, string $script, ?ServerLog $serverLog): ServerLog
{ {
$ssh = $this->server->ssh($user); $ssh = $this->server->ssh();
if ($serverLog) { if ($serverLog) {
$ssh->setLog($serverLog); $ssh->setLog($serverLog);
} }
$command = ''; $ssh->exec(
foreach ($variables as $key => $variable) { $this->getScript('run-script.sh', [
$command .= "$key=$variable".PHP_EOL;
}
$command .= $this->getScript('run-script.sh', [
'path' => $path, 'path' => $path,
'script' => $script, 'script' => $script,
]); ]),
$ssh->exec($command, 'run-script'); 'run-script'
);
info($command);
return $ssh->log; return $ssh->log;
} }
@ -216,11 +198,4 @@ public function resourceInfo(): array
'disk_free' => str($info)->after('disk_free:')->before(PHP_EOL)->toString(), 'disk_free' => str($info)->after('disk_free:')->before(PHP_EOL)->toString(),
]; ];
} }
private function deleteTempFile(string $name): void
{
if (Storage::disk('local')->exists($name)) {
Storage::disk('local')->delete($name);
}
}
} }

View File

@ -0,0 +1,3 @@
if ! echo "__content__" | tee __path__; then
echo 'VITO_SSH_ERROR' && exit 1
fi

View File

@ -1,8 +1,8 @@
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common curl zip unzip git gcc openssl ufw sudo DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common curl zip unzip git gcc openssl
git config --global user.email "__email__" git config --global user.email "__email__"
git config --global user.name "__name__" git config --global user.name "__name__"
# Install Node.js # Install Node.js
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -;
sudo DEBIAN_FRONTEND=noninteractive apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install nodejs -y sudo DEBIAN_FRONTEND=noninteractive apt-get install nodejs -y

View File

@ -6,7 +6,9 @@
abstract class AbstractService implements ServiceInterface abstract class AbstractService implements ServiceInterface
{ {
public function __construct(protected Service $service) {} public function __construct(protected Service $service)
{
}
public function creationRules(array $input): array public function creationRules(array $input): array
{ {

View File

@ -117,7 +117,6 @@ public function deleteUser(string $username, string $host): void
public function link(string $username, string $host, array $databases): void public function link(string $username, string $host, array $databases): void
{ {
$ssh = $this->service->server->ssh(); $ssh = $this->service->server->ssh();
$version = $this->service->version;
foreach ($databases as $database) { foreach ($databases as $database) {
$ssh->exec( $ssh->exec(
@ -125,7 +124,6 @@ public function link(string $username, string $host, array $databases): void
'username' => $username, 'username' => $username,
'host' => $host, 'host' => $host,
'database' => $database, 'database' => $database,
'version' => $version,
]), ]),
'link-user-to-database' 'link-user-to-database'
); );
@ -134,13 +132,10 @@ public function link(string $username, string $host, array $databases): void
public function unlink(string $username, string $host): void public function unlink(string $username, string $host): void
{ {
$version = $this->service->version;
$this->service->server->ssh()->exec( $this->service->server->ssh()->exec(
$this->getScript($this->getScriptsDir().'/unlink.sh', [ $this->getScript($this->getScriptsDir().'/unlink.sh', [
'username' => $username, 'username' => $username,
'host' => $host, 'host' => $host,
'version' => $version,
]), ]),
'unlink-user-from-databases' 'unlink-user-from-databases'
); );

View File

@ -1,14 +0,0 @@
wget https://downloads.mariadb.com/MariaDB/mariadb_repo_setup
chmod +x mariadb_repo_setup
sudo DEBIAN_FRONTEND=noninteractive ./mariadb_repo_setup \
--mariadb-server-version="mariadb-10.11"
sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install mariadb-server mariadb-backup -y
sudo systemctl unmask mysql.service
sudo service mysql start

View File

@ -1,14 +0,0 @@
wget https://downloads.mariadb.com/MariaDB/mariadb_repo_setup
chmod +x mariadb_repo_setup
sudo DEBIAN_FRONTEND=noninteractive ./mariadb_repo_setup \
--mariadb-server-version="mariadb-10.6"
sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install mariadb-server mariadb-backup -y
sudo systemctl unmask mysql.service
sudo service mysql start

View File

@ -1,14 +0,0 @@
wget https://downloads.mariadb.com/MariaDB/mariadb_repo_setup
chmod +x mariadb_repo_setup
sudo DEBIAN_FRONTEND=noninteractive ./mariadb_repo_setup \
--mariadb-server-version="mariadb-11.4"
sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install mariadb-server mariadb-backup -y
sudo systemctl unmask mysql.service
sudo service mysql start

View File

@ -1,16 +1,5 @@
USER_TO_LINK='__username__' if ! sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE __database__ TO __username__;"; then
DB_NAME='__database__'
DB_VERSION='__version__'
if ! sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE \"$DB_NAME\" TO $USER_TO_LINK;"; then
echo 'VITO_SSH_ERROR' && exit 1 echo 'VITO_SSH_ERROR' && exit 1
fi fi
# Check if PostgreSQL version is 15 or greater echo "Linking to __database__ finished"
if [ "$DB_VERSION" -ge 15 ]; then
if ! sudo -u postgres psql -d "$DB_NAME" -c "GRANT USAGE, CREATE ON SCHEMA public TO $USER_TO_LINK;"; then
echo 'VITO_SSH_ERROR' && exit 1
fi
fi
echo "Linking to $DB_NAME finished"

View File

@ -1,16 +1,10 @@
USER_TO_REVOKE='__username__' USER_TO_REVOKE='__username__'
DB_VERSION='__version__'
DATABASES=$(sudo -u postgres psql -t -c "SELECT datname FROM pg_database WHERE datistemplate = false;") DATABASES=$(sudo -u postgres psql -t -c "SELECT datname FROM pg_database WHERE datistemplate = false;")
for DB in $DATABASES; do for DB in $DATABASES; do
echo "Revoking privileges in database: $DB" echo "Revoking privileges in database: $DB"
sudo -u postgres psql -d "$DB" -c "REVOKE ALL PRIVILEGES ON DATABASE \"$DB\" FROM $USER_TO_REVOKE;" sudo -u postgres psql -d "$DB" -c "REVOKE ALL PRIVILEGES ON DATABASE \"$DB\" FROM $USER_TO_REVOKE;"
# Check if PostgreSQL version is 15 or greater
if [ "$DB_VERSION" -ge 15 ]; then
sudo -u postgres psql -d "$DB" -c "REVOKE USAGE, CREATE ON SCHEMA public FROM $USER_TO_REVOKE;"
fi
done done
echo "Privileges revoked from $USER_TO_REVOKE" echo "Privileges revoked from $USER_TO_REVOKE"

View File

@ -4,4 +4,6 @@
use App\SSH\Services\AbstractService; use App\SSH\Services\AbstractService;
abstract class AbstractFirewall extends AbstractService implements Firewall {} abstract class AbstractFirewall extends AbstractService implements Firewall
{
}

View File

@ -102,10 +102,12 @@ public function installComposer(): void
); );
} }
public function getPHPIni(string $type): string public function getPHPIni(): string
{ {
return $this->service->server->os()->readFile( return $this->service->server->ssh()->exec(
sprintf('/etc/php/%s/%s/php.ini', $this->service->version, $type) $this->getScript('get-php-ini.sh', [
'version' => $this->service->version,
])
); );
} }
} }

View File

@ -0,0 +1,3 @@
if ! cat /etc/php/__version__/cli/php.ini; then
echo 'VITO_SSH_ERROR' && exit 1
fi

View File

@ -4,4 +4,6 @@
use App\SSH\Services\AbstractService; use App\SSH\Services\AbstractService;
abstract class AbstractWebserver extends AbstractService implements Webserver {} abstract class AbstractWebserver extends AbstractService implements Webserver
{
}

View File

@ -27,7 +27,6 @@ server {
fastcgi_pass unix:/var/run/php/php__php_version__-fpm.sock; fastcgi_pass unix:/var/run/php/php__php_version__-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params; include fastcgi_params;
fastcgi_hide_header X-Powered-By;
} }
location ~ /\.(?!well-known).* { location ~ /\.(?!well-known).* {

View File

@ -23,7 +23,6 @@ server {
fastcgi_pass unix:/var/run/php/php__php_version__-fpm.sock; fastcgi_pass unix:/var/run/php/php__php_version__-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params; include fastcgi_params;
fastcgi_hide_header X-Powered-By;
} }
location ~ /\.(?!well-known).* { location ~ /\.(?!well-known).* {

View File

@ -7,5 +7,7 @@
abstract class AbstractStorage implements Storage abstract class AbstractStorage implements Storage
{ {
public function __construct(protected Server $server, protected StorageProvider $storageProvider) {} public function __construct(protected Server $server, protected StorageProvider $storageProvider)
{
}
} }

View File

@ -1,78 +0,0 @@
<?php
namespace App\SSH\Storage;
use App\Exceptions\SSHCommandError;
use App\Models\Server;
use App\Models\StorageProvider;
use App\SSH\HasS3Storage;
use App\SSH\HasScripts;
use Illuminate\Support\Facades\Log;
class S3 extends S3AbstractStorage
{
use HasS3Storage, HasScripts;
public function __construct(Server $server, StorageProvider $storageProvider)
{
parent::__construct($server, $storageProvider);
$this->setBucketRegion($this->storageProvider->credentials['region']);
$this->setApiUrl();
}
/**
* @throws SSHCommandError
*/
public function upload(string $src, string $dest): array
{
$uploadCommand = $this->getScript('s3/upload.sh', [
'src' => $src,
'bucket' => $this->storageProvider->credentials['bucket'],
'dest' => $this->prepareS3Path($this->storageProvider->credentials['path'].'/'.$dest),
'key' => $this->storageProvider->credentials['key'],
'secret' => $this->storageProvider->credentials['secret'],
'region' => $this->getBucketRegion(),
'endpoint' => $this->getApiUrl(),
]);
$upload = $this->server->ssh()->exec($uploadCommand, 'upload-to-s3');
if (str_contains($upload, 'Error') || ! str_contains($upload, 'upload:')) {
Log::error('Failed to upload to S3', ['output' => $upload]);
throw new SSHCommandError('Failed to upload to S3: '.$upload);
}
return [
'size' => null, // You can parse the size from the output if needed
];
}
/**
* @throws SSHCommandError
*/
public function download(string $src, string $dest): void
{
$downloadCommand = $this->getScript('s3/download.sh', [
'src' => $this->prepareS3Path($this->storageProvider->credentials['path'].'/'.$src),
'dest' => $dest,
'bucket' => $this->storageProvider->credentials['bucket'],
'key' => $this->storageProvider->credentials['key'],
'secret' => $this->storageProvider->credentials['secret'],
'region' => $this->getBucketRegion(),
'endpoint' => $this->getApiUrl(),
]);
$download = $this->server->ssh()->exec($downloadCommand, 'download-from-s3');
if (! str_contains($download, 'Download successful')) {
Log::error('Failed to download from S3', ['output' => $download]);
throw new SSHCommandError('Failed to download from S3: '.$download);
}
}
/**
* @TODO Implement delete method
*/
public function delete(string $path): void {}
}

View File

@ -1,32 +0,0 @@
<?php
namespace App\SSH\Storage;
abstract class S3AbstractStorage extends AbstractStorage
{
protected ?string $apiUrl = null;
protected ?string $bucketRegion = null;
public function getApiUrl(): string
{
return $this->apiUrl;
}
public function setApiUrl(?string $region = null): void
{
$this->bucketRegion = $region ?? $this->bucketRegion;
$this->apiUrl = "https://s3.{$this->bucketRegion}.amazonaws.com";
}
// Getter and Setter for $bucketRegion
public function getBucketRegion(): string
{
return $this->bucketRegion;
}
public function setBucketRegion(string $region): void
{
$this->bucketRegion = $region;
}
}

View File

@ -1,84 +0,0 @@
<?php
namespace App\SSH\Storage;
use App\Exceptions\SSHCommandError;
use App\Models\Server;
use App\Models\StorageProvider;
use App\SSH\HasS3Storage;
use App\SSH\HasScripts;
use Illuminate\Support\Facades\Log;
class Wasabi extends S3AbstractStorage
{
use HasS3Storage, HasScripts;
public function __construct(Server $server, StorageProvider $storageProvider)
{
parent::__construct($server, $storageProvider);
$this->setBucketRegion($this->storageProvider->credentials['region']);
$this->setApiUrl();
}
/**
* @throws SSHCommandError
*/
public function upload(string $src, string $dest): array
{
$uploadCommand = $this->getScript('wasabi/upload.sh', [
'src' => $src,
'bucket' => $this->storageProvider->credentials['bucket'],
'dest' => $this->prepareS3Path($this->storageProvider->credentials['path'].'/'.$dest),
'key' => $this->storageProvider->credentials['key'],
'secret' => $this->storageProvider->credentials['secret'],
'region' => $this->storageProvider->credentials['region'],
'endpoint' => $this->getApiUrl(),
]);
$upload = $this->server->ssh()->exec($uploadCommand, 'upload-to-wasabi');
if (str_contains($upload, 'Error') || ! str_contains($upload, 'upload:')) {
Log::error('Failed to upload to wasabi', ['output' => $upload]);
throw new SSHCommandError('Failed to upload to wasabi: '.$upload);
}
return [
'size' => null, // You can parse the size from the output if needed
];
}
/**
* @throws SSHCommandError
*/
public function download(string $src, string $dest): void
{
$downloadCommand = $this->getScript('wasabi/download.sh', [
'src' => $this->prepareS3Path($this->storageProvider->credentials['path'].'/'.$src),
'dest' => $dest,
'bucket' => $this->storageProvider->credentials['bucket'],
'key' => $this->storageProvider->credentials['key'],
'secret' => $this->storageProvider->credentials['secret'],
'region' => $this->storageProvider->credentials['region'],
'endpoint' => $this->getApiUrl(),
]);
$download = $this->server->ssh()->exec($downloadCommand, 'download-from-wasabi');
if (! str_contains($download, 'Download successful')) {
Log::error('Failed to download from wasabi', ['output' => $download]);
throw new SSHCommandError('Failed to download from wasabi: '.$download);
}
}
/**
* @TODO Implement delete method
*/
public function delete(string $path): void {}
public function setApiUrl(?string $region = null): void
{
$this->bucketRegion = $region ?? $this->bucketRegion;
$this->apiUrl = "https://{$this->storageProvider->credentials['bucket']}.s3.{$this->getBucketRegion()}.wasabisys.com";
}
}

View File

@ -1,32 +0,0 @@
#!/bin/bash
# Configure AWS CLI with provided credentials
/usr/local/bin/aws configure set aws_access_key_id "__key__"
/usr/local/bin/aws configure set aws_secret_access_key "__secret__"
/usr/local/bin/aws configure set default.region "__region__"
# Use the provided endpoint in the correct format
ENDPOINT="__endpoint__"
BUCKET="__bucket__"
REGION="__region__"
# Ensure that DEST does not have a trailing slash
SRC="__src__"
DEST="__dest__"
# Download the file from S3
echo "Downloading s3://__bucket__/__src__ to __dest__"
download_output=$(/usr/local/bin/aws s3 cp "s3://$BUCKET/$SRC" "$DEST" --endpoint-url="$ENDPOINT" --region "$REGION" 2>&1)
download_exit_code=$?
# Log output and exit code
echo "Download command output: $download_output"
echo "Download command exit code: $download_exit_code"
# Check if the download was successful
if [ $download_exit_code -eq 0 ]; then
echo "Download successful"
else
echo "Download failed"
exit 1
fi

View File

@ -1,59 +0,0 @@
#!/bin/bash
# Check if AWS CLI is installed
if ! command -v aws &> /dev/null
then
echo "AWS CLI is not installed. Installing..."
# Detect system architecture
ARCH=$(uname -m)
if [ "$ARCH" == "x86_64" ]; then
CLI_URL="https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip"
elif [ "$ARCH" == "aarch64" ]; then
CLI_URL="https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip"
else
echo "Unsupported architecture: $ARCH"
exit 1
fi
# Download and install AWS CLI
sudo curl "$CLI_URL" -o "awscliv2.zip"
sudo unzip awscliv2.zip
sudo ./aws/install --update
sudo rm -rf awscliv2.zip aws
echo "AWS CLI installation completed."
else
echo "AWS CLI is already installed."
/usr/local/bin/aws --version
fi
# Configure AWS CLI with provided credentials
/usr/local/bin/aws configure set aws_access_key_id "__key__"
/usr/local/bin/aws configure set aws_secret_access_key "__secret__"
# Use the provided endpoint in the correct format
ENDPOINT="__endpoint__"
BUCKET="__bucket__"
REGION="__region__"
# Ensure that DEST does not have a trailing slash
SRC="__src__"
DEST="__dest__"
# Upload the file
echo "Uploading __src__ to s3://$BUCKET/$DEST"
upload_output=$(/usr/local/bin/aws s3 cp "$SRC" "s3://$BUCKET/$DEST" --endpoint-url="$ENDPOINT" --region "$REGION" 2>&1)
upload_exit_code=$?
# Log output and exit code
echo "Upload command output: $upload_output"
echo "Upload command exit code: $upload_exit_code"
# Check if the upload was successful
if [ $upload_exit_code -eq 0 ]; then
echo "Upload successful"
else
echo "Upload failed"
exit 1
fi

View File

@ -1,31 +0,0 @@
#!/bin/bash
# Configure AWS CLI with provided credentials
/usr/local/bin/aws configure set aws_access_key_id "__key__"
/usr/local/bin/aws configure set aws_secret_access_key "__secret__"
# Use the provided endpoint in the correct format
ENDPOINT="__endpoint__"
BUCKET="__bucket__"
REGION="__region__"
# Ensure that DEST does not have a trailing slash
SRC="__src__"
DEST="__dest__"
# Download the file from S3
echo "Downloading s3://__bucket____src__ to __dest__"
download_output=$(/usr/local/bin/aws s3 cp "s3://$BUCKET/$SRC" "$DEST" --endpoint-url="$ENDPOINT" --region "$REGION" 2>&1)
download_exit_code=$?
# Log output and exit code
echo "Download command output: $download_output"
echo "Download command exit code: $download_exit_code"
# Check if the download was successful
if [ $download_exit_code -eq 0 ]; then
echo "Download successful"
else
echo "Download failed"
exit 1
fi

View File

@ -1,59 +0,0 @@
#!/bin/bash
# Check if AWS CLI is installed
if ! command -v aws &> /dev/null
then
echo "AWS CLI is not installed. Installing..."
# Detect system architecture
ARCH=$(uname -m)
if [ "$ARCH" == "x86_64" ]; then
CLI_URL="https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip"
elif [ "$ARCH" == "aarch64" ]; then
CLI_URL="https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip"
else
echo "Unsupported architecture: $ARCH"
exit 1
fi
# Download and install AWS CLI
sudo curl "$CLI_URL" -o "awscliv2.zip"
sudo unzip awscliv2.zip
sudo ./aws/install --update
sudo rm -rf awscliv2.zip aws
echo "AWS CLI installation completed."
else
echo "AWS CLI is already installed."
aws --version
fi
# Configure AWS CLI with provided credentials
/usr/local/bin/aws configure set aws_access_key_id "__key__"
/usr/local/bin/aws configure set aws_secret_access_key "__secret__"
# Use the provided endpoint in the correct format
ENDPOINT="__endpoint__"
BUCKET="__bucket__"
REGION="__region__"
# Ensure that DEST does not have a trailing slash
SRC="__src__"
DEST="__dest__"
# Upload the file
echo "Uploading __src__ to s3://$BUCKET/$DEST"
upload_output=$(/usr/local/bin/aws s3 cp "$SRC" "s3://$BUCKET/$DEST" --endpoint-url="$ENDPOINT" --region "$REGION" 2>&1)
upload_exit_code=$?
# Log output and exit code
echo "Upload command output: $upload_output"
echo "Upload command exit code: $upload_exit_code"
# Check if the upload was successful
if [ $upload_exit_code -eq 0 ]; then
echo "Upload successful"
else
echo "Upload failed"
exit 1
fi

View File

@ -6,7 +6,9 @@
class Systemd class Systemd
{ {
public function __construct(protected Server $server) {} public function __construct(protected Server $server)
{
}
public function status(string $unit): string public function status(string $unit): string
{ {

View File

@ -24,4 +24,4 @@ if ! wp --path=__path__ core install --url='http://__domain__' --title="__title_
echo 'VITO_SSH_ERROR' && exit 1 echo 'VITO_SSH_ERROR' && exit 1
fi fi
echo "Wordpress installed!" print "Wordpress installed!"

View File

@ -2,4 +2,6 @@
namespace App\SiteTypes; namespace App\SiteTypes;
class Laravel extends PHPSite {} class Laravel extends PHPSite
{
}

View File

@ -41,7 +41,7 @@ public function connect(): bool
$isConnected = $connection && $this->login($connection); $isConnected = $connection && $this->login($connection);
if ($isConnected) { if ($isConnected) {
\App\Facades\FTP::close($connection); ftp_close($connection);
} }
return $isConnected; return $isConnected;
@ -58,36 +58,31 @@ public function delete(array $paths): void
if ($connection && $this->login($connection)) { if ($connection && $this->login($connection)) {
if ($this->storageProvider->credentials['passive']) { if ($this->storageProvider->credentials['passive']) {
\App\Facades\FTP::passive($connection, true); ftp_pasv($connection, true);
} }
foreach ($paths as $path) { foreach ($paths as $path) {
\App\Facades\FTP::delete($connection, $this->storageProvider->credentials['path'].'/'.$path); ftp_delete($connection, $this->storageProvider->credentials['path'].'/'.$path);
} }
} }
\App\Facades\FTP::close($connection); ftp_close($connection);
} }
private function connection(): bool|Connection private function connection(): bool|Connection
{ {
$credentials = $this->storageProvider->credentials; $credentials = $this->storageProvider->credentials;
if ($credentials['ssl']) {
return \App\Facades\FTP::connect( return ftp_ssl_connect($credentials['host'], $credentials['port'], 5);
$credentials['host'],
$credentials['port'],
$credentials['ssl']
);
} }
private function login(bool|Connection $connection): bool return ftp_connect($credentials['host'], $credentials['port'], 5);
}
private function login(Connection $connection): bool
{ {
$credentials = $this->storageProvider->credentials; $credentials = $this->storageProvider->credentials;
return \App\Facades\FTP::login( return ftp_login($connection, $credentials['username'], $credentials['password']);
$credentials['username'],
$credentials['password'],
$connection
);
} }
} }

View File

@ -1,57 +0,0 @@
<?php
namespace App\StorageProviders;
use App\Models\Server;
use App\SSH\Storage\S3 as S3Storage;
use App\SSH\Storage\Storage;
use Aws\S3\Exception\S3Exception;
use Illuminate\Support\Facades\Log;
class S3 extends S3AbstractStorageProvider
{
public function validationRules(): array
{
return [
'key' => 'required|string',
'secret' => 'required|string',
'region' => 'required|string',
'bucket' => 'required|string',
'path' => 'required|string',
];
}
public function credentialData(array $input): array
{
return [
'key' => $input['key'],
'secret' => $input['secret'],
'region' => $input['region'],
'bucket' => $input['bucket'],
'path' => $input['path'],
];
}
public function connect(): bool
{
try {
$this->setBucketRegion($this->storageProvider->credentials['region']);
$this->setApiUrl();
$this->buildClientConfig();
$this->getClient()->listBuckets();
return true;
} catch (S3Exception $e) {
Log::error('Failed to connect to S3', ['exception' => $e]);
return false;
}
}
public function ssh(Server $server): Storage
{
return new S3Storage($server, $this->storageProvider);
}
public function delete(array $paths): void {}
}

View File

@ -1,74 +0,0 @@
<?php
namespace App\StorageProviders;
use App\Models\StorageProvider;
use Aws\S3\S3Client;
abstract class S3AbstractStorageProvider extends AbstractStorageProvider implements S3ClientInterface, S3StorageInterface
{
protected ?string $apiUrl = null;
protected ?string $bucketRegion = null;
protected ?S3Client $client = null;
protected StorageProvider $storageProvider;
protected array $clientConfig = [];
public function getApiUrl(): string
{
return $this->apiUrl;
}
public function setApiUrl(?string $region = null): void
{
$this->bucketRegion = $region ?? $this->bucketRegion;
$this->apiUrl = "https://s3.{$this->bucketRegion}.amazonaws.com";
}
public function getBucketRegion(): string
{
return $this->bucketRegion;
}
public function setBucketRegion(string $region): void
{
$this->bucketRegion = $region;
}
public function getClient(): S3Client
{
return new S3Client($this->clientConfig);
}
/**
* Build the configuration array for the S3 client.
* This method can be overridden by child classes to modify the configuration.
*/
public function buildClientConfig(): array
{
$this->clientConfig = [
'credentials' => [
'key' => $this->storageProvider->credentials['key'],
'secret' => $this->storageProvider->credentials['secret'],
],
'region' => $this->getBucketRegion(),
'version' => 'latest',
'endpoint' => $this->getApiUrl(),
];
return $this->clientConfig;
}
/**
* Set or update a configuration parameter for the S3 client.
*/
public function setConfigParam(array $param): void
{
foreach ($param as $key => $value) {
$this->clientConfig[$key] = $value;
}
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace App\StorageProviders;
use Aws\S3\S3Client;
interface S3ClientInterface
{
public function getClient(): S3Client;
}

View File

@ -1,14 +0,0 @@
<?php
namespace App\StorageProviders;
interface S3StorageInterface
{
public function getApiUrl(): string;
public function setApiUrl(?string $region = null): void;
public function getBucketRegion(): string;
public function setBucketRegion(string $region): void;
}

View File

@ -1,85 +0,0 @@
<?php
namespace App\StorageProviders;
use App\Models\Server;
use App\SSH\Storage\Storage;
use App\SSH\Storage\Wasabi as WasabiStorage;
use Aws\S3\Exception\S3Exception;
use Illuminate\Support\Facades\Log;
class Wasabi extends S3AbstractStorageProvider
{
private const DEFAULT_REGION = 'us-east-1';
public function validationRules(): array
{
return [
'key' => 'required|string',
'secret' => 'required|string',
'region' => 'required|string',
'bucket' => 'required|string',
'path' => 'required|string',
];
}
public function credentialData(array $input): array
{
return [
'key' => $input['key'],
'secret' => $input['secret'],
'region' => $input['region'],
'bucket' => $input['bucket'],
'path' => $input['path'],
];
}
public function connect(): bool
{
try {
$this->setBucketRegion(self::DEFAULT_REGION);
$this->setApiUrl();
$this->buildClientConfig();
$this->getClient()->listBuckets();
return true;
} catch (S3Exception $e) {
Log::error('Failed to connect to S3', ['exception' => $e]);
return false;
}
}
/**
* Build the configuration array for the S3 client.
* This method can be overridden by child classes to modify the configuration.
*/
public function buildClientConfig(): array
{
$this->clientConfig = [
'credentials' => [
'key' => $this->storageProvider->credentials['key'],
'secret' => $this->storageProvider->credentials['secret'],
],
'region' => $this->getBucketRegion(),
'version' => 'latest',
'endpoint' => $this->getApiUrl(),
'use_path_style_endpoint' => true,
];
return $this->clientConfig;
}
public function ssh(Server $server): Storage
{
return new WasabiStorage($server, $this->storageProvider);
}
public function setApiUrl(?string $region = null): void
{
$this->bucketRegion = $region ?? $this->bucketRegion;
$this->apiUrl = "https://s3.{$this->bucketRegion}.wasabisys.com";
}
public function delete(array $paths): void {}
}

View File

@ -1,60 +0,0 @@
<?php
namespace App\Support\Testing;
use FTP\Connection;
use PHPUnit\Framework\Assert;
class FTPFake
{
protected array $connections = [];
protected array $logins = [];
public function connect(string $host, string $port, bool $ssl = false): bool|Connection
{
$this->connections[] = compact('host', 'port', 'ssl');
return true;
}
public function login(string $username, string $password, bool|Connection $connection): bool
{
$this->logins[] = compact('username', 'password');
return true;
}
public function close(bool|Connection $connection): void
{
//
}
public function passive(bool|Connection $connection, bool $passive): void
{
//
}
public function delete(bool|Connection $connection, string $path): void
{
//
}
public function assertConnected(string $host): void
{
if (! $this->connections) {
Assert::fail('No connections are made');
}
$connected = false;
foreach ($this->connections as $connection) {
if ($connection['host'] === $host) {
$connected = true;
break;
}
}
if (! $connected) {
Assert::fail('The expected host is not connected');
}
Assert::assertTrue(true, $connected);
}
}

View File

@ -17,12 +17,6 @@ class SSHFake extends SSH
protected bool $connectionWillFail = false; protected bool $connectionWillFail = false;
protected string $uploadedLocalPath;
protected string $uploadedRemotePath;
protected string $uploadedContent;
public function __construct(?string $output = null) public function __construct(?string $output = null)
{ {
$this->output = $output; $this->output = $output;
@ -69,9 +63,6 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
public function upload(string $local, string $remote): void public function upload(string $local, string $remote): void
{ {
$this->uploadedLocalPath = $local;
$this->uploadedRemotePath = $remote;
$this->uploadedContent = file_get_contents($local);
$this->log = null; $this->log = null;
} }
@ -114,22 +105,4 @@ public function assertExecutedContains(string $command): void
} }
Assert::assertTrue(true, $executed); Assert::assertTrue(true, $executed);
} }
public function assertFileUploaded(string $toPath, ?string $content = null): void
{
if (! $this->uploadedLocalPath || ! $this->uploadedRemotePath) {
Assert::fail('File is not uploaded');
}
Assert::assertEquals($toPath, $this->uploadedRemotePath);
if ($content) {
Assert::assertEquals($content, $this->uploadedContent);
}
}
public function getUploadedLocalPath(): string
{
return $this->uploadedLocalPath;
}
} }

Some files were not shown because too many files have changed in this diff Show More