Compare commits

...

120 Commits
1.0.0 ... 1.x

Author SHA1 Message Date
dependabot[bot]
fb651ab5ce
Bump laravel/framework from 11.11.1 to 11.31.0 (#363)
Bumps [laravel/framework](https://github.com/laravel/framework) from 11.11.1 to 11.31.0.
- [Release notes](https://github.com/laravel/framework/releases)
- [Changelog](https://github.com/laravel/framework/blob/11.x/CHANGELOG.md)
- [Commits](https://github.com/laravel/framework/compare/v11.11.1...v11.31.0)

---
updated-dependencies:
- dependency-name: laravel/framework
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-14 21:51:08 +01:00
Eduardo Daniel Oseguera Mendoza
c2625a7352
added overflow to dropdown projects list (#315) 2024-11-08 21:31:07 +01:00
Bruno Bernard
b72a2ddb1c
feat: add missing ufw (#346) 2024-11-08 21:24:12 +01:00
dependabot[bot]
0e8e6ef56f
Bump symfony/process from 7.1.1 to 7.1.7 (#351)
Bumps [symfony/process](https://github.com/symfony/process) from 7.1.1 to 7.1.7.
- [Release notes](https://github.com/symfony/process/releases)
- [Changelog](https://github.com/symfony/process/blob/7.1/CHANGELOG.md)
- [Commits](https://github.com/symfony/process/compare/v7.1.1...v7.1.7)

---
updated-dependencies:
- dependency-name: symfony/process
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Saeed Vaziry <61919774+saeedvaziry@users.noreply.github.com>
2024-11-08 20:53:01 +01:00
dependabot[bot]
39fa25aee7
Bump symfony/http-foundation from 7.1.1 to 7.1.7 (#352) 2024-11-07 20:12:01 +01:00
Bernard Sarfo Twumasi
945c2e75c0
Fix Privileges for public Schema in PostgreSQL 15+ (#339) 2024-11-07 20:02:26 +01:00
dependabot[bot]
82933e29ff
Bump rollup from 3.29.4 to 3.29.5 (#301)
Bumps [rollup](https://github.com/rollup/rollup) from 3.29.4 to 3.29.5.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v3.29.4...v3.29.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-02 23:55:07 +02:00
Saeed Vaziry
82c1f36ef6
Update README.md (#295) 2024-10-02 21:35:18 +02:00
Saeed Vaziry
e06d23b31a
delete feature request template 2024-10-02 21:30:10 +02:00
Saeed Vaziry
f0e7faa0e7
Create config.yml 2024-10-02 21:28:21 +02:00
dependabot[bot]
319fdb44e7
Bump vite from 4.5.3 to 4.5.5 (#290)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.3 to 4.5.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.5/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-21 17:29:12 +02:00
dependabot[bot]
b62c40c97d
Bump micromatch from 4.0.5 to 4.0.8 (#287)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

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

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

* code style and npm audit fix

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

* build assets

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

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

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

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

View File

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

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>
@ -34,10 +34,9 @@ ## Useful Links
- [Documentation](https://vitodeploy.com) - [Documentation](https://vitodeploy.com)
- [Install on Server](https://vitodeploy.com/introduction/installation.html#install-on-vps-recommended) - [Install on Server](https://vitodeploy.com/introduction/installation.html#install-on-vps-recommended)
- [Install via Docker](https://vitodeploy.com/introduction/installation.html#install-via-docker) - [Install via Docker](https://vitodeploy.com/introduction/installation.html#install-via-docker)
- [Feedbacks](https://vitodeploy.featurebase.app) - [Roadmap](https://github.com/orgs/vitodeploy/projects/5)
- [Roadmap](https://vitodeploy.featurebase.app/roadmap) - [Video Demo](https://youtu.be/AbmUOBDOc28)
- [Video Demo](https://youtu.be/rLRHIyEfON8) - [Discord](https://discord.gg/uZeeHZZnm5)
- [Discord](https://discord.gg/dcUWA5DV)
- [Contribution](/CONTRIBUTING.md) - [Contribution](/CONTRIBUTING.md)
- [Security](/SECURITY.md) - [Security](/SECURITY.md)
@ -50,7 +49,6 @@ ## Credits
- Alpinejs - Alpinejs
- HTMX - HTMX
- Vite - Vite
- Toastr by CodeSeven
- Prettier - Prettier
- Postcss - Postcss
- Flowbite - Flowbite

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@
use App\Actions\Server\EditServer; use App\Actions\Server\EditServer;
use App\Actions\Server\RebootServer; use App\Actions\Server\RebootServer;
use App\Actions\Server\Update;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse; use App\Helpers\HtmxResponse;
use App\Models\Server; use App\Models\Server;
@ -15,11 +16,15 @@ class ServerSettingController extends Controller
{ {
public function index(Server $server): View public function index(Server $server): View
{ {
$this->authorize('manage', $server);
return view('server-settings.index', compact('server')); return view('server-settings.index', compact('server'));
} }
public function checkConnection(Server $server): RedirectResponse|HtmxResponse public function checkConnection(Server $server): RedirectResponse|HtmxResponse
{ {
$this->authorize('manage', $server);
$oldStatus = $server->status; $oldStatus = $server->status;
$server = $server->checkConnection(); $server = $server->checkConnection();
@ -41,6 +46,8 @@ public function checkConnection(Server $server): RedirectResponse|HtmxResponse
public function reboot(Server $server): HtmxResponse public function reboot(Server $server): HtmxResponse
{ {
$this->authorize('manage', $server);
app(RebootServer::class)->reboot($server); app(RebootServer::class)->reboot($server);
Toast::info('Server is rebooting.'); Toast::info('Server is rebooting.');
@ -50,10 +57,32 @@ public function reboot(Server $server): HtmxResponse
public function edit(Request $request, Server $server): RedirectResponse public function edit(Request $request, Server $server): RedirectResponse
{ {
$this->authorize('manage', $server);
app(EditServer::class)->edit($server, $request->input()); app(EditServer::class)->edit($server, $request->input());
Toast::success('Server updated.'); Toast::success('Server updated.');
return back(); return back();
} }
public function checkUpdates(Server $server): RedirectResponse
{
$this->authorize('manage', $server);
$server->checkForUpdates();
return back();
}
public function update(Server $server): HtmxResponse
{
$this->authorize('manage', $server);
app(Update::class)->update($server);
Toast::info('Updating server. This may take a few minutes.');
return htmx()->back();
}
} }

View File

@ -2,16 +2,22 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\Service\Install;
use App\Actions\Service\Uninstall;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class ServiceController extends Controller class ServiceController extends Controller
{ {
public function index(Server $server): View public function index(Server $server): View
{ {
$this->authorize('manage', $server);
return view('services.index', [ return view('services.index', [
'server' => $server, 'server' => $server,
'services' => $server->services, 'services' => $server->services,
@ -20,6 +26,8 @@ public function index(Server $server): View
public function start(Server $server, Service $service): RedirectResponse public function start(Server $server, Service $service): RedirectResponse
{ {
$this->authorize('manage', $server);
$service->start(); $service->start();
Toast::success('Service is being started!'); Toast::success('Service is being started!');
@ -29,6 +37,8 @@ public function start(Server $server, Service $service): RedirectResponse
public function stop(Server $server, Service $service): RedirectResponse public function stop(Server $server, Service $service): RedirectResponse
{ {
$this->authorize('manage', $server);
$service->stop(); $service->stop();
Toast::success('Service is being stopped!'); Toast::success('Service is being stopped!');
@ -38,6 +48,8 @@ public function stop(Server $server, Service $service): RedirectResponse
public function restart(Server $server, Service $service): RedirectResponse public function restart(Server $server, Service $service): RedirectResponse
{ {
$this->authorize('manage', $server);
$service->restart(); $service->restart();
Toast::success('Service is being restarted!'); Toast::success('Service is being restarted!');
@ -47,6 +59,8 @@ public function restart(Server $server, Service $service): RedirectResponse
public function enable(Server $server, Service $service): RedirectResponse public function enable(Server $server, Service $service): RedirectResponse
{ {
$this->authorize('manage', $server);
$service->enable(); $service->enable();
Toast::success('Service is being enabled!'); Toast::success('Service is being enabled!');
@ -56,10 +70,34 @@ public function enable(Server $server, Service $service): RedirectResponse
public function disable(Server $server, Service $service): RedirectResponse public function disable(Server $server, Service $service): RedirectResponse
{ {
$this->authorize('manage', $server);
$service->disable(); $service->disable();
Toast::success('Service is being disabled!'); Toast::success('Service is being disabled!');
return back(); return back();
} }
public function install(Server $server, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
app(Install::class)->install($server, $request->input());
Toast::success('Service is being installed!');
return htmx()->back();
}
public function uninstall(Server $server, Service $service): HtmxResponse
{
$this->authorize('manage', $server);
app(Uninstall::class)->uninstall($service);
Toast::success('Service is being uninstalled!');
return htmx()->back();
}
} }

View File

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

View File

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

View File

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

View File

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

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