Compare commits

...

103 Commits

Author SHA1 Message Date
Andras Bacsai
9d826d9fb4 Merge pull request #1202 from coollabsio/next
v4.0.0-beta.26
2023-09-08 09:08:19 +02:00
Andras Bacsai
0af221f9fc udpate 2023-09-08 09:06:00 +02:00
Andras Bacsai
9066c9bf90 update readme 2023-09-08 09:00:44 +02:00
Andras Bacsai
77e3208f00 feat: public database 2023-09-07 13:23:34 +02:00
Andras Bacsai
db5ecf07bd version++ 2023-09-07 09:14:27 +02:00
Andras Bacsai
7f28aa6985 Merge pull request #1201 from coollabsio/next
v4.0.0-beta.25
2023-09-07 09:04:33 +02:00
Andras Bacsai
9d53e04ce9 fix: saveModel email settings 2023-09-07 08:48:16 +02:00
Andras Bacsai
3db9a1dd6e version++ 2023-09-06 15:26:17 +02:00
Andras Bacsai
38f9761b67 Merge pull request #1200 from coollabsio/next
v4.0.0-beta.24
2023-09-06 15:16:44 +02:00
Andras Bacsai
7117ecf634 version++ 2023-09-06 15:16:33 +02:00
Andras Bacsai
522e20f10a proxy updates 2023-09-06 15:00:56 +02:00
Andras Bacsai
ebbce2396c fix: typo 2023-09-06 14:34:35 +02:00
Andras Bacsai
f9a2ff6d90 feat: add discord notifications 2023-09-06 14:31:38 +02:00
Andras Bacsai
df1b9e7319 oops 2023-09-06 12:08:52 +02:00
Andras Bacsai
e7c0c26b32 fix: stripe
add: custom error pages
fix: invititation
feat: new quick login for first users (UX++)
feat: more internal notifications
2023-09-06 12:07:34 +02:00
Andras Bacsai
0dbb8b4420 update name 2023-09-05 16:03:24 +02:00
Andras Bacsai
a4b44bacc1 updates 2023-09-05 15:43:56 +02:00
Andras Bacsai
1338e68b8c wip: backup existing database 2023-09-05 12:14:31 +02:00
Andras Bacsai
f6f4cdde24 new links 2023-09-05 11:53:34 +02:00
Andras Bacsai
3daadf13c6 fix: lowercase image names 2023-09-05 11:05:54 +02:00
Andras Bacsai
cbd5fab2e7 fix 2023-09-05 10:57:49 +02:00
Andras Bacsai
48ccb508f9 update tax collection 2023-09-05 10:49:17 +02:00
Andras Bacsai
67edce0612 update payment webhook 2023-09-05 10:46:36 +02:00
Andras Bacsai
e8a41d7e6e rename command 2023-09-05 10:22:24 +02:00
Andras Bacsai
be1dad03bd fix: do not show system wide git on cloud 2023-09-05 10:03:28 +02:00
Andras Bacsai
31db1db636 next helper image 2023-09-05 08:49:33 +02:00
Andras Bacsai
dca332a688 fix: overlapping apps 2023-09-04 16:59:02 +02:00
Andras Bacsai
35f19ed53f update helper 2023-09-04 16:51:36 +02:00
Andras Bacsai
a5c45ffe90 update pack + nixpacks 2023-09-04 16:50:39 +02:00
Andras Bacsai
b6c3a65d2d update dev build action 2023-09-04 16:48:36 +02:00
Andras Bacsai
220c8211fd update 2023-09-04 16:46:53 +02:00
Andras Bacsai
ffbd04df29 update helper 2023-09-04 16:40:16 +02:00
Andras Bacsai
73c59be865 fix helper 2023-09-04 16:25:18 +02:00
Andras Bacsai
3966abaf80 improve coolify-helper + add test new functionalities 2023-09-04 16:03:11 +02:00
Andras Bacsai
759517316a fix: add docker network to build process 2023-09-04 10:18:30 +02:00
Andras Bacsai
3e3024d47e fix: add navbar for source + keys 2023-09-04 09:44:44 +02:00
Andras Bacsai
517cb77637 update 2023-09-03 11:54:05 +02:00
Andras Bacsai
14bd89a991 update 2023-09-03 11:51:00 +02:00
Andras Bacsai
304de29924 update waitlsit 2023-09-03 11:46:00 +02:00
Andras Bacsai
ab3055150f test distributed application check 2023-09-02 15:53:12 +02:00
Andras Bacsai
6b9c7aa9c5 feat: send request in cloud 2023-09-02 15:37:25 +02:00
Andras Bacsai
040f47b59c fix: show hosted email service, just disable for non pro subs 2023-09-02 13:41:42 +02:00
Andras Bacsai
eac7834083 fix: form address 2023-09-02 13:39:44 +02:00
Andras Bacsai
135a298080 remove line 2023-09-02 13:27:03 +02:00
Andras Bacsai
0065a86371 update seeder 2023-09-01 16:36:41 +02:00
Andras Bacsai
2a842a2f50 Do not schedule autoupdate 2023-09-01 16:30:50 +02:00
Andras Bacsai
231c02e00e version++ 2023-09-01 16:16:35 +02:00
Andras Bacsai
4de4587ea6 Merge pull request #1196 from coollabsio/next
version++
2023-09-01 16:09:23 +02:00
Andras Bacsai
8675e1d13f version++ 2023-09-01 16:09:03 +02:00
Andras Bacsai
6ceacc68cc more unique container name 2023-09-01 16:07:46 +02:00
Andras Bacsai
4aacf134b7 Merge pull request #1195 from coollabsio/next
v4.0.0-beta.23
2023-09-01 16:03:28 +02:00
Andras Bacsai
0605772715 better emails 2023-09-01 15:55:55 +02:00
Andras Bacsai
3fa53556f4 better emails 2023-09-01 15:52:18 +02:00
Andras Bacsai
76510b8971 fix: button loading animation 2023-09-01 11:35:40 +02:00
Andras Bacsai
66162966b9 fix: sentry bug 2023-09-01 11:35:33 +02:00
Andras Bacsai
71e1571c39 Add affected users to sentry 2023-09-01 11:20:58 +02:00
Andras Bacsai
806b761e74 Merge pull request #1194 from coollabsio/next
Sentry version update
2023-09-01 10:33:19 +02:00
Andras Bacsai
0176b38958 oops 2023-09-01 10:32:29 +02:00
Andras Bacsai
7a180c7310 Merge pull request #1193 from coollabsio/next
v4.0.0-beta.21
2023-09-01 10:27:49 +02:00
Andras Bacsai
3e1120182c update 2023-09-01 10:21:35 +02:00
Andras Bacsai
8e86ce671c fix: dockerimage jobs are not overlapping 2023-09-01 10:11:00 +02:00
Andras Bacsai
f75a324030 fix: proxy start job 2023-09-01 10:02:24 +02:00
Andras Bacsai
3fabff93f6 fix a few things 2023-09-01 09:34:25 +02:00
Andras Bacsai
e74efc4e76 fix 2023-08-31 21:56:53 +02:00
Andras Bacsai
472ed0753d update 2023-08-31 21:51:05 +02:00
Andras Bacsai
67538ff60c no double slot 2023-08-31 21:46:30 +02:00
Andras Bacsai
ae8bd69106 able to use resend for pro+ users 2023-08-31 15:00:59 +02:00
Andras Bacsai
2538890b52 feat: add resend as transactional emails 2023-08-31 13:10:39 +02:00
Andras Bacsai
87dd819ae4 fix: password confirmation 2023-08-31 09:56:37 +02:00
Andras Bacsai
7ec560d4a2 update 2023-08-30 18:36:06 +02:00
Andras Bacsai
6f9cd6a16b no localhost in cloud 2023-08-30 18:35:20 +02:00
Andras Bacsai
923af88336 fix: subscriptions 2023-08-30 18:23:55 +02:00
Andras Bacsai
5b6667c461 refactor + fixes 2023-08-30 16:01:38 +02:00
Andras Bacsai
6f00740f67 better boarding flow 2023-08-30 14:46:51 +02:00
Andras Bacsai
248863cf16 update boarding process 2023-08-30 11:26:46 +02:00
Andras Bacsai
97d48823dd improve boarding 2023-08-30 11:06:44 +02:00
Andras Bacsai
5eb41e1a15 fix boarding 2023-08-29 20:34:01 +02:00
Andras Bacsai
4a4837d9f5 fix 2023-08-29 20:25:42 +02:00
Andras Bacsai
4ad72fab7b refactor 2023-08-29 16:31:46 +02:00
Andras Bacsai
fe68e45609 refactor 2023-08-29 15:51:30 +02:00
Andras Bacsai
291b9a84ef refactoring 2023-08-29 14:36:17 +02:00
Andras Bacsai
2f9b7b188a ui update 2023-08-29 10:11:18 +02:00
Andras Bacsai
d04d41bc23 update dockercleanupjob 2023-08-29 10:00:29 +02:00
Andras Bacsai
6cb3d7167f fix ui 2023-08-28 23:27:46 +02:00
Andras Bacsai
90b1659a18 fix 2023-08-28 22:25:18 +02:00
Andras Bacsai
1aaf44f9b0 fix 2023-08-28 21:22:53 +02:00
Andras Bacsai
d7cfb84351 fix gh create button 2023-08-28 21:15:30 +02:00
Andras Bacsai
d28cf0b76d fix: webhook endpoint in cloud and no system wide gh app 2023-08-28 21:03:07 +02:00
Andras Bacsai
b4a3236284 fix 2023-08-28 20:52:45 +02:00
Andras Bacsai
556168892d fix job 2023-08-28 20:45:53 +02:00
Andras Bacsai
77667be570 fix: validation 2023-08-28 20:29:44 +02:00
Andras Bacsai
f48a912287 update cloud no localhost server 2023-08-28 18:02:31 +02:00
Andras Bacsai
af30d0831d do not seed in coolify cloud 2023-08-28 15:03:44 +02:00
Andras Bacsai
5989eb8f6e rename license server 2023-08-28 15:02:03 +02:00
Andras Bacsai
2bb778834b remove cloud configs 2023-08-28 14:40:18 +02:00
Andras Bacsai
a5ce191e4d add example cloud 2023-08-28 13:33:20 +02:00
Andras Bacsai
7617756576 remove dynamic sentry version checker 2023-08-28 13:32:36 +02:00
Andras Bacsai
0dfd3a5b0e run scheduled jobs on one server 2023-08-28 11:43:01 +02:00
Andras Bacsai
61a54f48c5 update 2023-08-28 10:44:11 +02:00
Andras Bacsai
5ca0237e34 fix logging on ui 2023-08-27 22:05:37 +02:00
Andras Bacsai
ab1207e461 fix:dockerCleanupjob 2023-08-27 21:36:11 +02:00
Andras Bacsai
75fea4f7c0 disable docker cleanup job 2023-08-27 16:23:02 +02:00
Andras Bacsai
fb34eb5394 new version 2023-08-27 15:49:35 +02:00
219 changed files with 3734 additions and 2473 deletions

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ name: Coolify Helper Image (v4)
on:
push:
branches: [ "main", "next" ]
branches: [ "main" ]
paths:
- .github/workflows/coolify-helper.yml
- docker/coolify-helper/Dockerfile
@@ -55,7 +55,7 @@ jobs:
file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:aarch64
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
merge-manifest:
runs-on: ubuntu-latest
permissions:
@@ -78,3 +78,7 @@ jobs:
- name: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}

