Compare commits

..

125 Commits

Author SHA1 Message Date
Andras Bacsai
12fc5a8f91 Merge pull request #1223 from coollabsio/next
v4.0.0-beta.35
2023-09-13 12:34:45 +02:00
Andras Bacsai
9eba058cf7 fix: disable dockerfile based healtcheck for now 2023-09-13 12:08:44 +02:00
Andras Bacsai
fa4f5fea8c fix: forgot password 2023-09-13 11:29:31 +02:00
Andras Bacsai
898563fe7c email: server lost connection 2023-09-13 11:05:10 +02:00
Andras Bacsai
c418a17161 ui: show trial instead of sub 2023-09-12 17:24:46 +02:00
Andras Bacsai
cd0da04ea2 wip: server check instead of app check 2023-09-12 15:47:30 +02:00
Andras Bacsai
01e942c6a0 fix: show help modal everywhere 2023-09-12 15:06:07 +02:00
Andras Bacsai
bb9abafa82 test gh runner 2023-09-12 15:00:05 +02:00
Andras Bacsai
d0cd926517 fix: sub type 2023-09-12 14:53:54 +02:00
Andras Bacsai
9baf0161c7 fix: help should send cc on email 2023-09-12 14:51:35 +02:00
Andras Bacsai
8ba18b2ce1 fix: confirm email before sending 2023-09-12 13:19:55 +02:00
Andras Bacsai
ab021ee535 fix: server is functional check 2023-09-12 13:14:01 +02:00
Andras Bacsai
c76a1b1ba5 fix: webhooks should not run if server is not functional 2023-09-12 13:10:39 +02:00
Andras Bacsai
6266a5e500 internal: trial emails 2023-09-12 12:03:17 +02:00
Andras Bacsai
5d27e89bfa feat: dynamic trial period 2023-09-12 11:23:31 +02:00
Andras Bacsai
6da4e78374 feat: trial 2023-09-12 11:19:21 +02:00
Andras Bacsai
be30651172 fix: remove nixpkgarchive from ui 2023-09-12 09:46:10 +02:00
Andras Bacsai
95764c2b76 fix: remove nixpkgarchive 2023-09-12 09:45:20 +02:00
Andras Bacsai
92a75685b5 version plus plus 2023-09-11 23:06:24 +02:00
Andras Bacsai
a1592373aa Merge pull request #1222 from coollabsio/next
v4.0.0-beta.34
2023-09-11 23:05:30 +02:00
Andras Bacsai
5747a87f66 ui: fix 2023-09-11 22:55:23 +02:00
Andras Bacsai
9cda671aef Add email to user 2023-09-11 22:45:07 +02:00
Andras Bacsai
2c9983046c fix 2023-09-11 22:43:07 +02:00
Andras Bacsai
11d33f328e fix: queue after commit 2023-09-11 22:39:18 +02:00
Andras Bacsai
f79c741d95 prepare: global env variables 2023-09-11 22:29:47 +02:00
Andras Bacsai
8a39a4469a fix: proxy check, reduce jobs, etc 2023-09-11 22:29:34 +02:00
Andras Bacsai
42daae10c6 feat: able to invite more people at once 2023-09-11 20:51:31 +02:00
Andras Bacsai
64a65e2018 fix: errors 2023-09-11 17:36:30 +02:00
Andras Bacsai
16c71f3647 fix: old docker version error 2023-09-11 17:19:30 +02:00
Andras Bacsai
363f525ad1 fix: sentry 4469575117 2023-09-11 16:51:38 +02:00
Andras Bacsai
f4fb519d55 internal: add Plausible analytics 2023-09-11 16:41:43 +02:00
Andras Bacsai
b7786504b8 wip: nixpacksarchive 2023-09-11 15:53:05 +02:00
Andras Bacsai
f0adf10e6a version++ 2023-09-11 14:11:22 +02:00
Andras Bacsai
cc8c6c5d16 Merge pull request #1221 from coollabsio/next
UI/UX fixes
2023-09-11 13:59:19 +02:00
Andras Bacsai
4782446f42 ui: show registered users on waitlist page 2023-09-11 13:53:08 +02:00
Andras Bacsai
230155312f ui: services are not availble yet 2023-09-11 13:52:54 +02:00
Andras Bacsai
3ab4365fca ui: user should know that the public key 2023-09-11 13:42:32 +02:00
Andras Bacsai
7349068b95 Merge pull request #1220 from coollabsio/next
Email issues fix
2023-09-11 12:55:52 +02:00
Andras Bacsai
da6cc151d1 fix: email sending error 2023-09-11 12:31:31 +02:00
Andras Bacsai
50527cf0a3 fix: testing email
fix: expired invitiation link
2023-09-11 12:16:26 +02:00
Andras Bacsai
26f490bb00 Merge pull request #1219 from coollabsio/next
Fixes
2023-09-11 10:50:18 +02:00
Andras Bacsai
dc4f412227 fix: recovery code 2023-09-11 10:41:39 +02:00
Andras Bacsai
ec4234e243 fix: only send internal notifcations to enabled channels 2023-09-11 10:19:46 +02:00
Andras Bacsai
f47fcb01ce Merge pull request #1218 from coollabsio/next
feat: generate public key from private keys
2023-09-11 10:16:48 +02:00
Andras Bacsai
02f6673345 feat: generate public key from private keys 2023-09-11 10:15:45 +02:00
Andras Bacsai
e6cd8702b5 Merge pull request #1216 from coollabsio/next
v4.0.0-beta.32
2023-09-10 15:34:08 +02:00
Andras Bacsai
fda4ea8cca fix: errors in views 2023-09-10 15:33:00 +02:00
Andras Bacsai
655d004ce7 Merge pull request #1214 from coollabsio/next
v4.0.0-beta.31
2023-09-09 15:33:16 +02:00
Andras Bacsai
56981d134c fix: improve startProxy action
fix: improve installDocker action and process
feat: add noSubmit prop to custom modal
2023-09-09 15:30:46 +02:00
Andras Bacsai
b9d49d2951 fix: remove -q from docker compose 2023-09-09 13:52:40 +02:00
Andras Bacsai
0c1e7c499e Merge pull request #1213 from coollabsio/next
v4.0.0-beta.30
2023-09-09 13:23:59 +02:00
Andras Bacsai
32fead5753 version++ 2023-09-09 13:23:03 +02:00
Andras Bacsai
e5e9faba35 fix: delete database related things when delete database 2023-09-09 13:18:49 +02:00
Andras Bacsai
2852630d6c Update DatabaseBackupJob.php 2023-09-09 10:34:10 +02:00
Andras Bacsai
a4cc406114 Merge pull request #1212 from coollabsio/next
v4.0.0-beta.29
2023-09-08 18:52:41 +02:00
Andras Bacsai
53b15a5762 update sentry dsn 2023-09-08 18:40:32 +02:00
Andras Bacsai
929a4e6474 fix: coolify already exists should not throw error 2023-09-08 18:40:25 +02:00
Andras Bacsai
45b597bbab feat: cache team settings 2023-09-08 18:33:26 +02:00
Andras Bacsai
0d1a2aa5d1 update testemail command 2023-09-08 17:51:19 +02:00
Andras Bacsai
b82353d5e2 update testemail command 2023-09-08 17:42:08 +02:00
Andras Bacsai
b17c09f7a7 update testemail command 2023-09-08 17:31:02 +02:00
Andras Bacsai
f6c3fe7888 fix: test email on for admins or custom smtp 2023-09-08 17:26:59 +02:00
Andras Bacsai
2e855e030f fix: ui 2023-09-08 16:59:49 +02:00
Andras Bacsai
49f86621f4 fix: instance email settings 2023-09-08 16:56:14 +02:00
Andras Bacsai
03d9f93397 fix: retry notifications 2023-09-08 16:53:19 +02:00
Andras Bacsai
c472042a94 fix: ui 2023-09-08 16:46:53 +02:00
Andras Bacsai
9f4356f67d version++ 2023-09-08 16:28:56 +02:00
Andras Bacsai
a50317cc76 Merge pull request #1209 from coollabsio/next
v4.0.0-beta.28
2023-09-08 16:28:04 +02:00
Andras Bacsai
8afa98a1ca fix: sentry 4451028626 2023-09-08 16:19:45 +02:00
Andras Bacsai
f6737f21dd feat: developer view for env variables 2023-09-08 16:16:59 +02:00
Andras Bacsai
e4a51cc116 fix: sentry 4459819517 2023-09-08 16:16:28 +02:00
Andras Bacsai
acd78ae196 feat: Telegram topics separation 2023-09-08 14:15:28 +02:00
Andras Bacsai
953bcfb5bb fix: db backup job 2023-09-08 12:23:46 +02:00
Andras Bacsai
dacfab8b29 Update BUG_REPORT.yml 2023-09-08 12:08:16 +02:00
Andras Bacsai
48b3e99939 Merge pull request #1203 from coollabsio/next
v4.0.0-beta.27
2023-09-08 12:06:34 +02:00
Andras Bacsai
41ad67c7c9 updates 2023-09-08 12:05:15 +02:00
Andras Bacsai
b49725cb1c fix: bug 2023-09-08 09:37:58 +02:00
Andras Bacsai
75e674a966 version++ 2023-09-08 09:27:44 +02:00
Andras Bacsai
9d826d9fb4 Merge pull request #1202 from coollabsio/next
v4.0.0-beta.26
2023-09-08 09:08:19 +02:00
Andras Bacsai
0af221f9fc udpate 2023-09-08 09:06:00 +02:00
Andras Bacsai
9066c9bf90 update readme 2023-09-08 09:00:44 +02:00
Andras Bacsai
77e3208f00 feat: public database 2023-09-07 13:23:34 +02:00
Andras Bacsai
db5ecf07bd version++ 2023-09-07 09:14:27 +02:00
Andras Bacsai
7f28aa6985 Merge pull request #1201 from coollabsio/next
v4.0.0-beta.25
2023-09-07 09:04:33 +02:00
Andras Bacsai
9d53e04ce9 fix: saveModel email settings 2023-09-07 08:48:16 +02:00
Andras Bacsai
3db9a1dd6e version++ 2023-09-06 15:26:17 +02:00
Andras Bacsai
38f9761b67 Merge pull request #1200 from coollabsio/next
v4.0.0-beta.24
2023-09-06 15:16:44 +02:00
Andras Bacsai
7117ecf634 version++ 2023-09-06 15:16:33 +02:00
Andras Bacsai
522e20f10a proxy updates 2023-09-06 15:00:56 +02:00
Andras Bacsai
ebbce2396c fix: typo 2023-09-06 14:34:35 +02:00
Andras Bacsai
f9a2ff6d90 feat: add discord notifications 2023-09-06 14:31:38 +02:00
Andras Bacsai
df1b9e7319 oops 2023-09-06 12:08:52 +02:00
Andras Bacsai
e7c0c26b32 fix: stripe
add: custom error pages
fix: invititation
feat: new quick login for first users (UX++)
feat: more internal notifications
2023-09-06 12:07:34 +02:00
Andras Bacsai
0dbb8b4420 update name 2023-09-05 16:03:24 +02:00
Andras Bacsai
a4b44bacc1 updates 2023-09-05 15:43:56 +02:00
Andras Bacsai
1338e68b8c wip: backup existing database 2023-09-05 12:14:31 +02:00
Andras Bacsai
f6f4cdde24 new links 2023-09-05 11:53:34 +02:00
Andras Bacsai
3daadf13c6 fix: lowercase image names 2023-09-05 11:05:54 +02:00
Andras Bacsai
cbd5fab2e7 fix 2023-09-05 10:57:49 +02:00
Andras Bacsai
48ccb508f9 update tax collection 2023-09-05 10:49:17 +02:00
Andras Bacsai
67edce0612 update payment webhook 2023-09-05 10:46:36 +02:00
Andras Bacsai
e8a41d7e6e rename command 2023-09-05 10:22:24 +02:00
Andras Bacsai
be1dad03bd fix: do not show system wide git on cloud 2023-09-05 10:03:28 +02:00
Andras Bacsai
31db1db636 next helper image 2023-09-05 08:49:33 +02:00
Andras Bacsai
dca332a688 fix: overlapping apps 2023-09-04 16:59:02 +02:00
Andras Bacsai
35f19ed53f update helper 2023-09-04 16:51:36 +02:00
Andras Bacsai
a5c45ffe90 update pack + nixpacks 2023-09-04 16:50:39 +02:00
Andras Bacsai
b6c3a65d2d update dev build action 2023-09-04 16:48:36 +02:00
Andras Bacsai
220c8211fd update 2023-09-04 16:46:53 +02:00
Andras Bacsai
ffbd04df29 update helper 2023-09-04 16:40:16 +02:00
Andras Bacsai
73c59be865 fix helper 2023-09-04 16:25:18 +02:00
Andras Bacsai
3966abaf80 improve coolify-helper + add test new functionalities 2023-09-04 16:03:11 +02:00
Andras Bacsai
759517316a fix: add docker network to build process 2023-09-04 10:18:30 +02:00
Andras Bacsai
3e3024d47e fix: add navbar for source + keys 2023-09-04 09:44:44 +02:00
Andras Bacsai
517cb77637 update 2023-09-03 11:54:05 +02:00
Andras Bacsai
14bd89a991 update 2023-09-03 11:51:00 +02:00
Andras Bacsai
304de29924 update waitlsit 2023-09-03 11:46:00 +02:00
Andras Bacsai
ab3055150f test distributed application check 2023-09-02 15:53:12 +02:00
Andras Bacsai
6b9c7aa9c5 feat: send request in cloud 2023-09-02 15:37:25 +02:00
Andras Bacsai
040f47b59c fix: show hosted email service, just disable for non pro subs 2023-09-02 13:41:42 +02:00
Andras Bacsai
eac7834083 fix: form address 2023-09-02 13:39:44 +02:00
Andras Bacsai
135a298080 remove line 2023-09-02 13:27:03 +02:00
Andras Bacsai
0065a86371 update seeder 2023-09-01 16:36:41 +02:00
Andras Bacsai
2a842a2f50 Do not schedule autoupdate 2023-09-01 16:30:50 +02:00
Andras Bacsai
231c02e00e version++ 2023-09-01 16:16:35 +02:00
214 changed files with 2942 additions and 1735 deletions

