Compare commits

...

147 Commits
0.x ... 1.10.0

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

* code style and npm audit fix

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

* build assets

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

* add last update check
2024-05-11 10:09:46 +02:00
bbe3ca802d Add Ubuntu 24.04 support (#199)
* ubuntu 24
* updated aws regions and images
2024-05-10 19:10:33 +02:00
765ac21916 restart php after installing phpmyadmin (#198) 2024-05-09 12:52:28 +02:00
016886f307 fix new user bug (#197) 2024-05-09 00:55:52 +02:00
179aefefac source-controls (#193)
* edit source control
* assign project after creation
* global and project scoped source controls
2024-05-08 00:07:11 +02:00
e704a13d6b new toast notification ui (#188) 2024-05-03 10:40:01 +02:00
9936958259 local storage driver & some icon fixes (#187) 2024-05-01 22:54:44 +02:00
f81d928c66 Update README.md 2024-05-01 12:42:16 +02:00
3c4435701d fix deployment button ux (#186) 2024-05-01 09:34:01 +02:00
ebbd81348a build assets 2024-04-29 21:14:32 +02:00
5debbd4f5d update project lowercase 2024-04-29 21:11:07 +02:00
d846acaa8d User management (#185) 2024-04-29 20:58:04 +02:00
35f896eab1 Update feature_request.md
fix #179
2024-04-24 14:40:26 +02:00
25977d2ead move projects from sidebar to topbar (#170)
and fix #169
2024-04-23 21:34:39 +02:00
f0da1c6d8c Accurate deployment statuses (#168) 2024-04-21 16:26:26 +02:00
e2dd9177f7 fix number format 2024-04-17 22:08:19 +02:00
5a9e8d6799 fix monitoring numbers 2024-04-17 21:09:09 +02:00
868b70f530 add cron to docker 2024-04-17 17:14:12 +02:00
d07e9bcad2 remote monitor (#167) 2024-04-17 16:03:06 +02:00
0cd815cce6 ui fix and build 2024-04-14 18:17:44 +02:00
5ab6617b5d fix read file 2024-04-14 17:53:08 +02:00
72b37c56fd ui fix 2024-04-14 14:53:58 +02:00
8a4ef66946 update Feature/add remote server logs (#166) 2024-04-14 14:41:00 +02:00
4517ca7d2a Feature/add remote server logs (#159) 2024-04-14 14:34:47 +02:00
75aed62d75 fix search bar position (#165) 2024-04-14 09:52:06 +02:00
aaef73d89d fix custom vhost update (#164) 2024-04-13 23:47:52 +02:00
f03a029e36 fix metrics page 2024-04-13 22:44:11 +02:00
52d195710b add data retention to the metrics 2024-04-13 22:38:27 +02:00
ddacc32e64 docker release action 2024-04-13 13:19:20 +02:00
2ae9a14d02 docker 2024-04-13 12:44:12 +02:00
3019c3d213 fix docker 2024-04-13 12:31:53 +02:00
c43869d255 docker 2024-04-13 12:23:28 +02:00
18748f77ac build 2024-04-13 11:50:24 +02:00
052e28d2e3 Monitoring & Service Management (#163)
Monitoring & Service Management
2024-04-13 11:47:56 +02:00
87ec0af697 update demo link 2024-04-07 20:19:56 +02:00
e9016737d4 build frontend 2024-04-05 19:49:35 +02:00
f34d5eb82b Bump vite from 4.5.2 to 4.5.3 (#152) 2024-04-05 19:48:37 +02:00
12c500e125 Bug fixes (#155) 2024-04-05 19:45:09 +02:00
2d566b853f use textarea for code editor (#151) 2024-04-03 22:38:28 +02:00
ca93b521ec Merge branch 'main' into 1.x 2024-04-01 21:19:14 +02:00
bce05d3171 Merge pull request #148 from vitodeploy/versioning
show current version
2024-04-01 20:50:03 +02:00
929dd1dbaa show version a bit trasparent on mobile 2024-04-01 00:06:29 +02:00
2bcd145bea docker 2024-03-31 23:58:45 +02:00
c0f903d4ca show current version 2024-03-31 23:29:22 +02:00
cca4ab7ae3 fix code editor 2024-03-29 18:40:20 +01:00
51e7325d3d fix trusted procies 2024-03-29 18:25:14 +01:00
ce085879c1 Merge pull request #144 from vitodeploy/fix-env-update
empty content on editing file
2024-03-29 12:29:21 +01:00
8a49003e9e fix focus issue 2024-03-29 12:21:33 +01:00
dcc4276f09 fix spacing in the editor 2024-03-29 10:07:14 +01:00
f089779045 empty content on editing file 2024-03-29 00:42:36 +01:00
f1efb9a6c8 make project dropdown full width #132 2024-03-28 18:59:37 +01:00
a7d472fb45 update discord link 2024-03-27 22:33:24 +01:00
d01d406d3d Merge pull request #137 from vitodeploy/phpmyadmin
add phpmyadmin
2024-03-27 13:34:40 +01:00
c66c50835a fix tests 2024-03-27 13:32:25 +01:00
b6179d6693 build 2024-03-27 11:49:48 +01:00
9244e69fd8 add phpmyadmin 2024-03-27 11:41:29 +01:00
a7ba095919 build 2024-03-25 23:02:06 +01:00
807ae01646 Merge pull request #135 from vitodeploy/console
headless console
2024-03-25 22:57:56 +01:00
cc896d82e9 Merge branch 'console' of github.com:vitodeploy/vito into console 2024-03-25 22:55:48 +01:00
d16d3c1385 test 2024-03-25 22:52:45 +01:00
3946cf6b34 Merge branch '1.x' into console 2024-03-25 22:20:39 +01:00
165212fed2 add stop button 2024-03-25 22:20:21 +01:00
f6b36dfefc Update CONTRIBUTING.md 2024-03-25 22:19:30 +01:00
33594f2dba headless console 2024-03-24 21:58:48 +01:00
f68d6c7ca2 Merge pull request #133 from vitodeploy/fix-php-ini-update
fix php ini update bug
2024-03-24 15:19:06 +01:00
d504588f95 fix php ini update bug 2024-03-24 15:16:49 +01:00
a0af4e3e9d Merge pull request #128 from vitodeploy/1.x
Merge
2024-03-24 10:07:20 +01:00
ca0e33be2f Merge branch 'main' into 1.x 2024-03-24 09:58:50 +01:00
4d051330d6 Merge (#127) 2024-03-24 09:56:34 +01:00
ab2d6f64f3 Merge branch 'main' into 1.x 2024-03-24 09:56:07 +01:00
d9a56f95dd minify ace.js 2024-03-24 09:01:18 +01:00
7fc7944354 fix style 2024-03-24 00:09:58 +01:00
9ef7bf1bc9 fix style 2024-03-24 00:07:44 +01:00
35e5e16df9 add backward compatibility to filesystem 2024-03-23 18:53:28 +01:00
369bf1b965 fix code style 2024-03-23 18:35:03 +01:00
ee2b53f824 github actions 2024-03-23 18:24:07 +01:00
46845fb41f Merge remote-tracking branch 'origin/1.x' into 1.x 2024-03-23 18:22:15 +01:00
c78462b173 fix tests 2024-03-23 18:21:15 +01:00
5f192d69de Update README.md 2024-03-23 18:14:10 +01:00
3afb6b7eb4 sqlite to mysql migrate command 2024-03-23 17:39:51 +01:00
6529c349b1 upgrade fixes 2024-03-23 15:33:36 +01:00
6389e592a4 fix route cache 2024-03-23 14:57:43 +01:00
9f577ef2d0 fix migrations for upgrade 2024-03-23 14:34:02 +01:00
77a09cec48 fix route cache 2024-03-23 14:28:22 +01:00
7b0a71fb57 config 2024-03-23 13:12:09 +01:00
d5a567cc11 Update SECURITY.md 2024-03-23 10:40:00 +01:00
3575a6d469 AGPL-3.0 2024-03-23 10:34:01 +01:00
4137e2003f fix docker 2024-03-22 17:02:18 +01:00
d4f1a2d4da cleanup
fix installation
fix docker
2024-03-22 14:14:47 +01:00
d3aaf2a6fa Redesign (#124)
- redesign the dashboard
- add search bar
- Mobile-friendly design
2024-03-21 15:57:57 +01:00
7949165648 fix custom ssl creation (#122) 2024-03-18 17:17:45 +01:00
3dc38bf56b remove blade-ui-kit/blade-heroicons 2024-03-17 14:37:19 +01:00
4f252caaf3 remove laravel/sanctum 2024-03-17 13:10:26 +01:00
839e7f496c drop dependency to bensampo/laravel-enum (#120) 2024-03-17 12:26:48 +01:00
b07ae470f9 fix bitbucket (#119)
Fix Bitbucket API errors
2024-03-17 11:36:04 +01:00
f0c4fc4812 simplify firewal protocol select confusion 2024-03-16 23:52:27 +01:00
46d320561e simplify firewal protocol select confusion 2024-03-16 23:45:46 +01:00
1333acf1ef fix copy to clipboard bug 2024-03-16 23:42:22 +01:00
77d6914cd9 add enable/disable services 2024-03-16 14:02:28 +01:00
7a6dcb5654 add log viewer for queues 2024-03-16 12:20:32 +01:00
fd93f3dd47 add PHP 8.3 support 2024-03-16 11:08:45 +01:00
d8056441c0 add mariadb 10.4 2024-03-16 10:35:18 +01:00
287c9c2a8a add postgresql (#118) 2024-03-16 01:04:57 +01:00
a406491160 increase test coverage (#117)
70% test coverage
remove socialite
2024-03-15 22:23:45 +01:00
4f12de9586 backup command cleanup 2024-03-14 22:27:13 +01:00
f907bacdd0 refactoring leftovers 2024-03-14 21:18:59 +01:00
428140b931 refactoring (#116)
- refactoring architecture
- fix incomplete ssh logs
- code editor for scripts in the app
- remove Jobs and SSHCommands
2024-03-14 20:03:43 +01:00
cee4a70c3c drop node.js from installation process 2024-03-07 17:17:29 +01:00
60e2dc0807 add npm lint to the actions 2024-03-06 21:48:02 +01:00
9d705592da refactoring - remove unused code 2024-03-06 20:33:33 +01:00
cfc0645002 remove dependencies (#115)
upgrade php
upgrade packages
remove extra dependencies
2024-03-06 20:05:31 +01:00
b2083fc6b2 Migrate to HTMX (#114)
Dropped Livewire
Added HTMX
Added Blade code lint
Drop Mysql and Redis
Migrate to SQLite
2024-03-06 17:02:59 +01:00
5b2c419e91 create sqlite database for tests 2024-02-25 17:25:25 +01:00
470274279b drop mysql and redis and use sqlite for Vito itself to optimize the resources 2024-02-25 17:16:11 +01:00
1317 changed files with 27973 additions and 23246 deletions

View File

@ -2,28 +2,9 @@ APP_NAME=Vito
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true
APP_URL=http://vito.test APP_URL=
LOG_CHANNEL=stack
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=vito
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=null
CACHE_DRIVER=file
FILESYSTEM_DRIVER=local FILESYSTEM_DRIVER=local
QUEUE_CONNECTION=default
SESSION_DRIVER=database
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp MAIL_MAILER=smtp
MAIL_HOST= MAIL_HOST=
@ -31,8 +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}"
SSH_PUBLIC_KEY_NAME=ssh-public.key
SSH_PRIVATE_KEY_NAME=ssh-private.pem

View File

@ -4,26 +4,7 @@ APP_KEY=
APP_DEBUG=false APP_DEBUG=false
APP_URL= APP_URL=
LOG_CHANNEL=stack
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
BROADCAST_DRIVER=null
CACHE_DRIVER=file
FILESYSTEM_DRIVER=local FILESYSTEM_DRIVER=local
QUEUE_CONNECTION=default
SESSION_DRIVER=database
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp MAIL_MAILER=smtp
MAIL_HOST= MAIL_HOST=
@ -31,8 +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}"
SSH_PUBLIC_KEY_NAME=ssh-public.key
SSH_PRIVATE_KEY_NAME=ssh-private.pem

View File

@ -2,28 +2,10 @@ APP_NAME=Vito
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true
APP_URL=http://vito.test APP_URL=
APP_PORT=8000
LOG_CHANNEL=stack
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=vito
DB_USERNAME=sail
DB_PASSWORD=password
BROADCAST_DRIVER=null
CACHE_DRIVER=redis
FILESYSTEM_DRIVER=local FILESYSTEM_DRIVER=local
QUEUE_CONNECTION=default
SESSION_DRIVER=database
SESSION_LIFETIME=120
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp MAIL_MAILER=smtp
MAIL_HOST= MAIL_HOST=
@ -31,15 +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}"
SSH_PUBLIC_KEY_NAME=ssh-public.key
SSH_PRIVATE_KEY_NAME=ssh-private.pem
APP_SERVICE=vito
FORWARD_REDIS_PORT=2060
FORWARD_DB_PORT=2070
APP_PORT=2080
HMR_PORT=2090

View File

@ -1,27 +0,0 @@
APP_NAME=Vito
APP_ENV=local
APP_KEY=base64:d9kZW60V4lFEw2SPn6UiJ0cfi04v80EWP0GZ6kzoxNg=
APP_DEBUG=true
APP_URL=http://localhost:2080
LOG_CHANNEL=stack
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=vito_test
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=null
CACHE_DRIVER=array
FILESYSTEM_DRIVER=local
QUEUE_CONNECTION=database
SESSION_DRIVER=array
SESSION_LIFETIME=120
MAIL_MAILER=array
SSH_PUBLIC_KEY_NAME=ssh-public.key
SSH_PRIVATE_KEY_NAME=ssh-private.pem

View File

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

View File

@ -2,9 +2,9 @@ name: code-style
on: on:
push: push:
branches:
- 1.x
pull_request: pull_request:
schedule:
- cron: '0 0 * * *'
jobs: jobs:
code-style: code-style:
@ -13,7 +13,8 @@ jobs:
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
php: [ 8.1 ] php: [8.2]
node-version: ["20.x"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -31,9 +32,21 @@ jobs:
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-php- ${{ runner.os }}-php-
- name: Install dependencies - name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true' if: steps.composer-cache.outputs.cache-hit != 'true'
run: composer install --prefer-dist --no-progress --no-suggest run: composer install --prefer-dist --no-progress --no-suggest
- name: Run pint - name: Run pint
run: ./vendor/bin/pint --test run: ./vendor/bin/pint --test
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "20.x"
- name: Install NPM Dependencies
run: npm install
- name: Run lint
run: npm run lint

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

View File

@ -2,30 +2,18 @@ name: tests
on: on:
push: push:
branches:
- 1.x
pull_request: pull_request:
schedule:
- cron: '0 0 * * *'
jobs: jobs:
tests: tests:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
services:
mysql:
image: mysql
env:
MYSQL_DATABASE: test_db
MYSQL_USER: user
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: rootpassword
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
php: [ 8.1 ] php: [8.2]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -47,10 +35,8 @@ jobs:
if: steps.composer-cache.outputs.cache-hit != 'true' if: steps.composer-cache.outputs.cache-hit != 'true'
run: composer install --prefer-dist --no-progress --no-suggest run: composer install --prefer-dist --no-progress --no-suggest
- name: Create sqlite database
run: touch storage/database-test.sqlite
- name: Run test suite - name: Run test suite
run: php artisan test run: php artisan test
env:
DB_HOST: 127.0.0.1
DB_DATABASE: test_db
DB_USERNAME: user
DB_PASSWORD: password

4
.gitignore vendored
View File

@ -4,7 +4,11 @@
/public/storage /public/storage
/storage/*.key /storage/*.key
/storage/*.pem /storage/*.pem
/storage/test-key
/storage/test-key.pub
/vendor /vendor
/storage/database.sqlite
/storage/database-test.sqlite
.env .env
.env.backup .env.backup
.env.production .env.production

16
.prettierrc Normal file
View File

@ -0,0 +1,16 @@
{
"plugins": ["prettier-plugin-blade", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": ["*.blade.php"],
"options": {
"parser": "blade",
"printWidth": 120,
"htmlWhitespaceSensitivity": "ignore",
"tabWidth": 4,
"quoteProps": "consistent",
"trailingComma": "none"
}
}
]
}

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,65 +0,0 @@
FROM ubuntu:22.04
WORKDIR /var/www/html
ENV DEBIAN_FRONTEND noninteractive
RUN echo "mysql-server mysql-server/root_password password password" | debconf-set-selections
RUN echo "mysql-server mysql-server/root_password_again password password" | debconf-set-selections
# upgrade
RUN apt clean && apt update && apt update && apt upgrade -y && apt autoremove -y
# requirements
RUN apt install -y software-properties-common curl zip unzip git gcc
# nginx
RUN apt install -y nginx
# php
RUN apt update \
&& apt install -y gnupg gosu curl ca-certificates zip unzip git supervisor libcap2-bin libpng-dev \
python2 dnsutils librsvg2-bin fswatch wget \
&& add-apt-repository ppa:ondrej/php -y \
&& apt update \
&& apt install -y php8.1 php8.1-fpm php8.1-mbstring php8.1-mysql php8.1-mcrypt php8.1-gd php8.1-xml \
php8.1-curl php8.1-gettext php8.1-zip php8.1-bcmath php8.1-soap php8.1-redis
COPY docker/standalone/php.ini /etc/php/8.1/cli/conf.d/99-vito.ini
# composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# mysql
RUN wget -c https://dev.mysql.com/get/mysql-apt-config_0.8.22-1_all.deb \
&& mkdir -p /etc/apt/keyrings \
&& apt clean \
&& apt update \
&& dpkg -i mysql-apt-config_0.8.22-1_all.deb \
&& apt install mysql-server -y
RUN service mysql stop
# app
COPY . /var/www/html
RUN rm -rf /var/www/html/vendor
RUN rm -rf /var/www/html/.env
RUN composer install --no-dev --prefer-dist
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html/storage /var/www/html/bootstrap/cache
# webserver
RUN rm /etc/nginx/sites-available/default
RUN rm /etc/nginx/sites-enabled/default
COPY docker/standalone/nginx.conf /etc/nginx/sites-available/default
RUN ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
# supervisord
COPY docker/standalone/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# start
COPY docker/standalone/start.sh /start.sh
RUN chmod +x /start.sh
EXPOSE 80 3306
CMD ["/start.sh"]

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>
@ -11,6 +11,12 @@ ## About Vito
Vito is a self-hosted web application that helps you manage your servers and deploy your PHP applications into production servers without a hassle. Vito is a self-hosted web application that helps you manage your servers and deploy your PHP applications into production servers without a hassle.
## Quick Start
```sh
bash <(curl -Ls https://raw.githubusercontent.com/vitodeploy/vito/1.x/scripts/install.sh)
```
## Features ## Features
- Provisions and Manages the server - Provisions and Manages the server
@ -30,23 +36,20 @@ ## Useful Links
- [Install via Docker](https://vitodeploy.com/introduction/installation.html#install-via-docker) - [Install via Docker](https://vitodeploy.com/introduction/installation.html#install-via-docker)
- [Feedbacks](https://vitodeploy.featurebase.app) - [Feedbacks](https://vitodeploy.featurebase.app)
- [Roadmap](https://vitodeploy.featurebase.app/roadmap) - [Roadmap](https://vitodeploy.featurebase.app/roadmap)
- [Video Demo](https://youtu.be/rLRHIyEfON8) - [Video Demo](https://youtu.be/AbmUOBDOc28)
- [Discord](https://discord.gg/dcUWA5DV) - [Discord](https://discord.gg/uZeeHZZnm5)
- [Contribution](/CONTRIBUTING.md) - [Contribution](/CONTRIBUTING.md)
- [Security](/SECURITY.md) - [Security](/SECURITY.md)
## Credits ## Credits
- Laravel - Laravel
- Tailwindcss
- Livewire
- Alpinejs
- Vite
- Laravel Enum by BenSampo
- Log Viewer by Arunas Skirius
- PHPSecLib - PHPSecLib
- Laravel Blade Icons - PHPUnit
- Guzzlehttp - Tailwindcss
- Owenvoke for `owenvoke/blade-fontawesome` - Alpinejs
- Axios - HTMX
- Toastr by CodeSeven - Vite
- Prettier
- Postcss
- Flowbite

View File

@ -3,8 +3,9 @@ # Security Policy
## Supported Versions ## Supported Versions
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ----------|
| 0.x | :white_check_mark: | | 0.x | |
| 1.x | ✅ |
## Reporting a Vulnerability ## Reporting a Vulnerability

View File

@ -19,11 +19,14 @@ public function create(Server $server, array $input): void
'server_id' => $server->id, 'server_id' => $server->id,
'user' => $input['user'], 'user' => $input['user'],
'command' => $input['command'], 'command' => $input['command'],
'frequency' => $input['frequency'], 'frequency' => $input['frequency'] == 'custom' ? $input['custom'] : $input['frequency'],
'status' => CronjobStatus::CREATING, 'status' => CronjobStatus::CREATING,
]); ]);
$cronJob->save(); $cronJob->save();
$cronJob->addToServer();
$server->cron()->update($cronJob->user, CronJob::crontab($server, $cronJob->user));
$cronJob->status = CronjobStatus::READY;
$cronJob->save();
} }
/** /**
@ -40,9 +43,18 @@ private function validate(array $input): void
'in:root,'.config('core.ssh_user'), 'in:root,'.config('core.ssh_user'),
], ],
'frequency' => [ 'frequency' => [
'required',
new CronRule(acceptCustom: true),
],
])->validate();
if ($input['frequency'] == 'custom') {
Validator::make($input, [
'custom' => [
'required', 'required',
new CronRule(), new CronRule(),
], ],
])->validateWithBag('createCronJob'); ])->validate();
}
} }
} }

View File

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

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

@ -24,15 +24,15 @@ public function create($type, Server $server, array $input): Backup
$backup = new Backup([ $backup = new Backup([
'type' => $type, 'type' => $type,
'server_id' => $server->id, 'server_id' => $server->id,
'database_id' => $input['database'] ?? null, 'database_id' => $input['backup_database'] ?? null,
'storage_id' => $input['storage'], 'storage_id' => $input['backup_storage'],
'interval' => $input['interval'] == 'custom' ? $input['custom'] : $input['interval'], 'interval' => $input['backup_interval'] == 'custom' ? $input['backup_custom'] : $input['backup_interval'],
'keep_backups' => $input['keep'], 'keep_backups' => $input['backup_keep'],
'status' => BackupStatus::RUNNING, 'status' => BackupStatus::RUNNING,
]); ]);
$backup->save(); $backup->save();
$backup->run(); app(RunBackup::class)->run($backup);
return $backup; return $backup;
} }
@ -43,16 +43,16 @@ public function create($type, Server $server, array $input): Backup
private function validate($type, Server $server, array $input): void private function validate($type, Server $server, array $input): void
{ {
$rules = [ $rules = [
'storage' => [ 'backup_storage' => [
'required', 'required',
Rule::exists('storage_providers', 'id'), Rule::exists('storage_providers', 'id'),
], ],
'keep' => [ 'backup_keep' => [
'required', 'required',
'numeric', 'numeric',
'min:1', 'min:1',
], ],
'interval' => [ 'backup_interval' => [
'required', 'required',
Rule::in([ Rule::in([
'0 * * * *', '0 * * * *',
@ -63,13 +63,13 @@ private function validate($type, Server $server, array $input): void
]), ]),
], ],
]; ];
if ($input['interval'] == 'custom') { if ($input['backup_interval'] == 'custom') {
$rules['custom'] = [ $rules['backup_custom'] = [
'required', 'required',
]; ];
} }
if ($type === 'database') { if ($type === 'database') {
$rules['database'] = [ $rules['backup_database'] = [
'required', 'required',
Rule::exists('databases', 'id') Rule::exists('databases', 'id')
->where('server_id', $server->id) ->where('server_id', $server->id)

View File

@ -2,6 +2,7 @@
namespace App\Actions\Database; namespace App\Actions\Database;
use App\Enums\DatabaseStatus;
use App\Models\Database; use App\Models\Database;
use App\Models\Server; use App\Models\Server;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
@ -21,8 +22,11 @@ public function create(Server $server, array $input): Database
'server_id' => $server->id, 'server_id' => $server->id,
'name' => $input['name'], 'name' => $input['name'],
]); ]);
/** @var \App\SSH\Services\Database\Database */
$databaseHandler = $server->database()->handler();
$databaseHandler->create($database->name);
$database->status = DatabaseStatus::READY;
$database->save(); $database->save();
$database->createOnServer();
return $database; return $database;
} }

View File

@ -2,6 +2,7 @@
namespace App\Actions\Database; namespace App\Actions\Database;
use App\Enums\DatabaseUserStatus;
use App\Models\DatabaseUser; use App\Models\DatabaseUser;
use App\Models\Server; use App\Models\Server;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
@ -24,8 +25,17 @@ public function create(Server $server, array $input, array $links = []): Databas
'host' => isset($input['remote']) && $input['remote'] ? $input['host'] : 'localhost', 'host' => isset($input['remote']) && $input['remote'] ? $input['host'] : 'localhost',
'databases' => $links, 'databases' => $links,
]); ]);
$server->database()->handler()->createUser(
$databaseUser->username,
$databaseUser->password,
$databaseUser->host
);
$databaseUser->status = DatabaseUserStatus::READY;
$databaseUser->save(); $databaseUser->save();
$databaseUser->createOnServer();
if (count($links) > 0) {
app(LinkUser::class)->link($databaseUser, ['databases' => $links]);
}
return $databaseUser; return $databaseUser;
} }

View File

@ -0,0 +1,15 @@
<?php
namespace App\Actions\Database;
use App\Models\Database;
use App\Models\Server;
class DeleteDatabase
{
public function delete(Server $server, Database $database): void
{
$server->database()->handler()->delete($database->name);
$database->delete();
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Actions\Database;
use App\Models\DatabaseUser;
use App\Models\Server;
class DeleteDatabaseUser
{
public function delete(Server $server, DatabaseUser $databaseUser): void
{
$server->database()->handler()->deleteUser($databaseUser->username, $databaseUser->host);
$databaseUser->delete();
}
}

View File

@ -4,6 +4,9 @@
use App\Models\Database; use App\Models\Database;
use App\Models\DatabaseUser; use App\Models\DatabaseUser;
use App\Models\Server;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
class LinkUser class LinkUser
@ -11,20 +14,49 @@ class LinkUser
/** /**
* @throws ValidationException * @throws ValidationException
*/ */
public function link(DatabaseUser $databaseUser, array $databases): void public function link(DatabaseUser $databaseUser, array $input): void
{ {
$dbs = Database::query() if (! isset($input['databases']) || ! is_array($input['databases'])) {
->where('server_id', $databaseUser->server_id) $input['databases'] = [];
->whereIn('name', $databases)
->count();
if (count($databases) !== $dbs) {
throw ValidationException::withMessages(['databases' => __('Databases not found!')])
->errorBag('linkUser');
} }
$databaseUser->databases = $databases; $this->validate($databaseUser->server, $input);
$databaseUser->unlinkUser();
$databaseUser->linkUser(); $dbs = Database::query()
->where('server_id', $databaseUser->server_id)
->whereIn('name', $input['databases'])
->count();
if (count($input['databases']) !== $dbs) {
throw ValidationException::withMessages(['databases' => __('Databases not found!')]);
}
$databaseUser->databases = $input['databases'];
// Unlink the user from all databases
$databaseUser->server->database()->handler()->unlink(
$databaseUser->username,
$databaseUser->host
);
// Link the user to the selected databases
$databaseUser->server->database()->handler()->link(
$databaseUser->username,
$databaseUser->host,
$databaseUser->databases
);
$databaseUser->save(); $databaseUser->save();
} }
private function validate(Server $server, array $input): void
{
$rules = [
'databases.*' => [
'required',
Rule::exists('databases', 'name')->where('server_id', $server->id),
],
];
Validator::make($input, $rules)->validate();
}
} }

View File

@ -0,0 +1,39 @@
<?php
namespace App\Actions\Database;
use App\Enums\BackupFileStatus;
use App\Models\BackupFile;
use App\Models\Database;
use Illuminate\Support\Facades\Validator;
class RestoreBackup
{
public function restore(BackupFile $backupFile, array $input): void
{
$this->validate($input);
/** @var Database $database */
$database = Database::query()->findOrFail($input['database']);
$backupFile->status = BackupFileStatus::RESTORING;
$backupFile->restored_to = $database->name;
$backupFile->save();
dispatch(function () use ($backupFile, $database) {
$database->server->database()->handler()->restoreBackup($backupFile, $database->name);
$backupFile->status = BackupFileStatus::RESTORED;
$backupFile->restored_at = now();
$backupFile->save();
})->catch(function () use ($backupFile) {
$backupFile->status = BackupFileStatus::RESTORE_FAILED;
$backupFile->save();
})->onConnection('ssh');
}
private function validate(array $input): void
{
Validator::make($input, [
'database' => 'required|exists:databases,id',
])->validate();
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Actions\Database;
use App\Enums\BackupFileStatus;
use App\Models\Backup;
use App\Models\BackupFile;
use Illuminate\Support\Str;
class RunBackup
{
public function run(Backup $backup): BackupFile
{
$file = new BackupFile([
'backup_id' => $backup->id,
'name' => Str::of($backup->database->name)->slug().'-'.now()->format('YmdHis'),
'status' => BackupFileStatus::CREATING,
]);
$file->save();
dispatch(function () use ($file) {
$file->backup->server->database()->handler()->runBackup($file);
$file->status = BackupFileStatus::CREATED;
$file->save();
})->catch(function () use ($file) {
$file->status = BackupFileStatus::FAILED;
$file->save();
})->onConnection('ssh');
return $file;
}
}

View File

@ -21,10 +21,20 @@ public function create(Server $server, array $input): FirewallRule
'port' => $input['port'], 'port' => $input['port'],
'source' => $input['source'], 'source' => $input['source'],
'mask' => $input['mask'] ?? null, 'mask' => $input['mask'] ?? null,
'status' => FirewallRuleStatus::CREATING,
]); ]);
$server->firewall()
->handler()
->addRule(
$rule->type,
$rule->getRealProtocol(),
$rule->port,
$rule->source,
$rule->mask
);
$rule->status = FirewallRuleStatus::READY;
$rule->save(); $rule->save();
$rule->addToServer();
return $rule; return $rule;
} }
@ -56,6 +66,6 @@ private function validate(Server $server, array $input): void
'mask' => [ 'mask' => [
'numeric', 'numeric',
], ],
])->validateWithBag('createRule'); ])->validate();
} }
} }

View File

@ -0,0 +1,28 @@
<?php
namespace App\Actions\FirewallRule;
use App\Enums\FirewallRuleStatus;
use App\Models\FirewallRule;
use App\Models\Server;
class DeleteRule
{
public function delete(Server $server, FirewallRule $rule): void
{
$rule->status = FirewallRuleStatus::DELETING;
$rule->save();
$server->firewall()
->handler()
->removeRule(
$rule->type,
$rule->getRealProtocol(),
$rule->port,
$rule->source,
$rule->mask
);
$rule->delete();
}
}

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

@ -0,0 +1,32 @@
<?php
namespace App\Actions\PHP;
use App\Enums\ServiceStatus;
use App\Models\Server;
use App\SSH\Services\PHP\PHP;
use Illuminate\Validation\ValidationException;
class ChangeDefaultCli
{
public function change(Server $server, array $input): void
{
$this->validate($server, $input);
$service = $server->php($input['version']);
/** @var PHP $handler */
$handler = $service->handler();
$handler->setDefaultCli();
$server->defaultService('php')->update(['is_default' => 0]);
$service->update(['is_default' => 1]);
$service->update(['status' => ServiceStatus::READY]);
}
public function validate(Server $server, array $input): void
{
if (! isset($input['version']) || ! in_array($input['version'], $server->installedPHPVersions())) {
throw ValidationException::withMessages(
['version' => __('This version is not installed')]
);
}
}
}

View File

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

View File

@ -28,7 +28,14 @@ public function install(Server $server, array $input): void
'is_default' => false, 'is_default' => false,
]); ]);
$php->save(); $php->save();
$php->install();
dispatch(function () use ($php) {
$php->handler()->install();
$php->status = ServiceStatus::READY;
$php->save();
})->catch(function () use ($php) {
$php->delete();
})->onConnection('ssh');
} }
/** /**
@ -41,12 +48,12 @@ private function validate(Server $server, array $input): void
'required', 'required',
Rule::in(config('core.php_versions')), Rule::in(config('core.php_versions')),
], ],
])->validateWithBag('installPHP'); ])->validate();
if (in_array($input['version'], $server->installedPHPVersions())) { if (in_array($input['version'], $server->installedPHPVersions())) {
throw ValidationException::withMessages( throw ValidationException::withMessages(
['version' => __('This version is already installed')] ['version' => __('This version is already installed')]
)->errorBag('installPHP'); );
} }
} }
} }

View File

@ -2,25 +2,38 @@
namespace App\Actions\PHP; namespace App\Actions\PHP;
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\ValidationException; use Illuminate\Validation\ValidationException;
class InstallPHPExtension class InstallPHPExtension
{ {
/** public function install(Server $server, array $input): Service
* @throws ValidationException
*/
public function handle(Service $service, array $input): Service
{ {
$this->validate($server, $input);
/** @var Service $service */
$service = $server->php($input['version']);
$typeData = $service->type_data; $typeData = $service->type_data;
$typeData['extensions'] = $typeData['extensions'] ?? []; $typeData['extensions'] = $typeData['extensions'] ?? [];
$typeData['extensions'][] = $input['extension'];
$service->type_data = $typeData; $service->type_data = $typeData;
$service->save(); $service->save();
$this->validate($service, $input); dispatch(function () use ($service, $input) {
/** @var PHP $handler */
$service->handler()->installExtension($input['name']); $handler = $service->handler();
$handler->installExtension($input['extension']);
})->catch(function () use ($service, $input) {
$service->refresh();
$typeData = $service->type_data;
$typeData['extensions'] = array_values(array_diff($typeData['extensions'], [$input['extension']]));
$service->type_data = $typeData;
$service->save();
})->onConnection('ssh');
return $service; return $service;
} }
@ -28,18 +41,25 @@ public function handle(Service $service, array $input): Service
/** /**
* @throws ValidationException * @throws ValidationException
*/ */
private function validate(Service $service, array $input): void private function validate(Server $server, array $input): void
{ {
Validator::make($input, [ Validator::make($input, [
'name' => [ 'extension' => [
'required', 'required',
'in:'.implode(',', config('core.php_extensions')), 'in:'.implode(',', config('core.php_extensions')),
], ],
])->validateWithBag('installPHPExtension'); 'version' => [
'required',
Rule::in($server->installedPHPVersions()),
],
])->validate();
if (in_array($input['name'], $service->type_data['extensions'])) { /** @var Service $service */
$service = $server->php($input['version']);
if (in_array($input['extension'], $service->type_data['extensions'])) {
throw ValidationException::withMessages( throw ValidationException::withMessages(
['name' => __('This extension already installed')] ['extension' => __('This extension already installed')]
)->errorBag('installPHPExtension'); )->errorBag('installPHPExtension');
} }
} }

View File

@ -2,36 +2,48 @@
namespace App\Actions\PHP; namespace App\Actions\PHP;
use App\Enums\ServiceStatus;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
class UninstallPHP class UninstallPHP
{ {
public function uninstall(Server $server, string $version): void public function uninstall(Server $server, array $input): void
{ {
$this->validate($server, $version); $this->validate($server, $input);
/** @var Service $php */ /** @var Service $php */
$php = $server->services()->where('type', 'php')->where('version', $version)->first(); $php = $server->php($input['version']);
$php->status = ServiceStatus::UNINSTALLING;
$php->save();
$php->uninstall(); dispatch(function () use ($php) {
$php->handler()->uninstall();
$php->delete();
})->catch(function () use ($php) {
$php->status = ServiceStatus::FAILED;
$php->save();
})->onConnection('ssh');
} }
/** /**
* @throws ValidationException * @throws ValidationException
*/ */
private function validate(Server $server, string $version): void private function validate(Server $server, array $input): void
{ {
$php = $server->services()->where('type', 'php')->where('version', $version)->first(); Validator::make($input, [
'version' => 'required|string',
])->validate();
if (! $php) { if (! in_array($input['version'], $server->installedPHPVersions())) {
throw ValidationException::withMessages( throw ValidationException::withMessages(
['version' => __('This version has not been installed yet!')] ['version' => __('This version is not installed')]
); );
} }
$hasSite = $server->sites()->where('php_version', $version)->first(); $hasSite = $server->sites()->where('php_version', $input['version'])->first();
if ($hasSite) { if ($hasSite) {
throw ValidationException::withMessages( throw ValidationException::withMessages(
['version' => __('Cannot uninstall this version because some sites are using it!')] ['version' => __('Cannot uninstall this version because some sites are using it!')]

View File

@ -2,9 +2,13 @@
namespace App\Actions\PHP; namespace App\Actions\PHP;
use App\Models\Service; use App\Enums\PHPIniType;
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\Str; use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Throwable; use Throwable;
@ -13,23 +17,27 @@ class UpdatePHPIni
/** /**
* @throws ValidationException * @throws ValidationException
*/ */
public function update(Service $service, string $ini): void public function update(Server $server, array $input): void
{ {
$this->validate($server, $input);
$service = $server->php($input['version']);
$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, $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']]),
]); ]);
} }
@ -42,4 +50,25 @@ private function deleteTempFile(string $name): void
Storage::disk('local')->delete($name); Storage::disk('local')->delete($name);
} }
} }
public function validate(Server $server, array $input): void
{
Validator::make($input, [
'ini' => [
'required',
'string',
],
'version' => 'required|string',
'type' => [
'required',
Rule::in([PHPIniType::CLI, PHPIniType::FPM]),
],
])->validate();
if (! in_array($input['version'], $server->installedPHPVersions())) {
throw ValidationException::withMessages(
['version' => __('This version is not installed')]
);
}
}
} }

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