View File

@@ -3,6 +3,9 @@ name: Development Build (v4)
on:
push:
branches: ["next"]
paths-ignore:
- .github/workflows/coolify-helper.yml
- docker/coolify-helper/Dockerfile
env:
REGISTRY: ghcr.io
@@ -73,4 +76,4 @@ jobs:
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}

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

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

View File

@@ -9,15 +9,20 @@ class SaveConfigurationSync
{
public function __invoke(Server $server, string $configuration)
{
$proxy_path = get_proxy_path();
$docker_compose_yml_base64 = base64_encode($configuration);
try {
$proxy_path = get_proxy_path();
$docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
$server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
instant_remote_process([
"mkdir -p $proxy_path",
"echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml",
], $server);
} catch (\Throwable $th) {
ray($th);
}
instant_remote_process([
"mkdir -p $proxy_path",
"echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml",
], $server);
}
}

View File

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

View File

@@ -10,8 +10,14 @@ 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"
}
}');
if (isDev()) {
$activity = remote_process([
"echo ####### Installing Prerequisites...",
@@ -37,11 +43,14 @@ class InstallDocker
"docker network create --attachable coolify",
"echo ####### Done!"
], $server);
StandaloneDocker::create([
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,
]);
$found = StandaloneDocker::where('server_id', $server->id);
if ($found->count() == 0) {
StandaloneDocker::create([
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,
]);
}
}

View File

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

View File

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

View File