View File

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

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

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

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

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

View File

@@ -0,0 +1,84 @@
name: Coolify Helper Image Development (v4)
on:
push:
branches: [ "next" ]
paths:
- .github/workflows/coolify-helper.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
@@ -10,7 +13,7 @@ env:
jobs:
amd64:
runs-on: ubuntu-latest
runs-on: [self-hosted, x64]
steps:
- uses: actions/checkout@v3
- name: Login to ghcr.io
@@ -49,7 +52,7 @@ jobs:
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64
merge-manifest:
runs-on: ubuntu-latest
runs-on: [self-hosted, x64]
permissions:
contents: read
packages: write
@@ -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 }}

1
.gitignore vendored
View File

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

View File

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

View File

@@ -97,7 +97,6 @@ class RunRemoteProcess
'status' => $status->value,
]);
$this->activity->save();
if ($processResult->exitCode() != 0 && !$this->ignore_errors) {
throw new \RuntimeException($processResult->errorOutput());
}

View File

@@ -65,7 +65,7 @@ class StartPostgresql
],
'networks' => [
$this->database->destination->network => [
'external' => false,
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]

View File

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

View File

@@ -20,8 +20,8 @@ class SaveConfigurationSync
"mkdir -p $proxy_path",
"echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml",
], $server);
} catch (\Throwable $th) {
ray($th);
} catch (\Throwable $e) {
ray($e);
}
}

View File

@@ -2,8 +2,6 @@
namespace App\Actions\Proxy;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Support\Str;
use Spatie\Activitylog\Models\Activity;
@@ -30,19 +28,22 @@ class StartProxy
$server->save();
$activity = remote_process([
"echo 'Creating required Docker networks...'",
"echo '####### Creating required Docker networks...'",
...$create_networks_command,
"cd $proxy_path",
"echo 'Creating Docker Compose file...'",
"echo 'Pulling docker image...'",
'docker compose pull -q',
"echo 'Stopping existing proxy...'",
"echo '####### Creating Docker Compose file...'",
"echo '####### Pulling docker image...'",
'docker compose pull',
"echo '####### Stopping existing coolify-proxy...'",
'docker compose down -v --remove-orphans',
"lsof -nt -i:80 | xargs -r kill -9",
"lsof -nt -i:443 | xargs -r kill -9",
"echo 'Starting proxy...'",
"systemctl disable nginx > /dev/null 2>&1 || true",
"systemctl disable apache2 > /dev/null 2>&1 || true",
"systemctl disable apache > /dev/null 2>&1 || true",
"echo '####### Starting coolify-proxy...'",
'docker compose up -d --remove-orphans',
"echo 'Proxy installed successfully...'"
"echo '####### Proxy installed successfully...'"
], $server);
return $activity;

