Compare commits

...

109 Commits

Author SHA1 Message Date
b62c40c97d Bump micromatch from 4.0.5 to 4.0.8 (#287)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-07 18:58:45 +02:00
e39e8c17a2 Add S3 and Wasabi as storage providers (#281) 2024-09-06 23:29:43 +02:00
1391eb32d8 Fix Discord connection check (#285) 2024-09-03 21:33:17 +02:00
7f5e68e131 Tags (#277) 2024-08-20 21:26:27 +02:00
431da1b728 Fix FTP and add more tests (#274) 2024-08-09 19:45:00 +02:00
8c487a64fa Add ace editor (#269) 2024-08-07 21:12:31 +02:00
a67e586a5d fix: image ids updated for DigitalOcean (#271) 2024-08-07 19:58:33 +02:00
960db714b7 Fix update issue (#268) 2024-08-03 12:40:44 +02:00
7da0221ccb Revert "fix: Avoid echoing when asking for the password (#255)" (#266)
This reverts commit 55269dbcde.
2024-08-01 18:28:07 +02:00
9ac5f9ebb3 revert migration squash (#261) 2024-07-30 20:39:05 +02:00
ed8965b92b Fix .env updates for double quotations (#259) 2024-07-27 17:43:46 +02:00
9473d198e1 update php fpm ini (#258) 2024-07-27 12:51:13 +02:00
55269dbcde fix: Avoid echoing when asking for the password (#255) 2024-07-23 11:29:24 +02:00
3d67153912 Refactor validation rules to implement the new ValidationRule interface (#249) 2024-07-03 00:20:07 +02:00
11e3b167cc global storage-providers, notification channels and server providers (#247) 2024-06-29 11:22:03 +02:00
ad027eb033 Enable 2FA Without QR Code (#248) 2024-06-29 09:30:28 +02:00
e031bafba5 upgrade to Laravel 11 and schema squash (#245)
* upgrade to Laravel 11 and schema squash

* code style and npm audit fix

* fix #209
2024-06-24 23:03:02 +02:00
b5c8d99ef8 Added MariaDB 10.6, 10.11 & 11.4 (#243) 2024-06-24 19:22:55 +02:00
109d644ad8 Validate APP_KEY on initializing with Docker (#240) 2024-06-18 20:52:17 +02:00
5ccbab74b1 Hide X-Powered-By header (#239) 2024-06-18 10:23:14 +02:00
7d367465ff deployment script variables (#238) 2024-06-16 23:42:46 +02:00
eec83f577c [1.x] Updated server plans for Hetzner (#236) 2024-06-13 21:58:45 +02:00
fd77368cf3 [BUG] WordPress Install Error (#237) 2024-06-13 21:52:05 +02:00
a862a603f2 Scripts (#233) 2024-06-08 18:18:17 +02:00
3b42f93654 Update ssh key validation to accept other common standards (#228) 2024-06-05 09:38:31 +02:00
661292df5e Fixes a small typo (#226) 2024-06-04 11:40:31 +02:00
0cfb938320 fix missing ubuntu 24 providers (#220) 2024-05-25 12:03:11 +02:00
dd4a3d30c0 Use Site PHP Version to Run Composer Install (#218) 2024-05-22 10:37:16 +02:00
2b849c888e fix update bug 2024-05-15 22:55:35 +02:00
d9a791755e fix updater and add post-update (#213) 2024-05-15 22:49:07 +02:00
e3ea8f975f fix project deletion 404 error (#208) 2024-05-15 13:41:33 +02:00
de468ae1ba Manage site aliases (#206)
* manage site aliases

* build assets

* fix tests
2024-05-15 11:23:24 +02:00
30ef8ad5eb fix letsencrypt for aliases & blank php deployment fix (#204) 2024-05-13 00:37:51 +02:00
88223a61f9 enable/disable cronjobs (#203) 2024-05-11 13:19:19 +02:00
1067a5fd33 build assets 2024-05-11 11:10:15 +02:00
4361305206 build assets 2024-05-11 11:05:53 +02:00
fe331fd2b3 server updates (#202)
* server updates

* add last update check
2024-05-11 10:09:46 +02:00
bbe3ca802d Add Ubuntu 24.04 support (#199)
* ubuntu 24
* updated aws regions and images
2024-05-10 19:10:33 +02:00
765ac21916 restart php after installing phpmyadmin (#198) 2024-05-09 12:52:28 +02:00
016886f307 fix new user bug (#197) 2024-05-09 00:55:52 +02:00
179aefefac source-controls (#193)
* edit source control
* assign project after creation
* global and project scoped source controls
2024-05-08 00:07:11 +02:00
e704a13d6b new toast notification ui (#188) 2024-05-03 10:40:01 +02:00
9936958259 local storage driver & some icon fixes (#187) 2024-05-01 22:54:44 +02:00
f81d928c66 Update README.md 2024-05-01 12:42:16 +02:00
3c4435701d fix deployment button ux (#186) 2024-05-01 09:34:01 +02:00
ebbd81348a build assets 2024-04-29 21:14:32 +02:00
5debbd4f5d update project lowercase 2024-04-29 21:11:07 +02:00
d846acaa8d User management (#185) 2024-04-29 20:58:04 +02:00
35f896eab1 Update feature_request.md
fix #179
2024-04-24 14:40:26 +02:00
25977d2ead move projects from sidebar to topbar (#170)
and fix #169
2024-04-23 21:34:39 +02:00
f0da1c6d8c Accurate deployment statuses (#168) 2024-04-21 16:26:26 +02:00
e2dd9177f7 fix number format 2024-04-17 22:08:19 +02:00
5a9e8d6799 fix monitoring numbers 2024-04-17 21:09:09 +02:00
868b70f530 add cron to docker 2024-04-17 17:14:12 +02:00
d07e9bcad2 remote monitor (#167) 2024-04-17 16:03:06 +02:00
0cd815cce6 ui fix and build 2024-04-14 18:17:44 +02:00
5ab6617b5d fix read file 2024-04-14 17:53:08 +02:00
72b37c56fd ui fix 2024-04-14 14:53:58 +02:00
8a4ef66946 update Feature/add remote server logs (#166) 2024-04-14 14:41:00 +02:00
4517ca7d2a Feature/add remote server logs (#159) 2024-04-14 14:34:47 +02:00
75aed62d75 fix search bar position (#165) 2024-04-14 09:52:06 +02:00
aaef73d89d fix custom vhost update (#164) 2024-04-13 23:47:52 +02:00
f03a029e36 fix metrics page 2024-04-13 22:44:11 +02:00
52d195710b add data retention to the metrics 2024-04-13 22:38:27 +02:00
ddacc32e64 docker release action 2024-04-13 13:19:20 +02:00
2ae9a14d02 docker 2024-04-13 12:44:12 +02:00
3019c3d213 fix docker 2024-04-13 12:31:53 +02:00
c43869d255 docker 2024-04-13 12:23:28 +02:00
18748f77ac build 2024-04-13 11:50:24 +02:00
052e28d2e3 Monitoring & Service Management (#163)
Monitoring & Service Management
2024-04-13 11:47:56 +02:00
87ec0af697 update demo link 2024-04-07 20:19:56 +02:00
e9016737d4 build frontend 2024-04-05 19:49:35 +02:00
f34d5eb82b Bump vite from 4.5.2 to 4.5.3 (#152) 2024-04-05 19:48:37 +02:00
12c500e125 Bug fixes (#155) 2024-04-05 19:45:09 +02:00
2d566b853f use textarea for code editor (#151) 2024-04-03 22:38:28 +02:00
ca93b521ec Merge branch 'main' into 1.x 2024-04-01 21:19:14 +02:00
bce05d3171 Merge pull request #148 from vitodeploy/versioning
show current version
2024-04-01 20:50:03 +02:00
929dd1dbaa show version a bit trasparent on mobile 2024-04-01 00:06:29 +02:00
2bcd145bea docker 2024-03-31 23:58:45 +02:00
c0f903d4ca show current version 2024-03-31 23:29:22 +02:00
cca4ab7ae3 fix code editor 2024-03-29 18:40:20 +01:00
51e7325d3d fix trusted procies 2024-03-29 18:25:14 +01:00
ce085879c1 Merge pull request #144 from vitodeploy/fix-env-update
empty content on editing file
2024-03-29 12:29:21 +01:00
8a49003e9e fix focus issue 2024-03-29 12:21:33 +01:00
dcc4276f09 fix spacing in the editor 2024-03-29 10:07:14 +01:00
f089779045 empty content on editing file 2024-03-29 00:42:36 +01:00
f1efb9a6c8 make project dropdown full width #132 2024-03-28 18:59:37 +01:00
a7d472fb45 update discord link 2024-03-27 22:33:24 +01:00
d01d406d3d Merge pull request #137 from vitodeploy/phpmyadmin
add phpmyadmin
2024-03-27 13:34:40 +01:00
c66c50835a fix tests 2024-03-27 13:32:25 +01:00
b6179d6693 build 2024-03-27 11:49:48 +01:00
9244e69fd8 add phpmyadmin 2024-03-27 11:41:29 +01:00
a7ba095919 build 2024-03-25 23:02:06 +01:00
807ae01646 Merge pull request #135 from vitodeploy/console
headless console
2024-03-25 22:57:56 +01:00
cc896d82e9 Merge branch 'console' of github.com:vitodeploy/vito into console 2024-03-25 22:55:48 +01:00
d16d3c1385 test 2024-03-25 22:52:45 +01:00
3946cf6b34 Merge branch '1.x' into console 2024-03-25 22:20:39 +01:00
165212fed2 add stop button 2024-03-25 22:20:21 +01:00
f6b36dfefc Update CONTRIBUTING.md 2024-03-25 22:19:30 +01:00
33594f2dba headless console 2024-03-24 21:58:48 +01:00
f68d6c7ca2 Merge pull request #133 from vitodeploy/fix-php-ini-update
fix php ini update bug
2024-03-24 15:19:06 +01:00
d504588f95 fix php ini update bug 2024-03-24 15:16:49 +01:00
a0af4e3e9d Merge pull request #128 from vitodeploy/1.x
Merge
2024-03-24 10:07:20 +01:00
ca0e33be2f Merge branch 'main' into 1.x 2024-03-24 09:58:50 +01:00
4d051330d6 Merge (#127) 2024-03-24 09:56:34 +01:00
ab2d6f64f3 Merge branch 'main' into 1.x 2024-03-24 09:56:07 +01:00
d9a56f95dd minify ace.js 2024-03-24 09:01:18 +01:00
884f18db63 Update README.md 2024-03-23 18:13:16 +01:00
536df65fc6 AGPL-3.0 2024-03-23 10:34:51 +01:00
545 changed files with 15810 additions and 25741 deletions

View File

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

View File

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

View File

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

View File

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

35
.github/workflows/docker-1x.yml vendored Normal file
View File

@ -0,0 +1,35 @@
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

35
.github/workflows/docker-release.yml vendored Normal file
View File

@ -0,0 +1,35 @@
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,6 +7,8 @@
/storage/test-key /storage/test-key
/storage/test-key.pub /storage/test-key.pub
/vendor /vendor
/storage/database.sqlite
/storage/database-test.sqlite
.env .env
.env.backup .env.backup
.env.production .env.production

View File

@ -1,23 +1,5 @@
# Contributing # Contributing
Thank you for your interest in contributing! There are a couple of contribution guidelines that make it easier to apply the incoming suggestions.
If you want to contribute please start with the issues. Issues labeled with "Bug" are the higher priorities. Please read the contribution guide on the website
## Issues https://vitodeploy.com/introduction/contribution-guide.html
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"> <p align="center">
<img alt="srcshot 2024-02-23 at 16 26 21@2x" src="https://github.com/vitodeploy/vito/assets/61919774/9b3ae8fe-996a-4e10-b42e-74097f8e5512" alt="VitoDeploy> <img src="https://github.com/vitodeploy/vito/assets/61919774/8060fded-58e3-4d58-b58b-5b717b0718e9" alt="VitoDeploy>
<p align="center"> <p align="center">
<a href="https://github.com/vitodeploy/vito/actions"><img alt="GitHub Workflow Status" src="https://github.com/vitodeploy/vito/workflows/tests/badge.svg"></a> <a href="https://github.com/vitodeploy/vito/actions"><img alt="GitHub Workflow Status" src="https://github.com/vitodeploy/vito/workflows/tests/badge.svg"></a>
</p> </p>
@ -36,8 +36,8 @@ ## Useful Links
- [Install via Docker](https://vitodeploy.com/introduction/installation.html#install-via-docker) - [Install via Docker](https://vitodeploy.com/introduction/installation.html#install-via-docker)
- [Feedbacks](https://vitodeploy.featurebase.app) - [Feedbacks](https://vitodeploy.featurebase.app)
- [Roadmap](https://vitodeploy.featurebase.app/roadmap) - [Roadmap](https://vitodeploy.featurebase.app/roadmap)
- [Video Demo](https://youtu.be/rLRHIyEfON8) - [Video Demo](https://youtu.be/AbmUOBDOc28)
- [Discord](https://discord.gg/dcUWA5DV) - [Discord](https://discord.gg/uZeeHZZnm5)
- [Contribution](/CONTRIBUTING.md) - [Contribution](/CONTRIBUTING.md)
- [Security](/SECURITY.md) - [Security](/SECURITY.md)
@ -50,7 +50,6 @@ ## Credits
- Alpinejs - Alpinejs
- HTMX - HTMX
- Vite - Vite
- Toastr by CodeSeven
- Prettier - Prettier
- Postcss - Postcss
- Flowbite - Flowbite

View File

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

View File

@ -0,0 +1,20 @@
<?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

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

View File

@ -0,0 +1,150 @@
<?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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,57 @@
<?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

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

View File

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

View File

@ -0,0 +1,33 @@
<?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,10 +2,14 @@
namespace App\Actions\Site; namespace App\Actions\Site;
use App\Exceptions\SSHUploadFailed;
use App\Models\Site; use App\Models\Site;
class UpdateEnv class UpdateEnv
{ {
/**
* @throws SSHUploadFailed
*/
public function update(Site $site, array $input): void public function update(Site $site, array $input): void
{ {
$site->server->os()->editFile( $site->server->os()->editFile(

View File

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

View File

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

View File

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

View File

@ -0,0 +1,58 @@
<?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

@ -0,0 +1,49 @@
<?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

@ -0,0 +1,15 @@
<?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

@ -0,0 +1,36 @@
<?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

@ -0,0 +1,38 @@
<?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

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

View File

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

View File

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

View File

@ -0,0 +1,28 @@
<?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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

30
app/Facades/FTP.php Normal file
View File

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

37
app/Helpers/FTP.php Normal file
View File

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

View File

@ -0,0 +1,36 @@
<?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,10 +1,11 @@
<?php <?php
namespace App\Http\Controllers; namespace App\Http\Controllers\API;
use App\Actions\Site\Deploy; use App\Actions\Site\Deploy;
use App\Exceptions\SourceControlIsNotConnected; use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Notifier; use App\Facades\Notifier;
use App\Http\Controllers\Controller;
use App\Models\GitHook; use App\Models\GitHook;
use App\Models\ServerLog; use App\Models\ServerLog;
use App\Notifications\SourceControlDisconnected; use App\Notifications\SourceControlDisconnected;

View File

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

View File

@ -0,0 +1,47 @@
<?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,6 +4,8 @@
use App\Actions\CronJob\CreateCronJob; use App\Actions\CronJob\CreateCronJob;
use App\Actions\CronJob\DeleteCronJob; use App\Actions\CronJob\DeleteCronJob;
use App\Actions\CronJob\DisableCronJob;
use App\Actions\CronJob\EnableCronJob;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse; use App\Helpers\HtmxResponse;
use App\Models\CronJob; use App\Models\CronJob;
@ -16,6 +18,8 @@ class CronjobController extends Controller
{ {
public function index(Server $server): View public function index(Server $server): View
{ {
$this->authorize('manage', $server);
return view('cronjobs.index', [ return view('cronjobs.index', [
'server' => $server, 'server' => $server,
'cronjobs' => $server->cronJobs, 'cronjobs' => $server->cronJobs,
@ -24,6 +28,8 @@ public function index(Server $server): View
public function store(Server $server, Request $request): HtmxResponse public function store(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
app(CreateCronJob::class)->create($server, $request->input()); app(CreateCronJob::class)->create($server, $request->input());
Toast::success('Cronjob created successfully.'); Toast::success('Cronjob created successfully.');
@ -33,10 +39,34 @@ public function store(Server $server, Request $request): HtmxResponse
public function destroy(Server $server, CronJob $cronJob): RedirectResponse public function destroy(Server $server, CronJob $cronJob): RedirectResponse
{ {
$this->authorize('manage', $server);
app(DeleteCronJob::class)->delete($server, $cronJob); app(DeleteCronJob::class)->delete($server, $cronJob);
Toast::success('Cronjob deleted successfully.'); Toast::success('Cronjob deleted successfully.');
return back(); 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,6 +18,8 @@ class DatabaseBackupController extends Controller
{ {
public function show(Server $server, Backup $backup): View public function show(Server $server, Backup $backup): View
{ {
$this->authorize('manage', $server);
return view('databases.backups', [ return view('databases.backups', [
'server' => $server, 'server' => $server,
'databases' => $server->databases, 'databases' => $server->databases,
@ -28,6 +30,8 @@ public function show(Server $server, Backup $backup): View
public function run(Server $server, Backup $backup): RedirectResponse public function run(Server $server, Backup $backup): RedirectResponse
{ {
$this->authorize('manage', $server);
app(RunBackup::class)->run($backup); app(RunBackup::class)->run($backup);
Toast::success('Backup is running.'); Toast::success('Backup is running.');
@ -37,6 +41,8 @@ public function run(Server $server, Backup $backup): RedirectResponse
public function store(Server $server, Request $request): HtmxResponse public function store(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
app(CreateBackup::class)->create('database', $server, $request->input()); app(CreateBackup::class)->create('database', $server, $request->input());
Toast::success('Backup created successfully.'); Toast::success('Backup created successfully.');
@ -46,6 +52,8 @@ public function store(Server $server, Request $request): HtmxResponse
public function destroy(Server $server, Backup $backup): RedirectResponse public function destroy(Server $server, Backup $backup): RedirectResponse
{ {
$this->authorize('manage', $server);
$backup->delete(); $backup->delete();
Toast::success('Backup deleted successfully.'); Toast::success('Backup deleted successfully.');
@ -55,6 +63,8 @@ public function destroy(Server $server, Backup $backup): RedirectResponse
public function restore(Server $server, Backup $backup, BackupFile $backupFile, Request $request): HtmxResponse public function restore(Server $server, Backup $backup, BackupFile $backupFile, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
app(RestoreBackup::class)->restore($backupFile, $request->input()); app(RestoreBackup::class)->restore($backupFile, $request->input());
Toast::success('Backup restored successfully.'); Toast::success('Backup restored successfully.');
@ -64,8 +74,17 @@ public function restore(Server $server, Backup $backup, BackupFile $backupFile,
public function destroyFile(Server $server, Backup $backup, BackupFile $backupFile): RedirectResponse public function destroyFile(Server $server, Backup $backup, BackupFile $backupFile): RedirectResponse
{ {
$this->authorize('manage', $server);
$backupFile->delete(); $backupFile->delete();
$backupFile
->backup
->storage
->provider()
->ssh($server)
->delete($backupFile->storagePath());
Toast::success('Backup file deleted successfully.'); Toast::success('Backup file deleted successfully.');
return back(); return back();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@
use App\Actions\Server\EditServer; use App\Actions\Server\EditServer;
use App\Actions\Server\RebootServer; use App\Actions\Server\RebootServer;
use App\Actions\Server\Update;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse; use App\Helpers\HtmxResponse;
use App\Models\Server; use App\Models\Server;
@ -15,11 +16,15 @@ class ServerSettingController extends Controller
{ {
public function index(Server $server): View public function index(Server $server): View
{ {
$this->authorize('manage', $server);
return view('server-settings.index', compact('server')); return view('server-settings.index', compact('server'));
} }
public function checkConnection(Server $server): RedirectResponse|HtmxResponse public function checkConnection(Server $server): RedirectResponse|HtmxResponse
{ {
$this->authorize('manage', $server);
$oldStatus = $server->status; $oldStatus = $server->status;
$server = $server->checkConnection(); $server = $server->checkConnection();
@ -41,6 +46,8 @@ public function checkConnection(Server $server): RedirectResponse|HtmxResponse
public function reboot(Server $server): HtmxResponse public function reboot(Server $server): HtmxResponse
{ {
$this->authorize('manage', $server);
app(RebootServer::class)->reboot($server); app(RebootServer::class)->reboot($server);
Toast::info('Server is rebooting.'); Toast::info('Server is rebooting.');
@ -50,10 +57,32 @@ public function reboot(Server $server): HtmxResponse
public function edit(Request $request, Server $server): RedirectResponse public function edit(Request $request, Server $server): RedirectResponse
{ {
$this->authorize('manage', $server);
app(EditServer::class)->edit($server, $request->input()); app(EditServer::class)->edit($server, $request->input());
Toast::success('Server updated.'); Toast::success('Server updated.');
return back(); 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,16 +2,22 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\Service\Install;
use App\Actions\Service\Uninstall;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class ServiceController extends Controller class ServiceController extends Controller
{ {
public function index(Server $server): View public function index(Server $server): View
{ {
$this->authorize('manage', $server);
return view('services.index', [ return view('services.index', [
'server' => $server, 'server' => $server,
'services' => $server->services, 'services' => $server->services,
@ -20,6 +26,8 @@ public function index(Server $server): View
public function start(Server $server, Service $service): RedirectResponse public function start(Server $server, Service $service): RedirectResponse
{ {
$this->authorize('manage', $server);
$service->start(); $service->start();
Toast::success('Service is being started!'); Toast::success('Service is being started!');
@ -29,6 +37,8 @@ public function start(Server $server, Service $service): RedirectResponse
public function stop(Server $server, Service $service): RedirectResponse public function stop(Server $server, Service $service): RedirectResponse
{ {
$this->authorize('manage', $server);
$service->stop(); $service->stop();
Toast::success('Service is being stopped!'); Toast::success('Service is being stopped!');
@ -38,6 +48,8 @@ public function stop(Server $server, Service $service): RedirectResponse
public function restart(Server $server, Service $service): RedirectResponse public function restart(Server $server, Service $service): RedirectResponse
{ {
$this->authorize('manage', $server);
$service->restart(); $service->restart();
Toast::success('Service is being restarted!'); Toast::success('Service is being restarted!');
@ -47,6 +59,8 @@ public function restart(Server $server, Service $service): RedirectResponse
public function enable(Server $server, Service $service): RedirectResponse public function enable(Server $server, Service $service): RedirectResponse
{ {
$this->authorize('manage', $server);
$service->enable(); $service->enable();
Toast::success('Service is being enabled!'); Toast::success('Service is being enabled!');
@ -56,10 +70,34 @@ public function enable(Server $server, Service $service): RedirectResponse
public function disable(Server $server, Service $service): RedirectResponse public function disable(Server $server, Service $service): RedirectResponse
{ {
$this->authorize('manage', $server);
$service->disable(); $service->disable();
Toast::success('Service is being disabled!'); Toast::success('Service is being disabled!');
return back(); 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,6 +3,7 @@
namespace App\Http\Controllers\Settings; namespace App\Http\Controllers\Settings;
use App\Actions\NotificationChannels\AddChannel; use App\Actions\NotificationChannels\AddChannel;
use App\Actions\NotificationChannels\EditChannel;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse; use App\Helpers\HtmxResponse;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
@ -13,11 +14,17 @@
class NotificationChannelController extends Controller class NotificationChannelController extends Controller
{ {
public function index(): View public function index(Request $request): View
{ {
return view('settings.notification-channels.index', [ $data = [
'channels' => NotificationChannel::query()->latest()->get(), 'channels' => NotificationChannel::getByProjectId(auth()->user()->current_project_id)->get(),
]); ];
if ($request->has('edit')) {
$data['editChannel'] = NotificationChannel::find($request->input('edit'));
}
return view('settings.notification-channels.index', $data);
} }
public function add(Request $request): HtmxResponse public function add(Request $request): HtmxResponse
@ -29,7 +36,20 @@ public function add(Request $request): HtmxResponse
Toast::success('Channel added successfully'); Toast::success('Channel added successfully');
return htmx()->redirect(route('notification-channels')); return htmx()->redirect(route('settings.notification-channels'));
}
public function update(NotificationChannel $notificationChannel, Request $request): HtmxResponse
{
app(EditChannel::class)->edit(
$notificationChannel,
$request->user(),
$request->input(),
);
Toast::success('Channel updated.');
return htmx()->redirect(route('settings.notification-channels'));
} }
public function delete(int $id): RedirectResponse public function delete(int $id): RedirectResponse
@ -40,6 +60,6 @@ public function delete(int $id): RedirectResponse
Toast::success('Channel deleted successfully'); Toast::success('Channel deleted successfully');
return redirect()->route('notification-channels'); return redirect()->route('settings.notification-channels');
} }
} }

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@
use App\Actions\SourceControl\ConnectSourceControl; use App\Actions\SourceControl\ConnectSourceControl;
use App\Actions\SourceControl\DeleteSourceControl; use App\Actions\SourceControl\DeleteSourceControl;
use App\Actions\SourceControl\EditSourceControl;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse; use App\Helpers\HtmxResponse;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
@ -14,22 +15,42 @@
class SourceControlController extends Controller class SourceControlController extends Controller
{ {
public function index(): View public function index(Request $request): View
{ {
return view('settings.source-controls.index', [ $data = [
'sourceControls' => SourceControl::query()->orderByDesc('id')->get(), 'sourceControls' => SourceControl::getByProjectId(auth()->user()->current_project_id)->get(),
]); ];
if ($request->has('edit')) {
$data['editSourceControl'] = SourceControl::find($request->input('edit'));
}
return view('settings.source-controls.index', $data);
} }
public function connect(Request $request): HtmxResponse public function connect(Request $request): HtmxResponse
{ {
app(ConnectSourceControl::class)->connect( app(ConnectSourceControl::class)->connect(
$request->user(),
$request->input(), $request->input(),
); );
Toast::success('Source control connected.'); Toast::success('Source control connected.');
return htmx()->redirect(route('source-controls')); return htmx()->redirect(route('settings.source-controls'));
}
public function update(SourceControl $sourceControl, Request $request): HtmxResponse
{
app(EditSourceControl::class)->edit(
$sourceControl,
$request->user(),
$request->input(),
);
Toast::success('Source control updated.');
return htmx()->redirect(route('settings.source-controls'));
} }
public function delete(SourceControl $sourceControl): RedirectResponse public function delete(SourceControl $sourceControl): RedirectResponse
@ -44,6 +65,6 @@ public function delete(SourceControl $sourceControl): RedirectResponse
Toast::success('Source control deleted.'); Toast::success('Source control deleted.');
return redirect()->route('source-controls'); return redirect()->route('settings.source-controls');
} }
} }

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