Compare commits

...

198 Commits

Author SHA1 Message Date
Andras Bacsai
4de4587ea6 Merge pull request #1196 from coollabsio/next
version++
2023-09-01 16:09:23 +02:00
Andras Bacsai
8675e1d13f version++ 2023-09-01 16:09:03 +02:00
Andras Bacsai
6ceacc68cc more unique container name 2023-09-01 16:07:46 +02:00
Andras Bacsai
4aacf134b7 Merge pull request #1195 from coollabsio/next
v4.0.0-beta.23
2023-09-01 16:03:28 +02:00
Andras Bacsai
0605772715 better emails 2023-09-01 15:55:55 +02:00
Andras Bacsai
3fa53556f4 better emails 2023-09-01 15:52:18 +02:00
Andras Bacsai
76510b8971 fix: button loading animation 2023-09-01 11:35:40 +02:00
Andras Bacsai
66162966b9 fix: sentry bug 2023-09-01 11:35:33 +02:00
Andras Bacsai
71e1571c39 Add affected users to sentry 2023-09-01 11:20:58 +02:00
Andras Bacsai
806b761e74 Merge pull request #1194 from coollabsio/next
Sentry version update
2023-09-01 10:33:19 +02:00
Andras Bacsai
0176b38958 oops 2023-09-01 10:32:29 +02:00
Andras Bacsai
7a180c7310 Merge pull request #1193 from coollabsio/next
v4.0.0-beta.21
2023-09-01 10:27:49 +02:00
Andras Bacsai
3e1120182c update 2023-09-01 10:21:35 +02:00
Andras Bacsai
8e86ce671c fix: dockerimage jobs are not overlapping 2023-09-01 10:11:00 +02:00
Andras Bacsai
f75a324030 fix: proxy start job 2023-09-01 10:02:24 +02:00
Andras Bacsai
3fabff93f6 fix a few things 2023-09-01 09:34:25 +02:00
Andras Bacsai
e74efc4e76 fix 2023-08-31 21:56:53 +02:00
Andras Bacsai
472ed0753d update 2023-08-31 21:51:05 +02:00
Andras Bacsai
67538ff60c no double slot 2023-08-31 21:46:30 +02:00
Andras Bacsai
ae8bd69106 able to use resend for pro+ users 2023-08-31 15:00:59 +02:00
Andras Bacsai
2538890b52 feat: add resend as transactional emails 2023-08-31 13:10:39 +02:00
Andras Bacsai
87dd819ae4 fix: password confirmation 2023-08-31 09:56:37 +02:00
Andras Bacsai
7ec560d4a2 update 2023-08-30 18:36:06 +02:00
Andras Bacsai
6f9cd6a16b no localhost in cloud 2023-08-30 18:35:20 +02:00
Andras Bacsai
923af88336 fix: subscriptions 2023-08-30 18:23:55 +02:00
Andras Bacsai
5b6667c461 refactor + fixes 2023-08-30 16:01:38 +02:00
Andras Bacsai
6f00740f67 better boarding flow 2023-08-30 14:46:51 +02:00
Andras Bacsai
248863cf16 update boarding process 2023-08-30 11:26:46 +02:00
Andras Bacsai
97d48823dd improve boarding 2023-08-30 11:06:44 +02:00
Andras Bacsai
5eb41e1a15 fix boarding 2023-08-29 20:34:01 +02:00
Andras Bacsai
4a4837d9f5 fix 2023-08-29 20:25:42 +02:00
Andras Bacsai
4ad72fab7b refactor 2023-08-29 16:31:46 +02:00
Andras Bacsai
fe68e45609 refactor 2023-08-29 15:51:30 +02:00
Andras Bacsai
291b9a84ef refactoring 2023-08-29 14:36:17 +02:00
Andras Bacsai
2f9b7b188a ui update 2023-08-29 10:11:18 +02:00
Andras Bacsai
d04d41bc23 update dockercleanupjob 2023-08-29 10:00:29 +02:00
Andras Bacsai
6cb3d7167f fix ui 2023-08-28 23:27:46 +02:00
Andras Bacsai
90b1659a18 fix 2023-08-28 22:25:18 +02:00
Andras Bacsai
1aaf44f9b0 fix 2023-08-28 21:22:53 +02:00
Andras Bacsai
d7cfb84351 fix gh create button 2023-08-28 21:15:30 +02:00
Andras Bacsai
d28cf0b76d fix: webhook endpoint in cloud and no system wide gh app 2023-08-28 21:03:07 +02:00
Andras Bacsai
b4a3236284 fix 2023-08-28 20:52:45 +02:00
Andras Bacsai
556168892d fix job 2023-08-28 20:45:53 +02:00
Andras Bacsai
77667be570 fix: validation 2023-08-28 20:29:44 +02:00
Andras Bacsai
f48a912287 update cloud no localhost server 2023-08-28 18:02:31 +02:00
Andras Bacsai
af30d0831d do not seed in coolify cloud 2023-08-28 15:03:44 +02:00
Andras Bacsai
5989eb8f6e rename license server 2023-08-28 15:02:03 +02:00
Andras Bacsai
2bb778834b remove cloud configs 2023-08-28 14:40:18 +02:00
Andras Bacsai
a5ce191e4d add example cloud 2023-08-28 13:33:20 +02:00
Andras Bacsai
7617756576 remove dynamic sentry version checker 2023-08-28 13:32:36 +02:00
Andras Bacsai
0dfd3a5b0e run scheduled jobs on one server 2023-08-28 11:43:01 +02:00
Andras Bacsai
61a54f48c5 update 2023-08-28 10:44:11 +02:00
Andras Bacsai
5ca0237e34 fix logging on ui 2023-08-27 22:05:37 +02:00
Andras Bacsai
ab1207e461 fix:dockerCleanupjob 2023-08-27 21:36:11 +02:00
Andras Bacsai
75fea4f7c0 disable docker cleanup job 2023-08-27 16:23:02 +02:00
Andras Bacsai
fb34eb5394 new version 2023-08-27 15:49:35 +02:00
Andras Bacsai
41101217c6 Merge pull request #1187 from coollabsio/next
v4.0.0.beta-21
2023-08-27 15:48:33 +02:00
Andras Bacsai
19b1f5004a fix 2023-08-27 15:44:36 +02:00
Andras Bacsai
75fcd88f73 do not show boarding flow yet 2023-08-27 15:28:48 +02:00
Andras Bacsai
c21ce45d70 show public key of generated private key 2023-08-27 15:23:47 +02:00
Andras Bacsai
9f10cb2899 updates 2023-08-27 15:08:53 +02:00
Andras Bacsai
aa0c621223 update ignores 2023-08-27 15:03:53 +02:00
Andras Bacsai
f1dfb9051c delete file 2023-08-27 15:03:26 +02:00
Andras Bacsai
932e58531d fix: fqdn on apps 2023-08-27 14:55:57 +02:00
Andras Bacsai
35a75e1066 disable stopped notification until fixed 2023-08-27 14:53:16 +02:00
Andras Bacsai
522713473d proxy status poll every 10 seconds 2023-08-25 16:11:41 +02:00
Andras Bacsai
3d6e268d15 potential fix: dockercleanuppjob 2023-08-25 13:04:21 +02:00
Andras Bacsai
cf8129dcbb add gh private repo add new gh app 2023-08-25 12:50:52 +02:00
Andras Bacsai
3985cca8cc add pure dockerfile in seeder 2023-08-25 12:40:53 +02:00
Andras Bacsai
86ab65ef66 updates 2023-08-25 12:13:22 +02:00
Andras Bacsai
9db9616a43 fix route subs 2023-08-25 09:53:40 +02:00
Andras Bacsai
39fd6f054b fix 2023-08-24 21:42:47 +02:00
Andras Bacsai
9b5f6ceca8 fix server validated 2023-08-24 21:38:18 +02:00
Andras Bacsai
746da1a76e temporary fix 2023-08-24 21:34:24 +02:00
Andras Bacsai
0a2d0da36f fix proxy check jo 2023-08-24 21:24:44 +02:00
Andras Bacsai
0076455e6e fix proxy job 2023-08-24 21:20:24 +02:00
Andras Bacsai
b674a0ed88 throw errors in jobs 2023-08-24 21:09:58 +02:00
Andras Bacsai
90dba34ecc fix 2023-08-24 21:04:17 +02:00
Andras Bacsai
01b40b26f5 do not send discord message if there is not url 2023-08-24 21:00:19 +02:00
Andras Bacsai
c0805a285e update 2023-08-24 20:58:51 +02:00
Andras Bacsai
ac76870d67 fix dbcontainerstatusjob 2023-08-24 20:52:43 +02:00
Andras Bacsai
7160f50322 update autoupdate job 2023-08-24 20:51:14 +02:00
Andras Bacsai
ba39f2595c update 2023-08-24 20:49:54 +02:00
Andras Bacsai
38688b7065 fix 2023-08-24 17:52:43 +02:00
Andras Bacsai
9ef3218bb5 updates 2023-08-24 17:41:11 +02:00
Andras Bacsai
6f14e127a3 fix things 2023-08-24 17:08:32 +02:00
Andras Bacsai
39890b319a add stripe subscription 2023-08-24 16:14:09 +02:00
Andras Bacsai
2d8f166e4a update a few things 2023-08-23 16:40:59 +02:00
Andras Bacsai
d62af76097 update boarding flow 2023-08-23 10:14:39 +02:00
Andras Bacsai
b39ca51d41 wip: boarding 2023-08-22 17:44:49 +02:00
Andras Bacsai
2414ddd360 remove seeders in dev 2023-08-21 20:19:47 +02:00
Andras Bacsai
bed959f1cd feat: rolling update 2023-08-21 18:00:12 +02:00
Andras Bacsai
a3f3470137 add resend as mailer 2023-08-21 11:11:51 +02:00
Andras Bacsai
b7ec1d7d65 fix: limits & server creation page 2023-08-21 10:18:11 +02:00
Andras Bacsai
7e37068fc0 send notification of autoupdate 2023-08-17 16:26:55 +02:00
Andras Bacsai
d049acad70 update compose for prod 2023-08-17 16:22:49 +02:00
Andras Bacsai
07044680d4 change secrets for bunny sync 2023-08-17 16:20:27 +02:00
Andras Bacsai
847b3fe54f feat: invite by email from waitlist 2023-08-17 16:12:08 +02:00
Andras Bacsai
7e9f0cc07a Merge pull request #1181 from coollabsio/next
v4.0.0-beta.20
2023-08-17 15:40:40 +02:00
Andras Bacsai
392c1650db separate backups on fs & s3 layer 2023-08-17 15:32:40 +02:00
Andras Bacsai
0432c9bef3 separate backups by team as well 2023-08-17 15:30:15 +02:00
Andras Bacsai
e5e10ade72 small fix 2023-08-17 15:24:26 +02:00
Andras Bacsai
ee14d5caf5 fix: add new stuffs to magicbar
feat: add user invitation command
feat: add user_email function
fix: update pricing plans
2023-08-17 15:19:37 +02:00
Andras Bacsai
5a3457c180 on login, update updated_at 2023-08-17 15:18:03 +02:00
Andras Bacsai
c26002426f bugfixes 2023-08-17 13:14:46 +02:00
Andras Bacsai
d48af9cea4 formatter 2023-08-16 17:20:01 +02:00
Andras Bacsai
b34ab8a128 feat: monitor server connection 2023-08-16 17:18:50 +02:00
Andras Bacsai
fd74e07fc8 update pricing link on waitlist 2023-08-16 16:09:08 +02:00
Andras Bacsai
3ab38e69fc feat: send internal notification to discord 2023-08-16 16:03:30 +02:00
Andras Bacsai
d15e1bcc7d change favicon again 2023-08-16 15:47:04 +02:00
Andras Bacsai
701df4b1ad new favicon 2023-08-16 14:45:40 +02:00
Andras Bacsai
7712a9afac remove pricing from waitlist 2023-08-16 12:23:43 +02:00
Andras Bacsai
eb8f760dca update waitlist 2023-08-16 08:15:03 +02:00
Andras Bacsai
9940aa68e7 fix error 2023-08-15 16:45:44 +02:00
Andras Bacsai
878db64878 hmm 2023-08-15 16:28:38 +02:00
Andras Bacsai
56161e8e0d test webhook 2023-08-15 16:25:09 +02:00
Andras Bacsai
9c33689c11 udpate layout 2023-08-15 16:15:30 +02:00
Andras Bacsai
6eff24369b fix 2023-08-15 16:09:40 +02:00
Andras Bacsai
81f7dd9a1d update docker-compose.prod 2023-08-15 16:05:31 +02:00
Andras Bacsai
3d432d025a fix: make coolify-db backups unique dir 2023-08-15 15:39:15 +02:00
Andras Bacsai
61facbb871 update version 2023-08-15 15:17:05 +02:00
Andras Bacsai
aba86dffd7 Merge pull request #1179 from coollabsio/next
v4.0.0-beta.19
2023-08-15 15:15:58 +02:00
Andras Bacsai
316f855280 change proxy path in dev 2023-08-15 15:06:54 +02:00
Andras Bacsai
77dad42f97 update coolify.php 2023-08-15 14:54:20 +02:00
Andras Bacsai
c35c2fe1ee remove a few ray 2023-08-15 14:45:48 +02:00
Andras Bacsai
f8d7c5209e rate limit things 2023-08-15 14:27:45 +02:00
Andras Bacsai
88b3005589 feat: force password reset + waitlist 2023-08-15 14:11:38 +02:00
Andras Bacsai
952d335789 refactor is_instance_admin 2023-08-14 16:56:13 +02:00
Andras Bacsai
f8506f9d20 fix: self-hosted 2023-08-14 15:25:03 +02:00
Andras Bacsai
e4279bf257 feat: cloud 2023-08-14 15:22:29 +02:00
Andras Bacsai
b941f35812 fix: ui + subscription 2023-08-14 14:00:10 +02:00
Andras Bacsai
619d395331 feat: dockerfile build pack 2023-08-11 22:41:47 +02:00
Andras Bacsai
82a01b4483 format muhahaha 2023-08-11 20:48:52 +02:00
Andras Bacsai
c762195c8a format files lol 2023-08-11 20:19:42 +02:00
Andras Bacsai
054bebb081 fix: backup now button 2023-08-11 18:14:58 +02:00
Andras Bacsai
8a93f1fc0c fix: no storage 2023-08-11 17:49:54 +02:00
Andras Bacsai
833e45155d switch to auth()->user from session 2023-08-11 17:31:53 +02:00
Andras Bacsai
e60ec6c47e update migration 2023-08-11 16:39:05 +02:00
Andras Bacsai
61864970c1 Able to backup Coolify itself 2023-08-11 16:13:53 +02:00
Andras Bacsai
b7c9810461 Add coolify-helper image 2023-08-11 16:04:53 +02:00
Andras Bacsai
f097b80c35 refactor 2023-08-11 10:43:44 +02:00
Andras Bacsai
662b191dfd refactor 2023-08-11 10:42:57 +02:00
Andras Bacsai
c26407fa7e fix typo 2023-08-10 21:00:13 +02:00
Andras Bacsai
d973b87f5f feat: add backup notifications 2023-08-10 21:00:02 +02:00
Andras Bacsai
5675ec8c02 fix: DatabaseBackupJob.php 2023-08-10 18:20:12 +02:00
Andras Bacsai
1cafa02ba5 fix small issue 2023-08-10 17:55:03 +02:00
Andras Bacsai
c66f250cd9 Able to remove scheduled backups 2023-08-10 16:28:29 +02:00
Andras Bacsai
fcb2e10097 Able to remove scheduled backups 2023-08-10 16:25:59 +02:00
Andras Bacsai
e17f1935d2 Refactor + package updates + improve local backups 2023-08-10 15:52:54 +02:00
Andras Bacsai
d2a4dbf283 Able to add scheduled backups through the UI 2023-08-09 17:57:27 +02:00
Andras Bacsai
04622a9e3b Refactor db migrations 2023-08-09 16:47:24 +02:00
Andras Bacsai
a3353aac0c fix: postgres_username name to not name, lol 2023-08-09 16:05:51 +02:00
Andras Bacsai
a378b5108e refactor a lot of things
fix: postgres_passwords could be longer
feat: able to define postgresql databases from the ui
2023-08-09 15:57:53 +02:00
Andras Bacsai
d18de24cf9 wip: scheduled backups
fix: file locations vendor unlocking
2023-08-09 14:44:36 +02:00
Andras Bacsai
46909dca85 init: scheduled backups 2023-08-08 17:28:36 +02:00
Andras Bacsai
b4b1c671bd fix: input and textarea 2023-08-08 14:46:23 +02:00
Andras Bacsai
bd9a1dbaf3 Add port mappings for postgresql
Able to add init scripts fro postgresql
2023-08-08 14:35:01 +02:00
Andras Bacsai
f2228cec7b testing php storm code cleanup and styling 2023-08-08 11:51:36 +02:00
Andras Bacsai
a8ee779b31 add env variables + volumes for postgresql 2023-08-07 22:27:20 +02:00
Andras Bacsai
971d7f703d lots of updates + refactoring 2023-08-07 22:14:21 +02:00
Andras Bacsai
bfc20ef219 public database 2023-08-07 19:29:47 +02:00
Andras Bacsai
20e1cd6d6b rename postgres to standalonepostgres 2023-08-07 19:25:32 +02:00
Andras Bacsai
a020bc872d feat: init postgresql database 2023-08-07 18:46:40 +02:00
Andras Bacsai
0a040a0531 fix: empty description 2023-08-07 16:28:07 +02:00
Andras Bacsai
305bf18819 remove do seeder 2023-08-07 15:36:03 +02:00
Andras Bacsai
f6e888ecf9 feat: add s3 storages 2023-08-07 15:31:42 +02:00
Andras Bacsai
2a7e7e978b update builder github workflow 2023-08-07 09:22:24 +02:00
Andras Bacsai
ef7a41f896 update nixpacks versions 2023-08-07 09:21:07 +02:00
Andras Bacsai
b57b4f9bd3 fixes 2023-07-28 21:36:19 +02:00
Andras Bacsai
469e404725 refactor 2023-07-28 16:42:28 +02:00
Andras Bacsai
35d9e98e83 package updates 2023-07-28 14:48:46 +02:00
Andras Bacsai
5b6406d09d fix: proxy UI 2023-07-28 14:44:26 +02:00
Andras Bacsai
6aa6f4c8a2 fix: proxy stop/start ui 2023-07-28 13:40:47 +02:00
Andras Bacsai
0d56d3a555 fix: remove old modal 2023-07-28 13:31:47 +02:00
Andras Bacsai
331b85c57e fix: null fqdn notifications 2023-07-28 12:51:26 +02:00
Andras Bacsai
7f9afa9c46 fix 2023-07-28 12:01:22 +02:00
Andras Bacsai
f3a25939db fix 2023-07-28 11:54:36 +02:00
Andras Bacsai
adc56d5877 fix 2023-07-28 11:49:57 +02:00
Andras Bacsai
766fa85c4c fixes 2023-07-28 11:49:25 +02:00
Andras Bacsai
4a20abcbfa fix: text field 2023-07-28 11:15:30 +02:00
Andras Bacsai
7e4b1a8f8f refactor 2023-07-28 10:55:26 +02:00
Andras Bacsai
e9bd1f88c0 fixes 2023-07-28 09:10:35 +02:00
Andras Bacsai
9c393ff6bc fix: migrate db schema for smtp + discord 2023-07-27 21:26:15 +02:00
Andras Bacsai
d0a85713d2 fix: subscription link 2023-07-27 17:09:01 +02:00
Andras Bacsai
c7fc00095c feat: pricing plans ans subs 2023-07-27 16:22:13 +02:00
Andras Bacsai
97e2a5d30b fix: licensing 2023-07-27 14:45:34 +02:00
Andras Bacsai
e7d019028a rename stopped to status_changes 2023-07-26 15:32:46 +02:00
Andras Bacsai
8d8dcb1a3e fix: notifications 2023-07-26 15:20:04 +02:00
Andras Bacsai
802ef03013 fix: notifications
feat: add app stopped notification
2023-07-26 14:46:28 +02:00
Andras Bacsai
98d057a2ac remove Logs:: 2023-07-26 13:33:48 +02:00
Andras Bacsai
9321cc3de8 fix naming 2023-07-26 13:28:05 +02:00
Andras Bacsai
fd89735521 Added +add flows everywhere
ui: changed vibrant button to not so vibrant
2023-07-26 13:23:47 +02:00
Andras Bacsai
8deeb59d5c fix: readonly input box 2023-07-25 15:23:16 +02:00
Andras Bacsai
5c5e1c8b3c revert version 2023-07-25 15:13:35 +02:00
Andras Bacsai
ef065d480a version++ 2023-07-25 14:49:20 +02:00
Andras Bacsai
6cd29ad7e4 fix: able to change localhost's private key 2023-07-25 14:43:49 +02:00
Andras Bacsai
341f64839b fix: remove buggregator from dev 2023-07-25 14:43:32 +02:00
Andras Bacsai
54df58d44b version++ 2023-07-18 12:50:07 +02:00
493 changed files with 13540 additions and 4524 deletions