View File

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

@@ -43,11 +43,11 @@ class UpdateCoolify
$this->update();
}
send_internal_notification('InstanceAutoUpdateJob done to version: ' . $this->latestVersion . ' from version: ' . $this->currentVersion);
} catch (\Exception $th) {
} catch (\Throwable $e) {
ray('InstanceAutoUpdateJob failed');
ray($th->getMessage());
send_internal_notification('InstanceAutoUpdateJob failed: ' . $th->getMessage());
throw $th;
ray($e->getMessage());
send_internal_notification('InstanceAutoUpdateJob failed: ' . $e->getMessage());
throw $e;
}
}

View File

@@ -2,12 +2,16 @@
namespace App\Console\Commands;
use App\Jobs\SendConfirmationForWaitlistJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use App\Models\Team;
use App\Models\TeamInvitation;
use App\Models\User;
use App\Models\Waitlist;
use App\Notifications\Application\DeploymentFailed;
use App\Notifications\Application\DeploymentSuccess;
use App\Notifications\Application\StatusChanged;
@@ -22,31 +26,34 @@ use Illuminate\Notifications\Messages\MailMessage;
use Mail;
use Str;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\select;
use function Laravel\Prompts\text;
class TestEmail extends Command
class Emails extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'email:test';
protected $signature = 'emails';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send a test email to the admin';
protected $description = 'Send out test / prod emails';
/**
* Execute the console command.
*/
private ?MailMessage $mail = null;
private ?string $email = null;
public function handle()
{
$email = select(
$type = select(
'Which Email should be sent?',
options: [
'emails-test' => 'Test',
@@ -58,17 +65,22 @@ class TestEmail extends Command
'invitation-link' => 'Invitation Link',
'waitlist-invitation-link' => 'Waitlist Invitation Link',
'waitlist-confirmation' => 'Waitlist Confirmation',
'realusers-before-trial' => 'REAL - Registered Users Before Trial without Subscription',
'realusers-server-lost-connection' => 'REAL - Server Lost Connection',
],
);
$type = set_transanctional_email_settings();
if (!$type) {
throw new Exception('No email settings found.');
$emailsGathered = ['realusers-before-trial','realusers-server-lost-connection'];
if (!in_array($type, $emailsGathered)) {
$this->email = text('Email Address to send to');
}
set_transanctional_email_settings();
$this->mail = new MailMessage();
$this->mail->subject("Test Email");
switch ($email) {
switch ($type) {
case 'emails-test':
$this->mail = (new Test())->toMail();
$this->sendEmail();
break;
case 'application-deployment-success':
$application = Application::all()->first();
@@ -146,39 +158,90 @@ class TestEmail extends Command
case 'waitlist-invitation-link':
$this->mail = new MailMessage();
$this->mail->view('emails.waitlist-invitation', [
'email' => 'test2@example.com',
'password' => "supersecretpassword",
'loginLink' => 'https://coolify.io',
]);
$this->mail->subject('Congratulations! You are invited to join Coolify Cloud.');
$this->sendEmail();
break;
case 'waitlist-confirmation':
$found = Waitlist::where('email', $this->email)->first();
if ($found) {
SendConfirmationForWaitlistJob::dispatch($this->email, $found->uuid);
} else {
throw new Exception('Waitlist not found');
}
break;
case 'realusers-before-trial':
$this->mail = new MailMessage();
$this->mail->view(
'emails.waitlist-confirmation',
[
'confirmation_url' => 'http://example.com',
'cancel_url' => 'http://example.com',
]
);
$this->mail->subject('You are on the waitlist!');
$this->sendEmail();
$this->mail->view('emails.before-trial-conversion');
$this->mail->subject('Trial period has been added for all subscription plans.');
$teams = Team::doesntHave('subscription')->where('id', '!=', 0)->get();
if (!$teams || $teams->isEmpty()) {
echo 'No teams found.' . PHP_EOL;
return;
}
$emails = [];
foreach ($teams as $team) {
foreach ($team->members as $member) {
if ($member->email) {
$emails[] = $member->email;
}
}
}
$emails = array_unique($emails);
$this->info("Sending to " . count($emails) . " emails.");
foreach ($emails as $email) {
$this->info($email);
}
$confirmed = confirm('Are you sure?');
if ($confirmed) {
foreach ($emails as $email) {
$this->sendEmail($email);
}
}
break;
case 'realusers-server-lost-connection':
$serverId = text('Server Id');
$server = Server::find($serverId);
if (!$server) {
throw new Exception('Server not found');
}
$admins = [];
$members = $server->team->members;
foreach ($members as $member) {
if ($member->isAdmin()) {
$admins[] = $member->email;
}
}
$this->info('Sending to ' . count($admins) . ' admins.');
foreach ($admins as $admin) {
$this->info($admin);
}
$this->mail = new MailMessage();
$this->mail->view('emails.server-lost-connection', [
'name' => $server->name,
]);
$this->mail->subject('Action required: Server ' . $server->name . ' lost connection.');
foreach ($admins as $email) {
$this->sendEmail($email);
}
break;
}
}
private function sendEmail()
private function sendEmail(string $email = null)
{
if ($email) {
$this->email = $email;
}
Mail::send(
[],
[],
fn (Message $message) => $message
->from(
'internal@example.com',
'Test Email',
)
->to('test@example.com')
->to($this->email)
->subject($this->mail->subject)
->html((string)$this->mail->render())
);
$this->info("Email sent to $this->email successfully. 📧");
}
}

View File

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

View File

@@ -82,7 +82,7 @@ class SyncBunny extends Command
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
]);
echo "All files uploaded & purged...\n";
} catch (\Exception $e) {
} catch (\Throwable $e) {
echo $e->getMessage();
}
}

View File

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

View File

@@ -2,14 +2,21 @@
namespace App\Console;
use App\Enums\ProxyTypes;
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\Jobs\ProxyContainerStatusJob;
use App\Jobs\ServerDetailsCheckJob;
use App\Models\Application;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -17,28 +24,56 @@ class Kernel extends ConsoleKernel
{
protected function schedule(Schedule $schedule): void
{
// $schedule->call(fn() => $this->check_scheduled_backups($schedule))->everyTenSeconds();
if (isDev()) {
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new ResourceStatusJob)->everyMinute();
$schedule->job(new ProxyCheckJob)->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute();
$schedule->job(new ServerDetailsCheckJob(Server::find(0)))->everyTenMinutes()->onOneServer();
// $schedule->command('horizon:snapshot')->everyMinute();
// $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();
// $this->instance_auto_update($schedule);
// $this->check_scheduled_backups($schedule);
// $this->check_resources($schedule);
// $this->check_proxies($schedule);
} else {
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
$schedule->job(new ResourceStatusJob)->everyMinute()->onOneServer();
$schedule->job(new CleanupInstanceStuffsJob)->everyTenMinutes()->onOneServer();
$schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
$schedule->job(new ProxyCheckJob)->everyFiveMinutes()->onOneServer();
$schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer();
$schedule->job(new InstanceAutoUpdateJob)->everyTenMinutes();
$this->instance_auto_update($schedule);
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
$this->check_proxies($schedule);
}
$this->check_scheduled_backups($schedule);
}
private function check_proxies($schedule)
{
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true)->whereNotNull('proxy.type')->where('proxy.type', '!=', ProxyTypes::NONE->value);
foreach ($servers as $server) {
$schedule->job(new ProxyContainerStatusJob($server))->everyMinute()->onOneServer();
}
}
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');
@@ -51,13 +86,18 @@ class Kernel extends ConsoleKernel
if (!$scheduled_backup->enabled) {
continue;
}
if (is_null(data_get($scheduled_backup, 'database'))) {
ray('database not found');
$scheduled_backup->delete();
continue;
}
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
}
$schedule->job(new DatabaseBackupJob(
backup: $scheduled_backup
))->cron($scheduled_backup->frequency);
))->cron($scheduled_backup->frequency)->onOneServer();
}
}

