Compare commits

..

No commits in common. "1.x" and "1.0.0" have entirely different histories.
1.x ... 1.0.0

548 changed files with 25962 additions and 15988 deletions

View File

@ -12,5 +12,5 @@ MAIL_PORT=
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="noreply@${APP_NAME}"
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"

View File

@ -12,5 +12,5 @@ MAIL_PORT=
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="noreply@${APP_NAME}"
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"

View File

@ -13,5 +13,5 @@ MAIL_PORT=
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="noreply@${APP_NAME}"
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"

View File

@ -1,11 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Feature request
url: https://github.com/vitodeploy/vito/discussions/new?category=ideas
about: Share ideas for new features
- name: Support
url: https://github.com/vitodeploy/vito/discussions/new?category=q-a
about: Ask the community for help
- name: Discord
url: https://discord.gg/uZeeHZZnm5
about: Join the community

View File

@ -0,0 +1,12 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature
assignees: ''
---
To request a feature or suggest an idea please add it to the feedback boards
https://features.vitodeploy.com/

View File

@ -1,35 +0,0 @@
name: Build and push Docker image
on:
push:
branches:
- 1.x
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
run: |
docker buildx build . \
-f docker/Dockerfile \
-t vitodeploy/vito:1.x \
--build-arg="RELEASE=0" \
--platform linux/amd64,linux/arm64 \
--no-cache \
--push

View File

@ -1,35 +0,0 @@
name: Build and push Docker image
on:
release:
types: [created]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
run: |
docker buildx build . \
-f docker/Dockerfile \
-t vitodeploy/vito:${{ github.event.release.tag_name }} \
-t vitodeploy/vito:latest \
--build-arg="RELEASE=0" \
--platform linux/amd64,linux/arm64 \
--no-cache \
--push

2
.gitignore vendored
View File

@ -7,8 +7,6 @@
/storage/test-key
/storage/test-key.pub
/vendor
/storage/database.sqlite
/storage/database-test.sqlite
.env
.env.backup
.env.production

View File

@ -1,5 +1,23 @@
# Contributing
Thank you for your interest in contributing! There are a couple of contribution guidelines that make it easier to apply the incoming suggestions.
Please read the contribution guide on the website
If you want to contribute please start with the issues. Issues labeled with "Bug" are the higher priorities.
https://vitodeploy.com/introduction/contribution-guide.html
## Issues
1. Issues are the best place to propose a new feature.
2. If you are adding a feature that there is no issue for yet, please first open an issue and label it as "feature" and lets discuss it before you implement it.
3. Search the issues before proposing a feature to see if it is already under discussion. Referencing existing issues is a good way to increase the priority of your own.
4. We don't have an issue template yet, but the more detailed your explanation, the more quickly we'll be able to evaluate it.
5. Search for the issue that you also have. Give it a reaction (and comment, if you have something to add). We note that!
## Pull Requests
1. Open PRs represent issues that we're actively thinking about merging (at a pace we can manage). If we think a proposal needs more discussion, or that the existing code would require a lot of back-and-forth to merge, we might close it and suggest you make an issue.
2. All PRs should be made against the `main` branch. This can be changed in the future.
3. If you are making changes to the front-end layer, Please build the assets via `npm run build` and push it with the other changes.
4. Write tests for your code. Tests can be Unit or Feature.
5. Code refactors will be closed. For the architectural refactors open an issue first.
6. Use `./vendor/bin/pint` to style your code before opening a PR otherwise the actions will fail.
7. Typo fixes in documentation are welcome, but if it's at all debatable we might just close it.
## Misc
1. If you think we closed something incorrectly, feel free to (politely) tell us why! We're human and make mistakes.

View File

