Compare commits

...

17 Commits
2.2.1 ... 2.3.0

Author SHA1 Message Date
7cda14cb76 Bump version 2.3.0 2025-02-26 20:49:20 +01:00
3bf3f7eebc Fix filemanager permissions (#508)
* Fix filemanager permissions

* fix filemanager permissions

* fix tests warning
2025-02-26 20:46:07 +01:00
e17fdbb1a0 Bump esbuild, laravel-vite-plugin and vite (#507)
Bumps [esbuild](https://github.com/evanw/esbuild) to 0.25.0 and updates ancestor dependencies [esbuild](https://github.com/evanw/esbuild), [laravel-vite-plugin](https://github.com/laravel/vite-plugin) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). These dependencies need to be updated together.


Updates `esbuild` from 0.18.20 to 0.25.0
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2023.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.18.20...v0.25.0)

Updates `laravel-vite-plugin` from 0.7.8 to 1.2.0
- [Release notes](https://github.com/laravel/vite-plugin/releases)
- [Changelog](https://github.com/laravel/vite-plugin/blob/1.x/CHANGELOG.md)
- [Upgrade guide](https://github.com/laravel/vite-plugin/blob/1.x/UPGRADE.md)
- [Commits](https://github.com/laravel/vite-plugin/compare/v0.7.8...v1.2.0)

Updates `vite` from 4.5.9 to 6.2.0
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.2.0/packages/vite)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-type: indirect
- dependency-name: laravel-vite-plugin
  dependency-type: direct:development
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-26 19:33:33 +01:00
e99146209e Fix auto-deployment branch (#506) 2025-02-23 12:50:46 +01:00
1223ea1499 Fix .env Files for Isolated Users (#496) 2025-02-22 09:23:03 +01:00
2356e44f5b Fix deployment script command (#498) 2025-02-19 20:13:26 +01:00
8c7c3d2192 Refactor firewall and add edit rule (#488) 2025-02-16 20:31:58 +01:00
e2b9d18a71 Built-in File Manager (#458) 2025-02-16 19:56:21 +01:00
75e554ad74 Disable docker build action (#494) 2025-02-16 16:25:59 +01:00
4d59529767 Increase the content width (#492) 2025-02-16 15:22:44 +01:00
ea31d0978f Cache docker build 2025-02-16 13:15:02 +01:00
ee4e9e5452 Upgrade docker to Ubuntu 24.04 2025-02-16 12:38:48 +01:00
48c12e26b2 Fix docker build 2025-02-16 12:36:33 +01:00
72f68b5917 Fix docker build (#491) 2025-02-16 12:32:36 +01:00
a889cf11a2 Fix different PHP version for sites (#487) 2025-02-16 10:58:58 +01:00
75a4fde8de Update the vhost file to name the backend after the domain (#485) 2025-02-15 09:33:40 +01:00
fd67097884 Add mariadb missing blades (#476)
* Add missing views for Mariadb

* Add missing restore link

* adding test to avoid such issues

---------

Co-authored-by: Saeed Vaziry <mr.saeedvaziry@gmail.com>
2025-02-07 20:24:08 +01:00
72 changed files with 2255 additions and 468 deletions

View File

@ -2,9 +2,9 @@ name: Docker Latest
on: on:
workflow_dispatch: workflow_dispatch:
push: # push:
branches: # branches:
- 2.x # - 2.x
jobs: jobs:
build-and-push: build-and-push:
@ -31,5 +31,4 @@ jobs:
-f docker/Dockerfile \ -f docker/Dockerfile \
-t vitodeploy/vito:latest \ -t vitodeploy/vito:latest \
--platform linux/amd64,linux/arm64 \ --platform linux/amd64,linux/arm64 \
--no-cache \
--push --push

View File

@ -2,8 +2,8 @@ name: Docker Release
on: on:
workflow_dispatch: workflow_dispatch:
release: # release:
types: [ created ] # types: [ created ]
jobs: jobs:
build-and-push: build-and-push:

View File

@ -40,5 +40,8 @@ jobs:
- name: Create sqlite database - name: Create sqlite database
run: touch storage/database-test.sqlite run: touch storage/database-test.sqlite
- name: Set up the .env file
run: touch .env
- name: Run test suite - name: Run test suite
run: php artisan test run: php artisan test

View File

@ -15,16 +15,12 @@ class ManageBackupFile
*/ */
public function download(BackupFile $file): StreamedResponse public function download(BackupFile $file): StreamedResponse
{ {
$localFilename = "backup_{$file->id}_{$file->name}.zip";
if (! Storage::disk('backups')->exists($localFilename)) {
$file->backup->server->ssh()->download( $file->backup->server->ssh()->download(
Storage::disk('backups')->path($localFilename), Storage::disk('tmp')->path(basename($file->path())),
$file->path() $file->path()
); );
}
return Storage::disk('backups')->download($localFilename, $file->name.'.zip'); return Storage::disk('tmp')->download(basename($file->path()));
} }
public function delete(BackupFile $file): void public function delete(BackupFile $file): void

View File

@ -0,0 +1,39 @@
<?php
namespace App\Actions\FileManager;
use App\Exceptions\SSHError;
use App\Models\File;
use App\Models\Server;
use App\Models\User;
use Illuminate\Validation\Rule;
class FetchFiles
{
/**
* @throws SSHError
*/
public function fetch(User $user, Server $server, array $input): void
{
File::parse(
$user,
$server,
$input['path'],
$input['user'],
$server->os()->ls($input['path'], $input['user'])
);
}
public static function rules(Server $server): array
{
return [
'path' => [
'required',
],
'user' => [
'required',
Rule::in($server->getSshUsers()),
],
];
}
}

View File

@ -1,67 +0,0 @@
<?php
namespace App\Actions\FirewallRule;
use App\Enums\FirewallRuleStatus;
use App\Models\FirewallRule;
use App\Models\Server;
use App\SSH\Services\Firewall\Firewall;
use Illuminate\Validation\Rule;
class CreateRule
{
public function create(Server $server, array $input): FirewallRule
{
$rule = new FirewallRule([
'server_id' => $server->id,
'type' => $input['type'],
'protocol' => $input['protocol'],
'port' => $input['port'],
'source' => $input['source'],
'mask' => $input['mask'] ?? null,
]);
/** @var Firewall $firewallHandler */
$firewallHandler = $server->firewall()->handler();
$firewallHandler->addRule(
$rule->type,
$rule->getRealProtocol(),
$rule->port,
$rule->source,
$rule->mask
);
$rule->status = FirewallRuleStatus::READY;
$rule->save();
return $rule;
}
public static function rules(): array
{
return [
'type' => [
'required',
'in:allow,deny',
],
'protocol' => [
'required',
Rule::in(array_keys(config('core.firewall_protocols_port'))),
],
'port' => [
'required',
'numeric',
'min:1',
'max:65535',
],
'source' => [
'required',
'ip',
],
'mask' => [
'nullable',
'numeric',
],
];
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace App\Actions\FirewallRule;
use App\Enums\FirewallRuleStatus;
use App\Models\FirewallRule;
use App\Models\Server;
class DeleteRule
{
public function delete(Server $server, FirewallRule $rule): void
{
$rule->status = FirewallRuleStatus::DELETING;
$rule->save();
$server->firewall()
->handler()
->removeRule(
$rule->type,
$rule->getRealProtocol(),
$rule->port,
$rule->source,
$rule->mask
);
$rule->delete();
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace App\Actions\FirewallRule;
use App\Enums\FirewallRuleStatus;
use App\Models\FirewallRule;
use App\Models\Server;
use App\SSH\Services\Firewall\Firewall;
class ManageRule
{
public function create(Server $server, array $input): FirewallRule
{
$sourceAny = $input['source_any'] ?? empty($input['source'] ?? null);
$rule = new FirewallRule([
'name' => $input['name'],
'server_id' => $server->id,
'type' => $input['type'],
'protocol' => $input['protocol'],
'port' => $input['port'],
'source' => $sourceAny ? null : $input['source'],
'mask' => $sourceAny ? null : ($input['mask'] ?? null),
'status' => FirewallRuleStatus::CREATING,
]);
$rule->save();
dispatch(fn () => $this->applyRule($rule));
return $rule;
}
public function update(FirewallRule $rule, array $input): FirewallRule
{
$sourceAny = $input['source_any'] ?? empty($input['source'] ?? null);
$rule->update([
'name' => $input['name'],
'type' => $input['type'],
'protocol' => $input['protocol'],
'port' => $input['port'],
'source' => $sourceAny ? null : $input['source'],
'mask' => $sourceAny ? null : ($input['mask'] ?? null),
'status' => FirewallRuleStatus::UPDATING,
]);
dispatch(fn () => $this->applyRule($rule));
return $rule;
}
public function delete(FirewallRule $rule): void
{
$rule->status = FirewallRuleStatus::DELETING;
$rule->save();
dispatch(fn () => $this->applyRule($rule));
}
protected function applyRule($rule): void
{
try {
/** @var Firewall $handler */
$handler = $rule->server->firewall()->handler();
$handler->applyRules();
} catch (\Exception $e) {
$rule->server->firewallRules()
->where('status', '!=', FirewallRuleStatus::READY)
->update(['status' => FirewallRuleStatus::FAILED]);
throw $e;
}
if ($rule->status === FirewallRuleStatus::DELETING) {
$rule->delete();
return;
}
$rule->status = FirewallRuleStatus::READY;
$rule->save();
}
public static function rules(): array
{
return [
'name' => [
'required',
'string',
'max:18',
],
'type' => [
'required',
'in:allow,deny',
],
'protocol' => [
'required',
'in:tcp,udp',
],
'port' => [
'required',
'numeric',
'min:1',
'max:65535',
],
'source' => [
'nullable',
'ip',
],
'mask' => [
'nullable',
'numeric',
'min:1',
'max:32',
],
];
}
}

View File

@ -197,26 +197,29 @@ public function createFirewallRules(Server $server): void
$server->firewallRules()->createMany([ $server->firewallRules()->createMany([
[ [
'type' => 'allow', 'type' => 'allow',
'protocol' => 'ssh', 'name' => 'SSH',
'protocol' => 'tcp',
'port' => 22, 'port' => 22,
'source' => '0.0.0.0', 'source' => null,
'mask' => 0, 'mask' => null,
'status' => FirewallRuleStatus::READY, 'status' => FirewallRuleStatus::READY,
], ],
[ [
'type' => 'allow', 'type' => 'allow',
'protocol' => 'http', 'name' => 'HTTP',
'protocol' => 'tcp',
'port' => 80, 'port' => 80,
'source' => '0.0.0.0', 'source' => null,
'mask' => 0, 'mask' => null,
'status' => FirewallRuleStatus::READY, 'status' => FirewallRuleStatus::READY,
], ],
[ [
'type' => 'allow', 'type' => 'allow',
'protocol' => 'https', 'name' => 'HTTPS',
'protocol' => 'tcp',
'port' => 443, 'port' => 443,
'source' => '0.0.0.0', 'source' => null,
'mask' => 0, 'mask' => null,
'status' => FirewallRuleStatus::READY, 'status' => FirewallRuleStatus::READY,
], ],
]); ]);

View File

@ -2,19 +2,20 @@
namespace App\Actions\Site; namespace App\Actions\Site;
use App\Exceptions\SSHUploadFailed; use App\Exceptions\SSHError;
use App\Models\Site; use App\Models\Site;
class UpdateEnv class UpdateEnv
{ {
/** /**
* @throws SSHUploadFailed * @throws SSHError
*/ */
public function update(Site $site, array $input): void public function update(Site $site, array $input): void
{ {
$site->server->os()->editFile( $site->server->os()->editFileAs(
$site->path.'/.env', $site->path.'/.env',
$input['env'] $site->user,
trim($input['env']),
); );
} }
} }

View File

@ -6,7 +6,11 @@ final class FirewallRuleStatus
{ {
const CREATING = 'creating'; const CREATING = 'creating';
const UPDATING = 'updating';
const READY = 'ready'; const READY = 'ready';
const DELETING = 'deleting'; const DELETING = 'deleting';
const FAILED = 'failed';
} }

View File

@ -14,6 +14,9 @@
* @method static setLog(?ServerLog $log) * @method static setLog(?ServerLog $log)
* @method static connect() * @method static connect()
* @method static string exec(string $command, string $log = '', int $siteId = null, ?bool $stream = false, callable $streamCallback = null) * @method static string exec(string $command, string $log = '', int $siteId = null, ?bool $stream = false, callable $streamCallback = null)
* @method static string upload(string $local, string $remote, ?string $owner = null)
* @method static string download(string $local, string $remote)
* @method static string write(string $path, string $content, string $owner = null)
* @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 assertFileUploaded(string $toPath, ?string $content = null)

View File

@ -94,11 +94,7 @@ public function connect(bool $sftp = false): void
*/ */
public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false, ?callable $streamCallback = null): string public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false, ?callable $streamCallback = null): string
{ {
if (! $log) { if (! $this->log && $log) {
$log = 'run-command';
}
if (! $this->log) {
$this->log = ServerLog::make($this->server, $log); $this->log = ServerLog::make($this->server, $log);
if ($siteId) { if ($siteId) {
$this->log->forSite($siteId); $this->log->forSite($siteId);
@ -116,13 +112,15 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
try { try {
if ($this->asUser) { if ($this->asUser) {
$command = 'sudo su - '.$this->asUser.' -c '.'"'.addslashes($command).'"'; $command = addslashes($command);
$command = str_replace('\\\'', '\'', $command);
$command = 'sudo su - '.$this->asUser.' -c '.'"'.trim($command).'"';
} }
$this->connection->setTimeout(0); $this->connection->setTimeout(0);
if ($stream) { if ($stream) {
$this->connection->exec($command, function ($output) use ($streamCallback) { $this->connection->exec($command, function ($output) use ($streamCallback) {
$this->log->write($output); $this->log?->write($output);
return $streamCallback($output); return $streamCallback($output);
}); });
@ -131,7 +129,7 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
} else { } else {
$output = ''; $output = '';
$this->connection->exec($command, function ($out) use (&$output) { $this->connection->exec($command, function ($out) use (&$output) {
$this->log->write($out); $this->log?->write($out);
$output .= $out; $output .= $out;
}); });
@ -159,7 +157,7 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
/** /**
* @throws Throwable * @throws Throwable
*/ */
public function upload(string $local, string $remote): void public function upload(string $local, string $remote, ?string $owner = null): void
{ {
$this->log = null; $this->log = null;
@ -167,7 +165,17 @@ public function upload(string $local, string $remote): void
$this->connect(true); $this->connect(true);
} }
$this->connection->put($remote, $local, SFTP::SOURCE_LOCAL_FILE); $tmpName = Str::random(10).strtotime('now');
$tempPath = home_path($this->user).'/'.$tmpName;
$this->connection->put($tempPath, $local, SFTP::SOURCE_LOCAL_FILE);
$this->exec(sprintf('sudo mv %s %s', $tempPath, $remote));
if (! $owner) {
$owner = $this->user;
}
$this->exec(sprintf('sudo chown %s:%s %s', $owner, $owner, $remote));
$this->exec(sprintf('sudo chmod 644 %s', $remote));
} }
/** /**
@ -187,22 +195,15 @@ public function download(string $local, string $remote): void
/** /**
* @throws SSHError * @throws SSHError
*/ */
public function write(string $remotePath, string $content, bool $sudo = false): void public function write(string $remotePath, string $content, ?string $owner = null): void
{ {
$tmpName = Str::random(10).strtotime('now'); $tmpName = Str::random(10).strtotime('now');
try { try {
/** @var FilesystemAdapter $storageDisk */ /** @var FilesystemAdapter $storageDisk */
$storageDisk = Storage::disk('local'); $storageDisk = Storage::disk('local');
$storageDisk->put($tmpName, $content); $storageDisk->put($tmpName, $content);
$this->upload($storageDisk->path($tmpName), $remotePath, $owner);
if ($sudo) {
$this->upload($storageDisk->path($tmpName), sprintf('/home/%s/%s', $this->server->ssh_user, $tmpName));
$this->exec(sprintf('sudo mv /home/%s/%s %s', $this->server->ssh_user, $tmpName, $remotePath));
} else {
$this->upload($storageDisk->path($tmpName), $remotePath);
}
} catch (Throwable $e) { } catch (Throwable $e) {
throw new SSHCommandError( throw new SSHCommandError(
message: $e->getMessage() message: $e->getMessage()

View File

@ -2,8 +2,7 @@
namespace App\Http\Controllers\API; namespace App\Http\Controllers\API;
use App\Actions\FirewallRule\CreateRule; use App\Actions\FirewallRule\ManageRule;
use App\Actions\FirewallRule\DeleteRule;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Resources\FirewallRuleResource; use App\Http\Resources\FirewallRuleResource;
use App\Models\FirewallRule; use App\Models\FirewallRule;
@ -21,6 +20,7 @@
use Spatie\RouteAttributes\Attributes\Middleware; use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post; use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix; use Spatie\RouteAttributes\Attributes\Prefix;
use Spatie\RouteAttributes\Attributes\Put;
#[Prefix('api/projects/{project}/servers/{server}/firewall-rules')] #[Prefix('api/projects/{project}/servers/{server}/firewall-rules')]
#[Middleware(['auth:sanctum', 'can-see-project'])] #[Middleware(['auth:sanctum', 'can-see-project'])]
@ -41,10 +41,11 @@ public function index(Project $project, Server $server): ResourceCollection
#[Post('/', name: 'api.projects.servers.firewall-rules.create', middleware: 'ability:write')] #[Post('/', name: 'api.projects.servers.firewall-rules.create', middleware: 'ability:write')]
#[Endpoint(title: 'create', description: 'Create a new firewall rule.')] #[Endpoint(title: 'create', description: 'Create a new firewall rule.')]
#[BodyParam(name: 'name', required: true)]
#[BodyParam(name: 'type', required: true, enum: ['allow', 'deny'])] #[BodyParam(name: 'type', required: true, enum: ['allow', 'deny'])]
#[BodyParam(name: 'protocol', required: true, enum: ['tcp', 'udp'])] #[BodyParam(name: 'protocol', required: true, enum: ['tcp', 'udp'])]
#[BodyParam(name: 'port', required: true)] #[BodyParam(name: 'port', required: true)]
#[BodyParam(name: 'source', required: true)] #[BodyParam(name: 'source', required: false)]
#[BodyParam(name: 'mask', description: 'Mask for source IP.', example: '0')] #[BodyParam(name: 'mask', description: 'Mask for source IP.', example: '0')]
#[ResponseFromApiResource(FirewallRuleResource::class, FirewallRule::class)] #[ResponseFromApiResource(FirewallRuleResource::class, FirewallRule::class)]
public function create(Request $request, Project $project, Server $server): FirewallRuleResource public function create(Request $request, Project $project, Server $server): FirewallRuleResource
@ -53,9 +54,31 @@ public function create(Request $request, Project $project, Server $server): Fire
$this->validateRoute($project, $server); $this->validateRoute($project, $server);
$this->validate($request, CreateRule::rules()); $this->validate($request, ManageRule::rules());
$firewallRule = app(CreateRule::class)->create($server, $request->all()); $firewallRule = app(ManageRule::class)->create($server, $request->all());
return new FirewallRuleResource($firewallRule);
}
#[Put('{firewallRule}', name: 'api.projects.servers.firewall-rules.edit', middleware: 'ability:write')]
#[Endpoint(title: 'edit', description: 'Update an existing firewall rule.')]
#[BodyParam(name: 'name', required: true)]
#[BodyParam(name: 'type', required: true, enum: ['allow', 'deny'])]
#[BodyParam(name: 'protocol', required: true, enum: ['tcp', 'udp'])]
#[BodyParam(name: 'port', required: true)]
#[BodyParam(name: 'source', required: false)]
#[BodyParam(name: 'mask', description: 'Mask for source IP.', example: '0')]
#[ResponseFromApiResource(FirewallRuleResource::class, FirewallRule::class)]
public function edit(Request $request, Project $project, Server $server, FirewallRule $firewallRule): FirewallRuleResource
{
$this->authorize('update', [FirewallRule::class, $firewallRule]);
$this->validateRoute($project, $server);
$this->validate($request, ManageRule::rules());
$firewallRule = app(ManageRule::class)->update($firewallRule, $request->all());
return new FirewallRuleResource($firewallRule); return new FirewallRuleResource($firewallRule);
} }
@ -81,7 +104,7 @@ public function delete(Project $project, Server $server, FirewallRule $firewallR
$this->validateRoute($project, $server, $firewallRule); $this->validateRoute($project, $server, $firewallRule);
app(DeleteRule::class)->delete($server, $firewallRule); app(ManageRule::class)->delete($firewallRule);
return response()->noContent(); return response()->noContent();
} }

View File

@ -29,7 +29,8 @@ public function __invoke(Request $request)
->firstOrFail(); ->firstOrFail();
foreach ($gitHook->actions as $action) { foreach ($gitHook->actions as $action) {
if ($action == 'deploy') { $webhookBranch = $gitHook->site->sourceControl->provider()->getWebhookBranch($request->array());
if ($action == 'deploy' && $gitHook->site->branch === $webhookBranch) {
try { try {
app(Deploy::class)->run($gitHook->site); app(Deploy::class)->run($gitHook->site);
} catch (SourceControlIsNotConnected) { } catch (SourceControlIsNotConnected) {

View File

@ -13,6 +13,7 @@ public function toArray(Request $request): array
{ {
return [ return [
'id' => $this->id, 'id' => $this->id,
'name' => $this->name,
'server_id' => $this->server_id, 'server_id' => $this->server_id,
'type' => $this->type, 'type' => $this->type,
'protocol' => $this->protocol, 'protocol' => $this->protocol,

147
app/Models/File.php Normal file
View File

@ -0,0 +1,147 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $user_id
* @property int $server_id
* @property string $path
* @property string $type
* @property string $server_user
* @property string $name
* @property int $size
* @property int $links
* @property string $owner
* @property string $group
* @property string $date
* @property string $permissions
* @property User $user
* @property Server $server
*/
class File extends AbstractModel
{
use HasFactory;
protected $fillable = [
'user_id',
'server_id',
'server_user',
'path',
'type',
'name',
'size',
'links',
'owner',
'group',
'date',
'permissions',
];
protected $casts = [
'user_id' => 'integer',
'server_id' => 'integer',
'size' => 'integer',
'links' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
protected static function boot(): void
{
parent::boot();
static::deleting(function (File $file) {
if ($file->name === '.' || $file->name === '..') {
return false;
}
$file->server->os()->deleteFile($file->getFilePath(), $file->server_user);
return true;
});
}
public function server(): BelongsTo
{
return $this->belongsTo(Server::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public static function path(User $user, Server $server, string $serverUser): string
{
$file = self::query()
->where('user_id', $user->id)
->where('server_id', $server->id)
->where('server_user', $serverUser)
->first();
if ($file) {
return $file->path;
}
return home_path($serverUser);
}
public static function parse(User $user, Server $server, string $path, string $serverUser, string $listOutput): void
{
self::query()
->where('user_id', $user->id)
->where('server_id', $server->id)
->delete();
// Split output by line
$lines = explode("\n", trim($listOutput));
// Skip the first two lines (total count and . & .. directories)
array_shift($lines);
foreach ($lines as $line) {
if (preg_match('/^([drwx\-]+)\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+([\w\s:\-]+)\s+(.+)$/', $line, $matches)) {
$type = match ($matches[1][0]) {
'-' => 'file',
'd' => 'directory',
default => 'unknown',
};
if ($type === 'unknown') {
continue;
}
if ($matches[7] === '.') {
continue;
}
self::create([
'user_id' => $user->id,
'server_id' => $server->id,
'server_user' => $serverUser,
'path' => $path,
'type' => $type,
'name' => $matches[7],
'size' => (int) $matches[5],
'links' => (int) $matches[2],
'owner' => $matches[3],
'group' => $matches[4],
'date' => $matches[6],
'permissions' => $matches[1],
]);
}
}
}
public function getFilePath(): string
{
return $this->path.'/'.$this->name;
}
public function isExtractable(): bool
{
$extension = pathinfo($this->name, PATHINFO_EXTENSION);
return in_array($extension, ['zip', 'tar', 'tar.gz', 'bz2', 'tar.bz2']);
}
}

View File

@ -2,11 +2,13 @@
namespace App\Models; namespace App\Models;
use App\Enums\FirewallRuleStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
/** /**
* @property int $server_id * @property int $server_id
* @property string $name
* @property string $type * @property string $type
* @property string $protocol * @property string $protocol
* @property int $port * @property int $port
@ -21,6 +23,7 @@ class FirewallRule extends AbstractModel
use HasFactory; use HasFactory;
protected $fillable = [ protected $fillable = [
'name',
'server_id', 'server_id',
'type', 'type',
'protocol', 'protocol',
@ -36,13 +39,19 @@ class FirewallRule extends AbstractModel
'port' => 'integer', 'port' => 'integer',
]; ];
public function getStatusColor(): string
{
return match ($this->status) {
FirewallRuleStatus::CREATING,
FirewallRuleStatus::UPDATING,
FirewallRuleStatus::DELETING => 'warning',
FirewallRuleStatus::READY => 'success',
FirewallRuleStatus::FAILED => 'danger',
};
}
public function server(): BelongsTo public function server(): BelongsTo
{ {
return $this->belongsTo(Server::class); return $this->belongsTo(Server::class);
} }
public function getRealProtocol(): string
{
return $this->protocol === 'udp' ? 'udp' : 'tcp';
}
} }

View File

@ -5,6 +5,7 @@
use App\Actions\Server\CheckConnection; use App\Actions\Server\CheckConnection;
use App\Enums\ServerStatus; use App\Enums\ServerStatus;
use App\Enums\ServiceStatus; use App\Enums\ServiceStatus;
use App\Exceptions\SSHError;
use App\Facades\SSH; use App\Facades\SSH;
use App\ServerTypes\ServerType; use App\ServerTypes\ServerType;
use App\SSH\Cron\Cron; use App\SSH\Cron\Cron;
@ -22,6 +23,7 @@
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Throwable;
/** /**
* @property int $project_id * @property int $project_id
@ -146,7 +148,7 @@ public static function boot(): void
} }
$server->provider()->delete(); $server->provider()->delete();
DB::commit(); DB::commit();
} catch (\Throwable $e) { } catch (Throwable $e) {
DB::rollBack(); DB::rollBack();
throw $e; throw $e;
} }
@ -465,6 +467,9 @@ public function cron(): Cron
return new Cron($this); return new Cron($this);
} }
/**
* @throws SSHError
*/
public function checkForUpdates(): void public function checkForUpdates(): void
{ {
$this->updates = $this->os()->availableUpdates(); $this->updates = $this->os()->availableUpdates();
@ -480,4 +485,15 @@ public function getAvailableUpdatesAttribute(?int $value): int
return $value; return $value;
} }
/**
* @throws Throwable
*/
public function download(string $path, string $disk = 'tmp'): void
{
$this->ssh()->download(
Storage::disk($disk)->path(basename($path)),
$path
);
}
} }

View File

@ -15,6 +15,7 @@
use Filament\Panel; use Filament\Panel;
use Filament\Support\Assets\Js; use Filament\Support\Assets\Js;
use Filament\Support\Colors\Color; use Filament\Support\Colors\Color;
use Filament\Support\Enums\MaxWidth;
use Filament\Support\Facades\FilamentAsset; use Filament\Support\Facades\FilamentAsset;
use Filament\Support\Facades\FilamentColor; use Filament\Support\Facades\FilamentColor;
use Filament\Support\Facades\FilamentView; use Filament\Support\Facades\FilamentView;
@ -89,6 +90,7 @@ public function panel(Panel $panel): Panel
->colors([ ->colors([
'primary' => Color::Indigo, 'primary' => Color::Indigo,
]) ])
->maxContentWidth(MaxWidth::ScreenTwoExtraLarge)
->viteTheme('resources/css/filament/app/theme.css') ->viteTheme('resources/css/filament/app/theme.css')
->brandLogo(fn () => view('components.brand')) ->brandLogo(fn () => view('components.brand'))
->brandLogoHeight('30px') ->brandLogoHeight('30px')

View File

@ -3,14 +3,9 @@
namespace App\SSH\OS; namespace App\SSH\OS;
use App\Exceptions\SSHError; use App\Exceptions\SSHError;
use App\Exceptions\SSHUploadFailed;
use App\Models\Server; use App\Models\Server;
use App\Models\ServerLog; use App\Models\ServerLog;
use App\Models\Site; use App\Models\Site;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Throwable;
class OS class OS
{ {
@ -178,24 +173,23 @@ public function reboot(): void
} }
/** /**
* @throws SSHUploadFailed * @deprecated use write() instead
*
* @throws SSHError
*/ */
public function editFile(string $path, ?string $content = null): void public function editFileAs(string $path, string $user, ?string $content = null): void
{ {
$tmpName = Str::random(10).strtotime('now'); $sudo = $user === 'root';
try { $actualUser = $sudo ? $this->server->getSshUser() : $user;
/** @var FilesystemAdapter $storageDisk */
$storageDisk = Storage::disk('local'); $this->server->ssh($actualUser)->exec(
$storageDisk->put($tmpName, $content); view('ssh.os.edit-file', [
$this->server->ssh()->upload( 'path' => $path,
$storageDisk->path($tmpName), 'content' => $content,
$path 'sudo' => $sudo,
]),
'edit-file'
); );
} catch (Throwable) {
throw new SSHUploadFailed;
} finally {
$this->deleteTempFile($tmpName);
}
} }
/** /**
@ -203,11 +197,11 @@ public function editFile(string $path, ?string $content = null): void
*/ */
public function readFile(string $path): string public function readFile(string $path): string
{ {
return $this->server->ssh()->exec( return trim($this->server->ssh()->exec(
view('ssh.os.read-file', [ view('ssh.os.read-file', [
'path' => $path, 'path' => $path,
]) ])
); ));
} }
/** /**
@ -263,10 +257,14 @@ public function download(string $url, string $path): string
/** /**
* @throws SSHError * @throws SSHError
*/ */
public function unzip(string $path): string public function extract(string $path, ?string $destination = null, ?string $user = null): void
{ {
return $this->server->ssh()->exec( $this->server->ssh($user)->exec(
'unzip '.$path view('ssh.os.extract', [
'path' => $path,
'destination' => $destination,
]),
'extract'
); );
} }
@ -304,9 +302,9 @@ public function resourceInfo(): array
/** /**
* @throws SSHError * @throws SSHError
*/ */
public function deleteFile(string $path): void public function deleteFile(string $path, ?string $user = null): void
{ {
$this->server->ssh()->exec( $this->server->ssh($user)->exec(
view('ssh.os.delete-file', [ view('ssh.os.delete-file', [
'path' => $path, 'path' => $path,
]), ]),
@ -314,10 +312,31 @@ public function deleteFile(string $path): void
); );
} }
private function deleteTempFile(string $name): void /**
* @throws SSHError
*/
public function ls(string $path, ?string $user = null): string
{ {
if (Storage::disk('local')->exists($name)) { return $this->server->ssh($user)->exec('ls -la '.$path);
Storage::disk('local')->delete($name);
} }
/**
* @throws SSHError
*/
public function write(string $path, string $content, ?string $user = null): void
{
$this->server->ssh()->write(
$path,
$content,
$user
);
}
/**
* @throws SSHError
*/
public function mkdir(string $path, ?string $user = null): string
{
return $this->server->ssh($user)->exec('mkdir -p '.$path);
} }
} }

View File

@ -4,7 +4,5 @@
interface Firewall interface Firewall
{ {
public function addRule(string $type, string $protocol, int $port, string $source, ?string $mask): void; public function applyRules(): void;
public function removeRule(string $type, string $protocol, int $port, string $source, ?string $mask): void;
} }

View File

@ -2,6 +2,7 @@
namespace App\SSH\Services\Firewall; namespace App\SSH\Services\Firewall;
use App\Enums\FirewallRuleStatus;
use App\Exceptions\SSHError; use App\Exceptions\SSHError;
class Ufw extends AbstractFirewall class Ufw extends AbstractFirewall
@ -26,34 +27,16 @@ public function uninstall(): void
/** /**
* @throws SSHError * @throws SSHError
*/ */
public function addRule(string $type, string $protocol, int $port, string $source, ?string $mask): void public function applyRules(): void
{ {
$this->service->server->ssh()->exec( $rules = $this->service->server
view('ssh.services.firewall.ufw.add-rule', [ ->firewallRules()
'type' => $type, ->where('status', '!=', FirewallRuleStatus::DELETING)
'protocol' => $protocol, ->get();
'port' => $port,
'source' => $source,
'mask' => $mask || $mask === 0 ? '/'.$mask : '',
]),
'add-firewall-rule'
);
}
/**
* @throws SSHError
*/
public function removeRule(string $type, string $protocol, int $port, string $source, ?string $mask): void
{
$this->service->server->ssh()->exec( $this->service->server->ssh()->exec(
view('ssh.services.firewall.ufw.remove-rule', [ view('ssh.services.firewall.ufw.apply-rules', compact('rules')),
'type' => $type, 'apply-rules'
'protocol' => $protocol,
'port' => $port,
'source' => $source,
'mask' => $mask || $mask === 0 ? '/'.$mask : '',
]),
'remove-firewall-rule'
); );
} }
} }

View File

@ -135,7 +135,7 @@ public function createFpmPool(string $user, string $version, $site_id): void
'user' => $user, 'user' => $user,
'version' => $version, 'version' => $version,
]), ]),
true 'root'
); );
$this->service->server->systemd()->restart($this->service->unit); $this->service->server->systemd()->restart($this->service->unit);

View File

@ -55,7 +55,7 @@ public function create(
'numprocs' => (string) $numprocs, 'numprocs' => (string) $numprocs,
'logFile' => $logFile, 'logFile' => $logFile,
]), ]),
true 'root'
); );
$this->service->server->ssh()->exec( $this->service->server->ssh()->exec(

View File

@ -26,7 +26,7 @@ public function install(): void
view('ssh.services.webserver.nginx.nginx', [ view('ssh.services.webserver.nginx.nginx', [
'user' => $this->service->server->getSshUser(), 'user' => $this->service->server->getSshUser(),
]), ]),
true 'root'
); );
$this->service->server->systemd()->restart('nginx'); $this->service->server->systemd()->restart('nginx');
@ -83,7 +83,7 @@ public function createVHost(Site $site): void
view('ssh.services.webserver.nginx.vhost', [ view('ssh.services.webserver.nginx.vhost', [
'site' => $site, 'site' => $site,
]), ]),
true 'root'
); );
$this->service->server->ssh()->exec( $this->service->server->ssh()->exec(
@ -108,7 +108,7 @@ public function updateVHost(Site $site, ?string $vhost = null): void
$vhost ?? view('ssh.services.webserver.nginx.vhost', [ $vhost ?? view('ssh.services.webserver.nginx.vhost', [
'site' => $site, 'site' => $site,
]), ]),
true 'root'
); );
$this->service->server->systemd()->restart('nginx'); $this->service->server->systemd()->restart('nginx');
@ -199,11 +199,13 @@ public function setupSSL(Ssl $ssl): void
*/ */
public function removeSSL(Ssl $ssl): void public function removeSSL(Ssl $ssl): void
{ {
if ($ssl->certificate_path) {
$this->service->server->ssh()->exec( $this->service->server->ssh()->exec(
'sudo rm -rf '.dirname($ssl->certificate_path).'*', 'sudo rm -rf '.dirname($ssl->certificate_path),
'remove-ssl', 'remove-ssl',
$ssl->site_id $ssl->site_id
); );
}
$this->updateVHost($ssl->site); $this->updateVHost($ssl->site);
} }

View File

@ -70,4 +70,9 @@ protected function handleResponseErrors(Response $res, string $repo): void
throw new RepositoryPermissionDenied($repo); throw new RepositoryPermissionDenied($repo);
} }
} }
public function getWebhookBranch(array $payload): string
{
return str($payload['ref'] ?? '')->after('refs/heads/')->toString();
}
} }

View File

@ -183,4 +183,9 @@ private function getAuthenticationHeaders(): array
'Authorization' => 'Basic '.$basicAuth, 'Authorization' => 'Basic '.$basicAuth,
]; ];
} }
public function getWebhookBranch(array $payload): string
{
return data_get($payload, 'push.changes.0.new.name', 'default-branch');
}
} }

View File

@ -176,4 +176,9 @@ public function getApiUrl(): string
return $host.$this->apiVersion; return $host.$this->apiVersion;
} }
public function getWebhookBranch(array $payload): string
{
return $payload['ref'] ?? '';
}
} }

View File

@ -36,4 +36,6 @@ public function getLastCommit(string $repo, string $branch): ?array;
* @throws FailedToDeployGitKey * @throws FailedToDeployGitKey
*/ */
public function deployKey(string $title, string $repo, string $key): void; public function deployKey(string $title, string $repo, string $key): void;
public function getWebhookBranch(array $payload): string;
} }

View File

@ -82,7 +82,7 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
return $output; return $output;
} }
public function upload(string $local, string $remote): void public function upload(string $local, string $remote, ?string $owner = null): void
{ {
$this->uploadedLocalPath = $local; $this->uploadedLocalPath = $local;
$this->uploadedRemotePath = $remote; $this->uploadedRemotePath = $remote;

View File

@ -178,3 +178,31 @@ function get_from_route(string $modelName, string $routeKey): mixed
return null; return null;
} }
function absolute_path(string $path): string
{
$parts = explode('/', $path);
$absoluteParts = [];
foreach ($parts as $part) {
if ($part === '' || $part === '.') {
continue; // Skip empty and current directory parts
}
if ($part === '..') {
array_pop($absoluteParts); // Move up one directory
} else {
$absoluteParts[] = $part; // Add valid directory parts
}
}
return '/'.implode('/', $absoluteParts);
}
function home_path(string $user): string
{
if ($user === 'root') {
return '/root';
}
return '/home/'.$user;
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Web\Pages\Servers\FileManager;
use App\Web\Pages\Servers\Page;
class Index extends Page
{
protected static ?string $slug = 'servers/{server}/file-manager';
protected static ?string $title = 'File Manager';
protected $listeners = ['$refresh'];
public function mount(): void
{
$this->authorize('manage', $this->server);
}
public function getWidgets(): array
{
return [
[Widgets\FilesList::class, ['server' => $this->server]],
];
}
}

View File

@ -0,0 +1,372 @@
<?php
namespace App\Web\Pages\Servers\FileManager\Widgets;
use App\Actions\FileManager\FetchFiles;
use App\Exceptions\SSHError;
use App\Models\File;
use App\Models\Server;
use App\Web\Fields\CodeEditorField;
use App\Web\Pages\Servers\FileManager\Index;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\TextInput;
use Filament\Support\Enums\ActionSize;
use Filament\Support\Enums\IconPosition;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\ActionGroup;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Storage;
class FilesList extends Widget
{
public Server $server;
public string $serverUser;
public string $path;
protected $listeners = ['$refresh'];
public function mount(): void
{
$this->serverUser = $this->server->ssh_user;
$this->path = home_path($this->serverUser);
if (request()->has('path') && request()->has('user')) {
$this->path = request('path');
$this->serverUser = request('user');
}
$this->refresh();
}
protected function getTableHeaderActions(): array
{
return [
$this->homeAction(),
$this->userAction(),
ActionGroup::make([
$this->refreshAction(),
$this->newFileAction(),
$this->newDirectoryAction(),
$this->uploadAction(),
])
->tooltip('Toolbar')
->icon('heroicon-o-ellipsis-vertical')
->color('gray')
->size(ActionSize::Large)
->iconPosition(IconPosition::After)
->dropdownPlacement('bottom-end'),
];
}
protected function getTableQuery(): Builder
{
return File::query()
->where('user_id', auth()->id())
->where('server_id', $this->server->id);
}
public function table(Table $table): Table
{
return $table
->query($this->getTableQuery())
->headerActions($this->getTableHeaderActions())
->heading(str($this->path)->substr(-50)->start(str($this->path)->length() > 50 ? '...' : ''))
->columns([
IconColumn::make('type')
->sortable()
->icon(fn (File $file) => $this->getIcon($file)),
TextColumn::make('name')
->sortable(),
TextColumn::make('size')
->sortable(),
TextColumn::make('owner')
->sortable(),
TextColumn::make('group')
->sortable(),
TextColumn::make('date')
->sortable(),
TextColumn::make('permissions')
->sortable(),
])
->recordUrl(function (File $file) {
if ($file->type === 'directory') {
return Index::getUrl([
'server' => $this->server->id,
'user' => $file->server_user,
'path' => absolute_path($file->path.'/'.$file->name),
]);
}
return '';
})
->defaultSort('type')
->actions([
$this->extractAction(),
$this->downloadAction(),
$this->editAction(),
$this->deleteAction(),
])
->checkIfRecordIsSelectableUsing(
fn (File $file): bool => $file->name !== '..',
)
->bulkActions([
DeleteBulkAction::make()
->requiresConfirmation(),
]);
}
public function changeUser(string $user): void
{
$this->redirect(
Index::getUrl([
'server' => $this->server->id,
'user' => $user,
'path' => home_path($user),
]),
true
);
}
public function refresh(): void
{
try {
app(FetchFiles::class)->fetch(
auth()->user(),
$this->server,
[
'user' => $this->serverUser,
'path' => $this->path,
]
);
} catch (SSHError) {
abort(404);
}
$this->dispatch('$refresh');
}
protected function getIcon(File $file): string
{
if ($file->type === 'directory') {
return 'heroicon-o-folder';
}
if (str($file->name)->endsWith('.blade.php')) {
return 'laravel';
}
if (str($file->name)->endsWith('.php')) {
return 'php';
}
return 'heroicon-o-document-text';
}
protected function homeAction(): Action
{
return Action::make('home')
->label('Home')
->size(ActionSize::Small)
->icon('heroicon-o-home')
->action(function () {
$this->path = home_path($this->serverUser);
$this->refresh();
});
}
protected function userAction(): ActionGroup
{
$users = [];
foreach ($this->server->getSshUsers() as $user) {
$users[] = Action::make('user-'.$user)
->action(fn () => $this->changeUser($user))
->label($user);
}
return ActionGroup::make($users)
->tooltip('Change user')
->label($this->serverUser)
->button()
->size(ActionSize::Small)
->color('gray')
->icon('heroicon-o-chevron-up-down')
->iconPosition(IconPosition::After)
->dropdownPlacement('bottom-end');
}
protected function refreshAction(): Action
{
return Action::make('refresh')
->label('Refresh')
->icon('heroicon-o-arrow-path')
->action(fn () => $this->refresh());
}
protected function newFileAction(): Action
{
return Action::make('new-file')
->label('New File')
->icon('heroicon-o-document-text')
->action(function (array $data) {
run_action($this, function () use ($data) {
$this->server->os()->write(
$this->path.'/'.$data['name'],
str_replace("\r\n", "\n", $data['content']),
$this->serverUser
);
$this->refresh();
});
})
->form(function () {
return [
TextInput::make('name')
->placeholder('file-name.txt'),
CodeEditorField::make('content'),
];
})
->modalSubmitActionLabel('Create')
->modalHeading('New File')
->modalWidth('4xl');
}
protected function newDirectoryAction(): Action
{
return Action::make('new-directory')
->label('New Directory')
->icon('heroicon-o-folder')
->action(function (array $data) {
run_action($this, function () use ($data) {
$this->server->os()->mkdir(
$this->path.'/'.$data['name'],
$this->serverUser
);
$this->refresh();
});
})
->form(function () {
return [
TextInput::make('name')
->placeholder('directory name'),
];
})
->modalSubmitActionLabel('Create')
->modalHeading('New Directory')
->modalWidth('lg');
}
protected function uploadAction(): Action
{
return Action::make('upload')
->label('Upload File')
->icon('heroicon-o-arrow-up-on-square')
->action(function (array $data) {
//
})
->after(function (array $data) {
run_action($this, function () use ($data) {
foreach ($data['file'] as $file) {
$this->server->ssh()->upload(
Storage::disk('tmp')->path($file),
$this->path.'/'.$file,
$this->serverUser
);
}
$this->refresh();
});
})
->form(function () {
return [
FileUpload::make('file')
->disk('tmp')
->multiple()
->preserveFilenames(),
];
})
->modalSubmitActionLabel('Upload to Server')
->modalHeading('Upload File')
->modalWidth('xl');
}
protected function extractAction(): Action
{
return Action::make('extract')
->tooltip('Extract')
->icon('heroicon-o-archive-box')
->hiddenLabel()
->visible(fn (File $file) => $file->isExtractable())
->action(function (File $file) {
$file->server->os()->extract($file->getFilePath(), $file->path, $file->server_user);
$this->refresh();
});
}
protected function downloadAction(): Action
{
return Action::make('download')
->tooltip('Download')
->icon('heroicon-o-arrow-down-tray')
->hiddenLabel()
->visible(fn (File $file) => $file->type === 'file')
->action(function (File $file) {
$file->server->ssh($file->server_user)->download(
Storage::disk('tmp')->path($file->name),
$file->getFilePath()
);
return Storage::disk('tmp')->download($file->name);
});
}
protected function editAction(): Action
{
return Action::make('edit')
->tooltip('Edit')
->icon('heroicon-o-pencil')
->hiddenLabel()
->visible(fn (File $file) => $file->type === 'file')
->action(function (File $file, array $data) {
$file->server->os()->write(
$file->getFilePath(),
str_replace("\r\n", "\n", $data['content']),
$file->server_user
);
$this->refresh();
})
->form(function (File $file) {
return [
CodeEditorField::make('content')
->formatStateUsing(function () use ($file) {
$file->server->ssh($file->server_user)->download(
Storage::disk('tmp')->path($file->name),
$file->getFilePath()
);
return Storage::disk('tmp')->get(basename($file->getFilePath()));
}),
];
})
->modalSubmitActionLabel('Save')
->modalHeading('Edit')
->modalWidth('4xl');
}
protected function deleteAction(): Action
{
return Action::make('delete')
->tooltip('Delete')
->icon('heroicon-o-trash')
->color('danger')
->hiddenLabel()
->requiresConfirmation()
->visible(fn (File $file) => $file->name !== '..')
->action(function (File $file) {
run_action($this, function () use ($file) {
$file->delete();
});
});
}
}

View File

@ -2,14 +2,17 @@
namespace App\Web\Pages\Servers\Firewall; namespace App\Web\Pages\Servers\Firewall;
use App\Actions\FirewallRule\CreateRule; use App\Actions\FirewallRule\ManageRule;
use App\Models\FirewallRule; use App\Models\FirewallRule;
use App\Web\Pages\Servers\Page; use App\Web\Pages\Servers\Page;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth; use Filament\Support\Enums\MaxWidth;
use Illuminate\Support\Facades\Request;
class Index extends Page class Index extends Page
{ {
@ -31,6 +34,64 @@ public function getWidgets(): array
]; ];
} }
public static function getFirewallForm(?FirewallRule $record = null): array
{
return [
TextInput::make('name')
->label('Purpose')
->default($record->name ?? null)
->rules(ManageRule::rules()['name']),
Select::make('type')
->label('Type')
->default($record->type ?? 'allow')
->options([
'allow' => 'Allow',
'deny' => 'Deny',
])
->rules(ManageRule::rules()['type']),
Select::make('protocol')
->label('Protocol')
->default($record->protocol ?? 'tcp')
->options([
'tcp' => 'TCP',
'udp' => 'UDP',
])
->rules(ManageRule::rules()['protocol']),
TextInput::make('port')
->label('Port')
->default($record->port ?? null)
->rules(['required', 'integer']),
Checkbox::make('source_any')
->label('Any Source')
->default(($record->source ?? null) == null)
->rules(['boolean'])
->helperText('Allow connections from any source, regardless of their IP address or subnet mask.')
->live(),
TextInput::make('source')
->hidden(fn (Get $get) => $get('source_any') == true)
->label('Source')
->helperText('The IP address of the source of the connection.')
->rules(ManageRule::rules()['source'])
->default($record->source ?? null)
->suffixAction(
\Filament\Forms\Components\Actions\Action::make('get_ip')
->icon('heroicon-o-globe-alt')
->color('primary')
->tooltip('Use My IP')
->action(function ($set) {
$ip = Request::ip();
$set('source', $ip);
})
),
TextInput::make('mask')
->hidden(fn (Get $get) => $get('source_any') == true)
->label('Mask')
->default($record->mask ?? null)
->helperText('The subnet mask of the source of the connection. Leave blank for a single IP address.')
->rules(ManageRule::rules()['mask']),
];
}
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
@ -45,37 +106,19 @@ protected function getHeaderActions(): array
->label('Create a Rule') ->label('Create a Rule')
->icon('heroicon-o-plus') ->icon('heroicon-o-plus')
->modalWidth(MaxWidth::Large) ->modalWidth(MaxWidth::Large)
->form([ ->modalHeading('Create Firewall Rule')
Select::make('type') ->modalDescription('Add a new rule to the firewall')
->native(false) ->modalSubmitActionLabel('Create')
->options([ ->form(self::getFirewallForm())
'allow' => 'Allow',
'deny' => 'Deny',
])
->rules(CreateRule::rules()['type']),
Select::make('protocol')
->native(false)
->options([
'tcp' => 'TCP',
'udp' => 'UDP',
])
->rules(CreateRule::rules()['protocol']),
TextInput::make('port')
->rules(CreateRule::rules()['port']),
TextInput::make('source')
->rules(CreateRule::rules()['source']),
TextInput::make('mask')
->rules(CreateRule::rules()['mask']),
])
->action(function (array $data) { ->action(function (array $data) {
run_action($this, function () use ($data) { run_action($this, function () use ($data) {
app(CreateRule::class)->create($this->server, $data); app(ManageRule::class)->create($this->server, $data);
$this->dispatch('$refresh'); $this->dispatch('$refresh');
Notification::make() Notification::make()
->success() ->success()
->title('Firewall rule created!') ->title('Applying Firewall Rule')
->send(); ->send();
}); });
}), }),

View File

@ -2,15 +2,18 @@
namespace App\Web\Pages\Servers\Firewall\Widgets; namespace App\Web\Pages\Servers\Firewall\Widgets;
use App\Actions\FirewallRule\DeleteRule; use App\Actions\FirewallRule\ManageRule;
use App\Models\FirewallRule; use App\Models\FirewallRule;
use App\Models\Server; use App\Models\Server;
use App\Web\Pages\Servers\Firewall\Index;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth;
use Filament\Tables\Actions\Action; use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget; use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
class RulesList extends Widget class RulesList extends Widget
{ {
@ -26,19 +29,40 @@ protected function getTableQuery(): Builder
protected function getTableColumns(): array protected function getTableColumns(): array
{ {
return [ return [
TextColumn::make('name')
->searchable()
->sortable()
->label('Purpose'),
TextColumn::make('type') TextColumn::make('type')
->sortable() ->sortable()
->extraAttributes(['class' => 'uppercase']) ->badge()
->color(fn (FirewallRule $record) => $record->type === 'allow' ? 'green' : 'red'), ->color(fn ($state) => $state === 'allow' ? 'success' : 'warning')
->label('Type')
->formatStateUsing(fn ($state) => Str::upper($state)),
TextColumn::make('id')
->sortable()
->label('Source')
->formatStateUsing(function (FirewallRule $record) {
$source = $record->source == null ? 'any' : $record->source;
if ($source !== 'any' && $record->mask !== null) {
$source .= '/'.$record->mask;
}
return $source;
}),
TextColumn::make('protocol') TextColumn::make('protocol')
->sortable() ->sortable()
->extraAttributes(['class' => 'uppercase']), ->badge()
->color('primary')
->label('Protocol')
->formatStateUsing(fn ($state) => Str::upper($state)),
TextColumn::make('port') TextColumn::make('port')
->sortable(), ->sortable()
TextColumn::make('source') ->label('Port'),
->sortable(), TextColumn::make('status')
TextColumn::make('mask') ->label('Status')
->sortable(), ->badge()
->color(fn (FirewallRule $record) => $record->getStatusColor()),
]; ];
} }
@ -49,6 +73,28 @@ public function table(Table $table): Table
->query($this->getTableQuery()) ->query($this->getTableQuery())
->columns($this->getTableColumns()) ->columns($this->getTableColumns())
->actions([ ->actions([
Action::make('edit')
->icon('heroicon-o-pencil')
->tooltip('Edit')
->hiddenLabel()
->modalWidth(MaxWidth::Large)
->modalHeading('Edit Firewall Rule')
->modalDescription('Edit the associated servers firewall rule.')
->modalSubmitActionLabel('Update')
->authorize(fn (FirewallRule $record) => auth()->user()->can('update', $record))
->form(fn ($record) => Index::getFirewallForm($record))
->action(function (FirewallRule $record, array $data) {
run_action($this, function () use ($record, $data) {
app(ManageRule::class)->update($record, $data);
$this->dispatch('$refresh');
Notification::make()
->success()
->title('Applying Firewall Rule')
->send();
});
}),
Action::make('delete') Action::make('delete')
->icon('heroicon-o-trash') ->icon('heroicon-o-trash')
->tooltip('Delete') ->tooltip('Delete')
@ -58,7 +104,7 @@ public function table(Table $table): Table
->authorize(fn (FirewallRule $record) => auth()->user()->can('delete', $record)) ->authorize(fn (FirewallRule $record) => auth()->user()->can('delete', $record))
->action(function (FirewallRule $record) { ->action(function (FirewallRule $record) {
try { try {
app(DeleteRule::class)->delete($this->server, $record); app(ManageRule::class)->delete($record);
} catch (\Exception $e) { } catch (\Exception $e) {
Notification::make() Notification::make()
->danger() ->danger()

View File

@ -15,6 +15,7 @@
use App\Web\Pages\Servers\Console\Index as ConsoleIndex; use App\Web\Pages\Servers\Console\Index as ConsoleIndex;
use App\Web\Pages\Servers\CronJobs\Index as CronJobsIndex; use App\Web\Pages\Servers\CronJobs\Index as CronJobsIndex;
use App\Web\Pages\Servers\Databases\Index as DatabasesIndex; use App\Web\Pages\Servers\Databases\Index as DatabasesIndex;
use App\Web\Pages\Servers\FileManager\Index as FileManagerIndex;
use App\Web\Pages\Servers\Firewall\Index as FirewallIndex; use App\Web\Pages\Servers\Firewall\Index as FirewallIndex;
use App\Web\Pages\Servers\Logs\Index as LogsIndex; use App\Web\Pages\Servers\Logs\Index as LogsIndex;
use App\Web\Pages\Servers\Metrics\Index as MetricsIndex; use App\Web\Pages\Servers\Metrics\Index as MetricsIndex;
@ -59,6 +60,13 @@ public function getSubNavigation(): array
->url(DatabasesIndex::getUrl(parameters: ['server' => $this->server])); ->url(DatabasesIndex::getUrl(parameters: ['server' => $this->server]));
} }
if (auth()->user()->can('manage', $this->server)) {
$items[] = NavigationItem::make(FileManagerIndex::getNavigationLabel())
->icon('heroicon-o-folder')
->isActiveWhen(fn () => request()->routeIs(FileManagerIndex::getRouteName().'*'))
->url(FileManagerIndex::getUrl(parameters: ['server' => $this->server]));
}
if (auth()->user()->can('viewAny', [Service::class, $this->server])) { if (auth()->user()->can('viewAny', [Service::class, $this->server])) {
$items[] = NavigationItem::make(PHPIndex::getNavigationLabel()) $items[] = NavigationItem::make(PHPIndex::getNavigationLabel())
->icon('icon-php-alt') ->icon('icon-php-alt')

View File

@ -211,7 +211,7 @@
// 'ExampleClass' => App\Example\ExampleClass::class, // 'ExampleClass' => App\Example\ExampleClass::class,
])->toArray(), ])->toArray(),
'version' => '2.2.1', 'version' => '2.3.0',
'demo' => env('APP_DEMO', false), 'demo' => env('APP_DEMO', false),
]; ];

View File

@ -485,14 +485,6 @@
'post_max_size' => 'M', 'post_max_size' => 'M',
], ],
/*
* firewall
*/
'firewall_protocols_port' => [
'tcp' => '',
'udp' => '',
],
/* /*
* Disable these IPs for servers * Disable these IPs for servers
*/ */

View File

@ -62,7 +62,7 @@
'root' => storage_path('app/key-pairs'), 'root' => storage_path('app/key-pairs'),
], ],
'backups' => [ 'tmp' => [
'driver' => 'local', 'driver' => 'local',
'root' => sys_get_temp_dir(), 'root' => sys_get_temp_dir(),
], ],

View File

@ -0,0 +1,32 @@
<?php
namespace Database\Factories;
use App\Models\File;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
class FileFactory extends Factory
{
protected $model = File::class;
public function definition(): array
{
return [
'user_id' => $this->faker->randomNumber(), //
'server_id' => $this->faker->randomNumber(),
'server_user' => $this->faker->word(),
'path' => $this->faker->word(),
'type' => 'file',
'name' => $this->faker->name(),
'size' => $this->faker->randomNumber(),
'links' => $this->faker->randomNumber(),
'owner' => $this->faker->word(),
'group' => $this->faker->word(),
'date' => $this->faker->word(),
'permissions' => $this->faker->word(),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];
}
}

View File

@ -12,6 +12,7 @@ class FirewallRuleFactory extends Factory
public function definition(): array public function definition(): array
{ {
return [ return [
'name' => $this->faker->word,
'type' => 'allow', 'type' => 'allow',
'protocol' => 'tcp', 'protocol' => 'tcp',
'port' => $this->faker->numberBetween(1, 65535), 'port' => $this->faker->numberBetween(1, 65535),

View File

@ -0,0 +1,33 @@
<?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::create('files', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->unsignedBigInteger('server_id');
$table->string('server_user');
$table->string('path');
$table->string('type');
$table->string('name');
$table->unsignedBigInteger('size');
$table->unsignedBigInteger('links');
$table->string('owner');
$table->string('group');
$table->string('date');
$table->string('permissions');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('files');
}
};

View File

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('firewall_rules', function (Blueprint $table) {
$table->string('name')->default('Undefined')->after('id');
$table->ipAddress('source')->default(null)->nullable()->change();
});
DB::statement("UPDATE firewall_rules SET name = UPPER(protocol) WHERE protocol IN ('ssh', 'http', 'https')");
DB::statement("UPDATE firewall_rules SET protocol = 'tcp' WHERE protocol IN ('ssh', 'http', 'https')");
DB::statement("UPDATE firewall_rules SET source = null WHERE source = '0.0.0.0'");
DB::statement("UPDATE firewall_rules SET mask = null WHERE mask = '0'");
}
/**
* Reverse the migrations.
*/
public function down(): void
{
DB::statement("UPDATE firewall_rules SET protocol = LOWER(name) WHERE protocol = 'tcp' AND LOWER(name) IN ('ssh', 'http', 'https')");
DB::statement("UPDATE firewall_rules SET source = '0.0.0.0' WHERE source is null");
DB::statement("UPDATE firewall_rules SET mask = '0' WHERE mask is null");
Schema::table('firewall_rules', function (Blueprint $table) {
$table->dropColumn('name');
$table->ipAddress('source')->default('0.0.0.0')->change();
});
}
};

View File

@ -1,26 +1,22 @@
FROM ubuntu:22.04 FROM ubuntu:24.04
WORKDIR /var/www/html WORKDIR /var/www/html
ENV DEBIAN_FRONTEND noninteractive ENV DEBIAN_FRONTEND noninteractive
# upgrade # upgrade
RUN apt-get clean && apt-get update && apt-get update && apt-get upgrade -y && apt-get autoremove -y RUN apt-get update && apt-get upgrade -y && apt-get autoremove -y
# requirements # requirements
RUN apt-get install -y software-properties-common curl zip unzip gcc RUN apt-get install -y software-properties-common curl zip unzip gcc nginx \
cron gnupg gosu curl ca-certificates zip unzip supervisor libcap2-bin libpng-dev \
# nginx dnsutils librsvg2-bin fswatch wget openssh-client \
RUN apt-get install -y nginx
# php
RUN apt-get update \
&& apt-get install -y cron gnupg gosu curl ca-certificates zip unzip supervisor libcap2-bin libpng-dev \
python2 dnsutils librsvg2-bin fswatch wget openssh-client \
&& add-apt-repository ppa:ondrej/php -y \ && add-apt-repository ppa:ondrej/php -y \
&& apt-get update \ && apt-get update \
&& apt-get install -y php8.2 php8.2-fpm php8.2-mbstring php8.2-mcrypt php8.2-gd php8.2-xml \ && apt-get install -y php8.2 php8.2-fpm php8.2-mbstring php8.2-mcrypt php8.2-gd php8.2-xml \
php8.2-curl php8.2-gettext php8.2-zip php8.2-bcmath php8.2-soap php8.2-redis php8.2-sqlite3 php8.2-intl php8.2-curl php8.2-gettext php8.2-zip php8.2-bcmath php8.2-soap php8.2-redis php8.2-sqlite3 php8.2-intl
# php
COPY docker/php.ini /etc/php/8.2/cli/conf.d/99-vito.ini COPY docker/php.ini /etc/php/8.2/cli/conf.d/99-vito.ini
# composer # composer

21
docker/publish.sh Normal file
View File

@ -0,0 +1,21 @@
#!/bin/bash
TAG=$1
if [ -z "$TAG" ]; then
echo "No tag provided"
exit 1
fi
rm -rf /tmp/vito
git clone git@github.com:vitodeploy/vito.git /tmp/vito
cd /tmp/vito || exit
docker buildx build . \
-f docker/Dockerfile \
-t vitodeploy/vito:"$TAG" \
--platform linux/amd64,linux/arm64 \
--no-cache \
--push

646
package-lock.json generated
View File

@ -11,7 +11,7 @@
"apexcharts": "^3.44.2", "apexcharts": "^3.44.2",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"brace": "^0.11.1", "brace": "^0.11.1",
"laravel-vite-plugin": "^0.7.2", "laravel-vite-plugin": "^1.2.0",
"postcss": "^8.4.45", "postcss": "^8.4.45",
"postcss-nesting": "^13.0.0", "postcss-nesting": "^13.0.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
@ -19,7 +19,7 @@
"prettier-plugin-sh": "^0.14.0", "prettier-plugin-sh": "^0.14.0",
"prettier-plugin-tailwindcss": "^0.5.11", "prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.10", "tailwindcss": "^3.4.10",
"vite": "^4.5.5" "vite": "^6.2.0"
} }
}, },
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
@ -35,10 +35,27 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -49,13 +66,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -66,13 +83,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -83,13 +100,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -100,13 +117,13 @@
"darwin" "darwin"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -117,13 +134,13 @@
"darwin" "darwin"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -134,13 +151,13 @@
"freebsd" "freebsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -151,13 +168,13 @@
"freebsd" "freebsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -168,13 +185,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -185,13 +202,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -202,13 +219,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -219,13 +236,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@ -236,13 +253,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -253,13 +270,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -270,13 +287,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -287,13 +304,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -304,13 +321,30 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -321,13 +355,30 @@
"netbsd" "netbsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -338,13 +389,13 @@
"openbsd" "openbsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -355,13 +406,13 @@
"sunos" "sunos"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -372,13 +423,13 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -389,13 +440,13 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -406,7 +457,7 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
@ -529,6 +580,272 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz",
"integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz",
"integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz",
"integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz",
"integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz",
"integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz",
"integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz",
"integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz",
"integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz",
"integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz",
"integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz",
"integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz",
"integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz",
"integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz",
"integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz",
"integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz",
"integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz",
"integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz",
"integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz",
"integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@tailwindcss/forms": { "node_modules/@tailwindcss/forms": {
"version": "0.5.10", "version": "0.5.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
@ -558,6 +875,13 @@
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
} }
}, },
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true,
"license": "MIT"
},
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
"version": "3.1.5", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
@ -946,9 +1270,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@ -956,31 +1280,34 @@
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/android-arm": "0.18.20", "@esbuild/aix-ppc64": "0.25.0",
"@esbuild/android-arm64": "0.18.20", "@esbuild/android-arm": "0.25.0",
"@esbuild/android-x64": "0.18.20", "@esbuild/android-arm64": "0.25.0",
"@esbuild/darwin-arm64": "0.18.20", "@esbuild/android-x64": "0.25.0",
"@esbuild/darwin-x64": "0.18.20", "@esbuild/darwin-arm64": "0.25.0",
"@esbuild/freebsd-arm64": "0.18.20", "@esbuild/darwin-x64": "0.25.0",
"@esbuild/freebsd-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.25.0",
"@esbuild/linux-arm": "0.18.20", "@esbuild/freebsd-x64": "0.25.0",
"@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-arm": "0.25.0",
"@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-arm64": "0.25.0",
"@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-ia32": "0.25.0",
"@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-loong64": "0.25.0",
"@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-mips64el": "0.25.0",
"@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-ppc64": "0.25.0",
"@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-riscv64": "0.25.0",
"@esbuild/linux-x64": "0.18.20", "@esbuild/linux-s390x": "0.25.0",
"@esbuild/netbsd-x64": "0.18.20", "@esbuild/linux-x64": "0.25.0",
"@esbuild/openbsd-x64": "0.18.20", "@esbuild/netbsd-arm64": "0.25.0",
"@esbuild/sunos-x64": "0.18.20", "@esbuild/netbsd-x64": "0.25.0",
"@esbuild/win32-arm64": "0.18.20", "@esbuild/openbsd-arm64": "0.25.0",
"@esbuild/win32-ia32": "0.18.20", "@esbuild/openbsd-x64": "0.25.0",
"@esbuild/win32-x64": "0.18.20" "@esbuild/sunos-x64": "0.25.0",
"@esbuild/win32-arm64": "0.25.0",
"@esbuild/win32-ia32": "0.25.0",
"@esbuild/win32-x64": "0.25.0"
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
@ -1255,20 +1582,23 @@
} }
}, },
"node_modules/laravel-vite-plugin": { "node_modules/laravel-vite-plugin": {
"version": "0.7.8", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.7.8.tgz", "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.2.0.tgz",
"integrity": "sha512-HWYqpQYHR3kEQ1LsHX7gHJoNNf0bz5z5mDaHBLzS+PGLCTmYqlU5/SZyeEgObV7z7bC/cnStYcY9H1DI1D5Udg==", "integrity": "sha512-R0pJ+IcTVeqEMoKz/B2Ij57QVq3sFTABiFmb06gAwFdivbOgsUtuhX6N2MGLEArajrS3U5JbberzwOe7uXHMHQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"vite-plugin-full-reload": "^1.0.5" "vite-plugin-full-reload": "^1.1.0"
},
"bin": {
"clean-orphaned-assets": "bin/clean.js"
}, },
"engines": { "engines": {
"node": ">=14" "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"vite": "^3.0.0 || ^4.0.0" "vite": "^5.0.0 || ^6.0.0"
} }
}, },
"node_modules/lilconfig": { "node_modules/lilconfig": {
@ -1546,9 +1876,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.1", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -1998,19 +2328,41 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "3.29.5", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz",
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": {
"@types/estree": "1.0.6"
},
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
}, },
"engines": { "engines": {
"node": ">=14.18.0", "node": ">=18.0.0",
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.34.8",
"@rollup/rollup-android-arm64": "4.34.8",
"@rollup/rollup-darwin-arm64": "4.34.8",
"@rollup/rollup-darwin-x64": "4.34.8",
"@rollup/rollup-freebsd-arm64": "4.34.8",
"@rollup/rollup-freebsd-x64": "4.34.8",
"@rollup/rollup-linux-arm-gnueabihf": "4.34.8",
"@rollup/rollup-linux-arm-musleabihf": "4.34.8",
"@rollup/rollup-linux-arm64-gnu": "4.34.8",
"@rollup/rollup-linux-arm64-musl": "4.34.8",
"@rollup/rollup-linux-loongarch64-gnu": "4.34.8",
"@rollup/rollup-linux-powerpc64le-gnu": "4.34.8",
"@rollup/rollup-linux-riscv64-gnu": "4.34.8",
"@rollup/rollup-linux-s390x-gnu": "4.34.8",
"@rollup/rollup-linux-x64-gnu": "4.34.8",
"@rollup/rollup-linux-x64-musl": "4.34.8",
"@rollup/rollup-win32-arm64-msvc": "4.34.8",
"@rollup/rollup-win32-ia32-msvc": "4.34.8",
"@rollup/rollup-win32-x64-msvc": "4.34.8",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@ -2480,41 +2832,48 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.5.9", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.9.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",
"integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==", "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.18.10", "esbuild": "^0.25.0",
"postcss": "^8.4.27", "postcss": "^8.5.3",
"rollup": "^3.27.1" "rollup": "^4.30.1"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.0.0" "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/vitejs/vite?sponsor=1" "url": "https://github.com/vitejs/vite?sponsor=1"
}, },
"optionalDependencies": { "optionalDependencies": {
"fsevents": "~2.3.2" "fsevents": "~2.3.3"
}, },
"peerDependencies": { "peerDependencies": {
"@types/node": ">= 14", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"jiti": ">=1.21.0",
"less": "*", "less": "*",
"lightningcss": "^1.21.0", "lightningcss": "^1.21.0",
"sass": "*", "sass": "*",
"sass-embedded": "*",
"stylus": "*", "stylus": "*",
"sugarss": "*", "sugarss": "*",
"terser": "^5.4.0" "terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@types/node": { "@types/node": {
"optional": true "optional": true
}, },
"jiti": {
"optional": true
},
"less": { "less": {
"optional": true "optional": true
}, },
@ -2524,6 +2883,9 @@
"sass": { "sass": {
"optional": true "optional": true
}, },
"sass-embedded": {
"optional": true
},
"stylus": { "stylus": {
"optional": true "optional": true
}, },
@ -2532,6 +2894,12 @@
}, },
"terser": { "terser": {
"optional": true "optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
} }
} }
}, },

