Compare commits

...

145 Commits

Author SHA1 Message Date
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
Andras Bacsai
4de4587ea6 Merge pull request #1196 from coollabsio/next
version++
2023-09-01 16:09:23 +02:00
Andras Bacsai
8675e1d13f version++ 2023-09-01 16:09:03 +02:00
Andras Bacsai
6ceacc68cc more unique container name 2023-09-01 16:07:46 +02:00
Andras Bacsai
4aacf134b7 Merge pull request #1195 from coollabsio/next
v4.0.0-beta.23
2023-09-01 16:03:28 +02:00
Andras Bacsai
0605772715 better emails 2023-09-01 15:55:55 +02:00
Andras Bacsai
3fa53556f4 better emails 2023-09-01 15:52:18 +02:00
Andras Bacsai
76510b8971 fix: button loading animation 2023-09-01 11:35:40 +02:00
Andras Bacsai
66162966b9 fix: sentry bug 2023-09-01 11:35:33 +02:00
Andras Bacsai
71e1571c39 Add affected users to sentry 2023-09-01 11:20:58 +02:00
Andras Bacsai
806b761e74 Merge pull request #1194 from coollabsio/next
Sentry version update
2023-09-01 10:33:19 +02:00
Andras Bacsai
0176b38958 oops 2023-09-01 10:32:29 +02:00
Andras Bacsai
7a180c7310 Merge pull request #1193 from coollabsio/next
v4.0.0-beta.21
2023-09-01 10:27:49 +02:00
Andras Bacsai
3e1120182c update 2023-09-01 10:21:35 +02:00
Andras Bacsai
8e86ce671c fix: dockerimage jobs are not overlapping 2023-09-01 10:11:00 +02:00
Andras Bacsai
f75a324030 fix: proxy start job 2023-09-01 10:02:24 +02:00
Andras Bacsai
3fabff93f6 fix a few things 2023-09-01 09:34:25 +02:00
Andras Bacsai
e74efc4e76 fix 2023-08-31 21:56:53 +02:00
Andras Bacsai
472ed0753d update 2023-08-31 21:51:05 +02:00
Andras Bacsai
67538ff60c no double slot 2023-08-31 21:46:30 +02:00
Andras Bacsai
ae8bd69106 able to use resend for pro+ users 2023-08-31 15:00:59 +02:00
Andras Bacsai
2538890b52 feat: add resend as transactional emails 2023-08-31 13:10:39 +02:00
Andras Bacsai
87dd819ae4 fix: password confirmation 2023-08-31 09:56:37 +02:00
Andras Bacsai
7ec560d4a2 update 2023-08-30 18:36:06 +02:00
Andras Bacsai
6f9cd6a16b no localhost in cloud 2023-08-30 18:35:20 +02:00
Andras Bacsai
923af88336 fix: subscriptions 2023-08-30 18:23:55 +02:00
Andras Bacsai
5b6667c461 refactor + fixes 2023-08-30 16:01:38 +02:00
Andras Bacsai
6f00740f67 better boarding flow 2023-08-30 14:46:51 +02:00
Andras Bacsai
248863cf16 update boarding process 2023-08-30 11:26:46 +02:00
Andras Bacsai
97d48823dd improve boarding 2023-08-30 11:06:44 +02:00
Andras Bacsai
5eb41e1a15 fix boarding 2023-08-29 20:34:01 +02:00
Andras Bacsai
4a4837d9f5 fix 2023-08-29 20:25:42 +02:00
Andras Bacsai
4ad72fab7b refactor 2023-08-29 16:31:46 +02:00
Andras Bacsai
fe68e45609 refactor 2023-08-29 15:51:30 +02:00
Andras Bacsai
291b9a84ef refactoring 2023-08-29 14:36:17 +02:00
Andras Bacsai
2f9b7b188a ui update 2023-08-29 10:11:18 +02:00
Andras Bacsai
d04d41bc23 update dockercleanupjob 2023-08-29 10:00:29 +02:00
Andras Bacsai
6cb3d7167f fix ui 2023-08-28 23:27:46 +02:00
Andras Bacsai
90b1659a18 fix 2023-08-28 22:25:18 +02:00
Andras Bacsai
1aaf44f9b0 fix 2023-08-28 21:22:53 +02:00
Andras Bacsai
d7cfb84351 fix gh create button 2023-08-28 21:15:30 +02:00
Andras Bacsai
d28cf0b76d fix: webhook endpoint in cloud and no system wide gh app 2023-08-28 21:03:07 +02:00
Andras Bacsai
b4a3236284 fix 2023-08-28 20:52:45 +02:00
Andras Bacsai
556168892d fix job 2023-08-28 20:45:53 +02:00
Andras Bacsai
77667be570 fix: validation 2023-08-28 20:29:44 +02:00
Andras Bacsai
f48a912287 update cloud no localhost server 2023-08-28 18:02:31 +02:00
Andras Bacsai
af30d0831d do not seed in coolify cloud 2023-08-28 15:03:44 +02:00
Andras Bacsai
5989eb8f6e rename license server 2023-08-28 15:02:03 +02:00
Andras Bacsai
2bb778834b remove cloud configs 2023-08-28 14:40:18 +02:00
Andras Bacsai
a5ce191e4d add example cloud 2023-08-28 13:33:20 +02:00
Andras Bacsai
7617756576 remove dynamic sentry version checker 2023-08-28 13:32:36 +02:00
Andras Bacsai
0dfd3a5b0e run scheduled jobs on one server 2023-08-28 11:43:01 +02:00
Andras Bacsai
61a54f48c5 update 2023-08-28 10:44:11 +02:00
Andras Bacsai
5ca0237e34 fix logging on ui 2023-08-27 22:05:37 +02:00
Andras Bacsai
ab1207e461 fix:dockerCleanupjob 2023-08-27 21:36:11 +02:00
Andras Bacsai
75fea4f7c0 disable docker cleanup job 2023-08-27 16:23:02 +02:00
Andras Bacsai
fb34eb5394 new version 2023-08-27 15:49:35 +02:00
Andras Bacsai
41101217c6 Merge pull request #1187 from coollabsio/next
v4.0.0.beta-21
2023-08-27 15:48:33 +02:00
Andras Bacsai
19b1f5004a fix 2023-08-27 15:44:36 +02:00
Andras Bacsai
75fcd88f73 do not show boarding flow yet 2023-08-27 15:28:48 +02:00
Andras Bacsai
c21ce45d70 show public key of generated private key 2023-08-27 15:23:47 +02:00
Andras Bacsai
9f10cb2899 updates 2023-08-27 15:08:53 +02:00
Andras Bacsai
aa0c621223 update ignores 2023-08-27 15:03:53 +02:00
Andras Bacsai
f1dfb9051c delete file 2023-08-27 15:03:26 +02:00
Andras Bacsai
932e58531d fix: fqdn on apps 2023-08-27 14:55:57 +02:00
Andras Bacsai
35a75e1066 disable stopped notification until fixed 2023-08-27 14:53:16 +02:00
Andras Bacsai
522713473d proxy status poll every 10 seconds 2023-08-25 16:11:41 +02:00
Andras Bacsai
3d6e268d15 potential fix: dockercleanuppjob 2023-08-25 13:04:21 +02:00
Andras Bacsai
cf8129dcbb add gh private repo add new gh app 2023-08-25 12:50:52 +02:00
Andras Bacsai
3985cca8cc add pure dockerfile in seeder 2023-08-25 12:40:53 +02:00
Andras Bacsai
86ab65ef66 updates 2023-08-25 12:13:22 +02:00
Andras Bacsai
9db9616a43 fix route subs 2023-08-25 09:53:40 +02:00
Andras Bacsai
39fd6f054b fix 2023-08-24 21:42:47 +02:00
Andras Bacsai
9b5f6ceca8 fix server validated 2023-08-24 21:38:18 +02:00
Andras Bacsai
746da1a76e temporary fix 2023-08-24 21:34:24 +02:00
Andras Bacsai
0a2d0da36f fix proxy check jo 2023-08-24 21:24:44 +02:00
Andras Bacsai
0076455e6e fix proxy job 2023-08-24 21:20:24 +02:00
Andras Bacsai
b674a0ed88 throw errors in jobs 2023-08-24 21:09:58 +02:00
Andras Bacsai
90dba34ecc fix 2023-08-24 21:04:17 +02:00
Andras Bacsai
01b40b26f5 do not send discord message if there is not url 2023-08-24 21:00:19 +02:00
Andras Bacsai
c0805a285e update 2023-08-24 20:58:51 +02:00
Andras Bacsai
ac76870d67 fix dbcontainerstatusjob 2023-08-24 20:52:43 +02:00
Andras Bacsai
7160f50322 update autoupdate job 2023-08-24 20:51:14 +02:00
Andras Bacsai
ba39f2595c update 2023-08-24 20:49:54 +02:00
Andras Bacsai
38688b7065 fix 2023-08-24 17:52:43 +02:00
Andras Bacsai
9ef3218bb5 updates 2023-08-24 17:41:11 +02:00
Andras Bacsai
6f14e127a3 fix things 2023-08-24 17:08:32 +02:00
Andras Bacsai
39890b319a add stripe subscription 2023-08-24 16:14:09 +02:00
Andras Bacsai
2d8f166e4a update a few things 2023-08-23 16:40:59 +02:00
Andras Bacsai
d62af76097 update boarding flow 2023-08-23 10:14:39 +02:00
Andras Bacsai
b39ca51d41 wip: boarding 2023-08-22 17:44:49 +02:00
Andras Bacsai
2414ddd360 remove seeders in dev 2023-08-21 20:19:47 +02:00
Andras Bacsai
bed959f1cd feat: rolling update 2023-08-21 18:00:12 +02:00
Andras Bacsai
a3f3470137 add resend as mailer 2023-08-21 11:11:51 +02:00
Andras Bacsai
b7ec1d7d65 fix: limits & server creation page 2023-08-21 10:18:11 +02:00
Andras Bacsai
7e37068fc0 send notification of autoupdate 2023-08-17 16:26:55 +02:00
Andras Bacsai
d049acad70 update compose for prod 2023-08-17 16:22:49 +02:00
Andras Bacsai
07044680d4 change secrets for bunny sync 2023-08-17 16:20:27 +02:00
Andras Bacsai
847b3fe54f feat: invite by email from waitlist 2023-08-17 16:12:08 +02:00
284 changed files with 5871 additions and 2900 deletions