View File

@@ -20,3 +20,5 @@ yarn-error.log
/.npm
/.bash_history
/_data
.rnd
/.ssh

View File

@@ -14,3 +14,9 @@ APP_URL=http://localhost
APP_PORT=8000
DUSK_DRIVER_URL=http://selenium:4444
## For Andras only
# To purge cache
BUNNY_API_KEY=
# To upload assets
BUNNY_STORAGE_API_KEY=

View File

@@ -1,6 +0,0 @@
# Secrets related to pushing to GH, Sync files to BunnyCDN etc. Only for maintainers.
# Not related to Coolify, but to how we publish new versions.
GITHUB_TOKEN=
BUNNY_API_KEY=
BUNNY_STORAGE_API_KEY=

View File

@@ -1,15 +1,15 @@
name: Coolify Builder (v4)
name: Coolify Helper Image (v4)
on:
push:
branches: ["main", "v4"]
branches: [ "main", "next" ]
paths:
- .github/workflows/coolify-builder.yml
- docker/coolify-builder/Dockerfile
- .github/workflows/coolify-helper.yml
- docker/coolify-helper/Dockerfile
env:
REGISTRY: ghcr.io
IMAGE_NAME: "coollabsio/coolify-builder"
IMAGE_NAME: "coollabsio/coolify-helper"
jobs:
amd64:
@@ -30,12 +30,12 @@ jobs:
with:
no-cache: true
context: .
file: docker/coolify-builder/Dockerfile
file: docker/coolify-helper/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
aarch64:
runs-on: [self-hosted, arm64]
runs-on: [ self-hosted, arm64 ]
permissions:
contents: read
packages: write
@@ -52,7 +52,7 @@ jobs:
with:
no-cache: true
context: .
file: docker/coolify-builder/Dockerfile
file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:aarch64
@@ -61,7 +61,7 @@ jobs:
permissions:
contents: read
packages: write
needs: [amd64, aarch64]
needs: [ amd64, aarch64 ]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -77,4 +77,4 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

2
.gitignore vendored
View File

@@ -29,3 +29,5 @@ _ide_helper.php
.gitignore
.phpstorm.meta.php
_ide_helper_models.php
.rnd
/.ssh

View File