@ -1,6 +1,6 @@
<p align="center">
<img src="https://github.com/vitodeploy/vito/assets/61919774/8060fded-58e3-4d58-b58b-5b717b0718e9" alt="VitoDeploy>
<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>
<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>
@ -34,9 +34,10 @@ ## Useful Links
- [Documentation](https://vitodeploy.com)
- [Install on Server](https://vitodeploy.com/introduction/installation.html#install-on-vps-recommended)
- [Install via Docker](https://vitodeploy.com/introduction/installation.html#install-via-docker)
- [Roadmap](https://github.com/orgs/vitodeploy/projects/5)
- [Video Demo](https://youtu.be/AbmUOBDOc28)
- [Discord](https://discord.gg/uZeeHZZnm5)
- [Feedbacks](https://vitodeploy.featurebase.app)
- [Roadmap](https://vitodeploy.featurebase.app/roadmap)
- [Video Demo](https://youtu.be/rLRHIyEfON8)
- [Discord](https://discord.gg/dcUWA5DV)
- [Contribution](/CONTRIBUTING.md)
- [Security](/SECURITY.md)
@ -49,6 +50,7 @@ ## Credits
- Alpinejs
- HTMX
- Vite
- Toastr by CodeSeven
- Prettier
- Postcss
- Flowbite

View File

@ -2,7 +2,6 @@
namespace App\Actions\CronJob;
use App\Enums\CronjobStatus;
use App\Models\CronJob;
use App\Models\Server;
@ -11,9 +10,7 @@ class DeleteCronJob
public function delete(Server $server, CronJob $cronJob): void
{
$user = $cronJob->user;
$cronJob->status = CronjobStatus::DELETING;
$cronJob->save();
$server->cron()->update($cronJob->user, CronJob::crontab($server, $user));
$cronJob->delete();
$server->cron()->update($cronJob->user, CronJob::crontab($server, $user));
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace App\Actions\CronJob;
use App\Enums\CronjobStatus;
use App\Models\CronJob;
use App\Models\Server;
class DisableCronJob
{
public function disable(Server $server, CronJob $cronJob): void
{
$cronJob->status = CronjobStatus::DISABLING;
$cronJob->save();
$server->cron()->update($cronJob->user, CronJob::crontab($server, $cronJob->user));
$cronJob->status = CronjobStatus::DISABLED;
$cronJob->save();
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace App\Actions\CronJob;
use App\Enums\CronjobStatus;
use App\Models\CronJob;
use App\Models\Server;
class EnableCronJob
{
public function enable(Server $server, CronJob $cronJob): void
{
$cronJob->status = CronjobStatus::ENABLING;
$cronJob->save();
$server->cron()->update($cronJob->user, CronJob::crontab($server, $cronJob->user));
$cronJob->status = CronjobStatus::READY;
$cronJob->save();
}
}

View File

@ -22,9 +22,7 @@ public function create(Server $server, array $input): Database
'server_id' => $server->id,
'name' => $input['name'],
]);
/** @var \App\SSH\Services\Database\Database */
$databaseHandler = $server->database()->handler();
$databaseHandler->create($database->name);
$server->database()->handler()->create($database->name);
$database->status = DatabaseStatus::READY;
$database->save();

View File

@ -1,150 +0,0 @@
<?php
namespace App\Actions\Monitoring;
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Query\Expression;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class GetMetrics
{
public function filter(Server $server, array $input): array
{
if (isset($input['from']) && isset($input['to']) && $input['from'] === $input['to']) {
$input['from'] = Carbon::parse($input['from'])->format('Y-m-d').' 00:00:00';
$input['to'] = Carbon::parse($input['to'])->format('Y-m-d').' 23:59:59';
}
$defaultInput = [
'period' => '10m',
];
$input = array_merge($defaultInput, $input);
$this->validate($input);
return $this->metrics(
server: $server,
fromDate: $this->getFromDate($input),
toDate: $this->getToDate($input),
interval: $this->getInterval($input)
);
}
private function metrics(
Server $server,
Carbon $fromDate,
Carbon $toDate,
?Expression $interval = null
): array {
$metrics = DB::table('metrics')
->where('server_id', $server->id)
->whereBetween('created_at', [$fromDate->format('Y-m-d H:i:s'), $toDate->format('Y-m-d H:i:s')])
->select(
[
DB::raw('created_at as date'),
DB::raw('ROUND(AVG(load), 2) as load'),
DB::raw('ROUND(AVG(memory_total), 2) as memory_total'),
DB::raw('ROUND(AVG(memory_used), 2) as memory_used'),
DB::raw('ROUND(AVG(memory_free), 2) as memory_free'),
DB::raw('ROUND(AVG(disk_total), 2) as disk_total'),
DB::raw('ROUND(AVG(disk_used), 2) as disk_used'),
DB::raw('ROUND(AVG(disk_free), 2) as disk_free'),
$interval,
],
)
->groupByRaw('date_interval')
->orderBy('date_interval')
->get()
->map(function ($item) {
$item->date = Carbon::parse($item->date)->format('Y-m-d H:i');
return $item;
});
return [
'metrics' => $metrics,
];
}
private function getFromDate(array $input): Carbon
{
if ($input['period'] === 'custom') {
return new Carbon($input['from']);
}
return Carbon::parse('-'.convert_time_format($input['period']));
}
private function getToDate(array $input): Carbon
{
if ($input['period'] === 'custom') {
return new Carbon($input['to']);
}
return Carbon::now();
}
private function getInterval(array $input): Expression
{
if ($input['period'] === 'custom') {
$from = new Carbon($input['from']);
$to = new Carbon($input['to']);
$periodInHours = $from->diffInHours($to);
}
if (! isset($periodInHours)) {
$periodInHours = Carbon::parse(
convert_time_format($input['period'])
)->diffInHours();
}
if (abs($periodInHours) <= 1) {
return DB::raw("strftime('%Y-%m-%d %H:%M:00', created_at) as date_interval");
}
if ($periodInHours <= 24) {
return DB::raw("strftime('%Y-%m-%d %H:00:00', created_at) as date_interval");
}
if ($periodInHours > 24) {
return DB::raw("strftime('%Y-%m-%d 00:00:00', created_at) as date_interval");
}
}
private function validate(array $input): void
{
Validator::make($input, [
'period' => [
'required',
Rule::in([
'10m',
'30m',
'1h',
'12h',
'1d',
'7d',
'custom',
]),
],
])->validate();
if ($input['period'] === 'custom') {
Validator::make($input, [
'from' => [
'required',
'date',
'before:to',
],
'to' => [
'required',
'date',
'after:from',
],
])->validate();
}
}
}

View File

@ -1,32 +0,0 @@
<?php
namespace App\Actions\Monitoring;
use App\Models\Server;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class UpdateMetricSettings
{
public function update(Server $server, array $input): void
{
$this->validate($input);
$service = $server->monitoring();
$data = $service->handler()->data();
$data['data_retention'] = $input['data_retention'];
$service->type_data = $data;
$service->save();
}
private function validate(array $input): void
{
Validator::make($input, [
'data_retention' => [
'required',
Rule::in(config('core.metrics_data_retention')),
],
])->validate();
}
}

View File

@ -19,7 +19,6 @@ public function add(User $user, array $input): void
'user_id' => $user->id,
'provider' => $input['provider'],
'label' => $input['label'],
'project_id' => isset($input['global']) && $input['global'] ? null : $user->current_project_id,
]);
$this->validateType($channel, $input);
$channel->data = $channel->provider()->createData($input);

View File

@ -1,34 +0,0 @@
<?php
namespace App\Actions\NotificationChannels;
use App\Models\NotificationChannel;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class EditChannel
{
public function edit(NotificationChannel $notificationChannel, User $user, array $input): void
{
$this->validate($input);
$notificationChannel->label = $input['label'];
$notificationChannel->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
$notificationChannel->save();
}
/**
* @throws ValidationException
*/
private function validate(array $input): void
{
$rules = [
'label' => [
'required',
],
];
Validator::make($input, $rules)->validate();
}
}

View File

@ -4,7 +4,6 @@
use App\Enums\ServiceStatus;
use App\Models\Server;
use App\SSH\Services\PHP\PHP;
use Illuminate\Validation\ValidationException;
class ChangeDefaultCli
@ -13,9 +12,7 @@ public function change(Server $server, array $input): void
{
$this->validate($server, $input);
$service = $server->php($input['version']);
/** @var PHP $handler */
$handler = $service->handler();
$handler->setDefaultCli();
$service->handler()->setDefaultCli();
$server->defaultService('php')->update(['is_default' => 0]);
$service->update(['is_default' => 1]);
$service->update(['status' => ServiceStatus::READY]);

View File

@ -2,11 +2,7 @@
namespace App\Actions\PHP;
use App\Enums\PHPIniType;
use App\Models\Server;
use App\SSH\Services\PHP\PHP;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class GetPHPIni
@ -18,10 +14,7 @@ public function getIni(Server $server, array $input): string
$php = $server->php($input['version']);
try {
/** @var PHP $handler */
$handler = $php->handler();
return $handler->getPHPIni($input['type']);
return $php->handler()->getPHPIni();
} catch (\Throwable $e) {
throw ValidationException::withMessages(
['ini' => $e->getMessage()]
@ -31,13 +24,6 @@ public function getIni(Server $server, array $input): string
public function validate(Server $server, array $input): void
{
Validator::make($input, [
'type' => [
'required',
Rule::in([PHPIniType::CLI, PHPIniType::FPM]),
],
])->validate();
if (! isset($input['version']) || ! in_array($input['version'], $server->installedPHPVersions())) {
throw ValidationException::withMessages(
['version' => __('This version is not installed')]

View File

@ -4,7 +4,6 @@
use App\Models\Server;
use App\Models\Service;
use App\SSH\Services\PHP\PHP;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
@ -24,9 +23,7 @@ public function install(Server $server, array $input): Service
$service->save();
dispatch(function () use ($service, $input) {
/** @var PHP $handler */
$handler = $service->handler();
$handler->installExtension($input['extension']);
$service->handler()->installExtension($input['extension']);
})->catch(function () use ($service, $input) {
$service->refresh();
$typeData = $service->type_data;

View File

@ -2,13 +2,10 @@
namespace App\Actions\PHP;
use App\Enums\PHPIniType;
use App\Models\Server;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Throwable;
@ -25,19 +22,19 @@ public function update(Server $server, array $input): void
$tmpName = Str::random(10).strtotime('now');
try {
/** @var FilesystemAdapter $storageDisk */
/** @var \Illuminate\Filesystem\FilesystemAdapter $storageDisk */
$storageDisk = Storage::disk('local');
$storageDisk->put($tmpName, $input['ini']);
$service->server->ssh('root')->upload(
$storageDisk->path($tmpName),
sprintf('/etc/php/%s/%s/php.ini', $service->version, $input['type'])
"/etc/php/$service->version/cli/php.ini"
);
$this->deleteTempFile($tmpName);
} catch (Throwable) {
$this->deleteTempFile($tmpName);
throw ValidationException::withMessages([
'ini' => __("Couldn't update php.ini (:type) file!", ['type' => $input['type']]),
'ini' => __("Couldn't update php.ini file!"),
]);
}
@ -59,10 +56,6 @@ public function validate(Server $server, array $input): void
'string',
],
'version' => 'required|string',
'type' => [
'required',
Rule::in([PHPIniType::CLI, PHPIniType::FPM]),
],
])->validate();
if (! in_array($input['version'], $server->installedPHPVersions())) {

View File

@ -10,31 +10,26 @@ class CreateProject
{
public function create(User $user, array $input): Project
{
if (isset($input['name'])) {
$input['name'] = strtolower($input['name']);
}
$this->validate($input);
$this->validate($user, $input);
$project = new Project([
'user_id' => $user->id,
'name' => $input['name'],
]);
$project->save();
$project->users()->attach($user);
return $project;
}
private function validate(array $input): void
private function validate(User $user, array $input): void
{
Validator::make($input, [
'name' => [
'required',
'string',
'max:255',
'unique:projects,name',
'unique:projects,name,NULL,id,user_id,'.$user->id,
],
])->validate();
}

View File

@ -17,15 +17,11 @@ public function delete(User $user, Project $project): void
}
if ($user->current_project_id == $project->id) {
throw ValidationException::withMessages([
'project' => __('Cannot delete your current project.'),
]);
}
/** @var Project $randomProject */
$randomProject = $user->projects()->where('project_id', '!=', $project->id)->first();
$randomProject = $user->projects()->where('id', '!=', $project->id)->first();
$user->current_project_id = $randomProject->id;
$user->save();
}
$project->delete();
}

View File

@ -10,10 +10,6 @@ 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'];

View File

@ -6,7 +6,6 @@
use App\Enums\SslType;
use App\Models\Site;
use App\Models\Ssl;
use App\SSH\Services\Webserver\Webserver;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
@ -28,23 +27,16 @@ public function create(Site $site, array $input): void
'expires_at' => $input['type'] === SslType::LETSENCRYPT ? now()->addMonths(3) : $input['expires_at'],
'status' => SslStatus::CREATING,
]);
$ssl->domains = [$site->domain];
if (isset($input['aliases']) && $input['aliases']) {
$ssl->domains = array_merge($ssl->domains, $site->aliases);
}
$ssl->save();
dispatch(function () use ($site, $ssl) {
/** @var Webserver $webserver */
$webserver = $site->server->webserver()->handler();
$webserver->setupSSL($ssl);
$site->server->webserver()->handler()->setupSSL($ssl);
$ssl->status = SslStatus::CREATED;
$ssl->save();
$site->type()->edit();
})->catch(function () use ($ssl) {
$ssl->status = SslStatus::FAILED;
$ssl->save();
})->onConnection('ssh');
$ssl->delete();
});
}
/**

View File

@ -1,32 +0,0 @@
<?php
namespace App\Actions\Script;
use App\Models\Script;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
class CreateScript
{
public function create(User $user, array $input): Script
{
$this->validate($input);
$script = new Script([
'user_id' => $user->id,
'name' => $input['name'],
'content' => $input['content'],
]);
$script->save();
return $script;
}
private function validate(array $input): void
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
])->validate();
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace App\Actions\Script;
use App\Models\Script;
use Illuminate\Support\Facades\Validator;
class EditScript
{
public function edit(Script $script, array $input): Script
{
$this->validate($input);
$script->name = $input['name'];
$script->content = $input['content'];
$script->save();
return $script;
}
private function validate(array $input): void
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
])->validate();
}
}

View File

@ -1,62 +0,0 @@
<?php
namespace App\Actions\Script;
use App\Enums\ScriptExecutionStatus;
use App\Models\Script;
use App\Models\ScriptExecution;
use App\Models\Server;
use App\Models\ServerLog;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class ExecuteScript
{
public function execute(Script $script, Server $server, array $input): ScriptExecution
{
$this->validate($server, $input);
$execution = new ScriptExecution([
'script_id' => $script->id,
'user' => $input['user'],
'variables' => $input['variables'] ?? [],
'status' => ScriptExecutionStatus::EXECUTING,
]);
$execution->save();
dispatch(function () use ($execution, $server, $script) {
$content = $execution->getContent();
$log = ServerLog::make($server, 'script-'.$script->id.'-'.strtotime('now'));
$log->save();
$execution->server_log_id = $log->id;
$execution->save();
$server->os()->runScript('~/', $content, $log, $execution->user);
$execution->status = ScriptExecutionStatus::COMPLETED;
$execution->save();
})->catch(function () use ($execution) {
$execution->status = ScriptExecutionStatus::FAILED;
$execution->save();
})->onConnection('ssh');
return $execution;
}
private function validate(Server $server, array $input): void
{
Validator::make($input, [
'user' => [
'required',
Rule::in([
'root',
$server->ssh_user,
]),
],
'variables' => 'array',
'variables.*' => [
'required',
'string',
'max:255',
],
])->validate();
}
}

View File

@ -2,7 +2,6 @@
namespace App\Actions\Server;
use App\Enums\ServerStatus;
use App\Facades\Notifier;
use App\Models\Server;
use App\Notifications\ServerDisconnected;
@ -16,12 +15,12 @@ public function check(Server $server): Server
try {
$server->ssh()->connect();
$server->refresh();
if (in_array($status, [ServerStatus::DISCONNECTED, ServerStatus::UPDATING])) {
$server->status = ServerStatus::READY;
if ($status == 'disconnected') {
$server->status = 'ready';
$server->save();
}
} catch (Throwable) {
$server->status = ServerStatus::DISCONNECTED;
$server->status = 'disconnected';
$server->save();
Notifier::send($server, new ServerDisconnected($server));
}

View File

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

View File

@ -1,25 +0,0 @@
<?php
namespace App\Actions\Server;
use App\Enums\ServerStatus;
use App\Facades\Notifier;
use App\Models\Server;
use App\Notifications\ServerUpdateFailed;
class Update
{
public function update(Server $server): void
{
$server->status = ServerStatus::UPDATING;
$server->save();
dispatch(function () use ($server) {
$server->os()->upgrade();
$server->checkConnection();
$server->checkForUpdates();
})->catch(function () use ($server) {
Notifier::send($server, new ServerUpdateFailed($server));
$server->checkConnection();
})->onConnection('ssh');
}
}

View File

@ -38,7 +38,6 @@ public function create(User $user, array $input): ServerProvider
$serverProvider->profile = $input['name'];
$serverProvider->provider = $input['provider'];
$serverProvider->credentials = $provider->credentialData($input);
$serverProvider->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
$serverProvider->save();
return $serverProvider;

View File

@ -1,34 +0,0 @@
<?php
namespace App\Actions\ServerProvider;
use App\Models\ServerProvider;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class EditServerProvider
{
public function edit(ServerProvider $serverProvider, User $user, array $input): void
{
$this->validate($input);
$serverProvider->profile = $input['name'];
$serverProvider->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
$serverProvider->save();
}
/**
* @throws ValidationException
*/
private function validate(array $input): void
{
$rules = [
'name' => [
'required',
],
];
Validator::make($input, $rules)->validate();
}
}

View File

@ -1,57 +0,0 @@
<?php
namespace App\Actions\Service;
use App\Enums\ServiceStatus;
use App\Models\Server;
use App\Models\Service;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class Install
{
public function install(Server $server, array $input): Service
{
$this->validate($server, $input);
$service = new Service([
'server_id' => $server->id,
'name' => $input['name'],
'type' => $input['type'],
'version' => $input['version'],
'status' => ServiceStatus::INSTALLING,
]);
Validator::make($input, $service->handler()->creationRules($input))->validate();
$service->type_data = $service->handler()->creationData($input);
$service->save();
dispatch(function () use ($service) {
$service->handler()->install();
$service->status = ServiceStatus::READY;
$service->save();
})->catch(function () use ($service) {
$service->status = ServiceStatus::INSTALLATION_FAILED;
$service->save();
})->onConnection('ssh');
return $service;
}
private function validate(Server $server, array $input): void
{
Validator::make($input, [
'type' => [
'required',
Rule::in(config('core.service_types')),
],
'name' => [
'required',
Rule::in(array_keys(config('core.service_types'))),
],
'version' => 'required',
])->validate();
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace App\Actions\Service;
use App\Enums\ServiceStatus;
use App\Models\Service;
use Illuminate\Support\Facades\Validator;
class Uninstall
{
public function uninstall(Service $service): void
{
Validator::make([
'service' => $service->id,
], $service->handler()->deletionRules())->validate();
$service->status = ServiceStatus::UNINSTALLING;
$service->save();
dispatch(function () use ($service) {
$service->handler()->uninstall();
$service->delete();
})->catch(function () use ($service) {
$service->status = ServiceStatus::FAILED;
$service->save();
})->onConnection('ssh');
}
}

View File

@ -3,8 +3,6 @@
namespace App\Actions\Site;
use App\Enums\SiteStatus;
use App\Exceptions\RepositoryNotFound;
use App\Exceptions\RepositoryPermissionDenied;
use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Notifier;
use App\Models\Server;
@ -22,6 +20,7 @@ class CreateSite
{
/**
* @throws SourceControlIsNotConnected
* @throws ValidationException
*/
public function create(Server $server, array $input): Site
{
@ -33,7 +32,7 @@ public function create(Server $server, array $input): Site
'server_id' => $server->id,
'type' => $input['type'],
'domain' => $input['domain'],
'aliases' => $input['aliases'] ?? [],
'aliases' => isset($input['alias']) ? [$input['alias']] : [],
'path' => '/home/'.$server->getSshUser().'/'.$input['domain'],
'status' => SiteStatus::INSTALLING,
]);
@ -48,15 +47,7 @@ public function create(Server $server, array $input): Site
}
} catch (SourceControlIsNotConnected) {
throw ValidationException::withMessages([
'source_control' => 'Source control is not connected',
]);
} catch (RepositoryPermissionDenied) {
throw ValidationException::withMessages([
'repository' => 'You do not have permission to access this repository',
]);
} catch (RepositoryNotFound) {
throw ValidationException::withMessages([
'repository' => 'Repository not found',
'source_control' => __('Source control is not connected'),
]);
}
@ -115,7 +106,7 @@ private function validateInputs(Server $server, array $input): void
return $query->where('server_id', $server->id);
}),
],
'aliases.*' => [
'alias' => [
new DomainRule(),
],
];

View File

@ -6,7 +6,6 @@
use App\Exceptions\DeploymentScriptIsEmptyException;
use App\Exceptions\SourceControlIsNotConnected;
use App\Models\Deployment;
use App\Models\ServerLog;
use App\Models\Site;
class Deploy
@ -30,7 +29,7 @@ public function run(Site $site): Deployment
'deployment_script_id' => $site->deploymentScript->id,
'status' => DeploymentStatus::DEPLOYING,
]);
$lastCommit = $site->sourceControl()?->provider()?->getLastCommit($site->repository, $site->branch);
$lastCommit = $site->sourceControl()->provider()->getLastCommit($site->repository, $site->branch);
if ($lastCommit) {
$deployment->commit_id = $lastCommit['commit_id'];
$deployment->commit_data = $lastCommit['commit_data'];
@ -38,19 +37,9 @@ public function run(Site $site): Deployment
$deployment->save();
dispatch(function () use ($site, $deployment) {
/** @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(
path: $site->path,
script: $site->deploymentScript->content,
serverLog: $log,
variables: $site->environmentVariables($deployment)
);
$log = $site->server->os()->runScript($site->path, $site->deploymentScript->content, $site->id);
$deployment->status = DeploymentStatus::FINISHED;
$deployment->log_id = $log->id;
$deployment->save();
})->catch(function () use ($deployment) {
$deployment->status = DeploymentStatus::FAILED;

View File

@ -1,33 +0,0 @@
<?php
namespace App\Actions\Site;
use App\Models\Site;
use App\SSH\Services\Webserver\Webserver;
use App\ValidationRules\DomainRule;
use Illuminate\Support\Facades\Validator;
class UpdateAliases
{
public function update(Site $site, array $input): void
{
$this->validate($input);
$site->aliases = $input['aliases'] ?? [];
/** @var Webserver $webserver */
$webserver = $site->server->webserver()->handler();
$webserver->updateVHost($site, ! $site->hasSSL());
$site->save();
}
private function validate(array $input): void
{
Validator::make($input, [
'aliases.*' => [
new DomainRule(),
],
])->validate();
}
}

View File

@ -2,14 +2,10 @@
namespace App\Actions\Site;
use App\Exceptions\SSHUploadFailed;
use App\Models\Site;
class UpdateEnv
{
/**
* @throws SSHUploadFailed
*/
public function update(Site $site, array $input): void
{
$site->server->os()->editFile(

View File

@ -1,49 +0,0 @@
<?php
namespace App\Actions\Site;
use App\Exceptions\RepositoryNotFound;
use App\Exceptions\RepositoryPermissionDenied;
use App\Exceptions\SourceControlIsNotConnected;
use App\Models\Site;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class UpdateSourceControl
{
public function update(Site $site, array $input): void
{
$this->validate($input);
$site->source_control_id = $input['source_control'];
try {
if ($site->sourceControl()) {
$site->sourceControl()->getRepo($site->repository);
}
} catch (SourceControlIsNotConnected) {
throw ValidationException::withMessages([
'source_control' => 'Source control is not connected',
]);
} catch (RepositoryPermissionDenied) {
throw ValidationException::withMessages([
'repository' => 'You do not have permission to access this repository',
]);
} catch (RepositoryNotFound) {
throw ValidationException::withMessages([
'repository' => 'Repository not found',
]);
}
$site->save();
}
private function validate(array $input): void
{
Validator::make($input, [
'source_control' => [
'required',
Rule::exists('source_controls', 'id'),
],
])->validate();
}
}

View File

@ -3,7 +3,6 @@
namespace App\Actions\SourceControl;
use App\Models\SourceControl;
use App\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
@ -11,7 +10,7 @@
class ConnectSourceControl
{
public function connect(User $user, array $input): void
public function connect(array $input): void
{
$this->validate($input);
@ -19,7 +18,6 @@ public function connect(User $user, array $input): void
'provider' => $input['provider'],
'profile' => $input['name'],
'url' => Arr::has($input, 'url') ? $input['url'] : null,
'project_id' => isset($input['global']) && $input['global'] ? null : $user->current_project_id,
]);
$this->validateProvider($sourceControl, $input);

View File

@ -1,54 +0,0 @@
<?php
namespace App\Actions\SourceControl;
use App\Models\SourceControl;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class EditSourceControl
{
public function edit(SourceControl $sourceControl, User $user, array $input): void
{
$this->validate($input);
$sourceControl->profile = $input['name'];
$sourceControl->url = $input['url'] ?? null;
$sourceControl->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
$this->validateProvider($sourceControl, $input);
$sourceControl->provider_data = $sourceControl->provider()->createData($input);
if (! $sourceControl->provider()->connect()) {
throw ValidationException::withMessages([
'token' => __('Cannot connect to :provider or invalid token!', ['provider' => $sourceControl->provider]
),
]);
}
$sourceControl->save();
}
/**
* @throws ValidationException
*/
private function validate(array $input): void
{
$rules = [
'name' => [
'required',
],
];
Validator::make($input, $rules)->validate();
}
/**
* @throws ValidationException
*/
private function validateProvider(SourceControl $sourceControl, array $input): void
{
Validator::make($input, $sourceControl->provider()->createRules($input))->validate();
}
}

View File

@ -21,7 +21,6 @@ public function create(User $user, array $input): void
'user_id' => $user->id,
'provider' => $input['provider'],
'profile' => $input['name'],
'project_id' => isset($input['global']) && $input['global'] ? null : $user->current_project_id,
]);
$this->validateProvider($input, $storageProvider->provider()->validationRules());

View File

@ -1,34 +0,0 @@
<?php
namespace App\Actions\StorageProvider;
use App\Models\StorageProvider;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class EditStorageProvider
{
public function edit(StorageProvider $storageProvider, User $user, array $input): void
{
$this->validate($input);
$storageProvider->profile = $input['name'];
$storageProvider->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
$storageProvider->save();
}
/**
* @throws ValidationException
*/
private function validate(array $input): void
{
$rules = [
'name' => [
'required',
],
];
Validator::make($input, $rules)->validate();
}
}

View File

@ -1,58 +0,0 @@
<?php
namespace App\Actions\Tag;
use App\Models\Server;
use App\Models\Site;
use App\Models\Tag;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class AttachTag
{
public function attach(User $user, array $input): Tag
{
$this->validate($input);
/** @var Server|Site $taggable */
$taggable = $input['taggable_type']::findOrFail($input['taggable_id']);
$tag = Tag::query()->where('name', $input['name'])->first();
if ($tag) {
if (! $taggable->tags->contains($tag->id)) {
$taggable->tags()->attach($tag->id);
}
return $tag;
}
$tag = new Tag([
'project_id' => $user->currentProject->id,
'name' => $input['name'],
'color' => config('core.tag_colors')[array_rand(config('core.tag_colors'))],
]);
$tag->save();
$taggable->tags()->attach($tag->id);
return $tag;
}
private function validate(array $input): void
{
Validator::make($input, [
'name' => [
'required',
],
'taggable_id' => [
'required',
'integer',
],
'taggable_type' => [
'required',
Rule::in(config('core.taggable_types')),
],
])->validate();
}
}

View File

@ -1,49 +0,0 @@
<?php
namespace App\Actions\Tag;
use App\Models\Tag;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class CreateTag
{
public function create(User $user, array $input): Tag
{
$this->validate($input);
$tag = Tag::query()
->where('project_id', $user->current_project_id)
->where('name', $input['name'])
->first();
if ($tag) {
throw ValidationException::withMessages([
'name' => ['Tag with this name already exists.'],
]);
}
$tag = new Tag([
'project_id' => $user->currentProject->id,
'name' => $input['name'],
'color' => $input['color'],
]);
$tag->save();
return $tag;
}
private function validate(array $input): void
{
Validator::make($input, [
'name' => [
'required',
],
'color' => [
'required',
Rule::in(config('core.tag_colors')),
],
])->validate();
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace App\Actions\Tag;
use App\Models\Tag;
use Illuminate\Support\Facades\DB;
class DeleteTag
{
public function delete(Tag $tag): void
{
DB::table('taggables')->where('tag_id', $tag->id)->delete();
$tag->delete();
}
}

View File

@ -1,36 +0,0 @@
<?php
namespace App\Actions\Tag;
use App\Models\Server;
use App\Models\Site;
use App\Models\Tag;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class DetachTag
{
public function detach(Tag $tag, array $input): void
{
$this->validate($input);
/** @var Server|Site $taggable */
$taggable = $input['taggable_type']::findOrFail($input['taggable_id']);
$taggable->tags()->detach($tag->id);
}
private function validate(array $input): void
{
Validator::make($input, [
'taggable_id' => [
'required',
'integer',
],
'taggable_type' => [
'required',
Rule::in(config('core.taggable_types')),
],
])->validate();
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Actions\Tag;
use App\Models\Tag;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class EditTag
{
public function edit(Tag $tag, array $input): void
{
$this->validate($input);
$tag->name = $input['name'];
$tag->color = $input['color'];
$tag->save();
}
/**
* @throws ValidationException
*/
private function validate(array $input): void
{
$rules = [
'name' => [
'required',
],
'color' => [
'required',
Rule::in(config('core.tag_colors')),
],
];
Validator::make($input, $rules)->validate();
}
}

View File

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

View File

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

View File

@ -7,7 +7,7 @@
class CreateUserCommand extends Command
{
protected $signature = 'user:create {name} {email} {password} {--role=admin}';
protected $signature = 'user:create {name} {email} {password}';
protected $description = 'Create a new user';
@ -25,7 +25,6 @@ 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!');

View File

@ -1,28 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Service;
use Illuminate\Console\Command;
class DeleteOlderMetricsCommand extends Command
{
protected $signature = 'metrics:delete-older-metrics';
protected $description = 'Delete older metrics from database';
public function handle()
{
Service::query()->where('type', 'monitoring')->chunk(100, function ($services) {
$services->each(function ($service) {
$this->info("Deleting older metrics for service {$service->server->name}");
$service
->server
->metrics()
->where('created_at', '<', now()->subDays($service->handler()->data()['data_retention']))
->delete();
$this->info('Metrics deleted');
});
});
}
}

View File

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

View File

@ -16,8 +16,6 @@ protected function schedule(Schedule $schedule): void
$schedule->command('backups:run "0 0 * * *"')->daily();
$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();
}
/**

View File

@ -9,10 +9,4 @@ final class CronjobStatus
const READY = 'ready';
const DELETING = 'deleting';
const ENABLING = 'enabling';
const DISABLING = 'disabling';
const DISABLED = 'disabled';
}

View File

@ -10,9 +10,7 @@ final class Database
const MYSQL80 = 'mysql80';
const MARIADB103 = 'mariadb103';
const MARIADB104 = 'mariadb104';
const MARIADB = 'mariadb';
const POSTGRESQL12 = 'postgresql12';

View File

@ -9,6 +9,4 @@ final class OperatingSystem
const UBUNTU20 = 'ubuntu_20';
const UBUNTU22 = 'ubuntu_22';
const UBUNTU24 = 'ubuntu_24';
}

View File

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

View File

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

View File

@ -11,6 +11,4 @@ final class ServerStatus
const INSTALLATION_FAILED = 'installation_failed';
const DISCONNECTED = 'disconnected';
const UPDATING = 'updating';
}

View File

@ -11,6 +11,4 @@ final class SiteType
const LARAVEL = 'laravel';
const WORDPRESS = 'wordpress';
const PHPMYADMIN = 'phpmyadmin';
}

View File

@ -7,10 +7,4 @@ final class StorageProvider
const DROPBOX = 'dropbox';
const FTP = 'ftp';
const LOCAL = 'local';
const S3 = 's3';
const WASABI = 'wasabi';
}

View File

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

View File

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

View File

@ -2,7 +2,9 @@
namespace App\Exceptions;
class SSHAuthenticationError extends SSHError
use Exception;
class SSHAuthenticationError extends Exception
{
//
}

View File

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

View File

@ -2,7 +2,9 @@
namespace App\Exceptions;
class SSLCreationException extends SSHError
use Exception;
class SSLCreationException extends Exception
{
//
}

View File

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

View File

@ -1,30 +0,0 @@
<?php
namespace App\Facades;
use App\Support\Testing\FTPFake;
use FTP\Connection;
use Illuminate\Support\Facades\Facade;
/**
* @method static bool|Connection connect(string $host, string $port, bool $ssl = false)
* @method static bool login(string $username, string $password, bool|Connection $connection)
* @method static void close(bool|Connection $connection)
* @method static bool passive(bool|Connection $connection, bool $passive)
* @method static bool delete(bool|Connection $connection, string $path)
* @method static void assertConnected(string $host)
*/
class FTP extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'ftp';
}
public static function fake(): FTPFake
{
static::swap($fake = new FTPFake());
return $fake;
}
}

View File

@ -3,7 +3,6 @@
namespace App\Facades;
use App\Models\Server;
use App\Models\ServerLog;
use App\Support\Testing\SSHFake;
use Illuminate\Support\Facades\Facade as FacadeAlias;
@ -11,13 +10,11 @@
* Class SSH
*
* @method static init(Server $server, string $asUser = null)
* @method static setLog(?ServerLog $log)
* @method static setLog(string $logType, int $siteId = null)
* @method static connect()
* @method static string exec(string $command, string $log = '', int $siteId = null, ?bool $stream = false)
* @method static string exec(string $command, string $log = '', int $siteId = null)
* @method static string assertExecuted(array|string $commands)
* @method static string assertExecutedContains(string $command)
* @method static string assertFileUploaded(string $toPath, ?string $content = null)
* @method static string getUploadedLocalPath()
* @method static disconnect()
*/
class SSH extends FacadeAlias

View File

@ -1,37 +0,0 @@
<?php
namespace App\Helpers;
use FTP\Connection;
class FTP
{
public function connect(string $host, string $port, bool $ssl = false): bool|Connection
{
if ($ssl) {
return ftp_ssl_connect($host, $port, 5);
}
return ftp_connect($host, $port, 5);
}
public function login(string $username, string $password, bool|Connection $connection): bool
{
return ftp_login($connection, $username, $password);
}
public function close(bool|Connection $connection): void
{
ftp_close($connection);
}
public function passive(bool|Connection $connection, bool $passive): bool
{
return ftp_pasv($connection, $passive);
}
public function delete(bool|Connection $connection, string $path): bool
{
return ftp_delete($connection, $path);
}
}

View File

@ -50,9 +50,14 @@ public function init(Server $server, ?string $asUser = null): self
return $this;
}
public function setLog(ServerLog $log): self
public function setLog(string $logType, $siteId = null): self
{
$this->log = $log;
$this->log = $this->server->logs()->create([
'site_id' => $siteId,
'name' => $this->server->id.'-'.strtotime('now').'-'.$logType.'.log',
'type' => $logType,
'disk' => config('core.logs_disk'),
]);
return $this;
}
@ -91,14 +96,12 @@ public function connect(bool $sftp = false): void
* @throws SSHCommandError
* @throws SSHConnectionError
*/
public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false): string
public function exec(string|array $commands, string $log = '', ?int $siteId = null): string
{
if (! $this->log && $log) {
$this->log = ServerLog::make($this->server, $log);
if ($siteId) {
$this->log->forSite($siteId);
}
$this->log->save();
if ($log) {
$this->setLog($log, $siteId);
} else {
$this->log = null;
}
try {
@ -109,34 +112,18 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
throw new SSHConnectionError($e->getMessage());
}
if (! is_array($commands)) {
$commands = [$commands];
}
try {
if ($this->asUser) {
$command = 'sudo su - '.$this->asUser.' -c '.'"'.addslashes($command).'"';
$result = '';
foreach ($commands as $command) {
$result .= $this->executeCommand($command);
}
$this->connection->setTimeout(0);
if ($stream) {
$this->connection->exec($command, function ($output) {
$this->log?->write($output);
echo $output;
ob_flush();
flush();
});
return '';
} else {
$output = $this->connection->exec($command);
$this->log?->write($output);
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;
}
return $result;
} catch (Throwable $e) {
throw $e;
throw new SSHCommandError($e->getMessage());
}
}
@ -154,6 +141,28 @@ public function upload(string $local, string $remote): void
$this->connection->put($remote, $local, SFTP::SOURCE_LOCAL_FILE);
}
/**
* @throws Exception
*/
protected function executeCommand(string $command): string
{
if ($this->asUser) {
$command = 'sudo su - '.$this->asUser.' -c '.'"'.addslashes($command).'"';
}
$this->connection->setTimeout(0);
$output = $this->connection->exec($command);
$this->log?->write($output);
if (Str::contains($output, 'VITO_SSH_ERROR')) {
throw new Exception('SSH command failed with an error');
}
return $output;
}
/**
* @throws Exception
*/

View File

@ -1,36 +0,0 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Models\Server;
use App\Models\Service;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class AgentController extends Controller
{
public function __invoke(Request $request, Server $server, int $id): JsonResponse
{
$validated = $this->validate($request, [
'load' => 'required|numeric',
'memory_total' => 'required|numeric',
'memory_used' => 'required|numeric',
'memory_free' => 'required|numeric',
'disk_total' => 'required|numeric',
'disk_used' => 'required|numeric',
'disk_free' => 'required|numeric',
]);
/** @var Service $service */
$service = $server->services()->findOrFail($id);
if ($request->header('secret') !== $service->handler()->data()['secret']) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$server->metrics()->create(array_merge($validated, ['server_id' => $server->id]));
return response()->json();
}
}

View File

@ -1,16 +0,0 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
class HealthController extends Controller
{
public function __invoke()
{
return response()->json([
'success' => true,
'version' => vito_version(),
]);
}
}

View File

@ -7,10 +7,7 @@
use App\Actions\Site\UpdateDeploymentScript;
use App\Actions\Site\UpdateEnv;
use App\Exceptions\DeploymentScriptIsEmptyException;
use App\Exceptions\RepositoryNotFound;
use App\Exceptions\RepositoryPermissionDenied;
use App\Exceptions\SourceControlIsNotConnected;
use App\Exceptions\SSHUploadFailed;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Models\Deployment;
@ -23,20 +20,16 @@ class ApplicationController extends Controller
{
public function deploy(Server $server, Site $site): HtmxResponse
{
$this->authorize('manage', $server);
try {
app(Deploy::class)->run($site);
Toast::success('Deployment started!');
} catch (SourceControlIsNotConnected) {
Toast::error('Source control is not connected. Check site\'s settings.');
} catch (SourceControlIsNotConnected $e) {
Toast::error($e->getMessage());
return htmx()->redirect(route('source-controls'));
} catch (DeploymentScriptIsEmptyException) {
Toast::error('Deployment script is empty!');
} catch (RepositoryPermissionDenied) {
Toast::error('You do not have permission to access this repository!');
} catch (RepositoryNotFound) {
Toast::error('Repository not found!');
}
return htmx()->back();
@ -44,15 +37,11 @@ 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!');
@ -62,8 +51,6 @@ 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!');
@ -73,29 +60,20 @@ 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);
try {
app(UpdateEnv::class)->update($site, $request->input());
Toast::success('Env updated!');
} catch (SSHUploadFailed) {
Toast::error('Failed to update .env file!');
}
return back();
}
public function enableAutoDeployment(Server $server, Site $site): HtmxResponse
{
$this->authorize('manage', $server);
if (! $site->isAutoDeployment()) {
try {
$site->enableAutoDeployment();
@ -105,12 +83,6 @@ public function enableAutoDeployment(Server $server, Site $site): HtmxResponse
Toast::success('Auto deployment has been enabled.');
} catch (SourceControlIsNotConnected) {
Toast::error('Source control is not connected. Check site\'s settings.');
} catch (DeploymentScriptIsEmptyException) {
Toast::error('Deployment script is empty!');
} catch (RepositoryPermissionDenied) {
Toast::error('You do not have permission to access this repository!');
} catch (RepositoryNotFound) {
Toast::error('Repository not found!');
}
}
@ -119,8 +91,6 @@ 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();

View File

@ -1,47 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\Server;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class ConsoleController extends Controller
{
public function index(Server $server): View
{
$this->authorize('manage', $server);
return view('console.index', [
'server' => $server,
]);
}
public function run(Server $server, Request $request)
{
$this->authorize('manage', $server);
$this->validate($request, [
'user' => [
'required',
Rule::in(['root', $server->ssh_user]),
],
'command' => 'required|string',
]);
return response()->stream(
function () use ($server, $request) {
$ssh = $server->ssh($request->user);
$log = 'console-'.time();
$ssh->exec(command: $request->command, log: $log, stream: true);
},
200,
[
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no',
'Content-Type' => 'text/event-stream',
]
);
}
}

View File

@ -4,8 +4,6 @@
use App\Actions\CronJob\CreateCronJob;
use App\Actions\CronJob\DeleteCronJob;
use App\Actions\CronJob\DisableCronJob;
use App\Actions\CronJob\EnableCronJob;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Models\CronJob;
@ -18,8 +16,6 @@ class CronjobController extends Controller
{
public function index(Server $server): View
{
$this->authorize('manage', $server);
return view('cronjobs.index', [
'server' => $server,
'cronjobs' => $server->cronJobs,
@ -28,8 +24,6 @@ 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.');
@ -39,34 +33,10 @@ 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.');
return back();
}
public function enable(Server $server, CronJob $cronJob): RedirectResponse
{
$this->authorize('manage', $server);
app(EnableCronJob::class)->enable($server, $cronJob);
Toast::success('Cronjob enabled successfully.');
return back();
}
public function disable(Server $server, CronJob $cronJob): RedirectResponse
{
$this->authorize('manage', $server);
app(DisableCronJob::class)->disable($server, $cronJob);
Toast::success('Cronjob disabled successfully.');
return back();
}
}

View File

@ -18,8 +18,6 @@ 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,
@ -30,8 +28,6 @@ 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.');
@ -41,8 +37,6 @@ 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.');
@ -52,8 +46,6 @@ 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.');
@ -63,8 +55,6 @@ 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.');
@ -74,17 +64,8 @@ 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();

View File

@ -17,8 +17,6 @@ class DatabaseController extends Controller
{
public function index(Server $server): View
{
$this->authorize('manage', $server);
return view('databases.index', [
'server' => $server,
'databases' => $server->databases,
@ -29,8 +27,6 @@ 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')) {
@ -44,8 +40,6 @@ 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.');

View File

@ -16,8 +16,6 @@ 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')) {
@ -31,8 +29,6 @@ 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.');
@ -42,8 +38,6 @@ 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,
]);
@ -51,8 +45,6 @@ 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.');

View File

@ -16,8 +16,6 @@ class FirewallController extends Controller
{
public function index(Server $server): View
{
$this->authorize('manage', $server);
return view('firewall.index', [
'server' => $server,
'rules' => $server->firewallRules,
@ -26,8 +24,6 @@ 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!');
@ -37,8 +33,6 @@ 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!');

View File

@ -1,11 +1,10 @@
<?php
namespace App\Http\Controllers\API;
namespace App\Http\Controllers;
use App\Actions\Site\Deploy;
use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Notifier;
use App\Http\Controllers\Controller;
use App\Models\GitHook;
use App\Models\ServerLog;
use App\Notifications\SourceControlDisconnected;

View File

@ -1,50 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Monitoring\GetMetrics;
use App\Actions\Monitoring\UpdateMetricSettings;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Models\Server;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class MetricController extends Controller
{
public function index(Server $server, Request $request): View|RedirectResponse
{
$this->authorize('manage', $server);
$this->checkIfMonitoringServiceInstalled($server);
return view('metrics.index', [
'server' => $server,
'data' => app(GetMetrics::class)->filter($server, $request->input()),
'lastMetric' => $server->metrics()->latest()->first(),
]);
}
public function settings(Server $server, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
$this->checkIfMonitoringServiceInstalled($server);
app(UpdateMetricSettings::class)->update($server, $request->input());
Toast::success('Metric settings updated successfully');
return htmx()->back();
}
private function checkIfMonitoringServiceInstalled(Server $server): void
{
$this->authorize('manage', $server);
if (! $server->monitoring()) {
abort(404, 'Monitoring service is not installed on this server');
}
}
}

View File

@ -20,8 +20,6 @@ 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(),
@ -31,8 +29,6 @@ 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());
@ -46,8 +42,6 @@ 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');
@ -57,8 +51,6 @@ 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!');
@ -68,8 +60,6 @@ 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);
@ -77,21 +67,15 @@ 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 (:type) updated!', ['type' => $request->input('type')]));
Toast::success('PHP ini updated!');
return back()->with([
'ini' => $request->input('ini'),
]);
return back();
}
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!');

View File

@ -19,8 +19,6 @@ class QueueController extends Controller
{
public function index(Server $server, Site $site): View
{
$this->authorize('manage', $server);
return view('queues.index', [
'server' => $server,
'site' => $site,
@ -30,8 +28,6 @@ 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.');
@ -41,8 +37,6 @@ 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);
@ -52,8 +46,6 @@ 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.');
@ -63,8 +55,6 @@ 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));
}
}

View File

@ -17,8 +17,6 @@ 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,
@ -27,9 +25,7 @@ public function index(Server $server): View
public function store(Server $server, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
/** @var SshKey $key */
/** @var \App\Models\SshKey $key */
$key = app(CreateSshKey::class)->create(
$request->user(),
$request->input()
@ -42,8 +38,6 @@ 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.');
@ -53,8 +47,6 @@ 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,

View File

@ -17,8 +17,6 @@ class SSLController extends Controller
{
public function index(Server $server, Site $site): View
{
$this->authorize('manage', $server);
return view('ssls.index', [
'server' => $server,
'site' => $site,
@ -28,8 +26,6 @@ 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.');
@ -39,8 +35,6 @@ 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.');

View File

@ -1,111 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Script\CreateScript;
use App\Actions\Script\EditScript;
use App\Actions\Script\ExecuteScript;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Models\Script;
use App\Models\ScriptExecution;
use App\Models\Server;
use App\Models\User;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class ScriptController extends Controller
{
public function index(Request $request): View
{
$this->authorize('viewAny', Script::class);
/** @var User $user */
$user = auth()->user();
$data = [
'scripts' => $user->scripts,
];
if ($request->has('edit')) {
$data['editScript'] = $user->scripts()->findOrFail($request->input('edit'));
}
if ($request->has('execute')) {
$data['executeScript'] = $user->scripts()->findOrFail($request->input('execute'));
}
return view('scripts.index', $data);
}
public function show(Script $script): View
{
$this->authorize('view', $script);
return view('scripts.show', [
'script' => $script,
'executions' => $script->executions()->latest()->paginate(20),
]);
}
public function store(Request $request): HtmxResponse
{
$this->authorize('create', Script::class);
/** @var User $user */
$user = auth()->user();
app(CreateScript::class)->create($user, $request->input());
Toast::success('Script created.');
return htmx()->redirect(route('scripts.index'));
}
public function edit(Request $request, Script $script): HtmxResponse
{
$this->authorize('update', $script);
app(EditScript::class)->edit($script, $request->input());
Toast::success('Script updated.');
return htmx()->redirect(route('scripts.index'));
}
public function execute(Script $script, Request $request): HtmxResponse
{
$this->validate($request, [
'server' => 'required|exists:servers,id',
]);
$server = Server::findOrFail($request->input('server'));
$this->authorize('execute', [$script, $server]);
app(ExecuteScript::class)->execute($script, $server, $request->input());
Toast::success('Executing the script...');
return htmx()->redirect(route('scripts.show', $script));
}
public function delete(Script $script): RedirectResponse
{
$this->authorize('delete', $script);
$script->delete();
Toast::success('Script deleted.');
return redirect()->route('scripts.index');
}
public function log(Script $script, ScriptExecution $execution): RedirectResponse
{
$this->authorize('view', $script);
return back()->with('content', $execution->serverLog?->getContent());
}
}

View File

@ -4,7 +4,6 @@
use App\Models\Server;
use App\Models\Site;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@ -24,22 +23,10 @@ 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 = [];

View File

@ -19,9 +19,6 @@ 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'));
@ -29,15 +26,8 @@ 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::getByProjectId(auth()->user()->current_project_id)
->where('provider', $provider)
->get();
$serverProviders = ServerProvider::query()->where('provider', $provider)->get();
return view('servers.create', [
'serverProviders' => $serverProviders,
@ -50,13 +40,8 @@ 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(
$user,
$request->user(),
$request->input()
);
@ -67,17 +52,14 @@ 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.');

View File

@ -2,30 +2,22 @@
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);
}
@ -34,37 +26,4 @@ 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]);
}
}

View File

@ -4,7 +4,6 @@
use App\Actions\Server\EditServer;
use App\Actions\Server\RebootServer;
use App\Actions\Server\Update;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Models\Server;
@ -16,15 +15,11 @@ 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();
@ -46,8 +41,6 @@ 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.');
@ -57,32 +50,10 @@ 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.');
return back();
}
public function checkUpdates(Server $server): RedirectResponse
{
$this->authorize('manage', $server);
$server->checkForUpdates();
return back();
}
public function update(Server $server): HtmxResponse
{
$this->authorize('manage', $server);
app(Update::class)->update($server);
Toast::info('Updating server. This may take a few minutes.');
return htmx()->back();
}
}

View File

@ -2,22 +2,16 @@
namespace App\Http\Controllers;
use App\Actions\Service\Install;
use App\Actions\Service\Uninstall;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Models\Server;
use App\Models\Service;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class ServiceController extends Controller
{
public function index(Server $server): View
{
$this->authorize('manage', $server);
return view('services.index', [
'server' => $server,
'services' => $server->services,
@ -26,8 +20,6 @@ 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!');
@ -37,8 +29,6 @@ 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!');
@ -48,8 +38,6 @@ 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!');
@ -59,8 +47,6 @@ 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!');
@ -70,34 +56,10 @@ 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!');
return back();
}
public function install(Server $server, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
app(Install::class)->install($server, $request->input());
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!');
return htmx()->back();
}
}

View File

@ -3,7 +3,6 @@
namespace App\Http\Controllers\Settings;
use App\Actions\NotificationChannels\AddChannel;
use App\Actions\NotificationChannels\EditChannel;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Http\Controllers\Controller;
@ -14,17 +13,11 @@
class NotificationChannelController extends Controller
{
public function index(Request $request): View
public function index(): View
{
$data = [
'channels' => NotificationChannel::getByProjectId(auth()->user()->current_project_id)->get(),
];
if ($request->has('edit')) {
$data['editChannel'] = NotificationChannel::find($request->input('edit'));
}
return view('settings.notification-channels.index', $data);
return view('settings.notification-channels.index', [
'channels' => NotificationChannel::query()->latest()->get(),
]);
}
public function add(Request $request): HtmxResponse
@ -36,20 +29,7 @@ public function add(Request $request): HtmxResponse
Toast::success('Channel added successfully');
return htmx()->redirect(route('settings.notification-channels'));
}
public function update(NotificationChannel $notificationChannel, Request $request): HtmxResponse
{
app(EditChannel::class)->edit(
$notificationChannel,
$request->user(),
$request->input(),
);
Toast::success('Channel updated.');
return htmx()->redirect(route('settings.notification-channels'));
return htmx()->redirect(route('notification-channels'));
}
public function delete(int $id): RedirectResponse
@ -60,6 +40,6 @@ public function delete(int $id): RedirectResponse
Toast::success('Channel deleted successfully');
return redirect()->route('settings.notification-channels');
return redirect()->route('notification-channels');
}
}

View File

@ -1,10 +1,11 @@
<?php
namespace App\Http\Controllers;
namespace App\Http\Controllers\Settings;
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;
@ -13,7 +14,7 @@ class ProfileController extends Controller
{
public function index(): View
{
return view('profile.index');
return view('settings.profile.index');
}
public function info(Request $request): RedirectResponse

View File

@ -19,42 +19,40 @@ class ProjectController extends Controller
{
public function index(): View
{
$this->authorize('viewAny', Project::class);
return view('settings.projects.index', [
'projects' => Project::all(),
'projects' => auth()->user()->projects,
]);
}
public function create(Request $request): HtmxResponse
{
$this->authorize('create', Project::class);
app(CreateProject::class)->create($request->user(), $request->input());
Toast::success('Project created.');
return htmx()->redirect(route('settings.projects'));
return htmx()->redirect(route('projects'));
}
public function update(Request $request, Project $project): HtmxResponse
{
$this->authorize('update', $project);
/** @var Project $project */
$project = $request->user()->projects()->findOrFail($project->id);
app(UpdateProject::class)->update($project, $request->input());
Toast::success('Project updated.');
return htmx()->redirect(route('settings.projects'));
return htmx()->redirect(route('projects'));
}
public function delete(Project $project): RedirectResponse
{
$this->authorize('delete', $project);
/** @var User $user */
$user = auth()->user();
/** @var Project $project */
$project = $user->projects()->findOrFail($project->id);
try {
app(DeleteProject::class)->delete($user, $project);
} catch (ValidationException $e) {
@ -68,7 +66,7 @@ public function delete(Project $project): RedirectResponse
return back();
}
public function switch(Request $request, $projectId): RedirectResponse
public function switch($projectId): RedirectResponse
{
/** @var User $user */
$user = auth()->user();
@ -76,16 +74,9 @@ public function switch(Request $request, $projectId): RedirectResponse
/** @var Project $project */
$project = $user->projects()->findOrFail($projectId);
$this->authorize('view', $project);
$user->current_project_id = $project->id;
$user->save();
// check if the referer is settings/*
if (str_contains($request->headers->get('referer'), 'settings')) {
return redirect()->to($request->headers->get('referer'));
}
return redirect()->route('servers');
}
}

View File

@ -29,7 +29,7 @@ public function add(Request $request): HtmxResponse
Toast::success('SSH Key added');
return htmx()->redirect(route('settings.ssh-keys'));
return htmx()->redirect(route('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('settings.ssh-keys');
return redirect()->route('ssh-keys');
}
}

View File

@ -4,7 +4,6 @@
use App\Actions\ServerProvider\CreateServerProvider;
use App\Actions\ServerProvider\DeleteServerProvider;
use App\Actions\ServerProvider\EditServerProvider;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Http\Controllers\Controller;
@ -15,17 +14,11 @@
class ServerProviderController extends Controller
{
public function index(Request $request): View
public function index(): View
{
$data = [
'providers' => ServerProvider::getByProjectId(auth()->user()->current_project_id)->get(),
];
if ($request->has('edit')) {
$data['editProvider'] = ServerProvider::find($request->input('edit'));
}
return view('settings.server-providers.index', $data);
return view('settings.server-providers.index', [
'providers' => auth()->user()->serverProviders,
]);
}
public function connect(Request $request): HtmxResponse
@ -37,20 +30,7 @@ public function connect(Request $request): HtmxResponse
Toast::success('Server provider connected.');
return htmx()->redirect(route('settings.server-providers'));
}
public function update(ServerProvider $serverProvider, Request $request): HtmxResponse
{
app(EditServerProvider::class)->edit(
$serverProvider,
$request->user(),
$request->input(),
);
Toast::success('Provider updated.');
return htmx()->redirect(route('settings.server-providers'));
return htmx()->redirect(route('server-providers'));
}
public function delete(ServerProvider $serverProvider): RedirectResponse

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