Compare commits

..

19 Commits
1.3.0 ... 1.5.0

Author SHA1 Message Date
e704a13d6b new toast notification ui (#188) 2024-05-03 10:40:01 +02:00
9936958259 local storage driver & some icon fixes (#187) 2024-05-01 22:54:44 +02:00
f81d928c66 Update README.md 2024-05-01 12:42:16 +02:00
3c4435701d fix deployment button ux (#186) 2024-05-01 09:34:01 +02:00
ebbd81348a build assets 2024-04-29 21:14:32 +02:00
5debbd4f5d update project lowercase 2024-04-29 21:11:07 +02:00
d846acaa8d User management (#185) 2024-04-29 20:58:04 +02:00
35f896eab1 Update feature_request.md
fix #179
2024-04-24 14:40:26 +02:00
25977d2ead move projects from sidebar to topbar (#170)
and fix #169
2024-04-23 21:34:39 +02:00
f0da1c6d8c Accurate deployment statuses (#168) 2024-04-21 16:26:26 +02:00
e2dd9177f7 fix number format 2024-04-17 22:08:19 +02:00
5a9e8d6799 fix monitoring numbers 2024-04-17 21:09:09 +02:00
868b70f530 add cron to docker 2024-04-17 17:14:12 +02:00
d07e9bcad2 remote monitor (#167) 2024-04-17 16:03:06 +02:00
0cd815cce6 ui fix and build 2024-04-14 18:17:44 +02:00
5ab6617b5d fix read file 2024-04-14 17:53:08 +02:00
72b37c56fd ui fix 2024-04-14 14:53:58 +02:00
8a4ef66946 update Feature/add remote server logs (#166) 2024-04-14 14:41:00 +02:00
4517ca7d2a Feature/add remote server logs (#159) 2024-04-14 14:34:47 +02:00
212 changed files with 4576 additions and 3348 deletions

View File

@ -9,4 +9,4 @@
To request a feature or suggest an idea please add it to the feedback boards To request a feature or suggest an idea please add it to the feedback boards
https://features.vitodeploy.com/ https://vitodeploy.featurebase.app/

View File

@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<img alt="srcshot 2024-02-23 at 16 26 21@2x" src="https://github.com/vitodeploy/vito/assets/61919774/9b3ae8fe-996a-4e10-b42e-74097f8e5512" alt="VitoDeploy> <img src="https://github.com/vitodeploy/vito/assets/61919774/8060fded-58e3-4d58-b58b-5b717b0718e9" alt="VitoDeploy>
<p align="center"> <p align="center">
<a href="https://github.com/vitodeploy/vito/actions"><img alt="GitHub Workflow Status" src="https://github.com/vitodeploy/vito/workflows/tests/badge.svg"></a> <a href="https://github.com/vitodeploy/vito/actions"><img alt="GitHub Workflow Status" src="https://github.com/vitodeploy/vito/workflows/tests/badge.svg"></a>
</p> </p>
@ -50,8 +50,6 @@ ## Credits
- Alpinejs - Alpinejs
- HTMX - HTMX
- Vite - Vite
- Toastr by CodeSeven
- Prettier - Prettier
- Postcss - Postcss
- Flowbite - Flowbite
- svgrepo.com

View File

@ -10,10 +10,13 @@ class CreateProject
{ {
public function create(User $user, array $input): Project public function create(User $user, array $input): Project
{ {
if (isset($input['name'])) {
$input['name'] = strtolower($input['name']);
}
$this->validate($user, $input); $this->validate($user, $input);
$project = new Project([ $project = new Project([
'user_id' => $user->id,
'name' => $input['name'], 'name' => $input['name'],
]); ]);
@ -29,7 +32,7 @@ private function validate(User $user, array $input): void
'required', 'required',
'string', 'string',
'max:255', 'max:255',
'unique:projects,name,NULL,id,user_id,'.$user->id, 'unique:projects,name',
], ],
])->validate(); ])->validate();
} }

View File

@ -10,6 +10,10 @@ class UpdateProject
{ {
public function update(Project $project, array $input): Project public function update(Project $project, array $input): Project
{ {
if (isset($input['name'])) {
$input['name'] = strtolower($input['name']);
}
$this->validate($project, $input); $this->validate($project, $input);
$project->name = $input['name']; $project->name = $input['name'];

View File

@ -0,0 +1,35 @@
<?php
namespace App\Actions\Server;
use App\Models\Server;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class CreateServerLog
{
/**
* @throws ValidationException
*/
public function create(Server $server, array $input): void
{
$this->validate($input);
$server->logs()->create([
'is_remote' => true,
'name' => $input['path'],
'type' => 'remote',
'disk' => 'ssh',
]);
}
/**
* @throws ValidationException
*/
protected function validate(array $input): void
{
Validator::make($input, [
'path' => 'required',
])->validate();
}
}

View File

@ -6,6 +6,7 @@
use App\Exceptions\DeploymentScriptIsEmptyException; use App\Exceptions\DeploymentScriptIsEmptyException;
use App\Exceptions\SourceControlIsNotConnected; use App\Exceptions\SourceControlIsNotConnected;
use App\Models\Deployment; use App\Models\Deployment;
use App\Models\ServerLog;
use App\Models\Site; use App\Models\Site;
class Deploy class Deploy
@ -37,10 +38,15 @@ public function run(Site $site): Deployment
$deployment->save(); $deployment->save();
dispatch(function () use ($site, $deployment) { dispatch(function () use ($site, $deployment) {
$log = $site->server->os()->runScript($site->path, $site->deploymentScript->content, $site->id); /** @var ServerLog $log */
$deployment->status = DeploymentStatus::FINISHED; $log = ServerLog::make($site->server, 'deploy-'.strtotime('now'))
->forSite($site);
$log->save();
$deployment->log_id = $log->id; $deployment->log_id = $log->id;
$deployment->save(); $deployment->save();
$site->server->os()->runScript($site->path, $site->deploymentScript->content, $log);
$deployment->status = DeploymentStatus::FINISHED;
$deployment->save();
})->catch(function () use ($deployment) { })->catch(function () use ($deployment) {
$deployment->status = DeploymentStatus::FAILED; $deployment->status = DeploymentStatus::FAILED;
$deployment->save(); $deployment->save();

View File

@ -0,0 +1,40 @@
<?php
namespace App\Actions\User;
use App\Enums\UserRole;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class CreateUser
{
public function create(array $input): User
{
$this->validate($input);
/** @var User $user */
$user = User::query()->create([
'name' => $input['name'],
'email' => $input['email'],
'role' => $input['role'],
'password' => bcrypt($input['password']),
'timezone' => 'UTC',
]);
return $user;
}
private function validate(array $input): void
{
Validator::make($input, [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'password' => 'required|string|min:8',
'role' => [
'required',
Rule::in([UserRole::ADMIN, UserRole::USER]),
],
])->validate();
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Actions\User;
use App\Enums\UserRole;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class UpdateUser
{
public function update(User $user, array $input): void
{
$this->validate($user, $input);
$user->name = $input['name'];
$user->email = $input['email'];
$user->timezone = $input['timezone'];
$user->role = $input['role'];
if (isset($input['password']) && $input['password'] !== null) {
$user->password = bcrypt($input['password']);
}
$user->save();
}
private function validate(User $user, array $input): void
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
'timezone' => [
'required',
Rule::in(timezone_identifiers_list()),
],
'role' => [
'required',
Rule::in([UserRole::ADMIN, UserRole::USER]),
function ($attribute, $value, $fail) use ($user) {
if ($user->is(auth()->user()) && $value !== $user->role) {
$fail('You cannot change your own role');
}
},
],
])->validate();
}
}

View File

@ -7,7 +7,7 @@
class CreateUserCommand extends Command class CreateUserCommand extends Command
{ {
protected $signature = 'user:create {name} {email} {password}'; protected $signature = 'user:create {name} {email} {password} {--role=admin}';
protected $description = 'Create a new user'; protected $description = 'Create a new user';
@ -25,6 +25,7 @@ public function handle(): void
'name' => $this->argument('name'), 'name' => $this->argument('name'),
'email' => $this->argument('email'), 'email' => $this->argument('email'),
'password' => bcrypt($this->argument('password')), 'password' => bcrypt($this->argument('password')),
'role' => $this->option('role'),
]); ]);
$this->info('User created!'); $this->info('User created!');

View File

@ -0,0 +1,31 @@
<?php
namespace App\Console\Commands;
use App\Models\Server;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
class GetMetricsCommand extends Command
{
protected $signature = 'metrics:get';
protected $description = 'Get server metrics';
public function handle(): void
{
$checkedMetrics = 0;
Server::query()->whereHas('services', function (Builder $query) {
$query->where('type', 'monitoring')
->where('name', 'remote-monitor');
})->chunk(10, function ($servers) use (&$checkedMetrics) {
/** @var Server $server */
foreach ($servers as $server) {
$info = $server->os()->resourceInfo();
$server->metrics()->create(array_merge($info, ['server_id' => $server->id]));
$checkedMetrics++;
}
});
$this->info("Checked $checkedMetrics metrics");
}
}

View File

@ -17,6 +17,7 @@ protected function schedule(Schedule $schedule): void
$schedule->command('backups:run "0 0 * * 0"')->weekly(); $schedule->command('backups:run "0 0 * * 0"')->weekly();
$schedule->command('backups:run "0 0 1 * *"')->monthly(); $schedule->command('backups:run "0 0 1 * *"')->monthly();
$schedule->command('metrics:delete-older-metrics')->daily(); $schedule->command('metrics:delete-older-metrics')->daily();
$schedule->command('metrics:get')->everyMinute();
} }
/** /**

View File

@ -7,4 +7,6 @@ final class StorageProvider
const DROPBOX = 'dropbox'; const DROPBOX = 'dropbox';
const FTP = 'ftp'; const FTP = 'ftp';
const LOCAL = 'local';
} }

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

@ -0,0 +1,10 @@
<?php
namespace App\Enums;
final class UserRole
{
const USER = 'user';
const ADMIN = 'admin';
}

View File

@ -3,6 +3,7 @@
namespace App\Facades; namespace App\Facades;
use App\Models\Server; use App\Models\Server;
use App\Models\ServerLog;
use App\Support\Testing\SSHFake; use App\Support\Testing\SSHFake;
use Illuminate\Support\Facades\Facade as FacadeAlias; use Illuminate\Support\Facades\Facade as FacadeAlias;
@ -10,7 +11,7 @@
* Class SSH * Class SSH
* *
* @method static init(Server $server, string $asUser = null) * @method static init(Server $server, string $asUser = null)
* @method static setLog(string $logType, int $siteId = null) * @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) * @method static string exec(string $command, string $log = '', int $siteId = null, ?bool $stream = false)
* @method static string assertExecuted(array|string $commands) * @method static string assertExecuted(array|string $commands)

View File

@ -50,14 +50,9 @@ public function init(Server $server, ?string $asUser = null): self
return $this; return $this;
} }
public function setLog(string $logType, $siteId = null): self public function setLog(ServerLog $log): self
{ {
$this->log = $this->server->logs()->create([ $this->log = $log;
'site_id' => $siteId,
'name' => $this->server->id.'-'.strtotime('now').'-'.$logType.'.log',
'type' => $logType,
'disk' => config('core.logs_disk'),
]);
return $this; return $this;
} }
@ -98,10 +93,18 @@ public function connect(bool $sftp = false): void
*/ */
public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false): string public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false): string
{ {
if ($log) { if (! $this->log && $log) {
$this->setLog($log, $siteId); $this->log = $this->server->logs()->create([
} else { 'site_id' => $siteId,
$this->log = null; 'name' => $this->server->id.'-'.strtotime('now').'-'.$log.'.log',
'type' => $log,
'disk' => config('core.logs_disk'),
]);
$this->log = ServerLog::make($this->server, $log);
if ($siteId) {
$this->log->forSite($siteId);
}
$this->log->save();
} }
try { try {
@ -132,8 +135,8 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
$this->log?->write($output); $this->log?->write($output);
if (Str::contains($output, 'VITO_SSH_ERROR')) { if ($this->connection->getExitStatus() !== 0 || Str::contains($output, 'VITO_SSH_ERROR')) {
throw new SSHCommandError('SSH command failed with an error'); throw new SSHCommandError('SSH command failed with an error', $this->connection->getExitStatus());
} }
return $output; return $output;

View File

@ -22,6 +22,8 @@ class ApplicationController extends Controller
{ {
public function deploy(Server $server, Site $site): HtmxResponse public function deploy(Server $server, Site $site): HtmxResponse
{ {
$this->authorize('manage', $server);
try { try {
app(Deploy::class)->run($site); app(Deploy::class)->run($site);
@ -41,11 +43,15 @@ public function deploy(Server $server, Site $site): HtmxResponse
public function showDeploymentLog(Server $server, Site $site, Deployment $deployment): RedirectResponse public function showDeploymentLog(Server $server, Site $site, Deployment $deployment): RedirectResponse
{ {
$this->authorize('manage', $server);
return back()->with('content', $deployment->log?->getContent()); return back()->with('content', $deployment->log?->getContent());
} }
public function updateDeploymentScript(Server $server, Site $site, Request $request): RedirectResponse public function updateDeploymentScript(Server $server, Site $site, Request $request): RedirectResponse
{ {
$this->authorize('manage', $server);
app(UpdateDeploymentScript::class)->update($site, $request->input()); app(UpdateDeploymentScript::class)->update($site, $request->input());
Toast::success('Deployment script updated!'); Toast::success('Deployment script updated!');
@ -55,6 +61,8 @@ public function updateDeploymentScript(Server $server, Site $site, Request $requ
public function updateBranch(Server $server, Site $site, Request $request): RedirectResponse public function updateBranch(Server $server, Site $site, Request $request): RedirectResponse
{ {
$this->authorize('manage', $server);
app(UpdateBranch::class)->update($site, $request->input()); app(UpdateBranch::class)->update($site, $request->input());
Toast::success('Branch updated!'); Toast::success('Branch updated!');
@ -64,11 +72,15 @@ public function updateBranch(Server $server, Site $site, Request $request): Redi
public function getEnv(Server $server, Site $site): RedirectResponse public function getEnv(Server $server, Site $site): RedirectResponse
{ {
$this->authorize('manage', $server);
return back()->with('env', $site->getEnv()); return back()->with('env', $site->getEnv());
} }
public function updateEnv(Server $server, Site $site, Request $request): RedirectResponse public function updateEnv(Server $server, Site $site, Request $request): RedirectResponse
{ {
$this->authorize('manage', $server);
app(UpdateEnv::class)->update($site, $request->input()); app(UpdateEnv::class)->update($site, $request->input());
Toast::success('Env updated!'); Toast::success('Env updated!');
@ -78,6 +90,8 @@ public function updateEnv(Server $server, Site $site, Request $request): Redirec
public function enableAutoDeployment(Server $server, Site $site): HtmxResponse public function enableAutoDeployment(Server $server, Site $site): HtmxResponse
{ {
$this->authorize('manage', $server);
if (! $site->isAutoDeployment()) { if (! $site->isAutoDeployment()) {
try { try {
$site->enableAutoDeployment(); $site->enableAutoDeployment();
@ -101,6 +115,8 @@ public function enableAutoDeployment(Server $server, Site $site): HtmxResponse
public function disableAutoDeployment(Server $server, Site $site): HtmxResponse public function disableAutoDeployment(Server $server, Site $site): HtmxResponse
{ {
$this->authorize('manage', $server);
if ($site->isAutoDeployment()) { if ($site->isAutoDeployment()) {
try { try {
$site->disableAutoDeployment(); $site->disableAutoDeployment();

View File

@ -11,6 +11,8 @@ class ConsoleController extends Controller
{ {
public function index(Server $server): View public function index(Server $server): View
{ {
$this->authorize('manage', $server);
return view('console.index', [ return view('console.index', [
'server' => $server, 'server' => $server,
]); ]);
@ -18,6 +20,8 @@ public function index(Server $server): View
public function run(Server $server, Request $request) public function run(Server $server, Request $request)
{ {
$this->authorize('manage', $server);
$this->validate($request, [ $this->validate($request, [
'user' => [ 'user' => [
'required', 'required',

View File

@ -16,6 +16,8 @@ class CronjobController extends Controller
{ {
public function index(Server $server): View public function index(Server $server): View
{ {
$this->authorize('manage', $server);
return view('cronjobs.index', [ return view('cronjobs.index', [
'server' => $server, 'server' => $server,
'cronjobs' => $server->cronJobs, 'cronjobs' => $server->cronJobs,
@ -24,6 +26,8 @@ public function index(Server $server): View
public function store(Server $server, Request $request): HtmxResponse public function store(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
app(CreateCronJob::class)->create($server, $request->input()); app(CreateCronJob::class)->create($server, $request->input());
Toast::success('Cronjob created successfully.'); Toast::success('Cronjob created successfully.');
@ -33,6 +37,8 @@ public function store(Server $server, Request $request): HtmxResponse
public function destroy(Server $server, CronJob $cronJob): RedirectResponse public function destroy(Server $server, CronJob $cronJob): RedirectResponse
{ {
$this->authorize('manage', $server);
app(DeleteCronJob::class)->delete($server, $cronJob); app(DeleteCronJob::class)->delete($server, $cronJob);
Toast::success('Cronjob deleted successfully.'); Toast::success('Cronjob deleted successfully.');

View File

@ -18,6 +18,8 @@ class DatabaseBackupController extends Controller
{ {
public function show(Server $server, Backup $backup): View public function show(Server $server, Backup $backup): View
{ {
$this->authorize('manage', $server);
return view('databases.backups', [ return view('databases.backups', [
'server' => $server, 'server' => $server,
'databases' => $server->databases, 'databases' => $server->databases,
@ -28,6 +30,8 @@ public function show(Server $server, Backup $backup): View
public function run(Server $server, Backup $backup): RedirectResponse public function run(Server $server, Backup $backup): RedirectResponse
{ {
$this->authorize('manage', $server);
app(RunBackup::class)->run($backup); app(RunBackup::class)->run($backup);
Toast::success('Backup is running.'); Toast::success('Backup is running.');
@ -37,6 +41,8 @@ public function run(Server $server, Backup $backup): RedirectResponse
public function store(Server $server, Request $request): HtmxResponse public function store(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
app(CreateBackup::class)->create('database', $server, $request->input()); app(CreateBackup::class)->create('database', $server, $request->input());
Toast::success('Backup created successfully.'); Toast::success('Backup created successfully.');
@ -46,6 +52,8 @@ public function store(Server $server, Request $request): HtmxResponse
public function destroy(Server $server, Backup $backup): RedirectResponse public function destroy(Server $server, Backup $backup): RedirectResponse
{ {
$this->authorize('manage', $server);
$backup->delete(); $backup->delete();
Toast::success('Backup deleted successfully.'); Toast::success('Backup deleted successfully.');
@ -55,6 +63,8 @@ public function destroy(Server $server, Backup $backup): RedirectResponse
public function restore(Server $server, Backup $backup, BackupFile $backupFile, Request $request): HtmxResponse public function restore(Server $server, Backup $backup, BackupFile $backupFile, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
app(RestoreBackup::class)->restore($backupFile, $request->input()); app(RestoreBackup::class)->restore($backupFile, $request->input());
Toast::success('Backup restored successfully.'); Toast::success('Backup restored successfully.');
@ -64,8 +74,17 @@ public function restore(Server $server, Backup $backup, BackupFile $backupFile,
public function destroyFile(Server $server, Backup $backup, BackupFile $backupFile): RedirectResponse public function destroyFile(Server $server, Backup $backup, BackupFile $backupFile): RedirectResponse
{ {
$this->authorize('manage', $server);
$backupFile->delete(); $backupFile->delete();
$backupFile
->backup
->storage
->provider()
->ssh($server)
->delete($backupFile->storagePath());
Toast::success('Backup file deleted successfully.'); Toast::success('Backup file deleted successfully.');
return back(); return back();

View File

@ -17,6 +17,8 @@ class DatabaseController extends Controller
{ {
public function index(Server $server): View public function index(Server $server): View
{ {
$this->authorize('manage', $server);
return view('databases.index', [ return view('databases.index', [
'server' => $server, 'server' => $server,
'databases' => $server->databases, 'databases' => $server->databases,
@ -27,6 +29,8 @@ public function index(Server $server): View
public function store(Server $server, Request $request): HtmxResponse public function store(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
$database = app(CreateDatabase::class)->create($server, $request->input()); $database = app(CreateDatabase::class)->create($server, $request->input());
if ($request->input('user')) { if ($request->input('user')) {
@ -40,6 +44,8 @@ public function store(Server $server, Request $request): HtmxResponse
public function destroy(Server $server, Database $database): RedirectResponse public function destroy(Server $server, Database $database): RedirectResponse
{ {
$this->authorize('manage', $server);
app(DeleteDatabase::class)->delete($server, $database); app(DeleteDatabase::class)->delete($server, $database);
Toast::success('Database deleted successfully.'); Toast::success('Database deleted successfully.');

View File

@ -16,6 +16,8 @@ class DatabaseUserController extends Controller
{ {
public function store(Server $server, Request $request): HtmxResponse public function store(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
$database = app(CreateDatabaseUser::class)->create($server, $request->input()); $database = app(CreateDatabaseUser::class)->create($server, $request->input());
if ($request->input('user')) { if ($request->input('user')) {
@ -29,6 +31,8 @@ public function store(Server $server, Request $request): HtmxResponse
public function destroy(Server $server, DatabaseUser $databaseUser): RedirectResponse public function destroy(Server $server, DatabaseUser $databaseUser): RedirectResponse
{ {
$this->authorize('manage', $server);
app(DeleteDatabaseUser::class)->delete($server, $databaseUser); app(DeleteDatabaseUser::class)->delete($server, $databaseUser);
Toast::success('User deleted successfully.'); Toast::success('User deleted successfully.');
@ -38,6 +42,8 @@ public function destroy(Server $server, DatabaseUser $databaseUser): RedirectRes
public function password(Server $server, DatabaseUser $databaseUser): RedirectResponse public function password(Server $server, DatabaseUser $databaseUser): RedirectResponse
{ {
$this->authorize('manage', $server);
return back()->with([ return back()->with([
'password' => $databaseUser->password, 'password' => $databaseUser->password,
]); ]);
@ -45,6 +51,8 @@ public function password(Server $server, DatabaseUser $databaseUser): RedirectRe
public function link(Server $server, DatabaseUser $databaseUser, Request $request): HtmxResponse public function link(Server $server, DatabaseUser $databaseUser, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
app(LinkUser::class)->link($databaseUser, $request->input()); app(LinkUser::class)->link($databaseUser, $request->input());
Toast::success('Database linked successfully.'); Toast::success('Database linked successfully.');

View File

@ -16,6 +16,8 @@ class FirewallController extends Controller
{ {
public function index(Server $server): View public function index(Server $server): View
{ {
$this->authorize('manage', $server);
return view('firewall.index', [ return view('firewall.index', [
'server' => $server, 'server' => $server,
'rules' => $server->firewallRules, 'rules' => $server->firewallRules,
@ -24,6 +26,8 @@ public function index(Server $server): View
public function store(Server $server, Request $request): HtmxResponse public function store(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
app(CreateRule::class)->create($server, $request->input()); app(CreateRule::class)->create($server, $request->input());
Toast::success('Firewall rule created!'); Toast::success('Firewall rule created!');
@ -33,6 +37,8 @@ public function store(Server $server, Request $request): HtmxResponse
public function destroy(Server $server, FirewallRule $firewallRule): RedirectResponse public function destroy(Server $server, FirewallRule $firewallRule): RedirectResponse
{ {
$this->authorize('manage', $server);
app(DeleteRule::class)->delete($server, $firewallRule); app(DeleteRule::class)->delete($server, $firewallRule);
Toast::success('Firewall rule deleted!'); Toast::success('Firewall rule deleted!');

View File

@ -15,6 +15,8 @@ class MetricController extends Controller
{ {
public function index(Server $server, Request $request): View|RedirectResponse public function index(Server $server, Request $request): View|RedirectResponse
{ {
$this->authorize('manage', $server);
$this->checkIfMonitoringServiceInstalled($server); $this->checkIfMonitoringServiceInstalled($server);
return view('metrics.index', [ return view('metrics.index', [
@ -26,6 +28,8 @@ public function index(Server $server, Request $request): View|RedirectResponse
public function settings(Server $server, Request $request): HtmxResponse public function settings(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
$this->checkIfMonitoringServiceInstalled($server); $this->checkIfMonitoringServiceInstalled($server);
app(UpdateMetricSettings::class)->update($server, $request->input()); app(UpdateMetricSettings::class)->update($server, $request->input());
@ -37,6 +41,8 @@ public function settings(Server $server, Request $request): HtmxResponse
private function checkIfMonitoringServiceInstalled(Server $server): void private function checkIfMonitoringServiceInstalled(Server $server): void
{ {
$this->authorize('manage', $server);
if (! $server->monitoring()) { if (! $server->monitoring()) {
abort(404, 'Monitoring service is not installed on this server'); abort(404, 'Monitoring service is not installed on this server');
} }

View File

@ -20,6 +20,8 @@ class PHPController extends Controller
{ {
public function index(Server $server): View public function index(Server $server): View
{ {
$this->authorize('manage', $server);
return view('php.index', [ return view('php.index', [
'server' => $server, 'server' => $server,
'phps' => $server->services()->where('type', 'php')->get(), 'phps' => $server->services()->where('type', 'php')->get(),
@ -29,6 +31,8 @@ public function index(Server $server): View
public function install(Server $server, Request $request): HtmxResponse public function install(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
try { try {
app(InstallNewPHP::class)->install($server, $request->input()); app(InstallNewPHP::class)->install($server, $request->input());
@ -42,6 +46,8 @@ public function install(Server $server, Request $request): HtmxResponse
public function installExtension(Server $server, Request $request): HtmxResponse public function installExtension(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
app(InstallPHPExtension::class)->install($server, $request->input()); app(InstallPHPExtension::class)->install($server, $request->input());
Toast::success('PHP extension is being installed! Check the logs'); Toast::success('PHP extension is being installed! Check the logs');
@ -51,6 +57,8 @@ public function installExtension(Server $server, Request $request): HtmxResponse
public function defaultCli(Server $server, Request $request): HtmxResponse public function defaultCli(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
app(ChangeDefaultCli::class)->change($server, $request->input()); app(ChangeDefaultCli::class)->change($server, $request->input());
Toast::success('Default PHP CLI is being changed!'); Toast::success('Default PHP CLI is being changed!');
@ -60,6 +68,8 @@ public function defaultCli(Server $server, Request $request): HtmxResponse
public function getIni(Server $server, Request $request): RedirectResponse public function getIni(Server $server, Request $request): RedirectResponse
{ {
$this->authorize('manage', $server);
$ini = app(GetPHPIni::class)->getIni($server, $request->input()); $ini = app(GetPHPIni::class)->getIni($server, $request->input());
return back()->with('ini', $ini); return back()->with('ini', $ini);
@ -67,6 +77,8 @@ public function getIni(Server $server, Request $request): RedirectResponse
public function updateIni(Server $server, Request $request): RedirectResponse public function updateIni(Server $server, Request $request): RedirectResponse
{ {
$this->authorize('manage', $server);
app(UpdatePHPIni::class)->update($server, $request->input()); app(UpdatePHPIni::class)->update($server, $request->input());
Toast::success('PHP ini updated!'); Toast::success('PHP ini updated!');
@ -78,6 +90,8 @@ public function updateIni(Server $server, Request $request): RedirectResponse
public function uninstall(Server $server, Request $request): RedirectResponse public function uninstall(Server $server, Request $request): RedirectResponse
{ {
$this->authorize('manage', $server);
app(UninstallPHP::class)->uninstall($server, $request->input()); app(UninstallPHP::class)->uninstall($server, $request->input());
Toast::success('PHP is being uninstalled!'); Toast::success('PHP is being uninstalled!');

View File

@ -1,11 +1,10 @@
<?php <?php
namespace App\Http\Controllers\Settings; namespace App\Http\Controllers;
use App\Actions\User\UpdateUserPassword; use App\Actions\User\UpdateUserPassword;
use App\Actions\User\UpdateUserProfileInformation; use App\Actions\User\UpdateUserProfileInformation;
use App\Facades\Toast; use App\Facades\Toast;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -14,7 +13,7 @@ class ProfileController extends Controller
{ {
public function index(): View public function index(): View
{ {
return view('settings.profile.index'); return view('profile.index');
} }
public function info(Request $request): RedirectResponse public function info(Request $request): RedirectResponse

View File

@ -19,6 +19,8 @@ class QueueController extends Controller
{ {
public function index(Server $server, Site $site): View public function index(Server $server, Site $site): View
{ {
$this->authorize('manage', $server);
return view('queues.index', [ return view('queues.index', [
'server' => $server, 'server' => $server,
'site' => $site, 'site' => $site,
@ -28,6 +30,8 @@ public function index(Server $server, Site $site): View
public function store(Server $server, Site $site, Request $request): HtmxResponse public function store(Server $server, Site $site, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
app(CreateQueue::class)->create($site, $request->input()); app(CreateQueue::class)->create($site, $request->input());
Toast::success('Queue is being created.'); Toast::success('Queue is being created.');
@ -37,6 +41,8 @@ public function store(Server $server, Site $site, Request $request): HtmxRespons
public function action(Server $server, Site $site, Queue $queue, string $action): HtmxResponse public function action(Server $server, Site $site, Queue $queue, string $action): HtmxResponse
{ {
$this->authorize('manage', $server);
app(ManageQueue::class)->{$action}($queue); app(ManageQueue::class)->{$action}($queue);
Toast::success('Queue is about to '.$action); Toast::success('Queue is about to '.$action);
@ -46,6 +52,8 @@ public function action(Server $server, Site $site, Queue $queue, string $action)
public function destroy(Server $server, Site $site, Queue $queue): RedirectResponse public function destroy(Server $server, Site $site, Queue $queue): RedirectResponse
{ {
$this->authorize('manage', $server);
app(DeleteQueue::class)->delete($queue); app(DeleteQueue::class)->delete($queue);
Toast::success('Queue is being deleted.'); Toast::success('Queue is being deleted.');
@ -55,6 +63,8 @@ public function destroy(Server $server, Site $site, Queue $queue): RedirectRespo
public function logs(Server $server, Site $site, Queue $queue): RedirectResponse public function logs(Server $server, Site $site, Queue $queue): RedirectResponse
{ {
$this->authorize('manage', $server);
return back()->with('content', app(GetQueueLogs::class)->getLogs($queue)); return back()->with('content', app(GetQueueLogs::class)->getLogs($queue));
} }
} }

View File

@ -17,6 +17,8 @@ class SSHKeyController extends Controller
{ {
public function index(Server $server): View public function index(Server $server): View
{ {
$this->authorize('manage', $server);
return view('server-ssh-keys.index', [ return view('server-ssh-keys.index', [
'server' => $server, 'server' => $server,
'keys' => $server->sshKeys, 'keys' => $server->sshKeys,
@ -25,6 +27,8 @@ public function index(Server $server): View
public function store(Server $server, Request $request): HtmxResponse public function store(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
/** @var \App\Models\SshKey $key */ /** @var \App\Models\SshKey $key */
$key = app(CreateSshKey::class)->create( $key = app(CreateSshKey::class)->create(
$request->user(), $request->user(),
@ -38,6 +42,8 @@ public function store(Server $server, Request $request): HtmxResponse
public function destroy(Server $server, SshKey $sshKey): RedirectResponse public function destroy(Server $server, SshKey $sshKey): RedirectResponse
{ {
$this->authorize('manage', $server);
app(DeleteKeyFromServer::class)->delete($server, $sshKey); app(DeleteKeyFromServer::class)->delete($server, $sshKey);
Toast::success('SSH Key has been deleted.'); Toast::success('SSH Key has been deleted.');
@ -47,6 +53,8 @@ public function destroy(Server $server, SshKey $sshKey): RedirectResponse
public function deploy(Server $server, Request $request): HtmxResponse public function deploy(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
app(DeployKeyToServer::class)->deploy( app(DeployKeyToServer::class)->deploy(
$request->user(), $request->user(),
$server, $server,

View File

@ -17,6 +17,8 @@ class SSLController extends Controller
{ {
public function index(Server $server, Site $site): View public function index(Server $server, Site $site): View
{ {
$this->authorize('manage', $server);
return view('ssls.index', [ return view('ssls.index', [
'server' => $server, 'server' => $server,
'site' => $site, 'site' => $site,
@ -26,6 +28,8 @@ public function index(Server $server, Site $site): View
public function store(Server $server, Site $site, Request $request): HtmxResponse public function store(Server $server, Site $site, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
app(CreateSSL::class)->create($site, $request->input()); app(CreateSSL::class)->create($site, $request->input());
Toast::success('SSL certificate is being created.'); Toast::success('SSL certificate is being created.');
@ -35,6 +39,8 @@ public function store(Server $server, Site $site, Request $request): HtmxRespons
public function destroy(Server $server, Site $site, Ssl $ssl): RedirectResponse public function destroy(Server $server, Site $site, Ssl $ssl): RedirectResponse
{ {
$this->authorize('manage', $server);
app(DeleteSSL::class)->delete($ssl); app(DeleteSSL::class)->delete($ssl);
Toast::success('SSL certificate has been deleted.'); Toast::success('SSL certificate has been deleted.');

View File

@ -4,6 +4,7 @@
use App\Models\Server; use App\Models\Server;
use App\Models\Site; use App\Models\Site;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -23,10 +24,22 @@ public function search(Request $request): JsonResponse
$query->where('name', 'like', '%'.$request->input('q').'%') $query->where('name', 'like', '%'.$request->input('q').'%')
->orWhere('ip', 'like', '%'.$request->input('q').'%'); ->orWhere('ip', 'like', '%'.$request->input('q').'%');
}) })
->whereHas('project', function (Builder $projectQuery) {
$projectQuery->whereHas('users', function (Builder $userQuery) {
$userQuery->where('user_id', auth()->user()->id);
});
})
->get(); ->get();
$sites = Site::query() $sites = Site::query()
->where('domain', 'like', '%'.$request->input('q').'%') ->where('domain', 'like', '%'.$request->input('q').'%')
->whereHas('server', function (Builder $serverQuery) {
$serverQuery->whereHas('project', function (Builder $projectQuery) {
$projectQuery->whereHas('users', function (Builder $userQuery) {
$userQuery->where('user_id', auth()->user()->id);
});
});
})
->get(); ->get();
$result = []; $result = [];

View File

@ -19,6 +19,9 @@ public function index(): View
{ {
/** @var User $user */ /** @var User $user */
$user = auth()->user(); $user = auth()->user();
$this->authorize('viewAny', [Server::class, $user->currentProject]);
$servers = $user->currentProject->servers()->orderByDesc('created_at')->get(); $servers = $user->currentProject->servers()->orderByDesc('created_at')->get();
return view('servers.index', compact('servers')); return view('servers.index', compact('servers'));
@ -26,6 +29,11 @@ public function index(): View
public function create(Request $request): View public function create(Request $request): View
{ {
/** @var User $user */
$user = auth()->user();
$this->authorize('create', [Server::class, $user->currentProject]);
$provider = $request->query('provider', old('provider', \App\Enums\ServerProvider::CUSTOM)); $provider = $request->query('provider', old('provider', \App\Enums\ServerProvider::CUSTOM));
$serverProviders = ServerProvider::query()->where('provider', $provider)->get(); $serverProviders = ServerProvider::query()->where('provider', $provider)->get();
@ -40,8 +48,13 @@ public function create(Request $request): View
*/ */
public function store(Request $request): HtmxResponse public function store(Request $request): HtmxResponse
{ {
/** @var User $user */
$user = auth()->user();
$this->authorize('create', [Server::class, $user->currentProject]);
$server = app(CreateServer::class)->create( $server = app(CreateServer::class)->create(
$request->user(), $user,
$request->input() $request->input()
); );
@ -52,14 +65,17 @@ public function store(Request $request): HtmxResponse
public function show(Server $server): View public function show(Server $server): View
{ {
$this->authorize('view', $server);
return view('servers.show', [ return view('servers.show', [
'server' => $server, 'server' => $server,
'logs' => $server->logs()->latest()->limit(10)->get(),
]); ]);
} }
public function delete(Server $server): RedirectResponse public function delete(Server $server): RedirectResponse
{ {
$this->authorize('delete', $server);
$server->delete(); $server->delete();
Toast::success('Server deleted successfully.'); Toast::success('Server deleted successfully.');

View File

@ -2,22 +2,30 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\Server\CreateServerLog;
use App\Facades\Toast;
use App\Models\Server; use App\Models\Server;
use App\Models\ServerLog; use App\Models\ServerLog;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class ServerLogController extends Controller class ServerLogController extends Controller
{ {
public function index(Server $server): View public function index(Server $server): View
{ {
$this->authorize('manage', $server);
return view('server-logs.index', [ return view('server-logs.index', [
'server' => $server, 'server' => $server,
'pageTitle' => __('Vito Logs'),
]); ]);
} }
public function show(Server $server, ServerLog $serverLog): RedirectResponse public function show(Server $server, ServerLog $serverLog): RedirectResponse
{ {
$this->authorize('manage', $server);
if ($server->id != $serverLog->server_id) { if ($server->id != $serverLog->server_id) {
abort(404); abort(404);
} }
@ -26,4 +34,37 @@ public function show(Server $server, ServerLog $serverLog): RedirectResponse
'content' => $serverLog->getContent(), 'content' => $serverLog->getContent(),
]); ]);
} }
public function remote(Server $server): View
{
$this->authorize('manage', $server);
return view('server-logs.remote-logs', [
'server' => $server,
'remote' => true,
'pageTitle' => __('Remote Logs'),
]);
}
public function store(Server $server, Request $request): \App\Helpers\HtmxResponse
{
$this->authorize('manage', $server);
app(CreateServerLog::class)->create($server, $request->input());
Toast::success('Log added successfully.');
return htmx()->redirect(route('servers.logs.remote', ['server' => $server]));
}
public function destroy(Server $server, ServerLog $serverLog): RedirectResponse
{
$this->authorize('manage', $server);
$serverLog->delete();
Toast::success('Remote log deleted successfully.');
return redirect()->route('servers.logs.remote', ['server' => $server]);
}
} }

View File

@ -15,11 +15,15 @@ class ServerSettingController extends Controller
{ {
public function index(Server $server): View public function index(Server $server): View
{ {
$this->authorize('manage', $server);
return view('server-settings.index', compact('server')); return view('server-settings.index', compact('server'));
} }
public function checkConnection(Server $server): RedirectResponse|HtmxResponse public function checkConnection(Server $server): RedirectResponse|HtmxResponse
{ {
$this->authorize('manage', $server);
$oldStatus = $server->status; $oldStatus = $server->status;
$server = $server->checkConnection(); $server = $server->checkConnection();
@ -41,6 +45,8 @@ public function checkConnection(Server $server): RedirectResponse|HtmxResponse
public function reboot(Server $server): HtmxResponse public function reboot(Server $server): HtmxResponse
{ {
$this->authorize('manage', $server);
app(RebootServer::class)->reboot($server); app(RebootServer::class)->reboot($server);
Toast::info('Server is rebooting.'); Toast::info('Server is rebooting.');
@ -50,6 +56,8 @@ public function reboot(Server $server): HtmxResponse
public function edit(Request $request, Server $server): RedirectResponse public function edit(Request $request, Server $server): RedirectResponse
{ {
$this->authorize('manage', $server);
app(EditServer::class)->edit($server, $request->input()); app(EditServer::class)->edit($server, $request->input());
Toast::success('Server updated.'); Toast::success('Server updated.');

View File

@ -16,6 +16,8 @@ class ServiceController extends Controller
{ {
public function index(Server $server): View public function index(Server $server): View
{ {
$this->authorize('manage', $server);
return view('services.index', [ return view('services.index', [
'server' => $server, 'server' => $server,
'services' => $server->services, 'services' => $server->services,
@ -24,6 +26,8 @@ public function index(Server $server): View
public function start(Server $server, Service $service): RedirectResponse public function start(Server $server, Service $service): RedirectResponse
{ {
$this->authorize('manage', $server);
$service->start(); $service->start();
Toast::success('Service is being started!'); Toast::success('Service is being started!');
@ -33,6 +37,8 @@ public function start(Server $server, Service $service): RedirectResponse
public function stop(Server $server, Service $service): RedirectResponse public function stop(Server $server, Service $service): RedirectResponse
{ {
$this->authorize('manage', $server);
$service->stop(); $service->stop();
Toast::success('Service is being stopped!'); Toast::success('Service is being stopped!');
@ -42,6 +48,8 @@ public function stop(Server $server, Service $service): RedirectResponse
public function restart(Server $server, Service $service): RedirectResponse public function restart(Server $server, Service $service): RedirectResponse
{ {
$this->authorize('manage', $server);
$service->restart(); $service->restart();
Toast::success('Service is being restarted!'); Toast::success('Service is being restarted!');
@ -51,6 +59,8 @@ public function restart(Server $server, Service $service): RedirectResponse
public function enable(Server $server, Service $service): RedirectResponse public function enable(Server $server, Service $service): RedirectResponse
{ {
$this->authorize('manage', $server);
$service->enable(); $service->enable();
Toast::success('Service is being enabled!'); Toast::success('Service is being enabled!');
@ -60,6 +70,8 @@ public function enable(Server $server, Service $service): RedirectResponse
public function disable(Server $server, Service $service): RedirectResponse public function disable(Server $server, Service $service): RedirectResponse
{ {
$this->authorize('manage', $server);
$service->disable(); $service->disable();
Toast::success('Service is being disabled!'); Toast::success('Service is being disabled!');
@ -69,15 +81,19 @@ public function disable(Server $server, Service $service): RedirectResponse
public function install(Server $server, Request $request): HtmxResponse public function install(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
app(Install::class)->install($server, $request->input()); app(Install::class)->install($server, $request->input());
Toast::success('Service is being uninstalled!'); Toast::success('Service is being installed!');
return htmx()->back(); return htmx()->back();
} }
public function uninstall(Server $server, Service $service): HtmxResponse public function uninstall(Server $server, Service $service): HtmxResponse
{ {
$this->authorize('manage', $server);
app(Uninstall::class)->uninstall($service); app(Uninstall::class)->uninstall($service);
Toast::success('Service is being uninstalled!'); Toast::success('Service is being uninstalled!');

View File

@ -29,7 +29,7 @@ public function add(Request $request): HtmxResponse
Toast::success('Channel added successfully'); Toast::success('Channel added successfully');
return htmx()->redirect(route('notification-channels')); return htmx()->redirect(route('settings.notification-channels'));
} }
public function delete(int $id): RedirectResponse public function delete(int $id): RedirectResponse
@ -40,6 +40,6 @@ public function delete(int $id): RedirectResponse
Toast::success('Channel deleted successfully'); Toast::success('Channel deleted successfully');
return redirect()->route('notification-channels'); return redirect()->route('settings.notification-channels');
} }
} }

View File

@ -20,7 +20,7 @@ class ProjectController extends Controller
public function index(): View public function index(): View
{ {
return view('settings.projects.index', [ return view('settings.projects.index', [
'projects' => auth()->user()->projects, 'projects' => Project::all(),
]); ]);
} }
@ -30,7 +30,7 @@ public function create(Request $request): HtmxResponse
Toast::success('Project created.'); Toast::success('Project created.');
return htmx()->redirect(route('projects')); return htmx()->redirect(route('settings.projects'));
} }
public function update(Request $request, Project $project): HtmxResponse public function update(Request $request, Project $project): HtmxResponse
@ -42,7 +42,7 @@ public function update(Request $request, Project $project): HtmxResponse
Toast::success('Project updated.'); Toast::success('Project updated.');
return htmx()->redirect(route('projects')); return htmx()->redirect(route('settings.projects'));
} }
public function delete(Project $project): RedirectResponse public function delete(Project $project): RedirectResponse
@ -74,6 +74,8 @@ public function switch($projectId): RedirectResponse
/** @var Project $project */ /** @var Project $project */
$project = $user->projects()->findOrFail($projectId); $project = $user->projects()->findOrFail($projectId);
$this->authorize('view', $project);
$user->current_project_id = $project->id; $user->current_project_id = $project->id;
$user->save(); $user->save();

View File

@ -29,7 +29,7 @@ public function add(Request $request): HtmxResponse
Toast::success('SSH Key added'); Toast::success('SSH Key added');
return htmx()->redirect(route('ssh-keys')); return htmx()->redirect(route('settings.ssh-keys'));
} }
public function delete(int $id): RedirectResponse public function delete(int $id): RedirectResponse
@ -40,6 +40,6 @@ public function delete(int $id): RedirectResponse
Toast::success('SSH Key deleted'); Toast::success('SSH Key deleted');
return redirect()->route('ssh-keys'); return redirect()->route('settings.ssh-keys');
} }
} }

View File

@ -30,7 +30,7 @@ public function connect(Request $request): HtmxResponse
Toast::success('Server provider connected.'); Toast::success('Server provider connected.');
return htmx()->redirect(route('server-providers')); return htmx()->redirect(route('settings.server-providers'));
} }
public function delete(ServerProvider $serverProvider): RedirectResponse public function delete(ServerProvider $serverProvider): RedirectResponse

View File

@ -29,7 +29,7 @@ public function connect(Request $request): HtmxResponse
Toast::success('Source control connected.'); Toast::success('Source control connected.');
return htmx()->redirect(route('source-controls')); return htmx()->redirect(route('settings.source-controls'));
} }
public function delete(SourceControl $sourceControl): RedirectResponse public function delete(SourceControl $sourceControl): RedirectResponse
@ -44,6 +44,6 @@ public function delete(SourceControl $sourceControl): RedirectResponse
Toast::success('Source control deleted.'); Toast::success('Source control deleted.');
return redirect()->route('source-controls'); return redirect()->route('settings.source-controls');
} }
} }

View File

@ -30,7 +30,7 @@ public function connect(Request $request): HtmxResponse
Toast::success('Storage provider connected.'); Toast::success('Storage provider connected.');
return htmx()->redirect(route('storage-providers')); return htmx()->redirect(route('settings.storage-providers'));
} }
public function delete(StorageProvider $storageProvider): RedirectResponse public function delete(StorageProvider $storageProvider): RedirectResponse

View File

@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Actions\User\CreateUser;
use App\Actions\User\UpdateUser;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class UserController extends Controller
{
public function index(): View
{
$users = User::query()->paginate(20);
return view('settings.users.index', compact('users'));
}
public function store(Request $request): HtmxResponse
{
$user = app(CreateUser::class)->create($request->input());
return htmx()->redirect(route('settings.users.show', $user));
}
public function show(User $user): View
{
return view('settings.users.show', [
'user' => $user,
]);
}
public function update(User $user, Request $request): RedirectResponse
{
app(UpdateUser::class)->update($user, $request->input());
Toast::success('User updated successfully');
return back();
}
public function updateProjects(User $user, Request $request): HtmxResponse
{
$this->validate($request, [
'projects.*' => [
'required',
Rule::exists('projects', 'id'),
],
]);
$user->projects()->sync($request->projects);
Toast::success('Projects updated successfully');
return htmx()->redirect(route('settings.users.show', $user));
}
public function destroy(User $user): RedirectResponse
{
if ($user->is(request()->user())) {
Toast::error('You cannot delete your own account');
return back();
}
$user->delete();
Toast::success('User deleted successfully');
return redirect()->route('settings.users.index');
}
}

View File

@ -19,6 +19,8 @@ class SiteController extends Controller
{ {
public function index(Server $server): View public function index(Server $server): View
{ {
$this->authorize('manage', $server);
return view('sites.index', [ return view('sites.index', [
'server' => $server, 'server' => $server,
'sites' => $server->sites()->orderByDesc('id')->get(), 'sites' => $server->sites()->orderByDesc('id')->get(),
@ -27,6 +29,8 @@ public function index(Server $server): View
public function store(Server $server, Request $request): HtmxResponse public function store(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
$site = app(CreateSite::class)->create($server, $request->input()); $site = app(CreateSite::class)->create($server, $request->input());
Toast::success('Site created'); Toast::success('Site created');
@ -36,6 +40,8 @@ public function store(Server $server, Request $request): HtmxResponse
public function create(Server $server): View public function create(Server $server): View
{ {
$this->authorize('manage', $server);
return view('sites.create', [ return view('sites.create', [
'server' => $server, 'server' => $server,
'type' => old('type', request()->query('type', SiteType::LARAVEL)), 'type' => old('type', request()->query('type', SiteType::LARAVEL)),
@ -45,6 +51,8 @@ public function create(Server $server): View
public function show(Server $server, Site $site, Request $request): View|RedirectResponse|HtmxResponse public function show(Server $server, Site $site, Request $request): View|RedirectResponse|HtmxResponse
{ {
$this->authorize('manage', $server);
if (in_array($site->status, [SiteStatus::INSTALLING, SiteStatus::INSTALLATION_FAILED])) { if (in_array($site->status, [SiteStatus::INSTALLING, SiteStatus::INSTALLATION_FAILED])) {
if ($request->hasHeader('HX-Request')) { if ($request->hasHeader('HX-Request')) {
return htmx()->redirect(route('servers.sites.installing', [$server, $site])); return htmx()->redirect(route('servers.sites.installing', [$server, $site]));
@ -61,6 +69,8 @@ public function show(Server $server, Site $site, Request $request): View|Redirec
public function installing(Server $server, Site $site, Request $request): View|RedirectResponse|HtmxResponse public function installing(Server $server, Site $site, Request $request): View|RedirectResponse|HtmxResponse
{ {
$this->authorize('manage', $server);
if (! in_array($site->status, [SiteStatus::INSTALLING, SiteStatus::INSTALLATION_FAILED])) { if (! in_array($site->status, [SiteStatus::INSTALLING, SiteStatus::INSTALLATION_FAILED])) {
if ($request->hasHeader('HX-Request')) { if ($request->hasHeader('HX-Request')) {
return htmx()->redirect(route('servers.sites.show', [$server, $site])); return htmx()->redirect(route('servers.sites.show', [$server, $site]));
@ -77,6 +87,8 @@ public function installing(Server $server, Site $site, Request $request): View|R
public function destroy(Server $server, Site $site): RedirectResponse public function destroy(Server $server, Site $site): RedirectResponse
{ {
$this->authorize('manage', $server);
app(DeleteSite::class)->delete($site); app(DeleteSite::class)->delete($site);
Toast::success('Site is being deleted'); Toast::success('Site is being deleted');

View File

@ -10,9 +10,12 @@ class SiteLogController extends Controller
{ {
public function index(Server $server, Site $site): View public function index(Server $server, Site $site): View
{ {
$this->authorize('manage', $server);
return view('site-logs.index', [ return view('site-logs.index', [
'server' => $server, 'server' => $server,
'site' => $site, 'site' => $site,
'pageTitle' => __('Vito Logs'),
]); ]);
} }
} }

View File

@ -18,6 +18,8 @@ class SiteSettingController extends Controller
{ {
public function index(Server $server, Site $site): View public function index(Server $server, Site $site): View
{ {
$this->authorize('manage', $server);
return view('site-settings.index', [ return view('site-settings.index', [
'server' => $server, 'server' => $server,
'site' => $site, 'site' => $site,
@ -26,6 +28,8 @@ public function index(Server $server, Site $site): View
public function getVhost(Server $server, Site $site): RedirectResponse public function getVhost(Server $server, Site $site): RedirectResponse
{ {
$this->authorize('manage', $server);
/** @var Webserver $handler */ /** @var Webserver $handler */
$handler = $server->webserver()->handler(); $handler = $server->webserver()->handler();
@ -34,6 +38,8 @@ public function getVhost(Server $server, Site $site): RedirectResponse
public function updateVhost(Server $server, Site $site, Request $request): RedirectResponse public function updateVhost(Server $server, Site $site, Request $request): RedirectResponse
{ {
$this->authorize('manage', $server);
$this->validate($request, [ $this->validate($request, [
'vhost' => 'required|string', 'vhost' => 'required|string',
]); ]);
@ -53,6 +59,8 @@ public function updateVhost(Server $server, Site $site, Request $request): Redir
public function updatePHPVersion(Server $server, Site $site, Request $request): HtmxResponse public function updatePHPVersion(Server $server, Site $site, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
$this->validate($request, [ $this->validate($request, [
'version' => [ 'version' => [
'required', 'required',
@ -73,6 +81,8 @@ public function updatePHPVersion(Server $server, Site $site, Request $request):
public function updateSourceControl(Server $server, Site $site, Request $request): HtmxResponse public function updateSourceControl(Server $server, Site $site, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
$site = app(UpdateSourceControl::class)->update($site, $request->input()); $site = app(UpdateSourceControl::class)->update($site, $request->input());
Toast::success('Source control updated successfully!'); Toast::success('Source control updated successfully!');

View File

@ -68,5 +68,6 @@ class Kernel extends HttpKernel
'server-is-ready' => ServerIsReadyMiddleware::class, 'server-is-ready' => ServerIsReadyMiddleware::class,
'handle-ssh-errors' => HandleSSHErrors::class, 'handle-ssh-errors' => HandleSSHErrors::class,
'select-current-project' => SelectCurrentProject::class, 'select-current-project' => SelectCurrentProject::class,
'is-admin' => \App\Http\Middleware\IsAdmin::class,
]; ];
} }

View File

@ -14,17 +14,17 @@ class HandleSSHErrors
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {
$res = $next($request); $res = $next($request);
if ($res instanceof Response && $res->exception) { // if ($res instanceof Response && $res->exception) {
if ($res->exception instanceof SSHConnectionError || $res->exception instanceof SSHCommandError) { // if ($res->exception instanceof SSHConnectionError || $res->exception instanceof SSHCommandError) {
Toast::error($res->exception->getMessage()); // Toast::error($res->exception->getMessage());
if ($request->hasHeader('HX-Request')) { // if ($request->hasHeader('HX-Request')) {
return htmx()->back(); // return htmx()->back();
} // }
return back(); // return back();
} // }
} // }
return $res; return $res;
} }

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use App\Enums\UserRole;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IsAdmin
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (auth()->user()->role !== UserRole::ADMIN) {
abort(403, 'You are not authorized to access this page.');
}
return $next($request);
}
}

View File

@ -7,6 +7,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
/** /**
@ -53,4 +54,9 @@ public function notificationChannels(): HasMany
{ {
return $this->hasMany(NotificationChannel::class); return $this->hasMany(NotificationChannel::class);
} }
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class, 'user_project')->withTimestamps();
}
} }

View File

@ -16,6 +16,7 @@
* @property string $disk * @property string $disk
* @property Server $server * @property Server $server
* @property ?Site $site * @property ?Site $site
* @property bool $is_remote
*/ */
class ServerLog extends AbstractModel class ServerLog extends AbstractModel
{ {
@ -27,11 +28,13 @@ class ServerLog extends AbstractModel
'type', 'type',
'name', 'name',
'disk', 'disk',
'is_remote',
]; ];
protected $casts = [ protected $casts = [
'server_id' => 'integer', 'server_id' => 'integer',
'site_id' => 'integer', 'site_id' => 'integer',
'is_remote' => 'boolean',
]; ];
public static function boot(): void public static function boot(): void
@ -64,6 +67,17 @@ public function site(): BelongsTo
return $this->belongsTo(Site::class); return $this->belongsTo(Site::class);
} }
public static function getRemote($query, bool $active = true, ?Site $site = null)
{
$query->where('is_remote', $active);
if ($site) {
$query->where('name', 'like', $site->path.'%');
}
return $query;
}
public function write($buf): void public function write($buf): void
{ {
if (Str::contains($buf, 'VITO_SSH_ERROR')) { if (Str::contains($buf, 'VITO_SSH_ERROR')) {
@ -78,6 +92,10 @@ public function write($buf): void
public function getContent(): ?string public function getContent(): ?string
{ {
if ($this->is_remote) {
return $this->server->os()->tail($this->name, 150);
}
if (Storage::disk($this->disk)->exists($this->name)) { if (Storage::disk($this->disk)->exists($this->name)) {
return Storage::disk($this->disk)->get($this->name); return Storage::disk($this->disk)->get($this->name);
} }
@ -97,4 +115,27 @@ public static function log(Server $server, string $type, string $content, ?Site
$log->save(); $log->save();
$log->write($content); $log->write($content);
} }
public static function make(Server $server, string $type): ServerLog
{
return new static([
'server_id' => $server->id,
'name' => $server->id.'-'.strtotime('now').'-'.$type.'.log',
'type' => $type,
'disk' => config('core.logs_disk'),
]);
}
public function forSite(Site|int $site): ServerLog
{
if ($site instanceof Site) {
$site = $site->id;
}
if (is_int($site)) {
$this->site_id = $site;
}
return $this;
}
} }

View File

@ -3,7 +3,9 @@
namespace App\Models; namespace App\Models;
use App\Exceptions\SourceControlIsNotConnected; use App\Exceptions\SourceControlIsNotConnected;
use App\Exceptions\SSHError;
use App\SiteTypes\SiteType; use App\SiteTypes\SiteType;
use App\SSH\Services\Webserver\Webserver;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -185,7 +187,9 @@ public function php(): ?Service
public function changePHPVersion($version): void public function changePHPVersion($version): void
{ {
$this->server->webserver()->handler()->changePHPVersion($this, $version); /** @var Webserver $handler */
$handler = $this->server->webserver()->handler();
$handler->changePHPVersion($this, $version);
$this->php_version = $version; $this->php_version = $version;
$this->save(); $this->save();
} }
@ -268,6 +272,10 @@ public function hasFeature(string $feature): bool
public function getEnv(): string public function getEnv(): string
{ {
try {
return $this->server->os()->readFile($this->path.'/.env'); return $this->server->os()->readFile($this->path.'/.env');
} catch (SSHError) {
return '';
}
} }
} }

View File

@ -2,7 +2,9 @@
namespace App\Models; namespace App\Models;
use App\Enums\UserRole;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
@ -30,6 +32,7 @@
* @property int $current_project_id * @property int $current_project_id
* @property Project $currentProject * @property Project $currentProject
* @property Collection<Project> $projects * @property Collection<Project> $projects
* @property string $role
*/ */
class User extends Authenticatable class User extends Authenticatable
{ {
@ -43,6 +46,7 @@ class User extends Authenticatable
'password', 'password',
'timezone', 'timezone',
'current_project_id', 'current_project_id',
'role',
]; ];
protected $hidden = [ protected $hidden = [
@ -60,7 +64,9 @@ public static function boot(): void
parent::boot(); parent::boot();
static::created(function (User $user) { static::created(function (User $user) {
if (Project::count() === 0) {
$user->createDefaultProject(); $user->createDefaultProject();
}
}); });
} }
@ -117,9 +123,9 @@ public function connectedSourceControls(): array
return $connectedSourceControls; return $connectedSourceControls;
} }
public function projects(): HasMany public function projects(): BelongsToMany
{ {
return $this->hasMany(Project::class); return $this->belongsToMany(Project::class, 'user_project')->withTimestamps();
} }
public function currentProject(): HasOne public function currentProject(): HasOne
@ -138,9 +144,10 @@ public function createDefaultProject(): Project
if (! $project) { if (! $project) {
$project = new Project(); $project = new Project();
$project->user_id = $this->id; $project->name = 'default';
$project->name = 'Default';
$project->save(); $project->save();
$project->users()->attach($this->id);
} }
$this->current_project_id = $project->id; $this->current_project_id = $project->id;
@ -148,4 +155,9 @@ public function createDefaultProject(): Project
return $project; return $project;
} }
public function isAdmin(): bool
{
return $this->role === UserRole::ADMIN;
}
} }

View File

@ -35,7 +35,7 @@ public function connect(): bool
__('Congratulations! 🎉'), __('Congratulations! 🎉'),
__("You've connected your Discord to :app", ['app' => config('app.name')])."\n". __("You've connected your Discord to :app", ['app' => config('app.name')])."\n".
__('Manage your notification channels')."\n". __('Manage your notification channels')."\n".
route('notification-channels') route('settings.notification-channels')
); );
if (! $connect) { if (! $connect) {

View File

@ -35,7 +35,7 @@ public function connect(): bool
__('Congratulations! 🎉'), __('Congratulations! 🎉'),
__("You've connected your Slack to :app", ['app' => config('app.name')])."\n". __("You've connected your Slack to :app", ['app' => config('app.name')])."\n".
__('Manage your notification channels')."\n". __('Manage your notification channels')."\n".
route('notification-channels') route('settings.notification-channels')
); );
if (! $connect) { if (! $connect) {

View File

@ -0,0 +1,35 @@
<?php
namespace App\Policies;
use App\Enums\UserRole;
use App\Models\Project;
use App\Models\User;
class ProjectPolicy
{
public function viewAny(User $user): bool
{
return $user->role === UserRole::ADMIN;
}
public function view(User $user, Project $project): bool
{
return $user->role === UserRole::ADMIN || $project->users->contains($user);
}
public function create(User $user): bool
{
return $user->role === UserRole::ADMIN;
}
public function update(User $user, Project $project): bool
{
return $user->role === UserRole::ADMIN;
}
public function delete(User $user, Project $project): bool
{
return $user->role === UserRole::ADMIN;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Policies;
use App\Enums\UserRole;
use App\Models\Project;
use App\Models\Server;
use App\Models\User;
class ServerPolicy
{
public function viewAny(User $user, Project $project): bool
{
return $user->role === UserRole::ADMIN || $project->users->contains($user);
}
public function view(User $user, Server $server): bool
{
return $user->role === UserRole::ADMIN || $server->project->users->contains($user);
}
public function create(User $user, Project $project): bool
{
return $user->role === UserRole::ADMIN || $project->users->contains($user);
}
public function update(User $user, Server $server): bool
{
return $user->role === UserRole::ADMIN || $server->project->users->contains($user);
}
public function delete(User $user, Server $server): bool
{
return $user->role === UserRole::ADMIN || $server->project->users->contains($user);
}
public function manage(User $user, Server $server): bool
{
return $user->role === UserRole::ADMIN || $server->project->users->contains($user);
}
}

View File

@ -117,16 +117,28 @@ public function readFile(string $path): string
); );
} }
public function runScript(string $path, string $script, ?int $siteId = null): ServerLog public function tail(string $path, int $lines): string
{
return $this->server->ssh()->exec(
$this->getScript('tail.sh', [
'path' => $path,
'lines' => $lines,
])
);
}
public function runScript(string $path, string $script, ?ServerLog $serverLog): ServerLog
{ {
$ssh = $this->server->ssh(); $ssh = $this->server->ssh();
if ($serverLog) {
$ssh->setLog($serverLog);
}
$ssh->exec( $ssh->exec(
$this->getScript('run-script.sh', [ $this->getScript('run-script.sh', [
'path' => $path, 'path' => $path,
'script' => $script, 'script' => $script,
]), ]),
'run-script', 'run-script'
$siteId
); );
return $ssh->log; return $ssh->log;
@ -156,4 +168,21 @@ public function cleanup(): void
'cleanup' 'cleanup'
); );
} }
public function resourceInfo(): array
{
$info = $this->server->ssh()->exec(
$this->getScript('resource-info.sh'),
);
return [
'load' => str($info)->after('load:')->before(PHP_EOL)->toString(),
'memory_total' => str($info)->after('memory_total:')->before(PHP_EOL)->toString(),
'memory_used' => str($info)->after('memory_used:')->before(PHP_EOL)->toString(),
'memory_free' => str($info)->after('memory_free:')->before(PHP_EOL)->toString(),
'disk_total' => str($info)->after('disk_total:')->before(PHP_EOL)->toString(),
'disk_used' => str($info)->after('disk_used:')->before(PHP_EOL)->toString(),
'disk_free' => str($info)->after('disk_free:')->before(PHP_EOL)->toString(),
];
}
} }

View File

@ -1 +1 @@
[ -f __path__ ] && cat __path__ [ -f __path__ ] && sudo cat __path__

View File

@ -0,0 +1,7 @@
echo "load:$(uptime | awk -F'load average:' '{print $2}' | awk -F, '{print $1}' | tr -d ' ')"
echo "memory_total:$(free -k | awk 'NR==2{print $2}')"
echo "memory_used:$(free -k | awk 'NR==2{print $3}')"
echo "memory_free:$(free -k | awk 'NR==2{print $7}')"
echo "disk_total:$(df -BM / | awk 'NR==2{print $2}' | sed 's/M//')"
echo "disk_used:$(df -BM / | awk 'NR==2{print $3}' | sed 's/M//')"
echo "disk_free:$(df -BM / | awk 'NR==2{print $4}' | sed 's/M//')"

View File

@ -0,0 +1 @@
sudo tail -n __lines__ __path__

View File

@ -159,7 +159,7 @@ public function runBackup(BackupFile $backupFile): void
); );
// cleanup // cleanup
$this->service->server->ssh()->exec('rm '.$backupFile->name.'.zip'); $this->service->server->ssh()->exec('rm '.$backupFile->path());
$backupFile->size = $upload['size']; $backupFile->size = $upload['size'];
$backupFile->save(); $backupFile->save();

View File

@ -0,0 +1,53 @@
<?php
namespace App\SSH\Services\Monitoring\RemoteMonitor;
use App\Models\Metric;
use App\SSH\Services\AbstractService;
use Closure;
use Illuminate\Validation\Rule;
class RemoteMonitor extends AbstractService
{
public function creationRules(array $input): array
{
return [
'type' => [
function (string $attribute, mixed $value, Closure $fail) {
$monitoringExists = $this->service->server->monitoring();
if ($monitoringExists) {
$fail('You already have a monitoring service on the server.');
}
},
],
'version' => [
'required',
Rule::in(['latest']),
],
];
}
public function creationData(array $input): array
{
return [
'data_retention' => 10,
];
}
public function data(): array
{
return [
'data_retention' => $this->service->type_data['data_retention'] ?? 10,
];
}
public function install(): void
{
//
}
public function uninstall(): void
{
Metric::where('server_id', $this->service->server_id)->delete();
}
}

View File

@ -1,10 +1,11 @@
<?php <?php
namespace App\SSH\Services\VitoAgent; namespace App\SSH\Services\Monitoring\VitoAgent;
use App\Models\Metric; use App\Models\Metric;
use App\SSH\HasScripts; use App\SSH\HasScripts;
use App\SSH\Services\AbstractService; use App\SSH\Services\AbstractService;
use Closure;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
@ -21,7 +22,12 @@ public function creationRules(array $input): array
{ {
return [ return [
'type' => [ 'type' => [
Rule::unique('services', 'type')->where('server_id', $this->service->server_id), function (string $attribute, mixed $value, Closure $fail) {
$monitoringExists = $this->service->server->monitoring();
if ($monitoringExists) {
$fail('You already have a monitoring service on the server.');
}
},
], ],
'version' => [ 'version' => [
'required', 'required',

View File

@ -44,4 +44,12 @@ public function download(string $src, string $dest): void
'download-from-dropbox' 'download-from-dropbox'
); );
} }
/**
* @TODO Implement delete method
*/
public function delete(string $path): void
{
//
}
} }

View File

@ -45,4 +45,12 @@ public function download(string $src, string $dest): void
'download-from-ftp' 'download-from-ftp'
); );
} }
/**
* @TODO Implement delete method
*/
public function delete(string $path): void
{
//
}
} }

49
app/SSH/Storage/Local.php Normal file
View File

@ -0,0 +1,49 @@
<?php
namespace App\SSH\Storage;
use App\SSH\HasScripts;
class Local extends AbstractStorage
{
use HasScripts;
public function upload(string $src, string $dest): array
{
$destDir = dirname($this->storageProvider->credentials['path'].$dest);
$destFile = basename($this->storageProvider->credentials['path'].$dest);
$this->server->ssh()->exec(
$this->getScript('local/upload.sh', [
'src' => $src,
'dest_dir' => $destDir,
'dest_file' => $destFile,
]),
'upload-to-local'
);
return [
'size' => null,
];
}
public function download(string $src, string $dest): void
{
$this->server->ssh()->exec(
$this->getScript('local/download.sh', [
'src' => $this->storageProvider->credentials['path'].$src,
'dest' => $dest,
]),
'download-from-local'
);
}
public function delete(string $path): void
{
$this->server->ssh()->exec(
$this->getScript('local/delete.sh', [
'path' => $this->storageProvider->credentials['path'].$path,
]),
'delete-from-local'
);
}
}

View File

@ -7,4 +7,6 @@ interface Storage
public function upload(string $src, string $dest): array; public function upload(string $src, string $dest): array;
public function download(string $src, string $dest): void; public function download(string $src, string $dest): void;
public function delete(string $path): void;
} }

View File

@ -0,0 +1 @@
rm __path__

View File

@ -0,0 +1 @@
cp __src__ __dest__

View File

@ -0,0 +1,2 @@
mkdir -p __dest_dir__
cp __src__ __dest_dir__/__dest_file__

View File

@ -0,0 +1,38 @@
<?php
namespace App\StorageProviders;
use App\Models\Server;
use App\SSH\Storage\Storage;
class Local extends AbstractStorageProvider
{
public function validationRules(): array
{
return [
'path' => 'required',
];
}
public function credentialData(array $input): array
{
return [
'path' => $input['path'],
];
}
public function connect(): bool
{
return true;
}
public function ssh(Server $server): Storage
{
return new \App\SSH\Storage\Local($server, $this->storageProvider);
}
public function delete(array $paths): void
{
//
}
}

View File

@ -36,10 +36,13 @@ public function connect(bool $sftp = false): void
public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false): string public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false): string
{ {
if ($log) { if (! $this->log && $log) {
$this->setLog($log, $siteId); $this->log = $this->server->logs()->create([
} else { 'site_id' => $siteId,
$this->log = null; 'name' => $this->server->id.'-'.strtotime('now').'-'.$log.'.log',
'type' => $log,
'disk' => config('core.logs_disk'),
]);
} }
$this->commands[] = $command; $this->commands[] = $command;

View File

@ -32,7 +32,12 @@ function htmx(): HtmxResponse
function vito_version(): string function vito_version(): string
{ {
return exec('git describe --tags'); $version = exec('git describe --tags');
if (str($version)->contains('-')) {
return str($version)->before('-').' (dev)';
}
return $version;
} }
function convert_time_format($string): string function convert_time_format($string): string

View File

@ -5,10 +5,10 @@
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Illuminate\View\Component; use Illuminate\View\Component;
class ProfileLayout extends Component class SettingsLayout extends Component
{ {
public function render(): View public function render(): View
{ {
return view('layouts.profile'); return view('layouts.settings');
} }
} }

View File

@ -142,6 +142,7 @@
'ufw' => 'firewall', 'ufw' => 'firewall',
'supervisor' => 'process_manager', 'supervisor' => 'process_manager',
'vito-agent' => 'monitoring', 'vito-agent' => 'monitoring',
'remote-monitor' => 'monitoring',
], ],
'service_handlers' => [ 'service_handlers' => [
'nginx' => \App\SSH\Services\Webserver\Nginx::class, 'nginx' => \App\SSH\Services\Webserver\Nginx::class,
@ -152,7 +153,8 @@
'php' => \App\SSH\Services\PHP\PHP::class, 'php' => \App\SSH\Services\PHP\PHP::class,
'ufw' => \App\SSH\Services\Firewall\Ufw::class, 'ufw' => \App\SSH\Services\Firewall\Ufw::class,
'supervisor' => \App\SSH\Services\ProcessManager\Supervisor::class, 'supervisor' => \App\SSH\Services\ProcessManager\Supervisor::class,
'vito-agent' => \App\SSH\Services\VitoAgent\VitoAgent::class, 'vito-agent' => \App\SSH\Services\Monitoring\VitoAgent\VitoAgent::class,
'remote-monitor' => \App\SSH\Services\Monitoring\RemoteMonitor\RemoteMonitor::class,
], ],
'service_units' => [ 'service_units' => [
'nginx' => [ 'nginx' => [
@ -357,10 +359,12 @@
'storage_providers' => [ 'storage_providers' => [
\App\Enums\StorageProvider::DROPBOX, \App\Enums\StorageProvider::DROPBOX,
\App\Enums\StorageProvider::FTP, \App\Enums\StorageProvider::FTP,
\App\Enums\StorageProvider::LOCAL,
], ],
'storage_providers_class' => [ 'storage_providers_class' => [
'dropbox' => \App\StorageProviders\Dropbox::class, \App\Enums\StorageProvider::DROPBOX => \App\StorageProviders\Dropbox::class,
'ftp' => \App\StorageProviders\Ftp::class, \App\Enums\StorageProvider::FTP => \App\StorageProviders\Ftp::class,
\App\Enums\StorageProvider::LOCAL => \App\StorageProviders\Local::class,
], ],
'ssl_types' => [ 'ssl_types' => [

View File

@ -16,7 +16,6 @@ class ProjectFactory extends Factory
public function definition(): array public function definition(): array
{ {
return [ return [
'user_id' => $this->faker->randomNumber(),
'name' => $this->faker->name(), 'name' => $this->faker->name(),
'created_at' => Carbon::now(), 'created_at' => Carbon::now(),
'updated_at' => Carbon::now(), 'updated_at' => Carbon::now(),

View File

@ -7,7 +7,6 @@
use App\Enums\ServerStatus; use App\Enums\ServerStatus;
use App\Enums\ServerType; use App\Enums\ServerType;
use App\Models\Server; use App\Models\Server;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
class ServerFactory extends Factory class ServerFactory extends Factory
@ -16,11 +15,7 @@ class ServerFactory extends Factory
public function definition(): array public function definition(): array
{ {
/** @var User $user */
$user = User::factory()->create();
return [ return [
'user_id' => $user->id,
'name' => $this->faker->name(), 'name' => $this->faker->name(),
'ssh_user' => 'vito', 'ssh_user' => 'vito',
'ip' => $this->faker->ipv4(), 'ip' => $this->faker->ipv4(),

View File

@ -2,6 +2,7 @@
namespace Database\Factories; namespace Database\Factories;
use App\Enums\UserRole;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -18,6 +19,7 @@ public function definition(): array
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10), 'remember_token' => Str::random(10),
'timezone' => 'UTC', 'timezone' => 'UTC',
'role' => UserRole::ADMIN,
]; ];
} }
} }

View File

@ -9,15 +9,13 @@
public function up(): void public function up(): void
{ {
Schema::table('storage_providers', function (Blueprint $table) { Schema::table('storage_providers', function (Blueprint $table) {
$table->dropColumn('token');
$table->dropColumn('refresh_token');
$table->dropColumn('token_expires_at');
$table->dropColumn('label');
$table->dropColumn('connected');
$table->unsignedBigInteger('user_id')->after('id'); $table->unsignedBigInteger('user_id')->after('id');
$table->string('profile')->after('user_id'); $table->string('profile')->after('user_id');
$table->longText('credentials')->nullable()->after('provider'); $table->longText('credentials')->nullable()->after('provider');
}); });
Schema::table('storage_providers', function (Blueprint $table) {
$table->dropColumn(['token', 'refresh_token', 'token_expires_at', 'label', 'connected']);
});
} }
public function down(): void public function down(): void
@ -27,9 +25,9 @@ public function down(): void
$table->string('refresh_token')->nullable(); $table->string('refresh_token')->nullable();
$table->string('token_expires_at')->nullable(); $table->string('token_expires_at')->nullable();
$table->string('label')->nullable(); $table->string('label')->nullable();
$table->dropColumn('user_id'); });
$table->dropColumn('profile'); Schema::table('storage_providers', function (Blueprint $table) {
$table->dropColumn('credentials'); $table->dropColumn(['user_id', 'profile', 'credentials']);
}); });
} }
}; };

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('server_logs', function (Blueprint $table) {
$table->boolean('is_remote')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('server_logs', function (Blueprint $table) {
$table->dropColumn('is_remote');
});
}
};

View File

@ -0,0 +1,31 @@
<?php
use App\Enums\UserRole;
use App\Models\User;
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('users', function (Blueprint $table) {
$table->string('role')->default(UserRole::USER);
});
User::query()->update(['role' => UserRole::ADMIN]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('role');
});
}
};

View File

@ -0,0 +1,38 @@
<?php
use App\Models\Project;
use App\Models\User;
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::create('user_project', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->unsignedBigInteger('project_id');
$table->timestamps();
});
Project::all()->each(function (Project $project) {
$project->users()->attach($project->user_id);
});
User::all()->each(function (User $user) {
$user->current_project_id = $user->projects()->first()?->id;
$user->save();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_project');
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('projects', function (Blueprint $table) {
$table->dropColumn('user_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('projects', function (Blueprint $table) {
$table->bigInteger('user_id')->nullable();
});
}
};

View File

@ -19,7 +19,7 @@ RUN apt-get install -y nginx
# php # php
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor libcap2-bin libpng-dev \ && apt-get install -y cron gnupg gosu curl ca-certificates zip unzip git supervisor libcap2-bin libpng-dev \
python2 dnsutils librsvg2-bin fswatch wget \ python2 dnsutils librsvg2-bin fswatch wget \
&& add-apt-repository ppa:ondrej/php -y \ && add-apt-repository ppa:ondrej/php -y \
&& apt-get update \ && apt-get update \
@ -44,6 +44,8 @@ RUN rm /etc/nginx/sites-enabled/default
COPY docker/nginx.conf /etc/nginx/sites-available/default COPY docker/nginx.conf /etc/nginx/sites-available/default
RUN ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default RUN ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
RUN echo "* * * * * cd /var/www/html && php artisan schedule:run >> /var/log/cron.log 2>&1" | crontab -
# supervisord # supervisord
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf

View File

@ -37,6 +37,8 @@ php /var/www/html/artisan view:cache
php /var/www/html/artisan user:create "$NAME" "$EMAIL" "$PASSWORD" php /var/www/html/artisan user:create "$NAME" "$EMAIL" "$PASSWORD"
cron
echo "Vito is running! 🚀" echo "Vito is running! 🚀"
/usr/bin/supervisord /usr/bin/supervisord

2228
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,6 @@
"prettier-plugin-tailwindcss": "^0.5.11", "prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.1.0", "tailwindcss": "^3.1.0",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"toastr": "^2.1.4",
"vite": "^4.5.3", "vite": "^4.5.3",
"apexcharts": "^3.44.2", "apexcharts": "^3.44.2",
"flowbite-datepicker": "^1.2.6" "flowbite-datepicker": "^1.2.6"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{ {
"resources/css/app.css": { "resources/css/app.css": {
"file": "assets/app-53e4d707.css", "file": "assets/app-268661bd.css",
"isEntry": true, "isEntry": true,
"src": "resources/css/app.css" "src": "resources/css/app.css"
}, },
@ -12,7 +12,7 @@
"css": [ "css": [
"assets/app-a1ae07b3.css" "assets/app-a1ae07b3.css"
], ],
"file": "assets/app-66009dff.js", "file": "assets/app-01264060.js",
"isEntry": true, "isEntry": true,
"src": "resources/js/app.js" "src": "resources/js/app.js"
} }

View File

@ -1,74 +1,5 @@
<?xml version="1.0" encoding="iso-8859-1"?> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#4f46e5"
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> class="w-6 h-6">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <path stroke-linecap="round" stroke-linejoin="round"
viewBox="0 0 58 58" style="enable-background:new 0 0 58 58;" xml:space="preserve"> d="M21.75 17.25v-.228a4.5 4.5 0 0 0-.12-1.03l-2.268-9.64a3.375 3.375 0 0 0-3.285-2.602H7.923a3.375 3.375 0 0 0-3.285 2.602l-2.268 9.64a4.5 4.5 0 0 0-.12 1.03v.228m19.5 0a3 3 0 0 1-3 3H5.25a3 3 0 0 1-3-3m19.5 0a3 3 0 0 0-3-3H5.25a3 3 0 0 0-3 3m16.5 0h.008v.008h-.008v-.008Zm-3 0h.008v.008h-.008v-.008Z" />
<path style="fill:#556080;" d="M54.392,19.5H3.608C1.616,19.5,0,17.884,0,15.892V4.108C0,2.116,1.616,0.5,3.608,0.5h50.783
C56.384,0.5,58,2.116,58,4.108v11.783C58,17.884,56.384,19.5,54.392,19.5z"/>
<path style="fill:#424A60;" d="M54.392,38.5H3.608C1.616,38.5,0,36.884,0,34.892V23.108C0,21.116,1.616,19.5,3.608,19.5h50.783
c1.993,0,3.608,1.616,3.608,3.608v11.783C58,36.884,56.384,38.5,54.392,38.5z"/>
<path style="fill:#556080;" d="M54.392,57.5H3.608C1.616,57.5,0,55.884,0,53.892V42.108C0,40.116,1.616,38.5,3.608,38.5h50.783
c1.993,0,3.608,1.616,3.608,3.608v11.783C58,55.884,56.384,57.5,54.392,57.5z"/>
<circle style="fill:#7383BF;" cx="9.5" cy="10" r="3.5"/>
<circle style="fill:#7383BF;" cx="49" cy="8.5" r="1"/>
<circle style="fill:#7383BF;" cx="45" cy="8.5" r="1"/>
<circle style="fill:#7383BF;" cx="51" cy="11.5" r="1"/>
<circle style="fill:#7383BF;" cx="47" cy="11.5" r="1"/>
<circle style="fill:#7383BF;" cx="41" cy="8.5" r="1"/>
<circle style="fill:#7383BF;" cx="43" cy="11.5" r="1"/>
<circle style="fill:#7383BF;" cx="37" cy="8.5" r="1"/>
<circle style="fill:#7383BF;" cx="39" cy="11.5" r="1"/>
<circle style="fill:#7383BF;" cx="33" cy="8.5" r="1"/>
<circle style="fill:#7383BF;" cx="35" cy="11.5" r="1"/>
<circle style="fill:#7383BF;" cx="9.5" cy="29" r="3.5"/>
<circle style="fill:#7383BF;" cx="49" cy="27.5" r="1"/>
<circle style="fill:#7383BF;" cx="45" cy="27.5" r="1"/>
<circle style="fill:#7383BF;" cx="51" cy="30.5" r="1"/>
<circle style="fill:#7383BF;" cx="47" cy="30.5" r="1"/>
<circle style="fill:#7383BF;" cx="41" cy="27.5" r="1"/>
<circle style="fill:#7383BF;" cx="43" cy="30.5" r="1"/>
<circle style="fill:#7383BF;" cx="37" cy="27.5" r="1"/>
<circle style="fill:#7383BF;" cx="39" cy="30.5" r="1"/>
<circle style="fill:#7383BF;" cx="33" cy="27.5" r="1"/>
<circle style="fill:#7383BF;" cx="35" cy="30.5" r="1"/>
<circle style="fill:#7383BF;" cx="9.5" cy="48" r="3.5"/>
<circle style="fill:#7383BF;" cx="49" cy="46.5" r="1"/>
<circle style="fill:#7383BF;" cx="45" cy="46.5" r="1"/>
<circle style="fill:#7383BF;" cx="51" cy="49.5" r="1"/>
<circle style="fill:#7383BF;" cx="47" cy="49.5" r="1"/>
<circle style="fill:#7383BF;" cx="41" cy="46.5" r="1"/>
<circle style="fill:#7383BF;" cx="43" cy="49.5" r="1"/>
<circle style="fill:#7383BF;" cx="37" cy="46.5" r="1"/>
<circle style="fill:#7383BF;" cx="39" cy="49.5" r="1"/>
<circle style="fill:#7383BF;" cx="33" cy="46.5" r="1"/>
<circle style="fill:#7383BF;" cx="35" cy="49.5" r="1"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 505 B

View File

@ -1 +1,5 @@
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="envelope-open" class="svg-inline--fa fa-envelope-open fa-w-16 stroke-gray-500" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M494.586 164.516c-4.697-3.883-111.723-89.95-135.251-108.657C337.231 38.191 299.437 0 256 0c-43.205 0-80.636 37.717-103.335 55.859-24.463 19.45-131.07 105.195-135.15 108.549A48.004 48.004 0 0 0 0 201.485V464c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V201.509a48 48 0 0 0-17.414-36.993zM464 458a6 6 0 0 1-6 6H54a6 6 0 0 1-6-6V204.347c0-1.813.816-3.526 2.226-4.665 15.87-12.814 108.793-87.554 132.364-106.293C200.755 78.88 232.398 48 256 48c23.693 0 55.857 31.369 73.41 45.389 23.573 18.741 116.503 93.493 132.366 106.316a5.99 5.99 0 0 1 2.224 4.663V458zm-31.991-187.704c4.249 5.159 3.465 12.795-1.745 16.981-28.975 23.283-59.274 47.597-70.929 56.863C336.636 362.283 299.205 400 256 400c-43.452 0-81.287-38.237-103.335-55.86-11.279-8.967-41.744-33.413-70.927-56.865-5.21-4.187-5.993-11.822-1.745-16.981l15.258-18.528c4.178-5.073 11.657-5.843 16.779-1.726 28.618 23.001 58.566 47.035 70.56 56.571C200.143 320.631 232.307 352 256 352c23.602 0 55.246-30.88 73.41-45.389 11.994-9.535 41.944-33.57 70.563-56.568 5.122-4.116 12.601-3.346 16.778 1.727l15.258 18.526z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#4f46e5"
class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M16.5 12a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Zm0 0c0 1.657 1.007 3 2.25 3S21 13.657 21 12a9 9 0 1 0-2.636 6.364M16.5 12V8.25" />
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 331 B

View File

@ -1,129 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path style="fill:#53515E;" d="M477.486,424.797H34.514C15.452,424.797,0,409.345,0,390.283V38.488
C0,19.427,15.452,3.974,34.514,3.974h442.973C496.548,3.974,512,19.427,512,38.488v351.795
C512,409.345,496.548,424.797,477.486,424.797z"/>
<path style="fill:#474756;" d="M364.402,339.5c-4.512,0-8.169-3.657-8.169-8.169V82.374c0-4.512,3.657-8.169,8.169-8.169
s8.169,3.658,8.169,8.169v248.956C372.571,335.842,368.914,339.5,364.402,339.5z"/>
<path style="fill:#6F4BEF;" d="M477.486,3.974H34.514C15.452,3.974,0,19.427,0,38.488V86.97h509.662H512V38.488
C512,19.427,496.548,3.974,477.486,3.974z"/>
<path style="fill:#4628B2;" d="M323.799,54.227H171.836c-4.512,0-8.169-3.658-8.169-8.169s3.657-8.169,8.169-8.169h151.963
c4.512,0,8.169,3.658,8.169,8.169S328.311,54.227,323.799,54.227z"/>
<circle style="fill:#FFDC64;" cx="45.589" cy="46.06" r="10.92"/>
<circle style="fill:#86E56E;" cx="87.67" cy="46.06" r="10.92"/>
<circle style="fill:#FFDC64;" cx="127.42" cy="46.06" r="10.92"/>
<circle style="fill:#FF80BD;" cx="467.58" cy="46.06" r="10.92"/>
<path style="fill:#474756;" d="M460.791,316.909H53.767c-17.436,0-31.571,14.135-31.571,31.571v74.045
c3.827,1.463,7.977,2.272,12.318,2.272h442.973c5.327,0,10.372-1.209,14.876-3.364V348.48
C492.363,331.044,478.228,316.909,460.791,316.909z"/>
<circle style="fill:#AAA8C1;" cx="49.796" cy="130.059" r="8.169"/>
<path style="fill:#00AAF0;" d="M189.169,138.23H82.198c-4.512,0-8.169-3.658-8.169-8.169c0-4.512,3.657-8.169,8.169-8.169h106.971
c4.512,0,8.169,3.658,8.169,8.169C197.338,134.574,193.681,138.23,189.169,138.23z"/>
<g>
<path style="fill:#3C3B44;" d="M478.499,130.061h-80.425c-4.512,0-8.169-3.658-8.169-8.169c0-4.512,3.657-8.169,8.169-8.169h80.425
c4.512,0,8.169,3.658,8.169,8.169C486.669,126.404,483.012,130.061,478.499,130.061z"/>
<path style="fill:#3C3B44;" d="M478.499,165.189h-80.425c-4.512,0-8.169-3.658-8.169-8.169s3.657-8.169,8.169-8.169h80.425
c4.512,0,8.169,3.658,8.169,8.169S483.012,165.189,478.499,165.189z"/>
<path style="fill:#3C3B44;" d="M478.499,200.316h-80.425c-4.512,0-8.169-3.658-8.169-8.169s3.657-8.169,8.169-8.169h80.425
c4.512,0,8.169,3.658,8.169,8.169S483.012,200.316,478.499,200.316z"/>
<path style="fill:#3C3B44;" d="M478.499,235.445h-80.425c-4.512,0-8.169-3.658-8.169-8.169c0-4.512,3.657-8.169,8.169-8.169h80.425
c4.512,0,8.169,3.658,8.169,8.169C486.669,231.787,483.012,235.445,478.499,235.445z"/>
<path style="fill:#3C3B44;" d="M478.499,270.573h-80.425c-4.512,0-8.169-3.657-8.169-8.169s3.657-8.169,8.169-8.169h80.425
c4.512,0,8.169,3.657,8.169,8.169C486.669,266.915,483.012,270.573,478.499,270.573z"/>
<path style="fill:#3C3B44;" d="M478.499,305.7h-80.425c-4.512,0-8.169-3.657-8.169-8.169s3.657-8.169,8.169-8.169h80.425
c4.512,0,8.169,3.657,8.169,8.169S483.012,305.7,478.499,305.7z"/>
</g>
<circle style="fill:#AAA8C1;" cx="49.796" cy="164.369" r="8.169"/>
<path style="fill:#00AAF0;" d="M138.336,172.542H82.198c-4.512,0-8.169-3.658-8.169-8.169s3.657-8.169,8.169-8.169h56.138
c4.512,0,8.169,3.658,8.169,8.169S142.848,172.542,138.336,172.542z"/>
<circle style="fill:#AAA8C1;" cx="49.796" cy="265.67" r="8.169"/>
<path style="fill:#86E56E;" d="M166.711,273.84H82.198c-4.512,0-8.169-3.657-8.169-8.169s3.657-8.169,8.169-8.169h84.513
c4.512,0,8.169,3.657,8.169,8.169S171.223,273.84,166.711,273.84z"/>
<circle style="fill:#AAA8C1;" cx="49.796" cy="299.17" r="8.169"/>
<g>
<path style="fill:#00AAF0;" d="M166.711,307.334H82.198c-4.512,0-8.169-3.657-8.169-8.169s3.657-8.169,8.169-8.169h84.513
c4.512,0,8.169,3.657,8.169,8.169S171.223,307.334,166.711,307.334z"/>
<path style="fill:#00AAF0;" d="M223.638,273.84h-29.843c-4.512,0-8.169-3.657-8.169-8.169s3.657-8.169,8.169-8.169h29.843
c4.512,0,8.169,3.657,8.169,8.169S228.15,273.84,223.638,273.84z"/>
</g>
<path style="fill:#FFDC64;" d="M227.973,172.542h-56.137c-4.512,0-8.169-3.658-8.169-8.169s3.657-8.169,8.169-8.169h56.137
c4.512,0,8.169,3.658,8.169,8.169S232.485,172.542,227.973,172.542z"/>
<circle style="fill:#AAA8C1;" cx="49.796" cy="198.679" r="8.169"/>
<path style="fill:#FF80BD;" d="M193.795,206.852h-77.514c-4.512,0-8.169-3.658-8.169-8.169s3.657-8.169,8.169-8.169h77.514
c4.512,0,8.169,3.658,8.169,8.169S198.307,206.852,193.795,206.852z"/>
<circle style="fill:#AAA8C1;" cx="49.796" cy="231.359" r="8.169"/>
<path style="fill:#FFDC64;" d="M257.657,239.529H116.281c-4.512,0-8.169-3.658-8.169-8.169c0-4.512,3.657-8.169,8.169-8.169h141.376
c4.512,0,8.169,3.658,8.169,8.169C265.826,235.871,262.169,239.529,257.657,239.529z"/>
<path style="fill:#86E56E;" d="M305.487,206.852h-77.514c-4.512,0-8.169-3.658-8.169-8.169s3.657-8.169,8.169-8.169h77.514
c4.512,0,8.169,3.658,8.169,8.169S309.999,206.852,305.487,206.852z"/>
<path style="fill:#FF6B5C;" d="M440.54,508.025H74.019c-15.701,0-28.43-12.728-28.43-28.43V364.355
c0-15.701,12.728-28.43,28.43-28.43H440.54c15.701,0,28.43,12.728,28.43,28.43v115.239
C468.97,495.296,456.241,508.025,440.54,508.025z"/>
<path style="fill:#FF5450;" d="M440.54,335.927h-17.159l0,0c0,74.744-60.592,135.337-135.337,135.337H45.589v8.332
c0,15.701,12.728,28.43,28.43,28.43H440.54c15.701,0,28.43-12.728,28.43-28.43v-115.24
C468.97,348.655,456.241,335.927,440.54,335.927z"/>
<g>
<path style="fill:#FFFFFF;" d="M104.572,455.934v-70.32c0-1.698,0.737-3.007,2.215-3.931c1.476-0.922,3.247-1.384,5.315-1.384
h39.424c1.698,0,2.99,0.739,3.876,2.215c0.885,1.477,1.329,3.211,1.329,5.205c0,2.142-0.462,3.951-1.384,5.427
c-0.924,1.476-2.198,2.215-3.821,2.215h-29.678v18.715h16.611c1.623,0,2.896,0.664,3.821,1.993
c0.922,1.329,1.384,2.917,1.384,4.761c0,1.7-0.442,3.212-1.329,4.541c-0.886,1.329-2.178,1.993-3.876,1.993h-16.611v18.826h29.678
c1.623,0,2.896,0.738,3.821,2.215c0.922,1.476,1.384,3.285,1.384,5.425c0,1.993-0.444,3.729-1.329,5.206
c-0.886,1.477-2.178,2.215-3.876,2.215h-39.424c-2.068,0-3.839-0.46-5.315-1.385C105.309,458.943,104.572,457.633,104.572,455.934z
"/>
<path style="fill:#FFFFFF;" d="M164.259,455.934v-70.431c0-1.402,0.498-2.62,1.494-3.655c0.997-1.033,2.271-1.55,3.821-1.55h22.923
c18.162,0,27.242,7.9,27.242,23.698c0,11.517-4.468,19.01-13.399,22.48l13.51,24.474c0.369,0.517,0.554,1.182,0.554,1.993
c0,2.142-1.163,4.172-3.489,6.091c-2.325,1.921-4.78,2.878-7.364,2.878c-2.585,0-4.429-1.068-5.537-3.21l-15.172-29.458h-7.309
v26.689c0,1.699-0.85,3.009-2.548,3.93c-1.699,0.925-3.728,1.385-6.091,1.385c-2.364,0-4.393-0.46-6.091-1.385
C165.107,458.943,164.259,457.633,164.259,455.934z M181.534,415.957h10.962c3.249,0,5.721-0.794,7.42-2.381
c1.698-1.587,2.546-4.226,2.546-7.918c0-3.691-0.849-6.33-2.546-7.918c-1.7-1.587-4.172-2.381-7.42-2.381h-10.962V415.957z"/>
<path style="fill:#FFFFFF;" d="M228.931,455.934v-70.431c0-1.402,0.498-2.62,1.494-3.655c0.997-1.033,2.271-1.55,3.821-1.55h22.923
c18.162,0,27.242,7.9,27.242,23.698c0,11.517-4.468,19.01-13.399,22.48l13.51,24.474c0.369,0.517,0.554,1.182,0.554,1.993
c0,2.142-1.163,4.172-3.489,6.091c-2.325,1.921-4.78,2.878-7.364,2.878c-2.585,0-4.429-1.068-5.537-3.21l-15.172-29.458h-7.309
v26.689c0,1.699-0.85,3.009-2.548,3.93c-1.699,0.925-3.728,1.385-6.091,1.385c-2.364,0-4.393-0.46-6.091-1.385
C229.779,458.943,228.931,457.633,228.931,455.934z M246.206,415.957h10.962c3.249,0,5.721-0.794,7.42-2.381
c1.698-1.587,2.547-4.226,2.547-7.918c0-3.691-0.849-6.33-2.547-7.918c-1.7-1.587-4.172-2.381-7.42-2.381h-10.962V415.957z"/>
<path style="fill:#FFFFFF;" d="M292.718,435.779v-29.346c0-9.005,2.473-15.614,7.419-19.822c4.945-4.208,11.518-6.312,19.712-6.312
c8.267,0,14.875,2.104,19.822,6.312c4.945,4.208,7.419,10.817,7.419,19.822v29.346c0,9.008-2.474,15.614-7.419,19.822
c-4.947,4.208-11.555,6.312-19.822,6.312c-8.195,0-14.767-2.104-19.712-6.312C295.191,451.393,292.718,444.788,292.718,435.779z
M309.993,435.779c0,7.383,3.284,11.074,9.856,11.074c6.644,0,9.966-3.691,9.966-11.074v-29.346c0-7.381-3.322-11.073-9.966-11.073
c-6.573,0-9.856,3.692-9.856,11.073V435.779z"/>
<path style="fill:#FFFFFF;" d="M358.164,455.934v-70.431c0-1.402,0.498-2.62,1.495-3.655c0.997-1.033,2.271-1.55,3.821-1.55h22.923
c18.162,0,27.242,7.9,27.242,23.698c0,11.517-4.468,19.01-13.399,22.48l13.51,24.474c0.369,0.517,0.554,1.182,0.554,1.993
c0,2.142-1.163,4.172-3.489,6.091c-2.325,1.921-4.78,2.878-7.364,2.878c-2.585,0-4.429-1.068-5.537-3.21l-15.172-29.458h-7.309
v26.689c0,1.699-0.85,3.009-2.548,3.93c-1.699,0.925-3.728,1.385-6.091,1.385c-2.364,0-4.393-0.46-6.091-1.385
C359.012,458.943,358.164,457.633,358.164,455.934z M375.44,415.957h10.962c3.249,0,5.721-0.794,7.42-2.381
c1.698-1.587,2.547-4.226,2.547-7.918c0-3.691-0.849-6.33-2.547-7.918c-1.7-1.587-4.172-2.381-7.42-2.381H375.44V415.957z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#c546e5"
class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 0 0-1.883 2.542l.857 6a2.25 2.25 0 0 0 2.227 1.932H19.05a2.25 2.25 0 0 0 2.227-1.932l.857-6a2.25 2.25 0 0 0-1.883-2.542m-16.5 0V6A2.25 2.25 0 0 1 6 3.75h3.879a1.5 1.5 0 0 1 1.06.44l2.122 2.12a1.5 1.5 0 0 0 1.06.44H18A2.25 2.25 0 0 1 20.25 9v.776" />
</svg>

After

Width:  |  Height:  |  Size: 544 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#4f46e5"
class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
</svg>

After

Width:  |  Height:  |  Size: 565 B

View File

@ -1 +1,5 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="desktop" class="svg-inline--fa fa-desktop fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M528 0H48C21.5 0 0 21.5 0 48v320c0 26.5 21.5 48 48 48h192l-16 48h-72c-13.3 0-24 10.7-24 24s10.7 24 24 24h272c13.3 0 24-10.7 24-24s-10.7-24-24-24h-72l-16-48h192c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48zm-16 352H64V64h448v288z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#4f46e5"
class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25" />
</svg>

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 476 B

View File

@ -1 +0,0 @@
<svg viewBox="0.22 -0.01 599.63 582.58" xmlns="http://www.w3.org/2000/svg" width="2500" height="2430"><path d="M599.85 297.43C599.85 133.16 465.63-.01 300.03-.01 134.5-.01.22 133.16.22 297.43c0 109.12 59.43 204.21 147.69 256.04l19.22-127.5c-28.48-31.45-45.96-72.91-45.96-118.52 0-98 80.1-177.47 178.86-177.47 98.83 0 178.87 79.47 178.87 177.47 0 46.02-17.69 87.77-46.58 119.21l19.14 127.23c88.67-51.7 148.39-147 148.39-256.46z" fill="#ff7a24"/><path d="M332.93 387.41l36.42 195.16H228.64l36.28-194.82c-35.87-13.61-61.51-48.03-61.51-88.66 0-52.53 42.58-95.1 95.03-95.1 52.53 0 95.03 42.57 95.03 95.1 0 40.29-25.15 74.49-60.54 88.32z" fill="#123467"/></svg>

Before

Width:  |  Height:  |  Size: 655 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#46b3e5"
class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M3.75 3v11.25A2.25 2.25 0 0 0 6 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0 1 18 16.5h-2.25m-7.5 0h7.5m-7.5 0-1 3m8.5-3 1 3m0 0 .5 1.5m-.5-1.5h-9.5m0 0-.5 1.5m.75-9 3-3 2.148 2.148A12.061 12.061 0 0 1 16.5 7.605" />
</svg>

After

Width:  |  Height:  |  Size: 443 B

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