View File

@ -13,7 +13,7 @@
"apexcharts": "^3.44.2", "apexcharts": "^3.44.2",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"brace": "^0.11.1", "brace": "^0.11.1",
"laravel-vite-plugin": "^0.7.2", "laravel-vite-plugin": "^1.2.0",
"postcss": "^8.4.45", "postcss": "^8.4.45",
"postcss-nesting": "^13.0.0", "postcss-nesting": "^13.0.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
@ -21,6 +21,6 @@
"prettier-plugin-sh": "^0.14.0", "prettier-plugin-sh": "^0.14.0",
"prettier-plugin-tailwindcss": "^0.5.11", "prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.10", "tailwindcss": "^3.4.10",
"vite": "^4.5.5" "vite": "^6.2.0"
} }
} }

View File

@ -1 +1 @@
rm -f {{ $path }} rm -rf {{ $path }}

View File

@ -0,0 +1,9 @@
@if($sudo) sudo @endif tee {!! $path !!} << 'VITO_SSH_EOF' > /dev/null
{!! $content !!}
VITO_SSH_EOF
if [ $? -eq 0 ]; then
echo "Successfully wrote to {{ $path }}"
else
echo 'VITO_SSH_ERROR' && exit 1
fi

View File

@ -0,0 +1,15 @@
@php
$extension = pathinfo($path, PATHINFO_EXTENSION);
@endphp
@if($extension === 'zip')
unzip -o {{ $path }} -d {{ $destination }}
@elseif($extension === 'tar'))
tar -xf {{ $path }} -C {{ $destination }}
@elseif(in_array($extension, ['gz', 'tar.gz']))
tar -xzf {{ $path }} -C {{ $destination }}
@elseif(in_array($extension, ['bz2', 'tar.bz2']))
tar -xjf {{ $path }} -C {{ $destination }}
@else
echo "Unsupported archive format: {{ $extension }}"
@endif