View File

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

View File

@@ -6,6 +6,7 @@
USERID=
GROUPID=
############################################################################################################
APP_NAME=Coolify-localhost
APP_ID=development
APP_ENV=local
APP_KEY=
@@ -14,3 +15,9 @@ APP_URL=http://localhost
APP_PORT=8000
DUSK_DRIVER_URL=http://selenium:4444
## For Andras only
# To purge cache
BUNNY_API_KEY=
# To upload assets
BUNNY_STORAGE_API_KEY=

View File

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

View File

@@ -0,0 +1,84 @@
name: Coolify Helper Image Development (v4)
on:
push:
branches: [ "next" ]
paths:
- .github/workflows/coolify-helper.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:
@@ -78,3 +78,7 @@ jobs:
- name: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}: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
@@ -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 }}

3
.gitignore vendored
View File

@@ -29,3 +29,6 @@ _ide_helper.php
.gitignore
.phpstorm.meta.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

@@ -17,7 +17,7 @@ class StartPostgresql
public function __invoke(Server $server, StandalonePostgresql $database)
{
$this->database = $database;
$container_name = generate_container_name($this->database->uuid);
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
@@ -36,7 +36,7 @@ class StartPostgresql
'image' => $this->database->image,
'container_name' => $container_name,
'environment' => $environment_variables,
'restart' => 'always',
'restart' => RESTART_MODE,
'networks' => [
$this->database->destination->network,
],
@@ -65,7 +65,7 @@ class StartPostgresql
],
'networks' => [
$this->database->destination->network => [
'external' => false,
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]

View File

@@ -14,14 +14,14 @@ class CheckResaleLicense
$settings->update([
'is_resale_license_active' => false,
]);
if (is_dev()) {
if (isDev()) {
return;
}
if (!$settings->resale_license) {
return;
}
$base_url = config('coolify.license_url');
if (is_dev()) {
if (isDev()) {
$base_url = 'http://host.docker.internal:8787';
}
$instance_id = config('app.id');

View File

@@ -9,15 +9,20 @@ class SaveConfigurationSync
{
public function __invoke(Server $server, string $configuration)
{
$proxy_path = get_proxy_path();
$docker_compose_yml_base64 = base64_encode($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();
$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);
}
instant_remote_process([
"mkdir -p $proxy_path",
"echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml",
], $server);
}
}

View File

@@ -12,12 +12,6 @@ class StartProxy
{
public function __invoke(Server $server): Activity
{
// TODO: check for other proxies
if (is_null(data_get($server, 'proxy.type'))) {
$server->proxy->type = ProxyTypes::TRAEFIK_V2->value;
$server->proxy->status = ProxyStatus::EXITED->value;
$server->save();
}
$proxy_path = get_proxy_path();
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];

View File

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

View File

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

View File

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

View File

@@ -6,35 +6,56 @@ 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';
protected $signature = 'waitlist:invite {email?} {--only-email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send invitation to the next user in the waitlist';
protected $description = 'Send invitation to the next user (or by email) in the waitlist';
/**
* Execute the console command.
*/
public function handle()
{
$this->next_patient = Waitlist::orderBy('created_at', 'asc')->where('verified', true)->first();
if ($this->argument('email')) {
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;
}
} else {
$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();
@@ -47,7 +68,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),
@@ -65,10 +86,14 @@ class InviteFromWaitlist extends Command
}
private function send_email()
{
ray($this->next_patient->email, $this->password);
$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

@@ -2,14 +2,19 @@
namespace App\Console;
use App\Jobs\ApplicationContainerStatusJob;
use App\Jobs\CheckResaleLicenseJob;
use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\DatabaseBackupJob;
use App\Jobs\DatabaseContainerStatusJob;
use App\Jobs\DockerCleanupJob;
use App\Jobs\InstanceAutoUpdateJob;
use App\Jobs\ProxyCheckJob;
use App\Jobs\ResourceStatusJob;
use App\Models\Application;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\StandalonePostgresql;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -17,28 +22,46 @@ class Kernel extends ConsoleKernel
{
protected function schedule(Schedule $schedule): void
{
// $schedule->call(fn() => $this->check_scheduled_backups($schedule))->everyTenSeconds();
if (is_dev()) {
if (isDev()) {
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new ResourceStatusJob)->everyMinute();
// $schedule->job(new ResourceStatusJob)->everyMinute();
$schedule->job(new ProxyCheckJob)->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute();
// $schedule->job(new CheckResaleLicenseJob)->hourly();
// $schedule->job(new DockerCleanupJob)->everyOddHour();
// $schedule->job(new InstanceAutoUpdateJob(true))->everyMinute();
$schedule->job(new DockerCleanupJob)->everyOddHour();
} else {
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute();
$schedule->job(new ResourceStatusJob)->everyMinute();
$schedule->job(new CheckResaleLicenseJob)->hourly();
$schedule->job(new ProxyCheckJob)->everyFiveMinutes();
$schedule->job(new DockerCleanupJob)->everyTenMinutes();
$schedule->job(new InstanceAutoUpdateJob)->everyTenMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyTenMinutes()->onOneServer();
// $schedule->job(new ResourceStatusJob)->everyMinute()->onOneServer();
$schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
$schedule->job(new ProxyCheckJob)->everyFiveMinutes()->onOneServer();
$schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer();
}
$this->instance_auto_update($schedule);
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
}
private function check_resources($schedule)
{
$applications = Application::all();
foreach ($applications as $application) {
$schedule->job(new ApplicationContainerStatusJob($application))->everyMinute()->onOneServer();
}
$postgresqls = StandalonePostgresql::all();
foreach ($postgresqls as $postgresql) {
$schedule->job(new DatabaseContainerStatusJob($postgresql))->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();
}
}
private function check_scheduled_backups($schedule)
{
ray('check_scheduled_backups');
@@ -57,7 +80,7 @@ class Kernel extends ConsoleKernel
}
$schedule->job(new DatabaseBackupJob(
backup: $scheduled_backup
))->cron($scheduled_backup->frequency);
))->cron($scheduled_backup->frequency)->onOneServer();
}
}

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