@@ -6,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 {email?} {--only-email}';
/**
* The console command description.
@@ -34,7 +34,16 @@ class InviteFromWaitlist extends Command
public function handle()
{
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 +52,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 +68,7 @@ class InviteFromWaitlist extends Command
$already_registered = User::whereEmail($this->next_patient->email)->first();
if (!$already_registered) {
$this->password = Str::password();
$this->new_user = User::create([
User::create([
'name' => Str::of($this->next_patient->email)->before('@'),
'email' => $this->next_patient->email,
'password' => Hash::make($this->password),
@@ -73,10 +86,14 @@ class InviteFromWaitlist extends Command
}
private function send_email()
{
ray($this->next_patient->email, $this->password);
$token = Crypt::encryptString("{$this->next_patient->email}@@@$this->password");
$loginLink = route('auth.link', ['token' => $token]);
$mail = new MailMessage();
$mail->view('emails.waitlist-invitation', [
'email' => $this->next_patient->email,
'password' => $this->password,
'loginLink' => $loginLink,
]);
$mail->subject('Congratulations! You are invited to join Coolify Cloud.');
send_user_an_email($mail, $this->next_patient->email);

View File

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

View File

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

View File

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

View File

@@ -5,39 +5,57 @@ namespace App\Http\Controllers;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\S3Storage;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use App\Models\TeamInvitation;
use App\Models\User;
use App\Models\Waitlist;
use Auth;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http;
use Throwable;
use Str;
class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
public function waitlist() {
$waiting_in_line = Waitlist::whereVerified(true)->count();
return view('auth.waitlist', [
'waiting_in_line' => $waiting_in_line,
]);
public function link()
{
$token = request()->get('token');
if ($token) {
$decrypted = Crypt::decryptString($token);
$email = Str::of($decrypted)->before('@@@');
$password = Str::of($decrypted)->after('@@@');
$user = User::whereEmail($email)->first();
if (!$user) {
return redirect()->route('login');
}
if (Hash::check($password, $user->password)) {
Auth::login($user);
$team = $user->teams()->first();
session(['currentTeam' => $team]);
return redirect()->route('dashboard');
}
}
return redirect()->route('login')->with('error', 'Invalid credentials.');
}
public function subscription()
{
if (!is_cloud()) {
if (!isCloud()) {
abort(404);
}
return view('subscription.show', [
return view('subscription.index', [
'settings' => InstanceSettings::get(),
]);
}
public function license()
{
if (!is_cloud()) {
if (!isCloud()) {
abort(404);
}
return view('settings.license', [
@@ -45,27 +63,12 @@ class Controller extends BaseController
]);
}
public function force_passoword_reset() {
public function force_passoword_reset()
{
return view('auth.force-password-reset');
}
public function dashboard()
public function boarding()
{
$projects = Project::ownedByCurrentTeam()->get();
$servers = Server::ownedByCurrentTeam()->get();
$s3s = S3Storage::ownedByCurrentTeam()->get();
$resources = 0;
foreach ($projects as $project) {
$resources += $project->applications->count();
$resources += $project->postgresqls->count();
}
return view('dashboard', [
'servers' => $servers->count(),
'projects' => $projects->count(),
'resources' => $resources,
's3s' => $s3s,
]);
}
public function boarding() {
if (currentTeam()->boarding || isDev()) {
return view('boarding');
} else {
@@ -97,7 +100,7 @@ class Controller extends BaseController
if (auth()->user()->isAdminFromSession()) {
$invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
}
return view('team.show', [
return view('team.index', [
'invitations' => $invitations,
]);
}
@@ -146,7 +149,7 @@ class Controller extends BaseController
if ($diff <= config('constants.invitation.link.expiration')) {
$user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
$invitation->delete();
return redirect()->route('team.show');
return redirect()->route('team.index');
} else {
$invitation->delete();
abort(401);
@@ -168,7 +171,7 @@ class Controller extends BaseController
abort(401);
}
$invitation->delete();
return redirect()->route('team.show');
return redirect()->route('team.index');
} catch (Throwable $th) {
throw $th;
}

View File

@@ -43,6 +43,7 @@ class ProjectController extends Controller
{
$type = request()->query('type');
$destination_uuid = request()->query('destination');
$server = requesT()->query('server');
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {

View File

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

View File

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

View File

@@ -1,17 +1,20 @@
<?php
namespace App\Http\Livewire;
namespace App\Http\Livewire\Boarding;
use App\Actions\Server\InstallDocker;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use Illuminate\Support\Collection;
use Livewire\Component;
class Boarding extends Component
class Index extends Component
{
public string $currentState = 'welcome';
public ?Collection $privateKeys = null;
public ?int $selectedExistingPrivateKey = null;
public ?string $privateKeyType = null;
public ?string $privateKey = null;
public ?string $publicKey = null;
@@ -19,6 +22,8 @@ class Boarding extends Component
public ?string $privateKeyDescription = null;
public ?PrivateKey $createdPrivateKey = null;
public ?Collection $servers = null;
public ?int $selectedExistingServer = null;
public ?string $remoteServerName = null;
public ?string $remoteServerDescription = null;
public ?string $remoteServerHost = null;
@@ -26,6 +31,8 @@ class Boarding extends Component
public ?string $remoteServerUser = 'root';
public ?Server $createdServer = null;
public Collection|array $projects = [];
public ?int $selectedExistingProject = null;
public ?Project $createdProject = null;
public function mount()
@@ -45,6 +52,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->remoteServerHost = 'coolify-testing-host';
}
}
public function welcome() {
if (isCloud()) {
return $this->setServerType('remote');
}
$this->currentState = 'select-server-type';
}
public function restartBoarding()
{
if ($this->createdServer) {
@@ -63,20 +76,62 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
refreshSession();
return redirect()->route('dashboard');
}
public function setServer(string $type)
public function setServerType(string $type)
{
if ($type === 'localhost') {
$this->createdServer = Server::find(0);
if (!$this->createdServer) {
return $this->emit('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
}
$this->currentState = 'select-proxy';
return $this->validateServer();
} elseif ($type === 'remote') {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
if ($this->privateKeys->count() > 0) {
$this->selectedExistingPrivateKey = $this->privateKeys->first()->id;
}
$this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
if ($this->servers->count() > 0) {
$this->selectedExistingServer = $this->servers->first()->id;
$this->currentState = 'select-existing-server';
return;
}
$this->currentState = 'private-key';
}
}
public function selectExistingServer()
{
$this->createdServer = Server::find($this->selectedExistingServer);
if (!$this->createdServer) {
$this->emit('error', 'Server is not found.');
$this->currentState = 'private-key';
return;
}
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
$this->validateServer();
$this->getProxyType();
$this->getProjects();
}
public function getProxyType() {
$proxyTypeSet = $this->createdServer->proxy->type;
if (!$proxyTypeSet) {
$this->currentState = 'select-proxy';
return;
}
$this->getProjects();
}
public function selectExistingPrivateKey()
{
$this->currentState = 'create-server';
}
public function createNewServer()
{
$this->selectedExistingServer = null;
$this->currentState = 'private-key';
}
public function setPrivateKey(string $type)
{
$this->selectedExistingPrivateKey = null;
$this->privateKeyType = $type;
if ($type === 'create' && !isDev()) {
$this->createNewPrivateKey();
@@ -115,11 +170,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
'private_key_id' => $this->createdPrivateKey->id,
'team_id' => currentTeam()->id
]);
$this->validateServer();
}
public function validateServer() {
try {
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->createdServer);
if (!$uptime) {
$this->createdServer->delete();
$this->createdPrivateKey->delete();
throw new \Exception('Server is not reachable.');
} else {
$this->createdServer->settings->update([
@@ -127,11 +183,14 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
]);
$this->emit('success', 'Server is reachable.');
}
if ($dockerVersion) {
ray($dockerVersion, $uptime);
if (!$dockerVersion) {
$this->emit('error', 'Docker is not installed on the server.');
$this->currentState = 'install-docker';
return;
}
$this->getProxyType();
} catch (\Exception $e) {
return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this);
}
@@ -145,13 +204,25 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function selectProxy(string|null $proxyType = null)
{
if (!$proxyType) {
return $this->currentState = 'create-project';
return $this->getProjects();
}
$this->createdServer->proxy->type = $proxyType;
$this->createdServer->proxy->status = 'exited';
$this->createdServer->save();
$this->getProjects();
}
public function getProjects() {
$this->projects = Project::ownedByCurrentTeam(['name'])->get();
if ($this->projects->count() > 0) {
$this->selectedExistingProject = $this->projects->first()->id;
}
$this->currentState = 'create-project';
}
public function selectExistingProject() {
$this->createdProject = Project::find($this->selectedExistingProject);
$this->currentState = 'create-resource';
}
public function createNewProject()
{
$this->createdProject = Project::create([
@@ -168,7 +239,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
[
'project_uuid' => $this->createdProject->uuid,
'environment_name' => 'production',
'server'=> $this->createdServer->id,
]
);
}
@@ -176,6 +247,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
{
$this->privateKeyName = generate_random_name();
$this->privateKeyDescription = 'Created by Coolify';
['private' => $this->privateKey, 'public'=> $this->publicKey] = generateSSHKey();
['private' => $this->privateKey, 'public' => $this->publicKey] = generateSSHKey();
}
public function render()
{
return view('livewire.boarding.index')->layout('layouts.boarding');
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Http\Livewire;
use App\Models\Project;
use App\Models\S3Storage;
use App\Models\Server;
use Livewire\Component;
class Dashboard extends Component
{
public int $projects = 0;
public int $servers = 0;
public int $s3s = 0;
public int $resources = 0;
public function mount()
{
$this->servers = Server::ownedByCurrentTeam()->get()->count();
$this->s3s = S3Storage::ownedByCurrentTeam()->get()->count();
$projects = Project::ownedByCurrentTeam()->get();
foreach ($projects as $project) {
$this->resources += $project->applications->count();
$this->resources += $project->postgresqls->count();
}
$this->projects = $projects->count();
}
// public function getIptables()
// {
// $servers = Server::ownedByCurrentTeam()->get();
// foreach ($servers as $server) {
// checkRequiredCommands($server);
// $iptables = instant_remote_process(['docker run --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c "iptables -L -n | jc --iptables"'], $server);
// ray($iptables);
// }
// }
public function render()
{
return view('livewire.dashboard');
}
}

View File

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

View File

@@ -18,22 +18,26 @@ class ForcePasswordReset extends Component
'password' => 'required|min:8',
'password_confirmation' => 'required|same:password',
];
public function mount() {
public function mount()
{
$this->email = auth()->user()->email;
}
public function submit() {
public function submit()
{
try {
$this->rateLimit(10);
$this->validate();
$firstLogin = auth()->user()->created_at == auth()->user()->updated_at;
auth()->user()->forceFill([
'password' => Hash::make($this->password),
'force_password_reset' => false,
])->save();
auth()->logout();
return redirect()->route('login')->with('status', 'Your initial password has been set.');
} catch(\Exception $e) {
return general_error_handler(err:$e, that:$this);
if ($firstLogin) {
send_internal_notification('First login for ' . auth()->user()->email);
}
return redirect()->route('dashboard');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
}
}
}

View File

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

View File

@@ -8,26 +8,30 @@ use Livewire\Component;
class DiscordSettings extends Component
{
public Team $model;
public Team $team;
protected $rules = [
'model.discord_enabled' => 'nullable|boolean',
'model.discord_webhook_url' => 'required|url',
'model.discord_notifications_test' => 'nullable|boolean',
'model.discord_notifications_deployments' => 'nullable|boolean',
'model.discord_notifications_status_changes' => 'nullable|boolean',
'model.discord_notifications_database_backups' => 'nullable|boolean',
'team.discord_enabled' => 'nullable|boolean',
'team.discord_webhook_url' => 'required|url',
'team.discord_notifications_test' => 'nullable|boolean',
'team.discord_notifications_deployments' => 'nullable|boolean',
'team.discord_notifications_status_changes' => 'nullable|boolean',
'team.discord_notifications_database_backups' => 'nullable|boolean',
];
protected $validationAttributes = [
'model.discord_webhook_url' => 'Discord Webhook',
'team.discord_webhook_url' => 'Discord Webhook',
];
public function mount()
{
$this->team = auth()->user()->currentTeam();
}
public function instantSave()
{
try {
$this->submit();
} catch (\Exception $e) {
ray($e->getMessage());
$this->model->discord_enabled = false;
$this->team->discord_enabled = false;
$this->validate();
}
}
@@ -41,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

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

View File

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

View File

@@ -27,7 +27,7 @@ class Change extends Component
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) {

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

View File

@@ -7,15 +7,19 @@ use App\Models\GithubApp;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Traits\SaveFromRedirect;
use Illuminate\Support\Facades\Http;
use Livewire\Component;
use Route;
class GithubPrivateRepository extends Component
{
use SaveFromRedirect;
public $current_step = 'github_apps';
public $github_apps;
public GithubApp $github_app;
public $parameters;
public $currentRoute;
public $query;
public $type;
@@ -36,14 +40,30 @@ class GithubPrivateRepository extends Component
public string|null $publish_directory = null;
protected int $page = 1;
// public function saveFromRedirect(string $route, ?Collection $parameters = null){
// session()->forget('from');
// if (!$parameters || $parameters->count() === 0) {
// $parameters = $this->parameters;
// }
// $parameters = collect($parameters) ?? collect([]);
// $queries = collect($this->query) ?? collect([]);
// $parameters = $parameters->merge($queries);
// session(['from'=> [
// 'back'=> $this->currentRoute,
// 'route' => $route,
// 'parameters' => $parameters
// ]]);
// return redirect()->route($route);
// }
public function mount()
{
$this->currentRoute = Route::currentRouteName();
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->repositories = $this->branches = collect();
$this->github_apps = GithubApp::private();
}
public function loadRepositories($github_app_id)
{
$this->repositories = collect();

View File

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

View File

@@ -3,45 +3,79 @@
namespace App\Http\Livewire\Project\New;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Countable;
use Illuminate\Support\Collection;
use Livewire\Component;
use Route;
class Select extends Component
{
public $current_step = 'type';
public ?int $server = null;
public string $type;
public string $server_id;
public string $destination_uuid;
public Countable|array|Server $servers;
public $destinations = [];
public Collection|array $standaloneDockers = [];
public Collection|array $swarmDockers = [];
public array $parameters;
public ?string $existingPostgresqlUrl = null;
protected $queryString = [
'server',
];
public function mount()
{
$this->parameters = get_route_parameters();
if (isDev()) {
$this->existingPostgresqlUrl = 'postgres://coolify:password@coolify-db:5432';
}
}
public function set_type(string $type)
// public function addExistingPostgresql()
// {
// try {
// instantCommand("psql {$this->existingPostgresqlUrl} -c 'SELECT 1'");
// $this->emit('success', 'Successfully connected to the database.');
// } catch (\Exception $e) {
// return general_error_handler($e, $this);
// }
// }
public function setType(string $type)
{
$this->type = $type;
if ($type === "existing-postgresql") {
$this->current_step = $type;
return;
}
if (count($this->servers) === 1) {
$server = $this->servers->first();
$this->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->setServer($foundServer);
}
}
$this->current_step = 'servers';
}
public function set_server(Server $server)
public function setServer(Server $server)
{
$this->server_id = $server->id;
$this->destinations = $server->destinations();
$this->standaloneDockers = $server->standaloneDockers;
$this->swarmDockers = $server->swarmDockers;
$this->current_step = 'destinations';
}
public function set_destination(string $destination_uuid)
public function setDestination(string $destination_uuid)
{
$this->destination_uuid = $destination_uuid;
redirect()->route('project.resources.new', [

View File

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

View File

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

View File

@@ -2,6 +2,8 @@
namespace App\Http\Livewire\Server\New;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Livewire\Component;
@@ -67,6 +69,11 @@ 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();

View File

@@ -12,7 +12,7 @@ class Proxy extends Component
{
public Server $server;
public ProxyTypes $selectedProxy = ProxyTypes::TRAEFIK_V2;
public ?string $selectedProxy = null;
public $proxy_settings = null;
public string|null $redirect_url = null;
@@ -20,6 +20,7 @@ class Proxy extends Component
public function mount()
{
$this->selectedProxy = $this->server->proxy->type;
$this->redirect_url = $this->server->proxy->redirect_url;
}
@@ -35,11 +36,12 @@ class Proxy extends Component
$this->emit('proxyStatusUpdated');
}
public function select_proxy(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');
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,9 +47,12 @@ class PricingPlans extends Component
'tax_id_collection' => [
'enabled' => true,
],
'automatic_tax' => [
'enabled' => true,
],
'mode' => 'subscription',
'success_url' => route('subscription.success'),
'cancel_url' => route('subscription.show',['cancelled' => true]),
'success_url' => route('dashboard', ['success' => true]),
'cancel_url' => route('subscription.index', ['cancelled' => true]),
];
$customer = currentTeam()->subscription?->stripe_customer_id ?? null;
if ($customer) {

View File

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

View File

@@ -25,6 +25,6 @@ class Delete extends Component
});
refreshSession();
return redirect()->route('team.show');
return redirect()->route('team.index');
}
}

View File

@@ -28,7 +28,6 @@ class Form extends Component
try {
$this->team->save();
refreshSession();
$this->emit('reloadWindow');
} catch (\Throwable $th) {
return general_error_handler($th, $this);
}

View File

@@ -14,6 +14,7 @@ class Invitations extends Component
{
TeamInvitation::find($invitation_id)->delete();
$this->refreshInvitations();
$this->emit('success', 'Invitation revoked.');
}
public function refreshInvitations()

View File

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

View File

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

View File

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

View File

@@ -6,14 +6,14 @@ use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SubscriptionValid
class IsSubscriptionValid
{
public function handle(Request $request, Closure $next): Response
{
if (isInstanceAdmin()) {
return $next($request);
}
if (!auth()->user() || !is_cloud()) {
if (!auth()->user() || !isCloud()) {
if ($request->path() === 'subscription') {
return redirect('/');
} else {

View File

@@ -20,6 +20,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
@@ -65,6 +66,7 @@ class ApplicationDeploymentJob implements ShouldQueue
private $log_model;
private Collection $saved_outputs;
public $tries = 1;
public function __construct(int $application_deployment_queue_id)
{
ray()->clearScreen();
@@ -174,8 +176,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();
@@ -199,8 +201,8 @@ class ApplicationDeploymentJob implements ShouldQueue
$tag = $tag->substr(0, 128);
}
$this->build_image_name = "{$this->application->git_repository}:{$tag}-build";
$this->production_image_name = "{$this->application->uuid}:{$tag}";
$this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
if (!$this->force_rebuild) {
@@ -235,7 +237,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(
@@ -257,7 +259,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')) {
@@ -275,8 +277,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}.'",
@@ -297,12 +299,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,
],
[
@@ -487,7 +496,7 @@ class ApplicationDeploymentJob implements ShouldQueue
],
'networks' => [
$this->destination->network => [
'external' => false,
'external' => true,
'name' => $this->destination->network,
'attachable' => true,
]
@@ -647,12 +656,12 @@ class ApplicationDeploymentJob implements ShouldQueue
private function build_image()
{
$this->execute_remote_command([
"echo -n 'Building docker image.'",
"echo -n 'Building docker image for your application.'",
]);
if ($this->application->settings->is_static) {
$this->execute_remote_command([
$this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
$this->execute_in_builder("docker build --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
]);
$dockerfile = base64_encode("FROM {$this->application->static_image}
@@ -685,12 +694,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
]);
}
}
@@ -699,7 +708,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

@@ -37,7 +37,7 @@ class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique
private function cleanup_waitlist()
{
$waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.confirmation_valid_for_minutes')))->get();
$waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.expiration')))->get();
foreach ($waitlist as $item) {
$item->delete();
}

View File

@@ -77,7 +77,7 @@ 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([
@@ -107,7 +107,7 @@ class DatabaseBackupJob implements ShouldQueue
try {
ray($this->backup_dir);
$commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name pg_dumpall -U {$this->database->postgres_user} > $this->backup_location";
$commands[] = "docker exec $this->container_name pg_dump -Fc -U {$this->database->postgres_user} > $this->backup_location";
$this->backup_output = instant_remote_process($commands, $this->server);

View File

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

View File

@@ -33,8 +33,9 @@ class ProxyCheckJob implements ShouldQueue
if ($status === 'running') {
continue;
}
// $server->team->notify(new ProxyStoppedNotification($server));
resolve(StartProxy::class)($server);
if (data_get($server, 'proxy.type')) {
resolve(StartProxy::class)($server);
}
}
} catch (\Throwable $th) {
ray($th->getMessage());

View File

@@ -39,9 +39,9 @@ class ProxyContainerStatusJob implements ShouldQueue, ShouldBeUnique
public function handle(): void
{
try {
$container = getContainerStatus(server: $this->server, all_data: true, container_id: 'coolify-proxy', throwError: true);
$container = getContainerStatus(server: $this->server, all_data: true, container_id: 'coolify-proxy', throwError: false);
$status = data_get($container, 'State.Status');
if (data_get($this->server,'proxy.status') !== $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'];

View File

@@ -3,6 +3,8 @@
namespace App\Jobs;
use App\Actions\Proxy\StartProxy;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -27,6 +29,11 @@ class ProxyStartJob implements ShouldQueue
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());

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Stripe\Stripe;
class SubscriptionInvoiceFailedJob implements ShouldQueue
{

View File

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

View File

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

View File

@@ -33,6 +33,9 @@ class Server extends BaseModel
});
static::deleting(function ($server) {
$server->destinations()->each(function ($destination) {
$destination->delete();
});
$server->settings()->delete();
});
}
@@ -70,8 +73,6 @@ class Server extends BaseModel
return $standaloneDocker->concat($swarmDocker);
}
public function settings()
{
return $this->hasOne(ServerSetting::class);
@@ -84,12 +85,20 @@ class Server extends BaseModel
public function isEmpty()
{
if ($this->applications()->count() === 0) {
$applications = $this->applications()->count() === 0;
$databases = $this->databases()->count() === 0;
if ($applications && $databases) {
return true;
}
return false;
}
public function databases() {
return $this->destinations()->map(function ($standaloneDocker) {
$postgresqls = $standaloneDocker->postgresqls;
return $postgresqls?->concat([]) ?? collect([]);
})->flatten();
}
public function applications()
{
return $this->destinations()->map(function ($standaloneDocker) {

View File

@@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class Subscription extends Model
{
@@ -14,19 +15,44 @@ class Subscription extends Model
}
public function type()
{
$basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids'));
$pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids'));
$ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_plan_ids'));
if (isLemon()) {
$basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids'));
$pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids'));
$ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_plan_ids'));
$subscription = $this->lemon_variant_id;
if (in_array($subscription, $basic)) {
return 'basic';
$subscription = $this->lemon_variant_id;
if (in_array($subscription, $basic)) {
return 'basic';
}
if (in_array($subscription, $pro)) {
return 'pro';
}
if (in_array($subscription, $ultimate)) {
return 'ultimate';
}
}
if (in_array($subscription, $pro)) {
return 'pro';
}
if (in_array($subscription, $ultimate)) {
return 'ultimate';
if (isStripe()) {
if (!$this->stripe_plan_id) {
return 'unknown';
}
$subscription = Subscription::where('id', $this->id)->first();
if (!$subscription) {
return null;
}
$subscriptionPlanId = data_get($subscription,'stripe_plan_id');
if (!$subscriptionPlanId) {
return null;
}
$subscriptionConfigs = collect(config('subscription'));
$stripePlanId = null;
$subscriptionConfigs->map(function ($value, $key) use ($subscriptionPlanId, &$stripePlanId) {
if ($value === $subscriptionPlanId){
$stripePlanId = $key;
};
})->first();
if ($stripePlanId) {
return Str::of($stripePlanId)->after('stripe_price_id_')->before('_')->lower();
}
}
return 'unknown';
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Notifications\Channels\SendsDiscord;
use App\Notifications\Channels\SendsEmail;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
@@ -14,6 +15,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
protected $guarded = [];
protected $casts = [
'personal_team' => 'boolean',
'smtp_password' => 'encrypted',
'resend_api_key' => 'encrypted',
];
public function routeNotificationForDiscord()
@@ -21,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);
@@ -30,6 +41,27 @@ class Team extends Model implements SendsDiscord, SendsEmail
}
return explode(',', $recipients);
}
public function limits(): Attribute
{
return Attribute::make(
get: function () {
if (config('coolify.self_hosted') || $this->id === 0) {
$subscription = 'self-hosted';
} else {
$subscription = data_get($this, 'subscription');
if (is_null($subscription)) {
$subscription = 'zero';
} else {
$subscription = $subscription->type();
}
}
$serverLimit = config('constants.limits.server')[strtolower($subscription)];
$sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)];
return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled];
}
);
}
public function members()
{

View File

@@ -32,6 +32,7 @@ class User extends Authenticatable implements SendsEmail
$team = [
'name' => $user->name . "'s Team",
'personal_team' => true,
'show_boarding' => true
];
if ($user->id === 0) {
$team['id'] = 0;
@@ -91,29 +92,20 @@ class User extends Authenticatable implements SendsEmail
return $found_root_team->count() > 0;
}
public function personalTeam()
{
return $this->teams()->where('personal_team', true)->first();
}
public function currentTeam()
{
return $this->teams()->where('team_id', session('currentTeam')->id)->first();
return Team::find(session('currentTeam')->id);
}
public function otherTeams()
{
$team_id = currentTeam()->id;
return auth()->user()->teams->filter(function ($team) use ($team_id) {
return $team->id != $team_id;
return auth()->user()->teams->filter(function ($team) {
return $team->id != currentTeam()->id;
});
}
public function role()
{
if ($this->teams()->where('team_id', 0)->first()) {
return 'admin';
}
return $this->teams()->where('team_id', currentTeam()->id)->first()->pivot->role;
return session('currentTeam')->pivot->role;
}
}

View File

@@ -18,15 +18,15 @@ class DeploymentFailed extends Notification implements ShouldQueue
public Application $application;
public string $deployment_uuid;
public ApplicationPreview|null $preview;
public ?ApplicationPreview $preview = null;
public string $application_name;
public string|null $deployment_url = null;
public ?string $deployment_url = null;
public string $project_uuid;
public string $environment_name;
public string|null $fqdn;
public ?string $fqdn = null;
public function __construct(Application $application, string $deployment_uuid, ApplicationPreview|null $preview)
public function __construct(Application $application, string $deployment_uuid, ?ApplicationPreview $preview = null)
{
$this->application = $application;
$this->deployment_uuid = $deployment_uuid;
@@ -43,19 +43,7 @@ class DeploymentFailed extends Notification implements ShouldQueue
public function via(object $notifiable): array
{
$channels = [];
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_deployments');
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_deployments');
if ($isEmailEnabled && $isSubscribedToEmailEvent) {
$channels[] = EmailChannel::class;
}
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
}
return $channels;
return setNotificationChannels($notifiable, 'deployments');
}
public function toMail(): MailMessage
@@ -67,9 +55,8 @@ class DeploymentFailed extends Notification implements ShouldQueue
$mail->subject('❌ Deployment failed of ' . $this->application_name . '.');
} else {
$fqdn = $this->preview->fqdn;
$mail->subject('❌ Pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . ' deployment failed.');
$mail->subject('❌ Deployment failed of pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . '.');
}
$mail->view('emails.application-deployment-failed', [
'name' => $this->application_name,
'fqdn' => $fqdn,
@@ -90,4 +77,19 @@ class DeploymentFailed extends Notification implements ShouldQueue
}
return $message;
}
public function toTelegram(): array
{
if ($this->preview) {
$message = '❌ Pull request #' . $this->preview->pull_request_id . ' of **' . $this->application_name . '** (' . $this->preview->fqdn . ') deployment failed: ';
} else {
$message = '❌ Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): ';
}
return [
"message" => $message,
"buttons" => [
"text" => "View Deployment Logs",
"url" => $this->deployment_url
],
];
}
}

View File

@@ -43,19 +43,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
public function via(object $notifiable): array
{
$channels = [];
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_deployments');
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_deployments');
if ($isEmailEnabled && $isSubscribedToEmailEvent) {
$channels[] = EmailChannel::class;
}
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
}
return $channels;
return setNotificationChannels($notifiable, 'deployments');
}
public function toMail(): MailMessage
@@ -99,4 +87,34 @@ class DeploymentSuccess extends Notification implements ShouldQueue
}
return $message;
}
public function toTelegram(): array
{
if ($this->preview) {
$message = '✅ New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . '';
if ($this->preview->fqdn) {
$buttons[] = [
"text" => "Open Application",
"url" => $this->preview->fqdn
];
}
} else {
$message = '✅ New version successfully deployed of ' . $this->application_name . '';
if ($this->fqdn) {
$buttons[] = [
"text" => "Open Application",
"url" => $this->fqdn
];
}
}
$buttons[] = [
"text" => "Deployment logs",
"url" => $this->deployment_url
];
return [
"message" => $message,
"buttons" => [
...$buttons
],
];
}
}

View File

@@ -37,19 +37,7 @@ class StatusChanged extends Notification implements ShouldQueue
public function via(object $notifiable): array
{
$channels = [];
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_status_changes');
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_status_changes');
if ($isEmailEnabled && $isSubscribedToEmailEvent) {
$channels[] = EmailChannel::class;
}
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
}
return $channels;
return setNotificationChannels($notifiable, 'status_changes');
}
public function toMail(): MailMessage
@@ -70,7 +58,20 @@ class StatusChanged extends Notification implements ShouldQueue
$message = '⛔ ' . $this->application_name . ' has been stopped.
';
$message .= '[Application URL](' . $this->application_url . ')';
$message .= '[Open Application in Coolify](' . $this->application_url . ')';
return $message;
}
public function toTelegram(): array
{
$message = '⛔ ' . $this->application_name . ' has been stopped.';
return [
"message" => $message,
"buttons" => [
[
"text" => "Open Application in Coolify",
"url" => $this->application_url
]
],
];
}
}

View File

@@ -6,7 +6,6 @@ use Exception;
use Illuminate\Mail\Message;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
class EmailChannel
{
@@ -24,11 +23,7 @@ class EmailChannel
[],
[],
fn (Message $message) => $message
->from(
data_get($notifiable, 'smtp_from_address'),
data_get($notifiable, 'smtp_from_name'),
)
->bcc($recepients)
->to($recepients)
->subject($mailMessage->subject)
->html((string)$mailMessage->render())
);
@@ -36,13 +31,20 @@ class EmailChannel
private function bootConfigs($notifiable): void
{
$password = data_get($notifiable, 'smtp_password');
if ($password) $password = decrypt($password);
if (Str::contains(data_get($notifiable, 'smtp_host'),'resend.com')) {
if (data_get($notifiable, 'use_instance_email_settings')) {
$type = set_transanctional_email_settings();
if (!$type) {
throw new Exception('No email settings found.');
}
return;
}
config()->set('mail.from.address', data_get($notifiable, 'smtp_from_address'));
config()->set('mail.from.name', data_get($notifiable, 'smtp_from_name'));
if (data_get($notifiable, 'resend_enabled')) {
config()->set('mail.default', 'resend');
config()->set('resend.api_key', $password);
} else {
config()->set('resend.api_key', data_get($notifiable, 'resend_api_key'));
}
if (data_get($notifiable, 'smtp_enabled')) {
config()->set('mail.default', 'smtp');
config()->set('mail.mailers.smtp', [
"transport" => "smtp",
@@ -50,7 +52,7 @@ class EmailChannel
"port" => data_get($notifiable, 'smtp_port'),
"encryption" => data_get($notifiable, 'smtp_encryption'),
"username" => data_get($notifiable, 'smtp_username'),
"password" => $password,
"password" => data_get($notifiable, 'smtp_password'),
"timeout" => data_get($notifiable, 'smtp_timeout'),
"local_domain" => null,
]);

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Notifications\Channels;
interface SendsTelegram
{
public function routeNotificationForTelegram();
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Notifications\Channels;
use App\Jobs\SendMessageToTelegramJob;
class TelegramChannel
{
public function send($notifiable, $notification): void
{
$data = $notification->toTelegram($notifiable);
$telegramData = $notifiable->routeNotificationForTelegram();
$message = data_get($data, 'message');
$buttons = data_get($data, 'buttons', []);
ray($message, $buttons);
$telegramToken = data_get($telegramData, 'token');
$chatId = data_get($telegramData, 'chat_id');
if (!$telegramToken || !$chatId || !$message) {
throw new \Exception('Telegram token, chat id and message are required');
}
dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId));
}
}

View File

@@ -4,16 +4,19 @@ namespace App\Notifications\Channels;
use App\Models\InstanceSettings;
use App\Models\User;
use Exception;
use Illuminate\Mail\Message;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Mail;
use Log;
class TransactionalEmailChannel
{
public function send(User $notifiable, Notification $notification): void
{
$settings = InstanceSettings::get();
if (data_get($settings, 'smtp_enabled') !== true) {
if (!data_get($settings, 'smtp_enabled') && !data_get($settings, 'resend_enabled')) {
Log::info('SMTP/Resend not enabled');
return;
}
$email = $notifiable->email;
@@ -26,10 +29,6 @@ class TransactionalEmailChannel
[],
[],
fn (Message $message) => $message
->from(
data_get($settings, 'smtp_from_address'),
data_get($settings, 'smtp_from_name')
)
->to($email)
->subject($mailMessage->subject)
->html((string)$mailMessage->render())
@@ -38,6 +37,9 @@ class TransactionalEmailChannel
private function bootConfigs(): void
{
set_transanctional_email_settings();
$type = set_transanctional_email_settings();
if (!$type) {
throw new Exception('No email settings found.');
}
}
}

View File

@@ -14,42 +14,41 @@ class BackupFailed extends Notification implements ShouldQueue
{
use Queueable;
public string $message = 'Backup FAILED';
public string $name;
public string $frequency;
public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output)
{
$this->message = "❌ Database backup for {$database->name} with frequency of $backup->frequency was FAILED.\n\nReason: $output";
$this->name = $database->name;
$this->frequency = $backup->frequency;
}
public function via(object $notifiable): array
{
$channels = [];
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_database_backups');
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_database_backups');
if ($isEmailEnabled && $isSubscribedToEmailEvent) {
$channels[] = EmailChannel::class;
}
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
}
ray($channels);
return $channels;
return setNotificationChannels($notifiable, 'database_backups');
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("❌ Backup FAILED for {$this->database->name}");
$mail->line($this->message);
$mail->subject(" [ACTION REQUIRED] Backup FAILED for {$this->database->name}");
$mail->view('emails.backup-failed', [
'name' => $this->name,
'frequency' => $this->frequency,
'output' => $this->output,
]);
return $mail;
}
public function toDiscord(): string
{
return $this->message;
return "❌ Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
}
public function toTelegram(): array
{
$message = "❌ Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
return [
"message" => $message,
];
}
}

View File

@@ -14,41 +14,40 @@ class BackupSuccess extends Notification implements ShouldQueue
{
use Queueable;
public string $message = 'Backup Success';
public string $name;
public string $frequency;
public function __construct(ScheduledDatabaseBackup $backup, public $database)
{
$this->message = "✅ Database backup for {$database->name} with frequency of $backup->frequency was successful.";
$this->name = $database->name;
$this->frequency = $backup->frequency;
}
public function via(object $notifiable): array
{
$channels = [];
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_database_backups');
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_database_backups');
if ($isEmailEnabled && $isSubscribedToEmailEvent) {
$channels[] = EmailChannel::class;
}
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
}
return $channels;
return setNotificationChannels($notifiable, 'database_backups');
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("✅ Backup success for {$this->database->name}");
$mail->line($this->message);
$mail->subject("✅ Backup successfully done for {$this->database->name}");
$mail->view('emails.backup-success', [
'name' => $this->name,
'frequency' => $this->frequency,
]);
return $mail;
}
public function toDiscord(): string
{
return $this->message;
return "✅ Database backup for {$this->name} with frequency of {$this->frequency} was successful.";
}
public function toTelegram(): array
{
$message = "✅ Database backup for {$this->name} with frequency of {$this->frequency} was successful.";
return [
"message" => $message,
];
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Notifications\Internal;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
@@ -12,16 +13,22 @@ class GeneralNotification extends Notification implements ShouldQueue
use Queueable;
public function __construct(public string $message)
{}
{
}
public function via(object $notifiable): array
{
$channels[] = DiscordChannel::class;
return $channels;
return [TelegramChannel::class, DiscordChannel::class];
}
public function toDiscord(): string
{
return $this->message;
}
public function toTelegram(): array
{
return [
"message" => $this->message,
];
}
}

View File

@@ -22,19 +22,7 @@ class NotReachable extends Notification implements ShouldQueue
public function via(object $notifiable): array
{
$channels = [];
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_status_changes');
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_status_changes');
// if ($isEmailEnabled && $isSubscribedToEmailEvent) {
// $channels[] = EmailChannel::class;
// }
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
}
return $channels;
return setNotificationChannels($notifiable, 'status_changes');
}
public function toMail(): MailMessage
@@ -55,4 +43,10 @@ class NotReachable extends Notification implements ShouldQueue
$message = '⛔ Server \'' . $this->server->name . '\' is unreachable (could be a temporary issue). If you receive this more than twice in a row, please check your server.';
return $message;
}
public function toTelegram(): array
{
return [
"message" => '⛔ Server \'' . $this->server->name . '\' is unreachable (could be a temporary issue). If you receive this more than twice in a row, please check your server.'
];
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Notifications;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -19,24 +20,13 @@ class Test extends Notification implements ShouldQueue
public function via(object $notifiable): array
{
$channels = [];
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
if ($isDiscordEnabled && empty($this->emails)) {
$channels[] = DiscordChannel::class;
}
if ($isEmailEnabled && !empty($this->emails)) {
$channels[] = EmailChannel::class;
}
return $channels;
return setNotificationChannels($notifiable, 'test');
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("Coolify Test Notification");
$mail->subject("Test Email");
$mail->view('emails.test');
return $mail;
}
@@ -48,4 +38,16 @@ class Test extends Notification implements ShouldQueue
$message .= '[Go to your dashboard](' . base_url() . ')';
return $message;
}
public function toTelegram(): array
{
return [
"message" => 'This is a test Telegram notification from Coolify.',
"buttons" => [
[
"text" => "Go to your dashboard",
"url" => base_url()
]
],
];
}
}

View File

@@ -20,16 +20,19 @@ class InvitationLink extends Notification implements ShouldQueue
return [TransactionalEmailChannel::class];
}
public function toMail(User $user): MailMessage
public function __construct(public User $user)
{
$invitation = TeamInvitation::whereEmail($user->email)->first();
}
public function toMail(): MailMessage
{
$invitation = TeamInvitation::whereEmail($this->user->email)->first();
$invitation_team = Team::find($invitation->team->id);
$mail = new MailMessage();
$mail->subject('Invitation for ' . $invitation_team->name);
$mail->view('emails.invitation-link', [
'team' => $invitation_team->name,
'email' => $user->email,
'email' => $this->user->email,
'invitation_link' => $invitation->link,
]);
return $mail;

View File

@@ -31,24 +31,11 @@ class ResetPassword extends Notification
public function via($notifiable)
{
if ($this->settings->smtp_enabled) {
$password = data_get($this->settings, 'smtp_password');
if ($password) $password = decrypt($password);
config()->set('mail.default', 'smtp');
config()->set('mail.mailers.smtp', [
"transport" => "smtp",
"host" => data_get($this->settings, 'smtp_host'),
"port" => data_get($this->settings, 'smtp_port'),
"encryption" => data_get($this->settings, 'smtp_encryption'),
"username" => data_get($this->settings, 'smtp_username'),
"password" => $password,
"timeout" => data_get($this->settings, 'smtp_timeout'),
"local_domain" => null,
]);
return ['mail'];
$type = set_transanctional_email_settings();
if (!$type) {
throw new \Exception('No email settings found.');
}
throw new \Exception('SMTP is not enabled');
return ['mail'];
}
public function toMail($notifiable)
@@ -63,10 +50,6 @@ class ResetPassword extends Notification
protected function buildMailMessage($url)
{
$mail = new MailMessage();
$mail->from(
data_get($this->settings, 'smtp_from_address'),
data_get($this->settings, 'smtp_from_name'),
);
$mail->subject('Reset Password');
$mail->view('emails.reset-password', ['url' => $url, 'count' => config('auth.passwords.' . config('auth.defaults.passwords') . '.expire')]);
return $mail;

View File

@@ -24,7 +24,7 @@ class Test extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject('Test Notification');
$mail->subject('Test Email');
$mail->view('emails.test');
return $mail;
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Policies;
use App\Models\Server;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class ServerPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return true;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Server $server): bool
{
return $user->teams()->get()->firstWhere('id', $server->team_id) !== null;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return true;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Server $server): bool
{
return $user->teams()->get()->firstWhere('id', $server->team_id) !== null;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Server $server): bool
{
return $user->teams()->get()->firstWhere('id', $server->team_id) !== null;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Server $server): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Server $server): bool
{
return false;
}
}

View File

@@ -43,22 +43,16 @@ class FortifyServiceProvider extends ServiceProvider
*/
public function boot(): void
{
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::registerView(function () {
$settings = InstanceSettings::get();
$waiting_in_line = Waitlist::whereVerified(true)->count();
if (!$settings->is_registration_enabled) {
return redirect()->route('login');
}
if (config('coolify.waitlist')) {
return view('auth.waitlist',[
'waiting_in_line' => $waiting_in_line,
]);
return redirect()->route('waitlist.index');
} else {
return view('auth.register',[
'waiting_in_line' => $waiting_in_line,
]);
return view('auth.register');
}
});

View File

@@ -41,6 +41,7 @@ trait ExecuteRemoteCommand
$remote_command = generate_ssh_command($private_key_location, $ip, $user, $port, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) {
$output = Str::of($output)->trim();
$new_log_entry = [
'command' => $command,
'output' => $output,

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Traits;
use Illuminate\Support\Collection;
trait SaveFromRedirect
{
public function saveFromRedirect(string $route, ?Collection $parameters = null)
{
session()->forget('from');
if (!$parameters || $parameters->count() === 0) {
$parameters = $this->parameters;
}
$parameters = collect($parameters) ?? collect([]);
$queries = collect($this->query) ?? collect([]);
$parameters = $parameters->merge($queries);
session(['from' => [
'back' => $this->currentRoute,
'route' => $route,
'parameters' => $parameters
]]);
return redirect()->route($route);
}
}

View File

@@ -15,7 +15,7 @@ class Button extends Component
public bool $disabled = false,
public bool $isModal = false,
public bool $noStyle = false,
public string|null $modalId = null,
public ?string $modalId = null,
public string $defaultClass = "btn btn-primary btn-sm font-normal text-white normal-case no-animation rounded border-none"
) {
if ($this->noStyle) {
@@ -23,9 +23,6 @@ class Button extends Component
}
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.forms.button');

View File

@@ -17,7 +17,7 @@ class Checkbox extends Component
public string|null $value = null,
public string|null $label = null,
public string|null $helper = null,
public bool $instantSave = false,
public string|bool $instantSave = false,
public bool $disabled = false,
public string $defaultClass = "toggle toggle-xs toggle-warning rounded disabled:bg-coolgray-200 disabled:opacity-50 placeholder:text-neutral-700"
) {

View File

@@ -20,7 +20,7 @@ function create_standalone_postgresql($environment_id, $destination_uuid): Stand
}
return StandalonePostgresql::create([
'name' => generate_database_name('postgresql'),
'postgres_password' => \Illuminate\Support\Str::password(),
'postgres_password' => \Illuminate\Support\Str::password(symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),

View File

@@ -58,9 +58,11 @@ function format_docker_envs_to_json($rawOutput)
}
function getApplicationContainerStatus(Application $application) {
$server = $application->destination->server;
$server = data_get($application,'destination.server');
$id = $application->id;
if (!$server) {
return 'exited';
}
$containers = getCurrentApplicationContainerStatus($server, $id);
if ($containers->count() > 0) {
$status = data_get($containers[0], 'State', 'exited');
@@ -84,7 +86,7 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
function generateApplicationContainerName(string $uuid, int $pull_request_id = 0)
{
$now = now()->format('YmdHis');
$now = now()->format('Hisu');
if ($pull_request_id !== 0 && $pull_request_id !== null) {
return $uuid . '-pr-' . $pull_request_id . '-' . $now;
} else {

View File

@@ -1,6 +1,7 @@
<?php
use App\Models\Server;
use App\Models\StandalonePostgresql;
use Symfony\Component\Yaml\Yaml;
function get_proxy_path()
@@ -166,3 +167,75 @@ function setup_default_redirect_404(string|null $redirect_url, Server $server)
}
}
}
function startPostgresProxy(StandalonePostgresql $database)
{
$containerName = "{$database->uuid}-proxy";
$configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
events {
worker_connections 1024;
}
stream {
server {
listen $database->public_port;
proxy_pass $database->uuid:5432;
}
}
EOF;
$docker_compose = [
'version' => '3.8',
'services' => [
$containerName => [
'image' => "nginx:stable-alpine",
'container_name' => $containerName,
'restart' => RESTART_MODE,
'volumes' => [
"$configuration_dir/nginx.conf:/etc/nginx/nginx.conf:ro",
],
'ports' => [
"$database->public_port:$database->public_port",
],
'networks' => [
$database->destination->network,
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'stat /etc/nginx/nginx.conf || exit 1',
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 3,
'start_period' => '1s'
],
]
],
'networks' => [
$database->destination->network => [
'external' => true,
'name' => $database->destination->network,
'attachable' => true,
]
]
];
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
$nginxconf_base64 = base64_encode($nginxconf);
instant_remote_process([
"mkdir -p $configuration_dir",
"echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf",
"echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml",
"docker compose --project-directory {$configuration_dir} up -d >/dev/null",
], $database->destination->server);
}
function stopPostgresProxy(StandalonePostgresql $database)
{
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server);
}

View File

@@ -16,11 +16,6 @@ use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Sleep;
use Spatie\Activitylog\Models\Activity;
/**
* Run a Remote Process, which SSH's asynchronously into a machine to run the command(s).
* @TODO Change 'root' to 'coolify' when it's able to run Docker commands without sudo
*
*/
function remote_process(
array $command,
Server $server,
@@ -82,6 +77,7 @@ function generate_ssh_command(string $private_key_location, string $server_ip, s
if ($isMux && config('coolify.mux_enabled')) {
$ssh_command .= '-o ControlMaster=auto -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/ssh/mux/%h_%p_%r ';
}
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$ssh_command .= "-i {$private_key_location} "
. '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
. '-o PasswordAuthentication=no '
@@ -97,7 +93,18 @@ function generate_ssh_command(string $private_key_location, string $server_ip, s
return $ssh_command;
}
function instantCommand(string $command, $throwError = true) {
$process = Process::run($command);
$output = trim($process->output());
$exitCode = $process->exitCode();
if ($exitCode !== 0) {
if (!$throwError) {
return null;
}
throw new \RuntimeException($process->errorOutput(), $exitCode);
}
return $output;
}
function instant_remote_process(array $command, Server $server, $throwError = true, $repeat = 1)
{
$command_string = implode("\n", $command);
@@ -167,17 +174,23 @@ function validateServer(Server $server)
{
try {
refresh_server_connection($server->privateKey);
$uptime = instant_remote_process(['uptime'], $server);
$uptime = instant_remote_process(['uptime'], $server, false);
if (!$uptime) {
$uptime = 'Server not reachable.';
throw new \Exception('Server not reachable.');
$server->settings->is_reachable = false;
return [
"uptime" => null,
"dockerVersion" => null,
];
}
$server->settings->is_reachable = true;
$dockerVersion = instant_remote_process(['docker version|head -2|grep -i version'], $server, false);
if (!$dockerVersion) {
$dockerVersion = 'Not installed.';
throw new \Exception('Docker not installed.');
$dockerVersion = null;
return [
"uptime" => $uptime,
"dockerVersion" => null,
];
}
$server->settings->is_usable = true;
return [
@@ -215,3 +228,29 @@ function check_server_connection(Server $server)
$server->save();
}
}
function checkRequiredCommands(Server $server)
{
$commands = collect(["jq", "jc"]);
foreach ($commands as $command) {
$commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false);
if ($commandFound) {
ray($command . ' found');
continue;
}
try {
instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'apt update && apt install -y {$command}'"], $server);
} catch (\Exception $e) {
ray('could not install ' . $command);
ray($e);
break;
}
$commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false);
if ($commandFound) {
ray($command . ' found');
continue;
}
ray('could not install ' . $command);
break;
}
}

View File

@@ -2,6 +2,9 @@
use App\Models\InstanceSettings;
use App\Models\Team;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Internal\GeneralNotification;
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use Illuminate\Database\QueryException;
@@ -25,6 +28,10 @@ function database_configuration_dir(): string
{
return '/data/coolify/databases';
}
function database_proxy_dir($uuid): string
{
return "/data/coolify/databases/$uuid/proxy";
}
function backup_dir(): string
{
@@ -52,12 +59,13 @@ function showBoarding(): bool
}
function refreshSession(): void
{
$team = currentTeam();
$team = Team::find(currentTeam()->id);
session(['currentTeam' => $team]);
}
function general_error_handler(Throwable | null $err = null, $that = null, $isJson = false, $customErrorMessage = null): mixed
{
try {
ray($err);
ray('ERROR OCCURRED: ' . $err->getMessage());
if ($err instanceof QueryException) {
if ($err->errorInfo[0] === '23505') {
@@ -70,6 +78,9 @@ function general_error_handler(Throwable | null $err = null, $that = null, $isJs
} elseif ($err instanceof TooManyRequestsException) {
throw new Exception($customErrorMessage ?? "Too many requests. Please try again in {$err->secondsUntilAvailable} seconds.");
} else {
if ($err->getMessage() === 'This action is unauthorized.') {
return redirect()->route('dashboard')->with('error', $customErrorMessage ?? $err->getMessage());
}
throw new Exception($customErrorMessage ?? $err->getMessage());
}
} catch (Throwable $error) {
@@ -117,10 +128,11 @@ function generateSSHKey()
$key = RSA::createKey();
return [
'private' => $key->toString('PKCS1'),
'public' => $key->getPublicKey()->toString('OpenSSH',['comment' => 'coolify-generated-ssh-key'])
'public' => $key->getPublicKey()->toString('OpenSSH', ['comment' => 'coolify-generated-ssh-key'])
];
}
function formatPrivateKey(string $privateKey) {
function formatPrivateKey(string $privateKey)
{
$privateKey = trim($privateKey);
if (!str_ends_with($privateKey, "\n")) {
$privateKey .= "\n";
@@ -135,30 +147,36 @@ function generate_application_name(string $git_repository, string $git_branch):
function is_transactional_emails_active(): bool
{
return data_get(InstanceSettings::get(), 'smtp_enabled');
return isEmailEnabled(InstanceSettings::get());
}
function set_transanctional_email_settings(InstanceSettings | null $settings = null): void
function set_transanctional_email_settings(InstanceSettings | null $settings = null): string|null
{
if (!$settings) {
$settings = InstanceSettings::get();
}
$password = data_get($settings, 'smtp_password');
if (isset($password)) {
$password = decrypt($password);
config()->set('mail.from.address', data_get($settings, 'smtp_from_address'));
config()->set('mail.from.name', data_get($settings, 'smtp_from_name'));
if (data_get($settings, 'resend_enabled')) {
config()->set('mail.default', 'resend');
config()->set('resend.api_key', data_get($settings, 'resend_api_key'));
return 'resend';
}
config()->set('mail.default', 'smtp');
config()->set('mail.mailers.smtp', [
"transport" => "smtp",
"host" => data_get($settings, 'smtp_host'),
"port" => data_get($settings, 'smtp_port'),
"encryption" => data_get($settings, 'smtp_encryption'),
"username" => data_get($settings, 'smtp_username'),
"password" => $password,
"timeout" => data_get($settings, 'smtp_timeout'),
"local_domain" => null,
]);
if (data_get($settings, 'smtp_enabled')) {
config()->set('mail.default', 'smtp');
config()->set('mail.mailers.smtp', [
"transport" => "smtp",
"host" => data_get($settings, 'smtp_host'),
"port" => data_get($settings, 'smtp_port'),
"encryption" => data_get($settings, 'smtp_encryption'),
"username" => data_get($settings, 'smtp_username'),
"password" => data_get($settings, 'smtp_password'),
"timeout" => data_get($settings, 'smtp_timeout'),
"local_domain" => null,
]);
return 'smtp';
}
return null;
}
function base_ip(): string
@@ -212,7 +230,7 @@ function isDev(): bool
return config('app.env') === 'local';
}
function is_cloud(): bool
function isCloud(): bool
{
return !config('coolify.self_hosted');
}
@@ -231,9 +249,9 @@ function validate_cron_expression($expression_to_validate): bool
function send_internal_notification(string $message): void
{
try {
$baseUrl = base_url(false);
$baseUrl = config('app.name');
$team = Team::find(0);
$team->notify(new GeneralNotification("👀 Internal notifications from {$baseUrl}: " . $message));
$team->notify(new GeneralNotification("👀 {$baseUrl}: " . $message));
} catch (\Throwable $th) {
ray($th->getMessage());
}
@@ -241,17 +259,40 @@ function send_internal_notification(string $message): void
function send_user_an_email(MailMessage $mail, string $email): void
{
$settings = InstanceSettings::get();
set_transanctional_email_settings($settings);
$type = set_transanctional_email_settings($settings);
if (!$type) {
throw new Exception('No email settings found.');
}
Mail::send(
[],
[],
fn (Message $message) => $message
->from(
data_get($settings, 'smtp_from_address'),
data_get($settings, 'smtp_from_name')
)
->to($email)
->subject($mail->subject)
->html((string) $mail->render())
);
}
function isEmailEnabled($notifiable)
{
return data_get($notifiable, 'smtp_enabled') || data_get($notifiable, 'resend_enabled') || data_get($notifiable, 'use_instance_email_settings');
}
function setNotificationChannels($notifiable, $event)
{
$channels = [];
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSubscribedToDiscordEvent = data_get($notifiable, "discord_notifications_$event");
$isSubscribedToTelegramEvent = data_get($notifiable, "telegram_notifications_$event");
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
}
if ($isEmailEnabled) {
$channels[] = EmailChannel::class;
}
if ($isTelegramEnabled && $isSubscribedToTelegramEvent) {
$channels[] = TelegramChannel::class;
}
return $channels;
}

View File

@@ -47,6 +47,9 @@ function getEndDate()
function isSubscriptionActive()
{
if (!isCloud()) {
return false;
}
$team = currentTeam();
if (!$team) {
return false;
@@ -56,21 +59,19 @@ function isSubscriptionActive()
if (!$subscription) {
return false;
}
if (config('subscription.provider') === 'lemon') {
if (isLemon()) {
return $subscription->lemon_status === 'active';
}
if (config('subscription.provider') === 'stripe') {
// if (isPaddle()) {
// return $subscription->paddle_status === 'active';
// }
if (isStripe()) {
return $subscription->stripe_invoice_paid === true && $subscription->stripe_cancel_at_period_end === false;
}
return false;
// if (config('subscription.provider') === 'paddle') {
// return $subscription->paddle_status === 'active';
// }
}
function isSubscriptionOnGracePeriod()
{
$team = currentTeam();
if (!$team) {
return false;
@@ -79,12 +80,12 @@ function isSubscriptionOnGracePeriod()
if (!$subscription) {
return false;
}
if (config('subscription.provider') === 'lemon') {
if (isLemon()) {
$is_still_grace_period = $subscription->lemon_ends_at &&
Carbon::parse($subscription->lemon_ends_at) > Carbon::now();
return $is_still_grace_period;
}
if (config('subscription.provider') === 'stripe') {
if (isStripe()) {
return $subscription->stripe_cancel_at_period_end;
}
return false;
@@ -93,10 +94,22 @@ function subscriptionProvider()
{
return config('subscription.provider');
}
function isLemon()
{
return config('subscription.provider') === 'lemon';
}
function isStripe()
{
return config('subscription.provider') === 'stripe';
}
function isPaddle()
{
return config('subscription.provider') === 'paddle';
}
function getStripeCustomerPortalSession(Team $team)
{
Stripe::setApiKey(config('subscription.stripe_api_key'));
$return_url = route('team.show');
$return_url = route('team.index');
$stripe_customer_id = $team->subscription->stripe_customer_id;
$session = \Stripe\BillingPortal\Session::create([
'customer' => $stripe_customer_id,
@@ -124,6 +137,6 @@ function allowedPathsForBoardingAccounts()
return [
...allowedPathsForUnsubscribedAccounts(),
'boarding',
'livewire/message/boarding',
'livewire/message/boarding.index',
];
}

View File

@@ -15,6 +15,7 @@
"laravel/fortify": "^v1.16.0",
"laravel/framework": "^v10.7.1",
"laravel/horizon": "^5.15",
"laravel/prompts": "^0.1.6",
"laravel/sanctum": "^v3.2.1",
"laravel/tinker": "^v2.8.1",
"laravel/ui": "^4.2",
@@ -42,7 +43,7 @@
"laravel/pint": "^v1.8.0",
"mockery/mockery": "^1.5.1",
"nunomaduro/collision": "^v7.4.0",
"pestphp/pest": "^v2.4.0",
"pestphp/pest": "^2.16",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.0.19",
"serversideup/spin": "^v1.1.0",

142
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "da14dce99d76abcaaa6393166eda049a",
"content-hash": "0603276b60e77cd859fabacdaaf31550",
"packages": [
{
"name": "aws/aws-crt-php",
@@ -6654,16 +6654,16 @@
},
{
"name": "symfony/console",
"version": "v6.3.2",
"version": "v6.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "aa5d64ad3f63f2e48964fc81ee45cb318a723898"
"reference": "eca495f2ee845130855ddf1cf18460c38966c8b6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/aa5d64ad3f63f2e48964fc81ee45cb318a723898",
"reference": "aa5d64ad3f63f2e48964fc81ee45cb318a723898",
"url": "https://api.github.com/repos/symfony/console/zipball/eca495f2ee845130855ddf1cf18460c38966c8b6",
"reference": "eca495f2ee845130855ddf1cf18460c38966c8b6",
"shasum": ""
},
"require": {
@@ -6724,7 +6724,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v6.3.2"
"source": "https://github.com/symfony/console/tree/v6.3.4"
},
"funding": [
{
@@ -6740,7 +6740,7 @@
"type": "tidelift"
}
],
"time": "2023-07-19T20:17:28+00:00"
"time": "2023-08-16T10:10:12+00:00"
},
{
"name": "symfony/css-selector",
@@ -7761,16 +7761,16 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.27.0",
"version": "v1.28.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a"
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a",
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"shasum": ""
},
"require": {
@@ -7785,7 +7785,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.27-dev"
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -7823,7 +7823,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
},
"funding": [
{
@@ -7839,7 +7839,7 @@
"type": "tidelift"
}
],
"time": "2022-11-03T14:55:06+00:00"
"time": "2023-01-26T09:26:14+00:00"
},
{
"name": "symfony/polyfill-iconv",
@@ -7926,16 +7926,16 @@
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.27.0",
"version": "v1.28.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "511a08c03c1960e08a883f4cffcacd219b758354"
"reference": "875e90aeea2777b6f135677f618529449334a612"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354",
"reference": "511a08c03c1960e08a883f4cffcacd219b758354",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612",
"reference": "875e90aeea2777b6f135677f618529449334a612",
"shasum": ""
},
"require": {
@@ -7947,7 +7947,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.27-dev"
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -7987,7 +7987,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0"
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0"
},
"funding": [
{
@@ -8003,7 +8003,7 @@
"type": "tidelift"
}
],
"time": "2022-11-03T14:55:06+00:00"
"time": "2023-01-26T09:26:14+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
@@ -8094,16 +8094,16 @@
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.27.0",
"version": "v1.28.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6"
"reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6",
"reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
"reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
"shasum": ""
},
"require": {
@@ -8115,7 +8115,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.27-dev"
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -8158,7 +8158,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0"
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0"
},
"funding": [
{
@@ -8174,20 +8174,20 @@
"type": "tidelift"
}
],
"time": "2022-11-03T14:55:06+00:00"
"time": "2023-01-26T09:26:14+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.27.0",
"version": "v1.28.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
"reference": "42292d99c55abe617799667f454222c54c60e229"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
"reference": "42292d99c55abe617799667f454222c54c60e229",
"shasum": ""
},
"require": {
@@ -8202,7 +8202,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.27-dev"
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -8241,7 +8241,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
},
"funding": [
{
@@ -8257,7 +8257,7 @@
"type": "tidelift"
}
],
"time": "2022-11-03T14:55:06+00:00"
"time": "2023-07-28T09:04:16+00:00"
},
{
"name": "symfony/polyfill-php72",
@@ -8337,16 +8337,16 @@
},
{
"name": "symfony/polyfill-php80",
"version": "v1.27.0",
"version": "v1.28.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936"
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"shasum": ""
},
"require": {
@@ -8355,7 +8355,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.27-dev"
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
@@ -8400,7 +8400,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
},
"funding": [
{
@@ -8416,7 +8416,7 @@
"type": "tidelift"
}
],
"time": "2022-11-03T14:55:06+00:00"
"time": "2023-01-26T09:26:14+00:00"
},
{
"name": "symfony/polyfill-php83",
@@ -8579,16 +8579,16 @@
},
{
"name": "symfony/process",
"version": "v6.3.2",
"version": "v6.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d"
"reference": "0b5c29118f2e980d455d2e34a5659f4579847c54"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d",
"reference": "c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d",
"url": "https://api.github.com/repos/symfony/process/zipball/0b5c29118f2e980d455d2e34a5659f4579847c54",
"reference": "0b5c29118f2e980d455d2e34a5659f4579847c54",
"shasum": ""
},
"require": {
@@ -8620,7 +8620,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v6.3.2"
"source": "https://github.com/symfony/process/tree/v6.3.4"
},
"funding": [
{
@@ -8636,7 +8636,7 @@
"type": "tidelift"
}
],
"time": "2023-07-12T16:00:22+00:00"
"time": "2023-08-07T10:39:22+00:00"
},
{
"name": "symfony/psr-http-message-bridge",
@@ -9982,16 +9982,16 @@
"packages-dev": [
{
"name": "brianium/paratest",
"version": "v7.2.5",
"version": "v7.2.6",
"source": {
"type": "git",
"url": "https://github.com/paratestphp/paratest.git",
"reference": "4d7ad5b6564f63baa1b948ecad05439f22880942"
"reference": "7f372b5bb59b4271adedc67d3129df29b84c4173"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/4d7ad5b6564f63baa1b948ecad05439f22880942",
"reference": "4d7ad5b6564f63baa1b948ecad05439f22880942",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/7f372b5bb59b4271adedc67d3129df29b84c4173",
"reference": "7f372b5bb59b4271adedc67d3129df29b84c4173",
"shasum": ""
},
"require": {
@@ -10005,19 +10005,19 @@
"phpunit/php-code-coverage": "^10.1.3",
"phpunit/php-file-iterator": "^4.0.2",
"phpunit/php-timer": "^6.0",
"phpunit/phpunit": "^10.3.1",
"phpunit/phpunit": "^10.3.2",
"sebastian/environment": "^6.0.1",
"symfony/console": "^6.3.2",
"symfony/process": "^6.3.2"
"symfony/console": "^6.3.4",
"symfony/process": "^6.3.4"
},
"require-dev": {
"doctrine/coding-standard": "^12.0.0",
"ext-pcov": "*",
"ext-posix": "*",
"infection/infection": "^0.27.0",
"phpstan/phpstan": "^1.10.26",
"phpstan/phpstan-deprecation-rules": "^1.1.3",
"phpstan/phpstan-phpunit": "^1.3.13",
"phpstan/phpstan": "^1.10.32",
"phpstan/phpstan-deprecation-rules": "^1.1.4",
"phpstan/phpstan-phpunit": "^1.3.14",
"phpstan/phpstan-strict-rules": "^1.5.1",
"squizlabs/php_codesniffer": "^3.7.2",
"symfony/filesystem": "^6.3.1"
@@ -10061,7 +10061,7 @@
],
"support": {
"issues": "https://github.com/paratestphp/paratest/issues",
"source": "https://github.com/paratestphp/paratest/tree/v7.2.5"
"source": "https://github.com/paratestphp/paratest/tree/v7.2.6"
},
"funding": [
{
@@ -10073,7 +10073,7 @@
"type": "paypal"
}
],
"time": "2023-08-08T13:23:59+00:00"
"time": "2023-08-29T07:47:39+00:00"
},
{
"name": "fakerphp/faker",
@@ -10707,24 +10707,24 @@
},
{
"name": "pestphp/pest",
"version": "v2.16.0",
"version": "v2.16.1",
"source": {
"type": "git",
"url": "https://github.com/pestphp/pest.git",
"reference": "cbd6a650576714c673dbb0575989663f7f5c8b6d"
"reference": "55b92666482b7d4320b7869c4eea7333d35c5631"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pestphp/pest/zipball/cbd6a650576714c673dbb0575989663f7f5c8b6d",
"reference": "cbd6a650576714c673dbb0575989663f7f5c8b6d",
"url": "https://api.github.com/repos/pestphp/pest/zipball/55b92666482b7d4320b7869c4eea7333d35c5631",
"reference": "55b92666482b7d4320b7869c4eea7333d35c5631",
"shasum": ""
},
"require": {
"brianium/paratest": "^7.2.5",
"brianium/paratest": "^7.2.6",
"nunomaduro/collision": "^7.8.1",
"nunomaduro/termwind": "^1.15.1",
"pestphp/pest-plugin": "^2.0.1",
"pestphp/pest-plugin-arch": "^2.3.1",
"pestphp/pest-plugin": "^2.1.1",
"pestphp/pest-plugin-arch": "^2.3.3",
"php": "^8.1.0",
"phpunit/phpunit": "^10.3.2"
},
@@ -10734,8 +10734,8 @@
},
"require-dev": {
"pestphp/pest-dev-tools": "^2.16.0",
"pestphp/pest-plugin-type-coverage": "^2.0.0",
"symfony/process": "^6.3.2"
"pestphp/pest-plugin-type-coverage": "^2.2.0",
"symfony/process": "^6.3.4"
},
"bin": [
"bin/pest"
@@ -10793,7 +10793,7 @@
],
"support": {
"issues": "https://github.com/pestphp/pest/issues",
"source": "https://github.com/pestphp/pest/tree/v2.16.0"
"source": "https://github.com/pestphp/pest/tree/v2.16.1"
},
"funding": [
{
@@ -10805,7 +10805,7 @@
"type": "github"
}
],
"time": "2023-08-21T08:42:07+00:00"
"time": "2023-08-29T09:30:36+00:00"
},
{
"name": "pestphp/pest-plugin",

View File

@@ -17,7 +17,7 @@ return [
|
*/
'name' => env('APP_NAME', 'Laravel'),
'name' => env('APP_NAME', 'Coolify'),
/*
|--------------------------------------------------------------------------

View File

@@ -94,7 +94,7 @@ return [
'users' => [
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60,
'expire' => 10,
'throttle' => 60,
],
],

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