View File

@ -2,4 +2,4 @@
echo 'VITO_SSH_ERROR' && exit 1 echo 'VITO_SSH_ERROR' && exit 1
fi fi
{{ $script }} {!! $script !!}

View File

@ -0,0 +1,11 @@
if ! sudo DEBIAN_FRONTEND=noninteractive mysqldump -u root {{ $database }} > {{ $file }}.sql; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! DEBIAN_FRONTEND=noninteractive zip {{ $file }}.zip {{ $file }}.sql; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! rm {{ $file }}.sql; then
echo 'VITO_SSH_ERROR' && exit 1
fi

View File

@ -0,0 +1,9 @@
if ! sudo mysql -e "CREATE USER IF NOT EXISTS '{{ $username }}'@'{{ $host }}' IDENTIFIED BY '{{ $password }}'"; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! sudo mysql -e "FLUSH PRIVILEGES"; then
echo 'VITO_SSH_ERROR' && exit 1
fi
echo "Command executed"

View File

@ -0,0 +1,5 @@
if ! sudo mysql -e "CREATE DATABASE IF NOT EXISTS {{ $name }} CHARACTER SET utf8 COLLATE utf8_general_ci"; then
echo 'VITO_SSH_ERROR' && exit 1
fi
echo "Command executed"

View File