@ -8,11 +8,8 @@
class DeleteProject class DeleteProject
{ {
public function delete(User $user, int $projectId): void public function delete(User $user, Project $project): void
{ {
/** @var Project $project */
$project = $user->projects()->findOrFail($projectId);
if ($user->projects()->count() === 1) { if ($user->projects()->count() === 1) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'project' => __('Cannot delete the last project.'), 'project' => __('Cannot delete the last project.'),
@ -20,11 +17,15 @@ public function delete(User $user, int $projectId): 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'];
@ -26,7 +30,7 @@ private function validate(Project $project, array $input): void
'required', 'required',
'string', 'string',
'max:255', 'max:255',
Rule::unique('projects')->ignore($project->id), Rule::unique('projects')->where('user_id', $project->user_id)->ignore($project->id),
], ],
])->validate(); ])->validate();
} }

View File

@ -29,7 +29,23 @@ public function create(mixed $queueable, array $input): void
'status' => QueueStatus::CREATING, 'status' => QueueStatus::CREATING,
]); ]);
$queue->save(); $queue->save();
$queue->deploy();
dispatch(function () use ($queue) {
$queue->server->processManager()->handler()->create(
$queue->id,
$queue->command,
$queue->user,
$queue->auto_start,
$queue->auto_restart,
$queue->numprocs,
$queue->getLogFile(),
$queue->site_id
);
$queue->status = QueueStatus::RUNNING;
$queue->save();
})->catch(function () use ($queue) {
$queue->delete();
})->onConnection('ssh');
} }
/** /**
@ -60,6 +76,6 @@ protected function validate(array $input): void
], ],
]; ];
Validator::make($input, $rules)->validateWithBag('createQueue'); Validator::make($input, $rules)->validate();
} }
} }

View File

@ -0,0 +1,14 @@
<?php
namespace App\Actions\Queue;
use App\Models\Queue;
class DeleteQueue
{
public function delete(Queue $queue): void
{
$queue->server->processManager()->handler()->delete($queue->id, $queue->site_id);
$queue->delete();
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Actions\Queue;
use App\Models\Queue;
class GetQueueLogs
{
public function getLogs(Queue $queue): string
{
return $queue->server->processManager()->handler()->getLogs($queue->getLogFile());
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Actions\Queue;
use App\Enums\QueueStatus;
use App\Models\Queue;
class ManageQueue
{
public function start(Queue $queue): void
{
$queue->status = QueueStatus::STARTING;
$queue->save();
dispatch(function () use ($queue) {
$queue->server->processManager()->handler()->start($queue->id, $queue->site_id);
$queue->status = QueueStatus::RUNNING;
$queue->save();
})->onConnection('ssh');
}
public function stop(Queue $queue): void
{
$queue->status = QueueStatus::STOPPING;
$queue->save();
dispatch(function () use ($queue) {
$queue->server->processManager()->handler()->stop($queue->id, $queue->site_id);
$queue->status = QueueStatus::STOPPED;
$queue->save();
})->onConnection('ssh');
}
public function restart(Queue $queue): void
{
$queue->status = QueueStatus::RESTARTING;
$queue->save();
dispatch(function () use ($queue) {
$queue->server->processManager()->handler()->restart($queue->id, $queue->site_id);
$queue->status = QueueStatus::RUNNING;
$queue->save();
})->onConnection('ssh');
}
}

View File

@ -2,8 +2,11 @@
namespace App\Actions\SSL; namespace App\Actions\SSL;
use App\Enums\SslStatus;
use App\Enums\SslType; use App\Enums\SslType;
use App\Models\Site; use App\Models\Site;
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;
@ -17,13 +20,31 @@ public function create(Site $site, array $input): void
{ {
$this->validate($input); $this->validate($input);
if ($input['type'] == SslType::LETSENCRYPT) { $ssl = new Ssl([
$site->createFreeSsl(); 'site_id' => $site->id,
'type' => $input['type'],
'certificate' => $input['certificate'] ?? null,
'pk' => $input['private'] ?? null,
'expires_at' => $input['type'] === SslType::LETSENCRYPT ? now()->addMonths(3) : $input['expires_at'],
'status' => SslStatus::CREATING,
]);
$ssl->domains = [$site->domain];
if (isset($input['aliases']) && $input['aliases']) {
$ssl->domains = array_merge($ssl->domains, $site->aliases);
} }
$ssl->save();
if ($input['type'] == SslType::CUSTOM) { dispatch(function () use ($site, $ssl) {
$site->createCustomSsl($input['certificate'], $input['private']); /** @var Webserver $webserver */
} $webserver = $site->server->webserver()->handler();
$webserver->setupSSL($ssl);
$ssl->status = SslStatus::CREATED;
$ssl->save();
$site->type()->edit();
})->catch(function () use ($ssl) {
$ssl->status = SslStatus::FAILED;
$ssl->save();
})->onConnection('ssh');
} }
/** /**
@ -34,14 +55,15 @@ protected function validate(array $input): void
$rules = [ $rules = [
'type' => [ 'type' => [
'required', 'required',
Rule::in(SslType::getValues()), Rule::in(config('core.ssl_types')),
], ],
]; ];
if (isset($input['type']) && $input['type'] == SslType::CUSTOM) { if (isset($input['type']) && $input['type'] == SslType::CUSTOM) {
$rules['certificate'] = 'required'; $rules['certificate'] = 'required';
$rules['private'] = 'required'; $rules['private'] = 'required';
$rules['expires_at'] = 'required|date_format:Y-m-d|after_or_equal:'.now();
} }
Validator::make($input, $rules)->validateWithBag('createSSL'); Validator::make($input, $rules)->validate();
} }
} }

View File

@ -0,0 +1,14 @@
<?php
namespace App\Actions\SSL;
use App\Models\Ssl;
class DeleteSSL
{
public function delete(Ssl $ssl): void
{
$ssl->site->server->webserver()->handler()->removeSSL($ssl);
$ssl->delete();
}
}

View File

@ -5,19 +5,15 @@
use App\Models\Script; use App\Models\Script;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class CreateScript class CreateScript
{ {
/** public function create(User $user, array $input): Script
* @throws ValidationException
*/
public function handle(User $creator, array $input): Script
{ {
$this->validateInputs($input); $this->validate($input);
$script = new Script([ $script = new Script([
'user_id' => $creator->id, 'user_id' => $user->id,
'name' => $input['name'], 'name' => $input['name'],
'content' => $input['content'], 'content' => $input['content'],
]); ]);
@ -26,16 +22,11 @@ public function handle(User $creator, array $input): Script
return $script; return $script;
} }
/** private function validate(array $input): void
* @throws ValidationException
*/
private function validateInputs(array $input): void
{ {
$rules = [ Validator::make($input, [
'name' => 'required', 'name' => ['required', 'string', 'max:255'],
'content' => 'required', 'content' => ['required', 'string'],
]; ])->validate();
Validator::make($input, $rules)->validateWithBag('createScript');
} }
} }

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

