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);
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 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

@ -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) {
$command .= "$key=$variable".PHP_EOL;
}
$command .= $this->getScript('run-script.sh', [
'path' => $path, 'path' => $path,
'script' => $script, 'script' => $script,
]), ]);
'run-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;
} }
if ($this->acceptCustom && $value === 'custom') {
public function message(): string 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

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