@ -0,0 +1,9 @@
if ! sudo mysql -e "DROP USER IF EXISTS '{{ $username }}'@'{{ $host }}'"; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! sudo mysql -e "FLUSH PRIVILEGES"; then
echo 'VITO_SSH_ERROR' && exit 1
fi
echo "Command executed"

View File

@ -0,0 +1,5 @@
if ! sudo mysql -e "DROP DATABASE IF EXISTS {{ $name }}"; then
echo 'VITO_SSH_ERROR' && exit 1
fi
echo "Command executed"

View File

@ -0,0 +1,9 @@
if ! sudo mysql -e "GRANT ALL PRIVILEGES ON {{ $database }}.* TO '{{ $username }}'@'{{ $host }}'"; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! sudo mysql -e "FLUSH PRIVILEGES"; then
echo 'VITO_SSH_ERROR' && exit 1
fi
echo "Linking to {{ $database }} finished"

View File

@ -0,0 +1,11 @@
if ! DEBIAN_FRONTEND=noninteractive unzip {{ $file }}.zip; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! sudo DEBIAN_FRONTEND=noninteractive mysql -u root {{ $database }} < {{ $file }}.sql; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! rm {{ $file }}.sql {{ $file }}.zip; then
echo 'VITO_SSH_ERROR' && exit 1
fi