@ -1,17 +0,0 @@
<?php
namespace App\Actions\Script;
use App\Models\User;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class GetScripts
{
public function handle(User $user): LengthAwarePaginator
{
return $user->scripts()
->orderBy('id', 'desc')
->paginate(6)
->onEachSide(1);
}
}

View File

@ -1,37 +0,0 @@
<?php
namespace App\Actions\Script;
use App\Models\Script;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class UpdateScript
{
/**
* @throws ValidationException
*/
public function handle(Script $script, array $input): Script
{
$this->validateInputs($input);
$script->name = $input['name'];
$script->content = $input['content'];
$script->save();
return $script;
}
/**
* @throws ValidationException
*/
private function validateInputs(array $input): void
{
$rules = [
'name' => 'required',
'content' => 'required',
];
Validator::make($input, $rules)->validateWithBag('updateScript');
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Actions\Server;
use App\Enums\ServerStatus;
use App\Facades\Notifier;
use App\Models\Server;
use App\Notifications\ServerDisconnected;
use Throwable;
class CheckConnection
{
public function check(Server $server): Server
{
$status = $server->status;
try {
$server->ssh()->connect();
$server->refresh();
if (in_array($status, [ServerStatus::DISCONNECTED, ServerStatus::UPDATING])) {
$server->status = ServerStatus::READY;
$server->save();
}
} catch (Throwable) {
$server->status = ServerStatus::DISCONNECTED;
$server->save();
Notifier::send($server, new ServerDisconnected($server));
}
return $server;
}
}

View File

@ -3,13 +3,19 @@
namespace App\Actions\Server; namespace App\Actions\Server;
use App\Enums\FirewallRuleStatus; use App\Enums\FirewallRuleStatus;
use App\Enums\ServerProvider;
use App\Enums\ServerStatus;
use App\Exceptions\ServerProviderError; use App\Exceptions\ServerProviderError;
use App\Jobs\Installation\ContinueInstallation; use App\Facades\Notifier;
use App\Models\Server; use App\Models\Server;
use App\Models\User; use App\Models\User;
use App\Notifications\ServerInstallationFailed;
use App\Notifications\ServerInstallationSucceed;
use App\ValidationRules\RestrictedIPAddressesRule; use App\ValidationRules\RestrictedIPAddressesRule;
use Exception; use Exception;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
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\Rule;
@ -30,7 +36,7 @@ public function create(User $creator, array $input): Server
'user_id' => $creator->id, 'user_id' => $creator->id,
'name' => $input['name'], 'name' => $input['name'],
'ssh_user' => config('core.server_providers_default_user')[$input['provider']][$input['os']], 'ssh_user' => config('core.server_providers_default_user')[$input['provider']][$input['os']],
'ip' => $input['ip'], 'ip' => $input['ip'] ?? '',
'port' => $input['port'] ?? 22, 'port' => $input['port'] ?? 22,
'os' => $input['os'], 'os' => $input['os'],
'type' => $input['type'], 'type' => $input['type'],
@ -71,14 +77,8 @@ public function create(User $creator, array $input): Server
$server->type()->createServices($input); $server->type()->createServices($input);
// install server // install server
if ($server->provider == 'custom') { $this->install($server);
$server->install();
} else {
$server->progress_step = __('Installation will begin in 3 minutes!');
$server->save();
dispatch(new ContinueInstallation($server))
->delay(now()->addMinutes(2));
}
DB::commit(); DB::commit();
return $server; return $server;
@ -88,12 +88,44 @@ public function create(User $creator, array $input): Server
if ($e instanceof ServerProviderError) { if ($e instanceof ServerProviderError) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'provider' => __('Provider Error: ').$e->getMessage(), 'provider' => __('Provider Error: ').$e->getMessage(),
])->errorBag('createServer'); ]);
} }
throw $e; throw $e;
} }
} }
private function install(Server $server): void
{
$bus = Bus::chain([
function () use ($server) {
if (! $server->provider()->isRunning()) {
sleep(2);
}
$server->type()->install();
$server->update([
'status' => ServerStatus::READY,
]);
Notifier::send($server, new ServerInstallationSucceed($server));
},
])->catch(function (Throwable $e) use ($server) {
$server->update([
'status' => ServerStatus::INSTALLATION_FAILED,
]);
Notifier::send($server, new ServerInstallationFailed($server));
Log::error('server-installation-error', [
'error' => (string) $e,
]);
});
if ($server->provider != ServerProvider::CUSTOM) {
$server->progress_step = 'waiting-for-provider';
$server->save();
$bus->delay(now()->addMinutes(3));
}
$bus->onConnection('ssh')->dispatch();
}
/** /**
* @throws ValidationException * @throws ValidationException
*/ */
@ -136,7 +168,7 @@ private function validateInputs(array $input): void
*/ */
private function validateType(Server $server, array $input): void private function validateType(Server $server, array $input): void
{ {
Validator::make($input, $server->type()->createValidationRules($input)) Validator::make($input, $server->type()->createRules($input))
->validate(); ->validate();
} }
@ -145,7 +177,7 @@ private function validateType(Server $server, array $input): void
*/ */
private function validateProvider(Server $server, array $input): void private function validateProvider(Server $server, array $input): void
{ {
Validator::make($input, $server->provider()->createValidationRules($input)) Validator::make($input, $server->provider()->createRules($input))
->validate(); ->validate();
} }

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

