Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
e704a13d6b | |||
9936958259 | |||
f81d928c66 | |||
3c4435701d | |||
ebbd81348a | |||
5debbd4f5d | |||
d846acaa8d | |||
35f896eab1 | |||
25977d2ead | |||
f0da1c6d8c | |||
e2dd9177f7 | |||
5a9e8d6799 | |||
868b70f530 | |||
d07e9bcad2 | |||
0cd815cce6 | |||
5ab6617b5d | |||
72b37c56fd | |||
8a4ef66946 | |||
4517ca7d2a |
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -9,4 +9,4 @@
|
||||
|
||||
To request a feature or suggest an idea please add it to the feedback boards
|
||||
|
||||
https://features.vitodeploy.com/
|
||||
https://vitodeploy.featurebase.app/
|
||||
|
@ -1,6 +1,6 @@
|
||||
|
||||
<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">
|
||||
<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>
|
||||
@ -50,8 +50,6 @@ ## Credits
|
||||
- Alpinejs
|
||||
- HTMX
|
||||
- Vite
|
||||
- Toastr by CodeSeven
|
||||
- Prettier
|
||||
- Postcss
|
||||
- Flowbite
|
||||
- svgrepo.com
|
||||
|
@ -10,10 +10,13 @@ class CreateProject
|
||||
{
|
||||
public function create(User $user, array $input): Project
|
||||
{
|
||||
if (isset($input['name'])) {
|
||||
$input['name'] = strtolower($input['name']);
|
||||
}
|
||||
|
||||
$this->validate($user, $input);
|
||||
|
||||
$project = new Project([
|
||||
'user_id' => $user->id,
|
||||
'name' => $input['name'],
|
||||
]);
|
||||
|
||||
@ -29,7 +32,7 @@ private function validate(User $user, array $input): void
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
'unique:projects,name,NULL,id,user_id,'.$user->id,
|
||||
'unique:projects,name',
|
||||
],
|
||||
])->validate();
|
||||
}
|
||||
|
@ -10,6 +10,10 @@ class UpdateProject
|
||||
{
|
||||
public function update(Project $project, array $input): Project
|
||||
{
|
||||
if (isset($input['name'])) {
|
||||
$input['name'] = strtolower($input['name']);
|
||||
}
|
||||
|
||||
$this->validate($project, $input);
|
||||
|
||||
$project->name = $input['name'];
|
||||
|
35
app/Actions/Server/CreateServerLog.php
Executable 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();
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
use App\Exceptions\DeploymentScriptIsEmptyException;
|
||||
use App\Exceptions\SourceControlIsNotConnected;
|
||||
use App\Models\Deployment;
|
||||
use App\Models\ServerLog;
|
||||
use App\Models\Site;
|
||||
|
||||
class Deploy
|
||||
@ -37,10 +38,15 @@ public function run(Site $site): Deployment
|
||||
$deployment->save();
|
||||
|
||||
dispatch(function () use ($site, $deployment) {
|
||||
$log = $site->server->os()->runScript($site->path, $site->deploymentScript->content, $site->id);
|
||||
$deployment->status = DeploymentStatus::FINISHED;
|
||||
/** @var ServerLog $log */
|
||||
$log = ServerLog::make($site->server, 'deploy-'.strtotime('now'))
|
||||
->forSite($site);
|
||||
$log->save();
|
||||
$deployment->log_id = $log->id;
|
||||
$deployment->save();
|
||||
$site->server->os()->runScript($site->path, $site->deploymentScript->content, $log);
|
||||
$deployment->status = DeploymentStatus::FINISHED;
|
||||
$deployment->save();
|
||||
})->catch(function () use ($deployment) {
|
||||
$deployment->status = DeploymentStatus::FAILED;
|
||||
$deployment->save();
|
||||
|
40
app/Actions/User/CreateUser.php
Normal 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();
|
||||
}
|
||||
}
|
48
app/Actions/User/UpdateUser.php
Normal 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();
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
|
||||
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';
|
||||
|
||||
@ -25,6 +25,7 @@ public function handle(): void
|
||||
'name' => $this->argument('name'),
|
||||
'email' => $this->argument('email'),
|
||||
'password' => bcrypt($this->argument('password')),
|
||||
'role' => $this->option('role'),
|
||||
]);
|
||||
|
||||
$this->info('User created!');
|
||||
|
31
app/Console/Commands/GetMetricsCommand.php
Normal 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");
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ protected function schedule(Schedule $schedule): void
|
||||
$schedule->command('backups:run "0 0 * * 0"')->weekly();
|
||||
$schedule->command('backups:run "0 0 1 * *"')->monthly();
|
||||
$schedule->command('metrics:delete-older-metrics')->daily();
|
||||
$schedule->command('metrics:get')->everyMinute();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7,4 +7,6 @@ final class StorageProvider
|
||||
const DROPBOX = 'dropbox';
|
||||
|
||||
const FTP = 'ftp';
|
||||
|
||||
const LOCAL = 'local';
|
||||
}
|
||||
|
10
app/Enums/UserRole.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
final class UserRole
|
||||
{
|
||||
const USER = 'user';
|
||||
|
||||
const ADMIN = 'admin';
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
namespace App\Facades;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerLog;
|
||||
use App\Support\Testing\SSHFake;
|
||||
use Illuminate\Support\Facades\Facade as FacadeAlias;
|
||||
|
||||
@ -10,7 +11,7 @@
|
||||
* Class SSH
|
||||
*
|
||||
* @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 string exec(string $command, string $log = '', int $siteId = null, ?bool $stream = false)
|
||||
* @method static string assertExecuted(array|string $commands)
|
||||
|
@ -50,14 +50,9 @@ public function init(Server $server, ?string $asUser = null): self
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLog(string $logType, $siteId = null): self
|
||||
public function setLog(ServerLog $log): self
|
||||
{
|
||||
$this->log = $this->server->logs()->create([
|
||||
'site_id' => $siteId,
|
||||
'name' => $this->server->id.'-'.strtotime('now').'-'.$logType.'.log',
|
||||
'type' => $logType,
|
||||
'disk' => config('core.logs_disk'),
|
||||
]);
|
||||
$this->log = $log;
|
||||
|
||||
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
|
||||
{
|
||||
if ($log) {
|
||||
$this->setLog($log, $siteId);
|
||||
} else {
|
||||
$this->log = null;
|
||||
if (! $this->log && $log) {
|
||||
$this->log = $this->server->logs()->create([
|
||||
'site_id' => $siteId,
|
||||
'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 {
|
||||
@ -132,8 +135,8 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
|
||||
|
||||
$this->log?->write($output);
|
||||
|
||||
if (Str::contains($output, 'VITO_SSH_ERROR')) {
|
||||
throw new SSHCommandError('SSH command failed with an error');
|
||||
if ($this->connection->getExitStatus() !== 0 || Str::contains($output, 'VITO_SSH_ERROR')) {
|
||||
throw new SSHCommandError('SSH command failed with an error', $this->connection->getExitStatus());
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
@ -22,6 +22,8 @@ class ApplicationController extends Controller
|
||||
{
|
||||
public function deploy(Server $server, Site $site): HtmxResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
try {
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return back()->with('content', $deployment->log?->getContent());
|
||||
}
|
||||
|
||||
public function updateDeploymentScript(Server $server, Site $site, Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(UpdateDeploymentScript::class)->update($site, $request->input());
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(UpdateBranch::class)->update($site, $request->input());
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return back()->with('env', $site->getEnv());
|
||||
}
|
||||
|
||||
public function updateEnv(Server $server, Site $site, Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(UpdateEnv::class)->update($site, $request->input());
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
if (! $site->isAutoDeployment()) {
|
||||
try {
|
||||
$site->enableAutoDeployment();
|
||||
@ -101,6 +115,8 @@ public function enableAutoDeployment(Server $server, Site $site): HtmxResponse
|
||||
|
||||
public function disableAutoDeployment(Server $server, Site $site): HtmxResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
if ($site->isAutoDeployment()) {
|
||||
try {
|
||||
$site->disableAutoDeployment();
|
||||
|
@ -11,6 +11,8 @@ class ConsoleController extends Controller
|
||||
{
|
||||
public function index(Server $server): View
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return view('console.index', [
|
||||
'server' => $server,
|
||||
]);
|
||||
@ -18,6 +20,8 @@ public function index(Server $server): View
|
||||
|
||||
public function run(Server $server, Request $request)
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$this->validate($request, [
|
||||
'user' => [
|
||||
'required',
|
||||
|
@ -16,6 +16,8 @@ class CronjobController extends Controller
|
||||
{
|
||||
public function index(Server $server): View
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return view('cronjobs.index', [
|
||||
'server' => $server,
|
||||
'cronjobs' => $server->cronJobs,
|
||||
@ -24,6 +26,8 @@ public function index(Server $server): View
|
||||
|
||||
public function store(Server $server, Request $request): HtmxResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(CreateCronJob::class)->create($server, $request->input());
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(DeleteCronJob::class)->delete($server, $cronJob);
|
||||
|
||||
Toast::success('Cronjob deleted successfully.');
|
||||
|
@ -18,6 +18,8 @@ class DatabaseBackupController extends Controller
|
||||
{
|
||||
public function show(Server $server, Backup $backup): View
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return view('databases.backups', [
|
||||
'server' => $server,
|
||||
'databases' => $server->databases,
|
||||
@ -28,6 +30,8 @@ public function show(Server $server, Backup $backup): View
|
||||
|
||||
public function run(Server $server, Backup $backup): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(RunBackup::class)->run($backup);
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(CreateBackup::class)->create('database', $server, $request->input());
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$backup->delete();
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(RestoreBackup::class)->restore($backupFile, $request->input());
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$backupFile->delete();
|
||||
|
||||
$backupFile
|
||||
->backup
|
||||
->storage
|
||||
->provider()
|
||||
->ssh($server)
|
||||
->delete($backupFile->storagePath());
|
||||
|
||||
Toast::success('Backup file deleted successfully.');
|
||||
|
||||
return back();
|
||||
|
@ -17,6 +17,8 @@ class DatabaseController extends Controller
|
||||
{
|
||||
public function index(Server $server): View
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return view('databases.index', [
|
||||
'server' => $server,
|
||||
'databases' => $server->databases,
|
||||
@ -27,6 +29,8 @@ public function index(Server $server): View
|
||||
|
||||
public function store(Server $server, Request $request): HtmxResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$database = app(CreateDatabase::class)->create($server, $request->input());
|
||||
|
||||
if ($request->input('user')) {
|
||||
@ -40,6 +44,8 @@ public function store(Server $server, Request $request): HtmxResponse
|
||||
|
||||
public function destroy(Server $server, Database $database): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(DeleteDatabase::class)->delete($server, $database);
|
||||
|
||||
Toast::success('Database deleted successfully.');
|
||||
|
@ -16,6 +16,8 @@ class DatabaseUserController extends Controller
|
||||
{
|
||||
public function store(Server $server, Request $request): HtmxResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$database = app(CreateDatabaseUser::class)->create($server, $request->input());
|
||||
|
||||
if ($request->input('user')) {
|
||||
@ -29,6 +31,8 @@ public function store(Server $server, Request $request): HtmxResponse
|
||||
|
||||
public function destroy(Server $server, DatabaseUser $databaseUser): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(DeleteDatabaseUser::class)->delete($server, $databaseUser);
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return back()->with([
|
||||
'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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(LinkUser::class)->link($databaseUser, $request->input());
|
||||
|
||||
Toast::success('Database linked successfully.');
|
||||
|
@ -16,6 +16,8 @@ class FirewallController extends Controller
|
||||
{
|
||||
public function index(Server $server): View
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return view('firewall.index', [
|
||||
'server' => $server,
|
||||
'rules' => $server->firewallRules,
|
||||
@ -24,6 +26,8 @@ public function index(Server $server): View
|
||||
|
||||
public function store(Server $server, Request $request): HtmxResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(CreateRule::class)->create($server, $request->input());
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(DeleteRule::class)->delete($server, $firewallRule);
|
||||
|
||||
Toast::success('Firewall rule deleted!');
|
||||
|
@ -15,6 +15,8 @@ class MetricController extends Controller
|
||||
{
|
||||
public function index(Server $server, Request $request): View|RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$this->checkIfMonitoringServiceInstalled($server);
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$this->checkIfMonitoringServiceInstalled($server);
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
if (! $server->monitoring()) {
|
||||
abort(404, 'Monitoring service is not installed on this server');
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ class PHPController extends Controller
|
||||
{
|
||||
public function index(Server $server): View
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return view('php.index', [
|
||||
'server' => $server,
|
||||
'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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
try {
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(InstallPHPExtension::class)->install($server, $request->input());
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(ChangeDefaultCli::class)->change($server, $request->input());
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$ini = app(GetPHPIni::class)->getIni($server, $request->input());
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(UpdatePHPIni::class)->update($server, $request->input());
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(UninstallPHP::class)->uninstall($server, $request->input());
|
||||
|
||||
Toast::success('PHP is being uninstalled!');
|
||||
|
@ -1,11 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\User\UpdateUserPassword;
|
||||
use App\Actions\User\UpdateUserProfileInformation;
|
||||
use App\Facades\Toast;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@ -14,7 +13,7 @@ class ProfileController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('settings.profile.index');
|
||||
return view('profile.index');
|
||||
}
|
||||
|
||||
public function info(Request $request): RedirectResponse
|
@ -19,6 +19,8 @@ class QueueController extends Controller
|
||||
{
|
||||
public function index(Server $server, Site $site): View
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return view('queues.index', [
|
||||
'server' => $server,
|
||||
'site' => $site,
|
||||
@ -28,6 +30,8 @@ public function index(Server $server, Site $site): View
|
||||
|
||||
public function store(Server $server, Site $site, Request $request): HtmxResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(CreateQueue::class)->create($site, $request->input());
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(ManageQueue::class)->{$action}($queue);
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(DeleteQueue::class)->delete($queue);
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return back()->with('content', app(GetQueueLogs::class)->getLogs($queue));
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,8 @@ class SSHKeyController extends Controller
|
||||
{
|
||||
public function index(Server $server): View
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return view('server-ssh-keys.index', [
|
||||
'server' => $server,
|
||||
'keys' => $server->sshKeys,
|
||||
@ -25,6 +27,8 @@ public function index(Server $server): View
|
||||
|
||||
public function store(Server $server, Request $request): HtmxResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
/** @var \App\Models\SshKey $key */
|
||||
$key = app(CreateSshKey::class)->create(
|
||||
$request->user(),
|
||||
@ -38,6 +42,8 @@ public function store(Server $server, Request $request): HtmxResponse
|
||||
|
||||
public function destroy(Server $server, SshKey $sshKey): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(DeleteKeyFromServer::class)->delete($server, $sshKey);
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(DeployKeyToServer::class)->deploy(
|
||||
$request->user(),
|
||||
$server,
|
||||
|
@ -17,6 +17,8 @@ class SSLController extends Controller
|
||||
{
|
||||
public function index(Server $server, Site $site): View
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return view('ssls.index', [
|
||||
'server' => $server,
|
||||
'site' => $site,
|
||||
@ -26,6 +28,8 @@ public function index(Server $server, Site $site): View
|
||||
|
||||
public function store(Server $server, Site $site, Request $request): HtmxResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(CreateSSL::class)->create($site, $request->input());
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(DeleteSSL::class)->delete($ssl);
|
||||
|
||||
Toast::success('SSL certificate has been deleted.');
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@ -23,10 +24,22 @@ public function search(Request $request): JsonResponse
|
||||
$query->where('name', '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();
|
||||
|
||||
$sites = Site::query()
|
||||
->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();
|
||||
|
||||
$result = [];
|
||||
|
@ -19,6 +19,9 @@ public function index(): View
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
$this->authorize('viewAny', [Server::class, $user->currentProject]);
|
||||
|
||||
$servers = $user->currentProject->servers()->orderByDesc('created_at')->get();
|
||||
|
||||
return view('servers.index', compact('servers'));
|
||||
@ -26,6 +29,11 @@ public function index(): 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));
|
||||
$serverProviders = ServerProvider::query()->where('provider', $provider)->get();
|
||||
|
||||
@ -40,8 +48,13 @@ public function create(Request $request): View
|
||||
*/
|
||||
public function store(Request $request): HtmxResponse
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
$this->authorize('create', [Server::class, $user->currentProject]);
|
||||
|
||||
$server = app(CreateServer::class)->create(
|
||||
$request->user(),
|
||||
$user,
|
||||
$request->input()
|
||||
);
|
||||
|
||||
@ -52,14 +65,17 @@ public function store(Request $request): HtmxResponse
|
||||
|
||||
public function show(Server $server): View
|
||||
{
|
||||
$this->authorize('view', $server);
|
||||
|
||||
return view('servers.show', [
|
||||
'server' => $server,
|
||||
'logs' => $server->logs()->latest()->limit(10)->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(Server $server): RedirectResponse
|
||||
{
|
||||
$this->authorize('delete', $server);
|
||||
|
||||
$server->delete();
|
||||
|
||||
Toast::success('Server deleted successfully.');
|
||||
|
@ -2,22 +2,30 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Server\CreateServerLog;
|
||||
use App\Facades\Toast;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerLog;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ServerLogController extends Controller
|
||||
{
|
||||
public function index(Server $server): View
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return view('server-logs.index', [
|
||||
'server' => $server,
|
||||
'pageTitle' => __('Vito Logs'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Server $server, ServerLog $serverLog): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
if ($server->id != $serverLog->server_id) {
|
||||
abort(404);
|
||||
}
|
||||
@ -26,4 +34,37 @@ public function show(Server $server, ServerLog $serverLog): RedirectResponse
|
||||
'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]);
|
||||
}
|
||||
}
|
||||
|
@ -15,11 +15,15 @@ class ServerSettingController extends Controller
|
||||
{
|
||||
public function index(Server $server): View
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return view('server-settings.index', compact('server'));
|
||||
}
|
||||
|
||||
public function checkConnection(Server $server): RedirectResponse|HtmxResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$oldStatus = $server->status;
|
||||
|
||||
$server = $server->checkConnection();
|
||||
@ -41,6 +45,8 @@ public function checkConnection(Server $server): RedirectResponse|HtmxResponse
|
||||
|
||||
public function reboot(Server $server): HtmxResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(RebootServer::class)->reboot($server);
|
||||
|
||||
Toast::info('Server is rebooting.');
|
||||
@ -50,6 +56,8 @@ public function reboot(Server $server): HtmxResponse
|
||||
|
||||
public function edit(Request $request, Server $server): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(EditServer::class)->edit($server, $request->input());
|
||||
|
||||
Toast::success('Server updated.');
|
||||
|
@ -16,6 +16,8 @@ class ServiceController extends Controller
|
||||
{
|
||||
public function index(Server $server): View
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return view('services.index', [
|
||||
'server' => $server,
|
||||
'services' => $server->services,
|
||||
@ -24,6 +26,8 @@ public function index(Server $server): View
|
||||
|
||||
public function start(Server $server, Service $service): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$service->start();
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$service->stop();
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$service->restart();
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$service->enable();
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$service->disable();
|
||||
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(Install::class)->install($server, $request->input());
|
||||
|
||||
Toast::success('Service is being uninstalled!');
|
||||
Toast::success('Service is being installed!');
|
||||
|
||||
return htmx()->back();
|
||||
}
|
||||
|
||||
public function uninstall(Server $server, Service $service): HtmxResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(Uninstall::class)->uninstall($service);
|
||||
|
||||
Toast::success('Service is being uninstalled!');
|
||||
|
@ -29,7 +29,7 @@ public function add(Request $request): HtmxResponse
|
||||
|
||||
Toast::success('Channel added successfully');
|
||||
|
||||
return htmx()->redirect(route('notification-channels'));
|
||||
return htmx()->redirect(route('settings.notification-channels'));
|
||||
}
|
||||
|
||||
public function delete(int $id): RedirectResponse
|
||||
@ -40,6 +40,6 @@ public function delete(int $id): RedirectResponse
|
||||
|
||||
Toast::success('Channel deleted successfully');
|
||||
|
||||
return redirect()->route('notification-channels');
|
||||
return redirect()->route('settings.notification-channels');
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ class ProjectController extends Controller
|
||||
public function index(): View
|
||||
{
|
||||
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.');
|
||||
|
||||
return htmx()->redirect(route('projects'));
|
||||
return htmx()->redirect(route('settings.projects'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Project $project): HtmxResponse
|
||||
@ -42,7 +42,7 @@ public function update(Request $request, Project $project): HtmxResponse
|
||||
|
||||
Toast::success('Project updated.');
|
||||
|
||||
return htmx()->redirect(route('projects'));
|
||||
return htmx()->redirect(route('settings.projects'));
|
||||
}
|
||||
|
||||
public function delete(Project $project): RedirectResponse
|
||||
@ -74,6 +74,8 @@ public function switch($projectId): RedirectResponse
|
||||
/** @var Project $project */
|
||||
$project = $user->projects()->findOrFail($projectId);
|
||||
|
||||
$this->authorize('view', $project);
|
||||
|
||||
$user->current_project_id = $project->id;
|
||||
$user->save();
|
||||
|
||||
|
@ -29,7 +29,7 @@ public function add(Request $request): HtmxResponse
|
||||
|
||||
Toast::success('SSH Key added');
|
||||
|
||||
return htmx()->redirect(route('ssh-keys'));
|
||||
return htmx()->redirect(route('settings.ssh-keys'));
|
||||
}
|
||||
|
||||
public function delete(int $id): RedirectResponse
|
||||
@ -40,6 +40,6 @@ public function delete(int $id): RedirectResponse
|
||||
|
||||
Toast::success('SSH Key deleted');
|
||||
|
||||
return redirect()->route('ssh-keys');
|
||||
return redirect()->route('settings.ssh-keys');
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ public function connect(Request $request): HtmxResponse
|
||||
|
||||
Toast::success('Server provider connected.');
|
||||
|
||||
return htmx()->redirect(route('server-providers'));
|
||||
return htmx()->redirect(route('settings.server-providers'));
|
||||
}
|
||||
|
||||
public function delete(ServerProvider $serverProvider): RedirectResponse
|
||||
|
@ -29,7 +29,7 @@ public function connect(Request $request): HtmxResponse
|
||||
|
||||
Toast::success('Source control connected.');
|
||||
|
||||
return htmx()->redirect(route('source-controls'));
|
||||
return htmx()->redirect(route('settings.source-controls'));
|
||||
}
|
||||
|
||||
public function delete(SourceControl $sourceControl): RedirectResponse
|
||||
@ -44,6 +44,6 @@ public function delete(SourceControl $sourceControl): RedirectResponse
|
||||
|
||||
Toast::success('Source control deleted.');
|
||||
|
||||
return redirect()->route('source-controls');
|
||||
return redirect()->route('settings.source-controls');
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ public function connect(Request $request): HtmxResponse
|
||||
|
||||
Toast::success('Storage provider connected.');
|
||||
|
||||
return htmx()->redirect(route('storage-providers'));
|
||||
return htmx()->redirect(route('settings.storage-providers'));
|
||||
}
|
||||
|
||||
public function delete(StorageProvider $storageProvider): RedirectResponse
|
||||
|
78
app/Http/Controllers/Settings/UserController.php
Normal 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');
|
||||
}
|
||||
}
|
@ -19,6 +19,8 @@ class SiteController extends Controller
|
||||
{
|
||||
public function index(Server $server): View
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return view('sites.index', [
|
||||
'server' => $server,
|
||||
'sites' => $server->sites()->orderByDesc('id')->get(),
|
||||
@ -27,6 +29,8 @@ public function index(Server $server): View
|
||||
|
||||
public function store(Server $server, Request $request): HtmxResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$site = app(CreateSite::class)->create($server, $request->input());
|
||||
|
||||
Toast::success('Site created');
|
||||
@ -36,6 +40,8 @@ public function store(Server $server, Request $request): HtmxResponse
|
||||
|
||||
public function create(Server $server): View
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return view('sites.create', [
|
||||
'server' => $server,
|
||||
'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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
if (in_array($site->status, [SiteStatus::INSTALLING, SiteStatus::INSTALLATION_FAILED])) {
|
||||
if ($request->hasHeader('HX-Request')) {
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
if (! in_array($site->status, [SiteStatus::INSTALLING, SiteStatus::INSTALLATION_FAILED])) {
|
||||
if ($request->hasHeader('HX-Request')) {
|
||||
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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
app(DeleteSite::class)->delete($site);
|
||||
|
||||
Toast::success('Site is being deleted');
|
||||
|
@ -10,9 +10,12 @@ class SiteLogController extends Controller
|
||||
{
|
||||
public function index(Server $server, Site $site): View
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return view('site-logs.index', [
|
||||
'server' => $server,
|
||||
'site' => $site,
|
||||
'pageTitle' => __('Vito Logs'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ class SiteSettingController extends Controller
|
||||
{
|
||||
public function index(Server $server, Site $site): View
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
return view('site-settings.index', [
|
||||
'server' => $server,
|
||||
'site' => $site,
|
||||
@ -26,6 +28,8 @@ public function index(Server $server, Site $site): View
|
||||
|
||||
public function getVhost(Server $server, Site $site): RedirectResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
/** @var 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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$this->validate($request, [
|
||||
'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
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$this->validate($request, [
|
||||
'version' => [
|
||||
'required',
|
||||
@ -73,6 +81,8 @@ public function updatePHPVersion(Server $server, Site $site, Request $request):
|
||||
|
||||
public function updateSourceControl(Server $server, Site $site, Request $request): HtmxResponse
|
||||
{
|
||||
$this->authorize('manage', $server);
|
||||
|
||||
$site = app(UpdateSourceControl::class)->update($site, $request->input());
|
||||
|
||||
Toast::success('Source control updated successfully!');
|
||||
|
@ -68,5 +68,6 @@ class Kernel extends HttpKernel
|
||||
'server-is-ready' => ServerIsReadyMiddleware::class,
|
||||
'handle-ssh-errors' => HandleSSHErrors::class,
|
||||
'select-current-project' => SelectCurrentProject::class,
|
||||
'is-admin' => \App\Http\Middleware\IsAdmin::class,
|
||||
];
|
||||
}
|
||||
|
@ -14,17 +14,17 @@ class HandleSSHErrors
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$res = $next($request);
|
||||
if ($res instanceof Response && $res->exception) {
|
||||
if ($res->exception instanceof SSHConnectionError || $res->exception instanceof SSHCommandError) {
|
||||
Toast::error($res->exception->getMessage());
|
||||
// if ($res instanceof Response && $res->exception) {
|
||||
// if ($res->exception instanceof SSHConnectionError || $res->exception instanceof SSHCommandError) {
|
||||
// Toast::error($res->exception->getMessage());
|
||||
|
||||
if ($request->hasHeader('HX-Request')) {
|
||||
return htmx()->back();
|
||||
}
|
||||
// if ($request->hasHeader('HX-Request')) {
|
||||
// return htmx()->back();
|
||||
// }
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
// return back();
|
||||
// }
|
||||
// }
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
25
app/Http/Middleware/IsAdmin.php
Normal 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);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
@ -53,4 +54,9 @@ public function notificationChannels(): HasMany
|
||||
{
|
||||
return $this->hasMany(NotificationChannel::class);
|
||||
}
|
||||
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'user_project')->withTimestamps();
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
* @property string $disk
|
||||
* @property Server $server
|
||||
* @property ?Site $site
|
||||
* @property bool $is_remote
|
||||
*/
|
||||
class ServerLog extends AbstractModel
|
||||
{
|
||||
@ -27,11 +28,13 @@ class ServerLog extends AbstractModel
|
||||
'type',
|
||||
'name',
|
||||
'disk',
|
||||
'is_remote',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'server_id' => 'integer',
|
||||
'site_id' => 'integer',
|
||||
'is_remote' => 'boolean',
|
||||
];
|
||||
|
||||
public static function boot(): void
|
||||
@ -64,6 +67,17 @@ public function site(): BelongsTo
|
||||
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
|
||||
{
|
||||
if (Str::contains($buf, 'VITO_SSH_ERROR')) {
|
||||
@ -78,6 +92,10 @@ public function write($buf): void
|
||||
|
||||
public function getContent(): ?string
|
||||
{
|
||||
if ($this->is_remote) {
|
||||
return $this->server->os()->tail($this->name, 150);
|
||||
}
|
||||
|
||||
if (Storage::disk($this->disk)->exists($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->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;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,9 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\SourceControlIsNotConnected;
|
||||
use App\Exceptions\SSHError;
|
||||
use App\SiteTypes\SiteType;
|
||||
use App\SSH\Services\Webserver\Webserver;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@ -185,7 +187,9 @@ public function php(): ?Service
|
||||
|
||||
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->save();
|
||||
}
|
||||
@ -268,6 +272,10 @@ public function hasFeature(string $feature): bool
|
||||
|
||||
public function getEnv(): string
|
||||
{
|
||||
try {
|
||||
return $this->server->os()->readFile($this->path.'/.env');
|
||||
} catch (SSHError) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
@ -30,6 +32,7 @@
|
||||
* @property int $current_project_id
|
||||
* @property Project $currentProject
|
||||
* @property Collection<Project> $projects
|
||||
* @property string $role
|
||||
*/
|
||||
class User extends Authenticatable
|
||||
{
|
||||
@ -43,6 +46,7 @@ class User extends Authenticatable
|
||||
'password',
|
||||
'timezone',
|
||||
'current_project_id',
|
||||
'role',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
@ -60,7 +64,9 @@ public static function boot(): void
|
||||
parent::boot();
|
||||
|
||||
static::created(function (User $user) {
|
||||
if (Project::count() === 0) {
|
||||
$user->createDefaultProject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -117,9 +123,9 @@ public function connectedSourceControls(): array
|
||||
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
|
||||
@ -138,9 +144,10 @@ public function createDefaultProject(): Project
|
||||
|
||||
if (! $project) {
|
||||
$project = new Project();
|
||||
$project->user_id = $this->id;
|
||||
$project->name = 'Default';
|
||||
$project->name = 'default';
|
||||
$project->save();
|
||||
|
||||
$project->users()->attach($this->id);
|
||||
}
|
||||
|
||||
$this->current_project_id = $project->id;
|
||||
@ -148,4 +155,9 @@ public function createDefaultProject(): Project
|
||||
|
||||
return $project;
|
||||
}
|
||||
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->role === UserRole::ADMIN;
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ public function connect(): bool
|
||||
__('Congratulations! 🎉'),
|
||||
__("You've connected your Discord to :app", ['app' => config('app.name')])."\n".
|
||||
__('Manage your notification channels')."\n".
|
||||
route('notification-channels')
|
||||
route('settings.notification-channels')
|
||||
);
|
||||
|
||||
if (! $connect) {
|
||||
|
@ -35,7 +35,7 @@ public function connect(): bool
|
||||
__('Congratulations! 🎉'),
|
||||
__("You've connected your Slack to :app", ['app' => config('app.name')])."\n".
|
||||
__('Manage your notification channels')."\n".
|
||||
route('notification-channels')
|
||||
route('settings.notification-channels')
|
||||
);
|
||||
|
||||
if (! $connect) {
|
||||
|
35
app/Policies/ProjectPolicy.php
Normal 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;
|
||||
}
|
||||
}
|
41
app/Policies/ServerPolicy.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
if ($serverLog) {
|
||||
$ssh->setLog($serverLog);
|
||||
}
|
||||
$ssh->exec(
|
||||
$this->getScript('run-script.sh', [
|
||||
'path' => $path,
|
||||
'script' => $script,
|
||||
]),
|
||||
'run-script',
|
||||
$siteId
|
||||
'run-script'
|
||||
);
|
||||
|
||||
return $ssh->log;
|
||||
@ -156,4 +168,21 @@ public function cleanup(): void
|
||||
'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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
[ -f __path__ ] && cat __path__
|
||||
[ -f __path__ ] && sudo cat __path__
|
||||
|
7
app/SSH/OS/scripts/resource-info.sh
Normal 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//')"
|
1
app/SSH/OS/scripts/tail.sh
Normal file
@ -0,0 +1 @@
|
||||
sudo tail -n __lines__ __path__
|
@ -159,7 +159,7 @@ public function runBackup(BackupFile $backupFile): void
|
||||
);
|
||||
|
||||
// cleanup
|
||||
$this->service->server->ssh()->exec('rm '.$backupFile->name.'.zip');
|
||||
$this->service->server->ssh()->exec('rm '.$backupFile->path());
|
||||
|
||||
$backupFile->size = $upload['size'];
|
||||
$backupFile->save();
|
||||
|
53
app/SSH/Services/Monitoring/RemoteMonitor/RemoteMonitor.php
Normal 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();
|
||||
}
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\SSH\Services\VitoAgent;
|
||||
namespace App\SSH\Services\Monitoring\VitoAgent;
|
||||
|
||||
use App\Models\Metric;
|
||||
use App\SSH\HasScripts;
|
||||
use App\SSH\Services\AbstractService;
|
||||
use Closure;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
@ -21,7 +22,12 @@ public function creationRules(array $input): array
|
||||
{
|
||||
return [
|
||||
'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' => [
|
||||
'required',
|
@ -44,4 +44,12 @@ public function download(string $src, string $dest): void
|
||||
'download-from-dropbox'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO Implement delete method
|
||||
*/
|
||||
public function delete(string $path): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
@ -45,4 +45,12 @@ public function download(string $src, string $dest): void
|
||||
'download-from-ftp'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO Implement delete method
|
||||
*/
|
||||
public function delete(string $path): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
49
app/SSH/Storage/Local.php
Normal 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'
|
||||
);
|
||||
}
|
||||
}
|
@ -7,4 +7,6 @@ interface Storage
|
||||
public function upload(string $src, string $dest): array;
|
||||
|
||||
public function download(string $src, string $dest): void;
|
||||
|
||||
public function delete(string $path): void;
|
||||
}
|
||||
|
1
app/SSH/Storage/scripts/local/delete.sh
Normal file
@ -0,0 +1 @@
|
||||
rm __path__
|
1
app/SSH/Storage/scripts/local/download.sh
Normal file
@ -0,0 +1 @@
|
||||
cp __src__ __dest__
|
2
app/SSH/Storage/scripts/local/upload.sh
Normal file
@ -0,0 +1,2 @@
|
||||
mkdir -p __dest_dir__
|
||||
cp __src__ __dest_dir__/__dest_file__
|
38
app/StorageProviders/Local.php
Normal 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
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
if ($log) {
|
||||
$this->setLog($log, $siteId);
|
||||
} else {
|
||||
$this->log = null;
|
||||
if (! $this->log && $log) {
|
||||
$this->log = $this->server->logs()->create([
|
||||
'site_id' => $siteId,
|
||||
'name' => $this->server->id.'-'.strtotime('now').'-'.$log.'.log',
|
||||
'type' => $log,
|
||||
'disk' => config('core.logs_disk'),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->commands[] = $command;
|
||||
|
@ -32,7 +32,12 @@ function htmx(): HtmxResponse
|
||||
|
||||
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
|
||||
|
@ -5,10 +5,10 @@
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class ProfileLayout extends Component
|
||||
class SettingsLayout extends Component
|
||||
{
|
||||
public function render(): View
|
||||
{
|
||||
return view('layouts.profile');
|
||||
return view('layouts.settings');
|
||||
}
|
||||
}
|
@ -142,6 +142,7 @@
|
||||
'ufw' => 'firewall',
|
||||
'supervisor' => 'process_manager',
|
||||
'vito-agent' => 'monitoring',
|
||||
'remote-monitor' => 'monitoring',
|
||||
],
|
||||
'service_handlers' => [
|
||||
'nginx' => \App\SSH\Services\Webserver\Nginx::class,
|
||||
@ -152,7 +153,8 @@
|
||||
'php' => \App\SSH\Services\PHP\PHP::class,
|
||||
'ufw' => \App\SSH\Services\Firewall\Ufw::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' => [
|
||||
'nginx' => [
|
||||
@ -357,10 +359,12 @@
|
||||
'storage_providers' => [
|
||||
\App\Enums\StorageProvider::DROPBOX,
|
||||
\App\Enums\StorageProvider::FTP,
|
||||
\App\Enums\StorageProvider::LOCAL,
|
||||
],
|
||||
'storage_providers_class' => [
|
||||
'dropbox' => \App\StorageProviders\Dropbox::class,
|
||||
'ftp' => \App\StorageProviders\Ftp::class,
|
||||
\App\Enums\StorageProvider::DROPBOX => \App\StorageProviders\Dropbox::class,
|
||||
\App\Enums\StorageProvider::FTP => \App\StorageProviders\Ftp::class,
|
||||
\App\Enums\StorageProvider::LOCAL => \App\StorageProviders\Local::class,
|
||||
],
|
||||
|
||||
'ssl_types' => [
|
||||
|
@ -16,7 +16,6 @@ class ProjectFactory extends Factory
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => $this->faker->randomNumber(),
|
||||
'name' => $this->faker->name(),
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
|
@ -7,7 +7,6 @@
|
||||
use App\Enums\ServerStatus;
|
||||
use App\Enums\ServerType;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class ServerFactory extends Factory
|
||||
@ -16,11 +15,7 @@ class ServerFactory extends Factory
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = User::factory()->create();
|
||||
|
||||
return [
|
||||
'user_id' => $user->id,
|
||||
'name' => $this->faker->name(),
|
||||
'ssh_user' => 'vito',
|
||||
'ip' => $this->faker->ipv4(),
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
@ -18,6 +19,7 @@ public function definition(): array
|
||||
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
|
||||
'remember_token' => Str::random(10),
|
||||
'timezone' => 'UTC',
|
||||
'role' => UserRole::ADMIN,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -9,15 +9,13 @@
|
||||
public function up(): void
|
||||
{
|
||||
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->string('profile')->after('user_id');
|
||||
$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
|
||||
@ -27,9 +25,9 @@ public function down(): void
|
||||
$table->string('refresh_token')->nullable();
|
||||
$table->string('token_expires_at')->nullable();
|
||||
$table->string('label')->nullable();
|
||||
$table->dropColumn('user_id');
|
||||
$table->dropColumn('profile');
|
||||
$table->dropColumn('credentials');
|
||||
});
|
||||
Schema::table('storage_providers', function (Blueprint $table) {
|
||||
$table->dropColumn(['user_id', 'profile', 'credentials']);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
@ -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');
|
||||
}
|
||||
};
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
@ -19,7 +19,7 @@ RUN apt-get install -y nginx
|
||||
|
||||
# php
|
||||
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 \
|
||||
&& add-apt-repository ppa:ondrej/php -y \
|
||||
&& apt-get update \
|
||||
@ -44,6 +44,8 @@ RUN rm /etc/nginx/sites-enabled/default
|
||||
COPY docker/nginx.conf /etc/nginx/sites-available/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
|
||||
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
|
@ -37,6 +37,8 @@ php /var/www/html/artisan view:cache
|
||||
|
||||
php /var/www/html/artisan user:create "$NAME" "$EMAIL" "$PASSWORD"
|
||||
|
||||
cron
|
||||
|
||||
echo "Vito is running! 🚀"
|
||||
|
||||
/usr/bin/supervisord
|
||||
|
2228
package-lock.json
generated
@ -21,7 +21,6 @@
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"tailwindcss": "^3.1.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"toastr": "^2.1.4",
|
||||
"vite": "^4.5.3",
|
||||
"apexcharts": "^3.44.2",
|
||||
"flowbite-datepicker": "^1.2.6"
|
||||
|
732
public/build/assets/app-01264060.js
Normal file
1
public/build/assets/app-268661bd.css
Normal file
@ -1,6 +1,6 @@
|
||||
{
|
||||
"resources/css/app.css": {
|
||||
"file": "assets/app-53e4d707.css",
|
||||
"file": "assets/app-268661bd.css",
|
||||
"isEntry": true,
|
||||
"src": "resources/css/app.css"
|
||||
},
|
||||
@ -12,7 +12,7 @@
|
||||
"css": [
|
||||
"assets/app-a1ae07b3.css"
|
||||
],
|
||||
"file": "assets/app-66009dff.js",
|
||||
"file": "assets/app-01264060.js",
|
||||
"isEntry": true,
|
||||
"src": "resources/js/app.js"
|
||||
}
|
||||
|
@ -1,74 +1,5 @@
|
||||
<?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 58 58" style="enable-background:new 0 0 58 58;" xml:space="preserve">
|
||||
<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 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="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" />
|
||||
</svg>
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 505 B |
@ -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 |
@ -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 |
5
public/static/images/ftp.svg
Normal 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 |
5
public/static/images/local.svg
Normal 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 |
@ -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 |
@ -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 |
5
public/static/images/remote-monitor.svg
Normal 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 |