View File

@ -0,0 +1,5 @@
if ! sudo mysql -e "REVOKE ALL PRIVILEGES, GRANT OPTION FROM '{{ $username }}'@'{{ $host }}'"; then
echo 'VITO_SSH_ERROR' && exit 1
fi
echo "Command executed"

View File

@ -1,11 +0,0 @@
if ! sudo ufw {{ $type }} from {{ $source }}{{ $mask }} to any proto {{ $protocol }} port {{ $port }}; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! sudo ufw reload; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! sudo service ufw restart; then
echo 'VITO_SSH_ERROR' && exit 1
fi

View File

@ -0,0 +1,37 @@
@include('ssh.services.firewall.ufw.backup-rules')
if ! sudo ufw --force reset; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! sudo ufw default deny incoming; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! sudo ufw default allow outgoing; then
echo 'VITO_SSH_ERROR' && exit 1
fi
@foreach($rules as $rule)
@php
$source = isset($rule->source) && $rule->source !== null
? $rule->source . (isset($rule->mask) && $rule->mask !== null ? '/' . $rule->mask : '')
: 'any';
@endphp
if ! sudo ufw {{ $rule->type }} from {{ $source }} to any proto {{ $rule->protocol }} port {{ $rule->port }}; then
@include('ssh.services.firewall.ufw.restore-rules')
echo 'VITO_SSH_ERROR' && exit 1
fi
@endforeach
if ! sudo ufw --force enable; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! sudo ufw reload; then
echo 'VITO_SSH_ERROR' && exit 1
fi
@include('ssh.services.firewall.ufw.clear-backups')