@@ -5,6 +5,7 @@ namespace App\Exceptions;
use App\Models\InstanceSettings;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Sentry\Laravel\Integration;
use Sentry\State\Scope;
use Throwable;
class Handler extends ExceptionHandler
@@ -45,9 +46,14 @@ class Handler extends ExceptionHandler
{
$this->reportable(function (Throwable $e) {
$this->settings = InstanceSettings::get();
if ($this->settings->do_not_track || is_dev()) {
if ($this->settings->do_not_track || isDev()) {
return;
}
app('sentry')->configureScope(
function (Scope $scope){
$scope->setUser(['id'=> config('sentry.server_name')]);
}
);
Integration::captureUnhandledException($e);
});
}

View File

@@ -12,7 +12,7 @@ class ApplicationController extends Controller
public function configuration()
{
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
@@ -29,7 +29,7 @@ class ApplicationController extends Controller
public function deployments()
{
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
@@ -49,7 +49,7 @@ class ApplicationController extends Controller
{
$deploymentUuid = request()->route('deployment_uuid');
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}

View File

@@ -5,39 +5,57 @@ namespace App\Http\Controllers;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\S3Storage;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use App\Models\TeamInvitation;
use App\Models\User;
use App\Models\Waitlist;
use Auth;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http;
use Throwable;
use Str;
class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
public function waitlist() {
$waiting_in_line = Waitlist::whereVerified(true)->count();
return view('auth.waitlist', [
'waiting_in_line' => $waiting_in_line,
]);
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)) {
Auth::login($user);
$team = $user->teams()->first();
session(['currentTeam' => $team]);
return redirect()->route('dashboard');
}
}
return redirect()->route('login')->with('error', 'Invalid credentials.');
}
public function subscription()
{
if (!is_cloud()) {
if (!isCloud()) {
abort(404);
}
return view('subscription', [
return view('subscription.index', [
'settings' => InstanceSettings::get(),
]);
}
public function license()
{
if (!is_cloud()) {
if (!isCloud()) {
abort(404);
}
return view('settings.license', [
@@ -45,31 +63,22 @@ class Controller extends BaseController
]);
}
public function force_passoword_reset() {
public function force_passoword_reset()
{
return view('auth.force-password-reset');
}
public function dashboard()
public function boarding()
{
$projects = Project::ownedByCurrentTeam()->get();
$servers = Server::ownedByCurrentTeam()->get();
$s3s = S3Storage::ownedByCurrentTeam()->get();
$resources = 0;
foreach ($projects as $project) {
$resources += $project->applications->count();
$resources += $project->postgresqls->count();
if (currentTeam()->boarding || isDev()) {
return view('boarding');
} else {
return redirect()->route('dashboard');
}
return view('dashboard', [
'servers' => $servers->count(),
'projects' => $projects->count(),
'resources' => $resources,
's3s' => $s3s,
]);
}
public function settings()
{
if (is_instance_admin()) {
if (isInstanceAdmin()) {
$settings = InstanceSettings::get();
$database = StandalonePostgresql::whereName('coolify-db')->first();
if ($database) {
@@ -89,9 +98,9 @@ class Controller extends BaseController
{
$invitations = [];
if (auth()->user()->isAdminFromSession()) {
$invitations = TeamInvitation::whereTeamId(auth()->user()->currentTeam()->id)->get();
$invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
}
return view('team.show', [
return view('team.index', [
'invitations' => $invitations,
]);
}
@@ -116,7 +125,7 @@ class Controller extends BaseController
{
$invitations = [];
if (auth()->user()->isAdminFromSession()) {
$invitations = TeamInvitation::whereTeamId(auth()->user()->currentTeam()->id)->get();
$invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
}
return view('team.members', [
'invitations' => $invitations,
@@ -140,7 +149,7 @@ class Controller extends BaseController
if ($diff <= config('constants.invitation.link.expiration')) {
$user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
$invitation->delete();
return redirect()->route('team.show');
return redirect()->route('team.index');
} else {
$invitation->delete();
abort(401);
@@ -162,7 +171,7 @@ class Controller extends BaseController
abort(401);
}
$invitation->delete();
return redirect()->route('team.show');
return redirect()->route('team.index');
} catch (Throwable $th) {
throw $th;
}

View File

@@ -11,7 +11,7 @@ class DatabaseController extends Controller
public function configuration()
{
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
@@ -29,7 +29,7 @@ class DatabaseController extends Controller
public function executions()
{
$backup_uuid = request()->route('backup_uuid');
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
@@ -50,13 +50,13 @@ class DatabaseController extends Controller
'database' => $database,
'backup' => $backup,
'executions' => $executions,
's3s' => auth()->user()->currentTeam()->s3s,
's3s' => currentTeam()->s3s,
]);
}
public function backups()
{
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
@@ -70,7 +70,7 @@ class DatabaseController extends Controller
}
return view('project.database.backups.all', [
'database' => $database,
's3s' => auth()->user()->currentTeam()->s3s,
's3s' => currentTeam()->s3s,
]);
}
}

View File

@@ -41,7 +41,7 @@ class MagicController extends Controller
{
$project = Project::firstOrCreate(
['name' => request()->query('name') ?? generate_random_name()],
['team_id' => auth()->user()->currentTeam()->id]
['team_id' => currentTeam()->id]
);
return response()->json([
'project_uuid' => $project->uuid
@@ -68,7 +68,7 @@ class MagicController extends Controller
],
);
auth()->user()->teams()->attach($team, ['role' => 'admin']);
session(['currentTeam' => $team]);
refreshSession();
return redirect(request()->header('Referer'));
}
}

View File

@@ -18,7 +18,7 @@ class ProjectController extends Controller
public function edit()
{
$projectUuid = request()->route('project_uuid');
$teamId = auth()->user()->currentTeam()->id;
$teamId = currentTeam()->id;
$project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first();
if (!$project) {
return redirect()->route('dashboard');
@@ -29,7 +29,7 @@ class ProjectController extends Controller
public function show()
{
$projectUuid = request()->route('project_uuid');
$teamId = auth()->user()->currentTeam()->id;
$teamId = currentTeam()->id;
$project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first();
if (!$project) {
@@ -43,8 +43,9 @@ class ProjectController extends Controller
{
$type = request()->query('type');
$destination_uuid = request()->query('destination');
$server = requesT()->query('server');
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
@@ -67,7 +68,7 @@ class ProjectController extends Controller
public function resources()
{
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}

View File

@@ -12,20 +12,21 @@ class ServerController extends Controller
public function new_server()
{
if (!is_cloud() || is_instance_admin()) {
$privateKeys = PrivateKey::ownedByCurrentTeam()->get();
if (!isCloud()) {
return view('server.create', [
'limit_reached' => false,
'private_keys' => PrivateKey::ownedByCurrentTeam()->get(),
'private_keys' => $privateKeys,
]);
}
$servers = auth()->user()->currentTeam()->servers->count();
$subscription = auth()->user()->currentTeam()?->subscription->type();
$limits = config('constants.limits.server')[strtolower($subscription)];
$limit_reached = true ?? $servers >= $limits[$subscription];
$team = currentTeam();
$servers = $team->servers->count();
['serverLimit' => $serverLimit] = $team->limits;
$limit_reached = $servers >= $serverLimit;
return view('server.create', [
'limit_reached' => $limit_reached,
'private_keys' => PrivateKey::ownedByCurrentTeam()->get(),
'private_keys' => $privateKeys,
]);
}
}

View File

@@ -38,7 +38,8 @@ class Kernel extends HttpKernel
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\CheckForcePasswordReset::class,
\App\Http\Middleware\SubscriptionValid::class,
\App\Http\Middleware\IsSubscriptionValid::class,
\App\Http\Middleware\IsBoardingFlow::class,
],

View File

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

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Http\Livewire;
use App\Models\Project;
use App\Models\S3Storage;
use App\Models\Server;
use Livewire\Component;
class Dashboard extends Component
{
public int $projects = 0;
public int $servers = 0;
public int $s3s = 0;
public int $resources = 0;
public function mount()
{
$this->servers = Server::ownedByCurrentTeam()->get()->count();
$this->s3s = S3Storage::ownedByCurrentTeam()->get()->count();
$projects = Project::ownedByCurrentTeam()->get();
foreach ($projects as $project) {
$this->resources += $project->applications->count();
$this->resources += $project->postgresqls->count();
}
$this->projects = $projects->count();
}
// public function 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

@@ -60,20 +60,19 @@ class StandaloneDocker extends Component
$found = $this->server->standaloneDockers()->where('network', $this->network)->first();
if ($found) {
$this->createNetworkAndAttachToProxy();
$this->addError('network', 'Network already added to this server.');
$this->emit('error', 'Network already added to this server.');
return;
} else {
$docker = ModelsStandaloneDocker::create([
'name' => $this->name,
'network' => $this->network,
'server_id' => $this->server_id,
'team_id' => auth()->user()->currentTeam()->id
]);
}
$this->createNetworkAndAttachToProxy();
return redirect()->route('destination.show', $docker->uuid);
} catch (\Exception $e) {
return general_error_handler(err: $e);
return general_error_handler(err: $e, that: $this);
}
}

View File

@@ -1,41 +0,0 @@
<?php
namespace App\Http\Livewire\Dev;
use App\Models\S3Storage;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
use Livewire\WithFileUploads;
class S3Test extends Component
{
use WithFileUploads;
public $s3;
public $file;
public function mount()
{
$this->s3 = S3Storage::first();
}
public function save()
{
try {
$this->validate([
'file' => 'required|max:150', // 1MB Max
]);
set_s3_target($this->s3);
$this->file->storeAs('files', $this->file->getClientOriginalName(), 'custom-s3');
$this->emit('success', 'File uploaded successfully.');
} catch (\Throwable $th) {
return general_error_handler($th, $this, false);
}
}
public function get_files()
{
set_s3_target($this->s3);
dd(Storage::disk('custom-s3')->files('files'));
}
}

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 (\Exception $e) {
return general_error_handler(err: $e, that: $this);
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Livewire;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Illuminate\Notifications\Messages\MailMessage;
use Livewire\Component;
use Route;
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();
if (isDev()) {
$this->description = "I'm having trouble with {$this->path}";
$this->subject = "Help with {$this->path}";
}
}
public function submit()
{
try {
$this->rateLimit(1, 60);
$this->validate();
$subscriptionType = auth()->user()?->subscription?->type() ?? 'unknown';
$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, 'hi@coollabs.io');
$this->emit('success', 'Your message has been sent successfully. We will get in touch with you as soon as possible.');
} catch (\Exception $e) {
return general_error_handler($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) {
ray($e->getMessage());
$this->model->discord_enabled = false;
$this->team->discord_enabled = false;
$this->validate();
}
}
@@ -41,17 +45,16 @@ class DiscordSettings extends Component
public function saveModel()
{
ray($this->model);
$this->model->save();
if (is_a($this->model, Team::class)) {
session(['currentTeam' => $this->model]);
$this->team->save();
if (is_a($this->team, Team::class)) {
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

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

View File

@@ -0,0 +1,62 @@
<?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',
];
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 (\Exception $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();
if (is_a($this->team, Team::class)) {
refreshSession();
}
$this->emit('success', 'Settings saved.');
}
public function sendTestNotification()
{
$this->team->notify(new Test());
$this->emit('success', 'Test notification sent.');
}
}

View File

@@ -26,8 +26,8 @@ class Change extends Component
try {
if ($this->private_key->isEmpty()) {
$this->private_key->delete();
auth()->user()->currentTeam()->privateKeys = PrivateKey::where('team_id', auth()->user()->currentTeam()->id)->get();
return redirect()->route('private-key.all');
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
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) {
@@ -38,10 +38,7 @@ class Change extends Component
public function changePrivateKey()
{
try {
$this->private_key->private_key = trim($this->private_key->private_key);
if (!str_ends_with($this->private_key->private_key, "\n")) {
$this->private_key->private_key .= "\n";
}
$this->private_key->private_key = formatPrivateKey($this->private_key->private_key);
$this->private_key->save();
refresh_server_connection($this->private_key);
} catch (\Exception $e) {

View File

@@ -32,7 +32,7 @@ class Create extends Component
'name' => $this->name,
'description' => $this->description,
'private_key' => $this->value,
'team_id' => auth()->user()->currentTeam()->id
'team_id' => currentTeam()->id
]);
if ($this->from === 'server') {
return redirect()->route('server.create');

View File

@@ -25,7 +25,7 @@ class AddEmpty extends Component
$project = Project::create([
'name' => $this->name,
'description' => $this->description,
'team_id' => auth()->user()->currentTeam()->id,
'team_id' => currentTeam()->id,
]);
return redirect()->route('project.show', $project->uuid);
} catch (\Exception $e) {

View File

@@ -136,15 +136,20 @@ class General extends Component
public function submit()
{
ray($this->application);
try {
$this->validate();
$domains = Str::of($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return Str::of($domain)->trim()->lower();
});
$port = get_port_from_dockerfile($this->application->dockerfile);
if ($port) {
$this->application->ports_exposes = $port;
if (data_get($this->application,'fqdn')) {
$domains = Str::of($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return Str::of($domain)->trim()->lower();
});
$this->application->fqdn = $domains->implode(',');
}
if ($this->application->dockerfile) {
$port = get_port_from_dockerfile($this->application->dockerfile);
if ($port) {
$this->application->ports_exposes = $port;
}
}
if ($this->application->base_directory && $this->application->base_directory !== '/') {
$this->application->base_directory = rtrim($this->application->base_directory, '/');
@@ -152,7 +157,6 @@ class General extends Component
if ($this->application->publish_directory && $this->application->publish_directory !== '/') {
$this->application->publish_directory = rtrim($this->application->publish_directory, '/');
}
$this->application->fqdn = data_get($domains->implode(','), '', null);
$this->application->save();
$this->emit('success', 'Application settings updated!');
} catch (\Exception $e) {

View File

@@ -2,7 +2,7 @@
namespace App\Http\Livewire\Project\Application;
use App\Jobs\ContainerStatusJob;
use App\Jobs\ApplicationContainerStatusJob;
use App\Models\Application;
use App\Notifications\Application\StatusChanged;
use Livewire\Component;
@@ -22,9 +22,8 @@ class Heading extends Component
public function check_status()
{
dispatch_sync(new ContainerStatusJob(
resource: $this->application,
container_name: generate_container_name($this->application->uuid),
dispatch_sync(new ApplicationContainerStatusJob(
application: $this->application,
));
$this->application->refresh();
}
@@ -58,12 +57,21 @@ class Heading extends Component
public function stop()
{
remote_process(
["docker rm -f {$this->application->uuid}"],
$this->application->destination->server
);
$this->application->status = 'stopped';
$this->application->save();
$this->application->environment->project->team->notify(new StatusChanged($this->application));
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id);
if ($containers->count() === 0) {
return;
}
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containerName) {
remote_process(
["docker rm -f {$containerName}"],
$this->application->destination->server
);
$this->application->status = 'stopped';
$this->application->save();
// $this->application->environment->project->team->notify(new StatusChanged($this->application));
}
}
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Http\Livewire\Project\Application;
use App\Jobs\ContainerStatusJob;
use App\Jobs\ApplicationContainerStatusJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use Illuminate\Support\Collection;
@@ -25,10 +25,9 @@ class Previews extends Component
public function loadStatus($pull_request_id)
{
dispatch(new ContainerStatusJob(
resource: $this->application,
container_name: generate_container_name($this->application->uuid, $pull_request_id),
pull_request_id: $pull_request_id
dispatch(new ApplicationContainerStatusJob(
application: $this->application,
pullRequestId: $pull_request_id
));
}
@@ -82,7 +81,7 @@ class Previews extends Component
public function stop(int $pull_request_id)
{
try {
$container_name = generate_container_name($this->application->uuid, $pull_request_id);
$container_name = generateApplicationContainerName($this->application->uuid, $pull_request_id);
ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $this->application->destination->server, throwError: false);

View File

@@ -29,7 +29,7 @@ class Source extends Component
private function get_private_keys()
{
$this->private_keys = PrivateKey::whereTeamId(auth()->user()->currentTeam()->id)->get()->reject(function ($key) {
$this->private_keys = PrivateKey::whereTeamId(currentTeam()->id)->get()->reject(function ($key) {
return $key->id == $this->application->private_key_id;
});
}

View File

@@ -39,7 +39,7 @@ class CreateScheduledBackup extends Component
's3_storage_id' => $this->s3_storage_id,
'database_id' => $this->database->id,
'database_type' => $this->database->getMorphClass(),
'team_id' => auth()->user()->currentTeam()->id,
'team_id' => currentTeam()->id,
]);
$this->emit('refreshScheduledBackups');
} catch (\Exception $e) {

View File

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

View File

@@ -12,6 +12,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 +27,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 +41,43 @@ 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) {
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(Exception $e) {
$this->database->is_public = !$this->database->is_public;
return general_error_handler(err: $e, that: $this);
}
}
public function save_init_script($script)
{
$this->database->init_scripts = filter($this->database->init_scripts, fn ($s) => $s['filename'] !== $script['filename']);

View File

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

View File

@@ -7,15 +7,19 @@ use App\Models\GithubApp;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Traits\SaveFromRedirect;
use Illuminate\Support\Facades\Http;
use Livewire\Component;
use Route;
class GithubPrivateRepository extends Component
{
use SaveFromRedirect;
public $current_step = 'github_apps';
public $github_apps;
public GithubApp $github_app;
public $parameters;
public $currentRoute;
public $query;
public $type;
@@ -36,14 +40,30 @@ 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()
{
$this->currentRoute = Route::currentRouteName();
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->repositories = $this->branches = collect();
$this->github_apps = GithubApp::private();
}
public function loadRepositories($github_app_id)
{
$this->repositories = collect();

View File

@@ -43,19 +43,19 @@ class GithubPrivateRepositoryDeployKey extends Component
'publish_directory' => 'Publish directory',
];
private object $repository_url_parsed;
private GithubApp|GitlabApp $git_source;
private GithubApp|GitlabApp|null $git_source = null;
private string $git_host;
private string $git_repository;
private string $git_branch;
public function mount()
{
if (is_dev()) {
if (isDev()) {
$this->repository_url = 'https://github.com/coollabsio/coolify-examples';
}
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->private_keys = PrivateKey::where('team_id', auth()->user()->currentTeam()->id)->where('id', '!=', 0)->get();
$this->private_keys = PrivateKey::where('team_id', currentTeam()->id)->where('id', '!=', 0)->get();
}
public function instantSave()

View File

@@ -39,13 +39,13 @@ class PublicGitRepository extends Component
'publish_directory' => 'publish directory',
];
private object $repository_url_parsed;
private GithubApp|GitlabApp $git_source;
private GithubApp|GitlabApp|null $git_source = null;
private string $git_host;
private string $git_repository;
public function mount()
{
if (is_dev()) {
if (isDev()) {
$this->repository_url = 'https://github.com/coollabsio/coolify-examples';
$this->port = 3000;
}
@@ -67,18 +67,17 @@ class PublicGitRepository extends Component
public function load_branch()
{
$this->branch_found = false;
try {
$this->branch_found = false;
$this->validate([
'repository_url' => 'required|url'
]);
$this->get_git_source();
try {
$this->get_branch();
$this->selected_branch = $this->git_branch;
$this->get_branch();
$this->selected_branch = $this->git_branch;
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
}
if (!$this->branch_found && $this->git_branch == 'main') {
try {
$this->git_branch = 'master';
@@ -103,6 +102,9 @@ class PublicGitRepository extends Component
} elseif ($this->git_host == 'bitbucket.org') {
// Not supported yet
}
if (is_null($this->git_source)) {
throw new \Exception('Git source not found. What?!');
}
}
private function get_branch()

View File

@@ -3,37 +3,79 @@
namespace App\Http\Livewire\Project\New;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Countable;
use Illuminate\Support\Collection;
use Livewire\Component;
use Route;
class Select extends Component
{
public $current_step = 'type';
public ?int $server = null;
public string $type;
public string $server_id;
public string $destination_uuid;
public $servers = [];
public $destinations = [];
public Countable|array|Server $servers;
public Collection|array $standaloneDockers = [];
public Collection|array $swarmDockers = [];
public array $parameters;
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 (\Exception $e) {
// return general_error_handler($e, $this);
// }
// }
public function setType(string $type)
{
$this->type = $type;
if ($type === "existing-postgresql") {
$this->current_step = $type;
return;
}
if (count($this->servers) === 1) {
$server = $this->servers->first();
$this->setServer($server);
if (count($server->destinations()) === 1) {
$this->setDestination($server->destinations()->first()->uuid);
}
}
if (!is_null($this->server)) {
$foundServer = $this->servers->where('id', $this->server)->first();
if ($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->destinations = $server->destinations();
$this->standaloneDockers = $server->standaloneDockers;
$this->swarmDockers = $server->swarmDockers;
$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', [
@@ -46,6 +88,6 @@ class Select extends Component
public function load_servers()
{
$this->servers = Server::ownedByCurrentTeam()->get();
$this->servers = Server::isUsable()->get();
}
}

View File

@@ -19,7 +19,7 @@ class SimpleDockerfile extends Component
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
if (is_dev()) {
if (isDev()) {
$this->dockerfile = 'FROM nginx
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
@@ -59,6 +59,10 @@ CMD ["nginx", "-g", "daemon off;"]
'source_id' => 0,
'source_type' => GithubApp::class
]);
$application->update([
'name' => 'dockerfile-' . $application->id
]);
redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,

View File

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

View File

@@ -4,10 +4,12 @@ namespace App\Http\Livewire\Server;
use App\Actions\Server\InstallDocker;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class Form extends Component
{
use AuthorizesRequests;
public Server $server;
public $uptime;
public $dockerVersion;
@@ -42,7 +44,7 @@ class Form extends Component
public function installDocker()
{
$activity = resolve(InstallDocker::class)($this->server, auth()->user()->currentTeam());
$activity = resolve(InstallDocker::class)($this->server, currentTeam());
$this->emit('newMonitorActivity', $activity->id);
}
@@ -64,14 +66,20 @@ class Form extends Component
public function delete()
{
if (!$this->server->isEmpty()) {
$this->emit('error', 'Server has defined resources. Please delete them first.');
return;
try {
$this->authorize('delete', $this->server);
if (!$this->server->isEmpty()) {
$this->emit('error', 'Server has defined resources. Please delete them first.');
return;
}
$this->server->delete();
return redirect()->route('server.all');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
}
$this->server->delete();
redirect()->route('server.all');
}
}
public function submit()
{
$this->validate();

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;
@@ -65,8 +67,13 @@ class ByIp extends Component
'ip' => $this->ip,
'user' => $this->user,
'port' => $this->port,
'team_id' => auth()->user()->currentTeam()->id,
'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();

View File

@@ -12,7 +12,7 @@ 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;
@@ -20,6 +20,7 @@ class Proxy extends Component
public function mount()
{
$this->selectedProxy = $this->server->proxy->type;
$this->redirect_url = $this->server->proxy->redirect_url;
}
@@ -35,11 +36,12 @@ class Proxy extends Component
$this->emit('proxyStatusUpdated');
}
public function select_proxy(string $proxy_type)
public function select_proxy($proxy_type)
{
$this->server->proxy->type = $proxy_type;
$this->server->proxy->status = 'exited';
$this->server->save();
$this->selectedProxy = $this->server->proxy->type;
$this->emit('proxyStatusUpdated');
}

View File

@@ -17,7 +17,7 @@ class Deploy extends Component
$this->server->proxy->last_applied_settings &&
$this->server->proxy->last_saved_settings !== $this->server->proxy->last_applied_settings
) {
$this->emit('saveConfiguration', $server);
$this->emit('saveConfiguration', $this->server);
}
$activity = resolve(StartProxy::class)($this->server);
$this->emit('newMonitorActivity', $activity->id);

View File

@@ -12,10 +12,12 @@ class Status extends Component
public function get_status()
{
dispatch_sync(new ProxyContainerStatusJob(
server: $this->server
));
$this->server->refresh();
$this->emit('proxyStatusUpdated');
if (data_get($this->server,'settings.is_usable')) {
dispatch_sync(new ProxyContainerStatusJob(
server: $this->server
));
$this->server->refresh();
$this->emit('proxyStatusUpdated');
}
}
}

View File

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

View File

@@ -6,7 +6,7 @@ use App\Models\Server;
use Livewire\Component;
use Masmerise\Toaster\Toaster;
class PrivateKey extends Component
class ShowPrivateKey extends Component
{
public Server $server;
public $privateKeys;

View File

@@ -65,7 +65,7 @@ class Backup extends Component
'frequency' => '0 0 * * *',
'database_id' => $this->database->id,
'database_type' => 'App\Models\StandalonePostgresql',
'team_id' => auth()->user()->currentTeam()->id,
'team_id' => currentTeam()->id,
]);
$this->database->refresh();
$this->backup->refresh();

View File

@@ -20,6 +20,9 @@ class Email extends Component
'settings.smtp_timeout' => 'nullable',
'settings.smtp_from_address' => 'required|email',
'settings.smtp_from_name' => 'required',
'settings.resend_enabled' => 'nullable|boolean',
'settings.resend_api_key' => 'nullable'
];
protected $validationAttributes = [
'settings.smtp_from_address' => 'From Address',
@@ -30,48 +33,68 @@ class Email extends Component
'settings.smtp_encryption' => 'Encryption',
'settings.smtp_username' => 'Username',
'settings.smtp_password' => 'Password',
'settings.smtp_timeout' => 'Timeout',
'settings.resend_api_key' => 'Resend API Key'
];
public function mount()
{
$this->decrypt();
$this->emails = auth()->user()->email;
}
private function decrypt()
{
if (data_get($this->settings, 'smtp_password')) {
try {
$this->settings->smtp_password = decrypt($this->settings->smtp_password);
} catch (\Exception $e) {
}
public function submitFromFields() {
try {
$this->resetErrorBag();
$this->validate([
'settings.smtp_from_address' => 'required|email',
'settings.smtp_from_name' => 'required',
]);
$this->settings->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
return general_error_handler($e, $this);
}
}
public function submitResend() {
try {
$this->resetErrorBag();
$this->validate([
'settings.resend_api_key' => 'required'
]);
$this->settings->smtp_enabled = false;
$this->settings->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
$this->settings->resend_enabled = false;
return general_error_handler($e, $this);
}
}
public function instantSave()
{
try {
$this->submit();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
$this->settings->smtp_enabled = false;
$this->validate();
return general_error_handler($e, $this);
}
}
public function submit()
{
$this->resetErrorBag();
$this->validate();
if ($this->settings->smtp_password) {
$this->settings->smtp_password = encrypt($this->settings->smtp_password);
} else {
$this->settings->smtp_password = null;
try {
$this->resetErrorBag();
$this->validate([
'settings.smtp_host' => 'required',
'settings.smtp_port' => 'required|numeric',
'settings.smtp_encryption' => 'nullable',
'settings.smtp_username' => 'nullable',
'settings.smtp_password' => 'nullable',
'settings.smtp_timeout' => 'nullable',
]);
$this->settings->resend_enabled = false;
$this->settings->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
return general_error_handler($e, $this);
}
$this->settings->save();
$this->emit('success', 'Transaction email settings updated successfully.');
$this->decrypt();
}
public function sendTestNotification()

View File

@@ -37,9 +37,13 @@ class Change extends Component
public function mount()
{
$this->webhook_endpoint = $this->ipv4;
if (isCloud() && !isDev()) {
$this->webhook_endpoint = config('app.url');
} else {
$this->webhook_endpoint = $this->ipv4;
$this->is_system_wide = $this->github_app->is_system_wide;
}
$this->parameters = get_route_parameters();
$this->is_system_wide = $this->github_app->is_system_wide;
}
public function submit()

View File

@@ -32,16 +32,22 @@ 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' => auth()->user()->currentTeam()->id,
]);
'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);

View File

@@ -10,14 +10,14 @@ class Actions extends Component
public function cancel()
{
try {
$subscription_id = auth()->user()->currentTeam()->subscription->lemon_subscription_id;
$subscription_id = currentTeam()->subscription->lemon_subscription_id;
if (!$subscription_id) {
throw new \Exception('No subscription found');
}
$response = Http::withHeaders([
'Accept' => 'application/vnd.api+json',
'Content-Type' => 'application/vnd.api+json',
'Authorization' => 'Bearer ' . config('coolify.lemon_squeezy_api_key'),
'Authorization' => 'Bearer ' . config('subscription.lemon_squeezy_api_key'),
])->delete('https://api.lemonsqueezy.com/v1/subscriptions/' . $subscription_id);
$json = $response->json();
if ($response->failed()) {
@@ -37,14 +37,14 @@ class Actions extends Component
public function resume()
{
try {
$subscription_id = auth()->user()->currentTeam()->subscription->lemon_subscription_id;
$subscription_id = currentTeam()->subscription->lemon_subscription_id;
if (!$subscription_id) {
throw new \Exception('No subscription found');
}
$response = Http::withHeaders([
'Accept' => 'application/vnd.api+json',
'Content-Type' => 'application/vnd.api+json',
'Authorization' => 'Bearer ' . config('coolify.lemon_squeezy_api_key'),
'Authorization' => 'Bearer ' . config('subscription.lemon_squeezy_api_key'),
])->patch('https://api.lemonsqueezy.com/v1/subscriptions/' . $subscription_id, [
'data' => [
'type' => 'subscriptions',
@@ -69,4 +69,8 @@ class Actions extends Component
return general_error_handler($e, $this);
}
}
public function stripeCustomerPortal() {
$session = getStripeCustomerPortalSession(currentTeam());
redirect($session->url);
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Http\Livewire\Subscription;
use Livewire\Component;
use Stripe\Stripe;
use Stripe\Checkout\Session;
class PricingPlans extends Component
{
public function subscribeStripe($type)
{
Stripe::setApiKey(config('subscription.stripe_api_key'));
switch ($type) {
case 'basic-monthly':
$priceId = config('subscription.stripe_price_id_basic_monthly');
break;
case 'basic-yearly':
$priceId = config('subscription.stripe_price_id_basic_yearly');
break;
case 'ultimate-monthly':
$priceId = config('subscription.stripe_price_id_ultimate_monthly');
break;
case 'pro-monthly':
$priceId = config('subscription.stripe_price_id_pro_monthly');
break;
case 'pro-yearly':
$priceId = config('subscription.stripe_price_id_pro_yearly');
break;
case 'ultimate-yearly':
$priceId = config('subscription.stripe_price_id_ultimate_yearly');
break;
default:
$priceId = config('subscription.stripe_price_id_basic_monthly');
break;
}
if (!$priceId) {
$this->emit('error', 'Price ID not found! Please contact the administrator.');
return;
}
$payload = [
'client_reference_id' => auth()->user()->id . ':' . currentTeam()->id,
'line_items' => [[
'price' => $priceId,
'quantity' => 1,
]],
'tax_id_collection' => [
'enabled' => true,
],
'automatic_tax' => [
'enabled' => true,
],
'mode' => 'subscription',
'success_url' => route('dashboard', ['success' => true]),
'cancel_url' => route('subscription.index', ['cancelled' => true]),
];
$customer = currentTeam()->subscription?->stripe_customer_id ?? null;
if ($customer) {
$payload['customer'] = $customer;
$payload['customer_update'] = [
'name' => 'auto'
];
} else {
$payload['customer_email'] = auth()->user()->email;
}
$session = Session::create($payload);
return redirect($session->url, 303);
}
}

View File

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

View File

@@ -9,7 +9,7 @@ class Delete extends Component
{
public function delete()
{
$currentTeam = auth()->user()->currentTeam();
$currentTeam = currentTeam();
$currentTeam->delete();
$team = auth()->user()->teams()->first();
@@ -24,7 +24,7 @@ class Delete extends Component
}
});
session(['currentTeam' => $team]);
return redirect()->route('team.show');
refreshSession();
return redirect()->route('team.index');
}
}

View File

@@ -19,7 +19,7 @@ class Form extends Component
public function mount()
{
$this->team = auth()->user()->currentTeam();
$this->team = currentTeam();
}
public function submit()
@@ -27,8 +27,7 @@ class Form extends Component
$this->validate();
try {
$this->team->save();
session(['currentTeam' => $this->team]);
$this->emit('reloadWindow');
refreshSession();
} catch (\Throwable $th) {
return general_error_handler($th, $this);
}

View File

@@ -14,10 +14,11 @@ class Invitations extends Component
{
TeamInvitation::find($invitation_id)->delete();
$this->refreshInvitations();
$this->emit('success', 'Invitation revoked.');
}
public function refreshInvitations()
{
$this->invitations = TeamInvitation::whereTeamId(auth()->user()->currentTeam()->id)->get();
$this->invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
}
}

View File

@@ -15,7 +15,7 @@ class InviteLink extends Component
public function mount()
{
$this->email = is_dev() ? 'test3@example.com' : '';
$this->email = isDev() ? 'test3@example.com' : '';
}
public function viaEmail()
@@ -35,9 +35,9 @@ class InviteLink extends Component
return general_error_handler(that: $this, customErrorMessage: "$this->email must be registered first (or activate transactional emails to invite via email).");
}
$member_emails = auth()->user()->currentTeam()->members()->get()->pluck('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 " . auth()->user()->currentTeam()->name . ".");
return general_error_handler(that: $this, customErrorMessage: "$this->email is already a member of " . currentTeam()->name . ".");
}
$invitation = TeamInvitation::whereEmail($this->email);
@@ -53,7 +53,7 @@ class InviteLink extends Component
}
TeamInvitation::firstOrCreate([
'team_id' => auth()->user()->currentTeam()->id,
'team_id' => currentTeam()->id,
'uuid' => $uuid,
'email' => $this->email,
'role' => $this->role,

View File

@@ -11,19 +11,19 @@ class Member extends Component
public function makeAdmin()
{
$this->member->teams()->updateExistingPivot(auth()->user()->currentTeam()->id, ['role' => 'admin']);
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'admin']);
$this->emit('reloadWindow');
}
public function makeReadonly()
{
$this->member->teams()->updateExistingPivot(auth()->user()->currentTeam()->id, ['role' => 'member']);
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'member']);
$this->emit('reloadWindow');
}
public function remove()
{
$this->member->teams()->detach(auth()->user()->currentTeam());
$this->member->teams()->detach(currentTeam());
$this->emit('reloadWindow');
}
}

View File

@@ -36,7 +36,7 @@ class Create extends Component
public function mount()
{
if (is_dev()) {
if (isDev()) {
$this->name = 'Local MinIO';
$this->description = 'Local MinIO';
$this->key = 'minioadmin';
@@ -62,7 +62,7 @@ class Create extends Component
} else {
$this->storage->endpoint = $this->endpoint;
}
$this->storage->team_id = auth()->user()->currentTeam()->id;
$this->storage->team_id = currentTeam()->id;
$this->storage->testConnection();
$this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
$this->storage->save();

View File

@@ -18,7 +18,7 @@ class Upgrade extends Component
$this->latestVersion = get_latest_version_of_coolify();
$currentVersion = config('version');
version_compare($currentVersion, $this->latestVersion, '<') ? $this->isUpgradeAvailable = true : $this->isUpgradeAvailable = false;
if (is_dev()) {
if (isDev()) {
$this->isUpgradeAvailable = true;
}
$settings = InstanceSettings::get();

View File

@@ -1,23 +1,28 @@
<?php
namespace App\Http\Livewire;
namespace App\Http\Livewire\Waitlist;
use App\Jobs\SendConfirmationForWaitlistJob;
use App\Models\User;
use App\Models\Waitlist as ModelsWaitlist;
use App\Models\Waitlist;
use Livewire\Component;
class Waitlist extends Component
class Index extends Component
{
public string $email;
public int $waiting_in_line = 0;
public int $waitingInLine = 0;
protected $rules = [
'email' => 'required|email',
];
public function render()
{
return view('livewire.waitlist.index')->layout('layouts.simple');
}
public function mount()
{
if (is_dev()) {
$this->waitingInLine = Waitlist::whereVerified(true)->count();
if (isDev()) {
$this->email = 'waitlist@example.com';
}
}
@@ -29,7 +34,7 @@ class Waitlist extends Component
if ($already_registered) {
throw new \Exception('You are already on the waitlist or registered. <br>Please check your email to verify your email address or contact support.');
}
$found = ModelsWaitlist::where('email', $this->email)->first();
$found = Waitlist::where('email', $this->email)->first();
if ($found) {
if (!$found->verified) {
$this->emit('error', 'You are already on the waitlist. <br>Please check your email to verify your email address.');
@@ -38,7 +43,7 @@ class Waitlist extends Component
$this->emit('error', 'You are already on the waitlist. <br>You will be notified when your turn comes. <br>Thank you.');
return;
}
$waitlist = ModelsWaitlist::create([
$waitlist = Waitlist::create([
'email' => $this->email,
'type' => 'registration',
]);

View File

@@ -16,6 +16,12 @@ class CheckForcePasswordReset
public function handle(Request $request, Closure $next): Response
{
if (auth()->user()) {
if ($request->path() === 'auth/link') {
auth()->logout();
request()->session()->invalidate();
request()->session()->regenerateToken();
return $next($request);
}
$force_password_reset = auth()->user()->force_password_reset;
if ($force_password_reset) {
if ($request->routeIs('auth.force-password-reset') || $request->path() === 'livewire/message/force-password-reset') {

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IsBoardingFlow
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
ray()->showQueries()->color('orange');
if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
return redirect('boarding');
}
return $next($request);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IsSubscriptionValid
{
public function handle(Request $request, Closure $next): Response
{
if (isInstanceAdmin()) {
return $next($request);
}
if (!auth()->user() || !isCloud()) {
if ($request->path() === 'subscription') {
return redirect('/');
} else {
return $next($request);
}
}
if (isSubscriptionActive() && $request->path() === 'subscription') {
// ray('active subscription Middleware');
return redirect('/');
}
if (isSubscriptionOnGracePeriod()) {
// ray('is_subscription_in_grace_period Middleware');
return $next($request);
}
if (!isSubscriptionActive() && !isSubscriptionOnGracePeriod()) {
// ray('SubscriptionValid Middleware');
if (!in_array($request->path(), allowedPathsForUnsubscribedAccounts())) {
return redirect('subscription');
} else {
return $next($request);
}
}
return $next($request);
}
}

View File

@@ -1,53 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SubscriptionValid
{
public function handle(Request $request, Closure $next): Response
{
if (!auth()->user() || !is_cloud()) {
if ($request->path() === 'subscription') {
return redirect('/');
} else {
return $next($request);
}
}
$is_instance_admin = is_instance_admin();
if ($is_instance_admin) {
return $next($request);
}
if (is_subscription_active() && $request->path() === 'subscription') {
return redirect('/');
}
if (is_subscription_in_grace_period()) {
return $next($request);
}
if (!is_subscription_active() && !is_subscription_in_grace_period()) {
ray('SubscriptionValid Middleware');
$allowed_paths = [
'subscription',
'login',
'register',
'waitlist',
'force-password-reset',
'logout',
'livewire/message/force-password-reset',
'livewire/message/check-license',
'livewire/message/switch-team',
];
if (!in_array($request->path(), $allowed_paths)) {
return redirect('subscription');
} else {
return $next($request);
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Jobs;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Notifications\Application\StatusChanged;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ApplicationContainerStatusJob implements ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public string $containerName;
public function __construct(
public Application $application,
public int $pullRequestId = 0)
{
$this->containerName = generateApplicationContainerName($application->uuid, $pullRequestId);
}
public function uniqueId(): string
{
return $this->containerName;
}
public function handle(): void
{
try {
$status = getApplicationContainerStatus(application: $this->application);
if ($this->application->status === 'running' && $status !== 'running') {
// $this->application->environment->project->team->notify(new StatusChanged($this->application));
}
if ($this->pullRequestId !== 0) {
$preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pullRequestId);
$preview->status = $status;
$preview->save();
} else {
$this->application->status = $status;
$this->application->save();
}
} catch (\Exception $th) {
ray($th->getMessage());
throw $th;
}
}
}

View File

@@ -20,6 +20,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
@@ -50,6 +51,7 @@ class ApplicationDeploymentJob implements ShouldQueue
private ApplicationPreview|null $preview = null;
private string $container_name;
private string|null $currently_running_container_name = null;
private string $workdir;
private string $configuration_dir;
private string $build_workdir;
@@ -64,6 +66,7 @@ class ApplicationDeploymentJob implements ShouldQueue
private $log_model;
private Collection $saved_outputs;
public $tries = 1;
public function __construct(int $application_deployment_queue_id)
{
ray()->clearScreen();
@@ -86,7 +89,7 @@ class ApplicationDeploymentJob implements ShouldQueue
$this->build_workdir = "{$this->workdir}" . rtrim($this->application->base_directory, '/');
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->container_name = generate_container_name($this->application->uuid, $this->pull_request_id);
$this->container_name = generateApplicationContainerName($this->application->uuid, $this->pull_request_id);
$this->private_key_location = save_private_key_for_server($this->server);
$this->saved_outputs = collect();
@@ -113,6 +116,10 @@ class ApplicationDeploymentJob implements ShouldQueue
public function handle(): void
{
// ray()->measure();
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id);
if ($containers->count() > 0) {
$this->currently_running_container_name = data_get($containers[0], 'Names');
}
$this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
@@ -131,6 +138,7 @@ class ApplicationDeploymentJob implements ShouldQueue
} catch (Exception $e) {
ray($e);
$this->fail($e);
throw $e;
} finally {
if (isset($this->docker_compose_base64)) {
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
@@ -168,16 +176,16 @@ class ApplicationDeploymentJob implements ShouldQueue
$this->execute_in_builder("echo '$dockerfile_base64' | base64 -d > $this->workdir/Dockerfile")
],
);
$this->build_image_name = "{$this->application->git_repository}:build";
$this->production_image_name = "{$this->application->uuid}:latest";
$this->build_image_name = Str::lower("{$this->application->git_repository}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest");
ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->generate_compose_file();
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
$this->build_image();
$this->stop_running_container();
$this->start_by_compose_file();
$this->rolling_update();
}
private function deploy()
{
$this->execute_remote_command(
@@ -193,8 +201,8 @@ class ApplicationDeploymentJob implements ShouldQueue
$tag = $tag->substr(0, 128);
}
$this->build_image_name = "{$this->application->git_repository}:{$tag}-build";
$this->production_image_name = "{$this->application->uuid}:{$tag}";
$this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
if (!$this->force_rebuild) {
@@ -206,8 +214,7 @@ class ApplicationDeploymentJob implements ShouldQueue
"echo 'Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped...'"
]);
$this->generate_compose_file();
$this->stop_running_container();
$this->start_by_compose_file();
$this->rolling_update();
return;
}
}
@@ -219,13 +226,59 @@ class ApplicationDeploymentJob implements ShouldQueue
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
$this->build_image();
$this->stop_running_container();
$this->rolling_update();
}
private function rolling_update()
{
$this->start_by_compose_file();
$this->health_check();
$this->stop_running_container();
}
private function health_check()
{
ray('New container name: ', $this->container_name);
if ($this->container_name) {
$counter = 0;
$this->execute_remote_command(
[
"echo 'Waiting for health check to pass on the new version of your application.'"
],
);
while ($counter < $this->application->health_check_retries) {
$this->execute_remote_command(
[
"echo 'Attempt {$counter} of {$this->application->health_check_retries}'"
],
[
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
"hidden" => true,
"save" => "health_check"
],
);
$this->execute_remote_command(
[
"echo 'New version health check status: {$this->saved_outputs->get('health_check')}'"
],
);
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
$this->execute_remote_command(
[
"echo 'Rolling update completed.'"
],
);
break;
}
$counter++;
sleep($this->application->health_check_interval);
}
}
}
private function deploy_pull_request()
{
$this->build_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}-build";
$this->production_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}";
$this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}");
ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->execute_remote_command([
"echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->application->git_repository}:{$this->application->git_branch}.'",
@@ -241,18 +294,24 @@ class ApplicationDeploymentJob implements ShouldQueue
// $this->generate_build_env_variables();
// $this->add_build_env_variables_to_dockerfile();
$this->build_image();
$this->stop_running_container();
$this->start_by_compose_file();
$this->rolling_update();
}
private function prepare_builder_image()
{
$pull = "--pull=always";
if (isDev()) {
$pull = "--pull=never";
}
$helperImage = config('coolify.helper_image');
$runCommand = "docker run {$pull} -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
$this->execute_remote_command(
[
"echo -n 'Pulling latest version of the builder image (ghcr.io/coollabsio/coolify-helper).'",
"echo -n 'Pulling helper image from $helperImage.'",
],
[
"docker run --pull=always -d --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/coollabsio/coolify-helper",
$runCommand,
"hidden" => true,
],
[
@@ -409,7 +468,7 @@ class ApplicationDeploymentJob implements ShouldQueue
$this->container_name => [
'image' => $this->production_image_name,
'container_name' => $this->container_name,
'restart' => 'always',
'restart' => RESTART_MODE,
'environment' => $environment_variables,
'labels' => $this->set_labels_for_applications(),
'expose' => $ports,
@@ -437,7 +496,7 @@ class ApplicationDeploymentJob implements ShouldQueue
],
'networks' => [
$this->destination->network => [
'external' => false,
'external' => true,
'name' => $this->destination->network,
'attachable' => true,
]
@@ -539,8 +598,8 @@ class ApplicationDeploymentJob implements ShouldQueue
$schema = $url->getScheme();
$slug = Str::slug($host . $path);
$http_label = "{$this->application->uuid}-{$slug}-http";
$https_label = "{$this->application->uuid}-{$slug}-https";
$http_label = "{$this->container_name}-{$slug}-http";
$https_label = "{$this->container_name}-{$slug}-https";
if ($schema === 'https') {
// Set labels for https
@@ -597,12 +656,12 @@ class ApplicationDeploymentJob implements ShouldQueue
private function build_image()
{
$this->execute_remote_command([
"echo -n 'Building docker image.'",
"echo -n 'Building docker image for your application.'",
]);
if ($this->application->settings->is_static) {
$this->execute_remote_command([
$this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
$this->execute_in_builder("docker build --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
]);
$dockerfile = base64_encode("FROM {$this->application->static_image}
@@ -635,35 +694,34 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_in_builder("echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf")
],
[
$this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
$this->execute_in_builder("docker build --network host -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]
);
} else {
$this->execute_remote_command([
$this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
$this->execute_in_builder("docker build --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]);
}
}
private function stop_running_container()
{
$this->execute_remote_command(
["echo -n 'Removing old running application.'"],
[$this->execute_in_builder("docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true],
);
if ($this->currently_running_container_name) {
$this->execute_remote_command(
["echo -n 'Removing old version of your application.'"],
[$this->execute_in_builder("docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true],
);
}
}
private function start_by_compose_file()
{
$this->execute_remote_command(
["echo -n 'Starting new application... '"],
["echo -n 'Rolling update started.'"],
[$this->execute_in_builder("docker compose --project-directory {$this->workdir} up -d >/dev/null"), "hidden" => true],
["echo 'Done. 🎉'"],
);
}
private function generate_build_env_variables()
{
$this->build_args = collect(["--build-arg SOURCE_COMMIT={$this->commit}"]);

View File

@@ -22,7 +22,9 @@ class CheckResaleLicenseJob implements ShouldQueue
try {
resolve(CheckResaleLicense::class)();
} catch (\Throwable $th) {
send_internal_notification('CheckResaleLicenseJob failed with: ' . $th->getMessage());
ray($th);
throw $th;
}
}
}

View File

@@ -31,12 +31,13 @@ class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique
} catch (\Exception $e) {
send_internal_notification('CleanupInstanceStuffsJob failed with error: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
private function cleanup_waitlist()
{
$waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.confirmation_valid_for_minutes')))->get();
$waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.expiration')))->get();
foreach ($waitlist as $item) {
$item->delete();
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\ApplicationPreview;
use App\Notifications\Application\StatusChanged;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ContainerStatusJob implements ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public string $container_name;
public string|null $pull_request_id;
public $resource;
public function __construct($resource, string $container_name, string|null $pull_request_id = null)
{
$this->resource = $resource;
$this->container_name = $container_name;
$this->pull_request_id = $pull_request_id;
}
public function uniqueId(): string
{
return $this->container_name;
}
public function handle(): void
{
try {
$status = get_container_status(server: $this->resource->destination->server, container_id: $this->container_name, throwError: false);
if ($this->resource->status === 'running' && $status !== 'running') {
$this->resource->environment->project->team->notify(new StatusChanged($this->resource));
}
if ($this->pull_request_id) {
$preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->resource->id, $this->pull_request_id);
$preview->status = $status;
$preview->save();
} else {
$this->resource->status = $status;
$this->resource->save();
}
} catch (\Exception $e) {
ray($e->getMessage());
}
}
}

View File

@@ -28,7 +28,6 @@ class CoolifyTask implements ShouldQueue
*/
public function handle(): void
{
$remote_process = resolve(RunRemoteProcess::class, [
'activity' => $this->activity,
'ignore_errors' => $this->ignore_errors,

View File

@@ -64,35 +64,42 @@ class DatabaseBackupJob implements ShouldQueue
public function handle(): void
{
if ($this->database_status !== 'running') {
ray('database not running');
return;
}
$this->container_name = $this->database->uuid;
$this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name;
try {
if ($this->database_status !== 'running') {
ray('database not running');
return;
}
$this->container_name = $this->database->uuid;
$this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name;
if ($this->database->name === 'coolify-db') {
$this->container_name = "coolify-db";
$ip = Str::slug($this->server->ip);
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
}
$this->backup_file = "/dumpall-" . Carbon::now()->timestamp . ".sql";
$this->backup_location = $this->backup_dir . $this->backup_file;
if ($this->database->name === 'coolify-db') {
$this->container_name = "coolify-db";
$ip = Str::slug($this->server->ip);
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
}
$this->backup_file = "/pg_dump-" . Carbon::now()->timestamp . ".dump";
$this->backup_location = $this->backup_dir . $this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
if ($this->database_type === 'standalone-postgresql') {
$this->backup_standalone_postgresql();
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
if ($this->database_type === 'standalone-postgresql') {
$this->backup_standalone_postgresql();
}
$this->calculate_size();
$this->remove_old_backups();
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
$this->save_backup_logs();
// TODO: Notify user
} catch (\Throwable $th) {
ray($th->getMessage());
send_internal_notification('DatabaseBackupJob failed with: ' . $th->getMessage());
throw $th;
}
$this->calculate_size();
$this->remove_old_backups();
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
$this->save_backup_logs();
// TODO: Notify user
}
private function backup_standalone_postgresql(): void
@@ -100,7 +107,7 @@ class DatabaseBackupJob implements ShouldQueue
try {
ray($this->backup_dir);
$commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name pg_dumpall -U {$this->database->postgres_user} > $this->backup_location";
$commands[] = "docker exec $this->container_name pg_dump -Fc -U {$this->database->postgres_user} > $this->backup_location";
$this->backup_output = instant_remote_process($commands, $this->server);

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Jobs;
use App\Models\ApplicationPreview;
use App\Models\StandalonePostgresql;
use App\Notifications\Application\StatusChanged;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class DatabaseContainerStatusJob implements ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public string $containerName;
public function __construct(
public StandalonePostgresql $database,
) {
$this->containerName = $database->uuid;
}
public function uniqueId(): string
{
return $this->containerName;
}
public function handle(): void
{
try {
$status = getContainerStatus(
server: $this->database->destination->server,
container_id: $this->containerName,
throwError: false
);
if ($this->database->status === 'running' && $status !== 'running') {
if (data_get($this->database, 'environment.project.team')) {
// $this->database->environment->project->team->notify(new StatusChanged($this->database));
}
}
if ($this->database->status !== $status) {
$this->database->status = $status;
$this->database->save();
}
} catch (\Exception $e) {
send_internal_notification('DatabaseContainerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -2,61 +2,88 @@
namespace App\Jobs;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
class DockerCleanupJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 500;
public ?string $dockerRootFilesystem = null;
public ?int $usageBefore = null;
/**
* Create a new job instance.
*/
public function middleware(): array
{
return [
(new WithoutOverlapping("dockerimagejobs"))->shared(),
];
}
public function __construct()
{
//
}
/**
* Execute the job.
*/
public function handle(): void
{
$queue = ApplicationDeploymentQueue::where('status', '==', 'in_progress')->get();
if ($queue->count() > 0) {
ray('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping')->color('orange');
return;
}
try {
ray()->showQueries()->color('orange');
$servers = Server::all();
foreach ($servers as $server) {
if (is_dev()) {
$docker_root_filesystem = "/";
} else {
$docker_root_filesystem = instant_remote_process(['stat --printf=%m $(docker info --format "{{json .DockerRootDir}}" |sed \'s/"//g\')'], $server);
if (
!$server->settings->is_reachable && !$server->settings->is_usable
) {
continue;
}
$disk_percentage_before = $this->get_disk_usage($server, $docker_root_filesystem);
if ($disk_percentage_before >= $server->settings->cleanup_after_percentage) {
if (isDev()) {
$this->dockerRootFilesystem = "/";
} else {
$this->dockerRootFilesystem = instant_remote_process(
[
"stat --printf=%m $(docker info --format '{{json .DockerRootDir}}'' |sed 's/\"//g')"
],
$server,
false
);
}
if (!$this->dockerRootFilesystem) {
continue;
}
$this->usageBefore = $this->getFilesystemUsage($server);
if ($this->usageBefore >= $server->settings->cleanup_after_percentage) {
ray('Cleaning up ' . $server->name)->color('orange');
instant_remote_process(['docker image prune -af'], $server);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server);
instant_remote_process(['docker builder prune -af'], $server);
$disk_percentage_after = $this->get_disk_usage($server, $docker_root_filesystem);
if ($disk_percentage_after < $disk_percentage_before) {
ray('Saved ' . ($disk_percentage_before - $disk_percentage_after) . '% disk space on ' . $server->name);
$usageAfter = $this->getFilesystemUsage($server);
if ($usageAfter < $this->usageBefore) {
ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $server->name)->color('orange');
send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $server->name);
} else {
ray('DockerCleanupJob failed to save disk space on ' . $server->name)->color('orange');
}
} else {
ray('No need to clean up ' . $server->name)->color('orange');
}
}
} catch (\Exception $e) {
ray($e->getMessage());
send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage());
ray($e->getMessage())->color('orange');
throw $e;
}
}
private function get_disk_usage(Server $server, string $docker_root_filesystem)
private function getFilesystemUsage(Server $server)
{
$disk_usage = json_decode(instant_remote_process(['df -hP | awk \'BEGIN {printf"{\"disks\":["}{if($1=="Filesystem")next;if(a)printf",";printf"{\"mount\":\""$6"\",\"size\":\""$2"\",\"used\":\""$3"\",\"avail\":\""$4"\",\"use%\":\""$5"\"}";a++;}END{print"]}";}\''], $server), true);
$mount_point = collect(data_get($disk_usage, 'disks'))->where('mount', $docker_root_filesystem)->first();
return Str::of(data_get($mount_point, 'use%'))->trim()->replace('%', '')->value();
return instant_remote_process(["df '{$this->dockerRootFilesystem}'| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $server, false);
}
}

View File

@@ -22,18 +22,25 @@ class ProxyCheckJob implements ShouldQueue
{
try {
$container_name = 'coolify-proxy';
$servers = Server::isUsable()->whereNotNull('proxy')->get();
$servers = Server::all();
foreach ($servers as $server) {
$status = get_container_status(server: $server, container_id: $container_name);
if (
$server->settings->is_reachable === false || $server->settings->is_usable === false
) {
continue;
}
$status = getContainerStatus(server: $server, container_id: $container_name);
if ($status === 'running') {
continue;
}
// $server->team->notify(new ProxyStoppedNotification($server));
resolve(StartProxy::class)($server);
if (data_get($server, 'proxy.type')) {
resolve(StartProxy::class)($server);
}
}
} catch (\Throwable $th) {
ray($th->getMessage());
//throw $th;
send_internal_notification('ProxyCheckJob failed with: ' . $th->getMessage());
throw $th;
}
}
}

View File

@@ -39,9 +39,9 @@ class ProxyContainerStatusJob implements ShouldQueue, ShouldBeUnique
public function handle(): void
{
try {
$container = get_container_status(server: $this->server, all_data: true, container_id: 'coolify-proxy', throwError: true);
$status = $container['State']['Status'];
if ($this->server->proxy->status !== $status) {
$container = getContainerStatus(server: $this->server, all_data: true, container_id: 'coolify-proxy', throwError: false);
$status = data_get($container, 'State.Status');
if ($status && data_get($this->server, 'proxy.status') !== $status) {
$this->server->proxy->status = $status;
if ($this->server->proxy->status === 'running') {
$traefik = $container['Config']['Labels']['org.opencontainers.image.title'];
@@ -57,6 +57,9 @@ class ProxyContainerStatusJob implements ShouldQueue, ShouldBeUnique
$this->server->proxy->status = 'exited';
$this->server->save();
}
send_internal_notification('ProxyContainerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -3,6 +3,8 @@
namespace App\Jobs;
use App\Actions\Proxy\StartProxy;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -23,14 +25,20 @@ class ProxyStartJob implements ShouldQueue
try {
$container_name = 'coolify-proxy';
ray('Starting proxy for server: ' . $this->server->name);
$status = get_container_status(server: $this->server, container_id: $container_name);
$status = getContainerStatus(server: $this->server, container_id: $container_name);
if ($status === 'running') {
return;
}
if (is_null(data_get($this->server, 'proxy.type'))) {
$this->server->proxy->type = ProxyTypes::TRAEFIK_V2->value;
$this->server->proxy->status = ProxyStatus::EXITED->value;
$this->server->save();
}
resolve(StartProxy::class)($this->server);
} catch (\Throwable $th) {
send_internal_notification('ProxyStartJob failed with: ' . $th->getMessage());
ray($th->getMessage());
//throw $th;
throw $th;
}
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Jobs;
use App\Models\Application;
use App\Models\StandalonePostgresql;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -15,23 +16,31 @@ class ResourceStatusJob implements ShouldQueue, ShouldBeUnique
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $applications;
public $postgresqls;
public function __construct()
{
$this->applications = Application::all();
$this->postgresqls = StandalonePostgresql::all();
}
public function handle(): void
{
try {
foreach ($this->applications as $application) {
dispatch(new ContainerStatusJob(
resource: $application,
container_name: generate_container_name($application->uuid),
dispatch(new ApplicationContainerStatusJob(
application: $application,
));
}
} catch (\Exception $e) {
ray($e->getMessage());
foreach ($this->postgresqls as $postgresql) {
dispatch(new DatabaseContainerStatusJob(
database: $postgresql,
));
}
} catch (\Exception $th) {
send_internal_notification('ResourceStatusJob failed with: ' . $th->getMessage());
ray($th);
throw $th;
}
}
}

View File

@@ -37,7 +37,7 @@ class SendConfirmationForWaitlistJob implements ShouldQueue
$mail->subject('You are on the waitlist!');
send_user_an_email($mail, $this->email);
} catch (\Throwable $th) {
send_internal_notification('SendConfirmationForWaitlistJob failed with error: ' . $th->getMessage());
send_internal_notification("SendConfirmationForWaitlistJob failed for {$mail} with error: " . $th->getMessage());
ray($th->getMessage());
throw $th;
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Str;
class SendMessageToTelegramJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 5;
/**
* The maximum number of unhandled exceptions to allow before failing.
*/
public int $maxExceptions = 3;
public function __construct(
public string $text,
public array $buttons,
public string $token,
public string $chatId,
) {
}
/**
* Execute the job.
*/
public function handle(): void
{
$url = 'https://api.telegram.org/bot' . $this->token . '/sendMessage';
$inlineButtons = [];
if (!empty($this->buttons)) {
foreach ($this->buttons as $button) {
$buttonUrl = data_get($button, 'url');
if ($buttonUrl && Str::contains($buttonUrl, 'http://localhost')) {
$buttonUrl = str_replace('http://localhost', config('app.url'), $buttonUrl);
}
$inlineButtons[] = [
'text' => $button['text'],
'url' => $buttonUrl,
];
}
}
$payload = [
'parse_mode' => 'markdown',
'reply_markup' => json_encode([
'inline_keyboard' => [
[...$inlineButtons],
],
]),
'chat_id' => $this->chatId,
'text' => $this->text,
];
ray($payload);
$response = Http::post($url, $payload);
if ($response->failed()) {
throw new \Exception('Telegram notification failed with ' . $response->status() . ' status code.' . $response->body());
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Jobs;
use App\Models\Team;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SubscriptionInvoiceFailedJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(protected Team $team)
{
}
public function handle()
{
try {
$session = getStripeCustomerPortalSession($this->team);
$mail = new MailMessage();
$mail->view('emails.subscription-invoice-failed', [
'stripeCustomerPortal' => $session->url,
]);
$mail->subject('Your last payment was failed for Coolify Cloud.');
$this->team->members()->each(function ($member) use ($mail) {
ray($member);
if ($member->isAdmin()) {
send_user_an_email($mail, $member->email);
}
});
} catch (\Throwable $th) {
send_internal_notification('SubscriptionInvoiceFailedJob failed with: ' . $th->getMessage());
ray($th->getMessage());
throw $th;
}
}
}

View File

@@ -4,15 +4,7 @@ namespace App\Models;
class ApplicationPreview extends BaseModel
{
protected $fillable = [
'uuid',
'pull_request_id',
'pull_request_html_url',
'pull_request_issue_comment_id',
'fqdn',
'status',
'application_id',
];
protected $guarded = [];
static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id)
{

View File

@@ -41,7 +41,7 @@ class EnvironmentVariable extends Model
private function get_environment_variables(string $environment_variable): string|null
{
// $team_id = auth()->user()->currentTeam()->id;
// $team_id = currentTeam()->id;
if (str_contains(trim($environment_variable), '{{') && str_contains(trim($environment_variable), '}}')) {
$environment_variable = preg_replace('/\s+/', '', $environment_variable);
$environment_variable = str_replace('{{', '', $environment_variable);

View File

@@ -19,12 +19,12 @@ class GithubApp extends BaseModel
static public function public()
{
return GithubApp::whereTeamId(auth()->user()->currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get();
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get();
}
static public function private()
{
return GithubApp::whereTeamId(auth()->user()->currentTeam()->id)->whereisPublic(false)->whereNotNull('app_id')->get();
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(false)->whereNotNull('app_id')->get();
}
protected static function booted(): void

View File

@@ -13,6 +13,7 @@ class InstanceSettings extends Model implements SendsEmail
protected $guarded = [];
protected $casts = [
'resale_license' => 'encrypted',
'smtp_password' => 'encrypted',
];
public static function get()

View File

@@ -16,7 +16,7 @@ class PrivateKey extends BaseModel
static public function ownedByCurrentTeam(array $select = ['*'])
{
$selectArray = collect($select)->concat(['id']);
return PrivateKey::whereTeamId(auth()->user()->currentTeam()->id)->select($selectArray->all());
return PrivateKey::whereTeamId(currentTeam()->id)->select($selectArray->all());
}
public function isEmpty()

View File

@@ -4,16 +4,11 @@ namespace App\Models;
class Project extends BaseModel
{
protected $fillable = [
'name',
'description',
'team_id',
'project_id'
];
protected $guarded = [];
static public function ownedByCurrentTeam()
{
return Project::whereTeamId(auth()->user()->currentTeam()->id)->orderBy('name');
return Project::whereTeamId(currentTeam()->id)->orderBy('name');
}
protected static function booted()

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