Compare commits

...

311 Commits

Author SHA1 Message Date
Andras Bacsai
272d5547c8 Merge pull request #1262 from coollabsio/next
v4.0.0-beta.49
2023-09-29 10:21:54 +02:00
Andras Bacsai
fbaec769f0 fix: reporting handler 2023-09-29 10:21:11 +02:00
Andras Bacsai
d93640a8d9 Merge pull request #1261 from coollabsio/next
v4.0.0-beta.48
2023-09-29 09:51:51 +02:00
Andras Bacsai
3749970831 fix: update process if server has been renamed 2023-09-29 09:48:20 +02:00
Andras Bacsai
7d45bb1335 version check 2023-09-28 22:55:03 +02:00
Andras Bacsai
da04e0c027 fix: sync:bunny 2023-09-28 22:42:10 +02:00
Andras Bacsai
c637d2d90d version++ 2023-09-28 22:31:53 +02:00
Andras Bacsai
1bb8860f37 Merge pull request #1260 from coollabsio/next
v4.0.0-beta.47
2023-09-28 22:25:12 +02:00
Andras Bacsai
38a22dcf4d fix: service templates 2023-09-28 22:20:49 +02:00
Andras Bacsai
91e1eb7664 fix 2023-09-28 21:56:34 +02:00
Andras Bacsai
c7946e7551 fix: next helper image 2023-09-28 21:52:50 +02:00
Andras Bacsai
958645e37f Merge branch 'main' into next 2023-09-28 21:48:03 +02:00
Andras Bacsai
acdfa89ec1 Update coolify-helper.yml 2023-09-28 21:32:53 +02:00
Andras Bacsai
62ec85ee2e Update coolify-helper.yml 2023-09-28 21:31:50 +02:00
Andras Bacsai
8f24a66456 Update coolify-helper.yml 2023-09-28 21:26:55 +02:00
Andras Bacsai
0dd0888775 Update coolify-helper.yml 2023-09-28 21:23:25 +02:00
Andras Bacsai
5040ddea28 fix: sentry 4502634789 2023-09-28 13:46:53 +02:00
Andras Bacsai
1a04a57c01 fix: sentry 4504136641 2023-09-28 13:40:58 +02:00
Andras Bacsai
9b6c162224 fix: sentry 4510197209 2023-09-28 13:15:14 +02:00
Andras Bacsai
e22c5d22f5 fix: instance fqdn setting 2023-09-28 13:13:21 +02:00
Andras Bacsai
84e5b39830 fix: prevent sync version (it needs an option) 2023-09-28 13:07:52 +02:00
Andras Bacsai
cdb6964b0b fix: install script drops an error 2023-09-28 13:05:17 +02:00
Andras Bacsai
df4ecd47a7 fix: sync command 2023-09-28 11:54:20 +02:00
Andras Bacsai
4a84c7238a fix: cannot delete env with available services 2023-09-28 11:51:01 +02:00
Andras Bacsai
2b3057e1b4 version++ 2023-09-28 11:50:50 +02:00
Andras Bacsai
ae65172946 Merge pull request #1256 from coollabsio/next
v4.0.0-beta.46
2023-09-28 11:38:16 +02:00
Andras Bacsai
3a2d17bc05 fix: containerStatusJob 2023-09-28 11:33:16 +02:00
Andras Bacsai
4c1067cf36 baseurl for docs 2023-09-28 11:01:00 +02:00
Andras Bacsai
b046a3e9f7 fix: sslip for localhost 2023-09-28 10:53:00 +02:00
Andras Bacsai
199881c596 fixes 2023-09-28 09:54:21 +02:00
Andras Bacsai
99c8607ff4 fix: disable early updates 2023-09-27 22:43:32 +02:00
Andras Bacsai
e61fcc77f9 ui 2023-09-27 22:39:10 +02:00
Andras Bacsai
20dca179fb add umami 2023-09-27 22:35:15 +02:00
Andras Bacsai
0f542c65ae fix: manually create network for services 2023-09-27 22:35:10 +02:00
Andras Bacsai
d609fcaee1 fix: services 2023-09-27 21:51:06 +02:00
Andras Bacsai
fe8a7fc54f fix: services view 2023-09-27 21:14:13 +02:00
Andras Bacsai
398f122593 fix: aaaaaaaaaaaaaaaaa 2023-09-27 15:48:19 +02:00
Andras Bacsai
f0abdcc2da okay, now it is way better 2023-09-27 12:45:53 +02:00
Andras Bacsai
c9a278b750 update service templates 2023-09-26 16:32:05 +02:00
Andras Bacsai
6990c593a4 update 2023-09-26 16:27:48 +02:00
Andras Bacsai
8f54b51ecd update templates 2023-09-26 16:21:55 +02:00
Andras Bacsai
3eb628b773 fix: containerstatusjob 2023-09-26 15:07:33 +02:00
Andras Bacsai
fabb97330a puh, fixes 2023-09-26 14:45:52 +02:00
Andras Bacsai
03c9793d11 Merge pull request #1252 from khrj/patch-1
Remove space before ? from onboarding flow
2023-09-25 21:02:53 +02:00
Andras Bacsai
a4320b7cee cloud: marketing emails 2023-09-25 20:57:52 +02:00
Andras Bacsai
cbf9bc99ea ui: help 2023-09-25 18:07:20 +02:00
Andras Bacsai
0fbc382467 feat: image tag for services 2023-09-25 17:51:04 +02:00
Andras Bacsai
0f8ccac775 fix: new service template layout 2023-09-25 17:41:35 +02:00
Andras Bacsai
ee20c3339e fix: show real storage name on services 2023-09-25 17:14:19 +02:00
Andras Bacsai
0b11093d18 feat: services 2023-09-25 15:48:43 +02:00
Khushraj Rathod
de8118b59d Remove space before ? from onboarding flow 2023-09-25 19:04:11 +05:30
Andras Bacsai
58522b59b7 - package updates
- feat: service fixes
- ui: help menu
2023-09-25 12:49:55 +02:00
Andras Bacsai
356394c03d ui: update help modal 2023-09-25 11:15:35 +02:00
Andras Bacsai
3e4db2f5b2 ui: more visible feedback button 2023-09-25 09:34:32 +02:00
Andras Bacsai
872981b8b4 ui: a bit better error 2023-09-25 09:20:29 +02:00
Andras Bacsai
51c468ae0b fix: proxy configuration + starter 2023-09-25 09:17:42 +02:00
Andras Bacsai
80a797aec8 version++ 2023-09-24 21:56:57 +02:00
Andras Bacsai
cfdab13d77 Merge pull request #1250 from coollabsio/next
v4.0.0-beta.45
2023-09-24 21:41:58 +02:00
Andras Bacsai
4fc8988ff4 fix: put back build pack chooser 2023-09-24 21:38:11 +02:00
Andras Bacsai
ab5619292e Merge pull request #1249 from chz/fix/1240
ui: Fixed z-index for magicbar
2023-09-24 21:18:48 +02:00
Andras Bacsai
b02e5d3f27 fix: applications with port mappins do a normal update (not rolling update) 2023-09-24 21:15:43 +02:00
Andras Bacsai
5f0c9c3a31 ui: Add source button 2023-09-24 21:15:21 +02:00
Chingiz Mammadov
ea6ec07a45 ui: Fixed z-index for magicbar 2023-09-24 23:03:11 +04:00
Andras Bacsai
36be325d0d Merge pull request #1248 from chz/ui/fix-version-zindex
ui: Fixed z-index for version link.
2023-09-24 20:42:35 +02:00
Andras Bacsai
6138ddcac6 revert sending notification 2023-09-24 17:51:01 +02:00
Andras Bacsai
0509da6730 fix: also check docker socks permission on validation 2023-09-24 17:48:40 +02:00
Andras Bacsai
5b877b84c2 fix: add expose port for containers 2023-09-24 17:48:25 +02:00
Andras Bacsai
0c35726a8d fix: add traefik labels no matter if traefik is selected or not 2023-09-24 17:47:43 +02:00
Andras Bacsai
e74899611b fix: links with path 2023-09-24 17:39:12 +02:00
Andras Bacsai
e9149e534d temporary disable containerstatusjob failed 2023-09-24 17:31:41 +02:00
Chingiz Mammadov
7cded7a36d ui: Fixed z-index for version link. 2023-09-24 14:12:07 +04:00
Andras Bacsai
ba74d55b4c fix: report livewire errors 2023-09-24 12:10:36 +02:00
Andras Bacsai
a1d13fc14e Merge pull request #1247 from coollabsio/next
v4.0.0-beta.44 - fix schema
2023-09-24 11:58:36 +02:00
Andras Bacsai
cdb2a3a8e5 version-- 2023-09-24 11:58:10 +02:00
Andras Bacsai
074c56edaa version++ 2023-09-24 11:57:11 +02:00
Andras Bacsai
66bc03b4cd fix: database schema 2023-09-24 11:56:32 +02:00
Andras Bacsai
2dcb7fec05 fix: services 2023-09-24 11:52:13 +02:00
Andras Bacsai
249bccb49e Merge pull request #1246 from coollabsio/next
v4.0.0-beta.44
2023-09-24 11:39:36 +02:00
Andras Bacsai
a55eaa10ac ui: fix typo 2023-09-24 11:32:26 +02:00
Andras Bacsai
ca2c75ce19 fix: only show traefik dashboard if its available 2023-09-24 11:10:50 +02:00
Andras Bacsai
250d7cbc53 ui: fixes 2023-09-24 10:55:15 +02:00
Andras Bacsai
92a53a151e fix: server validation with cf tunnels 2023-09-24 10:48:54 +02:00
Andras Bacsai
db3148c080 typos 2023-09-24 10:32:41 +02:00
Andras Bacsai
c46eeac4b5 feat: add cloudflare tunnel support 2023-09-23 13:34:40 +02:00
Andras Bacsai
19111ba059 fix: startProxy 2023-09-23 12:21:56 +02:00
Andras Bacsai
9bc61a0a17 fix: make sure proxy path created 2023-09-23 11:53:30 +02:00
Andras Bacsai
78b9166bdb fix: sentry issue 4478125289 2023-09-23 11:47:24 +02:00
Andras Bacsai
1752448050 wip: services 2023-09-22 21:31:47 +02:00
Andras Bacsai
3fc78bc760 wip: services 2023-09-22 20:39:56 +02:00
Andras Bacsai
0c30b6222d fix: mappings 2023-09-22 15:49:59 +02:00
Andras Bacsai
3f74609c7f remove debug 2023-09-22 15:47:21 +02:00
Andras Bacsai
31b0ccba99 fix: prevent overwrite already existing env variables in services 2023-09-22 15:46:30 +02:00
Andras Bacsai
3fc544e0b9 feat: healthcheck for apps 2023-09-22 15:29:19 +02:00
Andras Bacsai
67078fdc71 wip: services
feat: able to map port<->domain
2023-09-22 14:47:25 +02:00
Andras Bacsai
c91f426af3 wip: services 2023-09-22 12:08:51 +02:00
Andras Bacsai
9c2fea4b2e fix: delete persistent storages on resource deletion 2023-09-22 11:34:27 +02:00
Andras Bacsai
53d1fa0331 wip: ui for services 2023-09-22 11:23:49 +02:00
Andras Bacsai
4ae7e46e81 fix: proxy connections 2023-09-22 08:52:07 +02:00
Andras Bacsai
ebfc0bd1e1 fix: add proxy to network with periodic check 2023-09-22 08:42:27 +02:00
Andras Bacsai
e1a1490911 wip 2023-09-21 21:30:13 +02:00
Andras Bacsai
6b75ff7de4 wip: services 2023-09-21 17:48:31 +02:00
Andras Bacsai
301469de6b fix: save proxy configuration 2023-09-21 11:03:52 +02:00
Andras Bacsai
b4d69a22df wip: services 2023-09-20 15:42:41 +02:00
Andras Bacsai
a86e971020 wip: services 2023-09-19 15:51:13 +02:00
Andras Bacsai
145af41c82 fix: delete environment variables on app/db delete 2023-09-19 14:08:20 +02:00
Andras Bacsai
69c0b7240a fix: add github app change on new app view 2023-09-19 13:30:17 +02:00
Andras Bacsai
6543132bcb test 7 days trial 2023-09-19 09:03:27 +02:00
Andras Bacsai
9134437218 version++ 2023-09-18 15:43:14 +02:00
Andras Bacsai
b7acf99cde Merge pull request #1238 from coollabsio/next
Fixes
2023-09-18 15:31:54 +02:00
Andras Bacsai
21d52d7846 oops 2023-09-18 15:23:23 +02:00
Andras Bacsai
ab5929cc69 typo 2023-09-18 15:20:48 +02:00
Andras Bacsai
1452cdf5ad fix: send internal notifications of email errors 2023-09-18 15:19:27 +02:00
Andras Bacsai
3eb1a1f48c debug with ray 2023-09-18 15:06:37 +02:00
Andras Bacsai
5f7a97c31f debug job 2023-09-18 15:04:50 +02:00
Andras Bacsai
9cba0a6df3 fix: boarding again 2023-09-18 14:41:31 +02:00
Andras Bacsai
af57b2aa73 pricing change 2023-09-18 13:42:35 +02:00
Andras Bacsai
b9b9582601 version++ 2023-09-18 13:14:48 +02:00
Andras Bacsai
c8ba98b93d fix: try to use old docker-compose 2023-09-18 13:14:05 +02:00
Andras Bacsai
a50e1e2f0c Merge pull request #1237 from coollabsio/next
Lots of fixes
2023-09-18 13:06:43 +02:00
Andras Bacsai
1093294f06 minimum toaster 2023-09-18 13:06:02 +02:00
Andras Bacsai
c023be2348 fix: improve localhost boarding process 2023-09-18 13:01:01 +02:00
Andras Bacsai
deece51e83 fix: stop/start UI on apps and dbs 2023-09-18 12:38:11 +02:00
Andras Bacsai
e2ab569244 version++ 2023-09-18 12:30:49 +02:00
Andras Bacsai
93b202bde4 fix: convert startProxy to action 2023-09-18 12:29:50 +02:00
Andras Bacsai
6bb6de188c Merge pull request #1236 from coollabsio/next
v4.0.0-beta.41
2023-09-18 12:26:00 +02:00
Andras Bacsai
61f58fa30f fix: proxy configuration saving 2023-09-18 12:18:45 +02:00
Andras Bacsai
026f3fd72d fix 2023-09-18 12:09:31 +02:00
Andras Bacsai
9a706d55b4 fix: proxy container status 2023-09-18 12:08:19 +02:00
Andras Bacsai
9646969107 fix: errors 2023-09-18 11:49:26 +02:00
Andras Bacsai
df433efe62 fix: boarding 2023-09-18 11:21:10 +02:00
Andras Bacsai
efa7cab3e2 fix: installDocker id not found 2023-09-18 10:44:32 +02:00
Andras Bacsai
a386a1bde5 fix: allow non ip address (DNS) 2023-09-18 10:41:19 +02:00
Andras Bacsai
cc1bf023be fix: do not remove localhost in boarding 2023-09-18 10:41:06 +02:00
Andras Bacsai
edb06fa233 version++ 2023-09-18 10:06:35 +02:00
Andras Bacsai
abdb8074b1 Merge pull request #1235 from coollabsio/next
4.0.0-beta.40
2023-09-18 10:05:52 +02:00
Andras Bacsai
157da798dd fix: proxy start (if not proxy defined, use Traefik) 2023-09-18 09:58:13 +02:00
Andras Bacsai
9c5501326e Merge pull request #1230 from coollabsio/next
4.0.0-beta.39
2023-09-16 16:53:17 +02:00
Andras Bacsai
3ea462efc9 fix: localhost 2023-09-16 16:49:33 +02:00
Andras Bacsai
f77df5b732 feat: sentry add email for better support 2023-09-15 21:13:50 +02:00
Andras Bacsai
b77074fe4e Merge pull request #1228 from coollabsio/next
v4.0.0-beta.38
2023-09-15 18:05:59 +02:00
Andras Bacsai
a7b7d3fa32 version++ 2023-09-15 18:02:55 +02:00
Andras Bacsai
f4f3034389 fix: 4.0.0-beta.37 2023-09-15 17:57:17 +02:00
Andras Bacsai
4b2ffb456f fix: team error 2023-09-15 17:30:26 +02:00
Andras Bacsai
e17ff99c5b fix: missing upgrade js 2023-09-15 15:50:37 +02:00
Andras Bacsai
1cf036bbc6 fix: generate new key 2023-09-15 15:39:25 +02:00
Andras Bacsai
da4c2ee60f fix: boarding
fix: error handling
fix: restarting state
2023-09-15 15:34:25 +02:00
Andras Bacsai
fcf7c5ddd5 fix: restarting container state on ui 2023-09-15 13:44:41 +02:00
Andras Bacsai
27926322e1 version++ 2023-09-15 13:06:27 +02:00
Andras Bacsai
c735ff545e Merge pull request #1226 from coollabsio/next
v4.0.0-beta.37
2023-09-15 12:44:50 +02:00
Andras Bacsai
b4f048b028 fixes 2023-09-15 12:43:03 +02:00
Andras Bacsai
54a57d217f fix: ssh-agent revert 2023-09-15 12:30:25 +02:00
Andras Bacsai
cf28490acc feat: generate ssh key 2023-09-15 11:55:58 +02:00
Andras Bacsai
019670d5d1 fix: smtp view 2023-09-15 11:28:44 +02:00
Andras Bacsai
b07cc500e7 fix: invitation 2023-09-15 11:19:36 +02:00
Andras Bacsai
82c235d5af add features to pricing 2023-09-14 23:32:58 +02:00
Andras Bacsai
307e4a6990 fix: collect billing address 2023-09-14 20:42:12 +02:00
Andras Bacsai
ddb7af63a6 fix 2023-09-14 20:39:33 +02:00
Andras Bacsai
7a429ee5bb fix: rate limit 2023-09-14 18:49:15 +02:00
Andras Bacsai
bb591604ac workflow update 2023-09-14 18:44:55 +02:00
Andras Bacsai
81f7a65dd5 fix: help 2023-09-14 18:41:21 +02:00
Andras Bacsai
4b313bb1c6 fix: simply reply to help messages 2023-09-14 18:33:05 +02:00
Andras Bacsai
aba0b2f13c remove unnecessary jobs 2023-09-14 18:29:54 +02:00
Andras Bacsai
9a284e47da version++ 2023-09-14 18:22:25 +02:00
Andras Bacsai
9f2fbc661a fix: registration
fix: user deletion
2023-09-14 18:22:08 +02:00
Andras Bacsai
949407368e fix: uniqueips 2023-09-14 18:10:13 +02:00
Andras Bacsai
93c65f6a79 fix: ip check 2023-09-14 18:07:29 +02:00
Andras Bacsai
d9fe16a3ee fix: redirect on server not found 2023-09-14 17:45:00 +02:00
Andras Bacsai
adaca4d4e3 fix: sub for root 2023-09-14 17:32:33 +02:00
Andras Bacsai
a6d5f3038c fix: help uri 2023-09-14 17:28:58 +02:00
Andras Bacsai
4d49132821 hm 2023-09-14 17:22:21 +02:00
Andras Bacsai
c287276d0e temporary fix for proxy 2023-09-14 17:10:37 +02:00
Andras Bacsai
e89868c692 fix: SaveConfigurationSync 2023-09-14 16:55:13 +02:00
Andras Bacsai
f93317cd2e Merge pull request #1224 from coollabsio/next
v4.0.0-beta.36
2023-09-14 16:28:52 +02:00
Andras Bacsai
8412802f4d oh wow, it is cool! 2023-09-14 15:52:04 +02:00
Andras Bacsai
53c20e1e99 feat: new container status checks 2023-09-14 12:45:50 +02:00
Andras Bacsai
3c8c8e20b1 fix: editable ip
fix: traefik dashboard
2023-09-14 11:37:20 +02:00
Andras Bacsai
49f8abcd79 remove unnecessary things 2023-09-14 10:56:25 +02:00
Andras Bacsai
4a4d73b87b fix: plus boarding step about Coolify 2023-09-14 10:39:05 +02:00
Andras Bacsai
046eab3776 fix: processWithEnv()->run 2023-09-14 10:26:48 +02:00
Andras Bacsai
17c0e91a0d feat: ssh-agent instead of filesystem based ssh keys 2023-09-14 10:12:58 +02:00
Andras Bacsai
fe4a0ae166 fix: encrypt jobs 2023-09-14 10:12:44 +02:00
Andras Bacsai
52c84f8d22 fix: lower case email on waitlist 2023-09-13 20:48:13 +02:00
Andras Bacsai
b22fecb615 fix: lowercase email in forgot password 2023-09-13 20:27:58 +02:00
Andras Bacsai
23b7fc3c54 fix: prevent weird ui bug for validateServer 2023-09-13 13:00:43 +02:00
Andras Bacsai
1efb1235b4 fix: add timeout for ssh commands 2023-09-13 13:00:16 +02:00
Andras Bacsai
0924070a13 version++ 2023-09-13 12:42:59 +02:00
Andras Bacsai
12fc5a8f91 Merge pull request #1223 from coollabsio/next
v4.0.0-beta.35
2023-09-13 12:34:45 +02:00
Andras Bacsai
9eba058cf7 fix: disable dockerfile based healtcheck for now 2023-09-13 12:08:44 +02:00
Andras Bacsai
fa4f5fea8c fix: forgot password 2023-09-13 11:29:31 +02:00
Andras Bacsai
898563fe7c email: server lost connection 2023-09-13 11:05:10 +02:00
Andras Bacsai
c418a17161 ui: show trial instead of sub 2023-09-12 17:24:46 +02:00
Andras Bacsai
cd0da04ea2 wip: server check instead of app check 2023-09-12 15:47:30 +02:00
Andras Bacsai
01e942c6a0 fix: show help modal everywhere 2023-09-12 15:06:07 +02:00
Andras Bacsai
bb9abafa82 test gh runner 2023-09-12 15:00:05 +02:00
Andras Bacsai
d0cd926517 fix: sub type 2023-09-12 14:53:54 +02:00
Andras Bacsai
9baf0161c7 fix: help should send cc on email 2023-09-12 14:51:35 +02:00
Andras Bacsai
8ba18b2ce1 fix: confirm email before sending 2023-09-12 13:19:55 +02:00
Andras Bacsai
ab021ee535 fix: server is functional check 2023-09-12 13:14:01 +02:00
Andras Bacsai
c76a1b1ba5 fix: webhooks should not run if server is not functional 2023-09-12 13:10:39 +02:00
Andras Bacsai
6266a5e500 internal: trial emails 2023-09-12 12:03:17 +02:00
Andras Bacsai
5d27e89bfa feat: dynamic trial period 2023-09-12 11:23:31 +02:00
Andras Bacsai
6da4e78374 feat: trial 2023-09-12 11:19:21 +02:00
Andras Bacsai
be30651172 fix: remove nixpkgarchive from ui 2023-09-12 09:46:10 +02:00
Andras Bacsai
95764c2b76 fix: remove nixpkgarchive 2023-09-12 09:45:20 +02:00
Andras Bacsai
92a75685b5 version plus plus 2023-09-11 23:06:24 +02:00
Andras Bacsai
a1592373aa Merge pull request #1222 from coollabsio/next
v4.0.0-beta.34
2023-09-11 23:05:30 +02:00
Andras Bacsai
5747a87f66 ui: fix 2023-09-11 22:55:23 +02:00
Andras Bacsai
9cda671aef Add email to user 2023-09-11 22:45:07 +02:00
Andras Bacsai
2c9983046c fix 2023-09-11 22:43:07 +02:00
Andras Bacsai
11d33f328e fix: queue after commit 2023-09-11 22:39:18 +02:00
Andras Bacsai
f79c741d95 prepare: global env variables 2023-09-11 22:29:47 +02:00
Andras Bacsai
8a39a4469a fix: proxy check, reduce jobs, etc 2023-09-11 22:29:34 +02:00
Andras Bacsai
42daae10c6 feat: able to invite more people at once 2023-09-11 20:51:31 +02:00
Andras Bacsai
64a65e2018 fix: errors 2023-09-11 17:36:30 +02:00
Andras Bacsai
16c71f3647 fix: old docker version error 2023-09-11 17:19:30 +02:00
Andras Bacsai
363f525ad1 fix: sentry 4469575117 2023-09-11 16:51:38 +02:00
Andras Bacsai
f4fb519d55 internal: add Plausible analytics 2023-09-11 16:41:43 +02:00
Andras Bacsai
b7786504b8 wip: nixpacksarchive 2023-09-11 15:53:05 +02:00
Andras Bacsai
f0adf10e6a version++ 2023-09-11 14:11:22 +02:00
Andras Bacsai
cc8c6c5d16 Merge pull request #1221 from coollabsio/next
UI/UX fixes
2023-09-11 13:59:19 +02:00
Andras Bacsai
4782446f42 ui: show registered users on waitlist page 2023-09-11 13:53:08 +02:00
Andras Bacsai
230155312f ui: services are not availble yet 2023-09-11 13:52:54 +02:00
Andras Bacsai
3ab4365fca ui: user should know that the public key 2023-09-11 13:42:32 +02:00
Andras Bacsai
7349068b95 Merge pull request #1220 from coollabsio/next
Email issues fix
2023-09-11 12:55:52 +02:00
Andras Bacsai
da6cc151d1 fix: email sending error 2023-09-11 12:31:31 +02:00
Andras Bacsai
50527cf0a3 fix: testing email
fix: expired invitiation link
2023-09-11 12:16:26 +02:00
Andras Bacsai
26f490bb00 Merge pull request #1219 from coollabsio/next
Fixes
2023-09-11 10:50:18 +02:00
Andras Bacsai
dc4f412227 fix: recovery code 2023-09-11 10:41:39 +02:00
Andras Bacsai
ec4234e243 fix: only send internal notifcations to enabled channels 2023-09-11 10:19:46 +02:00
Andras Bacsai
f47fcb01ce Merge pull request #1218 from coollabsio/next
feat: generate public key from private keys
2023-09-11 10:16:48 +02:00
Andras Bacsai
02f6673345 feat: generate public key from private keys 2023-09-11 10:15:45 +02:00
Andras Bacsai
e6cd8702b5 Merge pull request #1216 from coollabsio/next
v4.0.0-beta.32
2023-09-10 15:34:08 +02:00
Andras Bacsai
fda4ea8cca fix: errors in views 2023-09-10 15:33:00 +02:00
Andras Bacsai
655d004ce7 Merge pull request #1214 from coollabsio/next
v4.0.0-beta.31
2023-09-09 15:33:16 +02:00
Andras Bacsai
56981d134c fix: improve startProxy action
fix: improve installDocker action and process
feat: add noSubmit prop to custom modal
2023-09-09 15:30:46 +02:00
Andras Bacsai
b9d49d2951 fix: remove -q from docker compose 2023-09-09 13:52:40 +02:00
Andras Bacsai
0c1e7c499e Merge pull request #1213 from coollabsio/next
v4.0.0-beta.30
2023-09-09 13:23:59 +02:00
Andras Bacsai
32fead5753 version++ 2023-09-09 13:23:03 +02:00
Andras Bacsai
e5e9faba35 fix: delete database related things when delete database 2023-09-09 13:18:49 +02:00
Andras Bacsai
2852630d6c Update DatabaseBackupJob.php 2023-09-09 10:34:10 +02:00
Andras Bacsai
a4cc406114 Merge pull request #1212 from coollabsio/next
v4.0.0-beta.29
2023-09-08 18:52:41 +02:00
Andras Bacsai
53b15a5762 update sentry dsn 2023-09-08 18:40:32 +02:00
Andras Bacsai
929a4e6474 fix: coolify already exists should not throw error 2023-09-08 18:40:25 +02:00
Andras Bacsai
45b597bbab feat: cache team settings 2023-09-08 18:33:26 +02:00
Andras Bacsai
0d1a2aa5d1 update testemail command 2023-09-08 17:51:19 +02:00
Andras Bacsai
b82353d5e2 update testemail command 2023-09-08 17:42:08 +02:00
Andras Bacsai
b17c09f7a7 update testemail command 2023-09-08 17:31:02 +02:00
Andras Bacsai
f6c3fe7888 fix: test email on for admins or custom smtp 2023-09-08 17:26:59 +02:00
Andras Bacsai
2e855e030f fix: ui 2023-09-08 16:59:49 +02:00
Andras Bacsai
49f86621f4 fix: instance email settings 2023-09-08 16:56:14 +02:00
Andras Bacsai
03d9f93397 fix: retry notifications 2023-09-08 16:53:19 +02:00
Andras Bacsai
c472042a94 fix: ui 2023-09-08 16:46:53 +02:00
Andras Bacsai
9f4356f67d version++ 2023-09-08 16:28:56 +02:00
Andras Bacsai
a50317cc76 Merge pull request #1209 from coollabsio/next
v4.0.0-beta.28
2023-09-08 16:28:04 +02:00
Andras Bacsai
8afa98a1ca fix: sentry 4451028626 2023-09-08 16:19:45 +02:00
Andras Bacsai
f6737f21dd feat: developer view for env variables 2023-09-08 16:16:59 +02:00
Andras Bacsai
e4a51cc116 fix: sentry 4459819517 2023-09-08 16:16:28 +02:00
Andras Bacsai
acd78ae196 feat: Telegram topics separation 2023-09-08 14:15:28 +02:00
Andras Bacsai
953bcfb5bb fix: db backup job 2023-09-08 12:23:46 +02:00
Andras Bacsai
dacfab8b29 Update BUG_REPORT.yml 2023-09-08 12:08:16 +02:00
Andras Bacsai
48b3e99939 Merge pull request #1203 from coollabsio/next
v4.0.0-beta.27
2023-09-08 12:06:34 +02:00
Andras Bacsai
41ad67c7c9 updates 2023-09-08 12:05:15 +02:00
Andras Bacsai
b49725cb1c fix: bug 2023-09-08 09:37:58 +02:00
Andras Bacsai
75e674a966 version++ 2023-09-08 09:27:44 +02:00
Andras Bacsai
9d826d9fb4 Merge pull request #1202 from coollabsio/next
v4.0.0-beta.26
2023-09-08 09:08:19 +02:00
Andras Bacsai
0af221f9fc udpate 2023-09-08 09:06:00 +02:00
Andras Bacsai
9066c9bf90 update readme 2023-09-08 09:00:44 +02:00
Andras Bacsai
77e3208f00 feat: public database 2023-09-07 13:23:34 +02:00
Andras Bacsai
db5ecf07bd version++ 2023-09-07 09:14:27 +02:00
Andras Bacsai
7f28aa6985 Merge pull request #1201 from coollabsio/next
v4.0.0-beta.25
2023-09-07 09:04:33 +02:00
Andras Bacsai
9d53e04ce9 fix: saveModel email settings 2023-09-07 08:48:16 +02:00
Andras Bacsai
3db9a1dd6e version++ 2023-09-06 15:26:17 +02:00
Andras Bacsai
38f9761b67 Merge pull request #1200 from coollabsio/next
v4.0.0-beta.24
2023-09-06 15:16:44 +02:00
Andras Bacsai
7117ecf634 version++ 2023-09-06 15:16:33 +02:00
Andras Bacsai
522e20f10a proxy updates 2023-09-06 15:00:56 +02:00
Andras Bacsai
ebbce2396c fix: typo 2023-09-06 14:34:35 +02:00
Andras Bacsai
f9a2ff6d90 feat: add discord notifications 2023-09-06 14:31:38 +02:00
Andras Bacsai
df1b9e7319 oops 2023-09-06 12:08:52 +02:00
Andras Bacsai
e7c0c26b32 fix: stripe
add: custom error pages
fix: invititation
feat: new quick login for first users (UX++)
feat: more internal notifications
2023-09-06 12:07:34 +02:00
Andras Bacsai
0dbb8b4420 update name 2023-09-05 16:03:24 +02:00
Andras Bacsai
a4b44bacc1 updates 2023-09-05 15:43:56 +02:00
Andras Bacsai
1338e68b8c wip: backup existing database 2023-09-05 12:14:31 +02:00
Andras Bacsai
f6f4cdde24 new links 2023-09-05 11:53:34 +02:00
Andras Bacsai
3daadf13c6 fix: lowercase image names 2023-09-05 11:05:54 +02:00
Andras Bacsai
cbd5fab2e7 fix 2023-09-05 10:57:49 +02:00
Andras Bacsai
48ccb508f9 update tax collection 2023-09-05 10:49:17 +02:00
Andras Bacsai
67edce0612 update payment webhook 2023-09-05 10:46:36 +02:00
Andras Bacsai
e8a41d7e6e rename command 2023-09-05 10:22:24 +02:00
Andras Bacsai
be1dad03bd fix: do not show system wide git on cloud 2023-09-05 10:03:28 +02:00
Andras Bacsai
31db1db636 next helper image 2023-09-05 08:49:33 +02:00
Andras Bacsai
dca332a688 fix: overlapping apps 2023-09-04 16:59:02 +02:00
Andras Bacsai
35f19ed53f update helper 2023-09-04 16:51:36 +02:00
Andras Bacsai
a5c45ffe90 update pack + nixpacks 2023-09-04 16:50:39 +02:00
Andras Bacsai
b6c3a65d2d update dev build action 2023-09-04 16:48:36 +02:00
Andras Bacsai
220c8211fd update 2023-09-04 16:46:53 +02:00
Andras Bacsai
ffbd04df29 update helper 2023-09-04 16:40:16 +02:00
Andras Bacsai
73c59be865 fix helper 2023-09-04 16:25:18 +02:00
Andras Bacsai
3966abaf80 improve coolify-helper + add test new functionalities 2023-09-04 16:03:11 +02:00
Andras Bacsai
759517316a fix: add docker network to build process 2023-09-04 10:18:30 +02:00
Andras Bacsai
3e3024d47e fix: add navbar for source + keys 2023-09-04 09:44:44 +02:00
Andras Bacsai
517cb77637 update 2023-09-03 11:54:05 +02:00
Andras Bacsai
14bd89a991 update 2023-09-03 11:51:00 +02:00
Andras Bacsai
304de29924 update waitlsit 2023-09-03 11:46:00 +02:00
Andras Bacsai
ab3055150f test distributed application check 2023-09-02 15:53:12 +02:00
Andras Bacsai
6b9c7aa9c5 feat: send request in cloud 2023-09-02 15:37:25 +02:00
Andras Bacsai
040f47b59c fix: show hosted email service, just disable for non pro subs 2023-09-02 13:41:42 +02:00
Andras Bacsai
eac7834083 fix: form address 2023-09-02 13:39:44 +02:00
Andras Bacsai
135a298080 remove line 2023-09-02 13:27:03 +02:00
Andras Bacsai
0065a86371 update seeder 2023-09-01 16:36:41 +02:00
Andras Bacsai
2a842a2f50 Do not schedule autoupdate 2023-09-01 16:30:50 +02:00
Andras Bacsai
231c02e00e version++ 2023-09-01 16:16:35 +02:00
358 changed files with 8860 additions and 3710 deletions