View File

@ -0,0 +1,6 @@
sudo cp /etc/ufw/before.rules /tmp/ufw.before.backup
sudo cp /etc/ufw/after.rules /tmp/ufw.after.backup
sudo cp /etc/ufw/user.rules /tmp/ufw.user.backup
sudo cp /etc/ufw/before6.rules /tmp/ufw.before6.backup
sudo cp /etc/ufw/after6.rules /tmp/ufw.after6.backup
sudo cp /etc/ufw/user6.rules /tmp/ufw.user6.backup

View File

@ -0,0 +1,6 @@
sudo rm -f /tmp/ufw.before.backup
sudo rm -f /tmp/ufw.after.backup
sudo rm -f /tmp/ufw.user.backup
sudo rm -f /tmp/ufw.before6.backup
sudo rm -f /tmp/ufw.after6.backup
sudo rm -f /tmp/ufw.user6.backup

View File

@ -1,11 +0,0 @@
if ! sudo ufw delete {{ $type }} from {{ $source }}{{ $mask }} to any proto {{ $protocol }} port {{ $port }}; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! sudo ufw reload; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! sudo service ufw restart; then
echo 'VITO_SSH_ERROR' && exit 1
fi

View File

@ -0,0 +1,10 @@
sudo ufw --force disable
sudo cp /tmp/ufw.before.backup /etc/ufw/before.rules
sudo cp /tmp/ufw.after.backup /etc/ufw/after.rules
sudo cp /tmp/ufw.user.backup /etc/ufw/user.rules
sudo cp /tmp/ufw.before6.backup /etc/ufw/before6.rules
sudo cp /tmp/ufw.after6.backup /etc/ufw/after6.rules
sudo cp /tmp/ufw.user6.backup /etc/ufw/user6.rules
sudo ufw --force enable