@ -35,7 +35,7 @@ public function edit(Server $server, array $input): Server
$server->save(); $server->save();
if ($checkConnection) { if ($checkConnection) {
$server->checkConnection(); return $server->checkConnection();
} }
return $server; return $server;

View File

@ -1,14 +0,0 @@
<?php
namespace App\Actions\Server;
use App\Models\Server;
use Illuminate\Database\Eloquent\Collection;
class GetServers
{
public function handle(): Collection
{
return Server::query()->latest()->get();
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Actions\Server;
use App\Enums\ServerStatus;
use App\Models\Server;
use Throwable;
class RebootServer
{
public function reboot(Server $server): Server
{
try {
$server->os()->reboot();
$server->status = ServerStatus::DISCONNECTED;
$server->save();
} catch (Throwable) {
$server = $server->checkConnection();
}
return $server;
}
}

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

@ -2,9 +2,9 @@
namespace App\Actions\ServerProvider; namespace App\Actions\ServerProvider;
use App\Contracts\ServerProvider as ServerProviderContract;
use App\Models\ServerProvider; use App\Models\ServerProvider;
use App\Models\User; use App\Models\User;
use App\ServerProviders\ServerProvider as ServerProviderContract;
use Exception; use Exception;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
@ -28,7 +28,7 @@ public function create(User $user, array $input): ServerProvider
} catch (Exception) { } catch (Exception) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'provider' => [ 'provider' => [
__("Couldn't connect to provider. Please check your credentials and try again later."), sprintf("Couldn't connect to %s. Please check your credentials.", $input['provider']),
], ],
]); ]);
} }
@ -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,21 @@
<?php
namespace App\Actions\ServerProvider;
use App\Models\ServerProvider;
use Exception;
class DeleteServerProvider
{
/**
* @throws Exception
*/
public function delete(ServerProvider $serverProvider): void
{
if ($serverProvider->servers()->exists()) {
throw new Exception('This server provider is being used by a server.');
}
$serverProvider->delete();
}
}

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