View File

@@ -6,6 +6,7 @@
USERID=
GROUPID=
############################################################################################################
APP_NAME=Coolify-localhost
APP_ID=development
APP_ENV=local
APP_KEY=

26
.github/ISSUE_TEMPLATE/BUG_REPORT.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Bug report
description: Create a new bug report
title: '[Bug]: '
body:
- type: textarea
attributes:
label: Description
description: A clear and concise description of the problem
validations:
required: true
- type: textarea
attributes:
label: Minimal Reproduction (if possible, example repository)
description: Please provide a step by step guide to reproduce the issue
validations:
required: true
- type: textarea
attributes:
label: Exception or Error
description: Please provide error logs if possible.
- type: input
attributes:
label: Version
description: Coolify's version (see bottom left corner).
validations:
required: true

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

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 🤔 Community Support (Chat)
url: https://coollabs.io/discord
about: Reach out to us on Discord.
- name: 🙋‍♂️ Feature Requests
url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests-ideas
about: All feature requests will be discussed here.

View File

@@ -0,0 +1,84 @@
name: Coolify Helper Image Development (v4)
on:
push:
branches: [ "next" ]
paths:
- .github/workflows/coolify-helper-next.yml
- docker/coolify-helper/Dockerfile
env:
REGISTRY: ghcr.io
IMAGE_NAME: "coollabsio/coolify-helper"
jobs:
amd64:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Login to ghcr.io
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image and push to registry
uses: docker/build-push-action@v3
with:
no-cache: true
context: .
file: docker/coolify-helper/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Login to ghcr.io
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image and push to registry
uses: docker/build-push-action@v3
with:
no-cache: true
context: .
file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: [ amd64, aarch64 ]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to ghcr.io
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}