View File

@ -6,8 +6,12 @@
} }
@endif @endif
@php
$backendName = preg_replace("/[^A-Za-z0-9 ]/", '', $site->domain).'_backend';
@endphp
@if ($site->type === \App\Enums\SiteType::LOAD_BALANCER) @if ($site->type === \App\Enums\SiteType::LOAD_BALANCER)
upstream backend { upstream {{ $backendName }} {
@switch($site->type_data['method'] ?? \App\Enums\LoadBalancerMethod::ROUND_ROBIN) @switch($site->type_data['method'] ?? \App\Enums\LoadBalancerMethod::ROUND_ROBIN)
@case(\App\Enums\LoadBalancerMethod::LEAST_CONNECTIONS) @case(\App\Enums\LoadBalancerMethod::LEAST_CONNECTIONS)
least_conn; least_conn;
@ -49,7 +53,7 @@
@if ($site->type()->language() === 'php') @if ($site->type()->language() === 'php')
@php @php
$phpSocket = 'unix:/var/run/php/php-fpm.sock'; $phpSocket = "unix:/var/run/php/php{$site->php_version}-fpm.sock";
if ($site->isIsolated()) { if ($site->isIsolated()) {
$phpSocket = "unix:/run/php/php{$site->php_version}-fpm-{$site->user}.sock"; $phpSocket = "unix:/run/php/php{$site->php_version}-fpm-{$site->user}.sock";
} }
@ -67,7 +71,7 @@
@if ($site->type === \App\Enums\SiteType::LOAD_BALANCER) @if ($site->type === \App\Enums\SiteType::LOAD_BALANCER)
location / { location / {
proxy_pass http://backend$request_uri; proxy_pass http://{{ $backendName }}$request_uri;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@ -23,16 +23,47 @@ public function test_create_firewall_rule(): void
'project' => $this->server->project, 'project' => $this->server->project,
'server' => $this->server, 'server' => $this->server,
]), [ ]), [
'name' => 'Test',
'type' => 'allow', 'type' => 'allow',
'protocol' => 'tcp', 'protocol' => 'tcp',
'port' => '1234', 'port' => '1234',
'source' => '0.0.0.0', 'source' => '0.0.0.0',
'mask' => '0', 'mask' => '1',
]) ])
->assertSuccessful() ->assertSuccessful()
->assertJsonFragment([ ->assertJsonFragment([
'port' => 1234, 'port' => 1234,
'status' => FirewallRuleStatus::READY, 'status' => FirewallRuleStatus::CREATING,
]);
}
public function test_edit_firewall_rule(): void
{
SSH::fake();
Sanctum::actingAs($this->user, ['read', 'write']);
$rule = FirewallRule::factory()->create([
'server_id' => $this->server->id,
'port' => 1234,
]);
$this->json('PUT', route('api.projects.servers.firewall-rules.edit', [
'project' => $this->server->project,
'server' => $this->server,
'firewallRule' => $rule,
]), [
'name' => 'Test',
'type' => 'allow',
'protocol' => 'tcp',
'port' => '55',
'source' => null,
'mask' => null,
])
->assertSuccessful()
->assertJsonFragment([
'port' => 55,
'status' => FirewallRuleStatus::UPDATING,
]); ]);
} }