View File

@@ -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

@@ -25,7 +25,7 @@ class Handler extends ExceptionHandler
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [
//
ProcessException::class
];
/**
* A list of the inputs that are never flashed to the session on validation exceptions.
@@ -50,8 +50,13 @@ class Handler extends ExceptionHandler
return;
}
app('sentry')->configureScope(
function (Scope $scope){
$scope->setUser(['id'=> config('sentry.server_name')]);
function (Scope $scope) {
$scope->setUser(
[
'id' => config('sentry.server_name'),
'email' => auth()->user()->email
]
);
}
);
Integration::captureUnhandledException($e);

View File

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

View File

@@ -8,15 +8,41 @@ use App\Models\S3Storage;
use App\Models\StandalonePostgresql;
use App\Models\TeamInvitation;
use App\Models\User;
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 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 (!isCloud()) {
@@ -37,10 +63,12 @@ class Controller extends BaseController
]);
}
public function force_passoword_reset() {
public function force_passoword_reset()
{
return view('auth.force-password-reset');
}
public function boarding() {
public function boarding()
{
if (currentTeam()->boarding || isDev()) {
return view('boarding');
} else {
@@ -126,8 +154,8 @@ class Controller extends BaseController
$invitation->delete();
abort(401);
}
} catch (Throwable $th) {
throw $th;
} catch (Throwable $e) {
throw $e;
}
}
@@ -144,8 +172,8 @@ class Controller extends BaseController
}
$invitation->delete();
return redirect()->route('team.index');
} catch (Throwable $th) {
throw $th;
} catch (Throwable $e) {
throw $e;
}
}
}

View File

@@ -60,9 +60,6 @@ class ProjectController extends Controller
'environment_name' => $environment->name,
'database_uuid' => $standalone_postgresql->uuid,
]);
}
if ($server) {
}
return view('project.new', [
'type' => $type

View File

@@ -6,6 +6,7 @@ use App\Actions\Server\InstallDocker;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Support\Collection;
use Livewire\Component;
@@ -70,9 +71,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
}
public function skipBoarding()
{
currentTeam()->update([
Team::find(currentTeam()->id)->update([
'show_boarding' => false
]);
ray(currentTeam());
refreshSession();
return redirect()->route('dashboard');
}
@@ -133,7 +135,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
{
$this->selectedExistingPrivateKey = null;
$this->privateKeyType = $type;
if ($type === 'create' && !isDev()) {
if ($type === 'create') {
$this->createNewPrivateKey();
}
$this->currentState = 'create-private-key';
@@ -191,7 +193,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
}
$this->getProxyType();
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this);
}
}

View File

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

View File

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

View File

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

View File

@@ -71,7 +71,7 @@ class StandaloneDocker extends Component
}
$this->createNetworkAndAttachToProxy();
return redirect()->route('destination.show', $docker->uuid);
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
}
}

View File

@@ -18,22 +18,26 @@ class ForcePasswordReset extends Component
'password' => 'required|min:8',
'password_confirmation' => 'required|same:password',
];
public function mount() {
public function mount()
{
$this->email = auth()->user()->email;
}
public function submit() {
public function submit()
{
try {
$this->rateLimit(10);
$this->validate();
$firstLogin = auth()->user()->created_at == auth()->user()->updated_at;
auth()->user()->forceFill([
'password' => Hash::make($this->password),
'force_password_reset' => false,
])->save();
auth()->logout();
return redirect()->route('login')->with('status', 'Your initial password has been set.');
} catch(\Exception $e) {
return general_error_handler(err:$e, that:$this);
if ($firstLogin) {
send_internal_notification('First login for ' . auth()->user()->email);
}
return redirect()->route('dashboard');
} catch (\Throwable $e) {
return 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() ?? 'Free';
$debug = "Route: {$this->path}";
$mail = new MailMessage();
$mail->view(
'emails.help',
[
'description' => $this->description,
'debug' => $debug
]
);
$mail->subject("[HELP - {$subscriptionType}]: {$this->subject}");
send_user_an_email($mail, 'hi@coollabs.io', auth()->user()?->email);
$this->emit('success', 'Your message has been sent successfully. We will get in touch with you as soon as possible.');
} catch (\Throwable $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) {
} catch (\Throwable $e) {
ray($e->getMessage());
$this->model->discord_enabled = false;
$this->team->discord_enabled = false;
$this->validate();
}
}
@@ -41,8 +45,8 @@ class DiscordSettings extends Component
public function saveModel()
{
$this->model->save();
if (is_a($this->model, Team::class)) {
$this->team->save();
if (is_a($this->team, Team::class)) {
refreshSession();
}
$this->emit('success', 'Settings saved.');
@@ -50,7 +54,7 @@ class DiscordSettings extends Component
public function sendTestNotification()
{
$this->model->notify(new Test);
$this->team->notify(new Test());
$this->emit('success', 'Test notification sent.');
}
}

View File

@@ -49,6 +49,7 @@ class EmailSettings extends Component
public function mount()
{
$this->team = auth()->user()->currentTeam();
['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits;
$this->emails = auth()->user()->email;
}
@@ -62,7 +63,7 @@ class EmailSettings extends Component
]);
$this->team->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler($e, $this);
}
}
@@ -81,7 +82,7 @@ class EmailSettings extends Component
$this->team->resend_enabled = false;
$this->team->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler($e, $this);
}
}
@@ -91,7 +92,7 @@ class EmailSettings extends Component
try {
$this->team->smtp_enabled = false;
$this->submitResend();
} catch (\Exception $e) {
} catch (\Throwable $e) {
$this->team->smtp_enabled = false;
return general_error_handler($e, $this);
}
@@ -101,12 +102,19 @@ class EmailSettings extends Component
try {
$this->team->resend_enabled = false;
$this->submit();
} catch (\Exception $e) {
} catch (\Throwable $e) {
$this->team->smtp_enabled = false;
return general_error_handler($e, $this);
}
}
public function saveModel()
{
$this->team->save();
if (is_a($this->team, Team::class)) {
refreshSession();
}
$this->emit('success', 'Settings saved.');
}
public function submit()
{
try {
@@ -123,7 +131,7 @@ class EmailSettings extends Component
]);
$this->team->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
} catch (\Throwable $e) {
$this->team->smtp_enabled = false;
return general_error_handler($e, $this);
}
@@ -138,7 +146,7 @@ class EmailSettings extends Component
$this->team->save();
refreshSession();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
} catch (\Throwable $e) {
$this->team->resend_enabled = false;
return general_error_handler($e, $this);
}

View File

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

@@ -8,7 +8,7 @@ use Livewire\Component;
class Change extends Component
{
public PrivateKey $private_key;
public $public_key;
protected $rules = [
'private_key.name' => 'required|string',
'private_key.description' => 'nullable|string',
@@ -21,16 +21,24 @@ class Change extends Component
'private_key.private_key' => 'private key'
];
public function mount()
{
try {
$this->public_key = $this->private_key->publicKey();
}catch(\Exception $e) {
return general_error_handler(err: $e, that: $this);
}
}
public function delete()
{
try {
if ($this->private_key->isEmpty()) {
$this->private_key->delete();
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
return redirect()->route('private-key.all');
return redirect()->route('security.private-key.index');
}
$this->emit('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.');
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
}
}
@@ -41,7 +49,7 @@ class Change extends Component
$this->private_key->private_key = formatPrivateKey($this->private_key->private_key);
$this->private_key->save();
refresh_server_connection($this->private_key);
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
}
}

View File

@@ -37,8 +37,8 @@ class Create extends Component
if ($this->from === 'server') {
return redirect()->route('server.create');
}
return redirect()->route('private-key.show', ['private_key_uuid' => $private_key->uuid]);
} catch (\Exception $e) {
return redirect()->route('security.private-key.show', ['private_key_uuid' => $private_key->uuid]);
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
}
}

View File

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

View File

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

View File

@@ -159,7 +159,7 @@ class General extends Component
}
$this->application->save();
$this->emit('success', 'Application settings updated!');
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
}
}

View File

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

View File

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

View File

@@ -42,8 +42,13 @@ 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->emit('refresh');
// $this->database->environment->project->team->notify(new StatusChanged($this->database));
}

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,44 @@ class General extends Component
'database.init_scripts' => 'Init Scripts',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function mount()
{
$this->getDbUrl();
}
public function getDbUrl() {
if ($this->database->is_public) {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->destination->server->ip}:{$this->database->public_port}/{$this->database->postgres_db}";
} else {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->uuid}:5432/{$this->database->postgres_db}";
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...');
startPostgresProxy($this->database);
$this->emit('success', 'Database is now publicly accessible.');
} else {
stopPostgresProxy($this->database);
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->getDbUrl();
$this->database->save();
} catch(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

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

View File

@@ -164,7 +164,7 @@ class GithubPrivateRepository extends Component
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
]);
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
}
}

View File

@@ -117,7 +117,7 @@ class GithubPrivateRepositoryDeployKey extends Component
'environment_name' => $environment->name,
'application_uuid' => $application->uuid,
]);
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
}
}

View File

@@ -75,14 +75,14 @@ class PublicGitRepository extends Component
$this->get_git_source();
$this->get_branch();
$this->selected_branch = $this->git_branch;
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
}
if (!$this->branch_found && $this->git_branch == 'main') {
try {
$this->git_branch = 'master';
$this->get_branch();
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
}
}
@@ -161,7 +161,7 @@ class PublicGitRepository extends Component
'environment_name' => $environment->name,
'application_uuid' => $application->uuid,
]);
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
}
}

View File

@@ -22,34 +22,52 @@ class Select extends Component
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 (\Throwable $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->set_server($server);
$this->setServer($server);
if (count($server->destinations()) === 1) {
$this->set_destination($server->destinations()->first()->uuid);
$this->setDestination($server->destinations()->first()->uuid);
}
}
if (!is_null($this->server)) {
$foundServer = $this->servers->where('id', $this->server)->first();
if ($foundServer) {
return $this->set_server($foundServer);
return $this->setServer($foundServer);
}
}
$this->current_step = 'servers';
}
public function set_server(Server $server)
public function setServer(Server $server)
{
$this->server_id = $server->id;
$this->standaloneDockers = $server->standaloneDockers;
@@ -57,7 +75,7 @@ class Select extends Component
$this->current_step = 'destinations';
}
public function set_destination(string $destination_uuid)
public function setDestination(string $destination_uuid)
{
$this->destination_uuid = $destination_uuid;
redirect()->route('project.resources.new', [

View File

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

View File

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

View File

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

View File

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

View File

@@ -54,13 +54,20 @@ class Form extends Component
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->server);
if ($uptime) {
$this->uptime = $uptime;
$this->emit('success', 'Server is reachable!');
} else {
$this->emit('error', 'Server is not rachable');
return;
}
if ($dockerVersion) {
$this->dockerVersion = $dockerVersion;
$this->emit('proxyStatusUpdated');
$this->emit('success', 'Docker Engine 23+ is installed!');
} else {
$this->emit('error', 'Old (lower than 23) or no Docker version detected. Install Docker Engine on the General tab.');
}
} catch (\Exception $e) {
return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this);
} catch (\Throwable $e) {
return general_error_handler($e, that: $this);
}
}
@@ -74,11 +81,9 @@ class Form extends Component
}
$this->server->delete();
return redirect()->route('server.all');
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
}
}
public function submit()
{

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;
@@ -56,7 +58,7 @@ class ByIp extends Component
{
$this->validate();
try {
if (!$this->private_key_id) {
if (is_null($this->private_key_id)) {
return $this->emit('error', 'You must select a private key');
}
$server = Server::create([
@@ -67,11 +69,16 @@ class ByIp extends Component
'port' => $this->port,
'team_id' => currentTeam()->id,
'private_key_id' => $this->private_key_id,
'proxy' => [
"type" => ProxyTypes::TRAEFIK_V2->value,
"status" => ProxyStatus::EXITED->value,
]
]);
$server->settings->is_part_of_swarm = $this->is_part_of_swarm;
$server->settings->save();
return redirect()->route('server.show', $server->uuid);
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler(err: $e);
}
}

View File

@@ -12,15 +12,16 @@ class Proxy extends Component
{
public Server $server;
public ProxyTypes $selectedProxy = ProxyTypes::TRAEFIK_V2;
public ?string $selectedProxy = null;
public $proxy_settings = null;
public string|null $redirect_url = null;
public ?string $redirect_url = null;
protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit'];
public function mount()
{
$this->redirect_url = $this->server->proxy->redirect_url;
$this->selectedProxy = data_get($this->server, 'proxy.type');
$this->redirect_url = data_get($this->server, 'proxy.redirect_url');
}
public function proxyStatusUpdated()
@@ -35,11 +36,12 @@ class Proxy extends Component
$this->emit('proxyStatusUpdated');
}
public function select_proxy(ProxyTypes $proxy_type)
public function select_proxy($proxy_type)
{
$this->server->proxy->type = $proxy_type;
$this->server->proxy->status = 'exited';
$this->server->save();
$this->selectedProxy = $this->server->proxy->type;
$this->emit('proxyStatusUpdated');
}
@@ -53,7 +55,7 @@ class Proxy extends Component
setup_default_redirect_404(redirect_url: $this->server->proxy->redirect_url, server: $this->server);
$this->emit('success', 'Proxy configuration saved.');
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler(err: $e);
}
}
@@ -62,16 +64,17 @@ class Proxy extends Component
{
try {
$this->proxy_settings = resolve(CheckConfigurationSync::class)($this->server, true);
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler(err: $e);
}
}
public function load_proxy_configuration()
public function loadProxyConfiguration()
{
try {
ray('loadProxyConfiguration');
$this->proxy_settings = resolve(CheckConfigurationSync::class)($this->server);
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler(err: $e);
}
}

View File

@@ -10,15 +10,20 @@ class Deploy extends Component
{
public Server $server;
public $proxy_settings = null;
protected $listeners = ['proxyStatusUpdated'];
public function start_proxy()
public function proxyStatusUpdated() {
$this->server->refresh();
}
public function startProxy()
{
if (
$this->server->proxy->last_applied_settings &&
$this->server->proxy->last_saved_settings !== $this->server->proxy->last_applied_settings
) {
$this->emit('saveConfiguration', $this->server);
resolve(SaveConfigurationSync::class)($this->server, $this->proxy_settings);
}
$activity = resolve(StartProxy::class)($this->server);
$this->emit('newMonitorActivity', $activity->id);
}

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
namespace App\Http\Livewire\Settings;
use App\Jobs\ProxyStartJob;
use App\Jobs\ProxyContainerStatusJob;
use App\Models\InstanceSettings as ModelsInstanceSettings;
use App\Models\Server;
use Livewire\Component;
@@ -124,7 +124,7 @@ class Configuration extends Component
];
}
$this->save_configuration_to_disk($traefik_dynamic_conf, $file);
dispatch(new ProxyStartJob($this->server));
dispatch(new ProxyContainerStatusJob($this->server));
}
}

View File

@@ -50,7 +50,7 @@ class Email extends Component
]);
$this->settings->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler($e, $this);
}
}
@@ -60,19 +60,27 @@ class Email extends Component
$this->validate([
'settings.resend_api_key' => 'required'
]);
$this->settings->smtp_enabled = false;
$this->settings->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
} catch (\Throwable $e) {
$this->settings->resend_enabled = false;
return general_error_handler($e, $this);
}
}
public function instantSaveResend() {
try {
$this->settings->smtp_enabled = false;
$this->submitResend();
} catch (\Throwable $e) {
return general_error_handler($e, $this);
}
}
public function instantSave()
{
try {
$this->settings->resend_enabled = false;
$this->submit();
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler($e, $this);
}
}
@@ -89,10 +97,9 @@ class Email extends Component
'settings.smtp_password' => 'nullable',
'settings.smtp_timeout' => 'nullable',
]);
$this->settings->resend_enabled = false;
$this->settings->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler($e, $this);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -67,8 +67,8 @@ class Create extends Component
$this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
$this->storage->save();
return redirect()->route('team.storages.show', $this->storage->uuid);
} catch (\Throwable $th) {
return general_error_handler($th, $this);
} catch (\Throwable $e) {
return general_error_handler($e, $this);
}
}
@@ -77,8 +77,8 @@ class Create extends Component
try {
$this->storage->testConnection();
return $this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
} catch (\Throwable $th) {
return general_error_handler($th, $this);
} catch (\Throwable $e) {
return general_error_handler($e, $this);
}
}
}

View File

@@ -32,8 +32,8 @@ class Form extends Component
try {
$this->storage->testConnection();
return $this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
} catch (\Throwable $th) {
return general_error_handler($th, $this);
} catch (\Throwable $e) {
return general_error_handler($e, $this);
}
}
@@ -42,8 +42,8 @@ class Form extends Component
try {
$this->storage->delete();
return redirect()->route('team.storages.all');
} catch (\Throwable $th) {
return general_error_handler($th, $this);
} catch (\Throwable $e) {
return general_error_handler($e, $this);
}
}
@@ -55,8 +55,8 @@ class Form extends Component
$this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
$this->storage->save();
$this->emit('success', 'Storage settings saved.');
} catch (\Throwable $th) {
return general_error_handler($th, $this);
} catch (\Throwable $e) {
return general_error_handler($e, $this);
}
}
}

View File

@@ -37,7 +37,7 @@ class Upgrade extends Component
$this->showProgress = true;
resolve(UpdateCoolify::class)(true);
Toaster::success("Upgrading to {$this->latestVersion} version...");
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
}
}

View File

@@ -10,6 +10,7 @@ use Livewire\Component;
class Index extends Component
{
public string $email;
public int $users = 0;
public int $waitingInLine = 0;
protected $rules = [
@@ -22,6 +23,7 @@ class Index extends Component
public function mount()
{
$this->waitingInLine = Waitlist::whereVerified(true)->count();
$this->users = User::count();
if (isDev()) {
$this->email = 'waitlist@example.com';
}
@@ -50,7 +52,7 @@ class Index extends Component
$this->emit('success', 'Check your email to verify your email address.');
dispatch(new SendConfirmationForWaitlistJob($this->email, $waitlist->uuid));
} catch (\Exception $e) {
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
}
}

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

@@ -15,7 +15,7 @@ class IsBoardingFlow
*/
public function handle(Request $request, Closure $next): Response
{
ray()->showQueries()->color('orange');
// ray()->showQueries()->color('orange');
if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
return redirect('boarding');
}