View File

@@ -2,7 +2,7 @@ name: Coolify Helper Image (v4)
on:
push:
branches: [ "main", "next" ]
branches: [ "main" ]
paths:
- .github/workflows/coolify-helper.yml
- docker/coolify-helper/Dockerfile
@@ -55,7 +55,7 @@ jobs:
file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:aarch64
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
merge-manifest:
runs-on: ubuntu-latest
permissions:
@@ -77,4 +77,8 @@ 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 }}:latest-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}

View File

@@ -3,6 +3,9 @@ name: Development Build (v4)
on:
push:
branches: ["next"]
paths-ignore:
- .github/workflows/coolify-helper.yml
- docker/coolify-helper/Dockerfile
env:
REGISTRY: ghcr.io
@@ -10,7 +13,7 @@ env:
jobs:
amd64:
runs-on: ubuntu-latest
runs-on: [self-hosted, x64]
steps:
- uses: actions/checkout@v3
- name: Login to ghcr.io
@@ -73,4 +76,4 @@ jobs:
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}

View File

@@ -10,7 +10,7 @@ env:
jobs:
amd64:
runs-on: ubuntu-latest
runs-on: [self-hosted, x64]
steps:
- uses: actions/checkout@v3
- name: Login to ghcr.io

1
.gitignore vendored
View File

@@ -31,3 +31,4 @@ _ide_helper.php
_ide_helper_models.php
.rnd
/.ssh
scripts/load-test/*

View File

@@ -1,20 +1,30 @@
# Coolify v4 Beta
# About the Project
An open-source & self-hostable Heroku / Netlify alternative.
Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.
It helps you to manage your servers, applications, databases on your own hardware, all you need is SSH connection. You can manage VPS, Bare Metal, Raspberry PI's anything.
Image if you could have the ease of a cloud but with your own servers. That is **Coolify**.
No vendor lock-in, which means that all the configuration for your applications/databases/etc are saved to your server. So if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You just lose the automations and all the magic. 🪄️
For more information, take a look at our landing page [here](https://coolify.io).
> If you are looking for previous (v3) version, it is [here](https://github.com/coollabsio/coolify/tree/v3).
# Cloud
If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io
You can easily attach your own servers, get all the automations, free email notifications, etc.
For more information & pricing, take a look at our landing page [here](https://coolify.io).
# Beta
You are checking the next-gen of Coolify, aka v4. Hi 👋
The latest version (v4) is still in beta. That does not mean it is unstable. All the features that are available are stable enough be usable in real-life.
It is still in beta, lots of improvements will come every day. Things could break, but we are working hard to make it stable as soon as possible. If you find any bugs, please report them.
Automatic updates are available, so you will receive the latest version as soon as it is released.
If you are looking for v3, check out the [v3 branch](https://github.com/coollabsio/coolify/tree/v3).
## What's new?
Well, the whole tech stack changed, core is different, so yeah, a lot (documentation incoming).
There are hundreds of people using it for managing their client's applications, freelancers, hobbyists, businesses.
# Installation
@@ -26,13 +36,19 @@ You can find the installation script [here](./scripts/install.sh).
## Support
- Twitter: [@heyandras](https://twitter.com/heyandras)
- Mastodon: [@andrasbacsai@fosstodon.org](https://fosstodon.org/@andrasbacsai)
- Email: [andras@coollabs.io](mailto:andras@coollabs.io)
- Discord: [Invitation](https://coollabs.io/discord)
- Telegram: [@andrasbacsai](https://t.me/andrasbacsai)
Contact us [here](https://docs.coollabs.io/contact).
---
## Recognitions
<a href="https://news.ycombinator.com/item?id=26624341">
<img
style="width: 250px; height: 54px;" width="250" height="54"
alt="Featured on Hacker News"
src="https://hackernews-badge.vercel.app/api?id=26624341"
/>
</a>
<a href="https://www.producthunt.com/posts/coolify?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-coolify" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=338273&theme=light" alt="Coolify - An&#0032;open&#0045;source&#0032;&#0038;&#0032;self&#0045;hostable&#0032;Heroku&#0044;&#0032;Netlify&#0032;alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
## 💰 Financial Contributors

View File

@@ -5,14 +5,12 @@ namespace App\Actions\CoolifyTask;
use App\Enums\ActivityTypes;
use App\Enums\ProcessStatus;
use App\Jobs\ApplicationDeploymentJob;
use App\Models\Server;
use Illuminate\Process\ProcessResult;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Process;
use Spatie\Activitylog\Models\Activity;
const TIMEOUT = 3600;
const IDLE_TIMEOUT = 3600;
class RunRemoteProcess
{
public Activity $activity;
@@ -76,8 +74,7 @@ class RunRemoteProcess
$this->time_start = hrtime(true);
$status = ProcessStatus::IN_PROGRESS;
$processResult = Process::timeout(TIMEOUT)->idleTimeout(IDLE_TIMEOUT)->run($this->getCommand(), $this->handleOutput(...));
$processResult = Process::forever()->run($this->getCommand(), $this->handleOutput(...));
if ($this->activity->properties->get('status') === ProcessStatus::ERROR->value) {
$status = ProcessStatus::ERROR;
@@ -97,9 +94,8 @@ class RunRemoteProcess
'status' => $status->value,
]);
$this->activity->save();
if ($processResult->exitCode() != 0 && !$this->ignore_errors) {
throw new \RuntimeException($processResult->errorOutput());
throw new \RuntimeException($processResult->errorOutput(), $processResult->exitCode());
}
return $processResult;
@@ -107,13 +103,11 @@ class RunRemoteProcess
protected function getCommand(): string
{
$user = $this->activity->getExtraProperty('user');
$server_ip = $this->activity->getExtraProperty('server_ip');
$private_key_location = $this->activity->getExtraProperty('private_key_location');
$port = $this->activity->getExtraProperty('port');
$server_uuid = $this->activity->getExtraProperty('server_uuid');
$command = $this->activity->getExtraProperty('command');
$server = Server::whereUuid($server_uuid)->firstOrFail();
return generate_ssh_command($private_key_location, $server_ip, $user, $port, $command);
return generateSshCommand($server, $command);
}
protected function handleOutput(string $type, string $output)

View File

@@ -21,6 +21,7 @@ class StartPostgresql
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/"
];
@@ -65,7 +66,7 @@ class StartPostgresql
],
'networks' => [
$this->database->destination->network => [
'external' => false,
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]
@@ -96,6 +97,7 @@ class StartPostgresql
$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";
$this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $server);
}

View File

@@ -57,13 +57,13 @@ class CheckResaleLicense
throw new \Exception('Invalid license key.');
}
throw new \Exception('Cannot activate license key.');
} catch (\Throwable $th) {
ray($th);
} catch (\Throwable $e) {
ray($e);
$settings->update([
'resale_license' => null,
'is_resale_license_active' => false,
]);
throw $th;
throw $e;
}
}
}

View File

@@ -2,24 +2,27 @@
namespace App\Actions\Proxy;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Server;
use Illuminate\Support\Str;
class CheckConfigurationSync
class CheckConfiguration
{
public function __invoke(Server $server, bool $reset = false)
use AsAction;
public function handle(Server $server, bool $reset = false)
{
$proxy_path = get_proxy_path();
$proxy_configuration = instant_remote_process([
"mkdir -p $proxy_path",
"cat $proxy_path/docker-compose.yml",
], $server, false);
if ($reset || is_null($proxy_configuration)) {
if ($reset || !$proxy_configuration || is_null($proxy_configuration)) {
$proxy_configuration = Str::of(generate_default_proxy_configuration($server))->trim()->value;
resolve(SaveConfigurationSync::class)($server, $proxy_configuration);
return $proxy_configuration;
}
if (!$proxy_configuration || is_null($proxy_configuration)) {
throw new \Exception("Could not generate proxy configuration");
}
return $proxy_configuration;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Actions\Proxy;
use App\Models\Server;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
class SaveConfiguration
{
use AsAction;
public function handle(Server $server, ?string $proxy_settings = null)
{
if (is_null($proxy_settings)) {
$proxy_settings = CheckConfiguration::run($server, true);
}
$proxy_path = get_proxy_path();
$docker_compose_yml_base64 = base64_encode($proxy_settings);
$server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
return instant_remote_process([
"mkdir -p $proxy_path",
"echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml",
], $server);
}
}

View File

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

@@ -2,49 +2,64 @@
namespace App\Actions\Proxy;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\Activitylog\Models\Activity;
class StartProxy
{
public function __invoke(Server $server): Activity
use AsAction;
public function handle(Server $server, bool $async = true): Activity|string
{
$proxy_path = get_proxy_path();
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];
})->unique();
if ($networks->count() === 0) {
$networks = collect(['coolify']);
$commands = collect([]);
$proxyType = $server->proxyType();
if ($proxyType === 'none') {
return 'OK';
}
$create_networks_command = $networks->map(function ($network) {
return "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null 2>&1 || docker network create --attachable $network > /dev/null 2>&1";
});
$configuration = resolve(CheckConfigurationSync::class)($server);
$proxy_path = get_proxy_path();
$configuration = CheckConfiguration::run($server);
if (!$configuration) {
throw new \Exception("Configuration is not synced");
}
SaveConfiguration::run($server, $configuration);
$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,
"cd $proxy_path",
"echo 'Creating Docker Compose file...'",
"echo 'Pulling docker image...'",
'docker compose pull -q',
"echo 'Stopping existing proxy...'",
'docker compose down -v --remove-orphans',
"lsof -nt -i:80 | xargs -r kill -9",
"lsof -nt -i:443 | xargs -r kill -9",
"echo 'Starting proxy...'",
$commands = $commands->merge([
"apt-get update > /dev/null 2>&1 || true",
"command -v lsof >/dev/null || echo '####### Installing lsof.'",
"command -v lsof >/dev/null || apt install -y lsof",
"command -v lsof >/dev/null || command -v fuser >/dev/null || apt install -y psmisc",
"mkdir -p $proxy_path && cd $proxy_path",
"echo '####### Creating Docker Compose file.'",
"echo '####### Pulling docker image.'",
'docker compose pull',
"echo '####### Stopping existing coolify-proxy.'",
"docker compose down -v --remove-orphans > /dev/null 2>&1",
"command -v fuser >/dev/null || command -v lsof >/dev/null || echo '####### Could not kill existing processes listening on port 80 & 443. Please stop the process holding these ports...'",
"command -v lsof >/dev/null && lsof -nt -i:80 | xargs -r kill -9 || true",
"command -v lsof >/dev/null && lsof -nt -i:443 | xargs -r kill -9 || true",
"command -v fuser >/dev/null && fuser -k 80/tcp || true",
"command -v fuser >/dev/null && fuser -k 443/tcp || true",
"systemctl disable nginx > /dev/null 2>&1 || true",
"systemctl disable apache2 > /dev/null 2>&1 || true",
"systemctl disable apache > /dev/null 2>&1 || true",
"echo '####### Starting coolify-proxy.'",
'docker compose up -d --remove-orphans',
"echo 'Proxy installed successfully...'"
], $server);
return $activity;
"echo '####### Proxy installed successfully.'"
]);
$commands = $commands->merge(connectProxyToNetworks($server));
if ($async) {
$activity = remote_process($commands, $server);
return $activity;
} else {
instant_remote_process($commands, $server);
$server->proxy->set('status', 'running');
$server->proxy->set('type', $proxyType);
$server->save();
return 'OK';
}
}
}

View File

@@ -4,50 +4,57 @@ namespace App\Actions\Server;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\Team;
class InstallDocker
{
public function __invoke(Server $server, Team $team)
public function __invoke(Server $server)
{
$dockerVersion = '23.0';
$config = base64_encode('{ "live-restore": true }');
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);
$dockerVersion = '24.0';
$config = base64_encode('{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}');
$found = StandaloneDocker::where('server_id', $server->id);
if ($found->count() == 0 && $server->id) {
StandaloneDocker::create([
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,
]);
}
if (isDev() && $server->id === 0) {
$command = [
"echo '####### Installing Prerequisites...'",
"sleep 1",
"echo '####### Installing/updating Docker Engine...'",
"echo '####### Configuring Docker Engine (merging existing configuration with the required)...'",
"sleep 4",
"echo '####### Restarting Docker Engine...'",
"ls -l /tmp"
];
} else {
$activity = remote_process([
"echo ####### Installing Prerequisites...",
$command = [
"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...",
"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)...",
"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...",
"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,
]);
}
"echo '####### Creating default Docker network (coolify)...'",
"docker network create --attachable coolify >/dev/null 2>&1 || true",
"echo '####### Done!'"
];
}
return $activity;
return remote_process($command, $server);
}
}

View File

@@ -16,8 +16,7 @@ class UpdateCoolify
try {
$settings = InstanceSettings::get();
ray('Running InstanceAutoUpdateJob');
$localhost_name = 'localhost';
$this->server = Server::where('name', $localhost_name)->first();
$this->server = Server::find(0)->first();
if (!$this->server) {
return;
}
@@ -43,11 +42,11 @@ class UpdateCoolify
$this->update();
}
send_internal_notification('InstanceAutoUpdateJob done to version: ' . $this->latestVersion . ' from version: ' . $this->currentVersion);
} catch (\Exception $th) {
} catch (\Throwable $e) {
ray('InstanceAutoUpdateJob failed');
ray($th->getMessage());
send_internal_notification('InstanceAutoUpdateJob failed: ' . $th->getMessage());
throw $th;
ray($e->getMessage());
send_internal_notification('InstanceAutoUpdateJob failed: ' . $e->getMessage());
throw $e;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Actions\Service;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Service;
class StartService
{
use AsAction;
public function handle(Service $service)
{
$service->saveComposeConfigs();
$commands[] = "cd " . $service->workdir();
$commands[] = "echo '####### Saved configuration files to {$service->workdir()}.'";
$commands[] = "echo '####### Creating Docker network.'";
$commands[] = "docker network create --attachable {$service->uuid} >/dev/null 2>/dev/null || true";
$commands[] = "echo '####### Starting service {$service->name} on {$service->server->name}.'";
$commands[] = "echo '####### Pulling images.'";
$commands[] = "docker compose pull";
$commands[] = "echo '####### Starting containers.'";
$commands[] = "docker compose up -d --remove-orphans --force-recreate";
$commands[] = "docker network connect $service->uuid coolify-proxy 2>/dev/null || true";
$activity = remote_process($commands, $service->server);
return $activity;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Actions\Service;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Service;
class StopService
{
use AsAction;
public function handle(Service $service)
{
$applications = $service->applications()->get();
foreach ($applications as $application) {
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server);
$application->update(['status' => 'exited']);
}
$dbs = $service->databases()->get();
foreach ($dbs as $db) {
instant_remote_process(["docker rm -f {$db->name}-{$service->uuid}"], $service->server);
$db->update(['status' => 'exited']);
}
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false);
instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false);
}
}

View File

@@ -0,0 +1,280 @@
<?php
namespace App\Console\Commands;
use App\Jobs\SendConfirmationForWaitlistJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use App\Models\Team;
use App\Models\TeamInvitation;
use App\Models\User;
use App\Models\Waitlist;
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 Illuminate\Support\Str;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\select;
use function Laravel\Prompts\text;
class Emails extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'emails';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send out test / prod emails';
/**
* Execute the console command.
*/
private ?MailMessage $mail = null;
private ?string $email = null;
public function handle()
{
$type = select(
'Which Email should be sent?',
options: [
'updates' => 'Send Update Email to all users',
'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',
'realusers-before-trial' => 'REAL - Registered Users Before Trial without Subscription',
'realusers-server-lost-connection' => 'REAL - Server Lost Connection',
],
);
$emailsGathered = ['realusers-before-trial', 'realusers-server-lost-connection'];
if (!in_array($type, $emailsGathered)) {
$this->email = text('Email Address to send to');
}
set_transanctional_email_settings();
$this->mail = new MailMessage();
$this->mail->subject("Test Email");
switch ($type) {
case 'updates':
$teams = Team::all();
if (!$teams || $teams->isEmpty()) {
echo 'No teams found.' . PHP_EOL;
return;
}
$emails = [];
foreach ($teams as $team) {
foreach ($team->members as $member) {
if ($member->email && $member->marketing_emails) {
$emails[] = $member->email;
}
}
}
$emails = array_unique($emails);
$this->info("Sending to " . count($emails) . " emails.");
foreach ($emails as $email) {
$this->info($email);
}
$confirmed = confirm('Are you sure?');
if ($confirmed) {
foreach ($emails as $email) {
$this->mail = new MailMessage();
$this->mail->subject('One-click Services, Docker Compose support');
$unsubscribeUrl = route('unsubscribe.marketing.emails', [
'token' => encrypt($email),
]);
$this->mail->view('emails.updates',["unsubscribeUrl" => $unsubscribeUrl]);
$this->sendEmail($email);
}
}
break;
case 'emails-test':
$this->mail = (new Test())->toMail();
$this->sendEmail();
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', [
'loginLink' => 'https://coolify.io',
]);
$this->mail->subject('Congratulations! You are invited to join Coolify Cloud.');
$this->sendEmail();
break;
case 'waitlist-confirmation':
$found = Waitlist::where('email', $this->email)->first();
if ($found) {
SendConfirmationForWaitlistJob::dispatch($this->email, $found->uuid);
} else {
throw new Exception('Waitlist not found');
}
break;
case 'realusers-before-trial':
$this->mail = new MailMessage();
$this->mail->view('emails.before-trial-conversion');
$this->mail->subject('Trial period has been added for all subscription plans.');
$teams = Team::doesntHave('subscription')->where('id', '!=', 0)->get();
if (!$teams || $teams->isEmpty()) {
echo 'No teams found.' . PHP_EOL;
return;
}
$emails = [];
foreach ($teams as $team) {
foreach ($team->members as $member) {
if ($member->email) {
$emails[] = $member->email;
}
}
}
$emails = array_unique($emails);
$this->info("Sending to " . count($emails) . " emails.");
foreach ($emails as $email) {
$this->info($email);
}
$confirmed = confirm('Are you sure?');
if ($confirmed) {
foreach ($emails as $email) {
$this->sendEmail($email);
}
}
break;
case 'realusers-server-lost-connection':
$serverId = text('Server Id');
$server = Server::find($serverId);
if (!$server) {
throw new Exception('Server not found');
}
$admins = [];
$members = $server->team->members;
foreach ($members as $member) {
if ($member->isAdmin()) {
$admins[] = $member->email;
}
}
$this->info('Sending to ' . count($admins) . ' admins.');
foreach ($admins as $admin) {
$this->info($admin);
}
$this->mail = new MailMessage();
$this->mail->view('emails.server-lost-connection', [
'name' => $server->name,
]);
$this->mail->subject('Action required: Server ' . $server->name . ' lost connection.');
foreach ($admins as $email) {
$this->sendEmail($email);
}
break;
}
}
private function sendEmail(string $email = null)
{
if ($email) {
$this->email = $email;
}
Mail::send(
[],
[],
fn (Message $message) => $message
->to($this->email)
->subject($this->mail->subject)
->html((string)$this->mail->render())
);
$this->info("Email sent to $this->email successfully. 📧");
}
}