@ -1,60 +0,0 @@
<?php
namespace App\Actions\Service;
use App\Enums\ServiceStatus;
use App\Models\Server;
use App\Models\Service;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class InstallPHPMyAdmin
{
/**
* @throws ValidationException
*/
public function install(Server $server, array $input): Service
{
$this->validate($input);
$phpMyAdmin = $server->defaultService('phpmyadmin');
if ($phpMyAdmin) {
throw ValidationException::withMessages([
'allowed_ip' => __('Already installed'),
]);
}
$phpMyAdmin = new Service([
'server_id' => $server->id,
'type' => 'phpmyadmin',
'type_data' => [
'allowed_ip' => $input['allowed_ip'],
'port' => $input['port'],
'php' => $server->defaultService('php')->version,
],
'name' => 'phpmyadmin',
'version' => '5.1.2',
'status' => ServiceStatus::INSTALLING,
'is_default' => 1,
]);
$phpMyAdmin->save();
$phpMyAdmin->install();
return $phpMyAdmin;
}
/**
* @throws ValidationException
*/
private function validate(array $input): void
{
Validator::make($input, [
'allowed_ip' => 'required',
'port' => [
'required',
'numeric',
'min:1',
'max:65535',
],
])->validate();
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace App\Actions\Service;
use App\Enums\ServiceStatus;
use App\Models\Service;
class Manage
{
public function start(Service $service): void
{
$service->status = ServiceStatus::STARTING;
$service->save();
dispatch(function () use ($service) {
$status = $service->server->systemd()->start($service->unit);
if (str($status)->contains('Active: active')) {
$service->status = ServiceStatus::READY;
} else {
$service->status = ServiceStatus::FAILED;
}
$service->save();
})->onConnection('ssh');
}
public function stop(Service $service): void
{
$service->status = ServiceStatus::STOPPING;
$service->save();
dispatch(function () use ($service) {
$status = $service->server->systemd()->stop($service->unit);
if (str($status)->contains('Active: inactive')) {
$service->status = ServiceStatus::STOPPED;
} else {
$service->status = ServiceStatus::FAILED;
}
$service->save();
})->onConnection('ssh');
}
public function restart(Service $service): void
{
$service->status = ServiceStatus::RESTARTING;
$service->save();
dispatch(function () use ($service) {
$status = $service->server->systemd()->restart($service->unit);
if (str($status)->contains('Active: active')) {
$service->status = ServiceStatus::READY;
} else {
$service->status = ServiceStatus::FAILED;
}
$service->save();
})->onConnection('ssh');
}
public function enable(Service $service): void
{
$service->status = ServiceStatus::ENABLING;
$service->save();
dispatch(function () use ($service) {
$status = $service->server->systemd()->enable($service->unit);
if (str($status)->contains('Active: active')) {
$service->status = ServiceStatus::READY;
} else {
$service->status = ServiceStatus::FAILED;
}
$service->save();
})->onConnection('ssh');
}
public function disable(Service $service): void
{
$service->status = ServiceStatus::DISABLING;
$service->save();
dispatch(function () use ($service) {
$status = $service->server->systemd()->disable($service->unit);
if (str($status)->contains('Active: inactive')) {
$service->status = ServiceStatus::DISABLED;
} else {
$service->status = ServiceStatus::FAILED;
}
$service->save();
})->onConnection('ssh');
}
}

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

@ -1,30 +0,0 @@
<?php
namespace App\Actions\Site;
use App\Models\Site;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class ChangePHPVersion
{
/**
* @throws ValidationException
*/
public function handle(Site $site, array $input): void
{
$this->validate($site, $input);
$site->changePHPVersion($input['php_version']);
}
/**
* @throws ValidationException
*/
protected function validate(Site $site, array $input): void
{
Validator::make($input, [
'php_version' => 'required|in:'.implode(',', $site->server->installedPHPVersions()),
])->validateWithBag('changePHPVersion');
}
}

View File

@ -1,51 +0,0 @@
<?php
namespace App\Actions\Site;
use App\Models\Redirect;
use App\Models\Site;
use Exception;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class CreateRedirect
{
/**
* @throws Exception
*/
public function handle(Site $site, array $input): void
{
$this->validate($input);
$redirect = new Redirect([
'site_id' => $site->id,
'mode' => $input['mode'],
'from' => $input['from'],
'to' => $input['to'],
]);
$redirect->save();
$redirect->addToServer();
}
/**
* @throws ValidationException
*/
private function validate(array $input): void
{
$rules = [
'mode' => [
'required',
'in:301,302',
],
'from' => [
'required',
],
'to' => [
'required',
'url',
],
];
Validator::make($input, $rules)->validateWithBag('createRedirect');
}
}

View File

@ -3,9 +3,14 @@
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\Models\Server; use App\Models\Server;
use App\Models\Site; use App\Models\Site;
use App\Notifications\SiteInstallationFailed;
use App\Notifications\SiteInstallationSucceed;
use App\ValidationRules\DomainRule; use App\ValidationRules\DomainRule;
use Exception; use Exception;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@ -17,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
{ {
@ -29,12 +33,12 @@ 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->ssh_user.'/'.$input['domain'], 'path' => '/home/'.$server->getSshUser().'/'.$input['domain'],
'status' => SiteStatus::INSTALLING, 'status' => SiteStatus::INSTALLING,
]); ]);
// fields based on type // fields based on the type
$site->fill($site->type()->createFields($input)); $site->fill($site->type()->createFields($input));
// check has access to repository // check has access to repository
@ -44,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',
]); ]);
} }
@ -63,8 +75,19 @@ public function create(Server $server, array $input): Site
'content' => '', 'content' => '',
]); ]);
// install server // install site
$site->install(); dispatch(function () use ($site) {
$site->type()->install();
$site->update([
'status' => SiteStatus::READY,
'progress' => 100,
]);
Notifier::send($site, new SiteInstallationSucceed($site));
})->catch(function () use ($site) {
$site->status = SiteStatus::INSTALLATION_FAILED;
$site->save();
Notifier::send($site, new SiteInstallationFailed($site));
})->onConnection('ssh');
DB::commit(); DB::commit();
@ -92,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(),
], ],
]; ];
@ -105,7 +128,7 @@ private function validateInputs(Server $server, array $input): void
*/ */
private function validateType(Site $site, array $input): void private function validateType(Site $site, array $input): void
{ {
$rules = $site->type()->createValidationRules($input); $rules = $site->type()->createRules($input);
Validator::make($input, $rules)->validate(); Validator::make($input, $rules)->validate();
} }

