Compare commits

...

79 Commits
1.0.0 ... 1.6.0

Author SHA1 Message Date
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
395 changed files with 9657 additions and 24698 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

@ -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 ($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

@ -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

@ -3,6 +3,7 @@
namespace App\Actions\PHP; namespace App\Actions\PHP;
use App\Models\Server; use App\Models\Server;
use App\SSH\Services\PHP\PHP;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
class GetPHPIni class GetPHPIni
@ -14,7 +15,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();
} catch (\Throwable $e) { } catch (\Throwable $e) {
throw ValidationException::withMessages( throw ValidationException::withMessages(
['ini' => $e->getMessage()] ['ini' => $e->getMessage()]

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

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

@ -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

@ -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,15 @@ public function run(Site $site): Deployment
$deployment->save(); $deployment->save();
dispatch(function () use ($site, $deployment) { dispatch(function () use ($site, $deployment) {
$log = $site->server->os()->runScript($site->path, $site->deploymentScript->content, $site->id); /** @var ServerLog $log */
$deployment->status = DeploymentStatus::FINISHED; $log = ServerLog::make($site->server, 'deploy-'.strtotime('now'))
->forSite($site);
$log->save();
$deployment->log_id = $log->id; $deployment->log_id = $log->id;
$deployment->save(); $deployment->save();
$site->server->os()->runScript($site->path, $site->deploymentScript->content, $log);
$deployment->status = DeploymentStatus::FINISHED;
$deployment->save();
})->catch(function () use ($deployment) { })->catch(function () use ($deployment) {
$deployment->status = DeploymentStatus::FAILED; $deployment->status = DeploymentStatus::FAILED;
$deployment->save(); $deployment->save();

View File

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

@ -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 = isset($input['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

@ -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';
} }

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,6 @@ final class StorageProvider
const DROPBOX = 'dropbox'; const DROPBOX = 'dropbox';
const FTP = 'ftp'; const FTP = 'ftp';
const LOCAL = 'local';
} }

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

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

View File

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

View File

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

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,9 +11,9 @@
* 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 disconnect() * @method static disconnect()

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,6 +7,8 @@
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\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse; use App\Helpers\HtmxResponse;
@ -20,16 +22,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 +43,15 @@ public function deploy(Server $server, Site $site): HtmxResponse
public function showDeploymentLog(Server $server, Site $site, Deployment $deployment): RedirectResponse public function showDeploymentLog(Server $server, Site $site, Deployment $deployment): RedirectResponse
{ {
$this->authorize('manage', $server);
return back()->with('content', $deployment->log?->getContent()); return back()->with('content', $deployment->log?->getContent());
} }
public function updateDeploymentScript(Server $server, Site $site, Request $request): RedirectResponse public function updateDeploymentScript(Server $server, Site $site, Request $request): RedirectResponse
{ {
$this->authorize('manage', $server);
app(UpdateDeploymentScript::class)->update($site, $request->input()); app(UpdateDeploymentScript::class)->update($site, $request->input());
Toast::success('Deployment script updated!'); Toast::success('Deployment script updated!');
@ -51,6 +61,8 @@ public function updateDeploymentScript(Server $server, Site $site, Request $requ
public function updateBranch(Server $server, Site $site, Request $request): RedirectResponse public function updateBranch(Server $server, Site $site, Request $request): RedirectResponse
{ {
$this->authorize('manage', $server);
app(UpdateBranch::class)->update($site, $request->input()); app(UpdateBranch::class)->update($site, $request->input());
Toast::success('Branch updated!'); Toast::success('Branch updated!');
@ -60,11 +72,15 @@ public function updateBranch(Server $server, Site $site, Request $request): Redi
public function getEnv(Server $server, Site $site): RedirectResponse public function getEnv(Server $server, Site $site): RedirectResponse
{ {
$this->authorize('manage', $server);
return back()->with('env', $site->getEnv()); return back()->with('env', $site->getEnv());
} }
public function updateEnv(Server $server, Site $site, Request $request): RedirectResponse public function updateEnv(Server $server, Site $site, Request $request): RedirectResponse
{ {
$this->authorize('manage', $server);
app(UpdateEnv::class)->update($site, $request->input()); app(UpdateEnv::class)->update($site, $request->input());
Toast::success('Env updated!'); Toast::success('Env updated!');
@ -74,6 +90,8 @@ public function updateEnv(Server $server, Site $site, Request $request): Redirec
public function enableAutoDeployment(Server $server, Site $site): HtmxResponse public function enableAutoDeployment(Server $server, Site $site): HtmxResponse
{ {
$this->authorize('manage', $server);
if (! $site->isAutoDeployment()) { if (! $site->isAutoDeployment()) {
try { try {
$site->enableAutoDeployment(); $site->enableAutoDeployment();
@ -83,6 +101,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 +115,8 @@ public function enableAutoDeployment(Server $server, Site $site): HtmxResponse
public function disableAutoDeployment(Server $server, Site $site): HtmxResponse public function disableAutoDeployment(Server $server, Site $site): HtmxResponse
{ {
$this->authorize('manage', $server);
if ($site->isAutoDeployment()) { if ($site->isAutoDeployment()) {
try { try {
$site->disableAutoDeployment(); $site->disableAutoDeployment();

View File

@ -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 updated!');
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,6 +27,8 @@ public function index(Server $server): View
public function store(Server $server, Request $request): HtmxResponse public function store(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
/** @var \App\Models\SshKey $key */ /** @var \App\Models\SshKey $key */
$key = app(CreateSshKey::class)->create( $key = app(CreateSshKey::class)->create(
$request->user(), $request->user(),
@ -38,6 +42,8 @@ public function store(Server $server, Request $request): HtmxResponse
public function destroy(Server $server, SshKey $sshKey): RedirectResponse public function destroy(Server $server, SshKey $sshKey): RedirectResponse
{ {
$this->authorize('manage', $server);
app(DeleteKeyFromServer::class)->delete($server, $sshKey); app(DeleteKeyFromServer::class)->delete($server, $sshKey);
Toast::success('SSH Key has been deleted.'); Toast::success('SSH Key has been deleted.');
@ -47,6 +53,8 @@ public function destroy(Server $server, SshKey $sshKey): RedirectResponse
public function deploy(Server $server, Request $request): HtmxResponse public function deploy(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
app(DeployKeyToServer::class)->deploy( app(DeployKeyToServer::class)->deploy(
$request->user(), $request->user(),
$server, $server,

View File

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

View File

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

View File

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

View File

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

View File

@ -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

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

View File

@ -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) {
@ -74,6 +76,8 @@ public function switch($projectId): RedirectResponse
/** @var Project $project */ /** @var Project $project */
$project = $user->projects()->findOrFail($projectId); $project = $user->projects()->findOrFail($projectId);
$this->authorize('view', $project);
$user->current_project_id = $project->id; $user->current_project_id = $project->id;
$user->save(); $user->save();

View File

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

View File

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

View File

@ -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::getByCurrentProject(),
]); ];
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');
} }
} }

View File

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

View File

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

View File

@ -4,6 +4,7 @@
use App\Actions\Site\CreateSite; use App\Actions\Site\CreateSite;
use App\Actions\Site\DeleteSite; use App\Actions\Site\DeleteSite;
use App\Enums\SiteStatus;
use App\Enums\SiteType; use App\Enums\SiteType;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse; use App\Helpers\HtmxResponse;
@ -18,6 +19,8 @@ class SiteController extends Controller
{ {
public function index(Server $server): View public function index(Server $server): View
{ {
$this->authorize('manage', $server);
return view('sites.index', [ return view('sites.index', [
'server' => $server, 'server' => $server,
'sites' => $server->sites()->orderByDesc('id')->get(), 'sites' => $server->sites()->orderByDesc('id')->get(),
@ -26,6 +29,8 @@ public function index(Server $server): View
public function store(Server $server, Request $request): HtmxResponse public function store(Server $server, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
$site = app(CreateSite::class)->create($server, $request->input()); $site = app(CreateSite::class)->create($server, $request->input());
Toast::success('Site created'); Toast::success('Site created');
@ -35,6 +40,8 @@ public function store(Server $server, Request $request): HtmxResponse
public function create(Server $server): View public function create(Server $server): View
{ {
$this->authorize('manage', $server);
return view('sites.create', [ return view('sites.create', [
'server' => $server, 'server' => $server,
'type' => old('type', request()->query('type', SiteType::LARAVEL)), 'type' => old('type', request()->query('type', SiteType::LARAVEL)),
@ -42,16 +49,46 @@ public function create(Server $server): View
]); ]);
} }
public function show(Server $server, Site $site): View public function show(Server $server, Site $site, Request $request): View|RedirectResponse|HtmxResponse
{ {
$this->authorize('manage', $server);
if (in_array($site->status, [SiteStatus::INSTALLING, SiteStatus::INSTALLATION_FAILED])) {
if ($request->hasHeader('HX-Request')) {
return htmx()->redirect(route('servers.sites.installing', [$server, $site]));
}
return redirect()->route('servers.sites.installing', [$server, $site]);
}
return view('sites.show', [ return view('sites.show', [
'server' => $server, 'server' => $server,
'site' => $site, 'site' => $site,
]); ]);
} }
public function installing(Server $server, Site $site, Request $request): View|RedirectResponse|HtmxResponse
{
$this->authorize('manage', $server);
if (! in_array($site->status, [SiteStatus::INSTALLING, SiteStatus::INSTALLATION_FAILED])) {
if ($request->hasHeader('HX-Request')) {
return htmx()->redirect(route('servers.sites.show', [$server, $site]));
}
return redirect()->route('servers.sites.show', [$server, $site]);
}
return view('sites.installing', [
'server' => $server,
'site' => $site,
]);
}
public function destroy(Server $server, Site $site): RedirectResponse public function destroy(Server $server, Site $site): RedirectResponse
{ {
$this->authorize('manage', $server);
app(DeleteSite::class)->delete($site); app(DeleteSite::class)->delete($site);
Toast::success('Site is being deleted'); Toast::success('Site is being deleted');

View File

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

View File

@ -2,10 +2,13 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\Site\UpdateAliases;
use App\Actions\Site\UpdateSourceControl;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse; use App\Helpers\HtmxResponse;
use App\Models\Server; use App\Models\Server;
use App\Models\Site; use App\Models\Site;
use App\SSH\Services\Webserver\Webserver;
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;
@ -16,6 +19,8 @@ class SiteSettingController extends Controller
{ {
public function index(Server $server, Site $site): View public function index(Server $server, Site $site): View
{ {
$this->authorize('manage', $server);
return view('site-settings.index', [ return view('site-settings.index', [
'server' => $server, 'server' => $server,
'site' => $site, 'site' => $site,
@ -24,17 +29,26 @@ public function index(Server $server, Site $site): View
public function getVhost(Server $server, Site $site): RedirectResponse public function getVhost(Server $server, Site $site): RedirectResponse
{ {
return back()->with('vhost', $server->webserver()->handler()->getVHost($site)); $this->authorize('manage', $server);
/** @var Webserver $handler */
$handler = $server->webserver()->handler();
return back()->with('vhost', $handler->getVHost($site));
} }
public function updateVhost(Server $server, Site $site, Request $request): RedirectResponse public function updateVhost(Server $server, Site $site, Request $request): RedirectResponse
{ {
$this->authorize('manage', $server);
$this->validate($request, [ $this->validate($request, [
'vhost' => 'required|string', 'vhost' => 'required|string',
]); ]);
try { try {
$server->webserver()->handler()->updateVHost($site, false, $request->input('vhost')); /** @var Webserver $handler */
$handler = $server->webserver()->handler();
$handler->updateVHost($site, false, $request->input('vhost'));
Toast::success('VHost updated successfully!'); Toast::success('VHost updated successfully!');
} catch (Throwable $e) { } catch (Throwable $e) {
@ -46,6 +60,8 @@ public function updateVhost(Server $server, Site $site, Request $request): Redir
public function updatePHPVersion(Server $server, Site $site, Request $request): HtmxResponse public function updatePHPVersion(Server $server, Site $site, Request $request): HtmxResponse
{ {
$this->authorize('manage', $server);
$this->validate($request, [ $this->validate($request, [
'version' => [ 'version' => [
'required', 'required',
@ -63,4 +79,26 @@ public function updatePHPVersion(Server $server, Site $site, Request $request):
return htmx()->back(); return htmx()->back();
} }
public function updateSourceControl(Server $server, Site $site, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
app(UpdateSourceControl::class)->update($site, $request->input());
Toast::success('Source control updated successfully!');
return htmx()->back();
}
public function updateAliases(Server $server, Site $site, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
app(UpdateAliases::class)->update($site, $request->input());
Toast::success('Aliases updated successfully!');
return htmx()->back();
}
} }

View File

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

View File

@ -7,23 +7,24 @@
use App\Facades\Toast; use App\Facades\Toast;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response;
class HandleSSHErrors class HandleSSHErrors
{ {
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {
$res = $next($request); $res = $next($request);
if ($res->exception) { // if ($res instanceof Response && $res->exception) {
if ($res->exception instanceof SSHConnectionError || $res->exception instanceof SSHCommandError) { // if ($res->exception instanceof SSHConnectionError || $res->exception instanceof SSHCommandError) {
Toast::error($res->exception->getMessage()); // Toast::error($res->exception->getMessage());
if ($request->hasHeader('HX-Request')) { // if ($request->hasHeader('HX-Request')) {
return htmx()->back(); // return htmx()->back();
} // }
return back(); // return back();
} // }
} // }
return $res; return $res;
} }

View File

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

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Middleware;
use App\Facades\Toast;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class MustHaveCurrentProject
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
/** @var User $user */
$user = $request->user();
if (! $user->currentProject) {
Toast::warning('Please select a project to continue');
return redirect()->route('profile');
}
return $next($request);
}
}

View File

@ -22,7 +22,7 @@ public function handle(Request $request, Closure $next): Response
/** @var User $user */ /** @var User $user */
$user = $request->user(); $user = $request->user();
if ($server->project_id != $user->current_project_id) { if ($server->project_id != $user->current_project_id && $user->can('view', $server)) {
$user->current_project_id = $server->project_id; $user->current_project_id = $server->project_id;
$user->save(); $user->save();
} }

View File

@ -12,7 +12,7 @@ class TrustProxies extends Middleware
* *
* @var array<int, string>|string|null * @var array<int, string>|string|null
*/ */
protected $proxies; protected $proxies = '*';
/** /**
* The headers that should be used to detect proxies. * The headers that should be used to detect proxies.
@ -20,7 +20,7 @@ class TrustProxies extends Middleware
* @var int * @var int
*/ */
protected $headers = protected $headers =
Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_PROTO |

View File

@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Enums\CronjobStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -41,7 +42,14 @@ public function server(): BelongsTo
public static function crontab(Server $server, string $user): string public static function crontab(Server $server, string $user): string
{ {
$data = ''; $data = '';
$cronJobs = $server->cronJobs()->where('user', $user)->get(); $cronJobs = $server->cronJobs()
->where('user', $user)
->whereIn('status', [
CronjobStatus::READY,
CronjobStatus::CREATING,
CronjobStatus::ENABLING,
])
->get();
foreach ($cronJobs as $key => $cronJob) { foreach ($cronJobs as $key => $cronJob) {
$data .= $cronJob->frequency.' '.$cronJob->command; $data .= $cronJob->frequency.' '.$cronJob->command;
if ($key != count($cronJobs) - 1) { if ($key != count($cronJobs) - 1) {

53
app/Models/Metric.php Normal file
View File

@ -0,0 +1,53 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property int $server_id
* @property float $load
* @property float $memory_total
* @property float $memory_used
* @property float $memory_free
* @property float $disk_total
* @property float $disk_used
* @property float $disk_free
* @property Server $server
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class Metric extends Model
{
use HasFactory;
protected $fillable = [
'server_id',
'load',
'memory_total',
'memory_used',
'memory_free',
'disk_total',
'disk_used',
'disk_free',
];
protected $casts = [
'server_id' => 'integer',
'load' => 'float',
'memory_total' => 'float',
'memory_used' => 'float',
'memory_free' => 'float',
'disk_total' => 'float',
'disk_used' => 'float',
'disk_free' => 'float',
];
public function server(): BelongsTo
{
return $this->belongsTo(Server::class);
}
}

View File

@ -7,6 +7,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
/** /**
@ -18,6 +19,7 @@
* @property User $user * @property User $user
* @property Collection<Server> $servers * @property Collection<Server> $servers
* @property Collection<NotificationChannel> $notificationChannels * @property Collection<NotificationChannel> $notificationChannels
* @property Collection<SourceControl> $sourceControls
*/ */
class Project extends Model class Project extends Model
{ {
@ -53,4 +55,14 @@ public function notificationChannels(): HasMany
{ {
return $this->hasMany(NotificationChannel::class); return $this->hasMany(NotificationChannel::class);
} }
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class, 'user_project')->withTimestamps();
}
public function sourceControls(): HasMany
{
return $this->hasMany(SourceControl::class);
}
} }

View File

@ -9,6 +9,7 @@
use App\SSH\Cron\Cron; use App\SSH\Cron\Cron;
use App\SSH\OS\OS; use App\SSH\OS\OS;
use App\SSH\Systemd\Systemd; use App\SSH\Systemd\Systemd;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@ -55,6 +56,8 @@
* @property Queue[] $daemons * @property Queue[] $daemons
* @property SshKey[] $sshKeys * @property SshKey[] $sshKeys
* @property string $hostname * @property string $hostname
* @property int $updates
* @property Carbon $last_update_check
*/ */
class Server extends AbstractModel class Server extends AbstractModel
{ {
@ -82,6 +85,8 @@ class Server extends AbstractModel
'security_updates', 'security_updates',
'progress', 'progress',
'progress_step', 'progress_step',
'updates',
'last_update_check',
]; ];
protected $casts = [ protected $casts = [
@ -95,6 +100,8 @@ class Server extends AbstractModel
'available_updates' => 'integer', 'available_updates' => 'integer',
'security_updates' => 'integer', 'security_updates' => 'integer',
'progress' => 'integer', 'progress' => 'integer',
'updates' => 'integer',
'last_update_check' => 'datetime',
]; ];
protected $hidden = [ protected $hidden = [
@ -195,6 +202,11 @@ public function daemons(): HasMany
return $this->queues()->whereNull('site_id'); return $this->queues()->whereNull('site_id');
} }
public function metrics(): HasMany
{
return $this->hasMany(Metric::class);
}
public function sshKeys(): BelongsToMany public function sshKeys(): BelongsToMany
{ {
return $this->belongsToMany(SshKey::class, 'server_ssh_keys') return $this->belongsToMany(SshKey::class, 'server_ssh_keys')
@ -325,6 +337,24 @@ public function php(?string $version = null): ?Service
return $this->service('php', $version); return $this->service('php', $version);
} }
public function memoryDatabase(?string $version = null): ?Service
{
if (! $version) {
return $this->defaultService('memory_database');
}
return $this->service('memory_database', $version);
}
public function monitoring(?string $version = null): ?Service
{
if (! $version) {
return $this->defaultService('monitoring');
}
return $this->service('monitoring', $version);
}
public function sshKey(): array public function sshKey(): array
{ {
/** @var FilesystemAdapter $storageDisk */ /** @var FilesystemAdapter $storageDisk */
@ -361,4 +391,11 @@ public function cron(): Cron
{ {
return new Cron($this); return new Cron($this);
} }
public function checkForUpdates(): void
{
$this->updates = $this->os()->availableUpdates();
$this->last_update_check = now();
$this->save();
}
} }

View File

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

View File

@ -4,12 +4,7 @@
use App\Actions\Service\Manage; use App\Actions\Service\Manage;
use App\Exceptions\ServiceInstallationFailed; use App\Exceptions\ServiceInstallationFailed;
use App\SSH\Services\Database\Database as DatabaseHandler; use App\SSH\Services\ServiceInterface;
use App\SSH\Services\Firewall\Firewall as FirewallHandler;
use App\SSH\Services\PHP\PHP as PHPHandler;
use App\SSH\Services\ProcessManager\ProcessManager as ProcessManagerHandler;
use App\SSH\Services\Redis\Redis as RedisHandler;
use App\SSH\Services\Webserver\Webserver as WebserverHandler;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -53,7 +48,9 @@ public static function boot(): void
parent::boot(); parent::boot();
static::creating(function (Service $service) { static::creating(function (Service $service) {
$service->unit = config('core.service_units')[$service->name][$service->server->os][$service->version]; if (array_key_exists($service->name, config('core.service_units'))) {
$service->unit = config('core.service_units')[$service->name][$service->server->os][$service->version];
}
}); });
} }
@ -62,8 +59,8 @@ public function server(): BelongsTo
return $this->belongsTo(Server::class); return $this->belongsTo(Server::class);
} }
public function handler( public function handler(): ServiceInterface
): PHPHandler|WebserverHandler|DatabaseHandler|FirewallHandler|ProcessManagerHandler|RedisHandler { {
$handler = config('core.service_handlers')[$this->name]; $handler = config('core.service_handlers')[$this->name];
return new $handler($this); return new $handler($this);
@ -81,26 +78,26 @@ public function validateInstall($result): void
public function start(): void public function start(): void
{ {
app(Manage::class)->start($this); $this->unit && app(Manage::class)->start($this);
} }
public function stop(): void public function stop(): void
{ {
app(Manage::class)->stop($this); $this->unit && app(Manage::class)->stop($this);
} }
public function restart(): void public function restart(): void
{ {
app(Manage::class)->restart($this); $this->unit && app(Manage::class)->restart($this);
} }
public function enable(): void public function enable(): void
{ {
app(Manage::class)->enable($this); $this->unit && app(Manage::class)->enable($this);
} }
public function disable(): void public function disable(): void
{ {
app(Manage::class)->disable($this); $this->unit && app(Manage::class)->disable($this);
} }
} }

View File

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

View File

@ -3,7 +3,9 @@
namespace App\Models; namespace App\Models;
use App\SourceControlProviders\SourceControlProvider; use App\SourceControlProviders\SourceControlProvider;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
/** /**
@ -12,6 +14,7 @@
* @property ?string $profile * @property ?string $profile
* @property ?string $url * @property ?string $url
* @property string $access_token * @property string $access_token
* @property ?int $project_id
*/ */
class SourceControl extends AbstractModel class SourceControl extends AbstractModel
{ {
@ -23,11 +26,13 @@ class SourceControl extends AbstractModel
'profile', 'profile',
'url', 'url',
'access_token', 'access_token',
'project_id',
]; ];
protected $casts = [ protected $casts = [
'access_token' => 'encrypted', 'access_token' => 'encrypted',
'provider_data' => 'encrypted:array', 'provider_data' => 'encrypted:array',
'project_id' => 'integer',
]; ];
public function provider(): SourceControlProvider public function provider(): SourceControlProvider
@ -46,4 +51,16 @@ public function sites(): HasMany
{ {
return $this->hasMany(Site::class); return $this->hasMany(Site::class);
} }
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public static function getByCurrentProject(): Collection
{
return self::query()
->where('project_id', auth()->user()->current_project_id)
->orWhereNull('project_id')->get();
}
} }

View File

@ -17,6 +17,7 @@
* @property string $status * @property string $status
* @property Site $site * @property Site $site
* @property string $ca_path * @property string $ca_path
* @property ?array $domains
*/ */
class Ssl extends AbstractModel class Ssl extends AbstractModel
{ {
@ -30,6 +31,7 @@ class Ssl extends AbstractModel
'ca', 'ca',
'expires_at', 'expires_at',
'status', 'status',
'domains',
]; ];
protected $casts = [ protected $casts = [
@ -38,6 +40,7 @@ class Ssl extends AbstractModel
'pk' => 'encrypted', 'pk' => 'encrypted',
'ca' => 'encrypted', 'ca' => 'encrypted',
'expires_at' => 'datetime', 'expires_at' => 'datetime',
'domains' => 'array',
]; ];
public function site(): BelongsTo public function site(): BelongsTo
@ -111,4 +114,16 @@ public function validateSetup(string $result): bool
return true; return true;
} }
public function getDomains(): array
{
if (! empty($this->domains) && is_array($this->domains)) {
return $this->domains;
}
$this->domains = [$this->site->domain];
$this->save();
return $this->domains;
}
} }

View File

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

View File

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

View File

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

View File

@ -0,0 +1,33 @@
<?php
namespace App\Notifications;
use App\Models\Server;
use Illuminate\Notifications\Messages\MailMessage;
class ServerUpdateFailed extends AbstractNotification
{
protected Server $server;
public function __construct(Server $server)
{
$this->server = $server;
}
public function rawText(): string
{
return __("Update failed for server [:server] \nCheck your server's logs \n:logs", [
'server' => $this->server->name,
'logs' => url('/servers/'.$this->server->id.'/logs'),
]);
}
public function toEmail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject(__('Server update failed!'))
->line('Your server ['.$this->server->name.'] update has been failed.')
->line('Check your server logs')
->action('View Logs', url('/servers/'.$this->server->id.'/logs'));
}
}

View File

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

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