View File

@ -202,22 +202,25 @@ public function test_update_env_file(): void
->assertSuccessful() ->assertSuccessful()
->assertNotified('.env updated!'); ->assertNotified('.env updated!');
SSH::assertFileUploaded('/home/vito/'.$this->site->domain.'/.env', 'APP_ENV="production"'); SSH::assertExecutedContains('tee /home/vito/vito.test/.env << \'VITO_SSH_EOF\'');
SSH::assertExecutedContains('APP_ENV="production"');
} }
public function test_git_hook_deployment(): void /**
* @dataProvider hookData
*/
public function test_git_hook_deployment(string $provider, array $webhook, string $url, array $payload, bool $skip): void
{ {
SSH::fake(); SSH::fake();
Http::fake([ Http::fake([
'github.com/*' => Http::response([ $url => Http::response($payload),
'sha' => '123', ]);
'commit' => [
'message' => 'test commit message', $this->site->update([
'name' => 'test commit name', 'branch' => 'main',
'email' => 'user@example.com', ]);
'url' => 'https://github.com', $this->site->sourceControl->update([
], 'provider' => $provider,
], 200),
]); ]);
GitHook::factory()->create([ GitHook::factory()->create([
@ -232,15 +235,29 @@ public function test_git_hook_deployment(): void
'content' => 'git pull', 'content' => 'git pull',
]); ]);
$this->post(route('api.git-hooks'), [ $this->post(route('api.git-hooks', [
'secret' => 'secret', 'secret' => 'secret',
])->assertSessionDoesntHaveErrors(); ]), $webhook)->assertSessionDoesntHaveErrors();
if ($skip) {
$this->assertDatabaseMissing('deployments', [
'site_id' => $this->site->id,
'deployment_script_id' => $this->site->deploymentScript->id,
'status' => DeploymentStatus::FINISHED,
]);
return;
}
$this->assertDatabaseHas('deployments', [ $this->assertDatabaseHas('deployments', [
'site_id' => $this->site->id, 'site_id' => $this->site->id,
'deployment_script_id' => $this->site->deploymentScript->id, 'deployment_script_id' => $this->site->deploymentScript->id,
'status' => DeploymentStatus::FINISHED, 'status' => DeploymentStatus::FINISHED,
]); ]);
$deployment = $this->site->deployments()->first();
$this->assertEquals('saeed', $deployment->commit_data['name']);
$this->assertEquals('saeed@vitodeploy.com', $deployment->commit_data['email']);
} }
public function test_git_hook_deployment_invalid_secret(): void public function test_git_hook_deployment_invalid_secret(): void
@ -270,4 +287,146 @@ public function test_git_hook_deployment_invalid_secret(): void
'status' => DeploymentStatus::FINISHED, 'status' => DeploymentStatus::FINISHED,
]); ]);
} }
public static function hookData(): array
{
return [
[
'github',
[
'ref' => 'refs/heads/main',
],
'github.com/*',
[
'sha' => '123',
'commit' => [
'committer' => [
'name' => 'saeed',
'email' => 'saeed@vitodeploy.com',
],
'message' => 'test commit message',
'url' => 'https://github.com',
],
],
false,
],
[
'github',
[
'ref' => 'refs/heads/other-branch',
],
'github.com/*',
[
'sha' => '123',
'commit' => [
'committer' => [
'name' => 'saeed',
'email' => 'saeed@vitodeploy.com',
],
'message' => 'test commit message',
'url' => 'https://github.com',
],
],
true,
],
[
'gitlab',
[
'ref' => 'main',
],
'gitlab.com/*',
[
[
'id' => '123',
'committer_name' => 'saeed',
'committer_email' => 'saeed@vitodeploy.com',
'title' => 'test',
'web_url' => 'https://gitlab.com',
],
],
false,
],
[
'gitlab',
[
'ref' => 'other-branch',
],
'gitlab.com/*',
[
[
'id' => '123',
'committer_name' => 'saeed',
'committer_email' => 'saeed@vitodeploy.com',
'title' => 'test',
'web_url' => 'https://gitlab.com',
],
],
true,
],
[
'bitbucket',
[
'push' => [
'changes' => [
[
'new' => [
'name' => 'main',
],
],
],
],
],
'bitbucket.org/*',
[
'values' => [
[
'hash' => '123',
'author' => [
'raw' => 'saeed <saeed@vitodeploy.com>',
],
'message' => 'test',
'links' => [
'html' => [
'href' => 'https://bitbucket.org',
],
],
],
],
],
false,
],
[
'bitbucket',
[
'push' => [
'changes' => [
[
'new' => [
'name' => 'other-branch',
],
],
],
],
],
'bitbucket.org/*',
[
'values' => [
[
'hash' => '123',
'author' => [
'raw' => 'saeed <saeed@vitodeploy.com>',
],
'message' => 'test',
'links' => [
'html' => [
'href' => 'https://bitbucket.org',
],
],
],
],
],
true,
],
];
}
} }

View File

@ -22,11 +22,16 @@ class DatabaseBackupTest extends TestCase
{ {
use RefreshDatabase; use RefreshDatabase;
public function test_create_backup(): void /**
* @dataProvider data
*/
public function test_create_backup(string $db): void
{ {
SSH::fake(); SSH::fake();
Http::fake(); Http::fake();
$this->setupDatabase($db);
$this->actingAs($this->user); $this->actingAs($this->user);
$database = Database::factory()->create([ $database = Database::factory()->create([
@ -152,8 +157,13 @@ public function test_update_backup(): void
]); ]);
} }
public function test_delete_backup(): void /**
* @dataProvider data
*/
public function test_delete_backup(string $db): void
{ {
$this->setupDatabase($db);
$this->actingAs($this->user); $this->actingAs($this->user);
$database = Database::factory()->create([ $database = Database::factory()->create([
@ -182,11 +192,16 @@ public function test_delete_backup(): void
]); ]);
} }
public function test_restore_backup(): void /**
* @dataProvider data
*/
public function test_restore_backup(string $db): void
{ {
Http::fake(); Http::fake();
SSH::fake(); SSH::fake();
$this->setupDatabase($db);
$this->actingAs($this->user); $this->actingAs($this->user);
$database = Database::factory()->create([ $database = Database::factory()->create([
@ -220,4 +235,24 @@ public function test_restore_backup(): void
'status' => BackupFileStatus::RESTORED, 'status' => BackupFileStatus::RESTORED,
]); ]);
} }
private function setupDatabase(string $database): void
{
$this->server->services()->where('type', 'database')->delete();
$this->server->services()->create([
'type' => 'database',
'name' => config('core.databases_name.'.$database),
'version' => config('core.databases_version.'.$database),
]);
}
public static function data(): array
{
return [
[\App\Enums\Database::MYSQL80],
[\App\Enums\Database::MARIADB104],
[\App\Enums\Database::POSTGRESQL16],
];
}
} }

View File

@ -0,0 +1,138 @@
<?php
namespace Tests\Feature;
use App\Facades\SSH;
use App\Models\File;
use App\Web\Pages\Servers\FileManager\Index;
use App\Web\Pages\Servers\FileManager\Widgets\FilesList;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Livewire\Livewire;
use Tests\TestCase;
class FileManagerTest extends TestCase
{
use RefreshDatabase;
public function test_see_files(): void
{
SSH::fake(<<<'EOF'
total 32
drwxr-xr-x 7 vito vito 4096 Feb 2 19:42 .
drwxr-xr-x 3 root root 4096 Feb 1 18:44 ..
drwx------ 3 vito vito 4096 Feb 1 18:45 .cache
drwxrwxr-x 3 vito vito 4096 Feb 1 18:45 .config
-rw-rw-r-- 1 vito vito 82 Feb 2 14:13 .gitconfig
drwxrwxr-x 3 vito vito 4096 Feb 1 18:45 .local
drwxr-xr-x 2 vito vito 4096 Feb 2 14:13 .ssh
drwxrwxr-x 3 vito vito 4096 Feb 2 21:25 test.vitodeploy.com
EOF
);
$this->actingAs($this->user);
$this->get(
Index::getUrl([
'server' => $this->server,
])
)
->assertSuccessful()
->assertSee('.cache')
->assertSee('.config');
}
public function test_upload_file(): void
{
SSH::fake();
$this->actingAs($this->user);
Livewire::test(FilesList::class, [
'server' => $this->server,
])
->callTableAction('upload', null, [
'file' => UploadedFile::fake()->create('test.txt'),
])
->assertSuccessful();
}
public function test_create_file(): void
{
SSH::fake(<<<'EOF'
total 3
drwxr-xr-x 7 vito vito 4096 Feb 2 19:42 .
drwxr-xr-x 3 root root 4096 Feb 1 18:44 ..
-rw-rw-r-- 1 vito vito 82 Feb 2 14:13 test.txt
EOF
);
$this->actingAs($this->user);
Livewire::test(FilesList::class, [
'server' => $this->server,
])
->callTableAction('new-file', null, [
'name' => 'test.txt',
'content' => 'Hello, world!',
])
->assertSuccessful();
$this->assertDatabaseHas('files', [
'name' => 'test.txt',
]);
}
public function test_create_directory(): void
{
SSH::fake(<<<'EOF'
total 3
drwxr-xr-x 7 vito vito 4096 Feb 2 19:42 .
drwxr-xr-x 3 root root 4096 Feb 1 18:44 ..
drwxr-xr-x 2 vito vito 4096 Feb 2 14:13 test
EOF
);
$this->actingAs($this->user);
Livewire::test(FilesList::class, [
'server' => $this->server,
])
->callTableAction('new-directory', null, [
'name' => 'test',
])
->assertSuccessful();
$this->assertDatabaseHas('files', [
'name' => 'test',
]);
}
public function test_download_file(): void
{
SSH::fake(<<<'EOF'
total 3
drwxr-xr-x 7 vito vito 4096 Feb 2 19:42 .
drwxr-xr-x 3 root root 4096 Feb 1 18:44 ..
-rw-rw-r-- 1 vito vito 82 Feb 2 14:13 test.txt
EOF
);
$this->actingAs($this->user);
$this->get(
Index::getUrl([
'server' => $this->server,
])
)->assertSuccessful();
$file = File::query()->where('name', 'test.txt')->firstOrFail();
Livewire::test(FilesList::class, [
'server' => $this->server,
])
->assertTableActionVisible('download', $file)
->callTableAction('download', $file)
->assertSuccessful();
}
}

View File

@ -25,6 +25,7 @@ public function test_create_firewall_rule(): void
'server' => $this->server, 'server' => $this->server,
]) ])
->callAction('create', [ ->callAction('create', [
'name' => 'Test',
'type' => 'allow', 'type' => 'allow',
'protocol' => 'tcp', 'protocol' => 'tcp',
'port' => '1234', 'port' => '1234',