View File

@@ -26,7 +26,7 @@ class Init extends Command
$deployment->status = ApplicationDeploymentStatus::FAILED->value;
$deployment->save();
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
}
}

View File

@@ -7,6 +7,8 @@ use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Pool;
use Illuminate\Support\Facades\Http;
use function Laravel\Prompts\confirm;
class SyncBunny extends Command
{
/**
@@ -14,7 +16,7 @@ class SyncBunny extends Command
*
* @var string
*/
protected $signature = 'sync:bunny';
protected $signature = 'sync:bunny {--only-template} {--only-version}';
/**
* The console command description.
@@ -28,6 +30,9 @@ class SyncBunny extends Command
*/
public function handle()
{
$that = $this;
$only_template = $this->option('only-template');
$only_version = $this->option('only-version');
$bunny_cdn = "https://cdn.coollabs.io";
$bunny_cdn_path = "coolify";
$bunny_cdn_storage_name = "coolcdn";
@@ -39,51 +44,71 @@ class SyncBunny extends Command
$install_script = "install.sh";
$upgrade_script = "upgrade.sh";
$production_env = ".env.production";
$service_template = "service-templates.json";
$versions = "versions.json";
PendingRequest::macro('storage', function ($file) {
PendingRequest::macro('storage', function ($fileName) use($that) {
$headers = [
'AccessKey' => env('BUNNY_STORAGE_API_KEY'),
'Accept' => 'application/json',
'Content-Type' => 'application/octet-stream'
];
$fileStream = fopen($file, "r");
$file = fread($fileStream, filesize($file));
$fileStream = fopen($fileName, "r");
$file = fread($fileStream, filesize($fileName));
$that->info('Uploading: ' . $fileName);
return PendingRequest::baseUrl('https://storage.bunnycdn.com')->withHeaders($headers)->withBody($file)->throw();
});
PendingRequest::macro('purge', function ($url) {
PendingRequest::macro('purge', function ($url) use ($that) {
$headers = [
'AccessKey' => env('BUNNY_API_KEY'),
'Accept' => 'application/json',
];
ray('Purging: ' . $url);
$that->info('Purging: ' . $url);
return PendingRequest::withHeaders($headers)->get('https://api.bunny.net/purge', [
"url" => $url,
"async" => false
]);
});
try {
$confirmed = confirm('Are you sure you want to sync?');
if (!$confirmed) {
return;
}
if ($only_template) {
Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: "$parent_dir/templates/$service_template")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$service_template"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$service_template"),
]);
$this->info('Service template uploaded & purged...');
return;
}
if ($only_version) {
Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: "$parent_dir/$versions")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
]);
$this->info('versions.json uploaded & purged...');
return;
}
Http::pool(fn (Pool $pool) => [
$pool->storage(file: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
$pool->storage(file: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),
$pool->storage(file: "$parent_dir/$production_env")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$production_env"),
$pool->storage(file: "$parent_dir/scripts/$upgrade_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_script"),
$pool->storage(file: "$parent_dir/scripts/$install_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$install_script"),
$pool->storage(file: "$parent_dir/$versions")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
$pool->storage(fileName: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
$pool->storage(fileName: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),
$pool->storage(fileName: "$parent_dir/$production_env")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$production_env"),
$pool->storage(fileName: "$parent_dir/scripts/$upgrade_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_script"),
$pool->storage(fileName: "$parent_dir/scripts/$install_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$install_script"),
]);
ray("{$bunny_cdn}/{$bunny_cdn_path}");
Http::pool(fn (Pool $pool) => [
$pool->purge("$bunny_cdn/$bunny_cdn_path/$compose_file"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$compose_file_prod"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$production_env"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$upgrade_script"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$install_script"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
]);
echo "All files uploaded & purged...\n";
} catch (\Exception $e) {
echo $e->getMessage();
$this->info("All files uploaded & purged...");
} catch (\Throwable $e) {
$this->error("Error: " . $e->getMessage());
}
}
}

View File

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