View File

@@ -46,9 +46,9 @@ class ApplicationContainerStatusJob implements ShouldQueue, ShouldBeUnique
$this->application->status = $status;
$this->application->save();
}
} catch (\Exception $th) {
ray($th->getMessage());
throw $th;
} catch (\Throwable $e) {
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -28,6 +28,8 @@ use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Throwable;
use Visus\Cuid2\Cuid2;
use Yosymfony\Toml\Toml;
use Yosymfony\Toml\TomlArray;
class ApplicationDeploymentJob implements ShouldQueue
{
@@ -66,12 +68,7 @@ class ApplicationDeploymentJob implements ShouldQueue
private $log_model;
private Collection $saved_outputs;
public function middleware(): array
{
return [
(new WithoutOverlapping("dockerimagejobs"))->shared(),
];
}
public $tries = 1;
public function __construct(int $application_deployment_queue_id)
{
ray()->clearScreen();
@@ -138,7 +135,7 @@ class ApplicationDeploymentJob implements ShouldQueue
$this->deploy();
}
}
if ($this->application->fqdn) dispatch(new ProxyStartJob($this->server));
if ($this->application->fqdn) dispatch(new ProxyContainerStatusJob($this->server));
$this->next(ApplicationDeploymentStatus::FINISHED->value);
} catch (Exception $e) {
ray($e);
@@ -181,8 +178,8 @@ 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();
@@ -206,8 +203,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) {
@@ -242,7 +239,7 @@ class ApplicationDeploymentJob implements ShouldQueue
}
private function health_check()
{
ray('New container name: ',$this->container_name);
ray('New container name: ', $this->container_name);
if ($this->container_name) {
$counter = 0;
$this->execute_remote_command(
@@ -264,7 +261,7 @@ class ApplicationDeploymentJob implements ShouldQueue
);
$this->execute_remote_command(
[
"echo 'New application version health check status: {$this->saved_outputs->get('health_check')}'"
"echo 'New version health check status: {$this->saved_outputs->get('health_check')}'"
],
);
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
@@ -282,8 +279,8 @@ class ApplicationDeploymentJob implements ShouldQueue
}
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}.'",
@@ -304,12 +301,19 @@ class ApplicationDeploymentJob implements ShouldQueue
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,
],
[
@@ -494,9 +498,9 @@ class ApplicationDeploymentJob implements ShouldQueue
],
'networks' => [
$this->destination->network => [
'external' => false,
'external' => true,
'name' => $this->destination->network,
'attachable' => true,
'attachable' => true
]
]
];
@@ -636,6 +640,10 @@ class ApplicationDeploymentJob implements ShouldQueue
private function generate_healthcheck_commands()
{
if ($this->application->dockerfile) {
// TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl.
return 'exit 0';
}
if (!$this->application->health_check_port) {
$this->application->health_check_port = $this->application->ports_exposes_array[0];
}
@@ -654,12 +662,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}
@@ -692,12 +700,12 @@ 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
]);
}
}
@@ -706,7 +714,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
{
if ($this->currently_running_container_name) {
$this->execute_remote_command(
["echo -n 'Removing old application version.'"],
["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],
);
}

View File

@@ -57,7 +57,7 @@ class ApplicationPullRequestUpdateJob implements ShouldQueue
} else {
$this->create_comment();
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
ray($e);
throw $e;
}

View File

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

View File

@@ -27,8 +27,8 @@ class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique
public function handle(): void
{
try {
$this->cleanup_waitlist();
} catch (\Exception $e) {
// $this->cleanup_waitlist();
} catch (\Throwable $e) {
send_internal_notification('CleanupInstanceStuffsJob failed with error: ' . $e->getMessage());
ray($e->getMessage());
throw $e;

View File

@@ -24,31 +24,27 @@ class DatabaseBackupJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public Team|null $team = null;
public ?Team $team = null;
public Server $server;
public ScheduledDatabaseBackup|null $backup;
public string $database_type;
public ScheduledDatabaseBackup $backup;
public StandalonePostgresql $database;
public string $database_status;
public string|null $container_name = null;
public ScheduledDatabaseBackupExecution|null $backup_log = null;
public ?string $container_name = null;
public ?ScheduledDatabaseBackupExecution $backup_log = null;
public string $backup_status;
public string|null $backup_location = null;
public ?string $backup_location = null;
public string $backup_dir;
public string $backup_file;
public int $size = 0;
public string|null $backup_output = null;
public S3Storage|null $s3 = null;
public ?string $backup_output = null;
public ?S3Storage $s3 = null;
public function __construct($backup)
{
$this->backup = $backup;
$this->team = Team::find($backup->team_id);
$this->database = $this->backup->database;
$this->database_type = $this->database->type();
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->destination->server;
$this->database_status = $this->database->status;
$this->s3 = $this->backup->s3;
}
@@ -65,7 +61,7 @@ class DatabaseBackupJob implements ShouldQueue
public function handle(): void
{
try {
if ($this->database_status !== 'running') {
if (data_get($this->database, 'status') !== 'running') {
ray('database not running');
return;
}
@@ -77,14 +73,14 @@ class DatabaseBackupJob implements ShouldQueue
$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_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') {
if ($this->database->type() === 'standalone-postgresql') {
$this->backup_standalone_postgresql();
}
$this->calculate_size();
@@ -94,12 +90,11 @@ class DatabaseBackupJob implements ShouldQueue
}
$this->save_backup_logs();
// TODO: Notify user
} catch (\Throwable $th) {
ray($th->getMessage());
send_internal_notification('DatabaseBackupJob failed with: ' . $th->getMessage());
throw $th;
} catch (\Throwable $e) {
ray($e->getMessage());
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
throw $e;
}
}
private function backup_standalone_postgresql(): void
@@ -107,7 +102,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);
@@ -121,10 +116,10 @@ class DatabaseBackupJob implements ShouldQueue
$this->backup_status = 'success';
$this->team->notify(new BackupSuccess($this->backup, $this->database));
} catch (Throwable $th) {
} catch (Throwable $e) {
$this->backup_status = 'failed';
$this->add_to_backup_output($th->getMessage());
ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $th->getMessage());
$this->add_to_backup_output($e->getMessage());
ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage());
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
} finally {
$this->backup_log->update([
@@ -178,9 +173,9 @@ class DatabaseBackupJob implements ShouldQueue
instant_remote_process($commands, $this->server);
$this->add_to_backup_output('Uploaded to S3.');
ray('Uploaded to S3. ' . $this->backup_location . ' to s3://' . $bucket . $this->backup_dir);
} catch (\Throwable $th) {
$this->add_to_backup_output($th->getMessage());
ray($th->getMessage());
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
ray($e->getMessage());
} finally {
$command = "docker rm -f backup-of-{$this->backup->uuid}";
instant_remote_process([$command], $this->server);

View File

@@ -47,7 +47,7 @@ class DatabaseContainerStatusJob implements ShouldQueue, ShouldBeUnique
$this->database->status = $status;
$this->database->save();
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
send_internal_notification('DatabaseContainerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;

View File

@@ -2,6 +2,7 @@
namespace App\Jobs;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -9,7 +10,6 @@ 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
{
@@ -30,12 +30,17 @@ class DockerCleanupJob implements ShouldQueue
}
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');
// ray()->showQueries()->color('orange');
$servers = Server::all();
foreach ($servers as $server) {
if (
!$server->settings->is_reachable && !$server->settings->is_usable
!$server->isFunctional()
) {
continue;
}
@@ -70,7 +75,7 @@ class DockerCleanupJob implements ShouldQueue
ray('No need to clean up ' . $server->name)->color('orange');
}
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage());
ray($e->getMessage())->color('orange');
throw $e;

View File

@@ -1,46 +0,0 @@
<?php
namespace App\Jobs;
use App\Actions\Proxy\StartProxy;
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\SerializesModels;
class ProxyCheckJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct()
{
}
public function handle()
{
try {
$container_name = 'coolify-proxy';
$servers = Server::all();
foreach ($servers as $server) {
if (
$server->settings->is_reachable === false || $server->settings->is_usable === false
) {
continue;
}
$status = getContainerStatus(server: $server, container_id: $container_name);
if ($status === 'running') {
continue;
}
if (data_get($server, 'proxy.type')) {
resolve(StartProxy::class)($server);
}
}
} catch (\Throwable $th) {
ray($th->getMessage());
send_internal_notification('ProxyCheckJob failed with: ' . $th->getMessage());
throw $th;
}
}
}

View File

@@ -2,6 +2,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;
@@ -11,7 +13,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
class ProxyContainerStatusJob implements ShouldQueue, ShouldBeUnique
{
@@ -28,31 +29,51 @@ class ProxyContainerStatusJob implements ShouldQueue, ShouldBeUnique
public function middleware(): array
{
return [new WithoutOverlapping($this->server->id)];
return [new WithoutOverlapping($this->server->uuid)];
}
public function uniqueId(): int
public function uniqueId(): string
{
return $this->server->id;
ray($this->server->uuid);
return $this->server->uuid;
}
public function handle(): void
{
try {
$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'];
$version = $container['Config']['Labels']['org.opencontainers.image.version'];
if (isset($version) && isset($traefik) && $traefik === 'Traefik' && Str::of($version)->startsWith('v2')) {
$this->server->proxy->type = ProxyTypes::TRAEFIK_V2->value;
}
}
$this->server->save();
$proxyType = data_get($this->server, 'proxy.type');
if ($proxyType === ProxyTypes::NONE->value) {
return;
}
} catch (\Exception $e) {
if (is_null($proxyType)) {
if ($this->server->isProxyShouldRun()) {
$this->server->proxy->type = ProxyTypes::TRAEFIK_V2->value;
$this->server->proxy->status = ProxyStatus::EXITED->value;
$this->server->save();
resolve(StartProxy::class)($this->server);
return;
}
}
$container = getContainerStatus(server: $this->server, all_data: true, container_id: 'coolify-proxy', throwError: false);
$containerStatus = data_get($container, 'State.Status');
$databaseContainerStatus = data_get($this->server, 'proxy.status', 'exited');
if ($proxyType !== ProxyTypes::NONE->value) {
if ($containerStatus === 'running') {
$this->server->proxy->status = $containerStatus;
$this->server->save();
return;
}
if ((is_null($containerStatus) ||$containerStatus !== 'running' || $databaseContainerStatus !== 'running' || ($containerStatus && $databaseContainerStatus !== $containerStatus)) && $this->server->isProxyShouldRun()) {
$this->server->proxy->status = $containerStatus;
$this->server->save();
resolve(StartProxy::class)($this->server);
return;
}
}
} catch (\Throwable $e) {
if ($e->getCode() === 1) {
$this->server->proxy->status = 'exited';
$this->server->save();

View File

@@ -1,44 +0,0 @@
<?php
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;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProxyStartJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(protected Server $server)
{
}
public function handle()
{
try {
$container_name = 'coolify-proxy';
ray('Starting proxy for server: ' . $this->server->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;
}
}
}

View File

@@ -1,46 +0,0 @@
<?php
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;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
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 ApplicationContainerStatusJob(
application: $application,
));
}
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

@@ -25,10 +25,8 @@ class SendConfirmationForWaitlistJob implements ShouldQueue
{
try {
$mail = new MailMessage();
$confirmation_url = base_url() . '/webhooks/waitlist/confirm?email=' . $this->email . '&confirmation_code=' . $this->uuid;
$cancel_url = base_url() . '/webhooks/waitlist/cancel?email=' . $this->email . '&confirmation_code=' . $this->uuid;
$mail->view('emails.waitlist-confirmation',
[
'confirmation_url' => $confirmation_url,
@@ -36,10 +34,10 @@ 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 for {$mail} with error: " . $th->getMessage());
ray($th->getMessage());
throw $th;
} catch (\Throwable $e) {
send_internal_notification("SendConfirmationForWaitlistJob failed for {$this->email} with error: " . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -0,0 +1,75 @@
<?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,
public ?string $topicId = null,
) {
}
/**
* 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,
];
if ($this->topicId) {
$payload['message_thread_id'] = $this->topicId;
}
$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,79 @@
<?php
namespace App\Jobs;
use App\Models\Server;
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\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Str;
class ServerDetailsCheckJob implements ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 120;
public function __construct(public Server $server)
{
}
public function middleware(): array
{
return [new WithoutOverlapping($this->server->uuid)];
}
public function uniqueId(): string
{
return $this->server->uuid;
}
public function handle(): void
{
try {
ray()->clearAll();
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server);
$containers = format_docker_command_output_to_json($containers);
$applications = $this->server->applications();
// ray($applications);
// ray(format_docker_command_output_to_json($containers));
foreach ($applications as $application) {
$uuid = data_get($application, 'uuid');
$foundContainer = $containers->filter(function ($value, $key) use ($uuid) {
$image = data_get($value, 'Config.Image');
return Str::startsWith($image, $uuid);
})->first();
if ($foundContainer) {
$containerStatus = data_get($foundContainer, 'State.Status');
$databaseStatus = data_get($application, 'status');
ray($containerStatus, $databaseStatus);
if ($containerStatus !== $databaseStatus) {
// $application->update(['status' => $containerStatus]);
}
}
}
// foreach ($containers as $container) {
// $labels = format_docker_labels_to_json(data_get($container,'Config.Labels'));
// $foundLabel = $labels->filter(fn ($value, $key) => Str::startsWith($key, 'coolify.applicationId'));
// if ($foundLabel->count() > 0) {
// $appFound = $applications->where('id', $foundLabel['coolify.applicationId'])->first();
// if ($appFound) {
// $containerStatus = data_get($container, 'State.Status');
// $databaseStatus = data_get($appFound, 'status');
// ray($containerStatus, $databaseStatus);
// }
// }
// }
} catch (\Throwable $e) {
// send_internal_notification('ServerDetailsCheckJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -33,10 +33,10 @@ class SubscriptionInvoiceFailedJob implements ShouldQueue
send_user_an_email($mail, $member->email);
}
});
} catch (\Throwable $th) {
send_internal_notification('SubscriptionInvoiceFailedJob failed with: ' . $th->getMessage());
ray($th->getMessage());
throw $th;
} catch (\Throwable $e) {
send_internal_notification('SubscriptionInvoiceFailedJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -0,0 +1,44 @@
<?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 SubscriptionTrialEndedJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Team $team
) {
}
public function handle(): void
{
try {
$session = getStripeCustomerPortalSession($this->team);
$mail = new MailMessage();
$mail->subject('Action required: You trial in Coolify Cloud ended.');
$mail->view('emails.trial-ended', [
'stripeCustomerPortal' => $session->url,
]);
$this->team->members()->each(function ($member) use ($mail) {
if ($member->isAdmin()) {
ray('Sending trial ended email to ' . $member->email);
send_user_an_email($mail, $member->email);
send_internal_notification('Trial reminder email sent to ' . $member->email);
}
});
} catch (\Throwable $e) {
send_internal_notification('SubscriptionTrialEndsSoonJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -0,0 +1,44 @@
<?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 SubscriptionTrialEndsSoonJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Team $team
) {
}
public function handle(): void
{
try {
$session = getStripeCustomerPortalSession($this->team);
$mail = new MailMessage();
$mail->subject('You trial in Coolify Cloud ends soon.');
$mail->view('emails.trial-ends-soon', [
'stripeCustomerPortal' => $session->url,
]);
$this->team->members()->each(function ($member) use ($mail) {
if ($member->isAdmin()) {
ray('Sending trial ending email to ' . $member->email);
send_user_an_email($mail, $member->email);
send_internal_notification('Trial reminder email sent to ' . $member->email);
}
});
} catch (\Throwable $e) {
send_internal_notification('SubscriptionTrialEndsSoonJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -105,7 +105,7 @@ class Application extends BaseModel
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false);
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->orderBy('key', 'asc');
}
public function runtime_environment_variables(): HasMany
@@ -127,7 +127,7 @@ class Application extends BaseModel
public function environment_variables_preview(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true);
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc');
}
public function runtime_environment_variables_preview(): HasMany

View File

@@ -20,13 +20,16 @@ class EnvironmentVariable extends Model
{
static::created(function ($environment_variable) {
if ($environment_variable->application_id && !$environment_variable->is_preview) {
ModelsEnvironmentVariable::create([
'key' => $environment_variable->key,
'value' => $environment_variable->value,
'is_build_time' => $environment_variable->is_build_time,
'application_id' => $environment_variable->application_id,
'is_preview' => true,
]);
$found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview', true)->first();
if (!$found) {
ModelsEnvironmentVariable::create([
'key' => $environment_variable->key,
'value' => $environment_variable->value,
'is_build_time' => $environment_variable->is_build_time,
'application_id' => $environment_variable->application_id,
'is_preview' => true,
]);
}
}
});
}
@@ -42,26 +45,22 @@ class EnvironmentVariable extends Model
private function get_environment_variables(string $environment_variable): string|null
{
// $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);
$environment_variable = str_replace('}}', '', $environment_variable);
if (str_starts_with($environment_variable, 'global.')) {
$environment_variable = str_replace('global.', '', $environment_variable);
// $environment_variable = GlobalEnvironmentVariable::where('name', $environment_variable)->where('team_id', $team_id)->first()?->value;
return $environment_variable;
}
$environment_variable = trim(decrypt($environment_variable));
if (Str::startsWith($environment_variable, '{{') && Str::endsWith($environment_variable, '}}') && Str::contains($environment_variable, 'global.')) {
$variable = Str::after($environment_variable, 'global.');
$variable = Str::before($variable, '}}');
$variable = Str::of($variable)->trim()->value;
// $environment_variable = GlobalEnvironmentVariable::where('name', $environment_variable)->where('team_id', $team_id)->first()?->value;
ray('global env variable');
return $environment_variable;
}
return decrypt($environment_variable);
return $environment_variable;
}
private function set_environment_variables(string $environment_variable): string|null
{
$environment_variable = trim($environment_variable);
if (!str_contains(trim($environment_variable), '{{') && !str_contains(trim($environment_variable), '}}')) {
return encrypt($environment_variable);
}
return $environment_variable;
return encrypt($environment_variable);
}
protected function key(): Attribute

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use phpseclib3\Crypt\PublicKeyLoader;
class PrivateKey extends BaseModel
{
@@ -19,6 +20,15 @@ class PrivateKey extends BaseModel
return PrivateKey::whereTeamId(currentTeam()->id)->select($selectArray->all());
}
public function publicKey()
{
try {
return PublicKeyLoader::load($this->private_key)->getPublicKey()->toString('OpenSSH',['comment' => '']);
} catch (\Throwable $e) {
return 'Error loading private key';
}
}
public function isEmpty()
{
if ($this->servers()->count() === 0 && $this->applications()->count() === 0 && $this->githubApps()->count() === 0 && $this->gitlabApps()->count() === 0) {

View File

@@ -30,7 +30,6 @@ class Server extends BaseModel
'server_id' => $server->id,
]);
}
});
static::deleting(function ($server) {
$server->destinations()->each(function ($destination) {
@@ -72,7 +71,6 @@ class Server extends BaseModel
$swarmDocker = collect($server->swarmDockers->all());
return $standaloneDocker->concat($swarmDocker);
}
public function settings()
{
return $this->hasOne(ServerSetting::class);
@@ -93,7 +91,8 @@ class Server extends BaseModel
return false;
}
public function databases() {
public function databases()
{
return $this->destinations()->map(function ($standaloneDocker) {
$postgresqls = $standaloneDocker->postgresqls;
return $postgresqls?->concat([]) ?? collect([]);
@@ -137,4 +136,24 @@ class Server extends BaseModel
{
return $this->belongsTo(Team::class);
}
public function isProxyShouldRun()
{
$shouldRun = false;
foreach ($this->applications() as $application) {
if (data_get($application, 'fqdn')) {
$shouldRun = true;
break;
}
}
if ($this->id === 0) {
$settings = InstanceSettings::get();
if (data_get($settings, 'fqdn')) {
$shouldRun = true;
}
}
return $shouldRun;
}
public function isFunctional() {
return $this->settings->is_reachable && $this->settings->is_usable;
}
}

View File

@@ -23,6 +23,6 @@ class StandaloneDocker extends BaseModel
public function attachedTo()
{
return $this->applications->count() > 0 || $this->databases->count() > 0;
return $this->applications?->count() > 0 || $this->databases?->count() > 0;
}
}

View File

@@ -28,6 +28,11 @@ class StandalonePostgresql extends BaseModel
'is_readonly' => true
]);
});
static::deleted(function ($database) {
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();
instant_remote_process(['docker volume rm postgres-data-' . $database->uuid], $database->destination->server, false);
});
}
public function portsMappings(): Attribute

View File

@@ -33,7 +33,7 @@ class Subscription extends Model
}
if (isStripe()) {
if (!$this->stripe_plan_id) {
return 'unknown';
return 'zero';
}
$subscription = Subscription::where('id', $this->id)->first();
if (!$subscription) {
@@ -54,6 +54,6 @@ class Subscription extends Model
return Str::of($stripePlanId)->after('stripe_price_id_')->before('_')->lower();
}
}
return 'unknown';
return 'zero';
}
}

View File

@@ -24,6 +24,14 @@ class Team extends Model implements SendsDiscord, SendsEmail
return data_get($this, 'discord_webhook_url', null);
}
public function routeNotificationForTelegram()
{
return [
"token" => data_get($this, 'telegram_token', null),
"chat_id" => data_get($this, 'telegram_chat_id', null),
];
}
public function getRecepients($notification)
{
$recipients = data_get($notification, 'emails', null);
@@ -112,4 +120,20 @@ class Team extends Model implements SendsDiscord, SendsEmail
{
return $this->hasMany(S3Storage::class);
}
public function trialEnded() {
foreach ($this->servers as $server) {
$server->settings()->update([
'is_usable' => false,
'is_reachable' => false,
]);
}
}
public function trialEndedButSubscribed() {
foreach ($this->servers as $server) {
$server->settings()->update([
'is_usable' => true,
'is_reachable' => true,
]);
}
}
}

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