@@ -15,6 +15,7 @@ class PrepareCoolifyTask
{
protected Activity $activity;
protected CoolifyTaskArgs $remoteProcessArgs;
public function __construct(CoolifyTaskArgs $remoteProcessArgs)
{
$this->remoteProcessArgs = $remoteProcessArgs;

View File

@@ -49,6 +49,28 @@ class RunRemoteProcess
$this->ignore_errors = $ignore_errors;
}
public static function decodeOutput(?Activity $activity = null): string
{
if (is_null($activity)) {
return '';
}
try {
$decoded = json_decode(
data_get($activity, 'description'),
associative: true,
flags: JSON_THROW_ON_ERROR
);
} catch (\JsonException $exception) {
return '';
}
return collect($decoded)
->sortBy(fn ($i) => $i['order'])
->map(fn ($i) => $i['output'])
->implode("");
}
public function __invoke(): ProcessResult
{
$this->time_start = hrtime(true);
@@ -83,15 +105,6 @@ class RunRemoteProcess
return $processResult;
}
protected function getLatestCounter(): int
{
$description = json_decode($this->activity->description, associative: true, flags: JSON_THROW_ON_ERROR);
if ($description === null || count($description) === 0) {
return 1;
}
return end($description)['order'] + 1;
}
protected function getCommand(): string
{
$user = $this->activity->getExtraProperty('user');
@@ -120,6 +133,13 @@ class RunRemoteProcess
}
}
protected function elapsedTime(): int
{
$timeMs = (hrtime(true) - $this->time_start) / 1_000_000;
return intval($timeMs);
}
public function encodeOutput($type, $output)
{
$outputStack = json_decode($this->activity->description, associative: true, flags: JSON_THROW_ON_ERROR);
@@ -135,26 +155,13 @@ class RunRemoteProcess
return json_encode($outputStack, flags: JSON_THROW_ON_ERROR);
}
public static function decodeOutput(?Activity $activity = null): string
protected function getLatestCounter(): int
{
if (is_null($activity)) {
return '';
$description = json_decode($this->activity->description, associative: true, flags: JSON_THROW_ON_ERROR);
if ($description === null || count($description) === 0) {
return 1;
}
try {
$decoded = json_decode(
data_get($activity, 'description'),
associative: true,
flags: JSON_THROW_ON_ERROR
);
} catch (\JsonException $exception) {
return '';
}
return collect($decoded)
->sortBy(fn ($i) => $i['order'])
->map(fn ($i) => $i['output'])
->implode("");
return end($description)['order'] + 1;
}
/**
@@ -171,11 +178,4 @@ class RunRemoteProcess
return ($this->current_time - $this->throttle_interval_ms) > $this->last_write_at;
}
protected function elapsedTime(): int
{
$timeMs = (hrtime(true) - $this->time_start) / 1_000_000;
return intval($timeMs);
}
}

View File

@@ -0,0 +1,164 @@
<?php
namespace App\Actions\Database;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
class StartPostgresql
{
public StandalonePostgresql $database;
public array $commands = [];
public array $init_scripts = [];
public string $configuration_dir;
public function __invoke(Server $server, StandalonePostgresql $database)
{
$this->database = $database;
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"mkdir -p $this->configuration_dir",
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/"
];
$persistent_storages = $this->generate_local_persistent_volumes();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
$this->generate_init_scripts();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
'container_name' => $container_name,
'environment' => $environment_variables,
'restart' => RESTART_MODE,
'networks' => [
$this->database->destination->network,
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'pg_isready',
'-d',
$this->database->postgres_db,
'-U',
$this->database->postgres_user,
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s'
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
'networks' => [
$this->database->destination->network => [
'external' => false,
'name' => $this->database->destination->network,
'attachable' => true,
]
]
];
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (count($this->init_scripts) > 0) {
foreach ($this->init_scripts as $init_script) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $init_script,
'target' => '/docker-entrypoint-initdb.d/' . basename($init_script),
'read_only' => true,
];
}
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
return remote_process($this->commands, $server);
}
private function generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
return $local_persistent_volumes;
}
private function generate_local_persistent_volumes_only_volume_names()
{
$local_persistent_volumes_names = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
private function generate_environment_variables()
{
$environment_variables = collect();
ray('Generate Environment Variables')->green();
ray($this->database->runtime_environment_variables)->green();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_USER'))->isEmpty()) {
$environment_variables->push("POSTGRES_USER={$this->database->postgres_user}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_PASSWORD'))->isEmpty()) {
$environment_variables->push("POSTGRES_PASSWORD={$this->database->postgres_password}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_DB'))->isEmpty()) {
$environment_variables->push("POSTGRES_DB={$this->database->postgres_db}");
}
return $environment_variables->all();
}
private function generate_init_scripts()
{
if (is_null($this->database->init_scripts) || count($this->database->init_scripts) === 0) {
return;
}
foreach ($this->database->init_scripts as $init_script) {
$filename = data_get($init_script, 'filename');
$content = data_get($init_script, 'content');
$content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/docker-entrypoint-initdb.d/{$filename}";
$this->init_scripts[] = "$this->configuration_dir/docker-entrypoint-initdb.d/{$filename}";
}
}
}

View File

@@ -3,11 +3,8 @@
namespace App\Actions\Fortify;
use App\Models\InstanceSettings;
use App\Models\Team;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\CreatesNewUsers;
@@ -19,13 +16,12 @@ class CreateNewUser implements CreatesNewUsers
/**
* Validate and create a newly registered user.
*
* @param array<string, string> $input
* @param array<string, string> $input
*/
public function create(array $input): User
{
$settings = InstanceSettings::get();
if (!$settings->is_registration_enabled) {
Log::info('Registration is disabled');
abort(403);
}
Validator::make($input, [

View File

@@ -14,7 +14,7 @@ class ResetUserPassword implements ResetsUserPasswords
/**
* Validate and reset the user's forgotten password.
*
* @param array<string, string> $input
* @param array<string, string> $input
*/
public function reset(User $user, array $input): void
{

View File

@@ -14,7 +14,7 @@ class UpdateUserPassword implements UpdatesUserPasswords
/**
* Validate and update the user's password.
*
* @param array<string, string> $input
* @param array<string, string> $input
*/
public function update(User $user, array $input): void
{

View File

@@ -13,7 +13,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
/**
* Validate and update the given user's profile information.
*
* @param array<string, string> $input
* @param array<string, string> $input
*/
public function update(User $user, array $input): void
{
@@ -29,8 +29,10 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
],
])->validateWithBag('updateProfileInformation');
if ($input['email'] !== $user->email &&
$user instanceof MustVerifyEmail) {
if (
$input['email'] !== $user->email &&
$user instanceof MustVerifyEmail
) {
$this->updateVerifiedUser($user, $input);
} else {
$user->forceFill([
@@ -43,7 +45,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
/**
* Update the given verified user's profile information.
*
* @param array<string, string> $input
* @param array<string, string> $input
*/
protected function updateVerifiedUser(User $user, array $input): void
{

View File

@@ -4,7 +4,6 @@ namespace App\Actions\License;
use App\Models\InstanceSettings;
use Illuminate\Support\Facades\Http;
use Visus\Cuid2\Cuid2;
class CheckResaleLicense
{
@@ -12,41 +11,52 @@ class CheckResaleLicense
{
try {
$settings = InstanceSettings::get();
$instance_id = config('app.id');
$settings->update([
'is_resale_license_active' => false,
]);
if (isDev()) {
return;
}
if (!$settings->resale_license) {
return;
}
ray('Checking license key');
$base_url = config('coolify.license_url');
if (isDev()) {
$base_url = 'http://host.docker.internal:8787';
}
$instance_id = config('app.id');
ray("Checking license key against $base_url/lemon/validate");
$data = Http::withHeaders([
'Accept' => 'application/json',
])->post('https://api.lemonsqueezy.com/v1/licenses/validate', [
])->get("$base_url/lemon/validate", [
'license_key' => $settings->resale_license,
'instance_name' => $instance_id,
])->throw()->json();
$product_id = (int)data_get($data, 'meta.product_id');
$valid_product_id = (int)config('coolify.lemon_squeezy_product_id');
if ($product_id !== $valid_product_id) {
throw new \Exception('Invalid product id');
}
ray('Valid Product Id');
['valid' => $valid, 'license_key' => $license_key] = $data;
if ($valid) {
if (data_get($license_key, 'status') === 'inactive') {
Http::withHeaders([
'Accept' => 'application/json',
])->post('https://api.lemonsqueezy.com/v1/licenses/activate', [
'license_key' => $settings->resale_license,
'instance_name' => $instance_id,
])->throw()->json();
}
'instance_id' => $instance_id,
])->json();
if (data_get($data, 'valid') === true && data_get($data, 'license_key.status') === 'active') {
ray('Valid & active license key');
$settings->update([
'is_resale_license_active' => true,
]);
return;
}
throw new \Exception('Invalid license key');
$data = Http::withHeaders([
'Accept' => 'application/json',
])->get("$base_url/lemon/activate", [
'license_key' => $settings->resale_license,
'instance_id' => $instance_id,
])->json();
if (data_get($data, 'activated') === true) {
ray('Activated license key');
$settings->update([
'is_resale_license_active' => true,
]);
return;
}
if (data_get($data, 'license_key.status') === 'active') {
throw new \Exception('Invalid license key.');
}
throw new \Exception('Cannot activate license key.');
} catch (\Throwable $th) {
ray($th);
$settings->update([

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Actions\Proxy;
use App\Models\Server;
use Illuminate\Support\Str;
class CheckConfigurationSync
{
public function __invoke(Server $server, bool $reset = false)
{
$proxy_path = get_proxy_path();
$proxy_configuration = instant_remote_process([
"cat $proxy_path/docker-compose.yml",
], $server, false);
if ($reset || is_null($proxy_configuration)) {
$proxy_configuration = Str::of(generate_default_proxy_configuration($server))->trim()->value;
resolve(SaveConfigurationSync::class)($server, $proxy_configuration);
return $proxy_configuration;
}
return $proxy_configuration;
}
}

View File

@@ -1,33 +0,0 @@
<?php
namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Support\Str;
class CheckProxySettingsInSync
{
public function __invoke(Server $server, bool $reset = false)
{
$proxy_path = config('coolify.proxy_config_path');
$output = instant_remote_process([
"cat $proxy_path/docker-compose.yml",
], $server, false);
if (is_null($output) || $reset) {
$final_output = Str::of(getProxyConfiguration($server))->trim()->value;
} else {
$final_output = Str::of($output)->trim()->value;
}
$docker_compose_yml_base64 = base64_encode($final_output);
$server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
if (is_null($output) || $reset) {
instant_remote_process([
"mkdir -p $proxy_path",
"echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml",
], $server);
}
return $final_output;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Actions\Proxy;
use App\Models\Server;
use Illuminate\Support\Str;
class SaveConfigurationSync
{
public function __invoke(Server $server, string $configuration)
{
try {
$proxy_path = get_proxy_path();
$docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
instant_remote_process([
"mkdir -p $proxy_path",
"echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml",
], $server);
} catch (\Throwable $th) {
ray($th);
}
}
}

View File

@@ -5,21 +5,14 @@ namespace App\Actions\Proxy;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Str;
use Spatie\Activitylog\Models\Activity;
class StartProxy
{
public function __invoke(Server $server): Activity
{
// TODO: check for other proxies
if (is_null(data_get($server, 'proxy.type'))) {
$server->proxy->type = ProxyTypes::TRAEFIK_V2->value;
$server->proxy->status = ProxyStatus::EXITED->value;
$server->save();
}
$proxy_path = config('coolify.proxy_config_path');
$proxy_path = get_proxy_path();
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];
})->unique();
@@ -30,29 +23,24 @@ class StartProxy
return "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null 2>&1 || docker network create --attachable $network > /dev/null 2>&1";
});
$configuration = instant_remote_process([
"cat $proxy_path/docker-compose.yml",
], $server, false);
if (is_null($configuration)) {
$configuration = Str::of(getProxyConfiguration($server))->trim()->value;
} else {
$configuration = Str::of($configuration)->trim()->value;
}
$configuration = resolve(CheckConfigurationSync::class)($server);
$docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
$activity = remote_process([
"echo 'Creating required Docker networks...'",
...$create_networks_command,
"mkdir -p $proxy_path",
"cd $proxy_path",
"echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml",
"echo 'Creating Docker Compose file...'",
"echo 'Pulling docker image...'",
'docker compose pull -q',
"echo 'Stopping old proxy...'",
"echo 'Stopping existing proxy...'",
'docker compose down -v --remove-orphans',
"echo 'Starting new proxy...'",
"lsof -nt -i:80 | xargs -r kill -9",
"lsof -nt -i:443 | xargs -r kill -9",
"echo 'Starting proxy...'",
'docker compose up -d --remove-orphans',
"echo 'Proxy installed successfully...'"
], $server);

View File

@@ -12,29 +12,41 @@ class InstallDocker
{
$dockerVersion = '23.0';
$config = base64_encode('{ "live-restore": true }');
$activity = remote_process([
"echo ####### Installing Prerequisites...",
"command -v jq >/dev/null || apt-get update",
"command -v jq >/dev/null || apt install -y jq",
"echo ####### Installing/updating Docker Engine...",
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh",
"echo ####### Configuring Docker Engine (merging existing configuration with the required)...",
"test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json \"/etc/docker/daemon.json.original-`date +\"%Y%m%d-%H%M%S\"`\" || echo '{$config}' | base64 -d > /etc/docker/daemon.json",
"echo '{$config}' | base64 -d > /etc/docker/daemon.json.coolify",
"cat <<< $(jq . /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json.coolify",
"cat <<< $(jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json",
"echo ####### Restarting Docker Engine...",
"systemctl restart docker",
"echo ####### Creating default network...",
"docker network create --attachable coolify",
"echo ####### Done!"
], $server);
StandaloneDocker::create([
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,
'team_id' => $team->id
]);
if (isDev()) {
$activity = remote_process([
"echo ####### Installing Prerequisites...",
"echo ####### Installing/updating Docker Engine...",
"echo ####### Configuring Docker Engine (merging existing configuration with the required)...",
"echo ####### Restarting Docker Engine...",
], $server);
} else {
$activity = remote_process([
"echo ####### Installing Prerequisites...",
"command -v jq >/dev/null || apt-get update",
"command -v jq >/dev/null || apt install -y jq",
"echo ####### Installing/updating Docker Engine...",
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh",
"echo ####### Configuring Docker Engine (merging existing configuration with the required)...",
"test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json \"/etc/docker/daemon.json.original-`date +\"%Y%m%d-%H%M%S\"`\" || echo '{$config}' | base64 -d > /etc/docker/daemon.json",
"echo '{$config}' | base64 -d > /etc/docker/daemon.json.coolify",
"cat <<< $(jq . /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json.coolify",
"cat <<< $(jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json",
"echo ####### Restarting Docker Engine...",
"systemctl restart docker",
"echo ####### Creating default network...",
"docker network create --attachable coolify",
"echo ####### Done!"
], $server);
$found = StandaloneDocker::where('server_id', $server->id);
if ($found->count() == 0) {
StandaloneDocker::create([
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,
]);
}
}
return $activity;
}

View File

@@ -7,9 +7,9 @@ use App\Models\Server;
class UpdateCoolify
{
public Server $server;
public string $latest_version;
public string $current_version;
public ?Server $server = null;
public ?string $latestVersion = null;
public ?string $currentVersion = null;
public function __invoke(bool $force)
{
@@ -17,42 +17,44 @@ class UpdateCoolify
$settings = InstanceSettings::get();
ray('Running InstanceAutoUpdateJob');
$localhost_name = 'localhost';
if (isDev()) {
$localhost_name = 'testing-local-docker-container';
$this->server = Server::where('name', $localhost_name)->first();
if (!$this->server) {
return;
}
$this->server = Server::where('name', $localhost_name)->firstOrFail();
$this->latest_version = get_latest_version_of_coolify();
$this->current_version = config('version');
ray('latest version:' . $this->latest_version . " current version: " . $this->current_version . ' force: ' . $force);
$this->latestVersion = get_latest_version_of_coolify();
$this->currentVersion = config('version');
ray('latest version:' . $this->latestVersion . " current version: " . $this->currentVersion . ' force: ' . $force);
if ($settings->next_channel) {
ray('next channel enabled');
$this->latest_version = 'next';
$this->latestVersion = 'next';
}
if ($force) {
$this->update();
} else {
if (!$settings->is_auto_update_enabled) {
throw new \Exception('Auto update is disabled');
return 'Auto update is disabled';
}
if ($this->latest_version === $this->current_version) {
throw new \Exception('Already on latest version');
if ($this->latestVersion === $this->currentVersion) {
return 'Already on latest version';
}
if (version_compare($this->latest_version, $this->current_version, '<')) {
throw new \Exception('Latest version is lower than current version?!');
if (version_compare($this->latestVersion, $this->currentVersion, '<')) {
return 'Latest version is lower than current version?!';
}
$this->update();
}
return;
} catch (\Exception $e) {
send_internal_notification('InstanceAutoUpdateJob done to version: ' . $this->latestVersion . ' from version: ' . $this->currentVersion);
} catch (\Exception $th) {
ray('InstanceAutoUpdateJob failed');
ray($e->getMessage());
return;
ray($th->getMessage());
send_internal_notification('InstanceAutoUpdateJob failed: ' . $th->getMessage());
throw $th;
}
}
private function update()
{
if (isDev()) {
ray("Running update on local docker container. Updating to $this->latest_version");
ray("Running update on local docker container. Updating to $this->latestVersion");
remote_process([
"sleep 10"
], $this->server);
@@ -62,7 +64,7 @@ class UpdateCoolify
ray('Running update on production server');
remote_process([
"curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh",
"bash /data/coolify/source/upgrade.sh $this->latest_version"
"bash /data/coolify/source/upgrade.sh $this->latestVersion"
], $this->server);
return;
}

View File

@@ -10,10 +10,12 @@ class Init extends Command
{
protected $signature = 'app:init';
protected $description = 'Cleanup instance related stuffs';
public function handle()
{
$this->cleanup_in_progress_application_deployments();
}
private function cleanup_in_progress_application_deployments()
{
// Cleanup any failed deployments

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use App\Models\Waitlist;
use Illuminate\Console\Command;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class InviteFromWaitlist extends Command
{
public Waitlist|null $next_patient = null;
public User|null $new_user = null;
public string|null $password = null;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:invite-from-waitlist {email?}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send invitation to the next user (or by email) in the waitlist';
/**
* Execute the console command.
*/
public function handle()
{
if ($this->argument('email')) {
$this->next_patient = Waitlist::where('email', $this->argument('email'))->first();
if (!$this->next_patient) {
$this->error("{$this->argument('email')} not found in the waitlist.");
return;
}
} else {
$this->next_patient = Waitlist::orderBy('created_at', 'asc')->where('verified', true)->first();
}
if ($this->next_patient) {
$this->register_user();
$this->remove_from_waitlist();
$this->send_email();
} else {
$this->info('No verified user found in the waitlist. 👀');
}
}
private function register_user()
{
$already_registered = User::whereEmail($this->next_patient->email)->first();
if (!$already_registered) {
$this->password = Str::password();
$this->new_user = User::create([
'name' => Str::of($this->next_patient->email)->before('@'),
'email' => $this->next_patient->email,
'password' => Hash::make($this->password),
'force_password_reset' => true,
]);
$this->info("User registered ({$this->next_patient->email}) successfully. 🎉");
} else {
throw new \Exception('User already registered');
}
}
private function remove_from_waitlist()
{
$this->next_patient->delete();
$this->info("User removed from waitlist successfully.");
}
private function send_email()
{
$mail = new MailMessage();
$mail->view('emails.waitlist-invitation', [
'email' => $this->next_patient->email,
'password' => $this->password,
]);
$mail->subject('Congratulations! You are invited to join Coolify Cloud.');
send_user_an_email($mail, $this->next_patient->email);
$this->info("Email sent successfully. 📧");
}
}

View File

@@ -43,15 +43,16 @@ class NotifyDemo extends Command
style('coolify')->color('#9333EA');
style('title-box')->apply('mt-1 px-2 py-1 bg-coolify');
render(<<<'HTML'
render(
<<<'HTML'
<div>
<div class="title-box">
Coolify
</div>
<p class="ml-1 mt-1 ">
<p class="mt-1 ml-1 ">
Demo Notify <strong class="text-coolify">=></strong> Send a demo notification to a given channel.
</p>
<p class="ml-1 mt-1 bg-coolify px-1">
<p class="px-1 mt-1 ml-1 bg-coolify">
php artisan app:demo-notify {channel}
</p>
<div class="my-1">
@@ -64,7 +65,8 @@ class NotifyDemo extends Command
</ul>
</div>
</div>
HTML);
HTML
);
ask(<<<'HTML'
<div class="mr-1">

View File

@@ -4,8 +4,8 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\Pool;
use Illuminate\Support\Facades\Http;
class SyncBunny extends Command
{

View File

@@ -0,0 +1,184 @@
<?php
namespace App\Console\Commands;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup;
use App\Models\StandalonePostgresql;
use App\Models\TeamInvitation;
use App\Models\User;
use App\Notifications\Application\DeploymentFailed;
use App\Notifications\Application\DeploymentSuccess;
use App\Notifications\Application\StatusChanged;
use App\Notifications\Database\BackupFailed;
use App\Notifications\Database\BackupSuccess;
use App\Notifications\Test;
use App\Notifications\TransactionalEmails\InvitationLink;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage;
use Mail;
use Str;
use function Laravel\Prompts\select;
class TestEmail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'email:test';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send a test email to the admin';
/**
* Execute the console command.
*/
private ?MailMessage $mail = null;
public function handle()
{
$email = select(
'Which Email should be sent?',
options: [
'emails-test' => 'Test',
'application-deployment-success' => 'Application - Deployment Success',
'application-deployment-failed' => 'Application - Deployment Failed',
'application-status-changed' => 'Application - Status Changed',
'backup-success' => 'Database - Backup Success',
'backup-failed' => 'Database - Backup Failed',
'invitation-link' => 'Invitation Link',
'waitlist-invitation-link' => 'Waitlist Invitation Link',
'waitlist-confirmation' => 'Waitlist Confirmation',
],
);
$type = set_transanctional_email_settings();
if (!$type) {
throw new Exception('No email settings found.');
}
$this->mail = new MailMessage();
$this->mail->subject("Test Email");
switch ($email) {
case 'emails-test':
$this->mail = (new Test())->toMail();
break;
case 'application-deployment-success':
$application = Application::all()->first();
$this->mail = (new DeploymentSuccess($application, 'test'))->toMail();
$this->sendEmail();
break;
case 'application-deployment-failed':
$application = Application::all()->first();
$preview = ApplicationPreview::all()->first();
if (!$preview) {
$preview = ApplicationPreview::create([
'application_id' => $application->id,
'pull_request_id' => 1,
'pull_request_html_url' => 'http://example.com',
'fqdn' => $application->fqdn,
]);
}
$this->mail = (new DeploymentFailed($application, 'test'))->toMail();
$this->sendEmail();
$this->mail = (new DeploymentFailed($application, 'test', $preview))->toMail();
$this->sendEmail();
break;
case 'application-status-changed':
$application = Application::all()->first();
$this->mail = (new StatusChanged($application))->toMail();
$this->sendEmail();
break;
case 'backup-failed':
$backup = ScheduledDatabaseBackup::all()->first();
$db = StandalonePostgresql::all()->first();
if (!$backup) {
$backup = ScheduledDatabaseBackup::create([
'enabled' => true,
'frequency' => 'daily',
'save_s3' => false,
'database_id' => $db->id,
'database_type' => $db->getMorphClass(),
'team_id' => 0,
]);
}
$output = 'Because of an error, the backup of the database ' . $db->name . ' failed.';
$this->mail = (new BackupFailed($backup, $db, $output))->toMail();
$this->sendEmail();
break;
case 'backup-success':
$backup = ScheduledDatabaseBackup::all()->first();
$db = StandalonePostgresql::all()->first();
if (!$backup) {
$backup = ScheduledDatabaseBackup::create([
'enabled' => true,
'frequency' => 'daily',
'save_s3' => false,
'database_id' => $db->id,
'database_type' => $db->getMorphClass(),
'team_id' => 0,
]);
}
$this->mail = (new BackupSuccess($backup, $db))->toMail();
$this->sendEmail();
break;
case 'invitation-link':
$user = User::all()->first();
$invitation = TeamInvitation::whereEmail($user->email)->first();
if (!$invitation) {
$invitation = TeamInvitation::create([
'uuid' => Str::uuid(),
'email' => $user->email,
'team_id' => 1,
'link' => 'http://example.com',
]);
}
$this->mail = (new InvitationLink($user))->toMail();
$this->sendEmail();
break;
case 'waitlist-invitation-link':
$this->mail = new MailMessage();
$this->mail->view('emails.waitlist-invitation', [
'email' => 'test2@example.com',
'password' => "supersecretpassword",
]);
$this->mail->subject('Congratulations! You are invited to join Coolify Cloud.');
$this->sendEmail();
break;
case 'waitlist-confirmation':
$this->mail = new MailMessage();
$this->mail->view(
'emails.waitlist-confirmation',
[
'confirmation_url' => 'http://example.com',
'cancel_url' => 'http://example.com',
]
);
$this->mail->subject('You are on the waitlist!');
$this->sendEmail();
break;
}
}
private function sendEmail()
{
Mail::send(
[],
[],
fn (Message $message) => $message
->from(
'internal@example.com',
'Test Email',
)
->to('test@example.com')
->subject($this->mail->subject)
->html((string)$this->mail->render())
);
}
}

View File

@@ -3,10 +3,13 @@
namespace App\Console;
use App\Jobs\CheckResaleLicenseJob;
use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob;
use App\Jobs\InstanceAutoUpdateJob;
use App\Jobs\ProxyCheckJob;
use App\Jobs\DockerCleanupJob;
use App\Jobs\CheckResaleLicenseKeys;
use App\Jobs\ResourceStatusJob;
use App\Models\ScheduledDatabaseBackup;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -14,20 +17,50 @@ class Kernel extends ConsoleKernel
{
protected function schedule(Schedule $schedule): void
{
// $schedule->call(fn() => $this->check_scheduled_backups($schedule))->everyTenSeconds();
if (isDev()) {
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new ResourceStatusJob)->everyMinute();
$schedule->job(new ProxyCheckJob)->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute();
// $schedule->job(new CheckResaleLicenseJob)->hourly();
// $schedule->job(new DockerCleanupJob)->everyOddHour();
$schedule->job(new DockerCleanupJob)->everyOddHour();
// $schedule->job(new InstanceAutoUpdateJob(true))->everyMinute();
} else {
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new CheckResaleLicenseJob)->hourly();
$schedule->job(new ProxyCheckJob)->everyFiveMinutes();
$schedule->job(new DockerCleanupJob)->everyTenMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
$schedule->job(new ResourceStatusJob)->everyMinute()->onOneServer();
$schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
$schedule->job(new ProxyCheckJob)->everyFiveMinutes()->onOneServer();
$schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer();
$schedule->job(new InstanceAutoUpdateJob)->everyTenMinutes();
}
$this->check_scheduled_backups($schedule);
}
private function check_scheduled_backups($schedule)
{
ray('check_scheduled_backups');
$scheduled_backups = ScheduledDatabaseBackup::all();
if ($scheduled_backups->isEmpty()) {
ray('no scheduled backups');
return;
}
foreach ($scheduled_backups as $scheduled_backup) {
if (!$scheduled_backup->enabled) {
continue;
}
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
}
$schedule->job(new DatabaseBackupJob(
backup: $scheduled_backup
))->cron($scheduled_backup->frequency);
}
}
protected function commands(): void
{
$this->load(__DIR__ . '/Commands');

View File

@@ -9,7 +9,7 @@ use Spatie\LaravelData\Data;
class ServerMetadata extends Data
{
public function __construct(
public ?ProxyTypes $type,
public ?ProxyTypes $type,
public ?ProxyStatus $status
) {
}

View File

@@ -8,6 +8,7 @@ enum ProxyTypes: string
case NGINX = 'NGINX';
case CADDY = 'CADDY';
}
enum ProxyStatus: string
{
case EXITED = 'exited';

View File

@@ -4,13 +4,13 @@ namespace App\Exceptions;
use App\Models\InstanceSettings;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
use Sentry\Laravel\Integration;
use Sentry\State\Scope;
use Throwable;
class Handler extends ExceptionHandler
{
private InstanceSettings $settings;
/**
* A list of exception types with their corresponding custom log levels.
*
@@ -19,7 +19,6 @@ class Handler extends ExceptionHandler
protected $levels = [
//
];
/**
* A list of the exception types that are not reported.
*
@@ -28,7 +27,6 @@ class Handler extends ExceptionHandler
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed to the session on validation exceptions.
*
@@ -39,6 +37,7 @@ class Handler extends ExceptionHandler
'password',
'password_confirmation',
];
private InstanceSettings $settings;
/**
* Register the exception handling callbacks for the application.
@@ -50,6 +49,11 @@ class Handler extends ExceptionHandler
if ($this->settings->do_not_track || isDev()) {
return;
}
app('sentry')->configureScope(
function (Scope $scope){
$scope->setUser(['id'=> config('sentry.server_name')]);
}
);
Integration::captureUnhandledException($e);
});
}

View File

@@ -5,15 +5,14 @@ namespace App\Http\Controllers;
use App\Models\ApplicationDeploymentQueue;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Request;
use Spatie\Activitylog\Models\Activity;
class ApplicationController extends Controller
{
use AuthorizesRequests, ValidatesRequests;
public function configuration()
{
$project = session('currentTeam')->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
@@ -27,9 +26,10 @@ class ApplicationController extends Controller
}
return view('project.application.configuration', ['application' => $application]);
}
public function deployments()
{
$project = session('currentTeam')->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
@@ -49,7 +49,7 @@ class ApplicationController extends Controller
{
$deploymentUuid = request()->route('deployment_uuid');
$project = session('currentTeam')->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}

View File

@@ -2,16 +2,16 @@
namespace App\Http\Controllers;
use App\Http\Livewire\Team\Invitations;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Server;
use App\Models\S3Storage;
use App\Models\StandalonePostgresql;
use App\Models\TeamInvitation;
use App\Models\User;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\DB;
use Throwable;
class Controller extends BaseController
{
@@ -22,77 +22,88 @@ class Controller extends BaseController
if (!isCloud()) {
abort(404);
}
return view('subscription', [
'settings' => InstanceSettings::get()
return view('subscription.index', [
'settings' => InstanceSettings::get(),
]);
}
public function license()
{
if (!isCloud()) {
abort(404);
}
return view('settings.license', [
'settings' => InstanceSettings::get()
'settings' => InstanceSettings::get(),
]);
}
public function dashboard()
{
$projects = Project::ownedByCurrentTeam()->get();
$servers = Server::ownedByCurrentTeam()->get();
$resources = 0;
foreach ($projects as $project) {
$resources += $project->applications->count();
public function force_passoword_reset() {
return view('auth.force-password-reset');
}
public function boarding() {
if (currentTeam()->boarding || isDev()) {
return view('boarding');
} else {
return redirect()->route('dashboard');
}
return view('dashboard', [
'servers' => $servers->count(),
'projects' => $projects->count(),
'resources' => $resources,
]);
}
public function settings()
{
if (auth()->user()->isInstanceAdmin()) {
if (isInstanceAdmin()) {
$settings = InstanceSettings::get();
$database = StandalonePostgresql::whereName('coolify-db')->first();
if ($database) {
$s3s = S3Storage::whereTeamId(0)->get();
}
return view('settings.configuration', [
'settings' => $settings
]);
} else {
return redirect()->route('dashboard');
}
}
public function emails()
{
if (auth()->user()->isInstanceAdmin()) {
$settings = InstanceSettings::get();
return view('settings.emails', [
'settings' => $settings
'settings' => $settings,
'database' => $database,
's3s' => $s3s ?? [],
]);
} else {
return redirect()->route('dashboard');
}
}
public function team()
{
$invitations = [];
if (auth()->user()->isAdminFromSession()) {
$invitations = TeamInvitation::whereTeamId(auth()->user()->currentTeam()->id)->get();
$invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
}
return view('team.show', [
return view('team.index', [
'invitations' => $invitations,
]);
}
public function storages()
{
$s3 = S3Storage::ownedByCurrentTeam()->get();
return view('team.storages.all', [
's3' => $s3,
]);
}
public function storages_show()
{
$storage = S3Storage::ownedByCurrentTeam()->whereUuid(request()->storage_uuid)->firstOrFail();
return view('team.storages.show', [
'storage' => $storage,
]);
}
public function members()
{
$invitations = [];
if (auth()->user()->isAdminFromSession()) {
$invitations = TeamInvitation::whereTeamId(auth()->user()->currentTeam()->id)->get();
$invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
}
return view('team.members', [
'invitations' => $invitations,
]);
}
public function acceptInvitation()
{
try {
@@ -110,15 +121,16 @@ class Controller extends BaseController
if ($diff <= config('constants.invitation.link.expiration')) {
$user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
$invitation->delete();
return redirect()->route('team.show');
return redirect()->route('team.index');
} else {
$invitation->delete();
abort(401);
}
} catch (\Throwable $th) {
} catch (Throwable $th) {
throw $th;
}
}
public function revokeInvitation()
{
try {
@@ -131,8 +143,8 @@ class Controller extends BaseController
abort(401);
}
$invitation->delete();
return redirect()->route('team.show');
} catch (\Throwable $th) {
return redirect()->route('team.index');
} catch (Throwable $th) {
throw $th;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
class DatabaseController extends Controller
{
use AuthorizesRequests, ValidatesRequests;
public function configuration()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$database = $environment->databases->where('uuid', request()->route('database_uuid'))->first();
if (!$database) {
return redirect()->route('dashboard');
}
return view('project.database.configuration', ['database' => $database]);
}
public function executions()
{
$backup_uuid = request()->route('backup_uuid');
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$database = $environment->databases->where('uuid', request()->route('database_uuid'))->first();
if (!$database) {
return redirect()->route('dashboard');
}
$backup = $database->scheduledBackups->where('uuid', $backup_uuid)->first();
if (!$backup) {
return redirect()->route('dashboard');
}
$executions = collect($backup->executions)->sortByDesc('created_at');
return view('project.database.backups.executions', [
'database' => $database,
'backup' => $backup,
'executions' => $executions,
's3s' => currentTeam()->s3s,
]);
}
public function backups()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$database = $environment->databases->where('uuid', request()->route('database_uuid'))->first();
if (!$database) {
return redirect()->route('dashboard');
}
return view('project.database.backups.all', [
'database' => $database,
's3s' => currentTeam()->s3s,
]);
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Http\Controllers;
use App\Http\Livewire\Server\PrivateKey;
use App\Models\Environment;
use App\Models\Project;
use App\Models\Server;
@@ -16,34 +15,39 @@ class MagicController extends Controller
'servers' => Server::isUsable()->get()
]);
}
public function destinations()
{
return response()->json([
'destinations' => Server::destinationsByServer(request()->query('server_id'))->sortBy('name')
]);
}
public function projects()
{
return response()->json([
'projects' => Project::ownedByCurrentTeam()->get()
]);
}
public function environments()
{
return response()->json([
'environments' => Project::ownedByCurrentTeam()->whereUuid(request()->query('project_uuid'))->first()->environments
]);
}
public function newProject()
{
$project = Project::firstOrCreate(
['name' => request()->query('name') ?? generate_random_name()],
['team_id' => session('currentTeam')->id]
['team_id' => currentTeam()->id]
);
return response()->json([
'project_uuid' => $project->uuid
]);
}
public function newEnvironment()
{
$environment = Environment::firstOrCreate(
@@ -54,6 +58,7 @@ class MagicController extends Controller
'environment_name' => $environment->name,
]);
}
public function newTeam()
{
$team = Team::create(
@@ -63,7 +68,7 @@ class MagicController extends Controller
],
);
auth()->user()->teams()->attach($team, ['role' => 'admin']);
session(['currentTeam' => $team]);
refreshSession();
return redirect(request()->header('Referer'));
}
}

View File

@@ -3,46 +3,49 @@
namespace App\Http\Controllers;
use App\Models\Project;
use App\Models\Server;
class ProjectController extends Controller
{
public function all()
{
$teamId = session('currentTeam')->id;
$projects = Project::where('team_id', $teamId)->get();
return view('projects', ['projects' => $projects]);
return view('projects', [
'projects' => Project::ownedByCurrentTeam()->get(),
'servers' => Server::ownedByCurrentTeam()->count(),
]);
}
public function edit()
{
$projectUuid = request()->route('project_uuid');
$teamId = session('currentTeam')->id;
$teamId = currentTeam()->id;
$project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first();
if (!$project) {
return redirect()->route('dashboard');
}
return view('project.edit', ['project' => $project]);
}
public function show()
{
$projectUuid = request()->route('project_uuid');
$teamId = session('currentTeam')->id;
$teamId = currentTeam()->id;
$project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first();
if (!$project) {
return redirect()->route('dashboard');
}
$project->load(['environments']);
if (count($project->environments) == 1) {
return redirect()->route('project.resources', ['project_uuid' => $project->uuid, 'environment_name' => $project->environments->first()->name]);
}
return view('project.show', ['project' => $project]);
}
public function new()
{
$project = session('currentTeam')->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$type = request()->query('type');
$destination_uuid = request()->query('destination');
$server = requesT()->query('server');
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
@@ -50,16 +53,25 @@ class ProjectController extends Controller
if (!$environment) {
return redirect()->route('dashboard');
}
if (in_array($type, DATABASE_TYPES)) {
$standalone_postgresql = create_standalone_postgresql($environment->id, $destination_uuid);
return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
'database_uuid' => $standalone_postgresql->uuid,
]);
}
if ($server) {
$type = request()->query('type');
}
return view('project.new', [
'type' => $type
]);
}
public function resources()
{
$project = session('currentTeam')->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers;
use App\Models\PrivateKey;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
class ServerController extends Controller
{
use AuthorizesRequests, ValidatesRequests;
public function new_server()
{
$privateKeys = PrivateKey::ownedByCurrentTeam()->get();
if (!isCloud()) {
return view('server.create', [
'limit_reached' => false,
'private_keys' => $privateKeys,
]);
}
$team = currentTeam();
$servers = $team->servers->count();
['serverLimit' => $serverLimit] = $team->limits;
$limit_reached = $servers >= $serverLimit;
return view('server.create', [
'limit_reached' => $limit_reached,
'private_keys' => $privateKeys,
]);
}
}

View File

@@ -37,7 +37,9 @@ class Kernel extends HttpKernel
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\SubscriptionValid::class,
\App\Http\Middleware\CheckForcePasswordReset::class,
\App\Http\Middleware\IsSubscriptionValid::class,
\App\Http\Middleware\IsBoardingFlow::class,
],

View File

@@ -8,19 +8,13 @@ use Spatie\Activitylog\Models\Activity;
class ActivityMonitor extends Component
{
public bool $header = false;
public string|null $header = null;
public $activityId;
public $isPollingActive = false;
protected $activity;
protected $listeners = ['newMonitorActivity'];
public function hydrateActivity()
{
$this->activity = Activity::query()
->find($this->activityId);
}
public function newMonitorActivity($activityId)
{
$this->activityId = $activityId;
@@ -30,6 +24,12 @@ class ActivityMonitor extends Component
$this->isPollingActive = true;
}
public function hydrateActivity()
{
$this->activity = Activity::query()
->find($this->activityId);
}
public function polling()
{
$this->hydrateActivity();
@@ -42,8 +42,10 @@ class ActivityMonitor extends Component
$this->setStatus(ProcessStatus::ERROR);
}
$this->isPollingActive = false;
$this->emit('activityFinished');
}
}
protected function setStatus($status)
{
$this->activity->properties = $this->activity->properties->merge([

View File

@@ -0,0 +1,256 @@
<?php
namespace App\Http\Livewire\Boarding;
use App\Actions\Server\InstallDocker;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use Illuminate\Support\Collection;
use Livewire\Component;
class Index extends Component
{
public string $currentState = 'welcome';
public ?Collection $privateKeys = null;
public ?int $selectedExistingPrivateKey = null;
public ?string $privateKeyType = null;
public ?string $privateKey = null;
public ?string $publicKey = null;
public ?string $privateKeyName = null;
public ?string $privateKeyDescription = null;
public ?PrivateKey $createdPrivateKey = null;
public ?Collection $servers = null;
public ?int $selectedExistingServer = null;
public ?string $remoteServerName = null;
public ?string $remoteServerDescription = null;
public ?string $remoteServerHost = null;
public ?int $remoteServerPort = 22;
public ?string $remoteServerUser = 'root';
public ?Server $createdServer = null;
public Collection|array $projects = [];
public ?int $selectedExistingProject = null;
public ?Project $createdProject = null;
public function mount()
{
$this->privateKeyName = generate_random_name();
$this->remoteServerName = generate_random_name();
if (isDev()) {
$this->privateKey = '-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk
hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA
AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
-----END OPENSSH PRIVATE KEY-----';
$this->privateKeyDescription = 'Created by Coolify';
$this->remoteServerDescription = 'Created by Coolify';
$this->remoteServerHost = 'coolify-testing-host';
}
}
public function welcome() {
if (isCloud()) {
return $this->setServerType('remote');
}
$this->currentState = 'select-server-type';
}
public function restartBoarding()
{
if ($this->createdServer) {
$this->createdServer->delete();
}
if ($this->createdPrivateKey) {
$this->createdPrivateKey->delete();
}
return redirect()->route('boarding');
}
public function skipBoarding()
{
currentTeam()->update([
'show_boarding' => false
]);
refreshSession();
return redirect()->route('dashboard');
}
public function setServerType(string $type)
{
if ($type === 'localhost') {
$this->createdServer = Server::find(0);
if (!$this->createdServer) {
return $this->emit('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
}
return $this->validateServer();
} elseif ($type === 'remote') {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
if ($this->privateKeys->count() > 0) {
$this->selectedExistingPrivateKey = $this->privateKeys->first()->id;
}
$this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
if ($this->servers->count() > 0) {
$this->selectedExistingServer = $this->servers->first()->id;
$this->currentState = 'select-existing-server';
return;
}
$this->currentState = 'private-key';
}
}
public function selectExistingServer()
{
$this->createdServer = Server::find($this->selectedExistingServer);
if (!$this->createdServer) {
$this->emit('error', 'Server is not found.');
$this->currentState = 'private-key';
return;
}
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
$this->validateServer();
$this->getProxyType();
$this->getProjects();
}
public function getProxyType() {
$proxyTypeSet = $this->createdServer->proxy->type;
if (!$proxyTypeSet) {
$this->currentState = 'select-proxy';
return;
}
$this->getProjects();
}
public function selectExistingPrivateKey()
{
$this->currentState = 'create-server';
}
public function createNewServer()
{
$this->selectedExistingServer = null;
$this->currentState = 'private-key';
}
public function setPrivateKey(string $type)
{
$this->selectedExistingPrivateKey = null;
$this->privateKeyType = $type;
if ($type === 'create' && !isDev()) {
$this->createNewPrivateKey();
}
$this->currentState = 'create-private-key';
}
public function savePrivateKey()
{
$this->validate([
'privateKeyName' => 'required',
'privateKey' => 'required',
]);
$this->currentState = 'create-server';
}
public function saveServer()
{
$this->validate([
'remoteServerName' => 'required',
'remoteServerHost' => 'required',
'remoteServerPort' => 'required',
'remoteServerUser' => 'required',
]);
$this->privateKey = formatPrivateKey($this->privateKey);
$this->createdPrivateKey = PrivateKey::create([
'name' => $this->privateKeyName,
'description' => $this->privateKeyDescription,
'private_key' => $this->privateKey,
'team_id' => currentTeam()->id
]);
$this->createdServer = Server::create([
'name' => $this->remoteServerName,
'ip' => $this->remoteServerHost,
'port' => $this->remoteServerPort,
'user' => $this->remoteServerUser,
'description' => $this->remoteServerDescription,
'private_key_id' => $this->createdPrivateKey->id,
'team_id' => currentTeam()->id
]);
$this->validateServer();
}
public function validateServer() {
try {
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->createdServer);
if (!$uptime) {
throw new \Exception('Server is not reachable.');
} else {
$this->createdServer->settings->update([
'is_reachable' => true,
]);
$this->emit('success', 'Server is reachable.');
}
ray($dockerVersion, $uptime);
if (!$dockerVersion) {
$this->emit('error', 'Docker is not installed on the server.');
$this->currentState = 'install-docker';
return;
}
$this->getProxyType();
} catch (\Exception $e) {
return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this);
}
}
public function installDocker()
{
$activity = resolve(InstallDocker::class)($this->createdServer, currentTeam());
$this->emit('newMonitorActivity', $activity->id);
$this->currentState = 'select-proxy';
}
public function selectProxy(string|null $proxyType = null)
{
if (!$proxyType) {
return $this->getProjects();
}
$this->createdServer->proxy->type = $proxyType;
$this->createdServer->proxy->status = 'exited';
$this->createdServer->save();
$this->getProjects();
}
public function getProjects() {
$this->projects = Project::ownedByCurrentTeam(['name'])->get();
if ($this->projects->count() > 0) {
$this->selectedExistingProject = $this->projects->first()->id;
}
$this->currentState = 'create-project';
}
public function selectExistingProject() {
$this->createdProject = Project::find($this->selectedExistingProject);
$this->currentState = 'create-resource';
}
public function createNewProject()
{
$this->createdProject = Project::create([
'name' => "My first project",
'team_id' => currentTeam()->id
]);
$this->currentState = 'create-resource';
}
public function showNewResource()
{
$this->skipBoarding();
return redirect()->route(
'project.resources.new',
[
'project_uuid' => $this->createdProject->uuid,
'environment_name' => 'production',
'server'=> $this->createdServer->id,
]
);
}
private function createNewPrivateKey()
{
$this->privateKeyName = generate_random_name();
$this->privateKeyDescription = 'Created by Coolify';
['private' => $this->privateKey, 'public' => $this->publicKey] = generateSSHKey();
}
public function render()
{
return view('livewire.boarding.index')->layout('layouts.boarding');
}
}

View File

@@ -19,11 +19,13 @@ class CheckLicense extends Component
'instance_id' => 'Instance Id (Do not change this)',
'settings.is_resale_license_active' => 'Is License Active',
];
public function mount()
{
$this->instance_id = config('app.id');
$this->settings = InstanceSettings::get();
}
public function submit()
{
$this->validate();

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Http\Livewire;
use App\Models\Project;
use App\Models\S3Storage;
use App\Models\Server;
use Livewire\Component;
class Dashboard extends Component
{
public int $projects = 0;
public int $servers = 0;
public int $s3s = 0;
public int $resources = 0;
public function mount()
{
$this->servers = Server::ownedByCurrentTeam()->get()->count();
$this->s3s = S3Storage::ownedByCurrentTeam()->get()->count();
$projects = Project::ownedByCurrentTeam()->get();
foreach ($projects as $project) {
$this->resources += $project->applications->count();
$this->resources += $project->postgresqls->count();
}
$this->projects = $projects->count();
}
public function render()
{
return view('livewire.dashboard');
}
}

View File

@@ -18,11 +18,13 @@ class Form extends Component
'destination.network' => 'network',
'destination.server.ip' => 'IP Address',
];
public function submit()
{
$this->validate();
$this->destination->save();
}
public function delete()
{
try {

View File

@@ -5,6 +5,7 @@ namespace App\Http\Livewire\Destination\New;
use App\Models\Server;
use App\Models\StandaloneDocker as ModelsStandaloneDocker;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -27,6 +28,7 @@ class StandaloneDocker extends Component
'network' => 'network',
'server_id' => 'server'
];
public function mount()
{
if (request()->query('server_id')) {
@@ -41,36 +43,42 @@ class StandaloneDocker extends Component
} else {
$this->network = new Cuid2(7);
}
$this->name = generate_random_name();
$this->name = Str::kebab("{$this->servers->first()->name}-{$this->network}");
}
private function createNetworkAndAttachToProxy()
public function generate_name()
{
instant_remote_process(['docker network create --attachable ' . $this->network], $this->server, throwError: false);
instant_remote_process(["docker network connect $this->network coolify-proxy"], $this->server, throwError: false);
$this->server = Server::find($this->server_id);
$this->name = Str::kebab("{$this->server->name}-{$this->network}");
}
public function submit()
{
$this->validate();
try {
$this->server = Server::find($this->server_id);
$found = $this->server->standaloneDockers()->where('network', $this->network)->first();
if ($found) {
$this->createNetworkAndAttachToProxy();
$this->addError('network', 'Network already added to this server.');
$this->emit('error', 'Network already added to this server.');
return;
} else {
$docker = ModelsStandaloneDocker::create([
'name' => $this->name,
'network' => $this->network,
'server_id' => $this->server_id,
'team_id' => session('currentTeam')->id
]);
}
$this->createNetworkAndAttachToProxy();
return redirect()->route('destination.show', $docker->uuid);
} catch (\Exception $e) {
return general_error_handler(err: $e);
return general_error_handler(err: $e, that: $this);
}
}
private function createNetworkAndAttachToProxy()
{
instant_remote_process(['docker network create --attachable ' . $this->network], $this->server, throwError: false);
instant_remote_process(["docker network connect $this->network coolify-proxy"], $this->server, throwError: false);
}
}

View File

@@ -10,6 +10,7 @@ class Show extends Component
{
public Server $server;
public Collection|array $networks = [];
public function scan()
{
$alreadyAddedNetworks = $this->server->standaloneDockers;

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Livewire;
use Illuminate\Support\Facades\Hash;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Livewire\Component;
class ForcePasswordReset extends Component
{
use WithRateLimiting;
public string $email;
public string $password;
public string $password_confirmation;
protected $rules = [
'email' => 'required|email',
'password' => 'required|min:8',
'password_confirmation' => 'required|same:password',
];
public function mount() {
$this->email = auth()->user()->email;
}
public function submit() {
try {
$this->rateLimit(10);
$this->validate();
auth()->user()->forceFill([
'password' => Hash::make($this->password),
'force_password_reset' => false,
])->save();
auth()->logout();
return redirect()->route('login')->with('status', 'Your initial password has been set.');
} catch(\Exception $e) {
return general_error_handler(err:$e, that:$this);
}
}
}

View File

@@ -1,26 +0,0 @@
<?php
namespace App\Http\Livewire;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;
class License extends Component
{
public string $license;
public function submit()
{
ray('checking license');
$this->validate([
'license' => 'required'
]);
// Pretend we're checking the license
// if ($this->license === '123') {
// ray('license is valid');
// Cache::put('license_key', '123');
// return redirect()->to('/');
// } else {
// ray('license is invalid');
// }
}
}

View File

@@ -3,48 +3,54 @@
namespace App\Http\Livewire\Notifications;
use App\Models\Team;
use App\Notifications\Notifications\TestNotification;
use App\Notifications\Test;
use Livewire\Component;
class DiscordSettings extends Component
{
public Team $model;
protected $rules = [
'model.discord.enabled' => 'nullable|boolean',
'model.discord.webhook_url' => 'required|url',
'model.discord_notifications.test' => 'nullable|boolean',
'model.discord_notifications.deployments' => 'nullable|boolean',
'model.discord_enabled' => 'nullable|boolean',
'model.discord_webhook_url' => 'required|url',
'model.discord_notifications_test' => 'nullable|boolean',
'model.discord_notifications_deployments' => 'nullable|boolean',
'model.discord_notifications_status_changes' => 'nullable|boolean',
'model.discord_notifications_database_backups' => 'nullable|boolean',
];
protected $validationAttributes = [
'model.discord.webhook_url' => 'Discord Webhook',
'model.discord_webhook_url' => 'Discord Webhook',
];
public function instantSave()
{
try {
$this->submit();
} catch (\Exception $e) {
$this->model->discord->enabled = false;
ray($e->getMessage());
$this->model->discord_enabled = false;
$this->validate();
}
}
public function saveModel()
{
$this->model->save();
if (is_a($this->model, Team::class)) {
session(['currentTeam' => $this->model]);
}
$this->emit('success', 'Settings saved.');
}
public function submit()
{
$this->resetErrorBag();
$this->validate();
$this->saveModel();
}
public function saveModel()
{
$this->model->save();
if (is_a($this->model, Team::class)) {
refreshSession();
}
$this->emit('success', 'Settings saved.');
}
public function sendTestNotification()
{
$this->model->notify(new TestNotification('discord'));
$this->model->notify(new Test);
$this->emit('success', 'Test notification sent.');
}
}

View File

@@ -4,110 +4,178 @@ namespace App\Http\Livewire\Notifications;
use App\Models\InstanceSettings;
use App\Models\Team;
use App\Notifications\Notifications\TestNotification;
use App\Notifications\Test;
use Livewire\Component;
use Log;
class EmailSettings extends Component
{
public Team $model;
public Team $team;
public string $emails;
public bool $sharedEmailEnabled = false;
protected $rules = [
'model.smtp.enabled' => 'nullable|boolean',
'model.smtp.from_address' => 'required|email',
'model.smtp.from_name' => 'required',
'model.smtp.recipients' => 'nullable',
'model.smtp.host' => 'required',
'model.smtp.port' => 'required',
'model.smtp.encryption' => 'nullable',
'model.smtp.username' => 'nullable',
'model.smtp.password' => 'nullable',
'model.smtp.timeout' => 'nullable',
'model.smtp.test_recipients' => 'nullable',
'model.smtp_notifications.test' => 'nullable|boolean',
'model.smtp_notifications.deployments' => 'nullable|boolean',
'model.discord_notifications.test' => 'nullable|boolean',
'model.discord_notifications.deployments' => 'nullable|boolean',
'team.smtp_enabled' => 'nullable|boolean',
'team.smtp_from_address' => 'required|email',
'team.smtp_from_name' => 'required',
'team.smtp_recipients' => 'nullable',
'team.smtp_host' => 'required',
'team.smtp_port' => 'required',
'team.smtp_encryption' => 'nullable',
'team.smtp_username' => 'nullable',
'team.smtp_password' => 'nullable',
'team.smtp_timeout' => 'nullable',
'team.smtp_notifications_test' => 'nullable|boolean',
'team.smtp_notifications_deployments' => 'nullable|boolean',
'team.smtp_notifications_status_changes' => 'nullable|boolean',
'team.smtp_notifications_database_backups' => 'nullable|boolean',
'team.use_instance_email_settings' => 'boolean',
'team.resend_enabled' => 'nullable|boolean',
'team.resend_api_key' => 'nullable',
];
protected $validationAttributes = [
'model.smtp.from_address' => 'From Address',
'model.smtp.from_name' => 'From Name',
'model.smtp.recipients' => 'Recipients',
'model.smtp.host' => 'Host',
'model.smtp.port' => 'Port',
'model.smtp.encryption' => 'Encryption',
'model.smtp.username' => 'Username',
'model.smtp.password' => 'Password',
'model.smtp.test_recipients' => 'Test Recipients',
'team.smtp_from_address' => 'From Address',
'team.smtp_from_name' => 'From Name',
'team.smtp_recipients' => 'Recipients',
'team.smtp_host' => 'Host',
'team.smtp_port' => 'Port',
'team.smtp_encryption' => 'Encryption',
'team.smtp_username' => 'Username',
'team.smtp_password' => 'Password',
'team.smtp_timeout' => 'Timeout',
'team.resend_enabled' => 'Resend Enabled',
'team.resend_api_key' => 'Resend API Key',
];
private function decrypt()
{
if (data_get($this->model, 'smtp.password')) {
try {
$this->model->smtp->password = decrypt($this->model->smtp->password);
} catch (\Exception $e) {
}
}
}
public function mount()
{
$this->decrypt();
['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits;
$this->emails = auth()->user()->email;
}
public function copyFromInstanceSettings()
public function submitFromFields()
{
$settings = InstanceSettings::get();
if ($settings->smtp->enabled) {
$this->model->smtp->enabled = true;
$this->model->smtp->from_address = $settings->smtp->from_address;
$this->model->smtp->from_name = $settings->smtp->from_name;
$this->model->smtp->recipients = $settings->smtp->recipients;
$this->model->smtp->host = $settings->smtp->host;
$this->model->smtp->port = $settings->smtp->port;
$this->model->smtp->encryption = $settings->smtp->encryption;
$this->model->smtp->username = $settings->smtp->username;
$this->model->smtp->password = $settings->smtp->password;
$this->model->smtp->timeout = $settings->smtp->timeout;
$this->model->smtp->test_recipients = $settings->smtp->test_recipients;
$this->saveModel();
} else {
$this->emit('error', 'Instance SMTP settings are not enabled.');
try {
$this->resetErrorBag();
$this->validate([
'team.smtp_from_address' => 'required|email',
'team.smtp_from_name' => 'required',
]);
$this->team->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
return general_error_handler($e, $this);
}
}
public function submit()
{
$this->resetErrorBag();
$this->validate();
if ($this->model->smtp->password) {
$this->model->smtp->password = encrypt($this->model->smtp->password);
} else {
$this->model->smtp->password = null;
}
$this->model->smtp->recipients = str_replace(' ', '', $this->model->smtp->recipients);
$this->model->smtp->test_recipients = str_replace(' ', '', $this->model->smtp->test_recipients);
$this->saveModel();
}
public function saveModel()
{
$this->model->save();
$this->decrypt();
if (is_a($this->model, Team::class)) {
session(['currentTeam' => $this->model]);
}
$this->emit('success', 'Settings saved.');
}
public function sendTestNotification()
{
$this->model->notify(new TestNotification('smtp'));
$this->emit('success', 'Test notification sent.');
$this->team->notify(new Test($this->emails));
$this->emit('success', 'Test Email sent successfully.');
}
public function instantSaveInstance()
{
try {
if (!$this->sharedEmailEnabled) {
throw new \Exception('Not allowed to change settings. Please upgrade your subscription.');
}
$this->team->smtp_enabled = false;
$this->team->resend_enabled = false;
$this->team->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
return general_error_handler($e, $this);
}
}
public function instantSaveResend()
{
try {
$this->team->smtp_enabled = false;
$this->submitResend();
} catch (\Exception $e) {
$this->team->smtp_enabled = false;
return general_error_handler($e, $this);
}
}
public function instantSave()
{
try {
$this->team->resend_enabled = false;
$this->submit();
} catch (\Exception $e) {
$this->model->smtp->enabled = false;
$this->validate();
$this->team->smtp_enabled = false;
return general_error_handler($e, $this);
}
}
public function submit()
{
try {
$this->resetErrorBag();
$this->validate([
'team.smtp_from_address' => 'required|email',
'team.smtp_from_name' => 'required',
'team.smtp_host' => 'required',
'team.smtp_port' => 'required|numeric',
'team.smtp_encryption' => 'nullable',
'team.smtp_username' => 'nullable',
'team.smtp_password' => 'nullable',
'team.smtp_timeout' => 'nullable',
]);
$this->team->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
$this->team->smtp_enabled = false;
return general_error_handler($e, $this);
}
}
public function submitResend()
{
try {
$this->resetErrorBag();
$this->validate([
'team.resend_api_key' => 'required'
]);
$this->team->save();
refreshSession();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
$this->team->resend_enabled = false;
return general_error_handler($e, $this);
}
}
public function copyFromInstanceSettings()
{
$settings = InstanceSettings::get();
if ($settings->smtp_enabled) {
$team = currentTeam();
$team->update([
'smtp_enabled' => $settings->smtp_enabled,
'smtp_from_address' => $settings->smtp_from_address,
'smtp_from_name' => $settings->smtp_from_name,
'smtp_recipients' => $settings->smtp_recipients,
'smtp_host' => $settings->smtp_host,
'smtp_port' => $settings->smtp_port,
'smtp_encryption' => $settings->smtp_encryption,
'smtp_username' => $settings->smtp_username,
'smtp_password' => $settings->smtp_password,
'smtp_timeout' => $settings->smtp_timeout,
]);
refreshSession();
$this->team = $team;
$this->emit('success', 'Settings saved.');
return;
}
if ($settings->resend_enabled) {
$team = currentTeam();
$team->update([
'resend_enabled' => $settings->resend_enabled,
'resend_api_key' => $settings->resend_api_key,
]);
refreshSession();
$this->team = $team;
$this->emit('success', 'Settings saved.');
return;
}
$this->emit('error', 'Instance SMTP/Resend settings are not enabled.');
}
}

View File

@@ -20,12 +20,13 @@ class Change extends Component
'private_key.description' => 'description',
'private_key.private_key' => 'private key'
];
public function delete()
{
try {
if ($this->private_key->isEmpty()) {
$this->private_key->delete();
session('currentTeam')->privateKeys = PrivateKey::where('team_id', session('currentTeam')->id)->get();
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
return redirect()->route('private-key.all');
}
$this->emit('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.');
@@ -33,15 +34,13 @@ class Change extends Component
return general_error_handler(err: $e, that: $this);
}
}
public function changePrivateKey()
{
try {
$this->private_key->private_key = trim($this->private_key->private_key);
if (!str_ends_with($this->private_key->private_key, "\n")) {
$this->private_key->private_key .= "\n";
}
$this->private_key->private_key = formatPrivateKey($this->private_key->private_key);
$this->private_key->save();
session('currentTeam')->privateKeys = PrivateKey::where('team_id', session('currentTeam')->id)->get();
refresh_server_connection($this->private_key);
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
}

View File

@@ -7,7 +7,7 @@ use Livewire\Component;
class Create extends Component
{
protected string|null $from = null;
public string|null $from = null;
public string $name;
public string|null $description = null;
public string $value;
@@ -19,6 +19,7 @@ class Create extends Component
'name' => 'name',
'value' => 'private Key',
];
public function createPrivateKey()
{
$this->validate();
@@ -31,7 +32,7 @@ class Create extends Component
'name' => $this->name,
'description' => $this->description,
'private_key' => $this->value,
'team_id' => session('currentTeam')->id
'team_id' => currentTeam()->id
]);
if ($this->from === 'server') {
return redirect()->route('server.create');

View File

@@ -17,12 +17,14 @@ class Form extends Component
protected $validationAttributes = [
'name' => 'name',
];
public function mount()
{
$this->userId = auth()->user()->id;
$this->name = auth()->user()->name;
$this->email = auth()->user()->email;
}
public function submit()
{

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Http\Livewire\Project;
use App\Models\Project;
use Livewire\Component;
class AddEmpty extends Component
{
public string $name = '';
public string $description = '';
protected $rules = [
'name' => 'required|string|min:3',
'description' => 'nullable|string',
];
protected $validationAttributes = [
'name' => 'Project Name',
'description' => 'Project Description',
];
public function submit()
{
try {
$this->validate();
$project = Project::create([
'name' => $this->name,
'description' => $this->description,
'team_id' => currentTeam()->id,
]);
return redirect()->route('project.show', $project->uuid);
} catch (\Exception $e) {
general_error_handler($e, $this);
} finally {
$this->name = '';
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Http\Livewire\Project;
use App\Models\Environment;
use App\Models\Project;
use Livewire\Component;
class AddEnvironment extends Component
{
public Project $project;
public string $name = '';
public string $description = '';
protected $rules = [
'name' => 'required|string|min:3',
];
protected $validationAttributes = [
'name' => 'Environment Name',
];
public function submit()
{
try {
$this->validate();
$environment = Environment::create([
'name' => $this->name,
'project_id' => $this->project->id,
]);
return redirect()->route('project.resources', [
'project_uuid' => $this->project->uuid,
'environment_name' => $environment->name,
]);
} catch (\Exception $e) {
general_error_handler($e, $this);
} finally {
$this->name = '';
}
}
}

View File

@@ -10,10 +10,12 @@ class DeploymentLogs extends Component
public ApplicationDeploymentQueue $application_deployment_queue;
public $isKeepAliveOn = true;
protected $listeners = ['refreshQueue'];
public function refreshQueue()
{
$this->application_deployment_queue->refresh();
}
public function polling()
{
$this->emit('deploymentFinished');

View File

@@ -8,17 +8,16 @@ use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Process;
use Livewire\Component;
use Illuminate\Support\Str;
use Livewire\Component;
class DeploymentNavbar extends Component
{
protected $listeners = ['deploymentFinished'];
public ApplicationDeploymentQueue $application_deployment_queue;
public Application $application;
public Server $server;
public bool $is_debug_enabled = false;
protected $listeners = ['deploymentFinished'];
public function mount()
{
@@ -26,10 +25,12 @@ class DeploymentNavbar extends Component
$this->server = $this->application->destination->server;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
}
public function deploymentFinished()
{
$this->application_deployment_queue->refresh();
}
public function show_debug()
{
$this->application->settings->is_debug_enabled = !$this->application->settings->is_debug_enabled;
@@ -37,6 +38,7 @@ class DeploymentNavbar extends Component
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->emit('refreshQueue');
}
public function cancel()
{
try {
@@ -66,4 +68,4 @@ class DeploymentNavbar extends Component
return general_error_handler(err: $e, that: $this);
}
}
}
}

View File

@@ -20,6 +20,7 @@ class Deployments extends Component
$this->current_url = url()->current();
$this->show_more();
}
private function show_more()
{
if (count($this->deployments) !== 0) {
@@ -30,10 +31,12 @@ class Deployments extends Component
return;
}
}
public function reload_deployments()
{
$this->load_deployments();
}
public function load_deployments(int|null $take = null)
{
if ($take) {

View File

@@ -1,45 +0,0 @@
<?php
namespace App\Http\Livewire\Project\Application\EnvironmentVariable;
use App\Models\Application;
use App\Models\EnvironmentVariable;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class All extends Component
{
public Application $application;
public string|null $modalId = null;
protected $listeners = ['refreshEnvs', 'submit'];
public function mount()
{
$this->modalId = new Cuid2(7);
}
public function refreshEnvs()
{
$this->application->refresh();
}
public function submit($data)
{
try {
$found = $this->application->environment_variables()->where('key', $data['key'])->first();
if ($found) {
$this->emit('error', 'Environment variable already exists.');
return;
}
EnvironmentVariable::create([
'key' => $data['key'],
'value' => $data['value'],
'is_build_time' => $data['is_build_time'],
'is_preview' => $data['is_preview'],
'application_id' => $this->application->id,
]);
$this->application->refresh();
$this->emit('success', 'Environment variable added successfully.');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
}
}
}

View File

@@ -4,8 +4,8 @@ namespace App\Http\Livewire\Project\Application;
use App\Models\Application;
use App\Models\InstanceSettings;
use Livewire\Component;
use Illuminate\Support\Str;
use Livewire\Component;
use Spatie\Url\Url;
class General extends Component
@@ -33,6 +33,7 @@ class General extends Component
protected $rules = [
'application.name' => 'required',
'application.description' => 'nullable',
'application.fqdn' => 'nullable',
'application.git_repository' => 'required',
'application.git_branch' => 'required',
@@ -46,9 +47,11 @@ class General extends Component
'application.publish_directory' => 'nullable',
'application.ports_exposes' => 'required',
'application.ports_mappings' => 'nullable',
'application.dockerfile' => 'nullable',
];
protected $validationAttributes = [
'application.name' => 'name',
'application.description' => 'description',
'application.fqdn' => 'FQDN',
'application.git_repository' => 'Git repository',
'application.git_branch' => 'Git branch',
@@ -62,7 +65,9 @@ class General extends Component
'application.publish_directory' => 'Publish directory',
'application.ports_exposes' => 'Ports exposes',
'application.ports_mappings' => 'Ports mappings',
'application.dockerfile' => 'Dockerfile',
];
public function instantSave()
{
// @TODO: find another way - if possible
@@ -84,6 +89,7 @@ class General extends Component
$this->emit('success', 'Application settings updated!');
$this->checkWildCardDomain();
}
protected function checkWildCardDomain()
{
$coolify_instance_settings = InstanceSettings::get();
@@ -91,6 +97,7 @@ class General extends Component
$this->global_wildcard_domain = data_get($coolify_instance_settings, 'wildcard_domain');
$this->wildcard_domain = $this->server_wildcard_domain ?? $this->global_wildcard_domain ?? null;
}
public function mount()
{
$this->is_static = $this->application->settings->is_static;
@@ -102,6 +109,7 @@ class General extends Component
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
$this->checkWildCardDomain();
}
public function generateGlobalRandomDomain()
{
// Set wildcard domain based on Global wildcard domain
@@ -113,6 +121,7 @@ class General extends Component
$this->application->save();
$this->emit('success', 'Application settings updated!');
}
public function generateServerRandomDomain()
{
// Set wildcard domain based on Server wildcard domain
@@ -124,21 +133,30 @@ class General extends Component
$this->application->save();
$this->emit('success', 'Application settings updated!');
}
public function submit()
{
ray($this->application);
try {
$this->validate();
$domains = Str::of($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return Str::of($domain)->trim()->lower();
});
if (data_get($this->application,'fqdn')) {
$domains = Str::of($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return Str::of($domain)->trim()->lower();
});
$this->application->fqdn = $domains->implode(',');
}
if ($this->application->dockerfile) {
$port = get_port_from_dockerfile($this->application->dockerfile);
if ($port) {
$this->application->ports_exposes = $port;
}
}
if ($this->application->base_directory && $this->application->base_directory !== '/') {
$this->application->base_directory = rtrim($this->application->base_directory, '/');
}
if ($this->application->publish_directory && $this->application->publish_directory !== '/') {
$this->application->publish_directory = rtrim($this->application->publish_directory, '/');
}
$this->application->fqdn = $domains->implode(',');
$this->application->save();
$this->emit('success', 'Application settings updated!');
} catch (\Exception $e) {

View File

@@ -1,9 +1,10 @@
<?php
namespace App\Http\Livewire\Application;
namespace App\Http\Livewire\Project\Application;
use App\Jobs\ApplicationContainerStatusJob;
use App\Models\Application;
use App\Notifications\Application\StatusChanged;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -16,17 +17,22 @@ class Heading extends Component
public function mount()
{
$this->parameters = getRouteParameters();
$this->parameters = get_route_parameters();
}
public function check_status()
{
dispatch_sync(new ApplicationContainerStatusJob(
application: $this->application,
container_name: generate_container_name($this->application->uuid),
));
$this->application->refresh();
}
public function force_deploy_without_cache()
{
$this->deploy(force_rebuild: true);
}
public function deploy(bool $force_rebuild = false)
{
$this->setDeploymentUuid();
@@ -42,22 +48,30 @@ class Heading extends Component
'environment_name' => $this->parameters['environment_name'],
]);
}
public function force_deploy_without_cache()
{
$this->deploy(force_rebuild: true);
}
public function stop()
{
remote_process(
["docker rm -f {$this->application->uuid}"],
$this->application->destination->server
);
$this->application->status = 'stopped';
$this->application->save();
}
protected function setDeploymentUuid()
{
$this->deploymentUuid = new Cuid2(7);
$this->parameters['deployment_uuid'] = $this->deploymentUuid;
}
public function stop()
{
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id);
if ($containers->count() === 0) {
return;
}
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containerName) {
remote_process(
["docker rm -f {$containerName}"],
$this->application->destination->server
);
$this->application->status = 'stopped';
$this->application->save();
// $this->application->environment->project->team->notify(new StatusChanged($this->application));
}
}
}
}

View File

@@ -17,6 +17,7 @@ class Form extends Component
protected $validationAttributes = [
'application.preview_url_template' => 'preview url template',
];
public function resetToDefault()
{
$this->application->preview_url_template = '{{pr_id}}.{{domain}}';
@@ -24,6 +25,7 @@ class Form extends Component
$this->application->save();
$this->generate_real_url();
}
public function generate_real_url()
{
if (data_get($this->application, 'fqdn')) {
@@ -32,10 +34,12 @@ class Form extends Component
$this->preview_url_template = Str::of($this->application->preview_url_template)->replace('{{domain}}', $host);
}
}
public function mount()
{
$this->generate_real_url();
}
public function submit()
{
$this->validate();

View File

@@ -20,21 +20,17 @@ class Previews extends Component
public function mount()
{
$this->pull_requests = collect();
$this->parameters = getRouteParameters();
$this->parameters = get_route_parameters();
}
public function loadStatus($pull_request_id)
{
dispatch(new ApplicationContainerStatusJob(
application: $this->application,
container_name: generate_container_name($this->application->uuid, $pull_request_id),
pull_request_id: $pull_request_id
pullRequestId: $pull_request_id
));
}
protected function setDeploymentUuid()
{
$this->deployment_uuid = new Cuid2(7);
$this->parameters['deployment_uuid'] = $this->deployment_uuid;
}
public function load_prs()
{
try {
@@ -46,6 +42,7 @@ class Previews extends Component
return general_error_handler(err: $e, that: $this);
}
}
public function deploy(int $pull_request_id, string|null $pull_request_html_url = null)
{
try {
@@ -74,10 +71,17 @@ class Previews extends Component
return general_error_handler(err: $e, that: $this);
}
}
protected function setDeploymentUuid()
{
$this->deployment_uuid = new Cuid2(7);
$this->parameters['deployment_uuid'] = $this->deployment_uuid;
}
public function stop(int $pull_request_id)
{
try {
$container_name = generate_container_name($this->application->uuid, $pull_request_id);
$container_name = generateApplicationContainerName($this->application->uuid, $pull_request_id);
ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $this->application->destination->server, throwError: false);
@@ -87,6 +91,7 @@ class Previews extends Component
return general_error_handler(err: $e, that: $this);
}
}
public function previewRefresh()
{
$this->application->previews->each(function ($preview) {

View File

@@ -1,60 +0,0 @@
<?php
namespace App\Http\Livewire\Project\Application;
use App\Models\Application;
use Livewire\Component;
class ResourceLimits extends Component
{
public Application $application;
protected $rules = [
'application.limits_memory' => 'required|string',
'application.limits_memory_swap' => 'required|string',
'application.limits_memory_swappiness' => 'required|integer|min:0|max:100',
'application.limits_memory_reservation' => 'required|string',
'application.limits_cpus' => 'nullable',
'application.limits_cpuset' => 'nullable',
'application.limits_cpu_shares' => 'nullable',
];
protected $validationAttributes = [
'application.limits_memory' => 'memory',
'application.limits_memory_swap' => 'swap',
'application.limits_memory_swappiness' => 'swappiness',
'application.limits_memory_reservation' => 'reservation',
'application.limits_cpus' => 'cpus',
'application.limits_cpuset' => 'cpuset',
'application.limits_cpu_shares' => 'cpu shares',
];
public function submit()
{
try {
if (!$this->application->limits_memory) {
$this->application->limits_memory = "0";
}
if (!$this->application->limits_memory_swap) {
$this->application->limits_memory_swap = "0";
}
if (!$this->application->limits_memory_swappiness) {
$this->application->limits_memory_swappiness = "60";
}
if (!$this->application->limits_memory_reservation) {
$this->application->limits_memory_reservation = "0";
}
if (!$this->application->limits_cpus) {
$this->application->limits_cpus = "0";
}
if (!$this->application->limits_cpuset) {
$this->application->limits_cpuset = "0";
}
if (!$this->application->limits_cpu_shares) {
$this->application->limits_cpu_shares = 1024;
}
$this->validate();
$this->application->save();
$this->emit('success', 'Resource limits updated successfully.');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
}
}
}

View File

@@ -3,8 +3,8 @@
namespace App\Http\Livewire\Project\Application;
use App\Models\Application;
use Livewire\Component;
use Illuminate\Support\Str;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Rollback extends Component
@@ -16,8 +16,9 @@ class Rollback extends Component
public function mount()
{
$this->parameters = getRouteParameters();
$this->parameters = get_route_parameters();
}
public function rollbackImage($commit)
{
$deployment_uuid = new Cuid2(7);
@@ -36,6 +37,7 @@ class Rollback extends Component
'environment_name' => $this->parameters['environment_name'],
]);
}
public function loadImages()
{
try {

View File

@@ -21,16 +21,19 @@ class Source extends Component
'application.git_branch' => 'branch',
'application.git_commit_sha' => 'commit sha',
];
private function get_private_keys()
{
$this->private_keys = PrivateKey::whereTeamId(session('currentTeam')->id)->get()->reject(function ($key) {
return $key->id == $this->application->private_key_id;
});
}
public function mount()
{
$this->get_private_keys();
}
private function get_private_keys()
{
$this->private_keys = PrivateKey::whereTeamId(currentTeam()->id)->get()->reject(function ($key) {
return $key->id == $this->application->private_key_id;
});
}
public function setPrivateKey(int $private_key_id)
{
$this->application->private_key_id = $private_key_id;
@@ -38,6 +41,7 @@ class Source extends Component
$this->application->refresh();
$this->get_private_keys();
}
public function submit()
{
$this->validate();

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Http\Livewire\Project\Database;
use Livewire\Component;
class BackupEdit extends Component
{
public $backup;
public $s3s;
public array $parameters;
protected $rules = [
'backup.enabled' => 'required|boolean',
'backup.frequency' => 'required|string',
'backup.number_of_backups_locally' => 'required|integer|min:1',
'backup.save_s3' => 'required|boolean',
'backup.s3_storage_id' => 'nullable|integer',
];
protected $validationAttributes = [
'backup.enabled' => 'Enabled',
'backup.frequency' => 'Frequency',
'backup.number_of_backups_locally' => 'Number of Backups Locally',
'backup.save_s3' => 'Save to S3',
'backup.s3_storage_id' => 'S3 Storage',
];
protected $messages = [
'backup.s3_storage_id' => 'Select a S3 Storage',
];
public function mount()
{
$this->parameters = get_route_parameters();
if (is_null($this->backup->s3_storage_id)) {
$this->backup->s3_storage_id = 'default';
}
}
public function delete()
{
// TODO: Delete backup from server and add a confirmation modal
$this->backup->delete();
redirect()->route('project.database.backups.all', $this->parameters);
}
public function instantSave()
{
try {
$this->custom_validate();
$this->backup->save();
$this->backup->refresh();
$this->emit('success', 'Backup updated successfully');
} catch (\Exception $e) {
$this->emit('error', $e->getMessage());
}
}
private function custom_validate()
{
if (!is_numeric($this->backup->s3_storage_id)) {
$this->backup->s3_storage_id = null;
}
$isValid = validate_cron_expression($this->backup->frequency);
if (!$isValid) {
throw new \Exception('Invalid Cron / Human expression');
}
$this->validate();
}
public function submit()
{
ray($this->backup->s3_storage_id);
try {
$this->custom_validate();
$this->backup->save();
$this->backup->refresh();
$this->emit('success', 'Backup updated successfully');
} catch (\Exception $e) {
$this->emit('error', $e->getMessage());
}
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackupExecution;
use Livewire\Component;
class BackupExecution extends Component
{
public ScheduledDatabaseBackupExecution $execution;
public function download()
{
}
public function delete(): void
{
delete_backup_locally($this->execution->filename, $this->execution->scheduledDatabaseBackup->database->destination->server);
$this->execution->delete();
$this->emit('success', 'Backup deleted successfully.');
$this->emit('refreshBackupExecutions');
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Livewire\Project\Database;
use Livewire\Component;
class BackupExecutions extends Component
{
public $backup;
public $executions;
protected $listeners = ['refreshBackupExecutions'];
public function refreshBackupExecutions(): void
{
$this->executions = $this->backup->executions;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Http\Livewire\Project\Database;
use App\Jobs\DatabaseBackupJob;
use Livewire\Component;
class BackupNow extends Component
{
public $backup;
public function backup_now()
{
dispatch(new DatabaseBackupJob(
backup: $this->backup
));
$this->emit('success', 'Backup queued. It will be available in a few minutes');
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Http\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
use Livewire\Component;
class CreateScheduledBackup extends Component
{
public $database;
public $frequency;
public bool $enabled = true;
public bool $save_s3 = true;
public $s3_storage_id;
public $s3s;
protected $rules = [
'frequency' => 'required|string',
'save_s3' => 'required|boolean',
];
protected $validationAttributes = [
'frequency' => 'Backup Frequency',
'save_s3' => 'Save to S3',
];
public function submit(): void
{
try {
$this->validate();
$isValid = validate_cron_expression($this->frequency);
if (!$isValid) {
$this->emit('error', 'Invalid Cron / Human expression.');
return;
}
ScheduledDatabaseBackup::create([
'enabled' => true,
'frequency' => $this->frequency,
'save_s3' => $this->save_s3,
's3_storage_id' => $this->s3_storage_id,
'database_id' => $this->database->id,
'database_type' => $this->database->getMorphClass(),
'team_id' => currentTeam()->id,
]);
$this->emit('refreshScheduledBackups');
} catch (\Exception $e) {
general_error_handler($e, $this);
} finally {
$this->frequency = '';
$this->save_s3 = true;
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Http\Livewire\Project\Database;
use App\Actions\Database\StartPostgresql;
use App\Jobs\DatabaseContainerStatusJob;
use App\Notifications\Application\StatusChanged;
use Livewire\Component;
class Heading extends Component
{
public $database;
public array $parameters;
protected $listeners = ['activityFinished'];
public function activityFinished()
{
$this->database->update([
'started_at' => now(),
]);
$this->emit('refresh');
$this->check_status();
}
public function check_status()
{
dispatch_sync(new DatabaseContainerStatusJob(
database: $this->database,
));
$this->database->refresh();
}
public function mount()
{
$this->parameters = get_route_parameters();
}
public function stop()
{
remote_process(
["docker rm -f {$this->database->uuid}"],
$this->database->destination->server
);
$this->database->status = 'stopped';
$this->database->save();
// $this->database->environment->project->team->notify(new StatusChanged($this->database));
}
public function start()
{
if ($this->database->type() === 'standalone-postgresql') {
$activity = resolve(StartPostgresql::class)($this->database->destination->server, $this->database);
$this->emit('newMonitorActivity', $activity->id);
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Http\Livewire\Project\Database;
use Livewire\Component;
class InitScript extends Component
{
public array $script;
public int $index;
public string|null $filename;
public string|null $content;
protected $rules = [
'filename' => 'required|string',
'content' => 'required|string',
];
protected $validationAttributes = [
'filename' => 'Filename',
'content' => 'Content',
];
public function mount()
{
$this->index = data_get($this->script, 'index');
$this->filename = data_get($this->script, 'filename');
$this->content = data_get($this->script, 'content');
}
public function submit()
{
$this->validate();
try {
$this->script['index'] = $this->index;
$this->script['content'] = $this->content;
$this->script['filename'] = $this->filename;
$this->emitUp('save_init_script', $this->script);
} catch (Exception $e) {
return general_error_handler(err: $e, that: $this);
}
}
public function delete()
{
$this->emitUp('delete_init_script', $this->script);
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace App\Http\Livewire\Project\Database\Postgresql;
use App\Models\StandalonePostgresql;
use Exception;
use Livewire\Component;
use function Aws\filter;
class General extends Component
{
public StandalonePostgresql $database;
public string $new_filename;
public string $new_content;
protected $listeners = ['refresh', 'save_init_script', 'delete_init_script'];
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.postgres_user' => 'required',
'database.postgres_password' => 'required',
'database.postgres_db' => 'required',
'database.postgres_initdb_args' => 'nullable',
'database.postgres_host_auth_method' => 'nullable',
'database.init_scripts' => 'nullable',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.postgres_user' => 'Postgres User',
'database.postgres_password' => 'Postgres Password',
'database.postgres_db' => 'Postgres DB',
'database.postgres_initdb_args' => 'Postgres Initdb Args',
'database.postgres_host_auth_method' => 'Postgres Host Auth Method',
'database.init_scripts' => 'Init Scripts',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
];
public function save_init_script($script)
{
$this->database->init_scripts = filter($this->database->init_scripts, fn ($s) => $s['filename'] !== $script['filename']);
$this->database->init_scripts = array_merge($this->database->init_scripts, [$script]);
$this->database->save();
$this->emit('success', 'Init script saved successfully.');
}
public function delete_init_script($script)
{
$collection = collect($this->database->init_scripts);
$found = $collection->firstWhere('filename', $script['filename']);
if ($found) {
ray($collection->filter(fn ($s) => $s['filename'] !== $script['filename'])->toArray());
$this->database->init_scripts = $collection->filter(fn ($s) => $s['filename'] !== $script['filename'])->toArray();
$this->database->save();
$this->refresh();
$this->emit('success', 'Init script deleted successfully.');
return;
}
}
public function refresh(): void
{
$this->database->refresh();
}
public function save_new_init_script()
{
$this->validate([
'new_filename' => 'required|string',
'new_content' => 'required|string',
]);
$found = collect($this->database->init_scripts)->firstWhere('filename', $this->new_filename);
if ($found) {
$this->emit('error', 'Filename already exists.');
return;
}
if (!isset($this->database->init_scripts)) {
$this->database->init_scripts = [];
}
$this->database->init_scripts = array_merge($this->database->init_scripts, [
[
'index' => count($this->database->init_scripts),
'filename' => $this->new_filename,
'content' => $this->new_content,
]
]);
$this->database->save();
$this->emit('success', 'Init script added successfully.');
$this->new_content = '';
$this->new_filename = '';
}
public function submit()
{
try {
$this->validate();
$this->database->save();
$this->emit('success', 'Database updated successfully.');
} catch (Exception $e) {
return general_error_handler(err: $e, that: $this);
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Livewire\Project\Database;
use Livewire\Component;
class ScheduledBackups extends Component
{
public $database;
public $parameters;
protected $listeners = ['refreshScheduledBackups'];
public function mount(): void
{
$this->parameters = get_route_parameters();
}
public function delete($scheduled_backup_id): void
{
$this->database->scheduledBackups->find($scheduled_backup_id)->delete();
$this->emit('success', 'Scheduled backup deleted successfully.');
$this->refreshScheduledBackups();
}
public function refreshScheduledBackups(): void
{
ray('refreshScheduledBackups');
$this->database->refresh();
}
}

View File

@@ -12,8 +12,9 @@ class DeleteEnvironment extends Component
public function mount()
{
$this->parameters = getRouteParameters();
$this->parameters = get_route_parameters();
}
public function delete()
{
$this->validate([

View File

@@ -12,8 +12,9 @@ class DeleteProject extends Component
public function mount()
{
$this->parameters = getRouteParameters();
$this->parameters = get_route_parameters();
}
public function delete()
{
$this->validate([

View File

@@ -11,7 +11,7 @@ class EmptyProject extends Component
{
$project = Project::create([
'name' => generate_random_name(),
'team_id' => session('currentTeam')->id,
'team_id' => currentTeam()->id,
]);
return redirect()->route('project.show', ['project_uuid' => $project->uuid, 'environment_name' => 'production']);
}

View File

@@ -5,19 +5,21 @@ namespace App\Http\Livewire\Project\New;
use App\Models\Application;
use App\Models\GithubApp;
use App\Models\Project;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Illuminate\Support\Collection;
use App\Traits\SaveFromRedirect;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Livewire\Component;
use Route;
class GithubPrivateRepository extends Component
{
use SaveFromRedirect;
public $current_step = 'github_apps';
public $github_apps;
public GithubApp $github_app;
public $parameters;
public $currentRoute;
public $query;
public $type;
@@ -29,52 +31,39 @@ class GithubPrivateRepository extends Component
public string $selected_branch_name = 'main';
public string $token;
protected int $page = 1;
public $repositories;
public int $total_repositories_count = 0;
public $branches;
public int $total_branches_count = 0;
public int $port = 3000;
public bool $is_static = false;
public string|null $publish_directory = null;
protected int $page = 1;
// public function saveFromRedirect(string $route, ?Collection $parameters = null){
// session()->forget('from');
// if (!$parameters || $parameters->count() === 0) {
// $parameters = $this->parameters;
// }
// $parameters = collect($parameters) ?? collect([]);
// $queries = collect($this->query) ?? collect([]);
// $parameters = $parameters->merge($queries);
// session(['from'=> [
// 'back'=> $this->currentRoute,
// 'route' => $route,
// 'parameters' => $parameters
// ]]);
// return redirect()->route($route);
// }
public function mount()
{
$this->parameters = getRouteParameters();
$this->currentRoute = Route::currentRouteName();
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->repositories = $this->branches = collect();
$this->github_apps = GithubApp::private();
}
protected function loadRepositoryByPage()
{
$response = Http::withToken($this->token)->get("{$this->github_app->api_url}/installation/repositories?per_page=100&page={$this->page}");
$json = $response->json();
if ($response->status() !== 200) {
return $this->emit('error', $json['message']);
}
if ($json['total_count'] === 0) {
return;
}
$this->total_repositories_count = $json['total_count'];
$this->repositories = $this->repositories->concat(collect($json['repositories']));
}
protected function loadBranchByPage()
{
Log::info('Loading page ' . $this->page);
$response = Http::withToken($this->token)->get("{$this->github_app->api_url}/repos/{$this->selected_repository_owner}/{$this->selected_repository_repo}/branches?per_page=100&page={$this->page}");
$json = $response->json();
if ($response->status() !== 200) {
return $this->emit('error', $json['message']);
}
$this->total_branches_count = count($json);
$this->branches = $this->branches->concat(collect($json));
}
public function loadRepositories($github_app_id)
{
$this->repositories = collect();
@@ -90,7 +79,24 @@ class GithubPrivateRepository extends Component
}
}
$this->selected_repository_id = $this->repositories[0]['id'];
$this->current_step = 'repository';
}
protected function loadRepositoryByPage()
{
$response = Http::withToken($this->token)->get("{$this->github_app->api_url}/installation/repositories?per_page=100&page={$this->page}");
$json = $response->json();
if ($response->status() !== 200) {
return $this->emit('error', $json['message']);
}
if ($json['total_count'] === 0) {
return;
}
$this->total_repositories_count = $json['total_count'];
$this->repositories = $this->repositories->concat(collect($json['repositories']));
}
public function loadBranches()
{
$this->selected_repository_owner = $this->repositories->where('id', $this->selected_repository_id)->first()['owner']['login'];
@@ -105,6 +111,20 @@ class GithubPrivateRepository extends Component
}
}
}
protected function loadBranchByPage()
{
ray('Loading page ' . $this->page);
$response = Http::withToken($this->token)->get("{$this->github_app->api_url}/repos/{$this->selected_repository_owner}/{$this->selected_repository_repo}/branches?per_page=100&page={$this->page}");
$json = $response->json();
if ($response->status() !== 200) {
return $this->emit('error', $json['message']);
}
$this->total_branches_count = count($json);
$this->branches = $this->branches->concat(collect($json));
}
public function submit()
{
try {
@@ -134,7 +154,7 @@ class GithubPrivateRepository extends Component
'destination_id' => $destination->id,
'destination_type' => $destination_class,
'source_id' => $this->github_app->id,
'source_type' => $this->github_app->getMorphClass()
'source_type' => $this->github_app->getMorphClass()
]);
$application->settings->is_static = $this->is_static;
$application->settings->save();
@@ -148,6 +168,7 @@ class GithubPrivateRepository extends Component
return general_error_handler(err: $e, that: $this);
}
}
public function instantSave()
{
if ($this->is_static) {

View File

@@ -14,6 +14,7 @@ use Spatie\Url\Url;
class GithubPrivateRepositoryDeployKey extends Component
{
public $current_step = 'private_keys';
public $parameters;
public $query;
public $private_keys;
@@ -26,14 +27,7 @@ class GithubPrivateRepositoryDeployKey extends Component
public null|string $publish_directory = null;
public string $repository_url;
private object $repository_url_parsed;
public string $branch;
private GithubApp|GitlabApp $git_source;
private string $git_host;
private string $git_repository;
private string $git_branch;
protected $rules = [
'repository_url' => 'required|url',
'branch' => 'required|string',
@@ -48,15 +42,22 @@ class GithubPrivateRepositoryDeployKey extends Component
'is_static' => 'Is static',
'publish_directory' => 'Publish directory',
];
private object $repository_url_parsed;
private GithubApp|GitlabApp|null $git_source = null;
private string $git_host;
private string $git_repository;
private string $git_branch;
public function mount()
{
if (isDev()) {
$this->repository_url = 'https://github.com/coollabsio/coolify-examples';
}
$this->parameters = getRouteParameters();
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->private_keys = PrivateKey::where('team_id', session('currentTeam')->id)->where('id', '!=', 0)->get();
$this->private_keys = PrivateKey::where('team_id', currentTeam()->id)->where('id', '!=', 0)->get();
}
public function instantSave()
{
if ($this->is_static) {
@@ -67,29 +68,13 @@ class GithubPrivateRepositoryDeployKey extends Component
$this->publish_directory = null;
}
}
public function setPrivateKey($private_key_id)
{
$this->private_key_id = $private_key_id;
$this->current_step = 'repository';
}
private function get_git_source()
{
$this->repository_url_parsed = Url::fromString($this->repository_url);
$this->git_host = $this->repository_url_parsed->getHost();
$this->git_repository = $this->repository_url_parsed->getSegment(1) . '/' . $this->repository_url_parsed->getSegment(2);
if ($this->branch) {
$this->git_branch = $this->branch;
} else {
$this->git_branch = $this->repository_url_parsed->getSegment(4) ?? 'main';
}
if ($this->git_host == 'github.com') {
$this->git_source = GithubApp::where('name', 'Public GitHub')->first();
} elseif ($this->git_host == 'gitlab.com') {
$this->git_source = GitlabApp::where('name', 'Public GitLab')->first();
} elseif ($this->git_host == 'bitbucket.org') {
// Not supported yet
}
}
public function submit()
{
$this->validate();
@@ -121,7 +106,7 @@ class GithubPrivateRepositoryDeployKey extends Component
'destination_type' => $destination_class,
'private_key_id' => $this->private_key_id,
'source_id' => $this->git_source->id,
'source_type' => $this->git_source->getMorphClass()
'source_type' => $this->git_source->getMorphClass()
];
$application = Application::create($application_init);
$application->settings->is_static = $this->is_static;
@@ -136,4 +121,24 @@ class GithubPrivateRepositoryDeployKey extends Component
return general_error_handler(err: $e, that: $this);
}
}
private function get_git_source()
{
$this->repository_url_parsed = Url::fromString($this->repository_url);
$this->git_host = $this->repository_url_parsed->getHost();
$this->git_repository = $this->repository_url_parsed->getSegment(1) . '/' . $this->repository_url_parsed->getSegment(2);
if ($this->branch) {
$this->git_branch = $this->branch;
} else {
$this->git_branch = $this->repository_url_parsed->getSegment(4) ?? 'main';
}
if ($this->git_host == 'github.com') {
$this->git_source = GithubApp::where('name', 'Public GitHub')->first();
} elseif ($this->git_host == 'gitlab.com') {
$this->git_source = GitlabApp::where('name', 'Public GitLab')->first();
} elseif ($this->git_host == 'bitbucket.org') {
// Not supported yet
}
}
}

View File

@@ -15,13 +15,10 @@ use Spatie\Url\Url;
class PublicGitRepository extends Component
{
public string $repository_url;
private object $repository_url_parsed;
public int $port = 3000;
public string $type;
public $parameters;
public $query;
public bool $branch_found = false;
public string $selected_branch = 'main';
public bool $is_static = false;
@@ -29,11 +26,6 @@ class PublicGitRepository extends Component
public string $git_branch = 'main';
public int $rate_limit_remaining = 0;
public $rate_limit_reset = 0;
private GithubApp|GitlabApp $git_source;
private string $git_host;
private string $git_repository;
protected $rules = [
'repository_url' => 'required|url',
'port' => 'required|numeric',
@@ -46,13 +38,18 @@ class PublicGitRepository extends Component
'is_static' => 'static',
'publish_directory' => 'publish directory',
];
private object $repository_url_parsed;
private GithubApp|GitlabApp|null $git_source = null;
private string $git_host;
private string $git_repository;
public function mount()
{
if (isDev()) {
$this->repository_url = 'https://github.com/coollabsio/coolify-examples';
$this->port = 3000;
}
$this->parameters = getRouteParameters();
$this->parameters = get_route_parameters();
$this->query = request()->query();
}
@@ -67,24 +64,20 @@ class PublicGitRepository extends Component
}
$this->emit('success', 'Application settings updated!');
}
private function get_branch()
{
['rate_limit_remaining' => $this->rate_limit_remaining, 'rate_limit_reset' => $this->rate_limit_reset] = git_api(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches/{$this->git_branch}");
$this->rate_limit_reset = Carbon::parse((int)$this->rate_limit_reset)->format('Y-M-d H:i:s');
$this->branch_found = true;
}
public function load_branch()
{
$this->branch_found = false;
try {
$this->branch_found = false;
$this->validate([
'repository_url' => 'required|url'
]);
$this->get_git_source();
try {
$this->get_branch();
$this->get_branch();
$this->selected_branch = $this->git_branch;
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
}
if (!$this->branch_found && $this->git_branch == 'main') {
try {
$this->git_branch = 'master';
@@ -94,6 +87,7 @@ class PublicGitRepository extends Component
}
}
}
private function get_git_source()
{
$this->repository_url_parsed = Url::fromString($this->repository_url);
@@ -108,7 +102,18 @@ class PublicGitRepository extends Component
} elseif ($this->git_host == 'bitbucket.org') {
// Not supported yet
}
if (is_null($this->git_source)) {
throw new \Exception('Git source not found. What?!');
}
}
private function get_branch()
{
['rate_limit_remaining' => $this->rate_limit_remaining, 'rate_limit_reset' => $this->rate_limit_reset] = git_api(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches/{$this->git_branch}");
$this->rate_limit_reset = Carbon::parse((int)$this->rate_limit_reset)->format('Y-M-d H:i:s');
$this->branch_found = true;
}
public function submit()
{
try {

View File

@@ -0,0 +1,75 @@
<?php
namespace App\Http\Livewire\Project\New;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Countable;
use Illuminate\Support\Collection;
use Livewire\Component;
use Route;
class Select extends Component
{
public $current_step = 'type';
public ?int $server = null;
public string $type;
public string $server_id;
public string $destination_uuid;
public Countable|array|Server $servers;
public Collection|array $standaloneDockers = [];
public Collection|array $swarmDockers = [];
public array $parameters;
protected $queryString = [
'server',
];
public function mount()
{
$this->parameters = get_route_parameters();
}
public function set_type(string $type)
{
$this->type = $type;
if (count($this->servers) === 1) {
$server = $this->servers->first();
$this->set_server($server);
if (count($server->destinations()) === 1) {
$this->set_destination($server->destinations()->first()->uuid);
}
}
if (!is_null($this->server)) {
$foundServer = $this->servers->where('id', $this->server)->first();
if ($foundServer) {
return $this->set_server($foundServer);
}
}
$this->current_step = 'servers';
}
public function set_server(Server $server)
{
$this->server_id = $server->id;
$this->standaloneDockers = $server->standaloneDockers;
$this->swarmDockers = $server->swarmDockers;
$this->current_step = 'destinations';
}
public function set_destination(string $destination_uuid)
{
$this->destination_uuid = $destination_uuid;
redirect()->route('project.resources.new', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name'],
'type' => $this->type,
'destination' => $this->destination_uuid,
]);
}
public function load_servers()
{
$this->servers = Server::isUsable()->get();
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Http\Livewire\Project\New;
use App\Models\Application;
use App\Models\GithubApp;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class SimpleDockerfile extends Component
{
public string $dockerfile = '';
public array $parameters;
public array $query;
public function mount()
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
if (isDev()) {
$this->dockerfile = 'FROM nginx
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
';
}
}
public function submit()
{
$this->validate([
'dockerfile' => 'required'
]);
$destination_uuid = $this->query['destination'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (!$destination) {
$destination = SwarmDocker::where('uuid', $destination_uuid)->first();
}
if (!$destination) {
throw new \Exception('Destination not found. What?!');
}
$destination_class = $destination->getMorphClass();
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
$port = get_port_from_dockerfile($this->dockerfile);
$application = Application::create([
'name' => 'dockerfile-' . new Cuid2(7),
'repository_project_id' => 0,
'git_repository' => "coollabsio/coolify",
'git_branch' => 'main',
'build_pack' => 'dockerfile',
'dockerfile' => $this->dockerfile,
'ports_exposes' => $port,
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination_class,
'source_id' => 0,
'source_type' => GithubApp::class
]);
$application->update([
'name' => 'dockerfile-' . $application->id
]);
redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
]);
}
}

View File

@@ -1,28 +1,28 @@
<?php
namespace App\Http\Livewire\Project\Application;
namespace App\Http\Livewire\Project\Shared;
use App\Models\Application;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Danger extends Component
{
public Application $application;
public $resource;
public array $parameters;
public string|null $modalId = null;
public function mount()
{
$this->modalId = new Cuid2(7);
$this->parameters = getRouteParameters();
$this->parameters = get_route_parameters();
}
public function delete()
{
$destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
$destination = $this->resource->destination->getMorphClass()::where('id', $this->resource->destination->id)->first();
instant_remote_process(["docker rm -f {$this->application->uuid}"], $destination->server);
$this->application->delete();
instant_remote_process(["docker rm -f {$this->resource->uuid}"], $destination->server);
$this->resource->delete();
return redirect()->route('project.resources', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name']

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Http\Livewire\Project\Application;
namespace App\Http\Livewire\Project\Shared;
use Livewire\Component;

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Http\Livewire\Project\Application\EnvironmentVariable;
namespace App\Http\Livewire\Project\Shared\EnvironmentVariable;
use Livewire\Component;
@@ -23,10 +23,12 @@ class Add extends Component
'value' => 'value',
'is_build_time' => 'build',
];
public function mount()
{
$this->parameters = getRouteParameters();
$this->parameters = get_route_parameters();
}
public function submit()
{
ray('submitting');
@@ -39,6 +41,7 @@ class Add extends Component
]);
$this->clear();
}
public function clear()
{
$this->key = '';

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Http\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class All extends Component
{
public $resource;
public string|null $modalId = null;
protected $listeners = ['refreshEnvs', 'submit'];
public function mount()
{
$this->modalId = new Cuid2(7);
}
public function refreshEnvs()
{
$this->resource->refresh();
}
public function submit($data)
{
try {
$found = $this->resource->environment_variables()->where('key', $data['key'])->first();
if ($found) {
$this->emit('error', 'Environment variable already exists.');
return;
}
$environment = new EnvironmentVariable();
$environment->key = $data['key'];
$environment->value = $data['value'];
$environment->is_build_time = $data['is_build_time'];
$environment->is_preview = $data['is_preview'];
if ($this->resource->type() === 'application') {
$environment->application_id = $this->resource->id;
}
if ($this->resource->type() === 'standalone-postgresql') {
$environment->standalone_postgresql_id = $this->resource->id;
}
$environment->save();
$this->resource->refresh();
$this->emit('success', 'Environment variable added successfully.');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
}
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Http\Livewire\Project\Application\EnvironmentVariable;
namespace App\Http\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use Livewire\Component;
@@ -21,10 +21,16 @@ class Show extends Component
'value' => 'value',
'is_build_time' => 'build',
];
public function mount()
{
$this->modalId = new Cuid2(7);
$this->parameters = getRouteParameters();
$this->parameters = get_route_parameters();
}
public function instantSave()
{
$this->submit();
}
public function submit()
{
@@ -32,6 +38,7 @@ class Show extends Component
$this->env->save();
$this->emit('success', 'Environment variable updated successfully.');
}
public function delete()
{
$this->env->delete();

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Http\Livewire\Project\Shared;
use Livewire\Component;
class ResourceLimits extends Component
{
public $resource;
protected $rules = [
'resource.limits_memory' => 'required|string',
'resource.limits_memory_swap' => 'required|string',
'resource.limits_memory_swappiness' => 'required|integer|min:0|max:100',
'resource.limits_memory_reservation' => 'required|string',
'resource.limits_cpus' => 'nullable',
'resource.limits_cpuset' => 'nullable',
'resource.limits_cpu_shares' => 'nullable',
];
protected $validationAttributes = [
'resource.limits_memory' => 'memory',
'resource.limits_memory_swap' => 'swap',
'resource.limits_memory_swappiness' => 'swappiness',
'resource.limits_memory_reservation' => 'reservation',
'resource.limits_cpus' => 'cpus',
'resource.limits_cpuset' => 'cpuset',
'resource.limits_cpu_shares' => 'cpu shares',
];
public function submit()
{
try {
if (!$this->resource->limits_memory) {
$this->resource->limits_memory = "0";
}
if (!$this->resource->limits_memory_swap) {
$this->resource->limits_memory_swap = "0";
}
if (!$this->resource->limits_memory_swappiness) {
$this->resource->limits_memory_swappiness = "60";
}
if (!$this->resource->limits_memory_reservation) {
$this->resource->limits_memory_reservation = "0";
}
if (!$this->resource->limits_cpus) {
$this->resource->limits_cpus = "0";
}
if (!$this->resource->limits_cpuset) {
$this->resource->limits_cpuset = "0";
}
if (!$this->resource->limits_cpu_shares) {
$this->resource->limits_cpu_shares = 1024;
}
$this->validate();
$this->resource->save();
$this->emit('success', 'Resource limits updated successfully.');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
}
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Http\Livewire\Project\Application\Storages;
namespace App\Http\Livewire\Project\Shared\Storages;
use Livewire\Component;
@@ -22,10 +22,12 @@ class Add extends Component
'mount_path' => 'mount',
'host_path' => 'host',
];
public function mount()
{
$this->parameters = getRouteParameters();
$this->parameters = get_route_parameters();
}
public function submit()
{
$this->validate();
@@ -35,6 +37,7 @@ class Add extends Component
'host_path' => $this->host_path,
]);
}
public function clear()
{
$this->name = '';

View File

@@ -1,19 +1,20 @@
<?php
namespace App\Http\Livewire\Project\Application\Storages;
namespace App\Http\Livewire\Project\Shared\Storages;
use App\Models\Application;
use App\Models\LocalPersistentVolume;
use Livewire\Component;
class All extends Component
{
public Application $application;
public $resource;
protected $listeners = ['refreshStorages', 'submit'];
public function refreshStorages()
{
$this->application->refresh();
$this->resource->refresh();
}
public function submit($data)
{
try {
@@ -21,10 +22,10 @@ class All extends Component
'name' => $data['name'],
'mount_path' => $data['mount_path'],
'host_path' => $data['host_path'],
'resource_id' => $this->application->id,
'resource_type' => Application::class,
'resource_id' => $this->resource->id,
'resource_type' => $this->resource->getMorphClass(),
]);
$this->application->refresh();
$this->resource->refresh();
$this->emit('success', 'Storage added successfully');
$this->emit('clearAddStorage');
} catch (\Exception $e) {

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Http\Livewire\Project\Application\Storages;
namespace App\Http\Livewire\Project\Shared\Storages;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -19,16 +19,19 @@ class Show extends Component
'mount_path' => 'mount',
'host_path' => 'host',
];
public function mount()
{
$this->modalId = new Cuid2(7);
}
public function submit()
{
$this->validate();
$this->storage->save();
$this->emit('success', 'Storage updated successfully');
}
public function delete()
{
$this->storage->delete();

View File

@@ -2,7 +2,6 @@
namespace App\Http\Livewire;
use App\Enums\ActivityTypes;
use App\Models\Server;
use Livewire\Component;
@@ -20,6 +19,7 @@ class RunCommand extends Component
'server' => 'server',
'command' => 'command',
];
public function mount($servers)
{
$this->servers = $servers;

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Livewire\Server;
use App\Models\Server;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Component;
class All extends Component
{
public ?Collection $servers = null;
public function mount () {
$this->servers = Server::ownedByCurrentTeam()->get();
}
public function render()
{
return view('livewire.server.all');
}
}

View File

@@ -4,17 +4,17 @@ namespace App\Http\Livewire\Server;
use App\Actions\Server\InstallDocker;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Form extends Component
{
use AuthorizesRequests;
public Server $server;
public $uptime;
public $dockerVersion;
public string|null $wildcard_domain = null;
public int $cleanup_after_percentage;
public string|null $modalId = null;
protected $rules = [
'server.name' => 'required|min:6',
@@ -35,51 +35,50 @@ class Form extends Component
'server.settings.is_reachable' => 'is reachable',
'server.settings.is_part_of_swarm' => 'is part of swarm'
];
public function mount()
{
$this->modalId = new Cuid2(7);
$this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
}
public function installDocker()
{
$activity = resolve(InstallDocker::class)($this->server, session('currentTeam'));
$activity = resolve(InstallDocker::class)($this->server, currentTeam());
$this->emit('newMonitorActivity', $activity->id);
}
public function validateServer()
{
try {
$this->uptime = instant_remote_process(['uptime'], $this->server);
if ($this->uptime) {
$this->server->settings->is_reachable = true;
$this->server->settings->save();
} else {
$this->uptime = 'Server not reachable.';
throw new \Exception('Server not reachable.');
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->server);
if ($uptime) {
$this->uptime = $uptime;
}
$this->dockerVersion = instant_remote_process(['docker version|head -2|grep -i version'], $this->server, false);
if (!$this->dockerVersion) {
$this->dockerVersion = 'Not installed.';
} else {
$this->server->settings->is_usable = true;
$this->server->settings->save();
if ($dockerVersion) {
$this->dockerVersion = $dockerVersion;
$this->emit('proxyStatusUpdated');
}
} catch (\Exception $e) {
$this->server->settings->is_reachable = false;
$this->server->settings->is_usable = false;
$this->server->settings->save();
return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this);
}
}
public function delete()
{
if (!$this->server->isEmpty()) {
$this->emit('error', 'Server has defined resources. Please delete them first.');
return;
try {
$this->authorize('delete', $this->server);
if (!$this->server->isEmpty()) {
$this->emit('error', 'Server has defined resources. Please delete them first.');
return;
}
$this->server->delete();
return redirect()->route('server.all');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
}
$this->server->delete();
redirect()->route('server.all');
}
public function submit()
{

View File

@@ -2,13 +2,13 @@
namespace App\Http\Livewire\Server\New;
use App\Models\PrivateKey;
use App\Models\Server;
use Livewire\Component;
class ByIp extends Component
{
public $private_keys;
public $limit_reached;
public int|null $private_key_id = null;
public $new_private_key_name;
public $new_private_key_description;
@@ -35,19 +35,23 @@ class ByIp extends Component
'user' => 'user',
'port' => 'port',
];
public function mount()
{
$this->name = generate_random_name();
$this->private_key_id = $this->private_keys->first()->id;
}
public function setPrivateKey(string $private_key_id)
{
$this->private_key_id = $private_key_id;
}
public function instantSave()
{
$this->emit('success', 'Application settings updated!');
}
public function submit()
{
$this->validate();
@@ -61,7 +65,7 @@ class ByIp extends Component
'ip' => $this->ip,
'user' => $this->user,
'port' => $this->port,
'team_id' => session('currentTeam')->id,
'team_id' => currentTeam()->id,
'private_key_id' => $this->private_key_id,
]);
$server->settings->is_part_of_swarm = $this->is_part_of_swarm;

View File

@@ -2,9 +2,9 @@
namespace App\Http\Livewire\Server;
use App\Actions\Proxy\CheckProxySettingsInSync;
use App\Actions\Proxy\CheckConfigurationSync;
use App\Actions\Proxy\SaveConfigurationSync;
use App\Enums\ProxyTypes;
use Illuminate\Support\Str;
use App\Models\Server;
use Livewire\Component;
@@ -16,69 +16,61 @@ class Proxy extends Component
public $proxy_settings = null;
public string|null $redirect_url = null;
protected $listeners = ['proxyStatusUpdated', 'saveConfiguration'];
protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit'];
public function mount()
{
$this->redirect_url = $this->server->proxy->redirect_url;
}
public function proxyStatusUpdated()
{
$this->server->refresh();
}
public function switchProxy()
public function change_proxy()
{
$this->server->proxy = null;
$this->server->save();
$this->emit('proxyStatusUpdated');
}
public function setProxy(string $proxy_type)
public function select_proxy(ProxyTypes $proxy_type)
{
$this->server->proxy->type = $proxy_type;
$this->server->proxy->status = 'exited';
$this->server->save();
$this->emit('proxyStatusUpdated');
}
public function stopProxy()
{
instant_remote_process([
"docker rm -f coolify-proxy",
], $this->server);
$this->server->proxy->status = 'exited';
$this->server->save();
$this->emit('proxyStatusUpdated');
}
public function saveConfiguration()
public function submit()
{
try {
$proxy_path = config('coolify.proxy_config_path');
$this->proxy_settings = Str::of($this->proxy_settings)->trim()->value;
$docker_compose_yml_base64 = base64_encode($this->proxy_settings);
$this->server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
resolve(SaveConfigurationSync::class)($this->server, $this->proxy_settings);
$this->server->proxy->redirect_url = $this->redirect_url;
$this->server->save();
instant_remote_process([
"echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml",
], $this->server);
$this->server->refresh();
setup_default_redirect_404(redirect_url: $this->server->proxy->redirect_url, server: $this->server);
$this->emit('success', 'Proxy configuration saved.');
} catch (\Exception $e) {
return general_error_handler(err: $e);
}
}
public function resetProxy()
public function reset_proxy_configuration()
{
try {
$this->proxy_settings = resolve(CheckProxySettingsInSync::class)($this->server, true);
$this->proxy_settings = resolve(CheckConfigurationSync::class)($this->server, true);
} catch (\Exception $e) {
return general_error_handler(err: $e);
}
}
public function checkProxySettingsInSync()
public function load_proxy_configuration()
{
try {
$this->proxy_settings = resolve(CheckProxySettingsInSync::class)($this->server);
$this->proxy_settings = resolve(CheckConfigurationSync::class)($this->server);
} catch (\Exception $e) {
return general_error_handler(err: $e);
}

View File

@@ -5,28 +5,24 @@ namespace App\Http\Livewire\Server\Proxy;
use App\Actions\Proxy\StartProxy;
use App\Models\Server;
use Livewire\Component;
use Str;
class Deploy extends Component
{
public Server $server;
public $proxy_settings = null;
protected $listeners = ['proxyStatusUpdated'];
public function proxyStatusUpdated()
{
$this->server->refresh();
}
public function deploy()
public function start_proxy()
{
if (
$this->server->proxy->last_applied_settings &&
$this->server->proxy->last_saved_settings !== $this->server->proxy->last_applied_settings
) {
$this->saveConfiguration($this->server);
$this->emit('saveConfiguration', $this->server);
}
$activity = resolve(StartProxy::class)($this->server);
$this->emit('newMonitorActivity', $activity->id);
}
public function stop()
{
instant_remote_process([
@@ -36,8 +32,4 @@ class Deploy extends Component
$this->server->save();
$this->emit('proxyStatusUpdated');
}
private function saveConfiguration(Server $server)
{
$this->emit('saveConfiguration', $server);
}
}

View File

@@ -9,20 +9,15 @@ use Livewire\Component;
class Status extends Component
{
public Server $server;
protected $listeners = ['proxyStatusUpdated'];
public function proxyStatusUpdated()
public function get_status()
{
$this->server->refresh();
}
public function proxyStatus()
{
try {
if (data_get($this->server,'settings.is_usable')) {
dispatch_sync(new ProxyContainerStatusJob(
server: $this->server
));
$this->server->refresh();
$this->emit('proxyStatusUpdated');
} catch (\Exception $e) {
ray($e->getMessage());
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Livewire\Server;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class Show extends Component
{
use AuthorizesRequests;
public ?Server $server = null;
public function mount()
{
try {
$this->server = Server::ownedByCurrentTeam(['name', 'description', 'ip', 'port', 'user', 'proxy'])->whereUuid(request()->server_uuid)->firstOrFail();
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
}
}
public function render()
{
return view('livewire.server.show');
}
}

View File

@@ -3,45 +3,42 @@
namespace App\Http\Livewire\Server;
use App\Models\Server;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
use Masmerise\Toaster\Toaster;
class PrivateKey extends Component
class ShowPrivateKey extends Component
{
public Server $server;
public $privateKeys;
public $parameters;
public function checkConnection()
{
try {
$uptime = instant_remote_process(['uptime'], $this->server);
if ($uptime) {
Toaster::success('Server is reachable with this private key.');
$this->server->settings->is_reachable = true;
$this->server->settings->is_usable = true;
}
} catch (\Exception $e) {
$this->server->settings->is_reachable = false;
$this->server->settings->is_usable = false;
$this->server->settings->save();
return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this);
}
}
public function setPrivateKey($private_key_id)
{
$this->server->update([
'private_key_id' => $private_key_id
]);
// Delete the old ssh mux file to force a new one to be created
Storage::disk('ssh-mux')->delete($this->server->muxFilename());
refresh_server_connection($this->server->privateKey);
$this->server->refresh();
$this->checkConnection();
}
public function checkConnection()
{
try {
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->server);
if ($uptime) {
Toaster::success('Server is reachable with this private key.');
}
if ($dockerVersion) {
Toaster::success('Server is usable for Coolify.');
}
} catch (\Exception $e) {
return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this);
}
}
public function mount()
{
$this->parameters = getRouteParameters();
$this->parameters = get_route_parameters();
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace App\Http\Livewire\Settings;
use App\Jobs\DatabaseBackupJob;
use App\Models\InstanceSettings;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use Livewire\Component;
class Backup extends Component
{
public InstanceSettings $settings;
public $s3s;
public StandalonePostgresql|null|array $database = [];
public ScheduledDatabaseBackup|null|array $backup = [];
public $executions = [];
protected $rules = [
'database.uuid' => 'required',
'database.name' => 'required',
'database.description' => 'nullable',
'database.postgres_user' => 'required',
'database.postgres_password' => 'required',
];
protected $validationAttributes = [
'database.uuid' => 'uuid',
'database.name' => 'name',
'database.description' => 'description',
'database.postgres_user' => 'postgres user',
'database.postgres_password' => 'postgres password',
];
public function mount()
{
$this->backup = $this->database?->scheduledBackups->first() ?? [];
$this->executions = $this->backup?->executions ?? [];
}
public function add_coolify_database()
{
$server = Server::find(0);
$out = instant_remote_process(['docker inspect coolify-db'], $server);
$envs = format_docker_envs_to_json($out);
$postgres_password = $envs['POSTGRES_PASSWORD'];
$postgres_user = $envs['POSTGRES_USER'];
$postgres_db = $envs['POSTGRES_DB'];
$this->database = StandalonePostgresql::create([
'id' => 0,
'name' => 'coolify-db',
'description' => 'Coolify database',
'postgres_user' => $postgres_user,
'postgres_password' => $postgres_password,
'postgres_db' => $postgres_db,
'status' => 'running',
'destination_type' => 'App\Models\StandaloneDocker',
'destination_id' => 0,
]);
$this->backup = ScheduledDatabaseBackup::create([
'id' => 0,
'enabled' => true,
'save_s3' => false,
'frequency' => '0 0 * * *',
'database_id' => $this->database->id,
'database_type' => 'App\Models\StandalonePostgresql',
'team_id' => currentTeam()->id,
]);
$this->database->refresh();
$this->backup->refresh();
ray($this->backup);
$this->s3s = S3Storage::whereTeamId(0)->get();
}
public function backup_now()
{
dispatch(new DatabaseBackupJob(
backup: $this->backup
));
$this->emit('success', 'Backup queued. It will be available in a few minutes');
}
public function submit()
{
$this->emit('success', 'Backup updated successfully');
}
}

View File

@@ -31,6 +31,7 @@ class Configuration extends Component
'settings.public_port_min' => 'Public port min',
'settings.public_port_max' => 'Public port max',
];
public function mount()
{
$this->do_not_track = $this->settings->do_not_track;
@@ -38,6 +39,7 @@ class Configuration extends Component
$this->is_registration_enabled = $this->settings->is_registration_enabled;
$this->next_channel = $this->settings->next_channel;
}
public function instantSave()
{
$this->settings->do_not_track = $this->do_not_track;
@@ -47,6 +49,21 @@ class Configuration extends Component
$this->settings->save();
$this->emit('success', 'Settings updated!');
}
public function submit()
{
$this->resetErrorBag();
if ($this->settings->public_port_min > $this->settings->public_port_max) {
$this->addError('settings.public_port_min', 'The minimum port must be lower than the maximum port.');
return;
}
$this->validate();
$this->settings->save();
$this->server = Server::findOrFail(0);
$this->setup_instance_fqdn();
$this->emit('success', 'Instance settings updated successfully!');
}
private function setup_instance_fqdn()
{
$file = "$this->dynamic_config_path/coolify.yaml";
@@ -110,6 +127,7 @@ class Configuration extends Component
dispatch(new ProxyStartJob($this->server));
}
}
private function save_configuration_to_disk(array $traefik_dynamic_conf, string $file)
{
$yaml = Yaml::dump($traefik_dynamic_conf, 12, 2);
@@ -128,17 +146,4 @@ class Configuration extends Component
ray($yaml);
}
}
public function submit()
{
$this->resetErrorBag();
if ($this->settings->public_port_min > $this->settings->public_port_max) {
$this->addError('settings.public_port_min', 'The minimum port must be lower than the maximum port.');
return;
}
$this->validate();
$this->settings->save();
$this->server = Server::findOrFail(0);
$this->setup_instance_fqdn();
$this->emit('success', 'Instance settings updated successfully!');
}
}

View File

@@ -3,78 +3,103 @@
namespace App\Http\Livewire\Settings;
use App\Models\InstanceSettings;
use App\Notifications\TransactionalEmails\TestEmail;
use Illuminate\Support\Facades\Notification;
use App\Notifications\TransactionalEmails\Test;
use Livewire\Component;
class Email extends Component
{
public InstanceSettings $settings;
public string $emails;
protected $rules = [
'settings.smtp.enabled' => 'nullable|boolean',
'settings.smtp.host' => 'required',
'settings.smtp.port' => 'required|numeric',
'settings.smtp.encryption' => 'nullable',
'settings.smtp.username' => 'nullable',
'settings.smtp.password' => 'nullable',
'settings.smtp.timeout' => 'nullable',
'settings.smtp.test_recipients' => 'nullable',
'settings.smtp.from_address' => 'required|email',
'settings.smtp.from_name' => 'required',
'settings.smtp_enabled' => 'nullable|boolean',
'settings.smtp_host' => 'required',
'settings.smtp_port' => 'required|numeric',
'settings.smtp_encryption' => 'nullable',
'settings.smtp_username' => 'nullable',
'settings.smtp_password' => 'nullable',
'settings.smtp_timeout' => 'nullable',
'settings.smtp_from_address' => 'required|email',
'settings.smtp_from_name' => 'required',
'settings.resend_enabled' => 'nullable|boolean',
'settings.resend_api_key' => 'nullable'
];
protected $validationAttributes = [
'settings.smtp.from_address' => 'From Address',
'settings.smtp.from_name' => 'From Name',
'settings.smtp.recipients' => 'Recipients',
'settings.smtp.host' => 'Host',
'settings.smtp.port' => 'Port',
'settings.smtp.encryption' => 'Encryption',
'settings.smtp.username' => 'Username',
'settings.smtp.password' => 'Password',
'settings.smtp.test_recipients' => 'Test Recipients',
'settings.smtp_from_address' => 'From Address',
'settings.smtp_from_name' => 'From Name',
'settings.smtp_recipients' => 'Recipients',
'settings.smtp_host' => 'Host',
'settings.smtp_port' => 'Port',
'settings.smtp_encryption' => 'Encryption',
'settings.smtp_username' => 'Username',
'settings.smtp_password' => 'Password',
'settings.smtp_timeout' => 'Timeout',
'settings.resend_api_key' => 'Resend API Key'
];
public function mount()
{
$this->decrypt();
$this->emails = auth()->user()->email;
}
public function submitFromFields() {
try {
$this->resetErrorBag();
$this->validate([
'settings.smtp_from_address' => 'required|email',
'settings.smtp_from_name' => 'required',
]);
$this->settings->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
return general_error_handler($e, $this);
}
}
public function submitResend() {
try {
$this->resetErrorBag();
$this->validate([
'settings.resend_api_key' => 'required'
]);
$this->settings->smtp_enabled = false;
$this->settings->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
$this->settings->resend_enabled = false;
return general_error_handler($e, $this);
}
}
public function instantSave()
{
try {
$this->submit();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
$this->settings->smtp->enabled = false;
$this->validate();
}
}
public function testNotification()
{
$this->settings->notify(new TestEmail);
$this->emit('success', 'Test email sent.');
}
private function decrypt()
{
if (data_get($this->settings, 'smtp.password')) {
try {
$this->settings->smtp->password = decrypt($this->settings->smtp->password);
} catch (\Exception $e) {
}
return general_error_handler($e, $this);
}
}
public function submit()
{
$this->resetErrorBag();
$this->validate();
if ($this->settings->smtp->password) {
$this->settings->smtp->password = encrypt($this->settings->smtp->password);
} else {
$this->settings->smtp->password = null;
try {
$this->resetErrorBag();
$this->validate([
'settings.smtp_host' => 'required',
'settings.smtp_port' => 'required|numeric',
'settings.smtp_encryption' => 'nullable',
'settings.smtp_username' => 'nullable',
'settings.smtp_password' => 'nullable',
'settings.smtp_timeout' => 'nullable',
]);
$this->settings->resend_enabled = false;
$this->settings->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
return general_error_handler($e, $this);
}
}
$this->settings->smtp->test_recipients = str_replace(' ', '', $this->settings->smtp->test_recipients);
$this->settings->save();
$this->emit('success', 'Transaction email settings updated successfully.');
$this->decrypt();
public function sendTestNotification()
{
$this->settings->notify(new Test($this->emails));
$this->emit('success', 'Test email sent.');
}
}

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