@@ -6,20 +6,20 @@ use App\Models\User;
use App\Models\Waitlist;
use Illuminate\Console\Command;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class InviteFromWaitlist extends Command
class WaitlistInvite extends Command
{
public Waitlist|null $next_patient = null;
public User|null $new_user = null;
public Waitlist|User|null $next_patient = null;
public string|null $password = null;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:invite-from-waitlist {email?}';
protected $signature = 'waitlist:invite {--people=1} {--only-email} {email?}';
/**
* The console command description.
@@ -33,8 +33,23 @@ class InviteFromWaitlist extends Command
*/
public function handle()
{
$people = $this->option('people');
for ($i = 0; $i < $people; $i++) {
$this->main();
}
}
private function main() {
if ($this->argument('email')) {
$this->next_patient = Waitlist::where('email', $this->argument('email'))->first();
if ($this->option('only-email')) {
$this->next_patient = User::whereEmail($this->argument('email'))->first();
$this->password = Str::password();
$this->next_patient->update([
'password' => Hash::make($this->password),
'force_password_reset' => true,
]);
} else {
$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;
@@ -43,6 +58,10 @@ class InviteFromWaitlist extends Command
$this->next_patient = Waitlist::orderBy('created_at', 'asc')->where('verified', true)->first();
}
if ($this->next_patient) {
if ($this->option('only-email')) {
$this->send_email();
return;
}
$this->register_user();
$this->remove_from_waitlist();
$this->send_email();
@@ -55,7 +74,7 @@ class InviteFromWaitlist extends Command
$already_registered = User::whereEmail($this->next_patient->email)->first();
if (!$already_registered) {
$this->password = Str::password();
$this->new_user = User::create([
User::create([
'name' => Str::of($this->next_patient->email)->before('@'),
'email' => $this->next_patient->email,
'password' => Hash::make($this->password),
@@ -73,10 +92,11 @@ class InviteFromWaitlist extends Command
}
private function send_email()
{
$token = Crypt::encryptString("{$this->next_patient->email}@@@$this->password");
$loginLink = route('auth.link', ['token' => $token]);
$mail = new MailMessage();
$mail->view('emails.waitlist-invitation', [
'email' => $this->next_patient->email,
'password' => $this->password,
'loginLink' => $loginLink,
]);
$mail->subject('Congratulations! You are invited to join Coolify Cloud.');
send_user_an_email($mail, $this->next_patient->email);

View File

@@ -7,9 +7,10 @@ use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob;
use App\Jobs\InstanceAutoUpdateJob;
use App\Jobs\ProxyCheckJob;
use App\Jobs\ResourceStatusJob;
use App\Jobs\ContainerStatusJob;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -17,28 +18,42 @@ 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 ContainerStatusJob(Server::find(0)))->everyTenMinutes()->onOneServer();
// $schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
// $schedule->job(new CheckResaleLicenseJob)->hourly();
$schedule->job(new DockerCleanupJob)->everyOddHour();
// $schedule->job(new InstanceAutoUpdateJob(true))->everyMinute();
// $schedule->job(new DockerCleanupJob)->everyOddHour();
// $this->instance_auto_update($schedule);
// $this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
} else {
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
$schedule->job(new ResourceStatusJob)->everyMinute()->onOneServer();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->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->instance_auto_update($schedule);
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
}
}
private function check_resources($schedule)
{
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true);
foreach ($servers as $server) {
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
}
}
private function instance_auto_update($schedule)
{
if (isDev()) {
return;
}
$settings = InstanceSettings::get();
if ($settings->is_auto_update_enabled) {
$schedule->job(new InstanceAutoUpdateJob)->everyTenMinutes()->onOneServer();
}
$this->check_scheduled_backups($schedule);
}
private function check_scheduled_backups($schedule)
{
ray('check_scheduled_backups');
@@ -51,13 +66,18 @@ class Kernel extends ConsoleKernel
if (!$scheduled_backup->enabled) {
continue;
}
if (is_null(data_get($scheduled_backup, 'database'))) {
ray('database not found');
$scheduled_backup->delete();
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);
))->cron($scheduled_backup->frequency)->onOneServer();
}
}

View File

@@ -12,16 +12,16 @@ use Spatie\LaravelData\Data;
class CoolifyTaskArgs extends Data
{
public function __construct(
public string $server_ip,
public string $private_key_location,
public string $server_uuid,
public string $command,
public int $port,
public string $user,
public string $type,
public ?string $type_uuid = null,
public ?Model $model = null,
public string $status = ProcessStatus::QUEUED->value,
public ?string $status = null ,
public bool $ignore_errors = false,
) {
if(is_null($status)){
$this->status = ProcessStatus::QUEUED->value;
}
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Enums;
enum ProxyTypes: string
{
case NONE = 'NONE';
case TRAEFIK_V2 = 'TRAEFIK_V2';
case NGINX = 'NGINX';
case CADDY = 'CADDY';

View File

@@ -3,6 +3,7 @@
namespace App\Exceptions;
use App\Models\InstanceSettings;
use App\Models\User;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Sentry\Laravel\Integration;
use Sentry\State\Scope;
@@ -25,7 +26,7 @@ class Handler extends ExceptionHandler
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [
//
ProcessException::class
];
/**
* A list of the inputs that are never flashed to the session on validation exceptions.
@@ -45,13 +46,23 @@ class Handler extends ExceptionHandler
public function register(): void
{
$this->reportable(function (Throwable $e) {
if (isDev()) {
return;
}
$this->settings = InstanceSettings::get();
if ($this->settings->do_not_track || isDev()) {
if ($this->settings->do_not_track) {
return;
}
app('sentry')->configureScope(
function (Scope $scope){
$scope->setUser(['id'=> config('sentry.server_name')]);
function (Scope $scope) {
$email = auth()?->user() ? auth()->user()->email : 'guest';
$instanceAdmin = User::find(0)->email ?? 'admin@localhost';
$scope->setUser(
[
'email' => $email,
'instanceAdmin' => $instanceAdmin
]
);
}
);
Integration::captureUnhandledException($e);

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class ProcessException extends Exception
{
}

View File

@@ -3,7 +3,6 @@
namespace App\Http\Controllers;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\S3Storage;
use App\Models\StandalonePostgresql;
use App\Models\TeamInvitation;
@@ -11,12 +10,42 @@ use App\Models\User;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Throwable;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
public function link()
{
$token = request()->get('token');
if ($token) {
$decrypted = Crypt::decryptString($token);
$email = Str::of($decrypted)->before('@@@');
$password = Str::of($decrypted)->after('@@@');
$user = User::whereEmail($email)->first();
if (!$user) {
return redirect()->route('login');
}
if (Hash::check($password, $user->password)) {
$invitation = TeamInvitation::whereEmail($email);
if ($invitation->exists()) {
$team = $invitation->first()->team;
$user->teams()->attach($team->id, ['role' => $invitation->first()->role]);
$invitation->delete();
} else {
$team = $user->teams()->first();
}
Auth::login($user);
session(['currentTeam' => $team]);
return redirect()->route('dashboard');
}
}
return redirect()->route('login')->with('error', 'Invalid credentials.');
}
public function subscription()
{
if (!isCloud()) {
@@ -37,10 +66,12 @@ class Controller extends BaseController
]);
}
public function force_passoword_reset() {
public function force_passoword_reset()
{
return view('auth.force-password-reset');
}
public function boarding() {
public function boarding()
{
if (currentTeam()->boarding || isDev()) {
return view('boarding');
} else {
@@ -109,25 +140,21 @@ class Controller extends BaseController
try {
$invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail();
$user = User::whereEmail($invitation->email)->firstOrFail();
if (is_null(auth()->user())) {
return redirect()->route('login');
}
if (auth()->user()->id !== $user->id) {
abort(401);
}
$createdAt = $invitation->created_at;
$diff = $createdAt->diffInMinutes(now());
if ($diff <= config('constants.invitation.link.expiration')) {
$invitationValid = $invitation->isValid();
if ($invitationValid) {
$user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
refreshSession($invitation->team);
$invitation->delete();
return redirect()->route('team.index');
} else {
$invitation->delete();
abort(401);
}
} catch (Throwable $th) {
throw $th;
} catch (\Throwable $e) {
ray($e->getMessage());
throw $e;
}
}
@@ -144,8 +171,8 @@ class Controller extends BaseController
}
$invitation->delete();
return redirect()->route('team.index');
} catch (Throwable $th) {
throw $th;
} catch (\Throwable $e) {
throw $e;
}
}
}

View File

@@ -2,8 +2,12 @@
namespace App\Http\Controllers;
use App\Models\EnvironmentVariable;
use App\Models\Project;
use App\Models\Server;
use App\Models\Service;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class ProjectController extends Controller
{
@@ -41,9 +45,10 @@ class ProjectController extends Controller
public function new()
{
$type = request()->query('type');
$services = getServiceTemplates();
$type = Str::of(request()->query('type'));
$destination_uuid = request()->query('destination');
$server = requesT()->query('server');
$server_id = request()->query('server_id');
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
@@ -61,11 +66,71 @@ class ProjectController extends Controller
'database_uuid' => $standalone_postgresql->uuid,
]);
}
if ($server) {
if ($type->startsWith('one-click-service-') && !is_null( (int)$server_id)) {
$oneClickServiceName = $type->after('one-click-service-')->value();
$oneClickService = data_get($services, "$oneClickServiceName.compose");
ray($oneClickServiceName);
$oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
if ($oneClickDotEnvs) {
$oneClickDotEnvs = Str::of(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/');
}
if ($oneClickService) {
$service = Service::create([
'name' => "$oneClickServiceName-" . Str::random(10),
'docker_compose_raw' => base64_decode($oneClickService),
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
]);
$service->name = "$oneClickServiceName-" . $service->uuid;
$service->save();
if ($oneClickDotEnvs?->count() > 0) {
$oneClickDotEnvs->each(function ($value) use ($service) {
$key = Str::before($value, '=');
$value = Str::of(Str::after($value, '='));
$generatedValue = $value;
if ($value->contains('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_');
switch ($command->value()) {
case 'PASSWORD':
$generatedValue = Str::password(symbols: false);
break;
case 'PASSWORD_64':
$generatedValue = Str::password(length: 64, symbols: false);
break;
case 'BASE64_64':
$generatedValue = Str::random(64);
break;
case 'BASE64_128':
$generatedValue = Str::random(128);
break;
case 'BASE64':
$generatedValue = Str::random(32);
break;
case 'USER':
$generatedValue = Str::random(16);
break;
}
}
EnvironmentVariable::create([
'key' => $key,
'value' => $generatedValue,
'service_id' => $service->id,
'is_build_time' => false,
'is_preview' => false,
]);
});
}
$service->parse(isNew: true);
return redirect()->route('project.service', [
'service_uuid' => $service->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
]);
}
}
return view('project.new', [
'type' => $type
'type' => $type->value()
]);
}

View File

@@ -6,6 +6,7 @@ use App\Actions\Server\InstallDocker;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Support\Collection;
use Livewire\Component;
@@ -13,6 +14,7 @@ class Index extends Component
{
public string $currentState = 'welcome';
public ?string $selectedServerType = null;
public ?Collection $privateKeys = null;
public ?int $selectedExistingPrivateKey = null;
public ?string $privateKeyType = null;
@@ -35,6 +37,11 @@ class Index extends Component
public ?int $selectedExistingProject = null;
public ?Project $createdProject = null;
public bool $dockerInstallationStarted = false;
public string $serverPublicKey;
public bool $serverReachable = true;
public function mount()
{
$this->privateKeyName = generate_random_name();
@@ -52,40 +59,39 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->remoteServerHost = 'coolify-testing-host';
}
}
public function welcome() {
public function explanation()
{
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([
Team::find(currentTeam()->id)->update([
'show_boarding' => false
]);
ray(currentTeam());
refreshSession();
return redirect()->route('dashboard');
}
public function setServerType(string $type)
{
if ($type === 'localhost') {
$this->selectedServerType = $type;
if ($this->selectedServerType === '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->serverPublicKey = $this->createdServer->privateKey->publicKey();
return $this->validateServer('localhost');
} elseif ($this->selectedServerType === 'remote') {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
if ($this->privateKeys->count() > 0) {
$this->selectedExistingPrivateKey = $this->privateKeys->first()->id;
@@ -108,11 +114,11 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
return;
}
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
$this->validateServer();
$this->getProxyType();
$this->getProjects();
}
public function getProxyType() {
public function getProxyType()
{
$proxyTypeSet = $this->createdServer->proxy->type;
if (!$proxyTypeSet) {
$this->currentState = 'select-proxy';
@@ -122,6 +128,8 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
}
public function selectExistingPrivateKey()
{
$this->createdPrivateKey = PrivateKey::find($this->selectedExistingPrivateKey);
$this->privateKey = $this->createdPrivateKey->private_key;
$this->currentState = 'create-server';
}
public function createNewServer()
@@ -133,7 +141,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
{
$this->selectedExistingPrivateKey = null;
$this->privateKeyType = $type;
if ($type === 'create' && !isDev()) {
if ($type === 'create') {
$this->createNewPrivateKey();
}
$this->currentState = 'create-private-key';
@@ -144,6 +152,13 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
'privateKeyName' => 'required',
'privateKey' => 'required',
]);
$this->createdPrivateKey = PrivateKey::create([
'name' => $this->privateKeyName,
'description' => $this->privateKeyDescription,
'private_key' => $this->privateKey,
'team_id' => currentTeam()->id
]);
$this->createdPrivateKey->save();
$this->currentState = 'create-server';
}
public function saveServer()
@@ -151,16 +166,14 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->validate([
'remoteServerName' => 'required',
'remoteServerHost' => 'required',
'remoteServerPort' => 'required',
'remoteServerPort' => 'required|integer',
'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
]);
$foundServer = Server::whereIp($this->remoteServerHost)->first();
if ($foundServer) {
return $this->emit('error', 'IP address is already in use by another team.');
}
$this->createdServer = Server::create([
'name' => $this->remoteServerName,
'ip' => $this->remoteServerHost,
@@ -168,38 +181,52 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
'user' => $this->remoteServerUser,
'description' => $this->remoteServerDescription,
'private_key_id' => $this->createdPrivateKey->id,
'team_id' => currentTeam()->id
'team_id' => currentTeam()->id,
]);
$this->createdServer->save();
$this->validateServer();
}
public function 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();
$customErrorMessage = "Server is not reachable:";
config()->set('coolify.mux_enabled', false);
} catch (\Exception $e) {
return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this);
instant_remote_process(['uptime'], $this->createdServer, true);
$this->createdServer->settings()->update([
'is_reachable' => true,
]);
} catch (\Throwable $e) {
$this->serverReachable = false;
return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this);
}
try {
$dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $this->createdServer, true);
$dockerVersion = checkMinimumDockerEngineVersion($dockerVersion);
if (is_null($dockerVersion)) {
$this->currentState = 'install-docker';
throw new \Exception('Docker version is not supported or not installed.');
}
$this->createdServer->settings()->update([
'is_usable' => true,
]);
$this->getProxyType();
} catch (\Throwable $e) {
$this->dockerInstallationStarted = false;
return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this);
}
}
public function installDocker()
{
$activity = resolve(InstallDocker::class)($this->createdServer, currentTeam());
$this->dockerInstallationStarted = true;
$activity = resolve(InstallDocker::class)($this->createdServer);
$this->emit('newMonitorActivity', $activity->id);
$this->currentState = 'select-proxy';
}
public function dockerInstalledOrSkipped()
{
$this->validateServer();
}
public function selectProxy(string|null $proxyType = null)
{
@@ -212,14 +239,16 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->getProjects();
}
public function 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() {
public function selectExistingProject()
{
$this->createdProject = Project::find($this->selectedExistingProject);
$this->currentState = 'create-resource';
}
@@ -239,7 +268,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
[
'project_uuid' => $this->createdProject->uuid,
'environment_name' => 'production',
'server'=> $this->createdServer->id,
'server' => $this->createdServer->id,
]
);
}

View File

@@ -34,9 +34,9 @@ class CheckLicense extends Component
try {
resolve(CheckResaleLicense::class)();
$this->emit('reloadWindow');
} catch (\Throwable $th) {
session()->flash('error', 'Something went wrong. Please contact support. <br>Error: ' . $th->getMessage());
ray($th->getMessage());
} catch (\Throwable $e) {
session()->flash('error', 'Something went wrong. Please contact support. <br>Error: ' . $e->getMessage());
ray($e->getMessage());
return redirect()->to('/settings/license');
}
}

View File

@@ -25,6 +25,15 @@ class Dashboard extends Component
}
$this->projects = $projects->count();
}
// public function getIptables()
// {
// $servers = Server::ownedByCurrentTeam()->get();
// foreach ($servers as $server) {
// checkRequiredCommands($server);
// $iptables = instant_remote_process(['docker run --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c "iptables -L -n | jc --iptables"'], $server);
// ray($iptables);
// }
// }
public function render()
{
return view('livewire.dashboard');

View File

@@ -37,8 +37,8 @@ class Form extends Component
}
$this->destination->delete();
return redirect()->route('dashboard');
} catch (\Exception $e) {
return general_error_handler(err: $e);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -71,14 +71,14 @@ class StandaloneDocker extends Component
}
$this->createNetworkAndAttachToProxy();
return redirect()->route('destination.show', $docker->uuid);
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} catch (\Throwable $e) {
return handleError($e, $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);
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
}
}

View File

@@ -18,22 +18,26 @@ class ForcePasswordReset extends Component
'password' => 'required|min:8',
'password_confirmation' => 'required|same:password',
];
public function mount() {
public function mount()
{
$this->email = auth()->user()->email;
}
public function submit() {
public function submit()
{
try {
$this->rateLimit(10);
$this->validate();
$firstLogin = auth()->user()->created_at == auth()->user()->updated_at;
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);
if ($firstLogin) {
send_internal_notification('First login for ' . auth()->user()->email);
}
return redirect()->route('dashboard');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Livewire;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Route;
use Livewire\Component;
class Help extends Component
{
use WithRateLimiting;
public string $description;
public string $subject;
public ?string $path = null;
protected $rules = [
'description' => 'required|min:10',
'subject' => 'required|min:3'
];
public function mount()
{
$this->path = Route::current()?->uri() ?? null;
if (isDev()) {
$this->description = "I'm having trouble with {$this->path}";
$this->subject = "Help with {$this->path}";
}
}
public function submit()
{
try {
$this->rateLimit(3, 60);
$this->validate();
$subscriptionType = auth()->user()?->subscription?->type() ?? 'Free';
$debug = "Route: {$this->path}";
$mail = new MailMessage();
$mail->view(
'emails.help',
[
'description' => $this->description,
'debug' => $debug
]
);
$mail->subject("[HELP - {$subscriptionType}]: {$this->subject}");
send_user_an_email($mail, auth()->user()?->email, 'hi@coollabs.io');
$this->emit('success', 'Your message has been sent successfully. <br>We will get in touch with you as soon as possible.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.help')->layout('layouts.app');
}
}

View File

@@ -8,26 +8,30 @@ use Livewire\Component;
class DiscordSettings extends Component
{
public Team $model;
public Team $team;
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_notifications_status_changes' => 'nullable|boolean',
'model.discord_notifications_database_backups' => 'nullable|boolean',
'team.discord_enabled' => 'nullable|boolean',
'team.discord_webhook_url' => 'required|url',
'team.discord_notifications_test' => 'nullable|boolean',
'team.discord_notifications_deployments' => 'nullable|boolean',
'team.discord_notifications_status_changes' => 'nullable|boolean',
'team.discord_notifications_database_backups' => 'nullable|boolean',
];
protected $validationAttributes = [
'model.discord_webhook_url' => 'Discord Webhook',
'team.discord_webhook_url' => 'Discord Webhook',
];
public function mount()
{
$this->team = auth()->user()->currentTeam();
}
public function instantSave()
{
try {
$this->submit();
} catch (\Exception $e) {
} catch (\Throwable $e) {
ray($e->getMessage());
$this->model->discord_enabled = false;
$this->team->discord_enabled = false;
$this->validate();
}
}
@@ -41,16 +45,14 @@ class DiscordSettings extends Component
public function saveModel()
{
$this->model->save();
if (is_a($this->model, Team::class)) {
refreshSession();
}
$this->team->save();
refreshSession();
$this->emit('success', 'Settings saved.');
}
public function sendTestNotification()
{
$this->model->notify(new Test);
$this->team->notify(new Test());
$this->emit('success', 'Test notification sent.');
}
}

View File

@@ -49,6 +49,7 @@ class EmailSettings extends Component
public function mount()
{
$this->team = auth()->user()->currentTeam();
['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits;
$this->emails = auth()->user()->email;
}
@@ -61,9 +62,10 @@ class EmailSettings extends Component
'team.smtp_from_name' => 'required',
]);
$this->team->save();
refreshSession();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
return general_error_handler($e, $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function sendTestNotification()
@@ -80,9 +82,10 @@ class EmailSettings extends Component
$this->team->smtp_enabled = false;
$this->team->resend_enabled = false;
$this->team->save();
refreshSession();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
return general_error_handler($e, $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
@@ -91,9 +94,9 @@ class EmailSettings extends Component
try {
$this->team->smtp_enabled = false;
$this->submitResend();
} catch (\Exception $e) {
} catch (\Throwable $e) {
$this->team->smtp_enabled = false;
return general_error_handler($e, $this);
return handleError($e, $this);
}
}
public function instantSave()
@@ -101,12 +104,17 @@ class EmailSettings extends Component
try {
$this->team->resend_enabled = false;
$this->submit();
} catch (\Exception $e) {
} catch (\Throwable $e) {
$this->team->smtp_enabled = false;
return general_error_handler($e, $this);
return handleError($e, $this);
}
}
public function saveModel()
{
$this->team->save();
refreshSession();
$this->emit('success', 'Settings saved.');
}
public function submit()
{
try {
@@ -122,10 +130,11 @@ class EmailSettings extends Component
'team.smtp_timeout' => 'nullable',
]);
$this->team->save();
refreshSession();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
} catch (\Throwable $e) {
$this->team->smtp_enabled = false;
return general_error_handler($e, $this);
return handleError($e, $this);
}
}
public function submitResend()
@@ -133,14 +142,16 @@ class EmailSettings extends Component
try {
$this->resetErrorBag();
$this->validate([
'team.smtp_from_address' => 'required|email',
'team.smtp_from_name' => 'required',
'team.resend_api_key' => 'required'
]);
$this->team->save();
refreshSession();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
} catch (\Throwable $e) {
$this->team->resend_enabled = false;
return general_error_handler($e, $this);
return handleError($e, $this);
}
}
public function copyFromInstanceSettings()

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Http\Livewire\Notifications;
use App\Models\Team;
use App\Notifications\Test;
use Livewire\Component;
class TelegramSettings extends Component
{
public Team $team;
protected $rules = [
'team.telegram_enabled' => 'nullable|boolean',
'team.telegram_token' => 'required|string',
'team.telegram_chat_id' => 'required|string',
'team.telegram_notifications_test' => 'nullable|boolean',
'team.telegram_notifications_deployments' => 'nullable|boolean',
'team.telegram_notifications_status_changes' => 'nullable|boolean',
'team.telegram_notifications_database_backups' => 'nullable|boolean',
'team.telegram_notifications_test_message_thread_id' => 'nullable|string',
'team.telegram_notifications_deployments_message_thread_id' => 'nullable|string',
'team.telegram_notifications_status_changes_message_thread_id' => 'nullable|string',
'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string',
];
protected $validationAttributes = [
'team.telegram_token' => 'Token',
'team.telegram_chat_id' => 'Chat ID',
];
public function mount()
{
$this->team = auth()->user()->currentTeam();
}
public function instantSave()
{
try {
$this->submit();
} catch (\Throwable $e) {
ray($e->getMessage());
$this->team->telegram_enabled = false;
$this->validate();
}
}
public function submit()
{
$this->resetErrorBag();
$this->validate();
$this->saveModel();
}
public function saveModel()
{
$this->team->save();
refreshSession();
$this->emit('success', 'Settings saved.');
}
public function sendTestNotification()
{
$this->team->notify(new Test());
$this->emit('success', 'Test notification sent.');
}
}

View File

@@ -8,7 +8,7 @@ use Livewire\Component;
class Change extends Component
{
public PrivateKey $private_key;
public $public_key;
protected $rules = [
'private_key.name' => 'required|string',
'private_key.description' => 'nullable|string',
@@ -21,17 +21,25 @@ class Change extends Component
'private_key.private_key' => 'private key'
];
public function mount()
{
try {
$this->public_key = $this->private_key->publicKey();
}catch(\Throwable $e) {
return handleError($e, $this);
}
}
public function delete()
{
try {
if ($this->private_key->isEmpty()) {
$this->private_key->delete();
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
return redirect()->route('private-key.all');
return redirect()->route('security.private-key.index');
}
$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.');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
@@ -41,8 +49,8 @@ class Change extends Component
$this->private_key->private_key = formatPrivateKey($this->private_key->private_key);
$this->private_key->save();
refresh_server_connection($this->private_key);
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -3,14 +3,20 @@
namespace App\Http\Livewire\PrivateKey;
use App\Models\PrivateKey;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Livewire\Component;
use phpseclib3\Crypt\PublicKeyLoader;
class Create extends Component
{
public string|null $from = null;
use WithRateLimiting;
public string $name;
public string|null $description = null;
public string $value;
public ?string $from = null;
public ?string $description = null;
public ?string $publicKey = null;
protected $rules = [
'name' => 'required|string',
'value' => 'required|string',
@@ -20,6 +26,32 @@ class Create extends Component
'value' => 'private Key',
];
public function generateNewKey()
{
try {
$this->rateLimit(10);
$this->name = generate_random_name();
$this->description = 'Created by Coolify';
['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey();
} catch(\Throwable $e) {
return handleError($e, $this);
}
}
public function updated($updateProperty)
{
if ($updateProperty === 'value') {
try {
$this->publicKey = PublicKeyLoader::load($this->$updateProperty)->getPublicKey()->toString('OpenSSH',['comment' => '']);
} catch (\Throwable $e) {
if ($this->$updateProperty === "") {
$this->publicKey = "";
} else {
$this->publicKey = "Invalid private key";
}
}
}
$this->validateOnly($updateProperty);
}
public function createPrivateKey()
{
$this->validate();
@@ -37,9 +69,9 @@ class Create extends Component
if ($this->from === 'server') {
return redirect()->route('server.create');
}
return redirect()->route('private-key.show', ['private_key_uuid' => $private_key->uuid]);
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
return redirect()->route('security.private-key.show', ['private_key_uuid' => $private_key->uuid]);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -34,7 +34,7 @@ class Form extends Component
'name' => $this->name,
]);
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
return handleError($e, $this);
}
}
}

View File

@@ -28,8 +28,8 @@ class AddEmpty extends Component
'team_id' => currentTeam()->id,
]);
return redirect()->route('project.show', $project->uuid);
} catch (\Exception $e) {
general_error_handler($e, $this);
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->name = '';
}

View File

@@ -31,8 +31,8 @@ class AddEnvironment extends Component
'project_uuid' => $this->project->uuid,
'environment_name' => $environment->name,
]);
} catch (\Exception $e) {
general_error_handler($e, $this);
} catch (\Throwable $e) {
handleError($e, $this);
} finally {
$this->name = '';
}

View File

@@ -65,7 +65,7 @@ class DeploymentNavbar extends Component
]);
}
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
return handleError($e, $this);
}
}
}

View File

@@ -4,15 +4,18 @@ namespace App\Http\Livewire\Project\Application;
use App\Models\Application;
use App\Models\InstanceSettings;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Livewire\Component;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
class General extends Component
{
public string $applicationId;
public Application $application;
public Collection $services;
public string $name;
public string|null $fqdn;
public string $git_repository;
@@ -31,6 +34,7 @@ class General extends Component
public bool $is_auto_deploy_enabled;
public bool $is_force_https_enabled;
protected $rules = [
'application.name' => 'required',
'application.description' => 'nullable',
@@ -66,6 +70,7 @@ class General extends Component
'application.ports_exposes' => 'Ports exposes',
'application.ports_mappings' => 'Ports mappings',
'application.dockerfile' => 'Dockerfile',
];
public function instantSave()
@@ -86,8 +91,8 @@ class General extends Component
$this->application->settings->save();
$this->application->save();
$this->application->refresh();
$this->emit('success', 'Application settings updated!');
$this->checkWildCardDomain();
$this->emit('success', 'Application settings updated!');
}
protected function checkWildCardDomain()
@@ -136,16 +141,15 @@ class General extends Component
public function submit()
{
ray($this->application);
try {
$this->validate();
if (data_get($this->application,'fqdn')) {
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) {
if (data_get($this->application, 'dockerfile')) {
$port = get_port_from_dockerfile($this->application->dockerfile);
if ($port) {
$this->application->ports_exposes = $port;
@@ -159,8 +163,8 @@ class General extends Component
}
$this->application->save();
$this->emit('success', 'Application settings updated!');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -2,9 +2,8 @@
namespace App\Http\Livewire\Project\Application;
use App\Jobs\ApplicationContainerStatusJob;
use App\Jobs\ContainerStatusJob;
use App\Models\Application;
use App\Notifications\Application\StatusChanged;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -22,10 +21,11 @@ class Heading extends Component
public function check_status()
{
dispatch_sync(new ApplicationContainerStatusJob(
application: $this->application,
));
dispatch(new ContainerStatusJob($this->application->destination->server));
$this->application->refresh();
$this->application->previews->each(function ($preview) {
$preview->refresh();
});
}
public function force_deploy_without_cache()
@@ -64,14 +64,15 @@ class Heading extends Component
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containerName) {
remote_process(
instant_remote_process(
["docker rm -f {$containerName}"],
$this->application->destination->server
);
$this->application->status = 'stopped';
$this->application->status = 'exited';
$this->application->save();
// $this->application->environment->project->team->notify(new StatusChanged($this->application));
}
}
$this->application->refresh();
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Http\Livewire\Project\Application;
use App\Jobs\ApplicationContainerStatusJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use Illuminate\Support\Collection;
@@ -23,14 +22,6 @@ class Previews extends Component
$this->parameters = get_route_parameters();
}
public function loadStatus($pull_request_id)
{
dispatch(new ApplicationContainerStatusJob(
application: $this->application,
pullRequestId: $pull_request_id
));
}
public function load_prs()
{
try {
@@ -39,7 +30,7 @@ class Previews extends Component
$this->pull_requests = $data->sortBy('number')->values();
} catch (\Throwable $e) {
$this->rate_limit_remaining = 0;
return general_error_handler(err: $e, that: $this);
return handleError($e, $this);
}
}
@@ -68,7 +59,7 @@ class Previews extends Component
'environment_name' => $this->parameters['environment_name'],
]);
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
return handleError($e, $this);
}
}
@@ -81,14 +72,14 @@ class Previews extends Component
public function stop(int $pull_request_id)
{
try {
$container_name = generateApplicationContainerName($this->application->uuid, $pull_request_id);
$container_name = generateApplicationContainerName($this->application);
ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $this->application->destination->server, throwError: false);
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->delete();
$this->application->refresh();
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
return handleError($e, $this);
}
}

View File

@@ -65,7 +65,7 @@ class Rollback extends Component
];
})->toArray();
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
return handleError($e, $this);
}
}
}

View File

@@ -51,7 +51,7 @@ class BackupEdit extends Component
$this->backup->save();
$this->backup->refresh();
$this->emit('success', 'Backup updated successfully');
} catch (\Exception $e) {
} catch (\Throwable $e) {
$this->emit('error', $e->getMessage());
}
}
@@ -76,7 +76,7 @@ class BackupEdit extends Component
$this->backup->save();
$this->backup->refresh();
$this->emit('success', 'Backup updated successfully');
} catch (\Exception $e) {
} catch (\Throwable $e) {
$this->emit('error', $e->getMessage());
}
}

View File

@@ -42,8 +42,8 @@ class CreateScheduledBackup extends Component
'team_id' => currentTeam()->id,
]);
$this->emit('refreshScheduledBackups');
} catch (\Exception $e) {
general_error_handler($e, $this);
} catch (\Throwable $e) {
handleError($e, $this);
} finally {
$this->frequency = '';
$this->save_s3 = true;

View File

@@ -3,8 +3,7 @@
namespace App\Http\Livewire\Project\Database;
use App\Actions\Database\StartPostgresql;
use App\Jobs\DatabaseContainerStatusJob;
use App\Notifications\Application\StatusChanged;
use App\Jobs\ContainerStatusJob;
use Livewire\Component;
class Heading extends Component
@@ -25,9 +24,7 @@ class Heading extends Component
public function check_status()
{
dispatch_sync(new DatabaseContainerStatusJob(
database: $this->database,
));
dispatch_sync(new ContainerStatusJob($this->database->destination->server));
$this->database->refresh();
}
@@ -38,12 +35,17 @@ class Heading extends Component
public function stop()
{
remote_process(
instant_remote_process(
["docker rm -f {$this->database->uuid}"],
$this->database->destination->server
);
$this->database->status = 'stopped';
if ($this->database->is_public) {
stopPostgresProxy($this->database);
$this->database->is_public = false;
}
$this->database->status = 'exited';
$this->database->save();
$this->check_status();
// $this->database->environment->project->team->notify(new StatusChanged($this->database));
}

View File

@@ -36,7 +36,7 @@ class InitScript extends Component
$this->script['filename'] = $this->filename;
$this->emitUp('save_init_script', $this->script);
} catch (Exception $e) {
return general_error_handler(err: $e, that: $this);
return handleError($e, $this);
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Http\Livewire\Project\Database\Postgresql;
use App\Models\StandalonePostgresql;
use Exception;
use Livewire\Component;
use function Aws\filter;
class General extends Component
@@ -12,6 +13,7 @@ class General extends Component
public StandalonePostgresql $database;
public string $new_filename;
public string $new_content;
public string $db_url;
protected $listeners = ['refresh', 'save_init_script', 'delete_init_script'];
@@ -26,6 +28,8 @@ class General extends Component
'database.init_scripts' => 'nullable',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
];
protected $validationAttributes = [
'database.name' => 'Name',
@@ -38,8 +42,44 @@ class General extends Component
'database.init_scripts' => 'Init Scripts',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function mount()
{
$this->getDbUrl();
}
public function getDbUrl() {
if ($this->database->is_public) {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->destination->server->ip}:{$this->database->public_port}/{$this->database->postgres_db}";
} else {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->uuid}:5432/{$this->database->postgres_db}";
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...');
startPostgresProxy($this->database);
$this->emit('success', 'Database is now publicly accessible.');
} else {
stopPostgresProxy($this->database);
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->getDbUrl();
$this->database->save();
} catch(\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
}
public function save_init_script($script)
{
$this->database->init_scripts = filter($this->database->init_scripts, fn ($s) => $s['filename'] !== $script['filename']);
@@ -101,7 +141,7 @@ class General extends Component
$this->database->save();
$this->emit('success', 'Database updated successfully.');
} catch (Exception $e) {
return general_error_handler(err: $e, that: $this);
return handleError($e, $this);
}
}
}

View File

@@ -19,8 +19,8 @@ class Edit extends Component
try {
$this->project->save();
$this->emit('saved');
} catch (\Exception $e) {
return general_error_handler($e, $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace App\Http\Livewire\Project\New;
use App\Models\EnvironmentVariable;
use App\Models\Project;
use App\Models\Service;
use Livewire\Component;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
class DockerCompose extends Component
{
public string $dockerComposeRaw = '';
public string $envFile = '';
public array $parameters;
public array $query;
public function mount()
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
if (isDev()) {
$this->dockerComposeRaw = 'services:
ghost:
image: ghost:5
volumes:
- ~/configs:/etc/configs/:ro
- ./var/lib/ghost/content:/tmp/ghost2/content:ro
- /var/lib/ghost/content:/tmp/ghost/content:rw
- ghost-content-data:/var/lib/ghost/content
- type: volume
source: mydata
target: /data
volume:
nocopy: true
- type: bind
source: ./var/lib/ghost/data
target: /data
- type: bind
source: /tmp
target: /tmp
labels:
- "test.label=true"
ports:
- "3000"
- "3000-3005"
- "8000:8000"
- "9090-9091:8080-8081"
- "49100:22"
- "127.0.0.1:8001:8001"
- "127.0.0.1:5000-5010:5000-5010"
- "127.0.0.1::5000"
- "6060:6060/udp"
- "12400-12500:1240"
- target: 80
published: 8080
protocol: tcp
mode: host
networks:
- some-network
- other-network
environment:
- database__client=${DATABASE_CLIENT:-mysql}
- database__connection__database=${MYSQL_DATABASE:-ghost}
- database__connection__host=${DATABASE_CONNECTION_HOST:-mysql}
- test=${TEST:?true}
- url=$SERVICE_FQDN_GHOST
- database__connection__user=$SERVICE_USER_MYSQL
- database__connection__password=$SERVICE_PASSWORD_MYSQL
depends_on:
- mysql
mysql:
image: mysql:8.0
volumes:
- ghost-mysql-data:/var/lib/mysql
environment:
- MYSQL_USER=${SERVICE_USER_MYSQL}
- MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL}
- MYSQL_DATABASE=$MYSQL_DATABASE
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQLROOT}
- SESSION_SECRET
minio:
image: minio/minio
environment:
RACK_ENV: development
A: $A
SHOW: ${SHOW}
SHOW1: ${SHOW2-show1}
SHOW2: ${SHOW3:-show2}
SHOW3: ${SHOW4?show3}
SHOW4: ${SHOW5:?show4}
SHOW5: ${SERVICE_USER_MINIO}
SHOW6: ${SERVICE_PASSWORD_MINIO}
SHOW7: ${SERVICE_PASSWORD_64_MINIO}
SHOW8: ${SERVICE_BASE64_64_MINIO}
SHOW9: ${SERVICE_BASE64_128_MINIO}
SHOW10: ${SERVICE_BASE64_MINIO}
SHOW11:
';
}
}
public function submit()
{
try {
$this->validate([
'dockerComposeRaw' => 'required'
]);
$this->dockerComposeRaw = Yaml::dump(Yaml::parse($this->dockerComposeRaw), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
$server_id = $this->query['server_id'];
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
$service = Service::create([
'name' => 'service' . Str::random(10),
'docker_compose_raw' => $this->dockerComposeRaw,
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
]);
$variables = parseEnvFormatToArray($this->envFile);
foreach ($variables as $key => $variable) {
EnvironmentVariable::create([
'key' => $key,
'value' => $variable,
'is_build_time' => false,
'is_preview' => false,
'service_id' => $service->id,
]);
}
$service->name = "service-$service->uuid";
$service->parse(isNew: true);
return redirect()->route('project.service', [
'service_uuid' => $service->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
]);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -9,8 +9,8 @@ use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Traits\SaveFromRedirect;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
use Livewire\Component;
use Route;
class GithubPrivateRepository extends Component
{
@@ -40,21 +40,6 @@ class GithubPrivateRepository extends Component
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()
{
@@ -159,13 +144,18 @@ class GithubPrivateRepository extends Component
$application->settings->is_static = $this->is_static;
$application->settings->save();
$sslip = sslip($destination->server);
$application->fqdn = "http://{$application->uuid}.$sslip";
$application->name = generate_application_name($this->selected_repository_owner . '/' . $this->selected_repository_repo, $this->selected_branch_name, $application->uuid);
$application->save();
redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
]);
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}

View File

@@ -112,13 +112,18 @@ class GithubPrivateRepositoryDeployKey extends Component
$application->settings->is_static = $this->is_static;
$application->settings->save();
$sslip = sslip($destination->server);
$application->fqdn = "http://{$application->uuid}.$sslip";
$application->name = generate_random_name($application->uuid);
$application->save();
return redirect()->route('project.application.configuration', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
'application_uuid' => $application->uuid,
]);
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}

View File

@@ -69,21 +69,21 @@ class PublicGitRepository extends Component
{
try {
$this->branch_found = false;
$this->validate([
'repository_url' => 'required|url'
]);
$this->get_git_source();
$this->get_branch();
$this->selected_branch = $this->git_branch;
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
$this->validate([
'repository_url' => 'required|url'
]);
$this->get_git_source();
$this->get_branch();
$this->selected_branch = $this->git_branch;
} catch (\Throwable $e) {
return handleError($e, $this);
}
if (!$this->branch_found && $this->git_branch == 'main') {
try {
$this->git_branch = 'master';
$this->get_branch();
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}
@@ -137,7 +137,6 @@ class PublicGitRepository extends Component
$project = Project::where('uuid', $project_uuid)->first();
$environment = $project->load(['environments'])->environments->where('name', $environment_name)->first();
$application_init = [
'name' => generate_application_name($this->git_repository, $this->git_branch),
'git_repository' => $this->git_repository,
@@ -153,16 +152,22 @@ class PublicGitRepository extends Component
];
$application = Application::create($application_init);
$application->settings->is_static = $this->is_static;
$application->settings->save();
$sslip = sslip($destination->server);
$application->fqdn = "http://{$application->uuid}.$sslip";
$application->name = generate_application_name($this->git_repository, $this->git_branch, $application->uuid);
$application->save();
return redirect()->route('project.application.configuration', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
'application_uuid' => $application->uuid,
]);
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -3,12 +3,12 @@
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 Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Livewire\Component;
use Route;
class Select extends Component
{
@@ -17,39 +17,83 @@ class Select extends Component
public string $type;
public string $server_id;
public string $destination_uuid;
public Countable|array|Server $servers;
public Countable|array|Server $servers = [];
public Collection|array $standaloneDockers = [];
public Collection|array $swarmDockers = [];
public array $parameters;
public Collection|array $services = [];
public bool $loadingServices = true;
public bool $loading = false;
public ?string $existingPostgresqlUrl = null;
protected $queryString = [
'server',
];
public function mount()
{
$this->parameters = get_route_parameters();
if (isDev()) {
$this->existingPostgresqlUrl = 'postgres://coolify:password@coolify-db:5432';
}
}
public function set_type(string $type)
// public function addExistingPostgresql()
// {
// try {
// instantCommand("psql {$this->existingPostgresqlUrl} -c 'SELECT 1'");
// $this->emit('success', 'Successfully connected to the database.');
// } catch (\Throwable $e) {
// return handleError($e, $this);
// }
// }
public function loadThings()
{
$this->loadServices();
$this->loadServers();
}
public function loadServices(bool $forceReload = false)
{
try {
if ($forceReload) {
Cache::forget('services');
}
$this->services = getServiceTemplates();
$this->emit('success', 'Successfully loaded services.');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->loadingServices = false;
}
}
public function setType(string $type)
{
$this->type = $type;
if ($this->loading) return;
$this->loading = true;
if ($type === "existing-postgresql") {
$this->current_step = $type;
return;
}
if (count($this->servers) === 1) {
$server = $this->servers->first();
$this->set_server($server);
$this->setServer($server);
if (count($server->destinations()) === 1) {
$this->set_destination($server->destinations()->first()->uuid);
$this->setDestination($server->destinations()->first()->uuid);
}
}
if (!is_null($this->server)) {
$foundServer = $this->servers->where('id', $this->server)->first();
if ($foundServer) {
return $this->set_server($foundServer);
return $this->setServer($foundServer);
}
}
$this->current_step = 'servers';
}
public function set_server(Server $server)
public function setServer(Server $server)
{
$this->server_id = $server->id;
$this->standaloneDockers = $server->standaloneDockers;
@@ -57,7 +101,7 @@ class Select extends Component
$this->current_step = 'destinations';
}
public function set_destination(string $destination_uuid)
public function setDestination(string $destination_uuid)
{
$this->destination_uuid = $destination_uuid;
redirect()->route('project.resources.new', [
@@ -65,10 +109,11 @@ class Select extends Component
'environment_name' => $this->parameters['environment_name'],
'type' => $this->type,
'destination' => $this->destination_uuid,
'server_id' => $this->server_id,
]);
}
public function load_servers()
public function loadServers()
{
$this->servers = Server::isUsable()->get();
}

View File

@@ -59,8 +59,11 @@ CMD ["nginx", "-g", "daemon off;"]
'source_id' => 0,
'source_type' => GithubApp::class
]);
$sslip = sslip($destination->server);
$application->update([
'name' => 'dockerfile-' . $application->id
'name' => 'dockerfile-' . $application->uuid,
'fqdn' => "http://{$application->uuid}.$sslip"
]);
redirect()->route('project.application.configuration', [

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Models\ServiceApplication;
use Livewire\Component;
class Application extends Component
{
public ServiceApplication $application;
public $parameters;
protected $rules = [
'application.human_name' => 'nullable',
'application.description' => 'nullable',
'application.fqdn' => 'nullable',
'application.image' => 'required',
'application.exclude_from_status' => 'required|boolean',
'application.required_fqdn' => 'required|boolean',
];
public function render()
{
return view('livewire.project.service.application');
}
public function instantSave()
{
$this->submit();
}
public function delete()
{
try {
$this->application->delete();
$this->emit('success', 'Application deleted successfully.');
return redirect()->route('project.service', $this->parameters);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function mount()
{
$this->parameters = get_route_parameters();
}
public function submit()
{
try {
$this->validate();
$this->application->save();
updateCompose($this->application);
$this->emit('success', 'Application saved successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->emit('generateDockerCompose');
}
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Models\ServiceDatabase;
use Livewire\Component;
class Database extends Component
{
public ServiceDatabase $database;
public $fileStorages;
protected $listeners = ["refreshFileStorages"];
protected $rules = [
'database.human_name' => 'nullable',
'database.description' => 'nullable',
'database.image' => 'required',
'database.exclude_from_status' => 'required|boolean',
];
public function render()
{
return view('livewire.project.service.database');
}
public function mount() {
$this->refreshFileStorages();
}
public function instantSave() {
$this->submit();
}
public function refreshFileStorages()
{
$this->fileStorages = $this->database->fileStorages()->get();
}
public function submit()
{
try {
$this->validate();
$this->database->save();
updateCompose($this->database);
$this->emit('success', 'Database saved successfully.');
} catch (\Throwable $e) {
ray($e);
} finally {
$this->emit('generateDockerCompose');
}
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Models\LocalFileVolume;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Livewire\Component;
use Illuminate\Support\Str;
class FileStorage extends Component
{
public LocalFileVolume $fileStorage;
public ServiceApplication|ServiceDatabase $service;
public string $fs_path;
protected $rules = [
'fileStorage.is_directory' => 'required',
'fileStorage.fs_path' => 'required',
'fileStorage.mount_path' => 'required',
'fileStorage.content' => 'nullable',
];
public function mount()
{
$this->service = $this->fileStorage->service;
$this->fs_path = Str::of($this->fileStorage->fs_path)->beforeLast('/');
$file = Str::of($this->fileStorage->fs_path)->afterLast('/');
if (Str::of($this->fs_path)->startsWith('.')) {
$this->fs_path = Str::of($this->fs_path)->after('.');
$this->fs_path = $this->service->service->workdir() . $this->fs_path . "/" . $file;
}
}
public function submit()
{
try {
$this->validate();
$this->fileStorage->save();
$this->service->saveFileVolumes();
$this->emit('success', 'File updated successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
$this->submit();
}
public function render()
{
return view('livewire.project.service.file-storage');
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Jobs\ContainerStatusJob;
use App\Models\Service;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Livewire\Component;
class Index extends Component
{
public Service $service;
public $applications;
public $databases;
public array $parameters;
public array $query;
protected $rules = [
'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required',
'service.name' => 'required',
'service.description' => 'nullable',
];
public function checkStatus() {
dispatch_sync(new ContainerStatusJob($this->service->server));
$this->refreshStack();
}
public function refreshStack()
{
$this->applications = $this->service->applications->sort();
$this->applications->each(function ($application) {
$application->refresh();
});
$this->databases = $this->service->databases->sort();
$this->databases->each(function ($database) {
$database->refresh();
});
}
public function mount()
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$this->refreshStack();
}
public function render()
{
return view('livewire.project.service.index');
}
public function submit()
{
try {
$this->validate();
$this->service->save();
$this->service->parse();
$this->service->refresh();
$this->service->saveComposeConfigs();
$this->refreshStack();
$this->emit('refreshEnvs');
$this->emit('success', 'Service saved successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Http\Livewire\Project\Service;
use Livewire\Component;
class Modal extends Component
{
public function serviceStatusUpdated() {
$this->emit('serviceStatusUpdated');
}
public function render()
{
return view('livewire.project.service.modal');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Jobs\ContainerStatusJob;
use App\Models\Service;
use Livewire\Component;
class Navbar extends Component
{
public Service $service;
public array $parameters;
public array $query;
protected $listeners = ['serviceStatusUpdated'];
public function render()
{
return view('livewire.project.service.navbar');
}
public function serviceStatusUpdated()
{
ray('serviceStatusUpdated');
$this->check_status();
}
public function check_status()
{
dispatch_sync(new ContainerStatusJob($this->service->server));
$this->service->refresh();
}
public function deploy()
{
$this->service->parse();
$activity = StartService::run($this->service);
$this->emit('newMonitorActivity', $activity->id);
}
public function stop()
{
StopService::run($this->service);
$this->service->refresh();
$this->emit('success', 'Service stopped successfully.');
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Collection;
use Livewire\Component;
class Show extends Component
{
public Service $service;
public ?ServiceApplication $serviceApplication = null;
public ?ServiceDatabase $serviceDatabase = null;
public array $parameters;
public array $query;
public Collection $services;
protected $listeners = ['generateDockerCompose'];
public function mount()
{
$this->services = collect([]);
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$service = $this->service->applications()->whereName($this->parameters['service_name'])->first();
if ($service) {
$this->serviceApplication = $service;
} else {
$this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first();
}
}
public function generateDockerCompose()
{
$this->service->parse();
}
public function render()
{
return view('livewire.project.service.show');
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Livewire\Project\Shared;
use App\Actions\Service\StopService;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -19,13 +20,26 @@ class Danger extends Component
public function delete()
{
$destination = $this->resource->destination->getMorphClass()::where('id', $this->resource->destination->id)->first();
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']
]);
// Should be queued
try {
if ($this->resource->type() === 'service') {
$server = $this->resource->server;
StopService::run($this->resource);
} else {
$destination = data_get($this->resource, 'destination');
if ($destination) {
$destination = $this->resource->destination->getMorphClass()::where('id', $this->resource->destination->id)->first();
$server = $destination->server;
}
instant_remote_process(["docker rm -f {$this->resource->uuid}"], $server);
}
$this->resource->delete();
return redirect()->route('project.resources', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name']
]);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -9,13 +9,13 @@ class Add extends Component
public $parameters;
public bool $is_preview = false;
public string $key;
public string $value;
public ?string $value = null;
public bool $is_build_time = false;
protected $listeners = ['clearAddEnv' => 'clear'];
protected $rules = [
'key' => 'required|string',
'value' => 'required|string',
'value' => 'nullable',
'is_build_time' => 'required|boolean',
];
protected $validationAttributes = [
@@ -31,8 +31,8 @@ class Add extends Component
public function submit()
{
ray('submitting');
$this->validate();
ray($this->key, $this->value, $this->is_build_time);
$this->emitUp('submit', [
'key' => $this->key,
'value' => $this->value,

View File

@@ -5,21 +5,94 @@ namespace App\Http\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
use Illuminate\Support\Str;
class All extends Component
{
public $resource;
public bool $showPreview = false;
public string|null $modalId = null;
public ?string $variables = null;
public ?string $variablesPreview = null;
public string $view = 'normal';
protected $listeners = ['refreshEnvs', 'submit'];
public function mount()
{
$resourceClass = get_class($this->resource);
$resourceWithPreviews = ['App\Models\Application'];
$simpleDockerfile = !is_null(data_get($this->resource, 'dockerfile'));
if (Str::of($resourceClass)->contains($resourceWithPreviews) && !$simpleDockerfile) {
$this->showPreview = true;
}
$this->modalId = new Cuid2(7);
$this->getDevView();
}
public function getDevView()
{
$this->variables = $this->resource->environment_variables->map(function ($item) {
return "$item->key=$item->value";
})->sort()->join('
');
if ($this->showPreview) {
$this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) {
return "$item->key=$item->value";
})->sort()->join('
');
}
}
public function switch()
{
$this->view = $this->view === 'normal' ? 'dev' : 'normal';
}
public function saveVariables($isPreview)
{
if ($isPreview) {
$variables = parseEnvFormatToArray($this->variablesPreview);
$existingVariables = $this->resource->environment_variables_preview();
$this->resource->environment_variables_preview()->delete();
} else {
$variables = parseEnvFormatToArray($this->variables);
$existingVariables = $this->resource->environment_variables();
$this->resource->environment_variables()->delete();
}
foreach ($variables as $key => $variable) {
$found = $existingVariables->where('key', $key)->first();
if ($found) {
$found->value = $variable;
$found->save();
continue;
} else {
$environment = new EnvironmentVariable();
$environment->key = $key;
$environment->value = $variable;
$environment->is_build_time = false;
$environment->is_preview = $isPreview ? true : false;
switch ($this->resource->type()) {
case 'application':
$environment->application_id = $this->resource->id;
break;
case 'standalone-postgresql':
$environment->standalone_postgresql_id = $this->resource->id;
break;
case 'service':
$environment->service_id = $this->resource->id;
break;
}
$environment->save();
}
}
if ($isPreview) {
$this->emit('success', 'Preview environment variables updated successfully.');
} else {
$this->emit('success', 'Environment variables updated successfully.');
}
$this->refreshEnvs();
}
public function refreshEnvs()
{
$this->resource->refresh();
$this->getDevView();
}
public function submit($data)
@@ -36,17 +109,22 @@ class All extends Component
$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;
switch ($this->resource->type()) {
case 'application':
$environment->application_id = $this->resource->id;
break;
case 'standalone-postgresql':
$environment->standalone_postgresql_id = $this->resource->id;
break;
case 'service':
$environment->service_id = $this->resource->id;
break;
}
$environment->save();
$this->resource->refresh();
$this->refreshEnvs();
$this->emit('success', 'Environment variable added successfully.');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -5,15 +5,19 @@ namespace App\Http\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
use Illuminate\Support\Str;
class Show extends Component
{
public $parameters;
public ModelsEnvironmentVariable $env;
public string|null $modalId = null;
public ?string $modalId = null;
public bool $isDisabled = false;
public string $type;
protected $rules = [
'env.key' => 'required|string',
'env.value' => 'required|string',
'env.value' => 'nullable',
'env.is_build_time' => 'required|boolean',
];
protected $validationAttributes = [
@@ -24,6 +28,10 @@ class Show extends Component
public function mount()
{
$this->isDisabled = false;
if (Str::of($this->env->key)->startsWith('SERVICE_FQDN') || Str::of($this->env->key)->startsWith('SERVICE_URL')) {
$this->isDisabled = true;
}
$this->modalId = new Cuid2(7);
$this->parameters = get_route_parameters();
}
@@ -37,6 +45,7 @@ class Show extends Component
$this->validate();
$this->env->save();
$this->emit('success', 'Environment variable updated successfully.');
$this->emit('refreshEnvs');
}
public function delete()

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Livewire\Project\Shared;
use Livewire\Component;
class HealthChecks extends Component
{
public $resource;
protected $rules = [
'resource.health_check_path' => 'string',
'resource.health_check_port' => 'nullable|string',
'resource.health_check_host' => 'string',
'resource.health_check_method' => 'string',
'resource.health_check_return_code' => 'integer',
'resource.health_check_scheme' => 'string',
'resource.health_check_response_text' => 'nullable|string',
'resource.health_check_interval' => 'integer',
'resource.health_check_timeout' => 'integer',
'resource.health_check_retries' => 'integer',
'resource.health_check_start_period' => 'integer',
];
public function submit()
{
try {
$this->validate();
$this->resource->save();
$this->emit('saved');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.shared.health-checks');
}
}

View File

@@ -53,8 +53,8 @@ class ResourceLimits extends Component
$this->validate();
$this->resource->save();
$this->emit('success', 'Resource limits updated successfully.');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -28,8 +28,8 @@ class All extends Component
$this->resource->refresh();
$this->emit('success', 'Storage added successfully');
$this->emit('clearAddStorage');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -2,13 +2,17 @@
namespace App\Http\Livewire\Project\Shared\Storages;
use App\Models\LocalPersistentVolume;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Show extends Component
{
public $storage;
public string|null $modalId = null;
public LocalPersistentVolume $storage;
public bool $isReadOnly = false;
public ?string $modalId = null;
public ?string $realName = null;
protected $rules = [
'storage.name' => 'required|string',
'storage.mount_path' => 'required|string',
@@ -22,6 +26,11 @@ class Show extends Component
public function mount()
{
if ($this->storage->resource_type === 'App\Models\ServiceApplication' || $this->storage->resource_type === 'App\Models\ServiceDatabase') {
$this->realName = "{$this->storage->service->service->uuid}_{$this->storage->name}";
} else {
$this->realName = $this->storage->name;
}
$this->modalId = new Cuid2(7);
}

View File

@@ -32,8 +32,8 @@ class RunCommand extends Component
try {
$activity = remote_process([$this->command], Server::where('uuid', $this->server)->first(), ignore_errors: true);
$this->emit('newMonitorActivity', $activity->id);
} catch (\Exception $e) {
return general_error_handler(err: $e);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -15,6 +15,7 @@ class Form extends Component
public $dockerVersion;
public string|null $wildcard_domain = null;
public int $cleanup_after_percentage;
public bool $dockerInstallationStarted = false;
protected $rules = [
'server.name' => 'required|min:6',
@@ -22,6 +23,7 @@ class Form extends Component
'server.ip' => 'required',
'server.user' => 'required',
'server.port' => 'required',
'server.settings.is_cloudflare_tunnel' => 'required',
'server.settings.is_reachable' => 'required',
'server.settings.is_part_of_swarm' => 'required',
'wildcard_domain' => 'nullable|url',
@@ -32,6 +34,7 @@ class Form extends Component
'server.ip' => 'ip',
'server.user' => 'user',
'server.port' => 'port',
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
'server.settings.is_reachable' => 'is reachable',
'server.settings.is_part_of_swarm' => 'is part of swarm'
];
@@ -41,26 +44,39 @@ class Form extends Component
$this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
}
public function instantSave() {
refresh_server_connection($this->server->privateKey);
$this->validateServer();
$this->server->settings->save();
}
public function installDocker()
{
$activity = resolve(InstallDocker::class)($this->server, currentTeam());
$this->dockerInstallationStarted = true;
$activity = resolve(InstallDocker::class)($this->server);
$this->emit('newMonitorActivity', $activity->id);
}
public function validateServer()
{
try {
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->server);
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->server, true);
if ($uptime) {
$this->uptime = $uptime;
$this->emit('success', 'Server is reachable.');
} else {
$this->emit('error', 'Server is not reachable.');
return;
}
if ($dockerVersion) {
$this->dockerVersion = $dockerVersion;
$this->emit('proxyStatusUpdated');
$this->emit('success', 'Docker Engine 23+ is installed!');
} else {
$this->emit('error', 'No Docker Engine or older than 23 version installed.');
}
} catch (\Exception $e) {
return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this);
} catch (\Throwable $e) {
return handleError($e, $this, customErrorMessage: "Server is not reachable: ");
} finally {
$this->emit('proxyStatusUpdated');
}
}
@@ -74,26 +90,20 @@ class Form extends Component
}
$this->server->delete();
return redirect()->route('server.all');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
$this->validate();
// $validation = Validator::make($this->server->toArray(), [
// 'ip' => [
// 'ip'
// ],
// ]);
// if ($validation->fails()) {
// foreach ($validation->errors()->getMessages() as $key => $value) {
// $this->addError("server.{$key}", $value[0]);
// }
// return;
// }
$uniqueIPs = Server::all()->reject(function (Server $server) {
return $server->id === $this->server->id;
})->pluck('ip')->toArray();
if (in_array($this->server->ip, $uniqueIPs)) {
$this->emit('error', 'IP address is already in use by another team.');
return;
}
$this->server->settings->wildcard_domain = $this->wildcard_domain;
$this->server->settings->cleanup_after_percentage = $this->cleanup_after_percentage;
$this->server->settings->save();

View File

@@ -2,6 +2,8 @@
namespace App\Http\Livewire\Server\New;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Livewire\Component;
@@ -24,7 +26,7 @@ class ByIp extends Component
protected $rules = [
'name' => 'required|string',
'description' => 'nullable|string',
'ip' => 'required|ip',
'ip' => 'required',
'user' => 'required|string',
'port' => 'required|integer',
];
@@ -56,7 +58,7 @@ class ByIp extends Component
{
$this->validate();
try {
if (!$this->private_key_id) {
if (is_null($this->private_key_id)) {
return $this->emit('error', 'You must select a private key');
}
$server = Server::create([
@@ -67,12 +69,17 @@ class ByIp extends Component
'port' => $this->port,
'team_id' => currentTeam()->id,
'private_key_id' => $this->private_key_id,
'proxy' => [
"type" => ProxyTypes::TRAEFIK_V2->value,
"status" => ProxyStatus::EXITED->value,
]
]);
$server->settings->is_part_of_swarm = $this->is_part_of_swarm;
$server->settings->save();
return redirect()->route('server.show', $server->uuid);
} catch (\Exception $e) {
return general_error_handler(err: $e);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -2,25 +2,26 @@
namespace App\Http\Livewire\Server;
use App\Actions\Proxy\CheckConfigurationSync;
use App\Actions\Proxy\SaveConfigurationSync;
use App\Enums\ProxyTypes;
use App\Actions\Proxy\CheckConfiguration;
use App\Actions\Proxy\SaveConfiguration;
use App\Models\Server;
use Livewire\Component;
use Illuminate\Support\Str;
class Proxy extends Component
{
public Server $server;
public ProxyTypes $selectedProxy = ProxyTypes::TRAEFIK_V2;
public ?string $selectedProxy = null;
public $proxy_settings = null;
public string|null $redirect_url = null;
public ?string $redirect_url = null;
protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit'];
public function mount()
{
$this->redirect_url = $this->server->proxy->redirect_url;
$this->selectedProxy = data_get($this->server, 'proxy.type');
$this->redirect_url = data_get($this->server, 'proxy.redirect_url');
}
public function proxyStatusUpdated()
@@ -35,44 +36,50 @@ class Proxy extends Component
$this->emit('proxyStatusUpdated');
}
public function select_proxy(ProxyTypes $proxy_type)
public function select_proxy($proxy_type)
{
$this->server->proxy->type = $proxy_type;
$this->server->proxy->status = 'exited';
$this->server->proxy->set('status', 'exited');
$this->server->proxy->set('type', $proxy_type);
$this->server->save();
$this->selectedProxy = $this->server->proxy->type;
$this->emit('proxyStatusUpdated');
}
public function submit()
{
try {
resolve(SaveConfigurationSync::class)($this->server, $this->proxy_settings);
SaveConfiguration::run($this->server, $this->proxy_settings);
$this->server->proxy->redirect_url = $this->redirect_url;
$this->server->save();
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);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function reset_proxy_configuration()
{
try {
$this->proxy_settings = resolve(CheckConfigurationSync::class)($this->server, true);
} catch (\Exception $e) {
return general_error_handler(err: $e);
$this->proxy_settings = CheckConfiguration::run($this->server, true);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function load_proxy_configuration()
public function loadProxyConfiguration()
{
try {
$this->proxy_settings = resolve(CheckConfigurationSync::class)($this->server);
} catch (\Exception $e) {
return general_error_handler(err: $e);
$this->proxy_settings = CheckConfiguration::run($this->server);
if (Str::of($this->proxy_settings)->contains('--api.dashboard=true') && Str::of($this->proxy_settings)->contains('--api.insecure=true')) {
$this->emit('traefikDashboardAvailable', true);
} else {
$this->emit('traefikDashboardAvailable', false);
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -9,18 +9,28 @@ use Livewire\Component;
class Deploy extends Component
{
public Server $server;
public $proxy_settings = null;
public bool $traefikDashboardAvailable = false;
public ?string $currentRoute = null;
protected $listeners = ['proxyStatusUpdated', 'traefikDashboardAvailable'];
public function start_proxy()
public function mount() {
$this->currentRoute = request()->route()->getName();
}
public function traefikDashboardAvailable(bool $data) {
$this->traefikDashboardAvailable = $data;
}
public function proxyStatusUpdated()
{
if (
$this->server->proxy->last_applied_settings &&
$this->server->proxy->last_saved_settings !== $this->server->proxy->last_applied_settings
) {
$this->emit('saveConfiguration', $this->server);
$this->server->refresh();
}
public function startProxy()
{
try {
$activity = StartProxy::run($this->server);
$this->emit('newMonitorActivity', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
$activity = resolve(StartProxy::class)($this->server);
$this->emit('newMonitorActivity', $activity->id);
}
public function stop()

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Http\Livewire\Server\Proxy;
use App\Models\Server;
use Livewire\Component;
class Modal extends Component
{
public Server $server;
public function proxyStatusUpdated()
{
$this->server->proxy->set('status', 'running');
$this->server->save();
$this->emit('proxyStatusUpdated');
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Http\Livewire\Server\Proxy;
use App\Jobs\ProxyContainerStatusJob;
use App\Jobs\ContainerStatusJob;
use App\Models\Server;
use Livewire\Component;
@@ -10,14 +10,23 @@ class Status extends Component
{
public Server $server;
public function get_status()
protected $listeners = ['proxyStatusUpdated'];
public function proxyStatusUpdated()
{
if (data_get($this->server,'settings.is_usable')) {
dispatch_sync(new ProxyContainerStatusJob(
server: $this->server
));
$this->server->refresh();
$this->server->refresh();
}
public function getProxyStatus()
{
try {
dispatch_sync(new ContainerStatusJob($this->server));
$this->emit('proxyStatusUpdated');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function getProxyStatusWithNoti()
{
$this->emit('success', 'Refreshed proxy status.');
$this->getProxyStatus();
}
}

View File

@@ -13,9 +13,12 @@ class Show extends Component
public function mount()
{
try {
$this->server = Server::ownedByCurrentTeam(['name', 'description', 'ip', 'port', 'user', 'proxy'])->whereUuid(request()->server_uuid)->firstOrFail();
$this->server = Server::ownedByCurrentTeam(['name', 'description', 'ip', 'port', 'user', 'proxy'])->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.all');
}
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
return handleError($e, $this);
}
}
public function render()

View File

@@ -4,7 +4,6 @@ namespace App\Http\Livewire\Server;
use App\Models\Server;
use Livewire\Component;
use Masmerise\Toaster\Toaster;
class ShowPrivateKey extends Component
{
@@ -12,28 +11,57 @@ class ShowPrivateKey extends Component
public $privateKeys;
public $parameters;
public function setPrivateKey($private_key_id)
public function setPrivateKey($newPrivateKeyId)
{
$this->server->update([
'private_key_id' => $private_key_id
]);
refresh_server_connection($this->server->privateKey);
$this->server->refresh();
$this->checkConnection();
try {
$oldPrivateKeyId = $this->server->private_key_id;
refresh_server_connection($this->server->privateKey);
$this->server->update([
'private_key_id' => $newPrivateKeyId
]);
$this->server->refresh();
refresh_server_connection($this->server->privateKey);
$this->checkConnection();
} catch (\Throwable $e) {
$this->server->update([
'private_key_id' => $oldPrivateKeyId
]);
$this->server->refresh();
refresh_server_connection($this->server->privateKey);
return handleError($e, $this);
}
}
public function checkConnection()
{
try {
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->server);
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->server, true);
if ($uptime) {
Toaster::success('Server is reachable with this private key.');
$this->server->settings->update([
'is_reachable' => true
]);
$this->emit('success', 'Server is reachable with this private key.');
} else {
$this->server->settings->update([
'is_reachable' => false,
'is_usable' => false
]);
$this->emit('error', 'Server is not reachable with this private key.');
return;
}
if ($dockerVersion) {
Toaster::success('Server is usable for Coolify.');
$this->server->settings->update([
'is_usable' => true
]);
$this->emit('success', 'Server is usable for Coolify.');
} else {
$this->server->settings->update([
'is_usable' => false
]);
$this->emit('error', 'Old (lower than 23) or no Docker version detected. Install Docker Engine on the General tab.');
}
} catch (\Exception $e) {
return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Http\Livewire\Settings;
use App\Jobs\ProxyStartJob;
use App\Jobs\ContainerStatusJob;
use App\Models\InstanceSettings as ModelsInstanceSettings;
use App\Models\Server;
use Livewire\Component;
@@ -45,7 +45,12 @@ class Configuration extends Component
$this->settings->do_not_track = $this->do_not_track;
$this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
$this->settings->is_registration_enabled = $this->is_registration_enabled;
$this->settings->next_channel = $this->next_channel;
if ($this->next_channel) {
$this->settings->next_channel = false;
$this->next_channel = false;
} else {
$this->settings->next_channel = $this->next_channel;
}
$this->settings->save();
$this->emit('success', 'Settings updated!');
}
@@ -68,7 +73,7 @@ class Configuration extends Component
{
$file = "$this->dynamic_config_path/coolify.yaml";
if (empty($this->settings->fqdn)) {
remote_process([
instant_remote_process([
"rm -f $file",
], $this->server);
} else {
@@ -124,7 +129,6 @@ class Configuration extends Component
];
}
$this->save_configuration_to_disk($traefik_dynamic_conf, $file);
dispatch(new ProxyStartJob($this->server));
}
}
@@ -137,7 +141,7 @@ class Configuration extends Component
$yaml;
$base64 = base64_encode($yaml);
remote_process([
instant_remote_process([
"mkdir -p $this->dynamic_config_path",
"echo '$base64' | base64 -d > $file",
], $this->server);

View File

@@ -50,8 +50,8 @@ class Email extends Component
]);
$this->settings->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
return general_error_handler($e, $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submitResend() {
@@ -60,20 +60,28 @@ class Email extends Component
$this->validate([
'settings.resend_api_key' => 'required'
]);
$this->settings->smtp_enabled = false;
$this->settings->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
} catch (\Throwable $e) {
$this->settings->resend_enabled = false;
return general_error_handler($e, $this);
return handleError($e, $this);
}
}
public function instantSaveResend() {
try {
$this->settings->smtp_enabled = false;
$this->submitResend();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
$this->settings->resend_enabled = false;
$this->submit();
} catch (\Exception $e) {
return general_error_handler($e, $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
@@ -89,11 +97,10 @@ class Email extends Component
'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);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}

View File

@@ -51,8 +51,8 @@ class Change extends Component
try {
$this->validate();
$this->github_app->save();
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
@@ -65,8 +65,8 @@ class Change extends Component
try {
$this->github_app->delete();
redirect()->route('source.all');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -32,22 +32,25 @@ class Create extends Component
"custom_port" => 'required|int',
"is_system_wide" => 'required|bool',
]);
$github_app = GithubApp::create([
$payload = [
'name' => $this->name,
'organization' => $this->organization,
'api_url' => $this->api_url,
'html_url' => $this->html_url,
'custom_user' => $this->custom_user,
'custom_port' => $this->custom_port,
'is_system_wide' => $this->is_system_wide,
'team_id' => currentTeam()->id,
]);
];
if (isCloud()) {
$payload['is_system_wide'] = $this->is_system_wide;
}
$github_app = GithubApp::create($payload);
if (session('from')) {
session(['from' => session('from') + ['source_id' => $github_app->id]]);
}
redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]);
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -30,8 +30,8 @@ class Actions extends Component
$this->emit('success', 'Subscription cancelled successfully. Reloading in 5s.');
$this->emit('reloadWindow', 5000);
}
} catch (\Exception $e) {
return general_error_handler($e, $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function resume()
@@ -65,8 +65,8 @@ class Actions extends Component
$this->emit('success', 'Subscription resumed successfully. Reloading in 5s.');
$this->emit('reloadWindow', 5000);
}
} catch (\Exception $e) {
return general_error_handler($e, $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function stripeCustomerPortal() {

View File

@@ -8,8 +8,13 @@ use Stripe\Checkout\Session;
class PricingPlans extends Component
{
public bool $isTrial = false;
public function mount() {
$this->isTrial = !data_get(currentTeam(),'subscription.stripe_trial_already_ended');
}
public function subscribeStripe($type)
{
$team = currentTeam();
Stripe::setApiKey(config('subscription.stripe_api_key'));
switch ($type) {
case 'basic-monthly':
@@ -39,6 +44,7 @@ class PricingPlans extends Component
return;
}
$payload = [
'billing_address_collection' => 'required',
'client_reference_id' => auth()->user()->id . ':' . currentTeam()->id,
'line_items' => [[
'price' => $priceId,
@@ -47,10 +53,26 @@ class PricingPlans extends Component
'tax_id_collection' => [
'enabled' => true,
],
'automatic_tax' => [
'enabled' => true,
],
'mode' => 'subscription',
'success_url' => route('dashboard', ['success' => true]),
'cancel_url' => route('subscription.index', ['cancelled' => true]),
];
if (!data_get($team,'subscription.stripe_trial_already_ended')) {
$payload['subscription_data'] = [
'trial_period_days' => config('constants.limits.trial_period'),
'trial_settings' => [
'end_behavior' => [
'missing_payment_method' => 'cancel',
]
],
];
$payload['payment_method_collection'] = 'if_required';
}
$customer = currentTeam()->subscription?->stripe_customer_id ?? null;
if ($customer) {
$payload['customer'] = $customer;

View File

@@ -23,7 +23,7 @@ class SwitchTeam extends Component
if (!$team_to_switch_to) {
return;
}
session(['currentTeam' => $team_to_switch_to]);
refreshSession($team_to_switch_to);
return redirect(request()->header('Referer'));
}
}

View File

@@ -31,8 +31,8 @@ class Create extends Component
auth()->user()->teams()->attach($team, ['role' => 'admin']);
refreshSession();
return redirect()->route('team.index');
} catch (\Throwable $th) {
return general_error_handler($th, $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -12,7 +12,6 @@ class Delete extends Component
$currentTeam = currentTeam();
$currentTeam->delete();
$team = auth()->user()->teams()->first();
$currentTeam->members->each(function ($user) use ($currentTeam) {
if ($user->id === auth()->user()->id) {
return;

View File

@@ -28,8 +28,8 @@ class Form extends Component
try {
$this->team->save();
refreshSession();
} catch (\Throwable $th) {
return general_error_handler($th, $this);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -4,9 +4,13 @@ namespace App\Http\Livewire\Team;
use App\Models\TeamInvitation;
use App\Models\User;
use App\Notifications\TransactionalEmails\InvitationLink;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Artisan;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class InviteLink extends Component
{
@@ -20,64 +24,74 @@ class InviteLink extends Component
public function viaEmail()
{
$this->generate_invite_link(isEmail: true);
$this->generate_invite_link(sendEmail: true);
}
private function generate_invite_link(bool $isEmail = false)
public function viaLink()
{
$this->generate_invite_link(sendEmail: false);
}
private function generate_invite_link(bool $sendEmail = false)
{
try {
$uuid = new Cuid2(32);
$link = url('/') . config('constants.invitation.link.base_url') . $uuid;
$user = User::whereEmail($this->email);
if (!$user->exists()) {
return general_error_handler(that: $this, customErrorMessage: "$this->email must be registered first (or activate transactional emails to invite via email).");
}
$member_emails = currentTeam()->members()->get()->pluck('email');
if ($member_emails->contains($this->email)) {
return general_error_handler(that: $this, customErrorMessage: "$this->email is already a member of " . currentTeam()->name . ".");
return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of " . currentTeam()->name . ".");
}
$uuid = new Cuid2(32);
$link = url('/') . config('constants.invitation.link.base_url') . $uuid;
$user = User::whereEmail($this->email)->first();
$invitation = TeamInvitation::whereEmail($this->email);
if ($invitation->exists()) {
$created_at = $invitation->first()->created_at;
$diff = $created_at->diffInMinutes(now());
if ($diff <= config('constants.invitation.link.expiration')) {
return general_error_handler(that: $this, customErrorMessage: "Invitation already sent to $this->email and waiting for action.");
if (is_null($user)) {
$password = Str::password();
$user = User::create([
'name' => Str::of($this->email)->before('@'),
'email' => $this->email,
'password' => Hash::make($password),
'force_password_reset' => true,
]);
$token = Crypt::encryptString("{$user->email}@@@$password");
$link = route('auth.link', ['token' => $token]);
}
$invitation = TeamInvitation::whereEmail($this->email)->first();
if (!is_null($invitation)) {
$invitationValid = $invitation->isValid();
if ($invitationValid) {
return handleError(livewire: $this, customErrorMessage: "Pending invitation already exists for $this->email.");
} else {
$invitation->delete();
}
}
TeamInvitation::firstOrCreate([
$invitation = TeamInvitation::firstOrCreate([
'team_id' => currentTeam()->id,
'uuid' => $uuid,
'email' => $this->email,
'role' => $this->role,
'link' => $link,
'via' => $isEmail ? 'email' : 'link',
'via' => $sendEmail ? 'email' : 'link',
]);
if ($isEmail) {
$user->first()->notify(new InvitationLink);
if ($sendEmail) {
$mail = new MailMessage();
$mail->view('emails.invitation-link', [
'team' => currentTeam()->name,
'invitation_link' => $link,
]);
$mail->subject('You have been invited to ' . currentTeam()->name . ' on ' . config('app.name') . '.');
send_user_an_email($mail, $this->email);
$this->emit('success', 'Invitation sent via email successfully.');
$this->emit('refreshInvitations');
return;
} else {
$this->emit('success', 'Invitation link generated.');
$this->emit('refreshInvitations');
}
$this->emit('refreshInvitations');
} catch (\Throwable $e) {
$error_message = $e->getMessage();
if ($e->getCode() === '23505') {
$error_message = 'Invitation already sent.';
}
return general_error_handler(err: $e, that: $this, customErrorMessage: $error_message);
return handleError(error: $e, livewire: $this, customErrorMessage: $error_message);
}
}
public function viaLink()
{
$this->generate_invite_link();
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Http\Livewire\Team;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;
class Member extends Component
@@ -24,6 +25,10 @@ class Member extends Component
public function remove()
{
$this->member->teams()->detach(currentTeam());
Cache::forget("team:{$this->member->id}");
Cache::remember('team:' . $this->member->id, 3600, function() {
return $this->member->teams()->first();
});
$this->emit('reloadWindow');
}
}

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