View File

@ -0,0 +1,14 @@
<?php
namespace App\Actions\Site;
use App\Models\Site;
class DeleteSite
{
public function delete(Site $site): void
{
$site->server->webserver()->handler()->deleteSite($site);
$site->delete();
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Actions\Site;
use App\Enums\DeploymentStatus;
use App\Exceptions\DeploymentScriptIsEmptyException;
use App\Exceptions\SourceControlIsNotConnected;
use App\Models\Deployment;
use App\Models\ServerLog;
use App\Models\Site;
class Deploy
{
/**
* @throws SourceControlIsNotConnected
* @throws DeploymentScriptIsEmptyException
*/
public function run(Site $site): Deployment
{
if ($site->sourceControl()) {
$site->sourceControl()->getRepo($site->repository);
}
if (! $site->deploymentScript?->content) {
throw new DeploymentScriptIsEmptyException();
}
$deployment = new Deployment([
'site_id' => $site->id,
'deployment_script_id' => $site->deploymentScript->id,
'status' => DeploymentStatus::DEPLOYING,
]);
$lastCommit = $site->sourceControl()?->provider()?->getLastCommit($site->repository, $site->branch);
if ($lastCommit) {
$deployment->commit_id = $lastCommit['commit_id'];
$deployment->commit_data = $lastCommit['commit_data'];
}
$deployment->save();
dispatch(function () use ($site, $deployment) {
/** @var ServerLog $log */
$log = ServerLog::make($site->server, 'deploy-'.strtotime('now'))
->forSite($site);
$log->save();
$deployment->log_id = $log->id;
$deployment->save();
$site->server->os()->runScript(
path: $site->path,
script: $site->deploymentScript->content,
serverLog: $log,
variables: $site->environmentVariables($deployment)
);
$deployment->status = DeploymentStatus::FINISHED;
$deployment->save();
})->catch(function () use ($deployment) {
$deployment->status = DeploymentStatus::FAILED;
$deployment->save();
})->onConnection('ssh');
return $deployment;
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace App\Actions\Site;
use App\Models\Site;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class EditSite
{
/**
* @throws ValidationException
*/
public function handle(Site $site, array $input): Site
{
// validate type
$this->validateType($site, $input);
// set type data
$site->type_data = $site->type()->data($input);
// save
$site->port = $input['port'] ?? null;
$site->save();
// edit
$site->type()->edit();
return $site;
}
/**
* @throws ValidationException
*/
private function validateType(Site $site, array $input): void
{
$rules = $site->type()->editValidationRules($input);
Validator::make($input, $rules)->validateWithBag('editSite');
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace App\Actions\Site;
use App\Models\Server;
use Illuminate\Database\Eloquent\Collection;
class GetSites
{
public function handle(Server $server): Collection
{
return $server->sites()->orderBy('id', 'desc')->get();
}
}

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

@ -3,6 +3,7 @@
namespace App\Actions\Site; namespace App\Actions\Site;
use App\Models\Site; use App\Models\Site;
use App\SSH\Git\Git;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
@ -14,8 +15,9 @@ class UpdateBranch
public function update(Site $site, array $input): void public function update(Site $site, array $input): void
{ {
$this->validate($input); $this->validate($input);
$site->branch = $input['branch'];
$site->updateBranch($input['branch']); app(Git::class)->checkout($site);
$site->save();
} }
/** /**
@ -25,6 +27,6 @@ protected function validate(array $input): void
{ {
Validator::make($input, [ Validator::make($input, [
'branch' => 'required', 'branch' => 'required',
])->validateWithBag('updateBranch'); ]);
} }
} }

View File

@ -27,6 +27,6 @@ protected function validate(array $input): void
{ {
Validator::make($input, [ Validator::make($input, [
'script' => 'required', 'script' => 'required',
])->validateWithBag('updateDeploymentScript'); ]);
} }
} }

View File

@ -2,17 +2,19 @@
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
{ {
$typeData = $site->type_data; $site->server->os()->editFile(
$typeData['env'] = $input['env']; $site->path.'/.env',
$site->type_data = $typeData; $input['env']
$site->save(); );
$site->deployEnv();
} }
} }

28
app/Actions/Site/UpdateSourceControl.php Executable file → Normal file
View File

@ -2,6 +2,9 @@
namespace App\Actions\Site; namespace App\Actions\Site;
use App\Exceptions\RepositoryNotFound;
use App\Exceptions\RepositoryPermissionDenied;
use App\Exceptions\SourceControlIsNotConnected;
use App\Models\Site; use App\Models\Site;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
@ -9,21 +12,32 @@
class UpdateSourceControl class UpdateSourceControl
{ {
/**
* @throws ValidationException
*/
public function update(Site $site, array $input): void public function update(Site $site, array $input): void
{ {
$this->validate($input); $this->validate($input);
$site->source_control_id = $input['source_control']; $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(); $site->save();
} }
/** private function validate(array $input): void
* @throws ValidationException
*/
protected function validate(array $input): void
{ {
Validator::make($input, [ Validator::make($input, [
'source_control' => [ 'source_control' => [

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,16 +11,21 @@
class ConnectSourceControl class ConnectSourceControl
{ {
public function connect(array $input): void public function connect(User $user, array $input): void
{ {
$this->validate($input); $this->validate($input);
$sourceControl = new SourceControl([ $sourceControl = new SourceControl([
'provider' => $input['provider'], 'provider' => $input['provider'],
'profile' => $input['name'], 'profile' => $input['name'],
'access_token' => $input['token'],
'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);
$sourceControl->provider_data = $sourceControl->provider()->createData($input);
if (! $sourceControl->provider()->connect()) { if (! $sourceControl->provider()->connect()) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'token' => __('Cannot connect to :provider or invalid token!', ['provider' => $sourceControl->provider] 'token' => __('Cannot connect to :provider or invalid token!', ['provider' => $sourceControl->provider]
@ -38,20 +44,20 @@ private function validate(array $input): void
$rules = [ $rules = [
'provider' => [ 'provider' => [
'required', 'required',
Rule::in(\App\Enums\SourceControl::getValues()), Rule::in(config('core.source_control_providers')),
], ],
'name' => [ 'name' => [
'required', 'required',
], ],
'token' => [
'required',
],
'url' => [
'nullable',
'url:http,https',
'ends_with:/',
],
]; ];
Validator::make($input, $rules)->validate(); Validator::make($input, $rules)->validate();
} }
/**
* @throws ValidationException
*/
private function validateProvider(SourceControl $sourceControl, array $input): void
{
Validator::make($input, $sourceControl->provider()->createRules($input))->validate();
}
} }

View File

@ -0,0 +1,17 @@
<?php
namespace App\Actions\SourceControl;
use App\Models\SourceControl;
class DeleteSourceControl
{
public function delete(SourceControl $sourceControl): void
{
if ($sourceControl->sites()->exists()) {
throw new \Exception('This source control is being used by a site.');
}
$sourceControl->delete();
}
}

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

@ -0,0 +1,19 @@
<?php
namespace App\Actions\SshKey;
use App\Enums\SshKeyStatus;
use App\Models\Server;
use App\Models\SshKey;
class DeleteKeyFromServer
{
public function delete(Server $server, SshKey $sshKey): void
{
$sshKey->servers()->updateExistingPivot($server->id, [
'status' => SshKeyStatus::DELETING,
]);
$server->os()->deleteSSHKey($sshKey->public_key);
$server->sshKeys()->detach($sshKey);
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Actions\SshKey;
use App\Enums\SshKeyStatus;
use App\Models\Server;
use App\Models\SshKey;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class DeployKeyToServer
{
public function deploy(User $user, Server $server, array $input): void
{
$this->validate($user, $input);
/** @var SshKey $sshKey */
$sshKey = SshKey::findOrFail($input['key_id']);
$server->sshKeys()->attach($sshKey, [
'status' => SshKeyStatus::ADDING,
]);
$server->os()->deploySSHKey($sshKey->public_key);
$sshKey->servers()->updateExistingPivot($server->id, [
'status' => SshKeyStatus::ADDED,
]);
}
private function validate(User $user, array $input): void
{
Validator::make($input, [
'key_id' => [
'required',
Rule::exists('ssh_keys', 'id')->where('user_id', $user->id),
],
])->validate();
}
}

View File

@ -21,17 +21,25 @@ 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());
$storageProvider->credentials = $storageProvider->provider()->credentialData($input); $storageProvider->credentials = $storageProvider->provider()->credentialData($input);
try {
if (! $storageProvider->provider()->connect()) { if (! $storageProvider->provider()->connect()) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'provider' => __("Couldn't connect to the provider"), 'provider' => __("Couldn't connect to the provider"),
]); ]);
} }
} catch (\Throwable $e) {
throw ValidationException::withMessages([
'provider' => $e->getMessage(),
]);
}
$storageProvider->save(); $storageProvider->save();
} }

View File

@ -0,0 +1,21 @@
<?php
namespace App\Actions\StorageProvider;
use App\Models\StorageProvider;
use Exception;
class DeleteStorageProvider
{
/**
* @throws Exception
*/
public function delete(StorageProvider $storageProvider): void
{
if ($storageProvider->backups()->exists()) {
throw new Exception('This storage provider is being used by a backup.');
}
$storageProvider->delete();
}
}

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

@ -0,0 +1,74 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
class MigrateFromMysqlToSqlite extends Command
{
protected $signature = 'migrate-from-mysql-to-sqlite';
protected $description = 'Migrate from Mysql to SQLite';
public function handle(): void
{
$this->info('Migrating from Mysql to SQLite...');
File::exists(storage_path('database.sqlite'))
? File::delete(storage_path('database.sqlite'))
: null;
File::put(storage_path('database.sqlite'), '');
config(['database.default' => 'sqlite']);
$this->call('migrate', ['--force' => true]);
$this->migrateModel(\App\Models\Backup::class);
$this->migrateModel(\App\Models\BackupFile::class);
$this->migrateModel(\App\Models\CronJob::class);
$this->migrateModel(\App\Models\Database::class);
$this->migrateModel(\App\Models\DatabaseUser::class);
$this->migrateModel(\App\Models\Deployment::class);
$this->migrateModel(\App\Models\DeploymentScript::class);
$this->migrateModel(\App\Models\FirewallRule::class);
$this->migrateModel(\App\Models\GitHook::class);
$this->migrateModel(\App\Models\NotificationChannel::class);
$this->migrateModel(\App\Models\Project::class);
$this->migrateModel(\App\Models\Queue::class);
$this->migrateModel(\App\Models\Server::class);
$this->migrateModel(\App\Models\ServerLog::class);
$this->migrateModel(\App\Models\ServerProvider::class);
$this->migrateModel(\App\Models\Service::class);
$this->migrateModel(\App\Models\Site::class);
$this->migrateModel(\App\Models\SourceControl::class);
$this->migrateModel(\App\Models\SshKey::class);
$this->migrateModel(\App\Models\Ssl::class);
$this->migrateModel(\App\Models\StorageProvider::class);
$this->migrateModel(\App\Models\User::class);
$env = File::get(base_path('.env'));
$env = str_replace('DB_CONNECTION=mysql', 'DB_CONNECTION=sqlite', $env);
$env = str_replace('DB_DATABASE=vito', '', $env);
File::put(base_path('.env'), $env);
$this->info('Migrated from Mysql to SQLite');
}
private function migrateModel(string $model): void
{
$this->info("Migrating model: {$model}");
config(['database.default' => 'mysql']);
$rows = $model::where('id', '>', 0)->get();
foreach ($rows as $row) {
DB::connection('sqlite')->table($row->getTable())->insert($row->getAttributes());
}
$this->info("Migrated model: {$model}");
}
}

View File

@ -1,49 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Backup;
use Illuminate\Console\Command;
class RunBackup extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'backups:run {interval}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run backup';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*/
public function handle(): void
{
Backup::query()
->where('interval', $this->argument('interval'))
->where('status', 'running')
->chunk(100, function ($backups) {
/** @var Backup $backup */
foreach ($backups as $backup) {
$backup->run();
}
});
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use App\Actions\Database\RunBackup;
use App\Enums\BackupStatus;
use App\Models\Backup;
use Illuminate\Console\Command;
class RunBackupCommand extends Command
{
protected $signature = 'backups:run {interval}';
protected $description = 'Run backup';
public function handle(): void
{
$total = 0;
Backup::query()
->where('interval', $this->argument('interval'))
->where('status', BackupStatus::RUNNING)
->chunk(100, function ($backups) use (&$total) {
/** @var Backup $backup */
foreach ($backups as $backup) {
app(RunBackup::class)->run($backup);
$total++;
}
});
$this->info("{$total} backups started");
}
}

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