Compare commits

...

21 Commits
1.6.0 ... 1.9.1

Author SHA1 Message Date
9ac5f9ebb3 revert migration squash (#261) 2024-07-30 20:39:05 +02:00
ed8965b92b Fix .env updates for double quotations (#259) 2024-07-27 17:43:46 +02:00
9473d198e1 update php fpm ini (#258) 2024-07-27 12:51:13 +02:00
55269dbcde fix: Avoid echoing when asking for the password (#255) 2024-07-23 11:29:24 +02:00
3d67153912 Refactor validation rules to implement the new ValidationRule interface (#249) 2024-07-03 00:20:07 +02:00
11e3b167cc global storage-providers, notification channels and server providers (#247) 2024-06-29 11:22:03 +02:00
ad027eb033 Enable 2FA Without QR Code (#248) 2024-06-29 09:30:28 +02:00
e031bafba5 upgrade to Laravel 11 and schema squash (#245)
* upgrade to Laravel 11 and schema squash

* code style and npm audit fix

* fix #209
2024-06-24 23:03:02 +02:00
b5c8d99ef8 Added MariaDB 10.6, 10.11 & 11.4 (#243) 2024-06-24 19:22:55 +02:00
109d644ad8 Validate APP_KEY on initializing with Docker (#240) 2024-06-18 20:52:17 +02:00
5ccbab74b1 Hide X-Powered-By header (#239) 2024-06-18 10:23:14 +02:00
7d367465ff deployment script variables (#238) 2024-06-16 23:42:46 +02:00
eec83f577c [1.x] Updated server plans for Hetzner (#236) 2024-06-13 21:58:45 +02:00
fd77368cf3 [BUG] WordPress Install Error (#237) 2024-06-13 21:52:05 +02:00
a862a603f2 Scripts (#233) 2024-06-08 18:18:17 +02:00
3b42f93654 Update ssh key validation to accept other common standards (#228) 2024-06-05 09:38:31 +02:00
661292df5e Fixes a small typo (#226) 2024-06-04 11:40:31 +02:00
0cfb938320 fix missing ubuntu 24 providers (#220) 2024-05-25 12:03:11 +02:00
dd4a3d30c0 Use Site PHP Version to Run Composer Install (#218) 2024-05-22 10:37:16 +02:00
2b849c888e fix update bug 2024-05-15 22:55:35 +02:00
d9a791755e fix updater and add post-update (#213) 2024-05-15 22:49:07 +02:00
151 changed files with 3239 additions and 987 deletions

View File

@ -22,7 +22,9 @@ public function create(Server $server, array $input): Database
'server_id' => $server->id, 'server_id' => $server->id,
'name' => $input['name'], 'name' => $input['name'],
]); ]);
$server->database()->handler()->create($database->name); /** @var \App\SSH\Services\Database\Database */
$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 ($periodInHours <= 1) { if (abs($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,6 +19,7 @@ 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

@ -0,0 +1,34 @@
<?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,8 +2,11 @@
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
@ -18,7 +21,7 @@ public function getIni(Server $server, array $input): string
/** @var PHP $handler */ /** @var PHP $handler */
$handler = $php->handler(); $handler = $php->handler();
return $handler->getPHPIni(); return $handler->getPHPIni($input['type']);
} catch (\Throwable $e) { } catch (\Throwable $e) {
throw ValidationException::withMessages( throw ValidationException::withMessages(
['ini' => $e->getMessage()] ['ini' => $e->getMessage()]
@ -28,6 +31,13 @@ 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,10 +2,13 @@
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;
@ -22,19 +25,19 @@ public function update(Server $server, array $input): void
$tmpName = Str::random(10).strtotime('now'); $tmpName = Str::random(10).strtotime('now');
try { try {
/** @var \Illuminate\Filesystem\FilesystemAdapter $storageDisk */ /** @var 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),
"/etc/php/$service->version/cli/php.ini" sprintf('/etc/php/%s/%s/php.ini', $service->version, $input['type'])
); );
$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 file!"), 'ini' => __("Couldn't update php.ini (:type) file!", ['type' => $input['type']]),
]); ]);
} }
@ -56,6 +59,10 @@ 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

@ -0,0 +1,32 @@
<?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

@ -0,0 +1,28 @@
<?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

@ -0,0 +1,62 @@
<?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,6 +38,7 @@ 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

@ -0,0 +1,34 @@
<?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,7 +44,12 @@ 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->path, $site->deploymentScript->content, $log); $site->server->os()->runScript(
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,10 +2,14 @@
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 = isset($input['url']) ? $input['url'] : null; $sourceControl->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,6 +21,7 @@ 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

@ -0,0 +1,34 @@
<?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();
}
}

10
app/Enums/PHPIniType.php Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,8 @@
* @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

@ -10,6 +10,7 @@
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;
@ -81,9 +82,12 @@ public function updateEnv(Server $server, Site $site, Request $request): Redirec
{ {
$this->authorize('manage', $server); $this->authorize('manage', $server);
app(UpdateEnv::class)->update($site, $request->input()); try {
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 updated!'); Toast::success(__('PHP ini (:type) updated!', ['type' => $request->input('type')]));
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 \App\Models\SshKey $key */ /** @var SshKey $key */
$key = app(CreateSshKey::class)->create( $key = app(CreateSshKey::class)->create(
$request->user(), $request->user(),
$request->input() $request->input()

View File

@ -0,0 +1,111 @@
<?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,7 +35,9 @@ 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::query()->where('provider', $provider)->get(); $serverProviders = ServerProvider::getByProjectId(auth()->user()->current_project_id)
->where('provider', $provider)
->get();
return view('servers.create', [ return view('servers.create', [
'serverProviders' => $serverProviders, 'serverProviders' => $serverProviders,

View File

@ -3,6 +3,7 @@
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;
@ -13,11 +14,17 @@
class NotificationChannelController extends Controller class NotificationChannelController extends Controller
{ {
public function index(): View public function index(Request $request): View
{ {
return view('settings.notification-channels.index', [ $data = [
'channels' => NotificationChannel::query()->latest()->get(), 'channels' => NotificationChannel::getByProjectId(auth()->user()->current_project_id)->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
@ -32,6 +39,19 @@ 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($projectId): RedirectResponse public function switch(Request $request, $projectId): RedirectResponse
{ {
/** @var User $user */ /** @var User $user */
$user = auth()->user(); $user = auth()->user();
@ -81,6 +81,11 @@ public function switch($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,6 +4,7 @@
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;
@ -14,11 +15,17 @@
class ServerProviderController extends Controller class ServerProviderController extends Controller
{ {
public function index(): View public function index(Request $request): View
{ {
return view('settings.server-providers.index', [ $data = [
'providers' => auth()->user()->serverProviders, 'providers' => ServerProvider::getByProjectId(auth()->user()->current_project_id)->get(),
]); ];
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
@ -33,6 +40,19 @@ 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::getByCurrentProject(), 'sourceControls' => SourceControl::getByProjectId(auth()->user()->current_project_id)->get(),
]; ];
if ($request->has('edit')) { if ($request->has('edit')) {

View File

@ -4,6 +4,7 @@
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;
@ -14,11 +15,17 @@
class StorageProviderController extends Controller class StorageProviderController extends Controller
{ {
public function index(): View public function index(Request $request): View
{ {
return view('settings.storage-providers.index', [ $data = [
'providers' => auth()->user()->storageProviders, 'providers' => StorageProvider::getByProjectId(auth()->user()->current_project_id)->get(),
]); ];
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
@ -33,6 +40,19 @@ 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

@ -20,7 +20,7 @@ class TrustProxies extends Middleware
* @var int * @var int
*/ */
protected $headers = protected $headers =
Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_PROTO |

View File

@ -3,7 +3,9 @@
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;
/** /**
@ -12,6 +14,7 @@
* @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
{ {
@ -24,6 +27,7 @@ class NotificationChannel extends AbstractModel
'data', 'data',
'connected', 'connected',
'is_default', 'is_default',
'project_id',
]; ];
protected $casts = [ protected $casts = [
@ -47,4 +51,16 @@ 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');
}
} }

66
app/Models/Script.php Normal file
View File

@ -0,0 +1,66 @@
<?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

@ -0,0 +1,61 @@
<?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

@ -2,6 +2,7 @@
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,6 +14,7 @@
* @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
{ {
@ -24,12 +26,14 @@ 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
@ -46,4 +50,16 @@ 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

@ -283,4 +283,17 @@ 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\Collection; 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;
@ -57,10 +57,10 @@ public function project(): BelongsTo
return $this->belongsTo(Project::class); return $this->belongsTo(Project::class);
} }
public static function getByCurrentProject(): Collection public static function getByProjectId(int $projectId): Builder
{ {
return self::query() return self::query()
->where('project_id', auth()->user()->current_project_id) ->where('project_id', $projectId)
->orWhereNull('project_id')->get(); ->orWhereNull('project_id');
} }
} }

View File

@ -2,6 +2,7 @@
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;
@ -12,6 +13,7 @@
* @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
{ {
@ -22,11 +24,13 @@ 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
@ -45,4 +49,16 @@ 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

@ -3,6 +3,7 @@
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;
@ -105,24 +106,6 @@ 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();
@ -133,11 +116,6 @@ 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();
@ -160,4 +138,18 @@ 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,7 +7,5 @@
abstract class AbstractNotificationChannel implements NotificationChannelInterface abstract class AbstractNotificationChannel implements NotificationChannelInterface
{ {
public function __construct(protected NotificationChannel $notificationChannel) public function __construct(protected NotificationChannel $notificationChannel) {}
{
}
} }

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 sever is online and make sure that has our public keys in it'); ->line('Please check your server is online and make sure that has our public keys in it');
} }
} }

View File

@ -7,9 +7,7 @@
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,9 +7,7 @@
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,9 +7,7 @@
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

@ -0,0 +1,43 @@
<?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

@ -14,6 +14,7 @@ public function installDependencies(Site $site): void
$site->server->ssh()->exec( $site->server->ssh()->exec(
$this->getScript('composer-install.sh', [ $this->getScript('composer-install.sh', [
'path' => $site->path, 'path' => $site->path,
'php_version' => $site->php_version,
]), ]),
'composer-install', 'composer-install',
$site->id $site->id

View File

@ -2,6 +2,6 @@ if ! cd __path__; then
echo 'VITO_SSH_ERROR' && exit 1 echo 'VITO_SSH_ERROR' && exit 1
fi fi
if ! composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev; then if ! php__php_version__ /usr/local/bin/composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev; then
echo 'VITO_SSH_ERROR' && exit 1 echo 'VITO_SSH_ERROR' && exit 1
fi fi

View File

@ -6,9 +6,7 @@
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

@ -2,17 +2,20 @@
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
{ {
@ -111,14 +114,25 @@ public function reboot(): void
); );
} }
/**
* @throws SSHUploadFailed
*/
public function editFile(string $path, ?string $content = null): void public function editFile(string $path, ?string $content = null): void
{ {
$this->server->ssh()->exec( $tmpName = Str::random(10).strtotime('now');
$this->getScript('edit-file.sh', [ try {
'path' => $path, /** @var FilesystemAdapter $storageDisk */
'content' => $content ?? '', $storageDisk = Storage::disk('local');
]), $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
@ -140,19 +154,23 @@ public function tail(string $path, int $lines): string
); );
} }
public function runScript(string $path, string $script, ?ServerLog $serverLog): ServerLog public function runScript(string $path, string $script, ?ServerLog $serverLog, ?string $user = null, ?array $variables = []): ServerLog
{ {
$ssh = $this->server->ssh(); $ssh = $this->server->ssh($user);
if ($serverLog) { if ($serverLog) {
$ssh->setLog($serverLog); $ssh->setLog($serverLog);
} }
$ssh->exec( $command = '';
$this->getScript('run-script.sh', [ foreach ($variables as $key => $variable) {
'path' => $path, $command .= "$key=$variable".PHP_EOL;
'script' => $script, }
]), $command .= $this->getScript('run-script.sh', [
'run-script' 'path' => $path,
); 'script' => $script,
]);
$ssh->exec($command, 'run-script');
info($command);
return $ssh->log; return $ssh->log;
} }
@ -198,4 +216,11 @@ 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

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

View File

@ -6,9 +6,7 @@
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

@ -0,0 +1,14 @@
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

@ -0,0 +1,14 @@
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

@ -0,0 +1,14 @@
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

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

View File

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

View File

@ -4,6 +4,4 @@
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,6 +27,7 @@ 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,6 +23,7 @@ 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,7 +7,5 @@
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

@ -6,9 +6,7 @@
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
print "Wordpress installed!" echo "Wordpress installed!"

View File

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

View File

@ -17,6 +17,12 @@ 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;
@ -63,6 +69,9 @@ 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;
} }
@ -105,4 +114,22 @@ 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;
}
} }

View File

@ -3,9 +3,9 @@
namespace App\ValidationRules; namespace App\ValidationRules;
use Cron\CronExpression; use Cron\CronExpression;
use Illuminate\Contracts\Validation\Rule; use Illuminate\Contracts\Validation\ValidationRule;
class CronRule implements Rule class CronRule implements ValidationRule
{ {
private bool $acceptCustom; private bool $acceptCustom;
@ -14,13 +14,14 @@ public function __construct(bool $acceptCustom = false)
$this->acceptCustom = $acceptCustom; $this->acceptCustom = $acceptCustom;
} }
public function passes($attribute, $value): bool public function validate(string $attribute, mixed $value, \Closure $fail): void
{ {
return CronExpression::isValidExpression($value) || ($this->acceptCustom && $value === 'custom'); if (CronExpression::isValidExpression($value)) {
} return;
}
public function message(): string if ($this->acceptCustom && $value === 'custom') {
{ return;
return __('Invalid frequency'); }
$fail('Invalid frequency')->translate();
} }
} }

View File

@ -2,21 +2,19 @@
namespace App\ValidationRules; namespace App\ValidationRules;
use Illuminate\Contracts\Validation\Rule; use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class DomainRule implements Rule class DomainRule implements ValidationRule
{ {
public function passes($attribute, $value): bool public function validate(string $attribute, mixed $value, Closure $fail): void
{ {
if ($value) { if (! $value) {
return preg_match("/^(?!\-)(?:[a-zA-Z\d\-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+)[a-zA-Z\d]{1,63}$/", $value); return;
} }
if (preg_match("/^(?!\-)(?:[a-zA-Z\d\-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+)[a-zA-Z\d]{1,63}$/", $value) === 1) {
return true; return;
} }
$fail('Domain is not valid')->translate();
public function message(): string
{
return __('Domain is not valid');
} }
} }

View File

@ -2,27 +2,16 @@
namespace App\ValidationRules; namespace App\ValidationRules;
use Illuminate\Contracts\Validation\Rule; use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class RestrictedIPAddressesRule implements Rule class RestrictedIPAddressesRule implements ValidationRule
{ {
/** public function validate(string $attribute, mixed $value, Closure $fail): void
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{ {
return ! in_array($value, config('core.restricted_ip_addresses')); if (! in_array($value, config('core.restricted_ip_addresses'))) {
} return;
}
/** $fail('IP address is restricted')->translate();
* @return array|\Illuminate\Contracts\Translation\Translator|string|null
*/
public function message()
{
return __('IP address is restricted.');
} }
} }

View File

@ -2,49 +2,21 @@
namespace App\ValidationRules; namespace App\ValidationRules;
use Illuminate\Contracts\Validation\Rule; use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Exception\NoKeyLoadedException;
class SshKeyRule implements Rule class SshKeyRule implements ValidationRule
{ {
/** public function validate(string $attribute, mixed $value, Closure $fail): void
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{ {
$key_parts = explode(' ', $value, 3); try {
if (count($key_parts) < 2) { PublicKeyLoader::load($value);
return false;
}
if (count($key_parts) > 3) {
return false;
}
$algorithm = $key_parts[0];
$key = $key_parts[1];
if (! in_array($algorithm, ['ssh-rsa', 'ssh-dss'])) {
return false;
}
$key_base64_decoded = base64_decode($key, true);
if ($key_base64_decoded == false) {
return false;
}
$check = base64_decode(substr($key, 0, 16));
$check = preg_replace("/[^\w\-]/", '', $check);
if ((string) $check !== (string) $algorithm) {
return false;
}
return true; return;
} } catch (NoKeyLoadedException) {
$fail('Invalid key')->translate();
/** }
* @return array|\Illuminate\Contracts\Translation\Translator|string|null
*/
public function message()
{
return __('Invalid key');
} }
} }

View File

@ -8,9 +8,7 @@
class ServerLayout extends Component class ServerLayout extends Component
{ {
public function __construct(public Server $server) public function __construct(public Server $server) {}
{
}
public function render(): View public function render(): View
{ {

View File

@ -8,9 +8,7 @@
class SiteLayout extends Component class SiteLayout extends Component
{ {
public function __construct(public Site $site) public function __construct(public Site $site) {}
{
}
public function render(): View public function render(): View
{ {

View File

@ -12,7 +12,7 @@
"ext-ftp": "*", "ext-ftp": "*",
"aws/aws-sdk-php": "^3.158", "aws/aws-sdk-php": "^3.158",
"laravel/fortify": "^1.17", "laravel/fortify": "^1.17",
"laravel/framework": "^10.0", "laravel/framework": "^11.0",
"laravel/tinker": "^2.8", "laravel/tinker": "^2.8",
"phpseclib/phpseclib": "~3.0" "phpseclib/phpseclib": "~3.0"
}, },
@ -21,7 +21,7 @@
"laravel/pint": "^1.10", "laravel/pint": "^1.10",
"laravel/sail": "^1.18", "laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0", "nunomaduro/collision": "^8.1",
"phpunit/phpunit": "^10.0", "phpunit/phpunit": "^10.0",
"spatie/laravel-ignition": "^2.0" "spatie/laravel-ignition": "^2.0"
}, },

1386
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -38,6 +38,9 @@
'mysql80', 'mysql80',
'mariadb103', 'mariadb103',
'mariadb104', 'mariadb104',
'mariadb106',
'mariadb1011',
'mariadb114',
'postgresql12', 'postgresql12',
'postgresql13', 'postgresql13',
'postgresql14', 'postgresql14',
@ -50,6 +53,9 @@
'mysql80' => 'mysql', 'mysql80' => 'mysql',
'mariadb103' => 'mariadb', 'mariadb103' => 'mariadb',
'mariadb104' => 'mariadb', 'mariadb104' => 'mariadb',
'mariadb106' => 'mariadb',
'mariadb1011' => 'mariadb',
'mariadb114' => 'mariadb',
'postgresql12' => 'postgresql', 'postgresql12' => 'postgresql',
'postgresql13' => 'postgresql', 'postgresql13' => 'postgresql',
'postgresql14' => 'postgresql', 'postgresql14' => 'postgresql',
@ -63,6 +69,9 @@
'mariadb' => '10.3', 'mariadb' => '10.3',
'mariadb103' => '10.3', 'mariadb103' => '10.3',
'mariadb104' => '10.4', 'mariadb104' => '10.4',
'mariadb106' => '10.6',
'mariadb1011' => '10.11',
'mariadb114' => '11.4',
'postgresql12' => '12', 'postgresql12' => '12',
'postgresql13' => '13', 'postgresql13' => '13',
'postgresql14' => '14', 'postgresql14' => '14',
@ -193,14 +202,23 @@
\App\Enums\OperatingSystem::UBUNTU20 => [ \App\Enums\OperatingSystem::UBUNTU20 => [
'10.3' => 'mariadb', '10.3' => 'mariadb',
'10.4' => 'mariadb', '10.4' => 'mariadb',
'10.6' => 'mariadb',
'10.11' => 'mariadb',
'11.4' => 'mariadb',
], ],
\App\Enums\OperatingSystem::UBUNTU22 => [ \App\Enums\OperatingSystem::UBUNTU22 => [
'10.3' => 'mariadb', '10.3' => 'mariadb',
'10.4' => 'mariadb', '10.4' => 'mariadb',
'10.6' => 'mariadb',
'10.11' => 'mariadb',
'11.4' => 'mariadb',
], ],
\App\Enums\OperatingSystem::UBUNTU24 => [ \App\Enums\OperatingSystem::UBUNTU24 => [
'10.3' => 'mariadb', '10.3' => 'mariadb',
'10.4' => 'mariadb', '10.4' => 'mariadb',
'10.6' => 'mariadb',
'10.11' => 'mariadb',
'11.4' => 'mariadb',
], ],
], ],
'postgresql' => [ 'postgresql' => [

View File

@ -487,6 +487,7 @@
'ubuntu_18' => 'linode/ubuntu18.04', 'ubuntu_18' => 'linode/ubuntu18.04',
'ubuntu_20' => 'linode/ubuntu20.04', 'ubuntu_20' => 'linode/ubuntu20.04',
'ubuntu_22' => 'linode/ubuntu22.04', 'ubuntu_22' => 'linode/ubuntu22.04',
'ubuntu_24' => 'linode/ubuntu24.04',
], ],
], ],
'digitalocean' => [ 'digitalocean' => [
@ -618,6 +619,7 @@
'ubuntu_18' => '112929540', 'ubuntu_18' => '112929540',
'ubuntu_20' => '112929454', 'ubuntu_20' => '112929454',
'ubuntu_22' => '129211873', 'ubuntu_22' => '129211873',
'ubuntu_24' => '155133621',
], ],
], ],
'vultr' => [ 'vultr' => [
@ -789,30 +791,69 @@
'ubuntu_18' => '270', 'ubuntu_18' => '270',
'ubuntu_20' => '387', 'ubuntu_20' => '387',
'ubuntu_22' => '1743', 'ubuntu_22' => '1743',
'ubuntu_24' => '2284',
], ],
], ],
'hetzner' => [ 'hetzner' => [
'plans' => [ 'plans' => [
/* Shared vCPUs x86 */
[ [
'title' => 'CX11 - 1 Cores - 2 Memory - 20 Disk', 'title' => 'CX11 - 1 Cores (Intel) - 2 Memory - 20 Disk (eu only)',
'value' => 'cx11', 'value' => 'cx11',
], ],
[ [
'title' => 'CX21 - 2 Cores - 4 Memory - 40 Disk', 'title' => 'CX22 - 2 Cores (Intel) - 4 Memory - 40 Disk (eu only)',
'value' => 'cx22',
],
[
'title' => 'CPX11 - 2 Cores (AMD) - 2 Memory - 40 Disk (eu only)',
'value' => 'cpx11',
],
[
'title' => 'CX21 - 2 Cores (Intel) - 4 Memory - 40 Disk (eu only)',
'value' => 'cx21', 'value' => 'cx21',
], ],
[ [
'title' => 'CX31 - 2 Cores - 8 Memory - 80 Disk', 'title' => 'CX32 - 4 Cores (Intel) - 8 Memory - 80 Disk (eu only)',
'value' => 'cx32',
],
[
'title' => 'CPX21 - 3 Cores (AMD) - 4 Memory - 80 Disk',
'value' => 'cpx21',
],
[
'title' => 'CX31 - 2 Cores (Intel) - 8 Memory - 80 Disk (eu only)',
'value' => 'cx31', 'value' => 'cx31',
], ],
[ [
'title' => 'CX41 - 4 Cores - 16 Memory - 160 Disk', 'title' => 'CPX31 - 4 Cores (AMD) - 8 Memory - 160 Disk',
'value' => 'cpx31',
],
[
'title' => 'CX42 - 8 Cores (Intel) - 16 Memory - 160 Disk (eu only)',
'value' => 'cx42',
],
[
'title' => 'CX41 - 4 Cores (Intel) - 16 Memory - 160 Disk (eu only)',
'value' => 'cx41', 'value' => 'cx41',
], ],
[ [
'title' => 'CX51 - 8 Cores - 32 Memory - 240 Disk', 'title' => 'CPX41 - 8 Cores (AMD) - 16 Memory - 240 Disk',
'value' => 'cpx41',
],
[
'title' => 'CX52 - 16 Cores (Intel) - 32 Memory - 320 Disk (eu only)',
'value' => 'cx52',
],
[
'title' => 'CX51 - 8 Cores (Intel) - 32 Memory - 240 Disk (eu only)',
'value' => 'cx51', 'value' => 'cx51',
], ],
[
'title' => 'CPX51 - 16 Cores (AMD) - 32 Memory - 360 Disk',
'value' => 'cpx51',
],
[ [
'title' => 'CCX11 Dedicated CPU - 2 Cores - 8 Memory - 80 Disk', 'title' => 'CCX11 Dedicated CPU - 2 Cores - 8 Memory - 80 Disk',
'value' => 'ccx11', 'value' => 'ccx11',
@ -833,66 +874,50 @@
'title' => 'CCX51 Dedicated CPU - 32 Cores - 128 Memory - 600 Disk', 'title' => 'CCX51 Dedicated CPU - 32 Cores - 128 Memory - 600 Disk',
'value' => 'ccx51', 'value' => 'ccx51',
], ],
/* Shared vCPUs Arm64 */
[ [
'title' => 'CPX 11 - 2 Cores - 2 Memory - 40 Disk', 'title' => 'CAX11 - 2 Cores (ARM64) - 4 Memory - 40 Disk',
'value' => 'cpx11',
],
[
'title' => 'CPX 21 - 3 Cores - 4 Memory - 80 Disk',
'value' => 'cpx21',
],
[
'title' => 'CPX 31 - 4 Cores - 8 Memory - 160 Disk',
'value' => 'cpx31',
],
[
'title' => 'CPX 41 - 8 Cores - 16 Memory - 240 Disk',
'value' => 'cpx41',
],
[
'title' => 'CPX 51 - 16 Cores - 32 Memory - 360 Disk',
'value' => 'cpx51',
],
[
'title' => 'CCX12 Dedicated CPU - 2 Cores - 8 Memory - 80 Disk',
'value' => 'ccx12',
],
[
'title' => 'CCX22 Dedicated CPU - 4 Cores - 16 Memory - 160 Disk',
'value' => 'ccx22',
],
[
'title' => 'CCX32 Dedicated CPU - 8 Cores - 32 Memory - 240 Disk',
'value' => 'ccx32',
],
[
'title' => 'CCX42 Dedicated CPU - 16 Cores - 64 Memory - 360 Disk',
'value' => 'ccx42',
],
[
'title' => 'CCX52 Dedicated CPU - 32 Cores - 128 Memory - 600 Disk',
'value' => 'ccx52',
],
[
'title' => 'CCX62 Dedicated CPU - 48 Cores - 192 Memory - 960 Disk',
'value' => 'ccx62',
],
[
'title' => 'CAX11 - 2 Cores - 4 Memory - 40 Disk',
'value' => 'cax11', 'value' => 'cax11',
], ],
[ [
'title' => 'CAX21 - 4 Cores - 8 Memory - 80 Disk', 'title' => 'CAX21 - 4 Cores (ARM64) - 8 Memory - 80 Disk',
'value' => 'cax21', 'value' => 'cax21',
], ],
[ [
'title' => 'CAX31 - 8 Cores - 16 Memory - 160 Disk', 'title' => 'CAX31 - 8 Cores (ARM64) - 16 Memory - 160 Disk',
'value' => 'cax31', 'value' => 'cax31',
], ],
[ [
'title' => 'CAX41 - 16 Cores - 32 Memory - 320 Disk', 'title' => 'CAX41 - 16 Cores (ARM64) - 32 Memory - 320 Disk',
'value' => 'cax41', 'value' => 'cax41',
], ],
/* Dedicated vCPUs */
[
'title' => 'CCX13 Dedicated CPU - 2 Cores (AMD) - 8 Memory - 80 Disk',
'value' => 'ccx13',
],
[
'title' => 'CCX23 Dedicated CPU - 4 Cores (AMD) - 16 Memory - 160 Disk',
'value' => 'ccx23',
],
[
'title' => 'CCX33 Dedicated CPU - 8 Cores (AMD) - 32 Memory - 240 Disk',
'value' => 'ccx33',
],
[
'title' => 'CCX43 Dedicated CPU - 16 Cores (AMD) - 64 Memory - 360 Disk',
'value' => 'ccx43',
],
[
'title' => 'CCX53 Dedicated CPU - 32 Cores (AMD) - 128 Memory - 600 Disk',
'value' => 'ccx53',
],
[
'title' => 'CCX63 Dedicated CPU - 48 Cores (AMD) - 192 Memory - 960 Disk',
'value' => 'ccx63',
],
], ],
'regions' => [ 'regions' => [
[ [
@ -917,9 +942,9 @@
], ],
], ],
'images' => [ 'images' => [
'ubuntu_18' => 'ubuntu-18.04',
'ubuntu_20' => 'ubuntu-20.04', 'ubuntu_20' => 'ubuntu-20.04',
'ubuntu_22' => 'ubuntu-22.04', 'ubuntu_22' => 'ubuntu-22.04',
'ubuntu_24' => 'ubuntu-24.04',
], ],
], ],
]; ];

View File

@ -0,0 +1,24 @@
<?php
namespace Database\Factories;
use App\Enums\ScriptExecutionStatus;
use App\Models\ScriptExecution;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
class ScriptExecutionFactory extends Factory
{
protected $model = ScriptExecution::class;
public function definition(): array
{
return [
'user' => 'root',
'variables' => [],
'status' => ScriptExecutionStatus::EXECUTING,
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Database\Factories;
use App\Models\Script;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
class ScriptFactory extends Factory
{
protected $model = Script::class;
public function definition(): array
{
return [
'name' => $this->faker->name(),
'content' => 'ls -la',
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];
}
}

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::dropIfExists('script_executions');
Schema::create('script_executions', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('script_id');
$table->unsignedBigInteger('server_log_id')->nullable();
$table->string('user');
$table->json('variables')->nullable();
$table->string('status');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('script_executions');
}
};

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('notification_channels', function (Blueprint $table) {
$table->unsignedBigInteger('project_id')->nullable();
});
}
public function down(): void
{
Schema::table('notification_channels', function (Blueprint $table) {
$table->dropColumn('project_id');
});
}
};

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('storage_providers', function (Blueprint $table) {
$table->unsignedBigInteger('project_id')->nullable();
});
}
public function down(): void
{
Schema::table('storage_providers', function (Blueprint $table) {
$table->dropColumn('project_id');
});
}
};

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('server_providers', function (Blueprint $table) {
$table->unsignedBigInteger('project_id')->nullable();
});
}
public function down(): void
{
Schema::table('server_providers', function (Blueprint $table) {
$table->dropColumn('project_id');
});
}
};

View File

@ -5,6 +5,39 @@ NAME=${NAME:-"vito"}
EMAIL=${EMAIL:-"vito@vitodeploy.com"} EMAIL=${EMAIL:-"vito@vitodeploy.com"}
PASSWORD=${PASSWORD:-"password"} PASSWORD=${PASSWORD:-"password"}
# Function to check if a string is 32 characters long
check_length() {
local key=$1
if [ ${#key} -ne 32 ]; then
echo "Invalid APP_KEY"
exit 1
fi
}
# Check if APP_KEY is set
if [ -z "$APP_KEY" ]; then
echo "APP_KEY is not set"
exit 1
fi
# Check if APP_KEY starts with 'base64:'
if [[ $APP_KEY == base64:* ]]; then
# Remove 'base64:' prefix and decode the base64 string
decoded_key=$(echo "${APP_KEY:7}" | base64 --decode 2>/dev/null)
# Check if decoding was successful
if [ $? -ne 0 ]; then
echo "Invalid APP_KEY base64 encoding"
exit 1
fi
# Check the length of the decoded key
check_length "$decoded_key"
else
# Check the length of the raw APP_KEY
check_length "$APP_KEY"
fi
# check if the flag file does not exist, indicating a first run # check if the flag file does not exist, indicating a first run
if [ ! -f "$INIT_FLAG" ]; then if [ ! -f "$INIT_FLAG" ]; then
echo "Initializing..." echo "Initializing..."

15
package-lock.json generated
View File

@ -4,6 +4,7 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "vito",
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "^0.5.2", "@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
@ -694,12 +695,12 @@
} }
}, },
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"fill-range": "^7.0.1" "fill-range": "^7.1.1"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -969,9 +970,9 @@
} }
}, },
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"

File diff suppressed because one or more lines are too long

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