Compare commits

...

184 Commits

Author SHA1 Message Date
Andras Bacsai
1281a0f7e4 Merge pull request #226 from coollabsio/v2.0.31
v2.0.31
2022-03-20 15:21:57 +01:00
Andras Bacsai
d8350cd4ee Migration file 2022-03-20 15:14:33 +01:00
Andras Bacsai
e3b7c23ed9 chore: Version++ 2022-03-20 15:05:05 +01:00
Andras Bacsai
eae1ea21d6 fix: Add nginx + htaccess files 2022-03-20 15:03:24 +01:00
Andras Bacsai
541aa76b64 fix: Only cleanup same app 2022-03-20 14:21:11 +01:00
Andras Bacsai
7b8555d524 fix: Cleanup old builds 2022-03-20 14:20:29 +01:00
Andras Bacsai
fdf998c181 css fix for select 2022-03-20 14:03:52 +01:00
Andras Bacsai
3d6b343adc remove mysql 2022-03-19 23:47:05 +01:00
Andras Bacsai
e338cecc14 feat: Add PHP modules 2022-03-19 23:46:33 +01:00
Andras Bacsai
e5537a33fb Merge pull request #223 from coollabsio/v2.0.30
v2.0.30
2022-03-19 15:11:42 +01:00
Andras Bacsai
35384deb68 fix: Remove build logs in case of app removed 2022-03-19 15:06:25 +01:00
Andras Bacsai
547ca60c2a fix: Better queue system + more support on monorepos 2022-03-19 15:04:52 +01:00
Andras Bacsai
376f6f7455 fix: Basedir for dockerfiles 2022-03-19 13:33:31 +01:00
Andras Bacsai
abe92dedff fix: no webhook secret found? 2022-03-15 17:35:37 +01:00
Andras Bacsai
4b521ceedc chore: version++ 2022-03-15 17:25:17 +01:00
Andras Bacsai
6dfcb9e52b fix: No error if GitSource is missing 2022-03-15 17:22:28 +01:00
Andras Bacsai
335e3216e2 fix: Missing session data 2022-03-15 17:21:18 +01:00
Andras Bacsai
5b22bb4818 fix: No cookie found 2022-03-15 17:04:15 +01:00
Andras Bacsai
0097004882 Merge pull request #217 from coollabsio/v2.0.29
v2.0.29
2022-03-12 00:28:26 +01:00
Andras Bacsai
1bc9e4c2d3 fix: Autodeploy true by default for GH repos 2022-03-11 23:56:11 +01:00
Andras Bacsai
36c7e1a3c3 feat: Install pnpm into docker image if pnpm lock file is used 2022-03-11 23:55:57 +01:00
Andras Bacsai
c6b4d04e26 Revert double build 2022-03-11 22:48:55 +01:00
Andras Bacsai
fa6cf068c7 feat: Autodeploy pause 2022-03-11 22:36:21 +01:00
Andras Bacsai
7c273a3a48 feat: Check ssl for new apps/services first 2022-03-11 21:28:27 +01:00
Andras Bacsai
3de2ea1523 chore: version++ 2022-03-11 21:19:03 +01:00
Andras Bacsai
c5c9f84503 feat: Webhooks inititate all applications with the correct branch 2022-03-11 21:18:12 +01:00
Andras Bacsai
16ea9a3e07 Update options request 2022-03-11 20:52:11 +01:00
Andras Bacsai
48f952c798 fix: Personal Gitlab repos 2022-03-11 20:47:26 +01:00
Andras Bacsai
f78ea5de07 Remove colors Tailwind 2022-03-11 20:47:13 +01:00
Andras Bacsai
5adbd5e784 Merge pull request #210 from coollabsio/v2.0.28
v2.0.28
2022-03-04 15:39:35 +01:00
Andras Bacsai
5b2afa79d7 chore: version++ 2022-03-04 15:20:03 +01:00
Andras Bacsai
dc4e6d02b7 feat: Service secrets 2022-03-04 15:14:25 +01:00
Andras Bacsai
8ae61c8f78 fix: do not error if proxy is not running 2022-03-04 14:20:20 +01:00
Andras Bacsai
684b8e0914 Merge pull request #207 from coollabsio/v2.0.27
v2.0.27
2022-03-02 21:15:32 +01:00
Andras Bacsai
7c3314abae force image deletion 2022-03-02 21:14:53 +01:00
Andras Bacsai
ab9f8ff356 Before latest image 2022-03-02 21:08:36 +01:00
Andras Bacsai
892d8cd5c1 Reload proxy after ssl renewal 2022-03-02 21:04:42 +01:00
Andras Bacsai
8b8b45778d fix: application state in UI 2022-03-02 21:01:25 +01:00
Andras Bacsai
6655fb182c fix: cleanup coolify images 2022-03-02 20:57:28 +01:00
Andras Bacsai
0926d40247 fix: Reload haproxy if new cert is added 2022-03-02 20:43:16 +01:00
Andras Bacsai
ddc4d36688 Fix cleanup process of old coolify images 2022-03-02 20:32:29 +01:00
Andras Bacsai
53e1f22eb1 fix: check when a container is running 2022-03-02 20:32:18 +01:00
Andras Bacsai
3d2a34737b prevent restarting container check 2022-03-02 15:52:22 +01:00
Andras Bacsai
ebde77008c Cleanup coolify image fix 2022-03-02 15:52:06 +01:00
Andras Bacsai
3d27fd04ba cleanup old images of coolify 2022-03-02 14:53:43 +01:00
Andras Bacsai
d9fcaf3473 update packages 2022-03-02 14:53:33 +01:00
Andras Bacsai
d266f761aa Stats open with auth 2022-03-02 14:38:51 +01:00
Andras Bacsai
1d01405412 chore: Version++ 2022-03-02 13:38:58 +01:00
Andras Bacsai
7c62eb5bd6 feat: Send version with update request 2022-03-02 13:38:45 +01:00
Andras Bacsai
4dcc76d366 fix: Update process 2022-03-02 13:37:06 +01:00
Andras Bacsai
d2fad19a11 Update package.json 2022-03-02 12:23:40 +01:00
Andras Bacsai
7c92c4c964 Merge pull request #206 from coollabsio/feat/languagetool
v2.0.26
2022-03-02 12:22:44 +01:00
Andras Bacsai
5a71d33236 chore: Version++ 2022-03-02 12:20:20 +01:00
Andras Bacsai
1b4db4f793 fix 2022-03-02 12:20:02 +01:00
Andras Bacsai
c084b22815 fix: volume name 2022-03-02 12:17:48 +01:00
Andras Bacsai
acacef95cd fix: reload proxy on ssl cert 2022-03-02 12:10:12 +01:00
Andras Bacsai
5d722183d3 feat: Languagetool service 2022-03-02 11:57:03 +01:00
Andras Bacsai
ac19ea5407 Merge pull request #202 from coollabsio/fix
v2.0.24
2022-03-02 11:18:51 +01:00
Andras Bacsai
d19b05b970 fix: update process 2022-03-02 11:00:08 +01:00
Andras Bacsai
a0795136ac fix 2022-03-01 15:30:39 +01:00
Andras Bacsai
d2566e345a fix 2022-03-01 15:27:33 +01:00
Andras Bacsai
66cd7cf90e remove debug logging 2022-03-01 15:27:06 +01:00
Andras Bacsai
9a599981ef fix 2022-03-01 15:25:13 +01:00
Andras Bacsai
f51f7bc82a fix 2022-03-01 15:22:11 +01:00
Andras Bacsai
dbcbac0137 cleanup 2022-03-01 14:29:15 +01:00
Andras Bacsai
e722f8a87c fix: Reconfigure proxy on restart 2022-03-01 14:02:46 +01:00
Andras Bacsai
61679749eb fix: null proxyhash on restart 2022-03-01 13:15:23 +01:00
Andras Bacsai
23e12c9c44 fix: ssl + sslrenew 2022-03-01 13:07:34 +01:00
Andras Bacsai
6da78cd3e5 remove proxy refresh 2022-03-01 11:45:29 +01:00
Andras Bacsai
78ce8100a3 migrate file 2022-03-01 11:19:40 +01:00
Andras Bacsai
76ba338b45 fix 2022-03-01 11:17:34 +01:00
Andras Bacsai
823fe2deb2 refactor 2022-03-01 11:10:10 +01:00
Andras Bacsai
cb90f692f2 WIP proxy 2022-03-01 00:20:28 +01:00
Andras Bacsai
0325343ede save 2022-03-01 00:12:54 +01:00
Andras Bacsai
69d1556a1d WIP better automatic proxy conf 2022-03-01 00:08:54 +01:00
Andras Bacsai
2daa043840 WIP 2022-02-28 16:55:02 +01:00
Andras Bacsai
f340ca9d05 WIP 2022-02-28 16:06:44 +01:00
Andras Bacsai
02abd038fa fix: Better proxy check 2022-02-28 11:20:46 +01:00
Andras Bacsai
b9da68ec28 Merge pull request #199 from coollabsio/fixes
v2.0.23
2022-02-28 10:25:26 +01:00
Andras Bacsai
88b3910d80 fix: Cleanup old images, > 3 days 2022-02-28 10:12:04 +01:00
Andras Bacsai
160412f6e4 fix: Add coolify-image label for build images 2022-02-28 10:09:34 +01:00
Andras Bacsai
59a86b25fc UI: Application start 2022-02-28 10:00:09 +01:00
Andras Bacsai
49e58b39f5 chore: version++ 2022-02-28 09:57:36 +01:00
Andras Bacsai
58e0757bbd fix: Default npm command 2022-02-28 09:50:47 +01:00
Andras Bacsai
5ff4197572 fix: missing fqdn for services 2022-02-28 09:48:24 +01:00
Andras Bacsai
b56e28d27a UI: colorful states 2022-02-28 09:48:12 +01:00
Andras Bacsai
c3d39e1dd4 fix: Be sure .env exists 2022-02-28 09:31:36 +01:00
Andras Bacsai
716aa36bfd Merge pull request #195 from coollabsio/next
v2.0.22-fixes
2022-02-27 12:42:03 +01:00
Andras Bacsai
f01460170e Fix 2022-02-27 12:35:29 +01:00
Andras Bacsai
a414ce282d revert old update sequence 2022-02-27 12:31:39 +01:00
Andras Bacsai
6c32f3b130 fix: update version 2022-02-27 12:22:04 +01:00
Andras Bacsai
4cf907c572 fix: do not remove coolify proxy 2022-02-27 12:17:40 +01:00
Andras Bacsai
b28baaa5aa Merge pull request #193 from coollabsio/next
v2.0.22
2022-02-27 11:55:56 +01:00
Andras Bacsai
980dea64e0 fix: Fix proxy every 10 secs 2022-02-27 11:52:05 +01:00
Andras Bacsai
c340f6436f chore: Version++ 2022-02-27 11:19:03 +01:00
Andras Bacsai
54376fd105 remove remote docker for now 2022-02-27 11:14:46 +01:00
Andras Bacsai
ef006578b2 fix: Add icons for eleventy + astro 2022-02-26 22:15:22 +01:00
Andras Bacsai
b0b1ee0c60 fix: Always use a buildpack 2022-02-26 22:01:44 +01:00
Andras Bacsai
4e2026aa2d fix: Remove wrong/stuck proxy configurations 2022-02-26 22:01:24 +01:00
Andras Bacsai
e0e50b4bd5 WIP: Remote docker engine 2022-02-26 15:08:26 +01:00
Andras Bacsai
c9b52f1310 fix: Coolify image pulls 2022-02-24 10:37:37 +01:00
Andras Bacsai
0195213dfb Merge pull request #189 from coollabsio/next
v2.0.21
2022-02-24 10:17:38 +01:00
Andras Bacsai
d6225cbde3 fix: Improvement on image pulls 2022-02-24 10:11:48 +01:00
Andras Bacsai
7b4c194b97 chore: version++ 2022-02-24 09:38:11 +01:00
Andras Bacsai
a5ecff24a3 feat: Registration page 2022-02-24 09:32:34 +01:00
Andras Bacsai
c9c003dc9b fix: Docker scanner 2022-02-24 08:50:47 +01:00
Andras Bacsai
fd95936219 feat: 11ty buildpack 2022-02-24 00:30:33 +01:00
Andras Bacsai
15a3fd4456 feat: Astro buildpack 2022-02-23 22:07:06 +01:00
Andras Bacsai
df896542e4 update packages 2022-02-23 15:47:09 +01:00
Andras Bacsai
8927e81274 changes for demo 2022-02-23 13:38:43 +01:00
Andras Bacsai
340f061827 fix: http for demo, oops 2022-02-23 13:32:43 +01:00
Andras Bacsai
15cbac97c2 feat: Random domain for services 2022-02-23 13:25:57 +01:00
Andras Bacsai
bb32d0f7d1 feat: Random subdomain for demo 2022-02-23 13:20:17 +01:00
Andras Bacsai
c370fba9ba Merge pull request #185 from coollabsio/next
v2.0.20
2022-02-23 13:00:08 +01:00
Andras Bacsai
6e32421172 chore: Version++ 2022-02-23 12:58:46 +01:00
Andras Bacsai
6643687c0a fix: Revert default network 2022-02-23 12:58:32 +01:00
Andras Bacsai
ed01e78d77 improvement: dns check 2022-02-23 12:43:04 +01:00
Andras Bacsai
93aed52f88 Login page description for demo page 2022-02-23 11:14:04 +01:00
Andras Bacsai
bb6d1fd6a3 cleanup 2022-02-23 10:40:34 +01:00
Andras Bacsai
6e33179fc2 Update README.md 2022-02-23 10:40:21 +01:00
Andras Bacsai
277fd167cf Merge pull request #184 from coollabsio/next
v2.0.19
2022-02-23 10:29:10 +01:00
Andras Bacsai
98e8d5170b fix: Settings fqdn grr 2022-02-23 10:26:29 +01:00
Andras Bacsai
11ee1651ae fix: Random network name for demo 2022-02-23 10:22:25 +01:00
Andras Bacsai
0dfcf9b1e6 Merge pull request #159 from coollabsio/next
v2.0.18
2022-02-22 20:41:53 +01:00
Andras Bacsai
08f57ac5bc UI fix 2022-02-22 20:41:07 +01:00
Andras Bacsai
7095e781e9 small fixes 2022-02-22 20:37:11 +01:00
Andras Bacsai
df18b93809 Design day! 2022-02-22 12:56:58 +01:00
Andras Bacsai
0c2e028b38 Frontend for port range 2022-02-22 10:35:39 +01:00
Andras Bacsai
80cb1bc129 fix: Use normal docker-compose in dev 2022-02-22 09:54:23 +01:00
Andras Bacsai
74c1cb51f6 nothing here 2022-02-22 09:49:17 +01:00
Andras Bacsai
2e864bddf9 nothing important 2022-02-22 09:47:21 +01:00
Andras Bacsai
e60ae91b5d design: make copy/password visible 2022-02-22 09:45:00 +01:00
Andras Bacsai
d606cd86a0 feat: Ports range 2022-02-22 09:23:41 +01:00
Andras Bacsai
bc463c37f4 fix: Lowercase email everywhere 2022-02-22 08:12:45 +01:00
Andras Bacsai
76c1480903 fix: Email is lowercased in login 2022-02-22 08:10:33 +01:00
Andras Bacsai
6f312caf8b chore: Version++ 2022-02-21 12:46:37 +01:00
Andras Bacsai
980d8d374f Merge branch 'main' into next 2022-02-21 12:46:08 +01:00
Andras Bacsai
c49b34942f Merge pull request #164 from coollabsio/fix/github-token
v2.0.17
2022-02-21 12:41:36 +01:00
Andras Bacsai
fcfa8717a5 fix: Move tokens from session to cookie/store 2022-02-21 12:35:20 +01:00
Andras Bacsai
954a265965 chore: Version++ 2022-02-21 09:52:51 +01:00
Andras Bacsai
69845a020a Merge branch 'main' into fix/github-token 2022-02-21 09:51:46 +01:00
Andras Bacsai
22200fd8a7 fix: Github token 2022-02-21 09:50:15 +01:00
Andras Bacsai
add441675d feat: Public port range (WIP) 2022-02-20 15:12:01 +01:00
Andras Bacsai
d3d9754277 chore: Version ++ 2022-02-20 14:42:24 +01:00
Andras Bacsai
aa5e2edbc5 feat: Scan for lock files and set right commands 2022-02-20 14:40:15 +01:00
Andras Bacsai
310b099ecf Merge pull request #154 from coollabsio/next
v2.0.16
2022-02-20 00:48:08 +01:00
Andras Bacsai
1cfaef911c Small fixes 2022-02-20 00:17:44 +01:00
Andras Bacsai
b931c5f638 Migration file 2022-02-20 00:13:16 +01:00
Andras Bacsai
7c683668eb feat: Secrets for previews
UI: Some CSS changes
2022-02-20 00:00:31 +01:00
Andras Bacsai
cab7ac7d58 fix: If DNS not found, do not redirect 2022-02-19 22:37:45 +01:00
Andras Bacsai
15e69c538a feat: Preview secrets
chore: version++
2022-02-19 14:54:47 +01:00
Andras Bacsai
31ee938b66 Merge pull request #152 from coollabsio/next
v2.0.15
2022-02-19 14:52:35 +01:00
Andras Bacsai
e51a8d43d9 Browser 2022-02-19 14:03:33 +01:00
Andras Bacsai
64cd5b6e4b fix: Gitlab webhooks fixed 2022-02-19 13:57:56 +01:00
Andras Bacsai
6c9ef34905 chore: version++ 2022-02-18 23:25:43 +01:00
Andras Bacsai
aa89019236 fix: Database connection strings 2022-02-18 23:25:24 +01:00
Andras Bacsai
df58fcee16 Merge pull request #148 from coollabsio/next
v2.0.14
2022-02-18 22:18:42 +01:00
Andras Bacsai
ea3ffc429f fix: Plausible volume fixed 2022-02-18 22:05:02 +01:00
Andras Bacsai
2efca7a2b5 feat: Basic password reset form 2022-02-18 21:37:40 +01:00
Andras Bacsai
9db448a5e2 fix: follow icon 2022-02-18 15:51:59 +01:00
Andras Bacsai
feee90beef fix: Random port for certbot 2022-02-18 15:37:38 +01:00
Andras Bacsai
906a63b6b5 fix: ghToken in session now 2022-02-18 15:29:32 +01:00
Andras Bacsai
2ce64ac213 fix: hook.ts - relogin needed
updated packages
fix: Lots of typescript thingy
fix: ssl request flow
fix: proxy cleanup flow
2022-02-18 13:59:23 +01:00
Andras Bacsai
4d8bf57135 fix: Remove force SSL when doing let's encrypt request 2022-02-18 09:00:23 +01:00
Andras Bacsai
c5348ce4b3 fix: Minor fixes 2022-02-18 08:48:05 +01:00
Andras Bacsai
7f87c03f97 fix: Running state css 2022-02-18 00:25:15 +01:00
Andras Bacsai
9469f148ff fix: Grr 2022-02-18 00:22:17 +01:00
Andras Bacsai
ffb7dc4ec2 fix: SSL off for services 2022-02-18 00:17:07 +01:00
Andras Bacsai
242b8fa746 fix: Remove SSL with stop 2022-02-18 00:15:46 +01:00
Andras Bacsai
50cae5ac3b fix: Lets encrypt 2022-02-18 00:08:50 +01:00
Andras Bacsai
6a71233eb2 fix: Typo 2022-02-17 23:58:21 +01:00
Andras Bacsai
1aff8933c9 fix: Local docker host 2022-02-17 23:49:25 +01:00
Andras Bacsai
0ed87a5dfc fix: SSL app off 2022-02-17 23:47:37 +01:00
Andras Bacsai
24a6bcbd1e fix: Builder debug logging is better now
fix: www and non-www domain checker
2022-02-17 23:42:23 +01:00
Andras Bacsai
ca7f3da19d fix: Getport for letsencrypt
design: Add more scroll design
2022-02-17 22:56:15 +01:00
Andras Bacsai
bf047e2a3c feat: Dual certificates
desing: Lots of design/css updates
version++
2022-02-17 22:14:06 +01:00
Andras Bacsai
4454287be9 Merge branch 'main' into next 2022-02-17 10:38:47 +01:00
Andras Bacsai
8b20761e8b fix: Buggy svelte-kit-cookie-session 2022-02-17 10:11:46 +01:00
Andras Bacsai
655d0b5d5f fix: Secure cookie disabled by default 2022-02-16 17:13:24 +01:00
Andras Bacsai
91849cdd3a fix www - non-www ssl 2022-02-16 00:00:49 +01:00
Andras Bacsai
df25a694c3 fix: Add no user redis to uri 2022-02-15 23:59:26 +01:00
Andras Bacsai
eabaca145e Fix 2022-02-15 23:38:10 +01:00
Andras Bacsai
2f0e458765 Expand flag for lets encrypt 2022-02-15 23:12:28 +01:00
Andras Bacsai
ff8037f231 feat: Generate www & non-www SSL certs 2022-02-15 23:02:03 +01:00
165 changed files with 5095 additions and 3428 deletions

View File

@@ -1,4 +1,5 @@
FROM node:16.14.0-alpine FROM node:16.14.0-alpine
RUN apk add --no-cache g++ cmake make python3
WORKDIR /app WORKDIR /app
COPY package*.json . COPY package*.json .
RUN yarn install RUN yarn install

View File

@@ -2,6 +2,12 @@
An open-source & self-hostable Heroku / Netlify alternative. An open-source & self-hostable Heroku / Netlify alternative.
## Demo instance
https://demo.coolify.io/
(If it is unresponsible, that means someone overloaded the server. 🙃)
## Installation ## Installation
Installation is automated with the following command: Installation is automated with the following command:

View File

@@ -1,12 +1,12 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.0.13", "version": "2.0.31",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"dev": "docker compose -f docker-compose-dev.yaml up -d && NODE_ENV=development svelte-kit dev --host 0.0.0.0", "dev": "docker-compose -f docker-compose-dev.yaml up -d && NODE_ENV=development svelte-kit dev --host 0.0.0.0",
"dev:stop": "docker compose -f docker-compose-dev.yaml down", "dev:stop": "docker-compose -f docker-compose-dev.yaml down",
"dev:logs": "docker compose -f docker-compose-dev.yaml logs -f --tail 10", "dev:logs": "docker-compose -f docker-compose-dev.yaml logs -f --tail 10",
"studio": "npx prisma studio", "studio": "npx prisma studio",
"start": "npx prisma migrate deploy && npx prisma generate && npx prisma db seed && node index.js", "start": "npx prisma migrate deploy && npx prisma generate && npx prisma db seed && node index.js",
"build": "svelte-kit build", "build": "svelte-kit build",
@@ -25,58 +25,61 @@
"prepare": "husky install" "prepare": "husky install"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-node": "1.0.0-next.67", "@sveltejs/adapter-node": "1.0.0-next.70",
"@sveltejs/adapter-static": "1.0.0-next.27", "@sveltejs/adapter-static": "1.0.0-next.28",
"@sveltejs/kit": "1.0.0-next.259", "@sveltejs/kit": "1.0.0-next.288",
"@types/bcrypt": "5.0.0", "@types/bcrypt": "5.0.0",
"@types/js-cookie": "3.0.1", "@types/js-cookie": "3.0.1",
"@types/node": "17.0.18", "@types/node": "17.0.21",
"@types/node-forge": "1.0.0", "@types/node-forge": "1.0.0",
"@typescript-eslint/eslint-plugin": "4.31.1", "@typescript-eslint/eslint-plugin": "4.31.1",
"@typescript-eslint/parser": "4.31.1", "@typescript-eslint/parser": "4.31.1",
"@zerodevx/svelte-toast": "0.6.3", "@zerodevx/svelte-toast": "0.7.0",
"autoprefixer": "10.4.2", "autoprefixer": "10.4.2",
"cross-var": "1.1.0", "cross-var": "1.1.0",
"eslint": "7.32.0", "eslint": "7.32.0",
"eslint-config-prettier": "8.3.0", "eslint-config-prettier": "8.4.0",
"eslint-plugin-svelte3": "3.2.1", "eslint-plugin-svelte3": "3.4.1",
"husky": "7.0.4", "husky": "7.0.4",
"lint-staged": "12.3.4", "lint-staged": "12.3.4",
"postcss": "8.4.6", "postcss": "8.4.7",
"prettier": "2.5.1", "prettier": "2.5.1",
"prettier-plugin-svelte": "2.6.0", "prettier-plugin-svelte": "2.6.0",
"prettier-plugin-tailwindcss": "0.1.7", "prettier-plugin-tailwindcss": "0.1.8",
"prisma": "3.9.2", "prisma": "3.10.0",
"svelte": "3.46.4", "svelte": "3.46.4",
"svelte-check": "2.4.3", "svelte-check": "2.4.5",
"svelte-preprocess": "4.10.3", "svelte-preprocess": "4.10.4",
"tailwindcss": "3.0.22", "svelte-select": "^4.4.7",
"ts-node": "10.5.0", "tailwindcss": "3.0.23",
"ts-node": "10.6.0",
"tslib": "2.3.1", "tslib": "2.3.1",
"typescript": "4.5.5" "typescript": "4.6.2"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@prisma/client": "3.9.2", "@prisma/client": "3.10.0",
"@sentry/node": "6.17.8", "@sentry/node": "6.18.1",
"bcrypt": "5.0.1", "bcrypt": "5.0.1",
"bullmq": "1.72.0", "bullmq": "1.76.0",
"compare-versions": "4.1.3", "compare-versions": "4.1.3",
"cookie": "0.4.2", "cookie": "0.4.2",
"cuid": "2.1.8", "cuid": "2.1.8",
"dayjs": "1.10.7", "dayjs": "1.10.8",
"dockerode": "3.3.1", "dockerode": "3.3.1",
"dotenv-extended": "2.9.0", "dotenv-extended": "2.9.0",
"generate-password": "1.7.0", "generate-password": "1.7.0",
"get-port": "6.0.0", "get-port": "6.1.2",
"got": "12.0.1", "got": "12.0.1",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
"mustache": "^4.2.0",
"node-forge": "1.2.1", "node-forge": "1.2.1",
"svelte-kit-cookie-session": "2.0.2", "svelte-kit-cookie-session": "2.1.2",
"unique-names-generator": "4.6.0" "tailwindcss-scrollbar": "^0.1.0",
"unique-names-generator": "4.7.1"
}, },
"prisma": { "prisma": {
"seed": "node prisma/seed.cjs" "seed": "node prisma/seed.cjs"

376
pnpm-lock.yaml generated
View File

@@ -2,107 +2,113 @@ lockfileVersion: 5.3
specifiers: specifiers:
'@iarna/toml': 2.2.5 '@iarna/toml': 2.2.5
'@prisma/client': 3.9.2 '@prisma/client': 3.10.0
'@sentry/node': 6.17.8 '@sentry/node': 6.18.1
'@sveltejs/adapter-node': 1.0.0-next.67 '@sveltejs/adapter-node': 1.0.0-next.70
'@sveltejs/adapter-static': 1.0.0-next.27 '@sveltejs/adapter-static': 1.0.0-next.28
'@sveltejs/kit': 1.0.0-next.259 '@sveltejs/kit': 1.0.0-next.288
'@types/bcrypt': 5.0.0 '@types/bcrypt': 5.0.0
'@types/js-cookie': 3.0.1 '@types/js-cookie': 3.0.1
'@types/node': 17.0.18 '@types/node': 17.0.21
'@types/node-forge': 1.0.0 '@types/node-forge': 1.0.0
'@typescript-eslint/eslint-plugin': 4.31.1 '@typescript-eslint/eslint-plugin': 4.31.1
'@typescript-eslint/parser': 4.31.1 '@typescript-eslint/parser': 4.31.1
'@zerodevx/svelte-toast': 0.6.3 '@zerodevx/svelte-toast': 0.7.0
autoprefixer: 10.4.2 autoprefixer: 10.4.2
bcrypt: 5.0.1 bcrypt: 5.0.1
bullmq: 1.72.0 bullmq: 1.76.0
compare-versions: 4.1.3 compare-versions: 4.1.3
cookie: 0.4.2 cookie: 0.4.2
cross-var: 1.1.0 cross-var: 1.1.0
cuid: 2.1.8 cuid: 2.1.8
dayjs: 1.10.7 dayjs: 1.10.8
dockerode: 3.3.1 dockerode: 3.3.1
dotenv-extended: 2.9.0 dotenv-extended: 2.9.0
eslint: 7.32.0 eslint: 7.32.0
eslint-config-prettier: 8.3.0 eslint-config-prettier: 8.4.0
eslint-plugin-svelte3: 3.2.1 eslint-plugin-svelte3: 3.4.1
generate-password: 1.7.0 generate-password: 1.7.0
get-port: 6.0.0 get-port: 6.1.2
got: 12.0.1 got: 12.0.1
husky: 7.0.4 husky: 7.0.4
js-cookie: 3.0.1 js-cookie: 3.0.1
js-yaml: 4.1.0 js-yaml: 4.1.0
jsonwebtoken: 8.5.1 jsonwebtoken: 8.5.1
lint-staged: 12.3.4 lint-staged: 12.3.4
mustache: ^4.2.0
node-forge: 1.2.1 node-forge: 1.2.1
postcss: 8.4.6 postcss: 8.4.7
prettier: 2.5.1 prettier: 2.5.1
prettier-plugin-svelte: 2.6.0 prettier-plugin-svelte: 2.6.0
prettier-plugin-tailwindcss: 0.1.7 prettier-plugin-tailwindcss: 0.1.8
prisma: 3.9.2 prisma: 3.10.0
svelte: 3.46.4 svelte: 3.46.4
svelte-check: 2.4.3 svelte-check: 2.4.5
svelte-kit-cookie-session: 2.0.5 svelte-kit-cookie-session: 2.1.2
svelte-preprocess: 4.10.3 svelte-preprocess: 4.10.4
tailwindcss: 3.0.22 svelte-select: ^4.4.7
ts-node: 10.5.0 tailwindcss: 3.0.23
tailwindcss-scrollbar: ^0.1.0
ts-node: 10.6.0
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.5.5 typescript: 4.6.2
unique-names-generator: 4.6.0 unique-names-generator: 4.7.1
dependencies: dependencies:
'@iarna/toml': 2.2.5 '@iarna/toml': 2.2.5
'@prisma/client': 3.9.2_prisma@3.9.2 '@prisma/client': 3.10.0_prisma@3.10.0
'@sentry/node': 6.17.8 '@sentry/node': 6.18.1
bcrypt: 5.0.1 bcrypt: 5.0.1
bullmq: 1.72.0 bullmq: 1.76.0
compare-versions: 4.1.3 compare-versions: 4.1.3
cookie: 0.4.2 cookie: 0.4.2
cuid: 2.1.8 cuid: 2.1.8
dayjs: 1.10.7 dayjs: 1.10.8
dockerode: 3.3.1 dockerode: 3.3.1
dotenv-extended: 2.9.0 dotenv-extended: 2.9.0
generate-password: 1.7.0 generate-password: 1.7.0
get-port: 6.0.0 get-port: 6.1.2
got: 12.0.1 got: 12.0.1
js-cookie: 3.0.1 js-cookie: 3.0.1
js-yaml: 4.1.0 js-yaml: 4.1.0
jsonwebtoken: 8.5.1 jsonwebtoken: 8.5.1
mustache: 4.2.0
node-forge: 1.2.1 node-forge: 1.2.1
svelte-kit-cookie-session: 2.0.5 svelte-kit-cookie-session: 2.1.2
unique-names-generator: 4.6.0 tailwindcss-scrollbar: 0.1.0_tailwindcss@3.0.23
unique-names-generator: 4.7.1
devDependencies: devDependencies:
'@sveltejs/adapter-node': 1.0.0-next.67 '@sveltejs/adapter-node': 1.0.0-next.70
'@sveltejs/adapter-static': 1.0.0-next.27 '@sveltejs/adapter-static': 1.0.0-next.28
'@sveltejs/kit': 1.0.0-next.259_svelte@3.46.4 '@sveltejs/kit': 1.0.0-next.288_svelte@3.46.4
'@types/bcrypt': 5.0.0 '@types/bcrypt': 5.0.0
'@types/js-cookie': 3.0.1 '@types/js-cookie': 3.0.1
'@types/node': 17.0.18 '@types/node': 17.0.21
'@types/node-forge': 1.0.0 '@types/node-forge': 1.0.0
'@typescript-eslint/eslint-plugin': 4.31.1_5d7752337e5ea49772097d8af1823bf9 '@typescript-eslint/eslint-plugin': 4.31.1_386b67ad67ef29c6a0ccaf3e9b60f945
'@typescript-eslint/parser': 4.31.1_eslint@7.32.0+typescript@4.5.5 '@typescript-eslint/parser': 4.31.1_eslint@7.32.0+typescript@4.6.2
'@zerodevx/svelte-toast': 0.6.3 '@zerodevx/svelte-toast': 0.7.0
autoprefixer: 10.4.2_postcss@8.4.6 autoprefixer: 10.4.2_postcss@8.4.7
cross-var: 1.1.0 cross-var: 1.1.0
eslint: 7.32.0 eslint: 7.32.0
eslint-config-prettier: 8.3.0_eslint@7.32.0 eslint-config-prettier: 8.4.0_eslint@7.32.0
eslint-plugin-svelte3: 3.2.1_eslint@7.32.0+svelte@3.46.4 eslint-plugin-svelte3: 3.4.1_eslint@7.32.0+svelte@3.46.4
husky: 7.0.4 husky: 7.0.4
lint-staged: 12.3.4 lint-staged: 12.3.4
postcss: 8.4.6 postcss: 8.4.7
prettier: 2.5.1 prettier: 2.5.1
prettier-plugin-svelte: 2.6.0_prettier@2.5.1+svelte@3.46.4 prettier-plugin-svelte: 2.6.0_prettier@2.5.1+svelte@3.46.4
prettier-plugin-tailwindcss: 0.1.7_prettier@2.5.1 prettier-plugin-tailwindcss: 0.1.8_prettier@2.5.1
prisma: 3.9.2 prisma: 3.10.0
svelte: 3.46.4 svelte: 3.46.4
svelte-check: 2.4.3_postcss@8.4.6+svelte@3.46.4 svelte-check: 2.4.5_postcss@8.4.7+svelte@3.46.4
svelte-preprocess: 4.10.3_88b359da5cac6d8f6ee1bbb7080a3fa9 svelte-preprocess: 4.10.4_e836cbb8ceb5bfaa7513362dd6308834
tailwindcss: 3.0.22_c940fbabf228b85b1c73d314b43e31f1 svelte-select: 4.4.7
ts-node: 10.5.0_f3bd4037939c2ed2942ba074291f8ef2 tailwindcss: 3.0.23_4b9e11f8e85900587b5e2272c5d4c20c
ts-node: 10.6.0_e79e62fe450383fd2d418267dc75e645
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.5.5 typescript: 4.6.2
packages: packages:
/@babel/code-frame/7.12.11: /@babel/code-frame/7.12.11:
@@ -250,10 +256,10 @@ packages:
fastq: 1.13.0 fastq: 1.13.0
dev: true dev: true
/@prisma/client/3.9.2_prisma@3.9.2: /@prisma/client/3.10.0_prisma@3.10.0:
resolution: resolution:
{ {
integrity: sha512-VlEIYVMyfFZHbVBOlunPl47gmP/Z0zzPjPj8I7uKEIaABqrUy50ru3XS0aZd8GFvevVwt7p91xxkUjNjrWhKAQ== integrity: sha512-6P4sV7WFuODSfSoSEzCH1qfmWMrCUBk1LIIuTbQf6m1LI/IOpLN4lnqGDmgiBGprEzuWobnGLfe9YsXLn0inrg==
} }
engines: { node: '>=12.6' } engines: { node: '>=12.6' }
requiresBuild: true requiresBuild: true
@@ -263,21 +269,21 @@ packages:
prisma: prisma:
optional: true optional: true
dependencies: dependencies:
'@prisma/engines-version': 3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009 '@prisma/engines-version': 3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86
prisma: 3.9.2 prisma: 3.10.0
dev: false dev: false
/@prisma/engines-version/3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009: /@prisma/engines-version/3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86:
resolution: resolution:
{ {
integrity: sha512-5Dh+qTDhpPR66w6NNAnPs+/W/Qt4r1DSd+qhfPFcDThUK4uxoZKGlPb2IYQn5LL+18aIGnmteDf7BnVMmvBNSQ== integrity: sha512-cVYs5gyQH/qyut24hUvDznCfPrWiNMKNfPb9WmEoiU6ihlkscIbCfkmuKTtspVLWRdl0LqjYEC7vfnPv17HWhw==
} }
dev: false dev: false
/@prisma/engines/3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009: /@prisma/engines/3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86:
resolution: resolution:
{ {
integrity: sha512-qM+uJbkelB21bnK44gYE049YTHIjHysOuj0mj5U2gDGyNLfmiazlggzFPCgEjgme4U5YB2tYs6Z5Hq08Kl8pjA== integrity: sha512-LjRssaWu9w2SrXitofnutRIyURI7l0veQYIALz7uY4shygM9nMcK3omXcObRm7TAcw3Z+9ytfK1B+ySOsOesxQ==
} }
requiresBuild: true requiresBuild: true
dev: true dev: true
@@ -293,56 +299,56 @@ packages:
picomatch: 2.3.0 picomatch: 2.3.0
dev: true dev: true
/@sentry/core/6.17.8: /@sentry/core/6.18.1:
resolution: resolution:
{ {
integrity: sha512-4WTjgQom75Rvgn6XYy6e7vMIbWlj8utau1wWvr7kjqFKuuuuycRvPgVzAdVr4B3WDHHCInAZpUchsOLs2qwIEA== integrity: sha512-9V8Q+3Asi+3RL67CSIMMZ9mjMsu2/hrpQszYStX7hPPpAZIlAKk2MT5B+na/r80iWKhy+3Ts6aDFF218QtnsVw==
} }
engines: { node: '>=6' } engines: { node: '>=6' }
dependencies: dependencies:
'@sentry/hub': 6.17.8 '@sentry/hub': 6.18.1
'@sentry/minimal': 6.17.8 '@sentry/minimal': 6.18.1
'@sentry/types': 6.17.8 '@sentry/types': 6.18.1
'@sentry/utils': 6.17.8 '@sentry/utils': 6.18.1
tslib: 1.14.1 tslib: 1.14.1
dev: false dev: false
/@sentry/hub/6.17.8: /@sentry/hub/6.18.1:
resolution: resolution:
{ {
integrity: sha512-GW0XYpkoQu/kSJaTLfsF4extHDOBPNRnT0qKr/YO20Z5wGxYp8LsdnAuU3njcFHcAV2F/QDTj2BPq1U385/4+A== integrity: sha512-+zGzgc/xX3an/nKA3ELMn9YD9VmqbNaNwWZ5/SjNUvzsYHh2UNZ7YzT8WawQsRVOXLljyCKxkWpFB4EchiYGbw==
} }
engines: { node: '>=6' } engines: { node: '>=6' }
dependencies: dependencies:
'@sentry/types': 6.17.8 '@sentry/types': 6.18.1
'@sentry/utils': 6.17.8 '@sentry/utils': 6.18.1
tslib: 1.14.1 tslib: 1.14.1
dev: false dev: false
/@sentry/minimal/6.17.8: /@sentry/minimal/6.18.1:
resolution: resolution:
{ {
integrity: sha512-VJXFZBO/O8SViK0fdzodxpNr+pbpgczNgLpz/MNuSooV6EBesgCMVjXtxDUp1Ie1odc0GUprN/ZMLYBmYdIrKQ== integrity: sha512-dm+0MuasWNi/LASvHX+09oCo8IBZY5WpMK8qXvQMnwQ9FVfklrjcfEI3666WORDCmeUhDCSeL2MbjPDm+AmPLg==
} }
engines: { node: '>=6' } engines: { node: '>=6' }
dependencies: dependencies:
'@sentry/hub': 6.17.8 '@sentry/hub': 6.18.1
'@sentry/types': 6.17.8 '@sentry/types': 6.18.1
tslib: 1.14.1 tslib: 1.14.1
dev: false dev: false
/@sentry/node/6.17.8: /@sentry/node/6.18.1:
resolution: resolution:
{ {
integrity: sha512-b3zg1XjKtxp7o821ENORO1CCzMM4QzKP01rzztMwyMcj28dmUq36QXoQAnwdKn7jEYkJdLnMeniIBR6U6NUJrQ== integrity: sha512-aTb2gwfZUq0lGDRGH5zNOYDfFMOQZu6E0QcAsvH2ZBcEj3rUWZz3r25COFrHmfzHLUV1KcF2AmnWo1QU1jmm0g==
} }
engines: { node: '>=6' } engines: { node: '>=6' }
dependencies: dependencies:
'@sentry/core': 6.17.8 '@sentry/core': 6.18.1
'@sentry/hub': 6.17.8 '@sentry/hub': 6.18.1
'@sentry/tracing': 6.17.8 '@sentry/tracing': 6.18.1
'@sentry/types': 6.17.8 '@sentry/types': 6.18.1
'@sentry/utils': 6.17.8 '@sentry/utils': 6.18.1
cookie: 0.4.2 cookie: 0.4.2
https-proxy-agent: 5.0.0 https-proxy-agent: 5.0.0
lru_map: 0.3.3 lru_map: 0.3.3
@@ -351,36 +357,36 @@ packages:
- supports-color - supports-color
dev: false dev: false
/@sentry/tracing/6.17.8: /@sentry/tracing/6.18.1:
resolution: resolution:
{ {
integrity: sha512-WJ3W8O6iPI3w7MrzTnYcw3s5PGBNFqT4b9oBCl5Ndjexs8DsGlQOxjrsipo36z6TpnRHpAE4FEbOETb2R8JRJQ== integrity: sha512-OxozmSfxGx246Ae1XhO01I7ZWxO3briwMBh55E5KyjQb8fuS9gVE7Uy8ZRs5hhNjDutFAU7nMtC0zipfVxP6fg==
} }
engines: { node: '>=6' } engines: { node: '>=6' }
dependencies: dependencies:
'@sentry/hub': 6.17.8 '@sentry/hub': 6.18.1
'@sentry/minimal': 6.17.8 '@sentry/minimal': 6.18.1
'@sentry/types': 6.17.8 '@sentry/types': 6.18.1
'@sentry/utils': 6.17.8 '@sentry/utils': 6.18.1
tslib: 1.14.1 tslib: 1.14.1
dev: false dev: false
/@sentry/types/6.17.8: /@sentry/types/6.18.1:
resolution: resolution:
{ {
integrity: sha512-0i0f+dpvV62Pm5QMVBHNfEsTGIXoXRGQbeN2LGL4XbhzrzUmIrBPzrnZHv9c/JYtSJnI6A0B9OG7Bdlh3aku+Q== integrity: sha512-wp741NoBKnXE/4T9L723sWJ8EcNMxeTIT1smgNJOfbPwrsDICoYmGEt6JFa05XHpWBGI66WuNvnDjoHVeh6zhA==
} }
engines: { node: '>=6' } engines: { node: '>=6' }
dev: false dev: false
/@sentry/utils/6.17.8: /@sentry/utils/6.18.1:
resolution: resolution:
{ {
integrity: sha512-cAOM53A5FHv95hpDuXKJU8rI4B1XdZ6qe3Yo+/nDS9QDpOgzvyjcItgXPvKW1wUjdHCcnwu7VBfBxB7teYOW9g== integrity: sha512-IFZmuvA+c5lDGlZEri13JSyUP0BHelzY0S4dcKxAzskPW+BtBdQDgYGV90iED1y+IRMLawWb34GF7HyJSouN1Q==
} }
engines: { node: '>=6' } engines: { node: '>=6' }
dependencies: dependencies:
'@sentry/types': 6.17.8 '@sentry/types': 6.18.1
tslib: 1.14.1 tslib: 1.14.1
dev: false dev: false
@@ -392,28 +398,28 @@ packages:
engines: { node: '>=10' } engines: { node: '>=10' }
dev: false dev: false
/@sveltejs/adapter-node/1.0.0-next.67: /@sveltejs/adapter-node/1.0.0-next.70:
resolution: resolution:
{ {
integrity: sha512-+LuLn91xARZsRANiQNIIDpMMncUTnP2pJc8tyL+FdpVvs5UtlvkYJpeCBPFqjjseRpIIbi8Slu89GCdrRXBDUg== integrity: sha512-9EtqSTxVOipESyPQH8uVdkl+iSqsq3oRKS3RZoE2vAG9I849MuTTxr1g9L4kALbXaklsYRztwhmrLpTdcSM4vA==
} }
dependencies: dependencies:
tiny-glob: 0.2.9 tiny-glob: 0.2.9
dev: true dev: true
/@sveltejs/adapter-static/1.0.0-next.27: /@sveltejs/adapter-static/1.0.0-next.28:
resolution: resolution:
{ {
integrity: sha512-dcN1p1D7ZY/a9SClfN14mgm9pyWbLxdwM9gzPMZG6xXOoqMtwI03aZOFgGGumHPdv+XcGRZM96vUSRoDm6vBJQ== integrity: sha512-c4xLyeSwnbGQxe4f1SLpHTbxZDm3TEr43scR3tOlVgQN+mnAL9aDdl3nTtdzWmrUDmDEmY4GriAwLyFLZuINLw==
} }
dependencies: dependencies:
tiny-glob: 0.2.9 tiny-glob: 0.2.9
dev: true dev: true
/@sveltejs/kit/1.0.0-next.259_svelte@3.46.4: /@sveltejs/kit/1.0.0-next.288_svelte@3.46.4:
resolution: resolution:
{ {
integrity: sha512-+Tss6cQXmpi4Jno/ZP0zJ3INBLMED+WeW4UI81tmexheC76Y2p+cbInneKO/REx/8QFo1iroYrWAUkZPsOg8Ew== integrity: sha512-6ky4CUFNGKoU1QV+fY2LOIB7atHdUCIMRx3pX21B2g9yZzQzkot0zHBrMMBSKF0/4Wx19PLEWrlGxlhakYmi8Q==
} }
engines: { node: '>=14.13' } engines: { node: '>=14.13' }
hasBin: true hasBin: true
@@ -502,7 +508,7 @@ packages:
integrity: sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw== integrity: sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==
} }
dependencies: dependencies:
'@types/node': 17.0.18 '@types/node': 17.0.21
dev: true dev: true
/@types/cacheable-request/6.0.2: /@types/cacheable-request/6.0.2:
@@ -513,7 +519,7 @@ packages:
dependencies: dependencies:
'@types/http-cache-semantics': 4.0.1 '@types/http-cache-semantics': 4.0.1
'@types/keyv': 3.1.3 '@types/keyv': 3.1.3
'@types/node': 17.0.18 '@types/node': 17.0.21
'@types/responselike': 1.0.0 '@types/responselike': 1.0.0
dev: false dev: false
@@ -544,7 +550,7 @@ packages:
integrity: sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg== integrity: sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg==
} }
dependencies: dependencies:
'@types/node': 17.0.18 '@types/node': 17.0.21
dev: false dev: false
/@types/node-forge/1.0.0: /@types/node-forge/1.0.0:
@@ -553,13 +559,13 @@ packages:
integrity: sha512-h0bgwPKq5u99T9Gor4qtV1lCZ41xNkai0pie1n/a2mh2/4+jENWOlo7AJ4YKxTZAnSZ8FRurUpdIN7ohaPPuHA== integrity: sha512-h0bgwPKq5u99T9Gor4qtV1lCZ41xNkai0pie1n/a2mh2/4+jENWOlo7AJ4YKxTZAnSZ8FRurUpdIN7ohaPPuHA==
} }
dependencies: dependencies:
'@types/node': 17.0.18 '@types/node': 17.0.21
dev: true dev: true
/@types/node/17.0.18: /@types/node/17.0.21:
resolution: resolution:
{ {
integrity: sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA== integrity: sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==
} }
/@types/parse-json/4.0.0: /@types/parse-json/4.0.0:
@@ -582,7 +588,7 @@ packages:
integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==
} }
dependencies: dependencies:
'@types/node': 17.0.18 '@types/node': 17.0.21
dev: false dev: false
/@types/sass/1.16.1: /@types/sass/1.16.1:
@@ -591,10 +597,10 @@ packages:
integrity: sha512-iZUcRrGuz/Tbg3loODpW7vrQJkUtpY2fFSf4ELqqkApcS2TkZ1msk7ie8iZPB86lDOP8QOTTmuvWjc5S0R9OjQ== integrity: sha512-iZUcRrGuz/Tbg3loODpW7vrQJkUtpY2fFSf4ELqqkApcS2TkZ1msk7ie8iZPB86lDOP8QOTTmuvWjc5S0R9OjQ==
} }
dependencies: dependencies:
'@types/node': 17.0.18 '@types/node': 17.0.21
dev: true dev: true
/@typescript-eslint/eslint-plugin/4.31.1_5d7752337e5ea49772097d8af1823bf9: /@typescript-eslint/eslint-plugin/4.31.1_386b67ad67ef29c6a0ccaf3e9b60f945:
resolution: resolution:
{ {
integrity: sha512-UDqhWmd5i0TvPLmbK5xY3UZB0zEGseF+DHPghZ37Sb83Qd3p8ujhvAtkU4OF46Ka5Pm5kWvFIx0cCTBFKo0alA== integrity: sha512-UDqhWmd5i0TvPLmbK5xY3UZB0zEGseF+DHPghZ37Sb83Qd3p8ujhvAtkU4OF46Ka5Pm5kWvFIx0cCTBFKo0alA==
@@ -608,21 +614,21 @@ packages:
typescript: typescript:
optional: true optional: true
dependencies: dependencies:
'@typescript-eslint/experimental-utils': 4.31.1_eslint@7.32.0+typescript@4.5.5 '@typescript-eslint/experimental-utils': 4.31.1_eslint@7.32.0+typescript@4.6.2
'@typescript-eslint/parser': 4.31.1_eslint@7.32.0+typescript@4.5.5 '@typescript-eslint/parser': 4.31.1_eslint@7.32.0+typescript@4.6.2
'@typescript-eslint/scope-manager': 4.31.1 '@typescript-eslint/scope-manager': 4.31.1
debug: 4.3.3 debug: 4.3.3
eslint: 7.32.0 eslint: 7.32.0
functional-red-black-tree: 1.0.1 functional-red-black-tree: 1.0.1
regexpp: 3.2.0 regexpp: 3.2.0
semver: 7.3.5 semver: 7.3.5
tsutils: 3.21.0_typescript@4.5.5 tsutils: 3.21.0_typescript@4.6.2
typescript: 4.5.5 typescript: 4.6.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true dev: true
/@typescript-eslint/experimental-utils/4.31.1_eslint@7.32.0+typescript@4.5.5: /@typescript-eslint/experimental-utils/4.31.1_eslint@7.32.0+typescript@4.6.2:
resolution: resolution:
{ {
integrity: sha512-NtoPsqmcSsWty0mcL5nTZXMf7Ei0Xr2MT8jWjXMVgRK0/1qeQ2jZzLFUh4QtyJ4+/lPUyMw5cSfeeME+Zrtp9Q== integrity: sha512-NtoPsqmcSsWty0mcL5nTZXMf7Ei0Xr2MT8jWjXMVgRK0/1qeQ2jZzLFUh4QtyJ4+/lPUyMw5cSfeeME+Zrtp9Q==
@@ -634,7 +640,7 @@ packages:
'@types/json-schema': 7.0.9 '@types/json-schema': 7.0.9
'@typescript-eslint/scope-manager': 4.31.1 '@typescript-eslint/scope-manager': 4.31.1
'@typescript-eslint/types': 4.31.1 '@typescript-eslint/types': 4.31.1
'@typescript-eslint/typescript-estree': 4.31.1_typescript@4.5.5 '@typescript-eslint/typescript-estree': 4.31.1_typescript@4.6.2
eslint: 7.32.0 eslint: 7.32.0
eslint-scope: 5.1.1 eslint-scope: 5.1.1
eslint-utils: 3.0.0_eslint@7.32.0 eslint-utils: 3.0.0_eslint@7.32.0
@@ -643,7 +649,7 @@ packages:
- typescript - typescript
dev: true dev: true
/@typescript-eslint/parser/4.31.1_eslint@7.32.0+typescript@4.5.5: /@typescript-eslint/parser/4.31.1_eslint@7.32.0+typescript@4.6.2:
resolution: resolution:
{ {
integrity: sha512-dnVZDB6FhpIby6yVbHkwTKkn2ypjVIfAR9nh+kYsA/ZL0JlTsd22BiDjouotisY3Irmd3OW1qlk9EI5R8GrvRQ== integrity: sha512-dnVZDB6FhpIby6yVbHkwTKkn2ypjVIfAR9nh+kYsA/ZL0JlTsd22BiDjouotisY3Irmd3OW1qlk9EI5R8GrvRQ==
@@ -658,10 +664,10 @@ packages:
dependencies: dependencies:
'@typescript-eslint/scope-manager': 4.31.1 '@typescript-eslint/scope-manager': 4.31.1
'@typescript-eslint/types': 4.31.1 '@typescript-eslint/types': 4.31.1
'@typescript-eslint/typescript-estree': 4.31.1_typescript@4.5.5 '@typescript-eslint/typescript-estree': 4.31.1_typescript@4.6.2
debug: 4.3.3 debug: 4.3.3
eslint: 7.32.0 eslint: 7.32.0
typescript: 4.5.5 typescript: 4.6.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true dev: true
@@ -685,7 +691,7 @@ packages:
engines: { node: ^8.10.0 || ^10.13.0 || >=11.10.1 } engines: { node: ^8.10.0 || ^10.13.0 || >=11.10.1 }
dev: true dev: true
/@typescript-eslint/typescript-estree/4.31.1_typescript@4.5.5: /@typescript-eslint/typescript-estree/4.31.1_typescript@4.6.2:
resolution: resolution:
{ {
integrity: sha512-EGHkbsUvjFrvRnusk6yFGqrqMBTue5E5ROnS5puj3laGQPasVUgwhrxfcgkdHNFECHAewpvELE1Gjv0XO3mdWg== integrity: sha512-EGHkbsUvjFrvRnusk6yFGqrqMBTue5E5ROnS5puj3laGQPasVUgwhrxfcgkdHNFECHAewpvELE1Gjv0XO3mdWg==
@@ -703,8 +709,8 @@ packages:
globby: 11.0.4 globby: 11.0.4
is-glob: 4.0.3 is-glob: 4.0.3
semver: 7.3.5 semver: 7.3.5
tsutils: 3.21.0_typescript@4.5.5 tsutils: 3.21.0_typescript@4.6.2
typescript: 4.5.5 typescript: 4.6.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true dev: true
@@ -720,10 +726,10 @@ packages:
eslint-visitor-keys: 2.1.0 eslint-visitor-keys: 2.1.0
dev: true dev: true
/@zerodevx/svelte-toast/0.6.3: /@zerodevx/svelte-toast/0.7.0:
resolution: resolution:
{ {
integrity: sha512-k0W1JFoqHIcIQaP9ij99+Rv0ugaQSSNwOuNwwmTGRjWtIqrQr+ExLDE8LQGXLlJIprqDyMWB4lJkUql/r0RAtA== integrity: sha512-Xvpy1dTE/bUcYJKrbBxc5KuXtbwhyUjEzmTNtg92bS558SvsH6FDu02pgFbBWNjK9g9f2eQCDBhBhJM4q6vhDw==
} }
dev: true dev: true
@@ -1006,7 +1012,7 @@ packages:
typpy: 2.3.11 typpy: 2.3.11
dev: false dev: false
/autoprefixer/10.4.2_postcss@8.4.6: /autoprefixer/10.4.2_postcss@8.4.7:
resolution: resolution:
{ {
integrity: sha512-9fOPpHKuDW1w/0EKfRmVnxTDt8166MAnLI3mgZ1JCnhNtYWxcJ6Ud5CO/AVOZi/AvFa8DY9RTy3h3+tFBlrrdQ== integrity: sha512-9fOPpHKuDW1w/0EKfRmVnxTDt8166MAnLI3mgZ1JCnhNtYWxcJ6Ud5CO/AVOZi/AvFa8DY9RTy3h3+tFBlrrdQ==
@@ -1021,7 +1027,7 @@ packages:
fraction.js: 4.1.2 fraction.js: 4.1.2
normalize-range: 0.1.2 normalize-range: 0.1.2
picocolors: 1.0.0 picocolors: 1.0.0
postcss: 8.4.6 postcss: 8.4.7
postcss-value-parser: 4.2.0 postcss-value-parser: 4.2.0
dev: true dev: true
@@ -1746,10 +1752,10 @@ packages:
ieee754: 1.2.1 ieee754: 1.2.1
dev: false dev: false
/bullmq/1.72.0: /bullmq/1.76.0:
resolution: resolution:
{ {
integrity: sha512-Q0pk6GphHyYsacpjZZFhjp/+TY+2g2FDsJS3qwIyskQL4j7vZaa1iYX3gKDEBn4C5eZMP1EOl9GWkm2bhdB0Wg== integrity: sha512-e9No/JiE7B+wnLsoZnVBLTKTfc1An3bnuIaWewhjDImYNFq5aU/YmzoCdJRgUXrRWGc3EnATFSOLHiEY6Z2FpA==
} }
dependencies: dependencies:
cron-parser: 2.18.0 cron-parser: 2.18.0
@@ -2134,10 +2140,10 @@ packages:
} }
dev: false dev: false
/dayjs/1.10.7: /dayjs/1.10.8:
resolution: resolution:
{ {
integrity: sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig== integrity: sha512-wbNwDfBHHur9UOzNUjeKUOJ0fCb0a52Wx0xInmQ7Y8FstyajiV1NmK1e00cxsr9YrE9r7yAChE0VvpuY5Rnlow==
} }
dev: false dev: false
@@ -2704,10 +2710,10 @@ packages:
engines: { node: '>=10' } engines: { node: '>=10' }
dev: true dev: true
/eslint-config-prettier/8.3.0_eslint@7.32.0: /eslint-config-prettier/8.4.0_eslint@7.32.0:
resolution: resolution:
{ {
integrity: sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== integrity: sha512-CFotdUcMY18nGRo5KGsnNxpznzhkopOcOo0InID+sgQssPrzjvsyKZPvOgymTFeHrFuC3Tzdf2YndhXtULK9Iw==
} }
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -2716,10 +2722,10 @@ packages:
eslint: 7.32.0 eslint: 7.32.0
dev: true dev: true
/eslint-plugin-svelte3/3.2.1_eslint@7.32.0+svelte@3.46.4: /eslint-plugin-svelte3/3.4.1_eslint@7.32.0+svelte@3.46.4:
resolution: resolution:
{ {
integrity: sha512-YoBR9mLoKCjGghJ/gvpnFZKaMEu/VRcuxpSRS8KuozuEo7CdBH7bmBHa6FmMm0i4kJnOyx+PVsaptz96K6H/4Q== integrity: sha512-7p59WG8qV8L6wLdl4d/c3mdjkgVglQCdv5XOTk/iNPBKXuuV+Q0eFP5Wa6iJd/G2M1qR3BkLPEzaANOqKAZczw==
} }
engines: { node: '>=10' } engines: { node: '>=10' }
peerDependencies: peerDependencies:
@@ -3114,10 +3120,10 @@ packages:
engines: { node: '>=8' } engines: { node: '>=8' }
dev: false dev: false
/get-port/6.0.0: /get-port/6.1.2:
resolution: resolution:
{ {
integrity: sha512-qSVkVF6Eq1GdL/cBNiFuP4nUHMF7OEMTqEjC6alR2N90u8BFOoO0PFhNTX2QtAUoGrz8NnrSWj85TZ8YXZ6LOA== integrity: sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw==
} }
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
dev: false dev: false
@@ -4089,6 +4095,14 @@ packages:
msgpackr-extract: 1.0.15 msgpackr-extract: 1.0.15
dev: false dev: false
/mustache/4.2.0:
resolution:
{
integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
}
hasBin: true
dev: false
/nan/2.15.0: /nan/2.15.0:
resolution: resolution:
{ {
@@ -4097,10 +4111,10 @@ packages:
dev: false dev: false
optional: true optional: true
/nanoid/3.2.0: /nanoid/3.3.1:
resolution: resolution:
{ {
integrity: sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==
} }
engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 }
hasBin: true hasBin: true
@@ -4370,7 +4384,7 @@ packages:
engines: { node: '>=8.6' } engines: { node: '>=8.6' }
dev: true dev: true
/postcss-js/4.0.0_postcss@8.4.6: /postcss-js/4.0.0_postcss@8.4.7:
resolution: resolution:
{ {
integrity: sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ== integrity: sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==
@@ -4380,10 +4394,10 @@ packages:
postcss: ^8.3.3 postcss: ^8.3.3
dependencies: dependencies:
camelcase-css: 2.0.1 camelcase-css: 2.0.1
postcss: 8.4.6 postcss: 8.4.7
dev: true dev: true
/postcss-load-config/3.1.0_ts-node@10.5.0: /postcss-load-config/3.1.0_ts-node@10.6.0:
resolution: resolution:
{ {
integrity: sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g== integrity: sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g==
@@ -4397,11 +4411,11 @@ packages:
dependencies: dependencies:
import-cwd: 3.0.0 import-cwd: 3.0.0
lilconfig: 2.0.4 lilconfig: 2.0.4
ts-node: 10.5.0_f3bd4037939c2ed2942ba074291f8ef2 ts-node: 10.6.0_e79e62fe450383fd2d418267dc75e645
yaml: 1.10.2 yaml: 1.10.2
dev: true dev: true
/postcss-nested/5.0.6_postcss@8.4.6: /postcss-nested/5.0.6_postcss@8.4.7:
resolution: resolution:
{ {
integrity: sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA== integrity: sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==
@@ -4410,7 +4424,7 @@ packages:
peerDependencies: peerDependencies:
postcss: ^8.2.14 postcss: ^8.2.14
dependencies: dependencies:
postcss: 8.4.6 postcss: 8.4.7
postcss-selector-parser: 6.0.9 postcss-selector-parser: 6.0.9
dev: true dev: true
@@ -4432,14 +4446,14 @@ packages:
} }
dev: true dev: true
/postcss/8.4.6: /postcss/8.4.7:
resolution: resolution:
{ {
integrity: sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA== integrity: sha512-L9Ye3r6hkkCeOETQX6iOaWZgjp3LL6Lpqm6EtgbKrgqGGteRMNb9vzBfRL96YOSu8o7x3MfIH9Mo5cPJFGrW6A==
} }
engines: { node: ^10 || ^12 || >=14 } engines: { node: ^10 || ^12 || >=14 }
dependencies: dependencies:
nanoid: 3.2.0 nanoid: 3.3.1
picocolors: 1.0.0 picocolors: 1.0.0
source-map-js: 1.0.2 source-map-js: 1.0.2
dev: true dev: true
@@ -4465,10 +4479,10 @@ packages:
svelte: 3.46.4 svelte: 3.46.4
dev: true dev: true
/prettier-plugin-tailwindcss/0.1.7_prettier@2.5.1: /prettier-plugin-tailwindcss/0.1.8_prettier@2.5.1:
resolution: resolution:
{ {
integrity: sha512-tmBr45hCLuit2Cz9Pwow0/Jl1bGivYGsfcF29O+3sKcE++ybjz9dfie565S3ZsvAeV8uYer9SRMBWDsHPly2Lg== integrity: sha512-hwarSBCswAXa+kqYtaAkFr3Vop9o04WOyZs0qo3NyvW8L7f1rif61wRyq0+ArmVThOuRBcJF5hjGXYk86cwemg==
} }
engines: { node: '>=12.17.0' } engines: { node: '>=12.17.0' }
peerDependencies: peerDependencies:
@@ -4486,16 +4500,16 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/prisma/3.9.2: /prisma/3.10.0:
resolution: resolution:
{ {
integrity: sha512-i9eK6cexV74OgeWaH3+e6S07kvC9jEZTl6BqtBH398nlCU0tck7mE9dicY6YQd+euvMjjCtY89q4NgmaPnUsSg== integrity: sha512-dAld12vtwdz9Rz01nOjmnXe+vHana5PSog8t0XGgLemKsUVsaupYpr74AHaS3s78SaTS5s2HOghnJF+jn91ZrA==
} }
engines: { node: '>=12.6' } engines: { node: '>=12.6' }
hasBin: true hasBin: true
requiresBuild: true requiresBuild: true
dependencies: dependencies:
'@prisma/engines': 3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009 '@prisma/engines': 3.10.0-50.73e60b76d394f8d37d8ebd1f8918c79029f0db86
dev: true dev: true
/private/0.1.8: /private/0.1.8:
@@ -5160,10 +5174,10 @@ packages:
engines: { node: '>= 0.4' } engines: { node: '>= 0.4' }
dev: true dev: true
/svelte-check/2.4.3_postcss@8.4.6+svelte@3.46.4: /svelte-check/2.4.5_postcss@8.4.7+svelte@3.46.4:
resolution: resolution:
{ {
integrity: sha512-0zJMMgqYHoP7QEG3tfc5DekpHAOqoy4QOL8scWMSdHIpVVDVC0MuYK57nFyj3XVTW8Zfm85FlgnAdQYsVmST2Q== integrity: sha512-nRft8BbG2wcxyCdHDZ7X43xLcvDzua3xLwq6wzHGcAF3ka3Jyhv2rvgq0+SF9NwHLMefp9C2XkM6etzsxK/cMQ==
} }
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -5177,8 +5191,8 @@ packages:
sade: 1.7.4 sade: 1.7.4
source-map: 0.7.3 source-map: 0.7.3
svelte: 3.46.4 svelte: 3.46.4
svelte-preprocess: 4.10.3_88b359da5cac6d8f6ee1bbb7080a3fa9 svelte-preprocess: 4.10.4_e836cbb8ceb5bfaa7513362dd6308834
typescript: 4.5.5 typescript: 4.6.2
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
- coffeescript - coffeescript
@@ -5203,17 +5217,17 @@ packages:
svelte: 3.46.4 svelte: 3.46.4
dev: true dev: true
/svelte-kit-cookie-session/2.0.5: /svelte-kit-cookie-session/2.1.2:
resolution: resolution:
{ {
integrity: sha512-IX1IXtn42UTz/isem1LqH0SAZdCx6Z6Iu2V4Q83V2EScFbXZWfeFY08Azl8ZrPKdIDhSNHBLAAumRjA6TBxCvQ== integrity: sha512-PfxIWDhiyYWu7iKlL0GHpmwDrdFh+rX/WmBzOuvctF25UqngIo9MCiegWBSBLE1RBwNs5UqaIeI8+vligmY07g==
} }
dev: false dev: false
/svelte-preprocess/4.10.3_88b359da5cac6d8f6ee1bbb7080a3fa9: /svelte-preprocess/4.10.4_e836cbb8ceb5bfaa7513362dd6308834:
resolution: resolution:
{ {
integrity: sha512-ttw17lJfb/dx2ZJT9sesaXT5l7mPQ9Apx1H496Kli3Hkk7orIRGpOw6rCPkRNzr6ueVPqb4vzodS5x7sBFhKHw== integrity: sha512-fuwol0N4UoHsNQolLFbMqWivqcJ9N0vfWO9IuPAiX/5okfoGXURyJ6nECbuEIv0nU3M8Xe2I1ONNje2buk7l6A==
} }
engines: { node: '>= 9.11.2' } engines: { node: '>= 9.11.2' }
requiresBuild: true requiresBuild: true
@@ -5258,11 +5272,18 @@ packages:
'@types/sass': 1.16.1 '@types/sass': 1.16.1
detect-indent: 6.1.0 detect-indent: 6.1.0
magic-string: 0.25.7 magic-string: 0.25.7
postcss: 8.4.6 postcss: 8.4.7
sorcery: 0.10.0 sorcery: 0.10.0
strip-indent: 3.0.0 strip-indent: 3.0.0
svelte: 3.46.4 svelte: 3.46.4
typescript: 4.5.5 typescript: 4.6.2
dev: true
/svelte-select/4.4.7:
resolution:
{
integrity: sha512-fIf9Z8rPI6F8naHZ9wjXT0Pv5gLyhdHAFkHFJnCfVVfELE8e82uOoF0xEVQP6Kir+b4Q5yOvNAzZ61WbSU6A0A==
}
dev: true dev: true
/svelte/3.46.4: /svelte/3.46.4:
@@ -5288,10 +5309,21 @@ packages:
strip-ansi: 6.0.1 strip-ansi: 6.0.1
dev: true dev: true
/tailwindcss/3.0.22_c940fbabf228b85b1c73d314b43e31f1: /tailwindcss-scrollbar/0.1.0_tailwindcss@3.0.23:
resolution: resolution:
{ {
integrity: sha512-F8lt74RlNZirnkaSk310+vGQta7c0/hgx7/bqxruM4wS9lp8oqV93lzavajC3VT0Lp4UUtUVIt8ifKcmGzkr0A== integrity: sha512-egipxw4ooQDh94x02XQpPck0P0sfwazwoUGfA9SedPATIuYDR+6qe8d31Gl7YsSMRiOKDkkqfI0kBvEw9lT/Hg==
}
peerDependencies:
tailwindcss: '>= 2.x.x'
dependencies:
tailwindcss: 3.0.23_4b9e11f8e85900587b5e2272c5d4c20c
dev: false
/tailwindcss/3.0.23_4b9e11f8e85900587b5e2272c5d4c20c:
resolution:
{
integrity: sha512-+OZOV9ubyQ6oI2BXEhzw4HrqvgcARY38xv3zKcjnWtMIZstEsXdI9xftd1iB7+RbOnj2HOEzkA0OyB5BaSxPQA==
} }
engines: { node: '>=12.13.0' } engines: { node: '>=12.13.0' }
hasBin: true hasBin: true
@@ -5299,7 +5331,7 @@ packages:
autoprefixer: ^10.0.2 autoprefixer: ^10.0.2
dependencies: dependencies:
arg: 5.0.1 arg: 5.0.1
autoprefixer: 10.4.2_postcss@8.4.6 autoprefixer: 10.4.2_postcss@8.4.7
chalk: 4.1.2 chalk: 4.1.2
chokidar: 3.5.3 chokidar: 3.5.3
color-name: 1.1.4 color-name: 1.1.4
@@ -5312,10 +5344,10 @@ packages:
is-glob: 4.0.3 is-glob: 4.0.3
normalize-path: 3.0.0 normalize-path: 3.0.0
object-hash: 2.2.0 object-hash: 2.2.0
postcss: 8.4.6 postcss: 8.4.7
postcss-js: 4.0.0_postcss@8.4.6 postcss-js: 4.0.0_postcss@8.4.7
postcss-load-config: 3.1.0_ts-node@10.5.0 postcss-load-config: 3.1.0_ts-node@10.6.0
postcss-nested: 5.0.6_postcss@8.4.6 postcss-nested: 5.0.6_postcss@8.4.7
postcss-selector-parser: 6.0.9 postcss-selector-parser: 6.0.9
postcss-value-parser: 4.2.0 postcss-value-parser: 4.2.0
quick-lru: 5.1.1 quick-lru: 5.1.1
@@ -5407,10 +5439,10 @@ packages:
engines: { node: '>=0.10.0' } engines: { node: '>=0.10.0' }
dev: true dev: true
/ts-node/10.5.0_f3bd4037939c2ed2942ba074291f8ef2: /ts-node/10.6.0_e79e62fe450383fd2d418267dc75e645:
resolution: resolution:
{ {
integrity: sha512-6kEJKwVxAJ35W4akuiysfKwKmjkbYxwQMTBaAxo9KKAx/Yd26mPUyhGz3ji+EsJoAgrLqVsYHNuuYwQe22lbtw== integrity: sha512-CJen6+dfOXolxudBQXnVjRVvYTmTWbyz7cn+xq2XTsvnaXbHqr4gXSCNbS2Jj8yTZMuGwUoBESLaOkLascVVvg==
} }
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -5429,14 +5461,14 @@ packages:
'@tsconfig/node12': 1.0.9 '@tsconfig/node12': 1.0.9
'@tsconfig/node14': 1.0.1 '@tsconfig/node14': 1.0.1
'@tsconfig/node16': 1.0.2 '@tsconfig/node16': 1.0.2
'@types/node': 17.0.18 '@types/node': 17.0.21
acorn: 8.5.0 acorn: 8.5.0
acorn-walk: 8.2.0 acorn-walk: 8.2.0
arg: 4.1.3 arg: 4.1.3
create-require: 1.1.1 create-require: 1.1.1
diff: 4.0.2 diff: 4.0.2
make-error: 1.3.6 make-error: 1.3.6
typescript: 4.5.5 typescript: 4.6.2
v8-compile-cache-lib: 3.0.0 v8-compile-cache-lib: 3.0.0
yn: 3.1.1 yn: 3.1.1
dev: true dev: true
@@ -5454,7 +5486,7 @@ packages:
} }
dev: true dev: true
/tsutils/3.21.0_typescript@4.5.5: /tsutils/3.21.0_typescript@4.6.2:
resolution: resolution:
{ {
integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
@@ -5464,7 +5496,7 @@ packages:
typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
dependencies: dependencies:
tslib: 1.14.1 tslib: 1.14.1
typescript: 4.5.5 typescript: 4.6.2
dev: true dev: true
/tweetnacl/0.14.5: /tweetnacl/0.14.5:
@@ -5497,10 +5529,10 @@ packages:
engines: { node: '>=10' } engines: { node: '>=10' }
dev: true dev: true
/typescript/4.5.5: /typescript/4.6.2:
resolution: resolution:
{ {
integrity: sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== integrity: sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==
} }
engines: { node: '>=4.2.0' } engines: { node: '>=4.2.0' }
hasBin: true hasBin: true
@@ -5515,10 +5547,10 @@ packages:
function.name: 1.0.13 function.name: 1.0.13
dev: false dev: false
/unique-names-generator/4.6.0: /unique-names-generator/4.7.1:
resolution: resolution:
{ {
integrity: sha512-m0fke1emBeT96UYn2psPQYwljooDWRTKt9oUZ5vlt88ZFMBGxqwPyLHXwCfkbgdm8jzioCp7oIpo6KdM+fnUlQ== integrity: sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==
} }
engines: { node: '>=8' } engines: { node: '>=8' }
dev: false dev: false
@@ -5577,7 +5609,7 @@ packages:
optional: true optional: true
dependencies: dependencies:
esbuild: 0.14.21 esbuild: 0.14.21
postcss: 8.4.6 postcss: 8.4.7
resolve: 1.22.0 resolve: 1.22.0
rollup: 2.61.1 rollup: 2.61.1
optionalDependencies: optionalDependencies:

View File

@@ -0,0 +1,47 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Setting" (
"id" TEXT NOT NULL PRIMARY KEY,
"fqdn" TEXT,
"isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"proxyPassword" TEXT NOT NULL,
"proxyUser" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Setting" ("createdAt", "fqdn", "id", "isRegistrationEnabled", "proxyPassword", "proxyUser", "updatedAt") SELECT "createdAt", "fqdn", "id", "isRegistrationEnabled", "proxyPassword", "proxyUser", "updatedAt" FROM "Setting";
DROP TABLE "Setting";
ALTER TABLE "new_Setting" RENAME TO "Setting";
CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn");
CREATE TABLE "new_ApplicationSettings" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"debug" BOOLEAN NOT NULL DEFAULT false,
"previews" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ApplicationSettings" ("applicationId", "createdAt", "debug", "id", "previews", "updatedAt") SELECT "applicationId", "createdAt", "debug", "id", "previews", "updatedAt" FROM "ApplicationSettings";
DROP TABLE "ApplicationSettings";
ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings";
CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId");
CREATE TABLE "new_Service" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"fqdn" TEXT,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT,
"version" TEXT,
"destinationDockerId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Service_destinationDockerId_fkey" FOREIGN KEY ("destinationDockerId") REFERENCES "DestinationDocker" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_Service" ("createdAt", "destinationDockerId", "fqdn", "id", "name", "type", "updatedAt", "version") SELECT "createdAt", "destinationDockerId", "fqdn", "id", "name", "type", "updatedAt", "version" FROM "Service";
DROP TABLE "Service";
ALTER TABLE "new_Service" RENAME TO "Service";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,19 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Secret" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"value" TEXT NOT NULL,
"isPRMRSecret" BOOLEAN NOT NULL DEFAULT false,
"isBuildSecret" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"applicationId" TEXT NOT NULL,
CONSTRAINT "Secret_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Secret" ("applicationId", "createdAt", "id", "isBuildSecret", "name", "updatedAt", "value") SELECT "applicationId", "createdAt", "id", "isBuildSecret", "name", "updatedAt", "value" FROM "Secret";
DROP TABLE "Secret";
ALTER TABLE "new_Secret" RENAME TO "Secret";
CREATE UNIQUE INDEX "Secret_name_applicationId_isPRMRSecret_key" ON "Secret"("name", "applicationId", "isPRMRSecret");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,20 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Setting" (
"id" TEXT NOT NULL PRIMARY KEY,
"fqdn" TEXT,
"isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"minPort" INTEGER NOT NULL DEFAULT 9000,
"maxPort" INTEGER NOT NULL DEFAULT 9100,
"proxyPassword" TEXT NOT NULL,
"proxyUser" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Setting" ("createdAt", "dualCerts", "fqdn", "id", "isRegistrationEnabled", "proxyPassword", "proxyUser", "updatedAt") SELECT "createdAt", "dualCerts", "fqdn", "id", "isRegistrationEnabled", "proxyPassword", "proxyUser", "updatedAt" FROM "Setting";
DROP TABLE "Setting";
ALTER TABLE "new_Setting" RENAME TO "Setting";
CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Setting" ADD COLUMN "proxyHash" TEXT;

View File

@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "ServiceSecret" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"value" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"serviceId" TEXT NOT NULL,
CONSTRAINT "ServiceSecret_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "ServiceSecret_name_serviceId_key" ON "ServiceSecret"("name", "serviceId");

View File

@@ -0,0 +1,19 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_ApplicationSettings" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"debug" BOOLEAN NOT NULL DEFAULT false,
"previews" BOOLEAN NOT NULL DEFAULT false,
"autodeploy" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ApplicationSettings" ("applicationId", "createdAt", "debug", "dualCerts", "id", "previews", "updatedAt") SELECT "applicationId", "createdAt", "debug", "dualCerts", "id", "previews", "updatedAt" FROM "ApplicationSettings";
DROP TABLE "ApplicationSettings";
ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings";
CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "phpModules" TEXT;

View File

@@ -11,8 +11,12 @@ model Setting {
id String @id @default(cuid()) id String @id @default(cuid())
fqdn String? @unique fqdn String? @unique
isRegistrationEnabled Boolean @default(false) isRegistrationEnabled Boolean @default(false)
dualCerts Boolean @default(false)
minPort Int @default(9000)
maxPort Int @default(9100)
proxyPassword String proxyPassword String
proxyUser String proxyUser String
proxyHash String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
@@ -51,9 +55,9 @@ model Team {
permissions Permission[] permissions Permission[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
database Database[] @relation(fields: [databaseId], references: [id]) database Database[] @relation(references: [id])
databaseId String? databaseId String?
service Service[] @relation(fields: [serviceId], references: [id]) service Service[] @relation(references: [id])
serviceId String? serviceId String?
} }
@@ -91,29 +95,45 @@ model Application {
gitSourceId String? gitSourceId String?
gitSource GitSource? @relation(fields: [gitSourceId], references: [id]) gitSource GitSource? @relation(fields: [gitSourceId], references: [id])
secrets Secret[] secrets Secret[]
phpModules String?
} }
model ApplicationSettings { model ApplicationSettings {
id String @id @default(cuid()) id String @id @default(cuid())
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])
applicationId String @unique applicationId String @unique
dualCerts Boolean @default(false)
debug Boolean @default(false) debug Boolean @default(false)
previews Boolean @default(false) previews Boolean @default(false)
autodeploy Boolean @default(true)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model Secret { model Secret {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
value String value String
isPRMRSecret Boolean @default(false)
isBuildSecret Boolean @default(false) isBuildSecret Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])
applicationId String applicationId String
@@unique([name, applicationId]) @@unique([name, applicationId, isPRMRSecret])
}
model ServiceSecret {
id String @id @default(cuid())
name String
value String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
serviceId String
@@unique([name, serviceId])
} }
model BuildLog { model BuildLog {
@@ -234,6 +254,7 @@ model Service {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
fqdn String? fqdn String?
dualCerts Boolean @default(false)
type String? type String?
version String? version String?
teams Team[] teams Team[]
@@ -245,6 +266,7 @@ model Service {
minio Minio? minio Minio?
vscodeserver Vscodeserver? vscodeserver Vscodeserver?
wordpress Wordpress? wordpress Wordpress?
serviceSecret ServiceSecret[]
} }
model PlausibleAnalytics { model PlausibleAnalytics {

View File

@@ -27,6 +27,15 @@ async function main() {
proxyUser: cuid() proxyUser: cuid()
} }
}); });
} else {
await prisma.setting.update({
where: {
id: settingsFound.id
},
data: {
proxyHash: null
}
});
} }
const localDocker = await prisma.destinationDocker.findFirst({ const localDocker = await prisma.destinationDocker.findFirst({
where: { engine: '/var/run/docker.sock' } where: { engine: '/var/run/docker.sock' }

View File

@@ -1,112 +0,0 @@
#!/usr/bin/env bash
clear
ARG1=$1
WHO=$(whoami)
APP_ID=$(cat /proc/sys/kernel/random/uuid)
RANDOM_SECRET=$(echo $(($(date +%s%N) / 1000000)) | sha256sum | base64 | head -c 32)
SENTRY_DSN="https://9e7a74326f29422584d2d0bebdc8b7d3@o1082494.ingest.sentry.io/6091062"
DOCKER_MAJOR=20
DOCKER_MINOR=10
DOCKER_VERSION_OK="nok"
set -eou pipefail
if [ $ARG1 ] && [ $ARG1 == "-d" ]; then
set -x
fi
function errorchecker() {
exitCode=$?
if [ $exitCode -ne "0" ]; then
echo "$0 exited unexpectedly with status: $exitCode"
exit $exitCode
fi
}
trap 'errorchecker' EXIT
echo -e "Welcome to Coolify installer! \n"
echo "This script will install all the required packages and services to run Coolify."
echo -e "If you want to install Coolify on a different OS, please open an issue on Github to get supported version.\n\n"
echo -e "To see what I'm doing, please check:"
echo -e "https://github.com/coollabsio/get.coollabs.io/blob/main/static/coolify/install_v2.sh\n\n"
if [ $WHO != 'root' ]; then
echo 'Run as root please: sudo sh -c "$(curl -fsSL https://get.coollabs.io/coolify/install.sh)"'
exit 1
fi
if [ ! -x "$(command -v docker)" ]; then
while true; do
read -p "Docker Engine not found, should I install it automatically? [Yy/Nn] " yn
case $yn in
[Yy]*)
sh -c "$(curl -fsSL https://get.docker.com)"
break
;;
[Nn]*)
echo "Please install docker manually and update it to the latest, but at least to $DOCKER_MAJOR.$DOCKER_MINOR"
exit 0
;;
*) echo "Please answer Y or N." ;;
esac
done
fi
SERVER_VERSION=$(docker version -f "{{.Server.Version}}")
SERVER_VERSION_MAJOR=$(echo "$SERVER_VERSION" | cut -d'.' -f 1)
SERVER_VERSION_MINOR=$(echo "$SERVER_VERSION" | cut -d'.' -f 2)
if [ "$SERVER_VERSION_MAJOR" -ge "$DOCKER_MAJOR" ] &&
[ "$SERVER_VERSION_MINOR" -ge "$DOCKER_MINOR" ]; then
DOCKER_VERSION_OK="ok"
fi
if [ $DOCKER_VERSION_OK == 'nok' ]; then
echo "Docker version less than $DOCKER_MAJOR.$DOCKER_MINOR, please update it to at least to $DOCKER_MAJOR.$DOCKER_MINOR"
exit 1
fi
# Adding docker daemon configuration
cat <<EOF >/etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "5"
},
"features": {
"buildkit": true
},
"live-restore": true
}
EOF
# Restarting docker daemon
sh -c "systemctl daemon-reload && systemctl restart docker"
# Downloading docker compose cli plugin
mkdir -p ~/.docker/cli-plugins/
curl -SL https://github.com/docker/compose/releases/download/v2.2.2/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose
# Making base directory for coolify
if [ ! -d coolify ]; then
mkdir coolify
fi
if [ -f coolify/.env ]; then
echo -e "Coolify is already installed, using some of the existing settings."
else
echo "COOLIFY_APP_ID=$APP_ID
COOLIFY_SECRET_KEY=$RANDOM_SECRET
COOLIFY_DATABASE_URL=file:../db/prod.db
COOLIFY_SENTRY_DSN=$SENTRY_DSN
COOLIFY_HOSTED_ON=docker" > coolify/.env
fi
cd coolify && docker run -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db-sqlite coollabsio/coolify:latest /bin/sh -c "env | grep COOLIFY > .env && docker compose up -d --force-recreate"
echo -e "Congratulations! Your coolify is ready to use.\n"
echo "Please visit http://<Your Public IP Address>:3000/ to get started."
echo "It will take a few minutes to start up, don't worry."

View File

@@ -1,114 +0,0 @@
#!/usr/bin/env bash
clear
ARG1=$1
WHO=$(whoami)
APP_ID=$(cat /proc/sys/kernel/random/uuid)
RANDOM_SECRET=$(echo $(($(date +%s%N) / 1000000)) | sha256sum | base64 | head -c 32)
SENTRY_DSN="https://9e7a74326f29422584d2d0bebdc8b7d3@o1082494.ingest.sentry.io/6091062"
UBUNTU_MAJOR_MIN=20
UBUNTU_MINOR_MIN=04
OS_OK="nok"
set -eou pipefail
if [ $ARG1 ] && [ $ARG1 == "-d" ]; then
set -x
fi
function errorchecker() {
exitCode=$?
if [ $exitCode -ne "0" ]; then
echo "$0 exited unexpectedly with status: $exitCode"
exit $exitCode
fi
}
trap 'errorchecker' EXIT
if [ $WHO != 'root' ]; then
echo 'Run as root please: sudo sh -c "$(curl -fsSL https://get.coollabs.io/coolify/install.sh)"'
exit 1
fi
. /etc/lsb-release
if [ $DISTRIB_ID != 'Ubuntu' ]; then
echo 'Not supported OS, please open an issue on Github to get supported version.'
exit 1
fi
DISTRIB_RELEASE_MAJOR=$(echo "$DISTRIB_RELEASE" | cut -d'.' -f 1)
DISTRIB_RELEASE_MINOR=$(echo "$DISTRIB_RELEASE" | cut -d'.' -f 2)
if [ "$DISTRIB_RELEASE_MAJOR" -ge "$UBUNTU_MAJOR_MIN" ] &&
[ "$DISTRIB_RELEASE_MINOR" -ge "$UBUNTU_MINOR_MIN" ]; then
OS_OK="ok"
fi
if [ $OS_OK == 'nok' ]; then
echo "Ubuntu version less than $UBUNTU_MAJOR_MIN.$UBUNTU_MINOR_MIN."
exit 1
fi
function installPodman() {
apt-get update -y
apt-get install curl wget gnupg2 -y
if [ "$DISTRIB_RELEASE_MAJOR" -eq "20" ] && [ "$DISTRIB_RELEASE_MINOR" -eq "04" ]; then
echo 'Installing on 20.04'
source /etc/os-release
sh -c "echo 'deb http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/ /' > /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list"
wget -nv https://download.opensuse.org/repositories/devel:kubic:libcontainers:stable/xUbuntu_${VERSION_ID}/Release.key -O- | apt-key add -
apt-get update -y
apt-get -y install podman
return 0
elif [ "$DISTRIB_RELEASE_MAJOR" -eq "20" ] && [ "$DISTRIB_RELEASE_MINOR" -eq "10" ]; then
apt-get -y install podman
return 0
elif [ "$DISTRIB_RELEASE_MAJOR" -gt "20" ]; then
apt-get -y install podman
return 0
else
exit 1
fi
}
if [ ! -x "$(command -v podman)" ]; then
while true; do
read -p "Podman not found, should I install it automatically? [Yy/Nn] " yn
case $yn in
[Yy]*)
installPodman
break
;;
[Nn]*)
echo "Please install docker manually and update it to the latest, but at least to $DOCKER_MAJOR.$DOCKER_MINOR"
exit 0
;;
*) echo "Please answer Yy or Nn." ;;
esac
done
fi
# Making base directory for coolify
if [ ! -d coolify ]; then
mkdir coolify
fi
echo "COOLIFY_APP_ID=$APP_ID
COOLIFY_SECRET_KEY=$RANDOM_SECRET
COOLIFY_DATABASE_URL=file:../db/prod.db
COOLIFY_SENTRY_DSN=$SENTRY_DSN
COOLIFY_HOSTED_ON=docker" >coolify/.env
systemctl start podman.socket
systemctl enable podman.socket
podman volume create coolify-db
podman volume create coolify-ssl-certs
podman volume create coolify-letsencrypt
cd coolify && podman run --privileged -tid --env-file .env -v /var/run/podman/podman.sock:/var/run/podman/podman.sock -v coolify-db-sqlite:/app/db docker.io/coollabsio/coolify:latest /bin/sh -c "env | grep COOLIFY > .env && docker-compose up -d --force-recreate"
echo "Done"
exit 0

View File

@@ -1,74 +1,26 @@
/// <reference types="@sveltejs/kit" /> /// <reference types="@sveltejs/kit" />
interface Cookies {
teamId?: string; declare namespace App {
gitlabToken?: string; interface Locals {
'kit.session'?: string; session: import('svelte-kit-cookie-session').Session<SessionData>;
} cookies: Record<string, string>;
interface Locals { }
gitlabToken?: string; interface Platform {}
user: { interface Session extends SessionData {}
teamId: string; interface Stuff {}
permission: string;
isAdmin: boolean;
};
session: {
data: {
uid?: string;
teams?: string[];
expires?: string;
};
};
} }
type Applications = { interface SessionData {
name: string; version?: string;
domain: string; userId?: string | null;
}; teamId?: string | null;
permission?: string;
interface Hash { isAdmin?: boolean;
iv: string; expires?: string | null;
content: string; gitlabToken?: string | null;
ghToken?: string | null;
} }
interface BuildPack {
name: string;
}
// TODO: Not used, not working what?!
enum GitSource {
Github = 'github',
Gitlab = 'gitlab',
Bitbucket = 'bitbucket'
}
type RawHaproxyConfiguration = {
_version: number;
data: string;
};
type NewTransaction = {
_version: number;
id: string;
status: string;
};
type HttpRequestRuleForceSSL = {
return_hdrs: null;
cond: string;
cond_test: string;
index: number;
redir_code: number;
redir_type: string;
redir_value: string;
type: string;
};
// TODO: No any please
type HttpRequestRule = {
_version: number;
data: Array<any>;
};
type DateTimeFormatOptions = { type DateTimeFormatOptions = {
localeMatcher?: 'lookup' | 'best fit'; localeMatcher?: 'lookup' | 'best fit';
weekday?: 'long' | 'short' | 'narrow'; weekday?: 'long' | 'short' | 'narrow';
@@ -84,3 +36,24 @@ type DateTimeFormatOptions = {
hour12?: boolean; hour12?: boolean;
timeZone?: string; timeZone?: string;
}; };
interface Hash {
iv: string;
content: string;
}
type RawHaproxyConfiguration = {
_version: number;
data: string;
};
type NewTransaction = {
_version: number;
id: string;
status: string;
};
type Application = {
name: string;
domain: string;
};

View File

@@ -2,7 +2,7 @@ import dotEnvExtended from 'dotenv-extended';
dotEnvExtended.load(); dotEnvExtended.load();
import type { GetSession } from '@sveltejs/kit'; import type { GetSession } from '@sveltejs/kit';
import { handleSession } from 'svelte-kit-cookie-session'; import { handleSession } from 'svelte-kit-cookie-session';
import { getUserDetails, isTeamIdTokenAvailable, sentry } from '$lib/common'; import { getUserDetails, sentry } from '$lib/common';
import { version } from '$lib/common'; import { version } from '$lib/common';
import cookie from 'cookie'; import cookie from 'cookie';
import { dev } from '$app/env'; import { dev } from '$app/env';
@@ -10,27 +10,34 @@ import { dev } from '$app/env';
export const handle = handleSession( export const handle = handleSession(
{ {
secret: process.env['COOLIFY_SECRET_KEY'], secret: process.env['COOLIFY_SECRET_KEY'],
expires: 30 expires: 30,
cookie: { secure: false }
}, },
async function ({ event, resolve }) { async function ({ event, resolve }) {
let response; let response;
try { try {
const cookies: Cookies = cookie.parse(event.request.headers.get('cookie') || ''); if (event.locals.cookies) {
if (cookies['kit.session']) { if (event.locals.cookies['kit.session']) {
const { permission, teamId } = await getUserDetails(event, false); const { permission, teamId, userId } = await getUserDetails(event, false);
event.locals.user = { const newSession = {
teamId, userId,
permission, teamId,
isAdmin: permission === 'admin' || permission === 'owner' permission,
}; isAdmin: permission === 'admin' || permission === 'owner',
} expires: event.locals.session.data.expires
if (cookies.gitlabToken) { };
event.locals.gitlabToken = cookies.gitlabToken;
if (JSON.stringify(event.locals.session.data) !== JSON.stringify(newSession)) {
event.locals.session.data = { ...newSession };
}
}
} }
response = await resolve(event, { response = await resolve(event, {
ssr: !event.url.pathname.startsWith('/webhooks/success') ssr: !event.url.pathname.startsWith('/webhooks/success')
}); });
} catch (error) { } catch (error) {
console.log(error);
response = await resolve(event, { response = await resolve(event, {
ssr: !event.url.pathname.startsWith('/webhooks/success') ssr: !event.url.pathname.startsWith('/webhooks/success')
}); });
@@ -61,17 +68,13 @@ export const handle = handleSession(
} }
); );
export const getSession: GetSession = function (request) { export const getSession: GetSession = function ({ locals }) {
return { return {
version, version,
gitlabToken: request.locals?.gitlabToken || null, ...locals.session.data
uid: request.locals.session.data?.uid || null,
teamId: request.locals.user?.teamId || null,
permission: request.locals.user?.permission,
isAdmin: request.locals.user?.isAdmin || false
}; };
}; };
export async function handleError({ error, event }) { export async function handleError({ error, event }) {
if (!dev) sentry.captureException(error, { event }); if (!dev) sentry.captureException(error, event);
} }

View File

@@ -84,7 +84,15 @@ export function makeLabelForServices(type) {
} }
export const setDefaultConfiguration = async (data) => { export const setDefaultConfiguration = async (data) => {
let { buildPack, port, installCommand, startCommand, buildCommand, publishDirectory } = data; let {
buildPack,
port,
installCommand,
startCommand,
buildCommand,
publishDirectory,
baseDirectory
} = data;
const template = scanningTemplates[buildPack]; const template = scanningTemplates[buildPack];
if (!port) { if (!port) {
port = template?.port || 3000; port = template?.port || 3000;
@@ -97,6 +105,10 @@ export const setDefaultConfiguration = async (data) => {
if (!startCommand) startCommand = template?.startCommand || 'yarn start'; if (!startCommand) startCommand = template?.startCommand || 'yarn start';
if (!buildCommand) buildCommand = template?.buildCommand || null; if (!buildCommand) buildCommand = template?.buildCommand || null;
if (!publishDirectory) publishDirectory = template?.publishDirectory || null; if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
if (baseDirectory) {
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
if (!baseDirectory.endsWith('/')) baseDirectory = `${baseDirectory}/`;
}
return { return {
buildPack, buildPack,
@@ -104,7 +116,8 @@ export const setDefaultConfiguration = async (data) => {
installCommand, installCommand,
startCommand, startCommand,
buildCommand, buildCommand,
publishDirectory publishDirectory,
baseDirectory
}; };
}; };
@@ -175,3 +188,11 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
throw new Error(error); throw new Error(error);
} }
} }
export function checkPnpm(installCommand = null, buildCommand = null, startCommand = null) {
return (
installCommand?.includes('pnpm') ||
buildCommand?.includes('pnpm') ||
startCommand?.includes('pnpm')
);
}

View File

@@ -9,12 +9,14 @@ export default async function ({
docker, docker,
buildId, buildId,
baseDirectory, baseDirectory,
secrets secrets,
pullmergeRequestId
}) { }) {
try { try {
let file = `${workdir}/Dockerfile`; let file = `${workdir}/Dockerfile`;
if (baseDirectory) { if (baseDirectory) {
file = `${workdir}/${baseDirectory}/Dockerfile`; file = `${workdir}/${baseDirectory}/Dockerfile`;
workdir = `${workdir}/${baseDirectory}`;
} }
const Dockerfile: Array<string> = (await fs.readFile(`${file}`, 'utf8')) const Dockerfile: Array<string> = (await fs.readFile(`${file}`, 'utf8'))
@@ -24,7 +26,15 @@ export default async function ({
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`); if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
}
} }
}); });
} }

View File

@@ -7,7 +7,9 @@ const createDockerfile = async (data, imageforBuild): Promise<void> => {
Dockerfile.push(`FROM ${imageforBuild}`); Dockerfile.push(`FROM ${imageforBuild}`);
Dockerfile.push('WORKDIR /usr/share/nginx/html'); Dockerfile.push('WORKDIR /usr/share/nginx/html');
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`);
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]'); Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));

View File

@@ -10,6 +10,8 @@ import nuxtjs from './nuxtjs';
import vuejs from './vuejs'; import vuejs from './vuejs';
import php from './php'; import php from './php';
import rust from './rust'; import rust from './rust';
import astro from './static';
import eleventy from './static';
export { export {
node, node,
@@ -23,5 +25,7 @@ export {
nuxtjs, nuxtjs,
vuejs, vuejs,
php, php,
rust rust,
astro,
eleventy
}; };

View File

@@ -4,12 +4,19 @@ import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, port, startCommand, workdir, baseDirectory } = data; const { applicationId, tag, port, startCommand, workdir, baseDirectory } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
const isPnpm = startCommand.includes('pnpm');
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /usr/src/app');
Dockerfile.push(`LABEL coolify.image=true`);
if (isPnpm) {
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm');
Dockerfile.push('RUN pnpm add -g pnpm');
}
Dockerfile.push( Dockerfile.push(
`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${baseDirectory || ''} ./` `COPY --from=${applicationId}:${tag}-cache /usr/src/app/${baseDirectory || ''} ./`
); );
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ${startCommand}`); Dockerfile.push(`CMD ${startCommand}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));

View File

@@ -1,31 +1,45 @@
import { buildImage } from '$lib/docker'; import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { workdir, port, installCommand, buildCommand, startCommand, baseDirectory, secrets } = const {
data; workdir,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
secrets,
pullmergeRequestId
} = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /usr/src/app');
Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`); if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
}
} }
}); });
} }
Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`); if (isPnpm) {
try { Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm');
await fs.stat(`${workdir}/yarn.lock`); Dockerfile.push('RUN pnpm add -g pnpm');
Dockerfile.push(`COPY ./${baseDirectory || ''}yarn.lock ./`); }
} catch (error) {} Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
try {
await fs.stat(`${workdir}/pnpm-lock.yaml`);
Dockerfile.push(`COPY ./${baseDirectory || ''}pnpm-lock.yaml ./`);
} catch (error) {}
Dockerfile.push(`RUN ${installCommand}`); Dockerfile.push(`RUN ${installCommand}`);
Dockerfile.push(`COPY ./${baseDirectory || ''} ./`);
if (buildCommand) { if (buildCommand) {
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
} }

View File

@@ -1,31 +1,45 @@
import { buildImage } from '$lib/docker'; import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { workdir, port, installCommand, buildCommand, startCommand, baseDirectory, secrets } = const {
data; workdir,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
secrets,
pullmergeRequestId
} = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /usr/src/app');
Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`); if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
}
} }
}); });
} }
Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`); if (isPnpm) {
try { Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm');
await fs.stat(`${workdir}/yarn.lock`); Dockerfile.push('RUN pnpm add -g pnpm');
Dockerfile.push(`COPY ./${baseDirectory || ''}yarn.lock ./`); }
} catch (error) {} Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
try {
await fs.stat(`${workdir}/pnpm-lock.yaml`);
Dockerfile.push(`COPY ./${baseDirectory || ''}pnpm-lock.yaml ./`);
} catch (error) {}
Dockerfile.push(`RUN ${installCommand}`); Dockerfile.push(`RUN ${installCommand}`);
Dockerfile.push(`COPY ./${baseDirectory || ''} ./`);
if (buildCommand) { if (buildCommand) {
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
} }

View File

@@ -1,31 +1,44 @@
import { buildImage } from '$lib/docker'; import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { workdir, port, installCommand, buildCommand, startCommand, baseDirectory, secrets } = const {
data; workdir,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
secrets,
pullmergeRequestId
} = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /usr/src/app');
Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`); if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
}
} }
}); });
} }
Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`); if (isPnpm) {
try { Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm');
await fs.stat(`${workdir}/yarn.lock`); Dockerfile.push('RUN pnpm add -g pnpm');
Dockerfile.push(`COPY ./${baseDirectory || ''}yarn.lock ./`); }
} catch (error) {} Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
try {
await fs.stat(`${workdir}/pnpm-lock.yaml`);
Dockerfile.push(`COPY ./${baseDirectory || ''}pnpm-lock.yaml ./`);
} catch (error) {}
Dockerfile.push(`RUN ${installCommand}`); Dockerfile.push(`RUN ${installCommand}`);
Dockerfile.push(`COPY ./${baseDirectory || ''} ./`);
if (buildCommand) { if (buildCommand) {
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
} }

View File

@@ -4,11 +4,19 @@ import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { workdir, baseDirectory } = data; const { workdir, baseDirectory } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.image=true`);
if (data.phpModules?.length > 0) {
Dockerfile.push(
`ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/`
);
Dockerfile.push(`RUN chmod +x /usr/local/bin/install-php-extensions`);
Dockerfile.push(`RUN /usr/local/bin/install-php-extensions ${data.phpModules.join(' ')}`);
}
Dockerfile.push('RUN a2enmod rewrite'); Dockerfile.push('RUN a2enmod rewrite');
Dockerfile.push('WORKDIR /var/www/html'); Dockerfile.push('WORKDIR /var/www/html');
Dockerfile.push(`COPY ./${baseDirectory || ''} /var/www/html`); Dockerfile.push(`COPY .${baseDirectory || ''} /var/www/html`);
Dockerfile.push(`COPY /.htaccess /var/www/html/.htaccess`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["apache2-foreground"]'); Dockerfile.push('CMD ["apache2-foreground"]');
Dockerfile.push('RUN chown -R www-data /var/www/html'); Dockerfile.push('RUN chown -R www-data /var/www/html');

View File

@@ -6,8 +6,10 @@ const createDockerfile = async (data, image): Promise<void> => {
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push('WORKDIR /usr/share/nginx/html'); Dockerfile.push('WORKDIR /usr/share/nginx/html');
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`);
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]'); Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));

View File

@@ -8,6 +8,7 @@ const createDockerfile = async (data, image, name): Promise<void> => {
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /usr/src/app');
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/target target`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/target target`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/local/cargo /usr/local/cargo`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/local/cargo /usr/local/cargo`);
Dockerfile.push(`COPY . .`); Dockerfile.push(`COPY . .`);

View File

@@ -2,16 +2,33 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, workdir, buildCommand, baseDirectory, publishDirectory, secrets } = const {
data; applicationId,
tag,
workdir,
buildCommand,
baseDirectory,
publishDirectory,
secrets,
pullmergeRequestId
} = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/share/nginx/html'); Dockerfile.push('WORKDIR /usr/share/nginx/html');
Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`); if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
}
} }
}); });
} }
@@ -20,8 +37,9 @@ const createDockerfile = async (data, image): Promise<void> => {
`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./` `COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`
); );
} else { } else {
Dockerfile.push(`COPY ./${baseDirectory || ''} ./`); Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
} }
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]'); Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));

View File

@@ -7,7 +7,9 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/share/nginx/html'); Dockerfile.push('WORKDIR /usr/share/nginx/html');
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`);
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]'); Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));

View File

@@ -7,7 +7,9 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/share/nginx/html'); Dockerfile.push('WORKDIR /usr/share/nginx/html');
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`);
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]'); Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
@@ -18,6 +20,21 @@ export default async function (data) {
const image = 'nginx:stable-alpine'; const image = 'nginx:stable-alpine';
const imageForBuild = 'node:lts'; const imageForBuild = 'node:lts';
await buildCacheImageWithNode(data, imageForBuild); await buildCacheImageWithNode(data, imageForBuild);
// await fs.writeFile(`${data.workdir}/default.conf`, `server {
// listen 80;
// server_name localhost;
// location / {
// root /usr/share/nginx/html;
// try_files $uri $uri/ /index.html;
// }
// error_page 500 502 503 504 /50x.html;
// location = /50x.html {
// root /usr/share/nginx/html;
// }
// }
// `);
await createDockerfile(data, image); await createDockerfile(data, image);
await buildImage(data); await buildImage(data);
} catch (error) { } catch (error) {

View File

@@ -11,6 +11,7 @@ import { version as currentVersion } from '../../package.json';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import Cookie from 'cookie'; import Cookie from 'cookie';
import os from 'os'; import os from 'os';
import cuid from 'cuid';
try { try {
if (!dev) { if (!dev) {
@@ -67,10 +68,10 @@ export const isTeamIdTokenAvailable = (request) => {
}; };
export const getTeam = (event) => { export const getTeam = (event) => {
const cookies: Cookies = Cookie.parse(event.request.headers.get('cookie')); const cookies = Cookie.parse(event.request.headers.get('cookie'));
if (cookies.teamId) { if (cookies?.teamId) {
return cookies.teamId; return cookies.teamId;
} else if (event.locals.session.data.teamId) { } else if (event.locals.session.data?.teamId) {
return event.locals.session.data.teamId; return event.locals.session.data.teamId;
} }
return null; return null;
@@ -78,7 +79,7 @@ export const getTeam = (event) => {
export const getUserDetails = async (event, isAdminRequired = true) => { export const getUserDetails = async (event, isAdminRequired = true) => {
const teamId = getTeam(event); const teamId = getTeam(event);
const userId = event.locals.session.data.uid || null; const userId = event.locals.session.data.userId || null;
const { permission = 'read' } = await db.prisma.permission.findFirst({ const { permission = 'read' } = await db.prisma.permission.findFirst({
where: { teamId, userId }, where: { teamId, userId },
select: { permission: true }, select: { permission: true },
@@ -103,9 +104,14 @@ export const getUserDetails = async (event, isAdminRequired = true) => {
}; };
export function getEngine(engine) { export function getEngine(engine) {
return engine === '/var/run/docker.sock' ? 'unix:///var/run/docker.sock' : `tcp://${engine}:2375`; return engine === '/var/run/docker.sock' ? 'unix:///var/run/docker.sock' : engine;
} }
// export async function saveSshKey(destination) {
// return await asyncExecShell(
// `echo '${destination.sshPrivateKey}' > /tmp/id_rsa_${destination.id} && chmod 600 /tmp/id_rsa_${destination.id}`
// );
// }
export async function removeContainer(id, engine) { export async function removeContainer(id, engine) {
const host = getEngine(engine); const host = getEngine(engine);
try { try {

View File

@@ -1,9 +1,9 @@
<script> <script>
import { browser } from '$app/env'; import { browser } from '$app/env';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
export let value;
let showPassword = false; let showPassword = false;
export let value;
export let disabled = false; export let disabled = false;
export let isPasswordField = false; export let isPasswordField = false;
export let readonly = false; export let readonly = false;
@@ -14,31 +14,25 @@
export let name; export let name;
export let placeholder = ''; export let placeholder = '';
let disabledClass = 'bg-coolback disabled:bg-coolblack select-all'; let disabledClass = 'bg-coolback disabled:bg-coolblack';
let actionsShow = false;
let isHttps = browser && window.location.protocol === 'https:'; let isHttps = browser && window.location.protocol === 'https:';
function showActions(value) {
actionsShow = value;
}
function copyToClipboard() { function copyToClipboard() {
if (isHttps && navigator.clipboard) { if (isHttps && navigator.clipboard) {
navigator.clipboard.writeText(value); navigator.clipboard.writeText(value);
toast.push('Copied to clipboard'); toast.push('Copied to clipboard.');
} }
} }
</script> </script>
<span <div class="relative">
class="relative"
on:mouseenter={() => showActions(true)}
on:mouseleave={() => showActions(false)}
>
{#if !isPasswordField || showPassword} {#if !isPasswordField || showPassword}
{#if textarea} {#if textarea}
<textarea <textarea
rows="3" rows="5"
class={disabledClass} class={disabledClass}
class:pr-10={true}
class:pr-20={value && isHttps}
{placeholder} {placeholder}
type="text" type="text"
{id} {id}
@@ -52,6 +46,8 @@
<input <input
class={disabledClass} class={disabledClass}
type="text" type="text"
class:pr-10={true}
class:pr-20={value && isHttps}
{id} {id}
{name} {name}
{required} {required}
@@ -65,6 +61,8 @@
{:else} {:else}
<input <input
class={disabledClass} class={disabledClass}
class:pr-10={true}
class:pr-20={value && isHttps}
type="password" type="password"
{id} {id}
{name} {name}
@@ -77,69 +75,67 @@
/> />
{/if} {/if}
{#if actionsShow} <div class="absolute top-0 right-0 m-3 cursor-pointer text-stone-600 hover:text-white">
<div class="absolute top-0 right-0 mx-2 cursor-pointer text-warmGray-600 hover:text-white"> <div class="flex space-x-2">
<div class="flex space-x-2"> {#if isPasswordField}
{#if isPasswordField} <div on:click={() => (showPassword = !showPassword)}>
<div on:click={() => (showPassword = !showPassword)}> {#if showPassword}
{#if showPassword}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
/>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</svg>
{/if}
</div>
{/if}
{#if value && isHttps}
<div on:click={copyToClipboard}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none" fill="none"
stroke-linecap="round" viewBox="0 0 24 24"
stroke-linejoin="round" stroke="currentColor"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path
<rect x="8" y="8" width="12" height="12" rx="2" /> stroke-linecap="round"
<path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" /> stroke-linejoin="round"
stroke-width="2"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
/>
</svg> </svg>
</div> {:else}
{/if} <svg
</div> xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</svg>
{/if}
</div>
{/if}
{#if value && isHttps}
<div on:click={copyToClipboard}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="8" y="8" width="12" height="12" rx="2" />
<path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" />
</svg>
</div>
{/if}
</div> </div>
{/if} </div>
</span> </div>

View File

@@ -1,6 +1,6 @@
<script> <script>
export let text; export let text;
export let maxWidthClass = 'max-w-[24rem]'; export let customClass = 'max-w-[24rem]';
</script> </script>
<div class="py-1 text-xs text-stone-400 {maxWidthClass}">{@html text}</div> <div class="p-2 text-xs text-stone-400 {customClass}">{@html text}</div>

View File

@@ -11,7 +11,7 @@
<span class="loader" /> <span class="loader" />
</div> </div>
{:else} {:else}
<div class=" main h-64 py-24 left-0 top-0 flex flex-wrap content-center mx-auto"> <div class="main h-64 py-24 left-0 top-0 flex flex-wrap content-center mx-auto">
<span class="loader" /> <span class="loader" />
</div> </div>
{/if} {/if}

View File

@@ -4,15 +4,23 @@
export let setting; export let setting;
export let title; export let title;
export let description; export let description;
export let isPadding = true; export let isCenter = true;
export let disabled = false; export let disabled = false;
export let dataTooltip = null;
</script> </script>
<li class="flex items-center py-4"> <div class="flex items-center py-4 pr-8">
<div class="flex w-96 flex-col" class:px-4={isPadding} class:pr-32={!isPadding}> <div class="flex w-96 flex-col">
<p class="text-xs font-bold text-stone-100 md:text-base">{title}</p> <div class="text-xs font-bold text-stone-100 md:text-base">{title}</div>
<Explainer text={description} /> <Explainer text={description} />
</div> </div>
</div>
<div
class:tooltip={dataTooltip}
class:text-center={isCenter}
data-tooltip={dataTooltip}
class="flex justify-center"
>
<div <div
type="button" type="button"
on:click on:click
@@ -58,5 +66,4 @@
</span> </span>
</span> </span>
</div> </div>
<!-- {/if} --> </div>
</li>

View File

@@ -9,25 +9,12 @@ export const dateOptions: DateTimeFormatOptions = {
hour12: false hour12: false
}; };
export async function getGithubToken({ apiUrl, application, githubToken }): Promise<void> {
const response = await fetch(
`${apiUrl}/app/installations/${application.gitSource.githubApp.installationId}/access_tokens`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${githubToken}`
}
}
);
if (!response.ok) {
throw new Error('Git Source not configured.');
}
const data = await response.json();
return data.token;
}
export const staticDeployments = ['react', 'vuejs', 'static', 'svelte', 'gatsby', 'php']; export const staticDeployments = ['react', 'vuejs', 'static', 'svelte', 'gatsby', 'php'];
export const notNodeDeployments = ['php', 'docker', 'rust']; export const notNodeDeployments = ['php', 'docker', 'rust'];
export function getDomain(domain) { export function getDomain(domain) {
return domain?.replace('https://', '').replace('http://', ''); return domain?.replace('https://', '').replace('http://', '');
} }
export function generateRemoteEngine(destination) {
return `ssh://${destination.user}@${destination.ipAddress}:${destination.port}`;
}

View File

@@ -0,0 +1,21 @@
<svg
class="absolute top-0 left-0 -m-6 h-14 w-14"
viewBox="0 0 256 256"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
id="a"
fill="#302649"
fill-rule="evenodd"
clip-rule="evenodd"
d="M163.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53l-28.198-95.29a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z"
/>
<path
id="flame"
fill="#EF661E"
fill-rule="evenodd"
clip-rule="evenodd"
d="M168.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,6 @@
<svg viewBox="0 0 128 128" class="absolute top-0 left-0 -m-8 h-16 w-16">
<path fill="transparent" d="M18 0h92v128H18z" /><path
d="M55.3 36.3h.4c1.1 0 1.5.9 1.5 2.3v41.8c0 1.8-.4 3-1.6 3l-4.8-.1c-1.2 0-1.6-1-1.6-3V45.5l-2.1.5c-1 0-1.5-.8-1.5-2.2v-3c0-1.2.4-2 1.4-2.2l8.3-2.2zm16 36.1l.1 3 .6 1.3.6.6.8.1h2.2c1 0 1.7.8 1.7 2v1.9c0 1.2-.6 2-1.8 2h-3.2l-2.3-.1c-.7-.2-1.4-.5-2.2-1a5.7 5.7 0 01-2-1.9c-.4-.8-.8-1.9-1-3.2-.4-1.4-.5-3-.5-4.8v-16h-1.5c-1.1 0-1.6-1-1.6-2.4v-1.7c0-1.4.5-2.3 1.6-2.3h1.5v-.1l.6-12.3c0-1.5.5-2.5 1.6-2.5h3.1c1.2 0 1.6 1 1.6 2.5v12.3h3.6c1.1 0 1.6 1 1.6 2.4V54c0 1.4-.5 2.3-1.6 2.3h-3.6v16.2zm9.4 15.7c0-2 .3-3.2 1.5-3.2.2 0 .4 0 1.4.4l1.1.3.2-.1.4-.7c.3-.6.4-1.6.4-3l-.6-3.3L79 52.7v-.9c0-1.2.3-2 1.2-2H84c.5 0 1 .3 1.3.6.3.4.5.9.6 1.6l3.4 18 2.6-17.8c.1-.8.3-1.3.6-1.7.3-.4.8-.6 1.3-.6h2.6c1 0 1.4.8 1.4 2l-.1.8L92 82.2c-.5 2.5-1 4.4-1.5 5.7a6.6 6.6 0 01-2 3c-.9.6-1.9.9-3.1.9h-.3c-2 0-3.3-.6-4.1-1.7-.3-.4-.4-1-.4-2zM31.3 38.8l8.2-2.1h.5c1 0 1.4.8 1.4 2.2v41.9c0 1.8-.4 2.9-1.6 2.9h-4.7c-1.2 0-1.6-1.1-1.6-3v-35l-2 .6c-1.2 0-1.6-.9-1.6-2.2v-3c0-1.2.4-2 1.4-2.3z"
fill="#FFF"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,27 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'}
fill="none"
viewBox="0 0 140 140"
data-lt-extension-installed="true"
><g clip-path="url(#clip0)"
><path
fill="#fff"
fill-rule="evenodd"
d="M140 43.602c0-1.662.001-3.324-.01-4.987-.008-1.4-.024-2.8-.062-4.2-.082-3.05-.262-6.126-.805-9.142-.55-3.06-1.448-5.907-2.864-8.688A29.227 29.227 0 0 0 123.476 3.81c-2.783-1.416-5.634-2.314-8.697-2.864-3.016-.542-6.094-.722-9.144-.804-1.4-.038-2.801-.054-4.202-.063C99.77.068 98.107.07 96.444.07L77.135 0H62.694L43.726.07c-1.666 0-3.332-.002-4.998.008-1.404.01-2.807.025-4.21.063-3.058.082-6.142.262-9.166.805-3.067.55-5.922 1.447-8.709 2.862a29.293 29.293 0 0 0-7.419 5.377 29.223 29.223 0 0 0-5.389 7.4c-1.42 2.78-2.32 5.63-2.871 8.691-.543 3.016-.723 6.091-.806 9.14-.038 1.4-.054 2.8-.062 4.2C.086 40.277 0 42.342 0 44.004v33.3l.086 19.102c0 1.665 0 3.33.01 4.994a200.6 200.6 0 0 0 .062 4.205c.083 3.054.263 6.135.807 9.155.551 3.064 1.451 5.916 2.87 8.7a29.294 29.294 0 0 0 12.807 12.794c2.788 1.418 5.645 2.317 8.714 2.868 3.022.542 6.105.722 9.162.804 1.403.038 2.806.054 4.21.063 1.666.01 3.332.009 4.998.009l19.14.001h14.477l19.101-.001c1.663 0 3.326.001 4.989-.009a202.92 202.92 0 0 0 4.202-.063c3.052-.082 6.13-.262 9.148-.805 3.061-.551 5.911-1.45 8.692-2.867a29.215 29.215 0 0 0 7.405-5.384 29.22 29.22 0 0 0 5.378-7.409c1.417-2.785 2.315-5.639 2.866-8.704.542-3.02.722-6.099.804-9.152.038-1.402.054-2.804.062-4.205.011-1.665.01-3.33.01-4.993l-.001-19.103V62.694L140 43.602"
clip-rule="evenodd"
/><path
fill="#000"
fill-rule="evenodd"
d="M39.375 40.188h8.313a6.25 6.25 0 0 1 6.25 6.25v24.25h16.25v8.75h-18.75a6.25 6.25 0 0 1-6.25-6.25v-24.25h-5.813v-8.75zm63.563 6.25v6.5h-8.75v-4h-6.876v30.5h-8.75v-30.5h-6.874v4h-8.75v-6.5a6.25 6.25 0 0 1 6.25-6.25h27.5a6.25 6.25 0 0 1 6.25 6.25z"
clip-rule="evenodd"
/><path
fill="#239AFF"
d="M35.319 102.906l-8.138-5.812c2.39-3.347 4.857-5.936 7.452-7.753 2.884-2.018 5.948-3.091 9.117-3.091 2.942 0 5.491.714 7.768 2.08a17.622 17.622 0 0 1 2.615 1.94c.589.518 1.009.926 1.903 1.82 1.355 1.354 1.917 1.851 2.591 2.255.731.439 1.503.655 2.623.655 1.121 0 1.896-.217 2.631-.657.677-.405 1.245-.905 2.6-2.257l.012-.012c.89-.888 1.314-1.299 1.902-1.817a17.643 17.643 0 0 1 2.61-1.933c2.273-1.362 4.814-2.074 7.745-2.074s5.472.712 7.745 2.074c.916.55 1.758 1.183 2.61 1.933.589.518 1.013.929 1.902 1.817l.013.012c1.354 1.352 1.922 1.852 2.599 2.257.735.44 1.51.657 2.631.657.998 0 2.1-.386 3.383-1.284 1.572-1.1 3.272-2.886 5.048-5.372l8.138 5.812c-2.391 3.347-4.857 5.936-7.452 7.753-2.884 2.018-5.948 3.091-9.117 3.091-2.941 0-5.49-.713-7.769-2.078a17.627 17.627 0 0 1-2.619-1.938c-.59-.519-1.015-.93-1.906-1.82l-.013-.013c-1.351-1.348-1.917-1.846-2.59-2.25-.728-.436-1.494-.651-2.603-.651-1.109 0-1.875.215-2.603.651-.673.404-1.239.902-2.59 2.25l-.012.013c-.892.89-1.317 1.301-1.907 1.82-.855.752-1.7 1.388-2.62 1.938C66.74 104.287 64.192 105 61.25 105c-2.942 0-5.49-.714-7.768-2.08a17.654 17.654 0 0 1-2.615-1.939c-.588-.519-1.009-.927-1.902-1.82-1.355-1.355-1.918-1.852-2.592-2.256-.731-.439-1.503-.655-2.623-.655-.998 0-2.1.386-3.383 1.284-1.572 1.1-3.272 2.886-5.048 5.372z"
/></g
><defs><clipPath id="clip0"><path fill="#fff" d="M0 0h140v140H0z" /></clipPath></defs></svg
>

View File

@@ -1,127 +1,263 @@
const defaultBuildAndDeploy = { function defaultBuildAndDeploy(packageManager) {
installCommand: 'yarn install', return {
buildCommand: 'yarn build', installCommand:
startCommand: 'yarn start' packageManager === 'npm' ? `${packageManager} install` : `${packageManager} install`,
}; buildCommand:
export const buildPacks = [ packageManager === 'npm' ? `${packageManager} run build` : `${packageManager} build`,
{ startCommand:
packageManager === 'npm' ? `${packageManager} run start` : `${packageManager} start`
};
}
export function findBuildPack(pack, packageManager = 'npm') {
const metaData = buildPacks.find((b) => b.name === pack);
if (pack === 'node') {
return {
...metaData,
...defaultBuildAndDeploy(packageManager),
buildCommand: null,
startCommand: null,
publishDirectory: null,
port: null
};
}
if (pack === 'static') {
return {
...metaData,
installCommand: null,
buildCommand: null,
startCommand: null,
publishDirectory: null,
port: 80
};
}
if (pack === 'docker') {
return {
...metaData,
installCommand: null,
buildCommand: null,
startCommand: null,
publishDirectory: null,
port: null
};
}
if (pack === 'svelte') {
return {
...metaData,
...defaultBuildAndDeploy(packageManager),
publishDirectory: 'public',
port: 80
};
}
if (pack === 'nestjs') {
return {
...metaData,
...defaultBuildAndDeploy(packageManager),
startCommand:
packageManager === 'npm' ? 'npm run start:prod' : `${packageManager} run start:prod`,
publishDirectory: null,
port: 3000
};
}
if (pack === 'react') {
return {
...metaData,
...defaultBuildAndDeploy(packageManager),
publishDirectory: 'build',
port: 80
};
}
if (pack === 'nextjs') {
return {
...metaData,
...defaultBuildAndDeploy(packageManager),
publishDirectory: null,
port: 3000
};
}
if (pack === 'gatsby') {
return {
...metaData,
...defaultBuildAndDeploy(packageManager),
publishDirectory: 'public',
port: 80
};
}
if (pack === 'vuejs') {
return {
...metaData,
...defaultBuildAndDeploy(packageManager),
publishDirectory: 'dist',
port: 80
};
}
if (pack === 'nuxtjs') {
return {
...metaData,
...defaultBuildAndDeploy(packageManager),
publishDirectory: null,
port: 3000
};
}
if (pack === 'preact') {
return {
...metaData,
...defaultBuildAndDeploy(packageManager),
publishDirectory: 'build',
port: 80
};
}
if (pack === 'php') {
return {
...metaData,
installCommand: null,
buildCommand: null,
startCommand: null,
publishDirectory: null,
port: 80
};
}
if (pack === 'rust') {
return {
...metaData,
installCommand: null,
buildCommand: null,
startCommand: null,
publishDirectory: null,
port: 3000
};
}
if (pack === 'astro') {
return {
...metaData,
installCommand: `yarn install`,
buildCommand: `yarn build`,
startCommand: null,
publishDirectory: `dist`,
port: 80
};
}
if (pack === 'eleventy') {
return {
...metaData,
installCommand: `yarn install`,
buildCommand: `yarn build`,
startCommand: null,
publishDirectory: `_site`,
port: 80
};
}
return {
name: 'node', name: 'node',
fancyName: 'Node.js',
hoverColor: 'hover:bg-green-700',
color: 'bg-green-700',
installCommand: null, installCommand: null,
buildCommand: null, buildCommand: null,
startCommand: null, startCommand: null,
publishDirectory: null, publishDirectory: null,
port: null, port: null
};
}
export const buildPacks = [
{
name: 'node',
fancyName: 'Node.js', fancyName: 'Node.js',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700' color: 'bg-green-700'
}, },
{ {
name: 'static', name: 'static',
...defaultBuildAndDeploy,
publishDirectory: 'dist',
port: 80,
fancyName: 'Static', fancyName: 'Static',
hoverColor: 'hover:bg-orange-700', hoverColor: 'hover:bg-orange-700',
color: 'bg-orange-700' color: 'bg-orange-700'
}, },
{ {
name: 'docker', name: 'docker',
installCommand: null,
buildCommand: null,
startCommand: null,
publishDirectory: null,
port: null,
fancyName: 'Docker', fancyName: 'Docker',
hoverColor: 'hover:bg-sky-700', hoverColor: 'hover:bg-sky-700',
color: 'bg-sky-700' color: 'bg-sky-700'
}, },
{
name: 'php',
fancyName: 'PHP',
hoverColor: 'hover:bg-indigo-700',
color: 'bg-indigo-700'
},
{ {
name: 'svelte', name: 'svelte',
...defaultBuildAndDeploy,
publishDirectory: 'public',
port: 80,
fancyName: 'Svelte', fancyName: 'Svelte',
hoverColor: 'hover:bg-orange-700', hoverColor: 'hover:bg-orange-700',
color: 'bg-orange-700' color: 'bg-orange-700'
}, },
{
name: 'nestjs',
...defaultBuildAndDeploy,
startCommand: 'yarn start:prod',
port: 3000,
fancyName: 'NestJS',
hoverColor: 'hover:bg-red-700',
color: 'bg-red-700'
},
{
name: 'react',
...defaultBuildAndDeploy,
publishDirectory: 'build',
port: 80,
fancyName: 'React',
hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700'
},
{
name: 'nextjs',
...defaultBuildAndDeploy,
port: 3000,
fancyName: 'NextJS',
hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700'
},
{
name: 'gatsby',
...defaultBuildAndDeploy,
publishDirectory: 'public',
port: 80,
fancyName: 'Gatsby',
hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700'
},
{ {
name: 'vuejs', name: 'vuejs',
...defaultBuildAndDeploy,
publishDirectory: 'dist',
port: 80,
fancyName: 'VueJS', fancyName: 'VueJS',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700' color: 'bg-green-700'
}, },
{ {
name: 'nuxtjs', name: 'nuxtjs',
...defaultBuildAndDeploy,
port: 3000,
fancyName: 'NuxtJS', fancyName: 'NuxtJS',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700' color: 'bg-green-700'
}, },
{
name: 'gatsby',
fancyName: 'Gatsby',
hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700'
},
{
name: 'astro',
fancyName: 'Astro',
hoverColor: 'hover:bg-pink-700',
color: 'bg-pink-700'
},
{
name: 'eleventy',
fancyName: 'Eleventy',
hoverColor: 'hover:bg-red-700',
color: 'bg-red-700'
},
{
name: 'react',
fancyName: 'React',
hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700'
},
{ {
name: 'preact', name: 'preact',
...defaultBuildAndDeploy,
publishDirectory: 'build',
port: 80,
fancyName: 'Preact', fancyName: 'Preact',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700' color: 'bg-blue-700'
}, },
{ {
name: 'php', name: 'nextjs',
port: 80, fancyName: 'NextJS',
fancyName: 'PHP', hoverColor: 'hover:bg-blue-700',
hoverColor: 'hover:bg-indigo-700', color: 'bg-blue-700'
color: 'bg-indigo-700' },
{
name: 'nestjs',
fancyName: 'NestJS',
hoverColor: 'hover:bg-red-700',
color: 'bg-red-700'
}, },
{ {
name: 'rust', name: 'rust',
port: 3000,
fancyName: 'Rust', fancyName: 'Rust',
hoverColor: 'hover:bg-pink-700', hoverColor: 'hover:bg-pink-700',
color: 'bg-pink-700' color: 'bg-pink-700'
} }
]; ];
export const scanningTemplates = { export const scanningTemplates = {
astro: {
buildPack: 'astro'
},
'@11ty/eleventy': {
buildPack: 'eleventy'
},
svelte: { svelte: {
buildPack: 'svelte' buildPack: 'svelte'
}, },

View File

@@ -1,5 +1,4 @@
import { decrypt, encrypt } from '$lib/crypto'; import { decrypt, encrypt } from '$lib/crypto';
import { removeProxyConfiguration, removeWwwRedirection } from '$lib/haproxy';
import { asyncExecShell, getEngine } from '$lib/common'; import { asyncExecShell, getEngine } from '$lib/common';
import { getDomain, removeDestinationDocker } from '$lib/common'; import { getDomain, removeDestinationDocker } from '$lib/common';
@@ -59,29 +58,21 @@ export async function removeApplication({ id, teamId }) {
const id = containerObj.ID; const id = containerObj.ID;
const preview = containerObj.Image.split('-')[1]; const preview = containerObj.Image.split('-')[1];
await removeDestinationDocker({ id, engine: destinationDocker.engine }); await removeDestinationDocker({ id, engine: destinationDocker.engine });
try {
if (preview) {
await removeProxyConfiguration({ domain: `${preview}.${domain}` });
} else {
await removeProxyConfiguration({ domain });
}
} catch (error) {
console.log(error);
}
} }
} }
} }
await prisma.applicationSettings.deleteMany({ where: { application: { id } } }); await prisma.applicationSettings.deleteMany({ where: { application: { id } } });
await prisma.buildLog.deleteMany({ where: { applicationId: id } }); await prisma.buildLog.deleteMany({ where: { applicationId: id } });
await prisma.build.deleteMany({ where: { applicationId: id } });
await prisma.secret.deleteMany({ where: { applicationId: id } }); await prisma.secret.deleteMany({ where: { applicationId: id } });
await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } }); await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } });
} }
export async function getApplicationWebhook({ projectId, branch }) { export async function getApplicationWebhook({ projectId, branch }) {
try { try {
let body = await prisma.application.findFirst({ let application = await prisma.application.findFirst({
where: { projectId, branch }, where: { projectId, branch, settings: { autodeploy: true } },
include: { include: {
destinationDocker: true, destinationDocker: true,
settings: true, settings: true,
@@ -89,37 +80,49 @@ export async function getApplicationWebhook({ projectId, branch }) {
secrets: true secrets: true
} }
}); });
if (!application) {
if (body.gitSource?.githubApp?.clientSecret) { return null;
body.gitSource.githubApp.clientSecret = decrypt(body.gitSource.githubApp.clientSecret);
} }
if (body.gitSource?.githubApp?.webhookSecret) { if (application?.gitSource?.githubApp?.clientSecret) {
body.gitSource.githubApp.webhookSecret = decrypt(body.gitSource.githubApp.webhookSecret); application.gitSource.githubApp.clientSecret = decrypt(
application.gitSource.githubApp.clientSecret
);
} }
if (body.gitSource?.githubApp?.privateKey) { if (application?.gitSource?.githubApp?.webhookSecret) {
body.gitSource.githubApp.privateKey = decrypt(body.gitSource.githubApp.privateKey); application.gitSource.githubApp.webhookSecret = decrypt(
application.gitSource.githubApp.webhookSecret
);
} }
if (body?.gitSource?.gitlabApp?.appSecret) { if (application?.gitSource?.githubApp?.privateKey) {
body.gitSource.gitlabApp.appSecret = decrypt(body.gitSource.gitlabApp.appSecret); application.gitSource.githubApp.privateKey = decrypt(
application.gitSource.githubApp.privateKey
);
} }
if (body?.gitSource?.gitlabApp?.webhookToken) { if (application?.gitSource?.gitlabApp?.appSecret) {
body.gitSource.gitlabApp.webhookToken = decrypt(body.gitSource.gitlabApp.webhookToken); application.gitSource.gitlabApp.appSecret = decrypt(
application.gitSource.gitlabApp.appSecret
);
} }
if (body?.secrets.length > 0) { if (application?.gitSource?.gitlabApp?.webhookToken) {
body.secrets = body.secrets.map((s) => { application.gitSource.gitlabApp.webhookToken = decrypt(
application.gitSource.gitlabApp.webhookToken
);
}
if (application?.secrets.length > 0) {
application.secrets = application.secrets.map((s) => {
s.value = decrypt(s.value); s.value = decrypt(s.value);
return s; return s;
}); });
} }
return { ...application };
return { ...body };
} catch (e) { } catch (e) {
throw { status: 404, body: { message: e.message } }; throw { status: 404, body: { message: e.message } };
} }
} }
export async function getApplicationById({ id }) { export async function getApplicationById({ id }) {
const body = await prisma.application.findFirst({ const body = await prisma.application.findFirst({
where: { id } where: { id },
include: { destinationDocker: true }
}); });
return { ...body }; return { ...body };
@@ -135,13 +138,13 @@ export async function getApplication({ id, teamId }) {
} }
}); });
if (body.gitSource?.githubApp?.clientSecret) { if (body?.gitSource?.githubApp?.clientSecret) {
body.gitSource.githubApp.clientSecret = decrypt(body.gitSource.githubApp.clientSecret); body.gitSource.githubApp.clientSecret = decrypt(body.gitSource.githubApp.clientSecret);
} }
if (body.gitSource?.githubApp?.webhookSecret) { if (body?.gitSource?.githubApp?.webhookSecret) {
body.gitSource.githubApp.webhookSecret = decrypt(body.gitSource.githubApp.webhookSecret); body.gitSource.githubApp.webhookSecret = decrypt(body.gitSource.githubApp.webhookSecret);
} }
if (body.gitSource?.githubApp?.privateKey) { if (body?.gitSource?.githubApp?.privateKey) {
body.gitSource.githubApp.privateKey = decrypt(body.gitSource.githubApp.privateKey); body.gitSource.githubApp.privateKey = decrypt(body.gitSource.githubApp.privateKey);
} }
if (body?.gitSource?.gitlabApp?.appSecret) { if (body?.gitSource?.gitlabApp?.appSecret) {
@@ -153,28 +156,48 @@ export async function getApplication({ id, teamId }) {
return s; return s;
}); });
} }
if (body?.phpModules) {
body.phpModules = body.phpModules.split(',');
}
return { ...body }; return { ...body };
} }
export async function configureGitRepository({ id, repository, branch, projectId, webhookToken }) { export async function configureGitRepository({
id,
repository,
branch,
projectId,
webhookToken,
autodeploy
}) {
if (webhookToken) { if (webhookToken) {
const encryptedWebhookToken = encrypt(webhookToken); const encryptedWebhookToken = encrypt(webhookToken);
return await prisma.application.update({ await prisma.application.update({
where: { id }, where: { id },
data: { data: {
repository, repository,
branch, branch,
projectId, projectId,
gitSource: { update: { gitlabApp: { update: { webhookToken: encryptedWebhookToken } } } } gitSource: { update: { gitlabApp: { update: { webhookToken: encryptedWebhookToken } } } },
settings: { update: { autodeploy } }
} }
}); });
} else { } else {
return await prisma.application.update({ await prisma.application.update({
where: { id }, where: { id },
data: { repository, branch, projectId } data: { repository, branch, projectId, settings: { update: { autodeploy } } }
}); });
} }
if (!autodeploy) {
const applications = await prisma.application.findMany({ where: { branch, projectId } });
for (const application of applications) {
await prisma.applicationSettings.updateMany({
where: { applicationId: application.id },
data: { autodeploy: false }
});
}
}
} }
export async function configureBuildPack({ id, buildPack }) { export async function configureBuildPack({ id, buildPack }) {
@@ -191,7 +214,8 @@ export async function configureApplication({
buildCommand, buildCommand,
startCommand, startCommand,
baseDirectory, baseDirectory,
publishDirectory publishDirectory,
phpModules
}) { }) {
return await prisma.application.update({ return await prisma.application.update({
where: { id }, where: { id },
@@ -204,15 +228,20 @@ export async function configureApplication({
startCommand, startCommand,
baseDirectory, baseDirectory,
publishDirectory, publishDirectory,
name name,
phpModules
} }
}); });
} }
export async function setApplicationSettings({ id, debug, previews }) { export async function checkDoubleBranch(branch, projectId) {
const applications = await prisma.application.findMany({ where: { branch, projectId } });
return applications.length > 1;
}
export async function setApplicationSettings({ id, debug, previews, dualCerts, autodeploy }) {
return await prisma.application.update({ return await prisma.application.update({
where: { id }, where: { id },
data: { settings: { update: { debug, previews } } }, data: { settings: { update: { debug, previews, dualCerts, autodeploy } } },
include: { destinationDocker: true } include: { destinationDocker: true }
}); });
} }

View File

@@ -15,22 +15,44 @@ export async function isDockerNetworkExists({ network }) {
return await prisma.destinationDocker.findFirst({ where: { network } }); return await prisma.destinationDocker.findFirst({ where: { network } });
} }
export async function isSecretExists({ id, name }) { export async function isServiceSecretExists({ id, name }) {
return await prisma.secret.findFirst({ where: { name, applicationId: id } }); return await prisma.serviceSecret.findFirst({ where: { name, serviceId: id } });
}
export async function isSecretExists({ id, name, isPRMRSecret }) {
return await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } });
} }
export async function isDomainConfigured({ id, fqdn }) { export async function isDomainConfigured({ id, fqdn }) {
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace('www.', '');
const foundApp = await prisma.application.findFirst({ const foundApp = await prisma.application.findFirst({
where: { fqdn: { endsWith: `//${domain}` }, id: { not: id } }, where: {
OR: [
{ fqdn: { endsWith: `//${nakedDomain}` } },
{ fqdn: { endsWith: `//www.${nakedDomain}` } }
],
id: { not: id }
},
select: { fqdn: true } select: { fqdn: true }
}); });
const foundService = await prisma.service.findFirst({ const foundService = await prisma.service.findFirst({
where: { fqdn: { endsWith: `//${domain}` }, id: { not: id } }, where: {
OR: [
{ fqdn: { endsWith: `//${nakedDomain}` } },
{ fqdn: { endsWith: `//www.${nakedDomain}` } }
],
id: { not: id }
},
select: { fqdn: true } select: { fqdn: true }
}); });
const coolifyFqdn = await prisma.setting.findFirst({ const coolifyFqdn = await prisma.setting.findFirst({
where: { fqdn: { endsWith: `//${domain}` }, id: { not: id } }, where: {
OR: [
{ fqdn: { endsWith: `//${nakedDomain}` } },
{ fqdn: { endsWith: `//www.${nakedDomain}` } }
],
id: { not: id }
},
select: { fqdn: true } select: { fqdn: true }
}); });
if (foundApp || foundService || coolifyFqdn) return true; if (foundApp || foundService || coolifyFqdn) return true;

View File

@@ -2,6 +2,7 @@ import { dev } from '$app/env';
import { sentry } from '$lib/common'; import { sentry } from '$lib/common';
import * as Prisma from '@prisma/client'; import * as Prisma from '@prisma/client';
import { default as ProdPrisma } from '@prisma/client'; import { default as ProdPrisma } from '@prisma/client';
import type { PrismaClientOptions } from '@prisma/client/runtime';
import generator from 'generate-password'; import generator from 'generate-password';
import forge from 'node-forge'; import forge from 'node-forge';
@@ -19,28 +20,20 @@ if (!dev) {
PrismaClient = ProdPrisma.PrismaClient; PrismaClient = ProdPrisma.PrismaClient;
P = ProdPrisma.Prisma; P = ProdPrisma.Prisma;
} }
let prismaOptions = {
export const prisma = new PrismaClient({
errorFormat: 'pretty',
rejectOnNotFound: false rejectOnNotFound: false
}; });
if (dev) {
prismaOptions = {
errorFormat: 'pretty',
rejectOnNotFound: false,
log: [
{
emit: 'event',
level: 'query'
}
]
};
}
export const prisma = new PrismaClient(prismaOptions);
export function ErrorHandler(e) { export function ErrorHandler(e) {
if (e! instanceof Error) { if (e! instanceof Error) {
e = new Error(e.toString()); e = new Error(e.toString());
} }
let truncatedError = e; let truncatedError = e;
if (e.stdout) {
truncatedError = e.stdout;
}
if (e.message?.includes('docker run')) { if (e.message?.includes('docker run')) {
let truncatedArray = []; let truncatedArray = [];
truncatedArray = truncatedError.message.split('-').filter((line) => { truncatedArray = truncatedError.message.split('-').filter((line) => {
@@ -163,6 +156,15 @@ export const supportedServiceTypesAndVersions = [
ports: { ports: {
main: 80 main: 80
} }
},
{
name: 'languagetool',
fancyName: 'LanguageTool',
baseImage: 'silviof/docker-languagetool',
versions: ['latest'],
ports: {
main: 8010
}
} }
]; ];

View File

@@ -1,9 +1,9 @@
import { decrypt, encrypt } from '$lib/crypto'; import { decrypt, encrypt } from '$lib/crypto';
import { dockerInstance } from '$lib/docker'; import * as db from '$lib/database';
import cuid from 'cuid'; import cuid from 'cuid';
import { generatePassword } from '.'; import { generatePassword } from '.';
import { prisma, ErrorHandler } from './common'; import { prisma, ErrorHandler } from './common';
import getPort from 'get-port'; import getPort, { portNumbers } from 'get-port';
import { asyncExecShell, getEngine, removeContainer } from '$lib/common'; import { asyncExecShell, getEngine, removeContainer } from '$lib/common';
export async function listDatabases(teamId) { export async function listDatabases(teamId) {
@@ -16,24 +16,9 @@ export async function newDatabase({ name, teamId }) {
const rootUserPassword = encrypt(generatePassword()); const rootUserPassword = encrypt(generatePassword());
const defaultDatabase = cuid(); const defaultDatabase = cuid();
let publicPort = await getPort();
let i = 0;
do {
const usedPorts = await prisma.database.findMany({ where: { publicPort } });
if (usedPorts.length === 0) break;
publicPort = await getPort();
i++;
} while (i < 10);
if (i === 9) {
throw {
error: 'No free port found!? Is it possible?'
};
}
return await prisma.database.create({ return await prisma.database.create({
data: { data: {
name, name,
publicPort,
defaultDatabase, defaultDatabase,
dbUser, dbUser,
dbUserPassword, dbUserPassword,

View File

@@ -1,4 +1,5 @@
import { asyncExecShell, getEngine } from '$lib/common'; import { asyncExecShell, getEngine } from '$lib/common';
import { decrypt, encrypt } from '$lib/crypto';
import { dockerInstance } from '$lib/docker'; import { dockerInstance } from '$lib/docker';
import { startCoolifyProxy } from '$lib/haproxy'; import { startCoolifyProxy } from '$lib/haproxy';
import { getDatabaseImage } from '.'; import { getDatabaseImage } from '.';
@@ -47,7 +48,36 @@ export async function updateDestination({ id, name, engine, network }) {
return await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } }); return await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } });
} }
export async function newDestination({ name, teamId, engine, network, isCoolifyProxyUsed }) { export async function newRemoteDestination({
name,
teamId,
engine,
network,
isCoolifyProxyUsed,
remoteEngine,
ipAddress,
user,
port,
sshPrivateKey
}) {
const encryptedPrivateKey = encrypt(sshPrivateKey);
const destination = await prisma.destinationDocker.create({
data: {
name,
teams: { connect: { id: teamId } },
engine,
network,
isCoolifyProxyUsed,
remoteEngine,
ipAddress,
user,
port,
sshPrivateKey: encryptedPrivateKey
}
});
return destination.id;
}
export async function newLocalDestination({ name, teamId, engine, network, isCoolifyProxyUsed }) {
const host = getEngine(engine); const host = getEngine(engine);
const docker = dockerInstance({ destinationDocker: { engine, network } }); const docker = dockerInstance({ destinationDocker: { engine, network } });
const found = await docker.engine.listNetworks({ filters: { name: [`^${network}$`] } }); const found = await docker.engine.listNetworks({ filters: { name: [`^${network}$`] } });
@@ -94,9 +124,13 @@ export async function removeDestination({ id }) {
} }
export async function getDestination({ id, teamId }) { export async function getDestination({ id, teamId }) {
return await prisma.destinationDocker.findFirst({ let destination = await prisma.destinationDocker.findFirst({
where: { id, teams: { some: { id: teamId } } } where: { id, teams: { some: { id: teamId } } }
}); });
if (destination.remoteEngine) {
destination.sshPrivateKey = decrypt(destination.sshPrivateKey);
}
return destination;
} }
export async function getDestinationByApplicationId({ id, teamId }) { export async function getDestinationByApplicationId({ id, teamId }) {
return await prisma.destinationDocker.findFirst({ return await prisma.destinationDocker.findFirst({

View File

@@ -1,19 +1,78 @@
import { encrypt } from '$lib/crypto'; import { encrypt, decrypt } from '$lib/crypto';
import { prisma } from './common'; import { prisma } from './common';
export async function listSecrets({ applicationId }) { export async function listServiceSecrets(serviceId: string) {
return await prisma.secret.findMany({ let secrets = await prisma.serviceSecret.findMany({
where: { serviceId },
orderBy: { createdAt: 'desc' }
});
secrets = secrets.map((secret) => {
secret.value = decrypt(secret.value);
return secret;
});
return secrets;
}
export async function listSecrets(applicationId: string) {
let secrets = await prisma.secret.findMany({
where: { applicationId }, where: { applicationId },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' }
select: { id: true, createdAt: true, name: true, isBuildSecret: true } });
secrets = secrets.map((secret) => {
secret.value = decrypt(secret.value);
return secret;
});
return secrets;
}
export async function createServiceSecret({ id, name, value }) {
value = encrypt(value);
return await prisma.serviceSecret.create({
data: { name, value, service: { connect: { id } } }
});
}
export async function createSecret({ id, name, value, isBuildSecret, isPRMRSecret }) {
value = encrypt(value);
return await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
}); });
} }
export async function createSecret({ id, name, value, isBuildSecret }) { export async function updateServiceSecret({ id, name, value }) {
value = encrypt(value); value = encrypt(value);
return await prisma.secret.create({ const found = await prisma.serviceSecret.findFirst({ where: { serviceId: id, name } });
data: { name, value, isBuildSecret, application: { connect: { id } } }
}); if (found) {
return await prisma.serviceSecret.updateMany({
where: { serviceId: id, name },
data: { value }
});
} else {
return await prisma.serviceSecret.create({
data: { name, value, service: { connect: { id } } }
});
}
}
export async function updateSecret({ id, name, value, isBuildSecret, isPRMRSecret }) {
value = encrypt(value);
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
if (found) {
return await prisma.secret.updateMany({
where: { applicationId: id, name, isPRMRSecret },
data: { value, isBuildSecret, isPRMRSecret }
});
} else {
return await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
});
}
}
export async function removeServiceSecret({ id, name }) {
return await prisma.serviceSecret.deleteMany({ where: { serviceId: id, name } });
} }
export async function removeSecret({ id, name }) { export async function removeSecret({ id, name }) {

View File

@@ -19,7 +19,8 @@ export async function getService({ id, teamId }) {
plausibleAnalytics: true, plausibleAnalytics: true,
minio: true, minio: true,
vscodeserver: true, vscodeserver: true,
wordpress: true wordpress: true,
serviceSecret: true
} }
}); });
@@ -42,6 +43,12 @@ export async function getService({ id, teamId }) {
if (body.wordpress?.mysqlRootUserPassword) if (body.wordpress?.mysqlRootUserPassword)
body.wordpress.mysqlRootUserPassword = decrypt(body.wordpress.mysqlRootUserPassword); body.wordpress.mysqlRootUserPassword = decrypt(body.wordpress.mysqlRootUserPassword);
if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value);
return s;
});
}
return { ...body }; return { ...body };
} }
@@ -105,15 +112,29 @@ export async function configureServiceType({ id, type }) {
type type
} }
}); });
} else if (type === 'languagetool') {
await prisma.service.update({
where: { id },
data: {
type
}
});
} }
} }
export async function setService({ id, version }) { export async function setServiceVersion({ id, version }) {
return await prisma.service.update({ return await prisma.service.update({
where: { id }, where: { id },
data: { version } data: { version }
}); });
} }
export async function setServiceSettings({ id, dualCerts }) {
return await prisma.service.update({
where: { id },
data: { dualCerts }
});
}
export async function updatePlausibleAnalyticsService({ id, fqdn, email, username, name }) { export async function updatePlausibleAnalyticsService({ id, fqdn, email, username, name }) {
await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } }); await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } });
await prisma.service.update({ where: { id }, data: { name, fqdn } }); await prisma.service.update({ where: { id }, data: { name, fqdn } });
@@ -121,6 +142,9 @@ export async function updatePlausibleAnalyticsService({ id, fqdn, email, usernam
export async function updateNocoDbOrMinioService({ id, fqdn, name }) { export async function updateNocoDbOrMinioService({ id, fqdn, name }) {
return await prisma.service.update({ where: { id }, data: { fqdn, name } }); return await prisma.service.update({ where: { id }, data: { fqdn, name } });
} }
export async function updateLanguageToolService({ id, fqdn, name }) {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateVaultWardenService({ id, fqdn, name }) { export async function updateVaultWardenService({ id, fqdn, name }) {
return await prisma.service.update({ where: { id }, data: { fqdn, name } }); return await prisma.service.update({ where: { id }, data: { fqdn, name } });
} }
@@ -142,5 +166,7 @@ export async function removeService({ id }) {
await prisma.minio.deleteMany({ where: { serviceId: id } }); await prisma.minio.deleteMany({ where: { serviceId: id } });
await prisma.vscodeserver.deleteMany({ where: { serviceId: id } }); await prisma.vscodeserver.deleteMany({ where: { serviceId: id } });
await prisma.wordpress.deleteMany({ where: { serviceId: id } }); await prisma.wordpress.deleteMany({ where: { serviceId: id } });
await prisma.serviceSecret.deleteMany({ where: { serviceId: id } });
await prisma.service.delete({ where: { id } }); await prisma.service.delete({ where: { id } });
} }

View File

@@ -6,19 +6,28 @@ import { asyncExecShell, uniqueName } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { startCoolifyProxy } from '$lib/haproxy'; import { startCoolifyProxy } from '$lib/haproxy';
export async function hashPassword(password: string) {
export async function login({ email, password }) {
const saltRounds = 15; const saltRounds = 15;
return bcrypt.hash(password, saltRounds);
}
export async function login({ email, password, isLogin }) {
const users = await prisma.user.count(); const users = await prisma.user.count();
const userFound = await prisma.user.findUnique({ const userFound = await prisma.user.findUnique({
where: { email }, where: { email },
include: { teams: true }, include: { teams: true, permission: true },
rejectOnNotFound: false rejectOnNotFound: false
}); });
if (!userFound && isLogin) {
throw {
error: 'Wrong password or email address.'
};
}
// Registration disabled if database is not seeded properly // Registration disabled if database is not seeded properly
const { isRegistrationEnabled, id } = await db.listSettings(); const { isRegistrationEnabled, id } = await db.listSettings();
let uid = cuid(); let uid = cuid();
let permission = 'read';
let isAdmin = false;
// Disable registration if we are registering the first user. // Disable registration if we are registering the first user.
if (users === 0) { if (users === 0) {
await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } }); await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } });
@@ -50,6 +59,8 @@ export async function login({ email, password }) {
}; };
} }
uid = userFound.id; uid = userFound.id;
// permission = userFound.permission;
isAdmin = true;
} }
} else { } else {
// If registration disabled, return 403 // If registration disabled, return 403
@@ -59,8 +70,10 @@ export async function login({ email, password }) {
}; };
} }
const hashedPassword = await bcrypt.hash(password, saltRounds); const hashedPassword = await hashPassword(password);
if (users === 0) { if (users === 0) {
permission = 'owner';
isAdmin = true;
await prisma.user.create({ await prisma.user.create({
data: { data: {
id: uid, id: uid,
@@ -103,8 +116,10 @@ export async function login({ email, password }) {
'Set-Cookie': `teamId=${uid}; HttpOnly; Path=/; Max-Age=15778800;` 'Set-Cookie': `teamId=${uid}; HttpOnly; Path=/; Max-Age=15778800;`
}, },
body: { body: {
uid, userId: uid,
teamId: uid teamId: uid,
permission,
isAdmin
} }
}; };
} }

View File

@@ -1,5 +1,6 @@
import Dockerode from 'dockerode'; import Dockerode from 'dockerode';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { checkPnpm } from './buildPacks/common';
import { saveBuildLog } from './common'; import { saveBuildLog } from './common';
export async function buildCacheImageWithNode(data, imageForBuild) { export async function buildCacheImageWithNode(data, imageForBuild) {
@@ -13,32 +14,37 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
installCommand, installCommand,
buildCommand, buildCommand,
debug, debug,
secrets secrets,
pullmergeRequestId
} = data; } = data;
const isPnpm = checkPnpm(installCommand, buildCommand);
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`); Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push('WORKDIR /usr/src/app');
Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (!secret.isBuildSecret) { if (secret.isBuildSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`); if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
}
} }
}); });
} }
// TODO: If build command defined, install command should be the default yarn install if (isPnpm) {
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm');
Dockerfile.push('RUN pnpm add -g pnpm');
}
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
if (installCommand) { if (installCommand) {
Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`);
try {
await fs.stat(`${workdir}/yarn.lock`);
Dockerfile.push(`COPY ./${baseDirectory || ''}yarn.lock ./`);
} catch (error) {}
try {
await fs.stat(`${workdir}/pnpm-lock.yaml`);
Dockerfile.push(`COPY ./${baseDirectory || ''}pnpm-lock.yaml ./`);
} catch (error) {}
Dockerfile.push(`RUN ${installCommand}`); Dockerfile.push(`RUN ${installCommand}`);
} }
Dockerfile.push(`COPY ./${baseDirectory || ''} ./`);
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug }); await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });

View File

@@ -0,0 +1,265 @@
import { dev } from '$app/env';
import got from 'got';
import mustache from 'mustache';
import crypto from 'crypto';
import * as db from '$lib/database';
import { checkContainer, checkHAProxy } from '.';
import { asyncExecShell, getDomain, getEngine } from '$lib/common';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
let template = `program api
command /usr/bin/dataplaneapi -f /usr/local/etc/haproxy/dataplaneapi.hcl --userlist haproxy-dataplaneapi
no option start-on-reload
global
stats socket /var/run/api.sock user haproxy group haproxy mode 660 level admin expose-fd listeners
log stdout format raw local0 debug
defaults
mode http
log global
timeout http-request 60s
timeout connect 10s
timeout client 60s
timeout server 60s
userlist haproxy-dataplaneapi
user admin insecure-password "\${HAPROXY_PASSWORD}"
frontend http
mode http
bind :80
bind :443 ssl crt /usr/local/etc/haproxy/ssl/ alpn h2,http/1.1
acl is_certbot path_beg /.well-known/acme-challenge/
{{#applications}}
{{#isHttps}}
http-request redirect scheme https code ${
dev ? 302 : 301
} if { hdr(host) -i {{domain}} } !{ ssl_fc }
{{/isHttps}}
http-request redirect location {{{redirectValue}}} code ${
dev ? 302 : 301
} if { req.hdr(host) -i {{redirectTo}} }
{{/applications}}
{{#services}}
{{#isHttps}}
http-request redirect scheme https code ${
dev ? 302 : 301
} if { hdr(host) -i {{domain}} } !{ ssl_fc }
{{/isHttps}}
http-request redirect location {{{redirectValue}}} code ${
dev ? 302 : 301
} if { req.hdr(host) -i {{redirectTo}} }
{{/services}}
{{#coolify}}
{{#isHttps}}
http-request redirect scheme https code ${
dev ? 302 : 301
} if { hdr(host) -i {{domain}} } !{ ssl_fc }
{{/isHttps}}
http-request redirect location {{{redirectValue}}} code ${
dev ? 302 : 301
} if { req.hdr(host) -i {{redirectTo}} }
{{/coolify}}
use_backend backend-certbot if is_certbot
use_backend %[req.hdr(host),lower]
frontend stats
bind *:8404
stats enable
stats uri /
stats admin if TRUE
stats auth "\${HAPROXY_USERNAME}:\${HAPROXY_PASSWORD}"
backend backend-certbot
mode http
server certbot host.docker.internal:9080
{{#applications}}
{{#isRunning}}
# updatedAt={{updatedAt}}
backend {{domain}}
option forwardfor
server {{id}} {{id}}:{{port}} check
{{/isRunning}}
{{/applications}}
{{#services}}
{{#isRunning}}
# updatedAt={{updatedAt}}
backend {{domain}}
option forwardfor
server {{id}} {{id}}:{{port}} check
{{/isRunning}}
{{/services}}
{{#coolify}}
backend {{domain}}
option forwardfor
option httpchk GET /undead.json
server {{id}} {{id}}:{{port}} check fall 10
{{/coolify}}
`;
export async function haproxyInstance() {
const { proxyPassword } = await db.listSettings();
return got.extend({
prefixUrl: url,
username: 'admin',
password: proxyPassword
});
}
export async function configureHAProxy() {
const haproxy = await haproxyInstance();
try {
await checkHAProxy(haproxy);
} catch (error) {
return 'Error: HAProxy is not running';
}
try {
const data = {
applications: [],
services: [],
coolify: []
};
const applications = await db.prisma.application.findMany({
include: { destinationDocker: true, settings: true }
});
for (const application of applications) {
const {
fqdn,
id,
port,
destinationDocker,
destinationDockerId,
settings: { previews },
updatedAt
} = application;
if (destinationDockerId) {
const { engine, network } = destinationDocker;
const isRunning = await checkContainer(engine, id);
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
if (isRunning) {
data.applications.push({
id,
port: port || 3000,
domain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? domain : 'www.' + domain,
updatedAt: updatedAt.getTime()
});
}
if (previews) {
const host = getEngine(engine);
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
);
const containers = stdout
.trim()
.split('\n')
.filter((a) => a)
.map((c) => c.replace(/"/g, ''));
if (containers.length > 0) {
for (const container of containers) {
let previewDomain = `${container.split('-')[1]}.${domain}`;
data.applications.push({
id: container,
port: port || 3000,
domain: previewDomain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? previewDomain : 'www.' + previewDomain,
updatedAt: updatedAt.getTime()
});
}
}
}
}
}
}
const services = await db.prisma.service.findMany({
include: {
destinationDocker: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true
}
});
for (const service of services) {
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;
if (destinationDockerId) {
const { engine } = destinationDocker;
const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) {
const port = found.ports.main;
const publicPort = service[type]?.publicPort;
const isRunning = await checkContainer(engine, id);
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
if (isRunning) {
data.services.push({
id,
port,
publicPort,
domain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? domain : 'www.' + domain,
updatedAt: updatedAt.getTime()
});
}
}
}
}
}
const { fqdn } = await db.prisma.setting.findFirst();
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
data.coolify.push({
id: dev ? 'host.docker.internal' : 'coolify',
port: 3000,
domain,
isHttps,
redirectValue,
redirectTo: isWWW ? domain : 'www.' + domain
});
}
const output = mustache.render(template, data);
const newHash = crypto.createHash('md5').update(output).digest('hex');
const { proxyHash, id } = await db.listSettings();
if (proxyHash !== newHash) {
await db.prisma.setting.update({ where: { id }, data: { proxyHash: newHash } });
await haproxy.post(`v2/services/haproxy/configuration/raw`, {
searchParams: {
skip_version: true
},
body: output,
headers: {
'Content-Type': 'text/plain'
}
});
}
} catch (error) {
throw error;
}
}

View File

@@ -1,8 +1,7 @@
import { dev } from '$app/env'; import { dev } from '$app/env';
import { asyncExecShell, getDomain, getEngine } from '$lib/common'; import { asyncExecShell, getEngine } from '$lib/common';
import got from 'got'; import got from 'got';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { letsEncrypt } from '$lib/letsencrypt';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555'; const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
@@ -48,120 +47,14 @@ export async function completeTransaction(transactionId) {
const haproxy = await haproxyInstance(); const haproxy = await haproxyInstance();
return await haproxy.put(`v2/services/haproxy/transactions/${transactionId}`); return await haproxy.put(`v2/services/haproxy/transactions/${transactionId}`);
} }
export async function removeProxyConfiguration({ domain }) {
const haproxy = await haproxyInstance();
const backendFound = await haproxy
.get(`v2/services/haproxy/configuration/backends/${domain}`)
.json();
if (backendFound) {
const transactionId = await getNextTransactionId();
await haproxy
.delete(`v2/services/haproxy/configuration/backends/${domain}`, {
searchParams: {
transaction_id: transactionId
}
})
.json();
await completeTransaction(transactionId);
}
await removeWwwRedirection(domain);
}
export async function forceSSLOffApplication({ domain }) {
if (!dev) {
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
let transactionId;
try {
const rules: any = await haproxy
.get(`v2/services/haproxy/configuration/http_request_rules`, {
searchParams: {
parent_name: 'http',
parent_type: 'frontend'
}
})
.json();
if (rules.data.length > 0) {
const rule = rules.data.find((rule) => rule.cond_test.includes(`-i ${domain}`));
if (rule) {
transactionId = await getNextTransactionId();
await haproxy
.delete(`v2/services/haproxy/configuration/http_request_rules/${rule.index}`, {
searchParams: {
transaction_id: transactionId,
parent_name: 'http',
parent_type: 'frontend'
}
})
.json();
}
}
} catch (error) {
console.log(error);
} finally {
if (transactionId) await completeTransaction(transactionId);
}
} else {
console.log(`[DEBUG] Removing ssl for ${domain}`);
}
}
export async function forceSSLOnApplication({ domain }) {
if (!dev) {
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
let transactionId;
try {
const rules: any = await haproxy
.get(`v2/services/haproxy/configuration/http_request_rules`, {
searchParams: {
parent_name: 'http',
parent_type: 'frontend'
}
})
.json();
let nextRule = 0;
if (rules.data.length > 0) {
const rule = rules.data.find((rule) =>
rule.cond_test.includes(`{ hdr(host) -i ${domain} } !{ ssl_fc }`)
);
if (rule) return;
nextRule = rules.data[rules.data.length - 1].index + 1;
}
transactionId = await getNextTransactionId();
await haproxy
.post(`v2/services/haproxy/configuration/http_request_rules`, {
searchParams: {
transaction_id: transactionId,
parent_name: 'http',
parent_type: 'frontend'
},
json: {
index: nextRule,
cond: 'if',
cond_test: `{ hdr(host) -i ${domain} } !{ ssl_fc }`,
type: 'redirect',
redir_type: 'scheme',
redir_value: 'https',
redir_code: 301
}
})
.json();
} catch (error) {
console.log(error);
throw error;
} finally {
if (transactionId) await completeTransaction(transactionId);
}
} else {
console.log(`[DEBUG] Adding ssl for ${domain}`);
}
}
export async function deleteProxy({ id }) { export async function deleteProxy({ id }) {
const haproxy = await haproxyInstance(); const haproxy = await haproxyInstance();
await checkHAProxy(haproxy); try {
await checkHAProxy(haproxy);
} catch (error) {
return 'Error: HAProxy is not running';
}
let transactionId; let transactionId;
try { try {
await haproxy.get(`v2/services/haproxy/configuration/backends/${id}`).json(); await haproxy.get(`v2/services/haproxy/configuration/backends/${id}`).json();
@@ -182,7 +75,7 @@ export async function deleteProxy({ id }) {
}) })
.json(); .json();
} catch (error) { } catch (error) {
console.log(error.response.body); console.log(error.response?.body || error);
} finally { } finally {
if (transactionId) await completeTransaction(transactionId); if (transactionId) await completeTransaction(transactionId);
} }
@@ -192,110 +85,6 @@ export async function reloadHaproxy(engine) {
const host = getEngine(engine); const host = getEngine(engine);
return await asyncExecShell(`DOCKER_HOST=${host} docker exec coolify-haproxy kill -HUP 1`); return await asyncExecShell(`DOCKER_HOST=${host} docker exec coolify-haproxy kill -HUP 1`);
} }
export async function configureProxyForApplication({ domain, imageId, applicationId, port }) {
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
let serverConfigured = false;
let backendAvailable: any = null;
try {
backendAvailable = await haproxy
.get(`v2/services/haproxy/configuration/backends/${domain}`)
.json();
const server: any = await haproxy
.get(`v2/services/haproxy/configuration/servers/${imageId}`, {
searchParams: {
backend: domain
}
})
.json();
if (backendAvailable && server) {
// Very sophisticated way to check if the server is already configured in proxy
if (backendAvailable.data.forwardfor.enabled === 'enabled') {
if (backendAvailable.data.name === domain) {
if (server.data.check === 'enabled') {
if (server.data.address === imageId) {
if (server.data.port === port) {
serverConfigured = true;
}
}
}
}
}
}
} catch (error) {
//console.log('error getting backend or server', error?.response?.body);
//
}
if (serverConfigured) return;
const transactionId = await getNextTransactionId();
if (backendAvailable) {
await haproxy
.delete(`v2/services/haproxy/configuration/backends/${domain}`, {
searchParams: {
transaction_id: transactionId
}
})
.json();
}
try {
await haproxy.post('v2/services/haproxy/configuration/backends', {
searchParams: {
transaction_id: transactionId
},
json: {
'init-addr': 'last,libc,none',
forwardfor: { enabled: 'enabled' },
name: domain
}
});
await haproxy.post('v2/services/haproxy/configuration/servers', {
searchParams: {
transaction_id: transactionId,
backend: domain
},
json: {
address: imageId,
check: 'enabled',
name: imageId,
port: port
}
});
} catch (error) {
throw error?.response?.body || error;
} finally {
await completeTransaction(transactionId);
}
}
export async function configureCoolifyProxyOff(fqdn) {
const domain = getDomain(fqdn);
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
try {
await haproxy.get(`v2/services/haproxy/configuration/backends/${domain}`).json();
const transactionId = await getNextTransactionId();
await haproxy
.delete(`v2/services/haproxy/configuration/backends/${domain}`, {
searchParams: {
transaction_id: transactionId
}
})
.json();
await completeTransaction(transactionId);
if (!dev) {
await forceSSLOffApplication({ domain });
}
await setWwwRedirection(fqdn);
} catch (error) {
throw error?.response?.body || error;
}
}
export async function checkHAProxy(haproxy?: any) { export async function checkHAProxy(haproxy?: any) {
if (!haproxy) haproxy = await haproxyInstance(); if (!haproxy) haproxy = await haproxyInstance();
try { try {
@@ -307,76 +96,6 @@ export async function checkHAProxy(haproxy?: any) {
}; };
} }
} }
export async function configureCoolifyProxyOn(fqdn) {
const domain = getDomain(fqdn);
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
let serverConfigured = false;
let backendAvailable: any = null;
try {
backendAvailable = await haproxy
.get(`v2/services/haproxy/configuration/backends/${domain}`)
.json();
const server: any = await haproxy
.get(`v2/services/haproxy/configuration/servers/coolify`, {
searchParams: {
backend: domain
}
})
.json();
if (backendAvailable && server) {
// Very sophisticated way to check if the server is already configured in proxy
if (backendAvailable.data.forwardfor.enabled === 'enabled') {
if (backendAvailable.data.name === domain) {
if (server.data.check === 'enabled') {
if (server.data.address === dev ? 'host.docker.internal' : 'coolify') {
if (server.data.port === 3000) {
serverConfigured = true;
}
}
}
}
}
}
} catch (error) {}
if (serverConfigured) return;
const transactionId = await getNextTransactionId();
try {
await haproxy.post('v2/services/haproxy/configuration/backends', {
searchParams: {
transaction_id: transactionId
},
json: {
adv_check: 'httpchk',
httpchk_params: {
method: 'GET',
uri: '/undead.json'
},
'init-addr': 'last,libc,none',
forwardfor: { enabled: 'enabled' },
name: domain
}
});
await haproxy.post('v2/services/haproxy/configuration/servers', {
searchParams: {
transaction_id: transactionId,
backend: domain
},
json: {
address: dev ? 'host.docker.internal' : 'coolify',
check: 'enabled',
fall: 10,
name: 'coolify',
port: 3000
}
});
} catch (error) {
console.log(error);
throw error;
} finally {
await completeTransaction(transactionId);
}
}
export async function stopTcpHttpProxy(destinationDocker, publicPort) { export async function stopTcpHttpProxy(destinationDocker, publicPort) {
const { engine } = destinationDocker; const { engine } = destinationDocker;
@@ -440,7 +159,7 @@ export async function startHttpProxy(destinationDocker, id, publicPort, privateP
export async function startCoolifyProxy(engine) { export async function startCoolifyProxy(engine) {
const host = getEngine(engine); const host = getEngine(engine);
const found = await checkContainer(engine, 'coolify-haproxy'); const found = await checkContainer(engine, 'coolify-haproxy');
const { proxyPassword, proxyUser } = await db.listSettings(); const { proxyPassword, proxyUser, id } = await db.listSettings();
if (!found) { if (!found) {
const { stdout: Config } = await asyncExecShell( const { stdout: Config } = await asyncExecShell(
`DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'`
@@ -449,6 +168,7 @@ export async function startCoolifyProxy(engine) {
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST="${host}" docker run -e HAPROXY_USERNAME=${proxyUser} -e HAPROXY_PASSWORD=${proxyPassword} --restart always --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' -v coolify-ssl-certs:/usr/local/etc/haproxy/ssl --network coolify-infra -p "80:80" -p "443:443" -p "8404:8404" -p "5555:5555" -p "5000:5000" --name coolify-haproxy -d coollabsio/${defaultProxyImage}` `DOCKER_HOST="${host}" docker run -e HAPROXY_USERNAME=${proxyUser} -e HAPROXY_PASSWORD=${proxyPassword} --restart always --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' -v coolify-ssl-certs:/usr/local/etc/haproxy/ssl --network coolify-infra -p "80:80" -p "443:443" -p "8404:8404" -p "5555:5555" -p "5000:5000" --name coolify-haproxy -d coollabsio/${defaultProxyImage}`
); );
await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } });
} }
await configureNetworkCoolifyProxy(engine); await configureNetworkCoolifyProxy(engine);
} }
@@ -460,11 +180,9 @@ export async function checkContainer(engine, container) {
const { stdout } = await asyncExecShell( const { stdout } = await asyncExecShell(
`DOCKER_HOST="${host}" docker inspect --format '{{json .State}}' ${container}` `DOCKER_HOST="${host}" docker inspect --format '{{json .State}}' ${container}`
); );
const parsedStdout = JSON.parse(stdout); const parsedStdout = JSON.parse(stdout);
const status = parsedStdout.Status; const status = parsedStdout.Status;
const isRunning = parsedStdout.Running; const isRunning = status === 'running' ? true : false;
if (status === 'exited' || status === 'created') { if (status === 'exited' || status === 'created') {
await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`); await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`);
} }
@@ -481,6 +199,8 @@ export async function stopCoolifyProxy(engine) {
const host = getEngine(engine); const host = getEngine(engine);
const found = await checkContainer(engine, 'coolify-haproxy'); const found = await checkContainer(engine, 'coolify-haproxy');
await db.setDestinationSettings({ engine, isCoolifyProxyUsed: false }); await db.setDestinationSettings({ engine, isCoolifyProxyUsed: false });
const { id } = await db.prisma.setting.findFirst({});
await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } });
try { try {
if (found) { if (found) {
await asyncExecShell( await asyncExecShell(
@@ -505,165 +225,3 @@ export async function configureNetworkCoolifyProxy(engine) {
} }
}); });
} }
export async function configureSimpleServiceProxyOn({ id, domain, port }) {
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
let serverConfigured = false;
let backendAvailable: any = null;
try {
backendAvailable = await haproxy
.get(`v2/services/haproxy/configuration/backends/${domain}`)
.json();
const server: any = await haproxy
.get(`v2/services/haproxy/configuration/servers/${id}`, {
searchParams: {
backend: domain
}
})
.json();
if (backendAvailable && server) {
// Very sophisticated way to check if the server is already configured in proxy
if (backendAvailable.data.forwardfor.enabled === 'enabled') {
if (backendAvailable.data.name === domain) {
if (server.data.check === 'enabled') {
if (server.data.address === id) {
if (server.data.port === port) {
serverConfigured = true;
}
}
}
}
}
}
} catch (error) {}
if (serverConfigured) return;
const transactionId = await getNextTransactionId();
await haproxy.post('v2/services/haproxy/configuration/backends', {
searchParams: {
transaction_id: transactionId
},
json: {
'init-addr': 'last,libc,none',
forwardfor: { enabled: 'enabled' },
name: domain
}
});
await haproxy.post('v2/services/haproxy/configuration/servers', {
searchParams: {
transaction_id: transactionId,
backend: domain
},
json: {
address: id,
check: 'enabled',
name: id,
port: port
}
});
await completeTransaction(transactionId);
}
export async function configureSimpleServiceProxyOff({ domain }) {
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
try {
await haproxy.get(`v2/services/haproxy/configuration/backends/${domain}`).json();
const transactionId = await getNextTransactionId();
await haproxy
.delete(`v2/services/haproxy/configuration/backends/${domain}`, {
searchParams: {
transaction_id: transactionId
}
})
.json();
await completeTransaction(transactionId);
} catch (error) {}
await removeWwwRedirection(domain);
return;
}
export async function removeWwwRedirection(domain) {
const haproxy = await haproxyInstance();
await checkHAProxy();
const rules: any = await haproxy
.get(`v2/services/haproxy/configuration/http_request_rules`, {
searchParams: {
parent_name: 'http',
parent_type: 'frontend'
}
})
.json();
if (rules.data.length > 0) {
const rule = rules.data.find((rule) =>
rule.redir_value.includes(`${domain}%[capture.req.uri]`)
);
if (rule) {
const transactionId = await getNextTransactionId();
await haproxy
.delete(`v2/services/haproxy/configuration/http_request_rules/${rule.index}`, {
searchParams: {
transaction_id: transactionId,
parent_name: 'http',
parent_type: 'frontend'
}
})
.json();
await completeTransaction(transactionId);
}
}
}
export async function setWwwRedirection(fqdn) {
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
let transactionId;
try {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const contTest = `{ req.hdr(host) -i ${isWWW ? domain.replace('www.', '') : `www.${domain}`} }`;
const rules: any = await haproxy
.get(`v2/services/haproxy/configuration/http_request_rules`, {
searchParams: {
parent_name: 'http',
parent_type: 'frontend'
}
})
.json();
let nextRule = 0;
if (rules.data.length > 0) {
const rule = rules.data.find((rule) =>
rule.redir_value.includes(`${domain}%[capture.req.uri]`)
);
if (rule) return;
nextRule = rules.data[rules.data.length - 1].index + 1;
}
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
transactionId = await getNextTransactionId();
await haproxy
.post(`v2/services/haproxy/configuration/http_request_rules`, {
searchParams: {
transaction_id: transactionId,
parent_name: 'http',
parent_type: 'frontend'
},
json: {
index: nextRule,
cond: 'if',
cond_test: contTest,
type: 'redirect',
redir_type: 'location',
redir_value: redirectValue,
redir_code: dev ? 302 : 301
}
})
.json();
} catch (error) {
console.log(error);
throw error;
} finally {
if (transactionId) await completeTransaction(transactionId);
}
}

View File

@@ -14,7 +14,7 @@ export default async function ({
buildId buildId
}): Promise<any> { }): Promise<any> {
try { try {
saveBuildLog({ line: 'GitHub importer started', buildId, applicationId }); saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId });
const { privateKey, appId, installationId } = await db.getUniqueGithubApp({ githubAppId }); const { privateKey, appId, installationId } = await db.getUniqueGithubApp({ githubAppId });
const githubPrivateKey = privateKey.replace(/\\n/g, '\n').replace(/"/g, ''); const githubPrivateKey = privateKey.replace(/\\n/g, '\n').replace(/"/g, '');

View File

@@ -1,50 +0,0 @@
import { dev } from '$app/env';
import { forceSSLOffApplication, forceSSLOnApplication, getNextTransactionId } from '$lib/haproxy';
import { asyncExecShell, getEngine } from './common';
import * as db from '$lib/database';
import cuid from 'cuid';
export async function letsEncrypt({ domain, isCoolify = false, id = null }) {
try {
const randomCuid = cuid();
if (dev) {
return await forceSSLOnApplication({ domain });
} else {
if (isCoolify) {
await asyncExecShell(
`docker run --rm --name certbot-${randomCuid} -p 9080:9080 -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port 9080 -d ${domain} --agree-tos --non-interactive --register-unsafely-without-email`
);
const { stderr } = await asyncExecShell(
`docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest cat /etc/letsencrypt/live/${domain}/fullchain.pem /etc/letsencrypt/live/${domain}/privkey.pem > /app/ssl/${domain}.pem`
);
if (stderr) throw new Error(stderr);
return;
}
let data: any = await db.prisma.application.findUnique({
where: { id },
include: { destinationDocker: true }
});
if (!data) {
data = await db.prisma.service.findUnique({
where: { id },
include: { destinationDocker: true }
});
}
// Set SSL with Let's encrypt
if (data.destinationDockerId && data.destinationDocker) {
const host = getEngine(data.destinationDocker.engine);
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:9080 -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port 9080 -d ${domain} --agree-tos --non-interactive --register-unsafely-without-email`
);
const { stderr } = await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name bash-${randomCuid} -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest cat /etc/letsencrypt/live/${domain}/fullchain.pem /etc/letsencrypt/live/${domain}/privkey.pem > /app/ssl/${domain}.pem`
);
if (stderr) throw new Error(stderr);
await forceSSLOnApplication({ domain });
}
}
} catch (error) {
throw error;
}
}

View File

@@ -0,0 +1,180 @@
import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import { checkContainer, reloadHaproxy } from '$lib/haproxy';
import * as db from '$lib/database';
import { dev } from '$app/env';
import cuid from 'cuid';
import getPort, { portNumbers } from 'get-port';
export async function letsEncrypt(domain, id = null, isCoolify = false) {
try {
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
const nakedDomain = domain.replace('www.', '');
const wwwDomain = `www.${nakedDomain}`;
const randomCuid = cuid();
const randomPort = await getPort({ port: portNumbers(minPort, maxPort) });
let host;
let dualCerts = false;
if (isCoolify) {
dualCerts = data.dualCerts;
host = 'unix:///var/run/docker.sock';
} else {
const applicationData = await db.prisma.application.findUnique({
where: { id },
include: { destinationDocker: true, settings: true }
});
if (applicationData) {
if (applicationData?.destinationDockerId && applicationData?.destinationDocker) {
host = getEngine(applicationData.destinationDocker.engine);
}
if (applicationData?.settings?.dualCerts) {
dualCerts = applicationData.settings.dualCerts;
}
}
// Check Service
const serviceData = await db.prisma.service.findUnique({
where: { id },
include: { destinationDocker: true }
});
if (serviceData) {
if (serviceData?.destinationDockerId && serviceData?.destinationDocker) {
host = getEngine(serviceData.destinationDocker.engine);
}
if (serviceData?.dualCerts) {
dualCerts = serviceData.dualCerts;
}
}
}
if (dualCerts) {
let found = false;
try {
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "ls -1 /app/ssl/${wwwDomain}.pem"`
);
found = true;
} catch (error) {
//
}
if (found) return;
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${nakedDomain} -d ${wwwDomain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
dev ? '--test-cert' : ''
}`
);
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "test -d /etc/letsencrypt/live/${nakedDomain}/ && cat /etc/letsencrypt/live/${nakedDomain}/fullchain.pem /etc/letsencrypt/live/${nakedDomain}/privkey.pem > /app/ssl/${nakedDomain}.pem || cat /etc/letsencrypt/live/${wwwDomain}/fullchain.pem /etc/letsencrypt/live/${wwwDomain}/privkey.pem > /app/ssl/${wwwDomain}.pem"`
);
await reloadHaproxy(host);
} else {
let found = false;
try {
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "ls -1 /app/ssl/${domain}.pem"`
);
found = true;
} catch (error) {
//
}
if (found) return;
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${domain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
dev ? '--test-cert' : ''
}`
);
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "cat /etc/letsencrypt/live/${domain}/fullchain.pem /etc/letsencrypt/live/${domain}/privkey.pem > /app/ssl/${domain}.pem"`
);
await reloadHaproxy(host);
}
} catch (error) {
if (error.code !== 0) {
throw error;
}
}
}
export async function generateSSLCerts() {
const ssls = [];
const applications = await db.prisma.application.findMany({
include: { destinationDocker: true, settings: true },
orderBy: { createdAt: 'desc' }
});
for (const application of applications) {
const {
fqdn,
id,
destinationDocker: { engine, network },
settings: { previews }
} = application;
const isRunning = await checkContainer(engine, id);
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
if (isRunning) {
if (isHttps) ssls.push({ domain, id, isCoolify: false });
}
if (previews) {
const host = getEngine(engine);
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
);
const containers = stdout
.trim()
.split('\n')
.filter((a) => a)
.map((c) => c.replace(/"/g, ''));
if (containers.length > 0) {
for (const container of containers) {
let previewDomain = `${container.split('-')[1]}.${domain}`;
if (isHttps) ssls.push({ domain: previewDomain, id, isCoolify: false });
}
}
}
}
const services = await db.prisma.service.findMany({
include: {
destinationDocker: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true
},
orderBy: { createdAt: 'desc' }
});
for (const service of services) {
const {
fqdn,
id,
type,
destinationDocker: { engine }
} = service;
const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isRunning = await checkContainer(engine, id);
if (isRunning) {
if (isHttps) ssls.push({ domain, id, isCoolify: false });
}
}
}
const { fqdn } = await db.prisma.setting.findFirst();
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
if (isHttps) ssls.push({ domain, id: 'coolify', isCoolify: true });
}
if (ssls.length > 0) {
for (const ssl of ssls) {
if (!dev) {
console.log('Checking SSL for', ssl.domain);
await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
} else {
console.log('Checking SSL for', ssl.domain);
}
}
}
}

View File

@@ -3,8 +3,14 @@ import fs from 'fs/promises';
import * as buildpacks from '../buildPacks'; import * as buildpacks from '../buildPacks';
import * as importers from '../importers'; import * as importers from '../importers';
import { dockerInstance } from '../docker'; import { dockerInstance } from '../docker';
import { asyncExecShell, createDirectories, getDomain, getEngine, saveBuildLog } from '../common'; import {
import { configureProxyForApplication, reloadHaproxy, setWwwRedirection } from '../haproxy'; asyncExecShell,
asyncSleep,
createDirectories,
getDomain,
getEngine,
saveBuildLog
} from '../common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { decrypt } from '$lib/crypto'; import { decrypt } from '$lib/crypto';
import { sentry } from '$lib/common'; import { sentry } from '$lib/common';
@@ -13,7 +19,6 @@ import {
makeLabelForStandaloneApplication, makeLabelForStandaloneApplication,
setDefaultConfiguration setDefaultConfiguration
} from '$lib/buildPacks/common'; } from '$lib/buildPacks/common';
import { letsEncrypt } from '$lib/letsencrypt';
export default async function (job) { export default async function (job) {
/* /*
@@ -40,6 +45,7 @@ export default async function (job) {
publishDirectory, publishDirectory,
projectId, projectId,
secrets, secrets,
phpModules,
type, type,
pullmergeRequestId = null, pullmergeRequestId = null,
sourceBranch = null, sourceBranch = null,
@@ -47,6 +53,17 @@ export default async function (job) {
} = job.data; } = job.data;
const { debug } = settings; const { debug } = settings;
await asyncSleep(1000);
await db.prisma.build.updateMany({
where: {
status: 'queued',
id: { not: buildId },
applicationId,
createdAt: { lt: new Date(new Date().getTime() - 60 * 60 * 1000) }
},
data: { status: 'failed' }
});
let imageId = applicationId; let imageId = applicationId;
let domain = getDomain(fqdn); let domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
@@ -64,22 +81,12 @@ export default async function (job) {
if (destinationDockerId) { if (destinationDockerId) {
destinationType = 'docker'; destinationType = 'docker';
} }
if (destinationType === 'docker') { if (destinationType === 'docker') {
const docker = dockerInstance({ destinationDocker }); const docker = dockerInstance({ destinationDocker });
const host = getEngine(destinationDocker.engine); const host = getEngine(destinationDocker.engine);
const build = await db.createBuild({ await db.prisma.build.update({ where: { id: buildId }, data: { status: 'running' } });
id: buildId, const { workdir, repodir } = await createDirectories({ repository, buildId });
applicationId,
destinationDockerId: destinationDocker.id,
gitSourceId: gitSource.id,
githubAppId: gitSource.githubApp?.id,
gitlabAppId: gitSource.gitlabApp?.id,
type
});
const { workdir, repodir } = await createDirectories({ repository, buildId: build.id });
const configuration = await setDefaultConfiguration(job.data); const configuration = await setDefaultConfiguration(job.data);
@@ -89,6 +96,7 @@ export default async function (job) {
startCommand = configuration.startCommand; startCommand = configuration.startCommand;
buildCommand = configuration.buildCommand; buildCommand = configuration.buildCommand;
publishDirectory = configuration.publishDirectory; publishDirectory = configuration.publishDirectory;
baseDirectory = configuration.baseDirectory;
let commit = await importers[gitSource.type]({ let commit = await importers[gitSource.type]({
applicationId, applicationId,
@@ -99,7 +107,7 @@ export default async function (job) {
gitlabAppId: gitSource.gitlabApp?.id, gitlabAppId: gitSource.gitlabApp?.id,
repository, repository,
branch, branch,
buildId: build.id, buildId,
apiUrl: gitSource.apiUrl, apiUrl: gitSource.apiUrl,
projectId, projectId,
deployKeyId: gitSource.gitlabApp?.deployKeyId || null, deployKeyId: gitSource.gitlabApp?.deployKeyId || null,
@@ -111,7 +119,7 @@ export default async function (job) {
} }
try { try {
await db.prisma.build.update({ where: { id: build.id }, data: { commit } }); db.prisma.build.update({ where: { id: buildId }, data: { commit } });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
@@ -162,7 +170,7 @@ export default async function (job) {
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId); await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId);
if (buildpacks[buildPack]) if (buildpacks[buildPack])
await buildpacks[buildPack]({ await buildpacks[buildPack]({
buildId: build.id, buildId,
applicationId, applicationId,
domain, domain,
name, name,
@@ -183,7 +191,8 @@ export default async function (job) {
buildCommand, buildCommand,
startCommand, startCommand,
baseDirectory, baseDirectory,
secrets secrets,
phpModules
}); });
else { else {
saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId }); saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
@@ -205,7 +214,15 @@ export default async function (job) {
const envs = []; const envs = [];
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
envs.push(`${secret.name}=${secret.value}`); if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
}
}); });
} }
await fs.writeFile(`${workdir}/.env`, envs.join('\n')); await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
@@ -227,10 +244,16 @@ export default async function (job) {
baseDirectory, baseDirectory,
publishDirectory publishDirectory
}); });
let envFound = false;
try {
envFound = !!(await fs.stat(`${workdir}/.env`));
} catch (error) {
//
}
try { try {
saveBuildLog({ line: 'Deployment started.', buildId, applicationId }); saveBuildLog({ line: 'Deployment started.', buildId, applicationId });
const { stderr } = await asyncExecShell( const { stderr } = await asyncExecShell(
`DOCKER_HOST=${host} docker run --env-file=${workdir}/.env ${labels.join( `DOCKER_HOST=${host} docker run ${envFound && `--env-file=${workdir}/.env`} ${labels.join(
' ' ' '
)} --name ${imageId} --network ${ )} --name ${imageId} --network ${
docker.network docker.network
@@ -239,25 +262,10 @@ export default async function (job) {
if (stderr) console.log(stderr); if (stderr) console.log(stderr);
saveBuildLog({ line: 'Deployment successful!', buildId, applicationId }); saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} catch (error) { } catch (error) {
saveBuildLog({ line: error, buildId, applicationId });
sentry.captureException(error);
throw new Error(error); throw new Error(error);
} }
try { saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
saveBuildLog({ line: 'Proxy configuration started!', buildId, applicationId });
await configureProxyForApplication({ domain, imageId, applicationId, port });
if (isHttps) await letsEncrypt({ domain, id: applicationId });
await setWwwRedirection(fqdn);
await reloadHaproxy(destinationDocker.engine);
saveBuildLog({ line: 'Proxy configuration successful!', buildId, applicationId });
} else {
saveBuildLog({
line: 'Coolify Proxy is not configured for this destination. Nothing else to do.',
buildId,
applicationId
});
}
} catch (error) {
sentry.captureException(error);
}
} }
} }

View File

@@ -1,51 +1,71 @@
import { dev } from '$app/env'; import { dev } from '$app/env';
import { asyncExecShell, getEngine } from '$lib/common'; import { asyncExecShell, getEngine, version } from '$lib/common';
import { prisma } from '$lib/database'; import { prisma } from '$lib/database';
import { defaultProxyImageHttp, defaultProxyImageTcp } from '$lib/haproxy'; import { defaultProxyImageHttp, defaultProxyImageTcp } from '$lib/haproxy';
export default async function () { export default async function () {
if (!dev) { const destinationDockers = await prisma.destinationDocker.findMany();
const destinationDockers = await prisma.destinationDocker.findMany(); for (const destinationDocker of destinationDockers) {
for (const destinationDocker of destinationDockers) { const host = getEngine(destinationDocker.engine);
const host = getEngine(destinationDocker.engine); // Cleanup old coolify images
// Tagging images with labels try {
try { let { stdout: images } = await asyncExecShell(
const images = [ `DOCKER_HOST=${host} docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs `
`coollabsio/${defaultProxyImageTcp}`, );
`coollabsio/${defaultProxyImageHttp}`, images = images.trim();
'certbot/certbot:latest', if (images) {
'node:16.14.0-alpine', await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`);
'alpine:latest',
'nginx:stable-alpine',
'node:lts',
'php:apache',
'rust:latest'
];
for (const image of images) {
await asyncExecShell(
`DOCKER_HOST=${host} docker pull ${image} && echo "FROM ${image}" | docker build --label coolify.image="true" -t "${image}" -`
);
}
} catch (error) {}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`);
} catch (error) {
console.log(error);
}
// Cleanup images that are not managed by coolify
try {
await asyncExecShell(
`DOCKER_HOST=${host} docker image prune --filter 'label!=coolify.image=true' -a -f`
);
} catch (error) {
console.log(error);
}
// Cleanup dangling images
try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f`);
} catch (error) {
console.log(error);
} }
} catch (error) {
console.log(error);
} }
try {
await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`);
} catch (error) {
console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f`);
} catch (error) {
console.log(error);
}
// Tagging images with labels
// try {
// const images = [
// `coollabsio/${defaultProxyImageTcp}`,
// `coollabsio/${defaultProxyImageHttp}`,
// 'certbot/certbot:latest',
// 'node:16.14.0-alpine',
// 'alpine:latest',
// 'nginx:stable-alpine',
// 'node:lts',
// 'php:apache',
// 'rust:latest'
// ];
// for (const image of images) {
// try {
// await asyncExecShell(`DOCKER_HOST=${host} docker image inspect ${image}`);
// } catch (error) {
// await asyncExecShell(
// `DOCKER_HOST=${host} docker pull ${image} && echo "FROM ${image}" | docker build --label coolify.image="true" -t "${image}" -`
// );
// }
// }
// } catch (error) {}
// if (!dev) {
// // Cleanup images that are not managed by coolify
// try {
// await asyncExecShell(
// `DOCKER_HOST=${host} docker image prune --filter 'label!=coolify.image=true' -a -f`
// );
// } catch (error) {
// console.log(error);
// }
// // Cleanup old images >3 days
// try {
// await asyncExecShell(`DOCKER_HOST=${host} docker image prune --filter "until=72h" -a -f`);
// } catch (error) {
// console.log(error);
// }
// }
} }
} }

View File

@@ -86,8 +86,8 @@ const cron = async () => {
); );
await queue.proxy.add('proxy', {}, { repeat: { every: 10000 } }); await queue.proxy.add('proxy', {}, { repeat: { every: 10000 } });
// await queue.ssl.add('ssl', {}, { repeat: { every: 10000 } }); await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } });
if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 600000 } }); if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } });
await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } }); await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } });
const events = { const events = {
@@ -110,33 +110,36 @@ cron().catch((error) => {
const buildQueueName = 'build_queue'; const buildQueueName = 'build_queue';
const buildQueue = new Queue(buildQueueName, connectionOptions); const buildQueue = new Queue(buildQueueName, connectionOptions);
const buildWorker = new Worker(buildQueueName, async (job) => await builder(job), { const buildWorker = new Worker(buildQueueName, async (job) => await builder(job), {
concurrency: 2, concurrency: 1,
...connectionOptions ...connectionOptions
}); });
buildWorker.on('completed', async (job: Bullmq.Job) => { buildWorker.on('completed', async (job: Bullmq.Job) => {
try { try {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } }); await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } });
} catch (err) { } catch (error) {
console.log(err); console.log(error);
} finally { } finally {
const workdir = `/tmp/build-sources/${job.data.repository}/`; const workdir = `/tmp/build-sources/${job.data.repository}/${job.data.build_id}`;
await asyncExecShell(`rm -fr ${workdir}`); if (!dev) await asyncExecShell(`rm -fr ${workdir}`);
} }
return; return;
}); });
buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => { buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => {
console.log(failedReason);
try { try {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } }); await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } });
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} finally { } finally {
const workdir = `/tmp/build-sources/${job.data.repository}`; const workdir = `/tmp/build-sources/${job.data.repository}`;
await asyncExecShell(`rm -fr ${workdir}`); if (!dev) await asyncExecShell(`rm -fr ${workdir}`);
} }
saveBuildLog({ line: 'Failed build!', buildId: job.data.build_id, applicationId: job.data.id }); saveBuildLog({
line: 'Failed to deploy!',
buildId: job.data.build_id,
applicationId: job.data.id
});
saveBuildLog({ saveBuildLog({
line: `Reason: ${failedReason.toString()}`, line: `Reason: ${failedReason.toString()}`,
buildId: job.data.build_id, buildId: job.data.build_id,

View File

@@ -1,107 +1,11 @@
import { getDomain } from '$lib/common'; import { ErrorHandler } from '$lib/database';
import { getApplicationById, prisma, supportedServiceTypesAndVersions } from '$lib/database'; import { configureHAProxy } from '$lib/haproxy/configuration';
import { dockerInstance } from '$lib/docker';
import {
checkContainer,
configureCoolifyProxyOn,
configureProxyForApplication,
configureSimpleServiceProxyOn,
forceSSLOnApplication,
reloadHaproxy,
setWwwRedirection,
startCoolifyProxy,
startHttpProxy
} from '$lib/haproxy';
import * as db from '$lib/database';
export default async function () { export default async function () {
try { try {
// Check destination containers and configure proxy if needed return await configureHAProxy();
const destinationDockers = await prisma.destinationDocker.findMany({});
for (const destination of destinationDockers) {
if (destination.isCoolifyProxyUsed) {
const docker = dockerInstance({ destinationDocker: destination });
const containers = await docker.engine.listContainers();
const configurations = containers.filter(
(container) => container.Labels['coolify.managed']
);
for (const configuration of configurations) {
if (configuration.Labels['coolify.configuration']) {
const parsedConfiguration = JSON.parse(
Buffer.from(configuration.Labels['coolify.configuration'], 'base64').toString()
);
if (
parsedConfiguration &&
configuration.Labels['coolify.type'] === 'standalone-application'
) {
const { fqdn, applicationId, port, pullmergeRequestId } = parsedConfiguration;
if (fqdn) {
const found = await getApplicationById({ id: applicationId });
if (found) {
const domain = getDomain(fqdn);
await configureProxyForApplication({
domain,
imageId: pullmergeRequestId
? `${applicationId}-${pullmergeRequestId}`
: applicationId,
applicationId,
port
});
const isHttps = fqdn.startsWith('https://');
if (isHttps) await forceSSLOnApplication({ domain });
await setWwwRedirection(fqdn);
}
}
}
}
}
for (const container of containers) {
const image = container.Image.split(':')[0];
const found = supportedServiceTypesAndVersions.find((a) => a.baseImage === image);
if (found) {
const type = found.name;
const mainPort = found.ports.main;
const id = container.Names[0].replace('/', '');
const service = await db.prisma.service.findUnique({
where: { id },
include: {
destinationDocker: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true
}
});
const { fqdn } = service;
const domain = getDomain(fqdn);
await configureSimpleServiceProxyOn({ id, domain, port: mainPort });
const publicPort = service[type]?.publicPort;
if (publicPort) {
const containerFound = await checkContainer(
destination.engine,
`haproxy-for-${publicPort}`
);
if (!containerFound) {
await startHttpProxy(destination, id, publicPort, 9000);
}
}
}
}
}
}
const services = await prisma.service.findMany({});
// Check Coolify FQDN and configure proxy if needed
const { fqdn } = await db.listSettings();
if (fqdn) {
const domain = getDomain(fqdn);
await startCoolifyProxy('/var/run/docker.sock');
await configureCoolifyProxyOn(fqdn);
await setWwwRedirection(fqdn);
const isHttps = fqdn.startsWith('https://');
if (isHttps) await forceSSLOnApplication({ domain });
}
} catch (error) { } catch (error) {
console.log(error); console.log(error.response?.body || error);
throw error; return ErrorHandler(error.response?.body || error);
} }
} }

View File

@@ -1,69 +1,9 @@
import { asyncExecShell, getDomain, getEngine } from '$lib/common'; import { generateSSLCerts } from '$lib/letsencrypt';
import { prisma } from '$lib/database';
import { dockerInstance } from '$lib/docker';
import { forceSSLOnApplication } from '$lib/haproxy';
import * as db from '$lib/database';
import { dev } from '$app/env';
export default async function () { export default async function () {
try { try {
const destinationDockers = await prisma.destinationDocker.findMany({}); return await generateSSLCerts();
for (const destination of destinationDockers) {
if (destination.isCoolifyProxyUsed) {
const docker = dockerInstance({ destinationDocker: destination });
const containers = await docker.engine.listContainers();
const configurations = containers.filter(
(container) => container.Labels['coolify.managed']
);
for (const configuration of configurations) {
const parsedConfiguration = JSON.parse(
Buffer.from(configuration.Labels['coolify.configuration'], 'base64').toString()
);
if (configuration.Labels['coolify.type'] === 'standalone-application') {
const { fqdn } = parsedConfiguration;
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
if (isHttps) {
if (dev) {
console.log('DEV MODE: SSL is enabled');
} else {
const host = getEngine(destination.engine);
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot -p 9080:9080 -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port 9080 -d ${domain} --agree-tos --non-interactive --register-unsafely-without-email`
);
const { stderr } = await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name bash -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest cat /etc/letsencrypt/live/${domain}/fullchain.pem /etc/letsencrypt/live/${domain}/privkey.pem > /app/ssl/${domain}.pem`
);
if (stderr) throw new Error(stderr);
}
}
}
}
}
}
}
const { fqdn } = await db.listSettings();
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
if (isHttps) {
if (dev) {
console.log('DEV MODE: SSL is enabled');
} else {
await asyncExecShell(
`docker run --rm --name certbot -p 9080:9080 -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port 9080 -d ${domain} --agree-tos --non-interactive --register-unsafely-without-email`
);
const { stderr } = await asyncExecShell(
`docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest cat /etc/letsencrypt/live/${domain}/fullchain.pem /etc/letsencrypt/live/${domain}/privkey.pem > /app/ssl/${domain}.pem`
);
if (stderr) throw new Error(stderr);
}
}
}
} catch (error) { } catch (error) {
console.log(error);
throw error; throw error;
} }
} }

View File

@@ -1,10 +1,12 @@
import { asyncExecShell } from '$lib/common'; import { asyncExecShell } from '$lib/common';
import { reloadHaproxy } from '$lib/haproxy';
export default async function () { export default async function () {
try { try {
return await asyncExecShell( await asyncExecShell(
`docker run --rm --name certbot-renewal -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs renew` `docker run --rm --name certbot-renewal -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs renew`
); );
await reloadHaproxy('unix:///var/run/docker.sock');
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@@ -1,5 +1,8 @@
export const publicPaths = [ export const publicPaths = [
'/login', '/login',
'/register',
'/reset',
'/reset/password',
'/webhooks/success', '/webhooks/success',
'/webhooks/github', '/webhooks/github',
'/webhooks/github/install', '/webhooks/github/install',

6
src/lib/store.ts Normal file
View File

@@ -0,0 +1,6 @@
import { writable } from 'svelte/store';
export const gitTokens = writable({
githubToken: null,
gitlabToken: null
});

View File

@@ -2,14 +2,14 @@
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
import { publicPaths } from '$lib/settings'; import { publicPaths } from '$lib/settings';
export const load: Load = async ({ fetch, url, params, session }) => { export const load: Load = async ({ fetch, url, session }) => {
if (!session.uid && !publicPaths.includes(url.pathname)) { if (!session.userId && !publicPaths.includes(url.pathname)) {
return { return {
status: 302, status: 302,
redirect: '/login' redirect: '/login'
}; };
} }
if (!session.uid) { if (!session.userId) {
return {}; return {};
} }
const endpoint = `/teams.json`; const endpoint = `/teams.json`;
@@ -30,7 +30,6 @@
<script> <script>
export let teams; export let teams;
export let selectedTeamId; export let selectedTeamId;
import { fade } from 'svelte/transition';
import '../tailwind.css'; import '../tailwind.css';
import { SvelteToast, toast } from '@zerodevx/svelte-toast'; import { SvelteToast, toast } from '@zerodevx/svelte-toast';
@@ -39,17 +38,18 @@
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { asyncSleep } from '$lib/components/common'; import { asyncSleep } from '$lib/components/common';
import { del, get, post } from '$lib/api'; import { del, get, post } from '$lib/api';
import { browser } from '$app/env'; import { browser, dev } from '$app/env';
let isUpdateAvailable = false; let isUpdateAvailable = false;
let updateStatus = { let updateStatus = {
found: false,
loading: false, loading: false,
checking: false,
success: null success: null
}; };
let latestVersion = 'latest'; let latestVersion = 'latest';
onMount(async () => { onMount(async () => {
if ($session.uid) { if ($session.userId) {
const overrideVersion = browser && window.localStorage.getItem('latestVersion'); const overrideVersion = browser && window.localStorage.getItem('latestVersion');
try { try {
await get(`/login.json`); await get(`/login.json`);
@@ -59,17 +59,18 @@
return errorNotification(error); return errorNotification(error);
} }
if ($session.teamId === '0') { if ($session.teamId === '0') {
updateStatus.checking = true;
try { try {
const data = await get(`/update.json`); const data = await get(`/update.json`);
if (overrideVersion || data?.isUpdateAvailable) { if (overrideVersion || data?.isUpdateAvailable) {
latestVersion = overrideVersion || data.latestVersion; latestVersion = overrideVersion || data.latestVersion;
isUpdateAvailable = overrideVersion ? true : data?.isUpdateAvailable; if (overrideVersion) {
await post(`/update.json`, { type: 'pull', latestVersion }); isUpdateAvailable = true;
} else {
isUpdateAvailable = data.isUpdateAvailable;
}
} }
} catch (error) { } catch (error) {
} finally { } finally {
updateStatus.checking = false;
} }
} }
} }
@@ -84,7 +85,7 @@
} }
async function switchTeam() { async function switchTeam() {
try { try {
await post(`/index.json?from=${$page.url.pathname}`, { await post(`/dashboard.json?from=${$page.url.pathname}`, {
cookie: 'teamId', cookie: 'teamId',
value: selectedTeamId value: selectedTeamId
}); });
@@ -97,26 +98,32 @@
async function update() { async function update() {
updateStatus.loading = true; updateStatus.loading = true;
try { try {
await post(`/update.json`, { type: 'update', latestVersion }); if (dev) {
toast.push('Update completed.<br>Waiting for the new version to start...'); console.log(`updating to ${latestVersion}`);
let reachable = false;
let tries = 0;
do {
await asyncSleep(4000); await asyncSleep(4000);
try { return window.location.reload();
await get(`/undead.json`); } else {
reachable = true; await post(`/update.json`, { type: 'update', latestVersion });
} catch (error) { toast.push('Update completed.<br><br>Waiting for the new version to start...');
reachable = false; let reachable = false;
} let tries = 0;
if (reachable) break; do {
tries++; await asyncSleep(4000);
} while (!reachable || tries < 120); try {
toast.push('New version reachable. Reloading...'); await get(`/undead.json`);
updateStatus.loading = false; reachable = true;
updateStatus.success = true; } catch (error) {
await asyncSleep(3000); reachable = false;
return window.location.reload(); }
if (reachable) break;
tries++;
} while (!reachable || tries < 120);
toast.push('New version reachable. Reloading...');
updateStatus.loading = false;
updateStatus.success = true;
await asyncSleep(3000);
return window.location.reload();
}
} catch ({ error }) { } catch ({ error }) {
updateStatus.success = false; updateStatus.success = false;
updateStatus.loading = false; updateStatus.loading = false;
@@ -129,7 +136,7 @@
<title>Coolify</title> <title>Coolify</title>
</svelte:head> </svelte:head>
<SvelteToast options={{ intro: { y: -64 }, duration: 3000, pausable: true }} /> <SvelteToast options={{ intro: { y: -64 }, duration: 3000, pausable: true }} />
{#if $session.uid} {#if $session.userId}
<nav class="nav-main"> <nav class="nav-main">
<div class="flex h-screen w-full flex-col items-center transition-all duration-100"> <div class="flex h-screen w-full flex-col items-center transition-all duration-100">
<div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div> <div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div>
@@ -311,42 +318,17 @@
<div class="flex flex-col space-y-4 py-2"> <div class="flex flex-col space-y-4 py-2">
{#if $session.teamId === '0'} {#if $session.teamId === '0'}
{#if updateStatus.checking} {#if isUpdateAvailable}
<button
disabled
in:fade={{ duration: 150 }}
class="icons tooltip-right bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105"
data-tooltip="Checking for updates..."
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-9 w-8 animate-spin"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="11" y1="19.94" x2="11" y2="19.95" />
</svg></button
>
{:else if isUpdateAvailable}
<button <button
disabled={updateStatus.success === false} disabled={updateStatus.success === false}
data-tooltip="Update available" title="Update available"
on:click={update} on:click={update}
class="icons tooltip-right bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105" class="icons tooltip-right bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105"
> >
{#if updateStatus.loading} {#if updateStatus.loading}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-8 h-9 lds-heart" class="lds-heart h-9 w-8"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@@ -444,7 +426,8 @@
</button> </button>
{/if} {/if}
{/if} {/if}
</div>
<div class="flex flex-col space-y-4 py-2">
<a <a
sveltekit:prefetch sveltekit:prefetch
href="/teams" href="/teams"
@@ -519,20 +502,20 @@
<path d="M7 12h14l-3 -3m0 6l3 -3" /> <path d="M7 12h14l-3 -3m0 6l3 -3" />
</svg> </svg>
</div> </div>
</div> <div
<div class="w-full text-center font-bold text-stone-400 hover:bg-coolgray-200 hover:text-white"
class="w-full text-center font-bold text-stone-400 hover:bg-coolgray-200 hover:text-white"
>
<a
class="text-[10px] no-underline"
href={`https://github.com/coollabsio/coolify/releases/tag/v${$session.version}`}
target="_blank">v{$session.version}</a
> >
<a
class="text-[10px] no-underline"
href={`https://github.com/coollabsio/coolify/releases/tag/v${$session.version}`}
target="_blank">v{$session.version}</a
>
</div>
</div> </div>
</div> </div>
</nav> </nav>
<select <select
class="fixed right-0 bottom-0 z-50 m-2 p-2 px-4" class="fixed right-0 bottom-0 z-50 m-2 w-64 bg-opacity-30 p-2 px-4"
bind:value={selectedTeamId} bind:value={selectedTeamId}
on:change={switchTeam} on:change={switchTeam}
> >

View File

@@ -17,13 +17,20 @@
const endpoint = `/applications/${params.id}.json`; const endpoint = `/applications/${params.id}.json`;
const res = await fetch(endpoint); const res = await fetch(endpoint);
if (res.ok) { if (res.ok) {
const { application, githubToken, ghToken, isRunning, appId } = await res.json(); let { application, isRunning, appId, githubToken, gitlabToken } = await res.json();
if (!application || Object.entries(application).length === 0) { if (!application || Object.entries(application).length === 0) {
return { return {
status: 302, status: 302,
redirect: '/applications' redirect: '/applications'
}; };
} }
if (application.gitSource?.githubAppId && !githubToken) {
const response = await fetch(`/applications/${params.id}/configuration/githubToken.json`);
if (response.ok) {
const { token } = await response.json();
githubToken = token;
}
}
const configurationPhase = checkConfiguration(application); const configurationPhase = checkConfiguration(application);
if ( if (
configurationPhase && configurationPhase &&
@@ -38,12 +45,12 @@
return { return {
props: { props: {
application, application,
isRunning isRunning,
githubToken,
gitlabToken
}, },
stuff: { stuff: {
isRunning, isRunning,
ghToken,
githubToken,
application, application,
appId appId
} }
@@ -60,12 +67,19 @@
<script lang="ts"> <script lang="ts">
export let application; export let application;
export let isRunning; export let isRunning;
export let githubToken;
export let gitlabToken;
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import Loading from '$lib/components/Loading.svelte'; import Loading from '$lib/components/Loading.svelte';
import { del, post } from '$lib/api'; import { del, post } from '$lib/api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { gitTokens } from '$lib/store';
import { toast } from '@zerodevx/svelte-toast';
if (githubToken) $gitTokens.githubToken = githubToken;
if (gitlabToken) $gitTokens.gitlabToken = gitlabToken;
let loading = false; let loading = false;
const { id } = $page.params; const { id } = $page.params;
@@ -73,6 +87,7 @@
async function handleDeploySubmit() { async function handleDeploySubmit() {
try { try {
const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application }); const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application });
toast.push('Deployment queued.');
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`); return await goto(`/applications/${id}/logs/build?buildId=${buildId}`);
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
@@ -98,8 +113,6 @@
return window.location.reload(); return window.location.reload();
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} finally {
loading = false;
} }
} }
</script> </script>
@@ -115,7 +128,7 @@
title="Stop application" title="Stop application"
type="submit" type="submit"
disabled={!$session.isAdmin} disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-green-600 hover:text-white" class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-red-500"
data-tooltip={$session.isAdmin data-tooltip={$session.isAdmin
? 'Stop application' ? 'Stop application'
: 'You do not have permission to stop the application.'} : 'You do not have permission to stop the application.'}
@@ -140,7 +153,7 @@
title="Rebuild application" title="Rebuild application"
type="submit" type="submit"
disabled={!$session.isAdmin} disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-green-600 hover:text-white" class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:text-green-500"
data-tooltip={$session.isAdmin data-tooltip={$session.isAdmin
? 'Rebuild application' ? 'Rebuild application'
: 'You do not have permission to rebuild application.'} : 'You do not have permission to rebuild application.'}
@@ -169,7 +182,7 @@
title="Build and start application" title="Build and start application"
type="submit" type="submit"
disabled={!$session.isAdmin} disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-green-600 hover:text-white" class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-green-500"
data-tooltip={$session.isAdmin data-tooltip={$session.isAdmin
? 'Build and start application' ? 'Build and start application'
: 'You do not have permission to Build and start application.'} : 'You do not have permission to Build and start application.'}

View File

@@ -1,24 +1,47 @@
import { asyncExecShell, getDomain, getEngine, getUserDetails } from '$lib/common'; import { dev } from '$app/env';
import { getDomain, getUserDetails } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { promises as dns } from 'dns';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event); const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
let { fqdn, forceSave } = await event.request.json();
let { fqdn } = await event.request.json();
fqdn = fqdn.toLowerCase(); fqdn = fqdn.toLowerCase();
try { try {
const domain = getDomain(fqdn);
const found = await db.isDomainConfigured({ id, fqdn }); const found = await db.isDomainConfigured({ id, fqdn });
if (found) { if (found) {
throw { throw {
message: `Domain ${getDomain(fqdn)} is already configured.` message: `Domain ${getDomain(fqdn).replace('www.', '')} is already used.`
}; };
} }
if (!dev && !forceSave) {
let ip = [];
let localIp = [];
dns.setServers(['1.1.1.1', '8.8.8.8']);
try {
localIp = await dns.resolve4(event.url.hostname);
} catch (error) {}
try {
ip = await dns.resolve4(domain);
} catch (error) {}
if (localIp?.length > 0) {
if (ip?.length === 0 || !ip.includes(localIp[0])) {
throw {
message: `DNS not set or propogated for ${domain}.<br><br>Please check your DNS settings.`
};
}
}
}
return { return {
status: 200 status: 200
}; };

View File

@@ -3,6 +3,7 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import { post } from '$lib/api'; import { post } from '$lib/api';
import { findBuildPack } from '$lib/components/templates';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
const { id } = $page.params; const { id } = $page.params;
@@ -11,10 +12,13 @@
export let buildPack; export let buildPack;
export let foundConfig; export let foundConfig;
export let scanning; export let scanning;
export let packageManager;
async function handleSubmit(name) { async function handleSubmit(name) {
try { try {
const tempBuildPack = JSON.parse(JSON.stringify(buildPack)); const tempBuildPack = JSON.parse(
JSON.stringify(findBuildPack(buildPack.name, packageManager))
);
delete tempBuildPack.name; delete tempBuildPack.name;
delete tempBuildPack.fancyName; delete tempBuildPack.fancyName;
delete tempBuildPack.color; delete tempBuildPack.color;

View File

@@ -1,14 +1,12 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation';
export let githubToken;
export let application; export let application;
import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { getGithubToken } from '$lib/components/common'; import { errorNotification } from '$lib/form';
import { enhance, errorNotification } from '$lib/form';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { gitTokens } from '$lib/store';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
@@ -27,22 +25,17 @@
let selected = { let selected = {
projectId: undefined, projectId: undefined,
repository: undefined, repository: undefined,
branch: undefined branch: undefined,
autodeploy: application.settings.autodeploy || true
}; };
let showSave = false; let showSave = false;
let token = null;
async function loadRepositoriesByPage(page = 0) { async function loadRepositoriesByPage(page = 0) {
try { return await get(`${apiUrl}/installation/repositories?per_page=100&page=${page}`, {
return await get(`${apiUrl}/installation/repositories?per_page=100&page=${page}`, { Authorization: `token ${$gitTokens.githubToken}`
Authorization: `token ${token}` });
});
} catch ({ error }) {
return errorNotification(error);
}
} }
async function loadRepositories() { async function loadRepositories() {
token = await getGithubToken({ apiUrl, githubToken, application });
let page = 1; let page = 1;
let reposCount = 0; let reposCount = 0;
const loadedRepos = await loadRepositoriesByPage(); const loadedRepos = await loadRepositoriesByPage();
@@ -63,7 +56,7 @@
selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id; selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id;
try { try {
branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, { branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, {
Authorization: `token ${token}` Authorization: `token ${$gitTokens.githubToken}`
}); });
return; return;
} catch ({ error }) { } catch ({ error }) {
@@ -78,7 +71,14 @@
`/applications/${id}/configuration/repository.json?repository=${selected.repository}&branch=${selected.branch}` `/applications/${id}/configuration/repository.json?repository=${selected.repository}&branch=${selected.branch}`
); );
if (data.used) { if (data.used) {
errorNotification('This branch is already used by another application.'); const sure = confirm(
`This branch is already used by another application. Webhooks won't work in this case for both applications. Are you sure you want to use it?`
);
if (sure) {
selected.autodeploy = false;
showSave = true;
return true;
}
showSave = false; showSave = false;
return true; return true;
} }
@@ -90,7 +90,47 @@
} }
onMount(async () => { onMount(async () => {
await loadRepositories(); try {
if (!$gitTokens.githubToken) {
const { token } = await get(`/applications/${id}/configuration/githubToken.json`);
$gitTokens.githubToken = token;
}
await loadRepositories();
} catch (error) {
if (
error.error === 'invalid_token' ||
error.error_description ===
'Token is expired. You can either do re-authorization or token refresh.' ||
error.message === '401 Unauthorized'
) {
if (application.gitSource.gitlabAppId) {
let htmlUrl = application.gitSource.htmlUrl;
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
`${htmlUrl}/oauth/authorize?client_id=${application.gitSource.gitlabApp.appId}&redirect_uri=${window.location.origin}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${$page.params.id}`,
'GitLab',
'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
top +
', left=' +
left +
', toolbar=0, menubar=0, status=0'
);
const timer = setInterval(() => {
if (newWindow?.closed) {
clearInterval(timer);
window.location.reload();
}
}, 100);
}
}
if (error.message === 'Bad credentials') {
const { token } = await get(`/applications/${id}/configuration/githubToken.json`);
$gitTokens.githubToken = token;
return await loadRepositories();
}
return errorNotification(error);
}
}); });
async function handleSubmit() { async function handleSubmit() {
try { try {

View File

@@ -8,6 +8,7 @@
import cuid from 'cuid'; import cuid from 'cuid';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { del, get, post, put } from '$lib/api'; import { del, get, post, put } from '$lib/api';
import { gitTokens } from '$lib/store';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
@@ -29,6 +30,7 @@
let projects = []; let projects = [];
let branches = []; let branches = [];
let showSave = false; let showSave = false;
let autodeploy = application.settings.autodeploy || true;
let selected = { let selected = {
group: undefined, group: undefined,
@@ -36,13 +38,13 @@
branch: undefined branch: undefined
}; };
onMount(async () => { onMount(async () => {
if (!$session.gitlabToken) { if (!$gitTokens.gitlabToken) {
getGitlabToken(); getGitlabToken();
} else { } else {
loading.base = true; loading.base = true;
try { try {
const user = await get(`${apiUrl}/v4/user`, { const user = await get(`${apiUrl}/v4/user`, {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
}); });
username = user.username; username = user.username;
} catch (error) { } catch (error) {
@@ -50,7 +52,7 @@
} }
try { try {
groups = await get(`${apiUrl}/v4/groups?per_page=5000`, { groups = await get(`${apiUrl}/v4/groups?per_page=5000`, {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
}); });
} catch (error) { } catch (error) {
errorNotification(error); errorNotification(error);
@@ -88,7 +90,7 @@
projects = await get( projects = await get(
`${apiUrl}/v4/users/${selected.group.name}/projects?min_access_level=40&page=1&per_page=25&archived=false`, `${apiUrl}/v4/users/${selected.group.name}/projects?min_access_level=40&page=1&per_page=25&archived=false`,
{ {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
} }
); );
} catch (error) { } catch (error) {
@@ -102,7 +104,7 @@
projects = await get( projects = await get(
`${apiUrl}/v4/groups/${selected.group.id}/projects?page=1&per_page=25&archived=false`, `${apiUrl}/v4/groups/${selected.group.id}/projects?page=1&per_page=25&archived=false`,
{ {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
} }
); );
} catch (error) { } catch (error) {
@@ -120,7 +122,7 @@
branches = await get( branches = await get(
`${apiUrl}/v4/projects/${selected.project.id}/repository/branches?per_page=100&page=1`, `${apiUrl}/v4/projects/${selected.project.id}/repository/branches?per_page=100&page=1`,
{ {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
} }
); );
} catch (error) { } catch (error) {
@@ -137,7 +139,14 @@
`/applications/${id}/configuration/repository.json?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}` `/applications/${id}/configuration/repository.json?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}`
); );
if (data.used) { if (data.used) {
errorNotification('This branch is already used by another application.'); const sure = confirm(
`This branch is already used by another application. Webhooks won't work in this case for both applications. Are you sure you want to use it?`
);
if (sure) {
autodeploy = false;
showSave = true;
return true;
}
showSave = false; showSave = false;
return true; return true;
} }
@@ -170,7 +179,7 @@
merge_requests_events: true merge_requests_events: true
}, },
{ {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
} }
); );
} catch (error) { } catch (error) {
@@ -194,7 +203,7 @@
publicSshKey = publicKey; publicSshKey = publicKey;
} }
const deployKeys = await get(deployKeyUrl, { const deployKeys = await get(deployKeyUrl, {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
}); });
const deployKeyFound = deployKeys.filter((dk) => dk.title === `${appId}-coolify-deploy-key`); const deployKeyFound = deployKeys.filter((dk) => dk.title === `${appId}-coolify-deploy-key`);
if (deployKeyFound.length > 0) { if (deployKeyFound.length > 0) {
@@ -203,7 +212,7 @@
`${deployKeyUrl}/${deployKey.id}`, `${deployKeyUrl}/${deployKey.id}`,
{}, {},
{ {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
} }
); );
} }
@@ -216,7 +225,7 @@
can_push: false can_push: false
}, },
{ {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
} }
); );
await post(updateDeployKeyIdUrl, { deployKeyId: id }); await post(updateDeployKeyIdUrl, { deployKeyId: id });
@@ -234,10 +243,14 @@
const url = `/applications/${id}/configuration/repository.json`; const url = `/applications/${id}/configuration/repository.json`;
try { try {
const repository = `${selected.group.full_path.replace('-personal', '')}/${
selected.project.name
}`;
await post(url, { await post(url, {
repository: `${selected.group.full_path}/${selected.project.name}`, repository,
branch: selected.branch.name, branch: selected.branch.name,
projectId: selected.project.id, projectId: selected.project.id,
autodeploy,
webhookToken webhookToken
}); });
return await goto(from || `/applications/${id}/configuration/buildpack`); return await goto(from || `/applications/${id}/configuration/buildpack`);

View File

@@ -1,7 +1,7 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, url, stuff }) => { export const load: Load = async ({ fetch, params, url, stuff }) => {
const { application, ghToken } = stuff; const { application } = stuff;
if (application?.buildPack && !url.searchParams.get('from')) { if (application?.buildPack && !url.searchParams.get('from')) {
return { return {
status: 302, status: 302,
@@ -14,8 +14,7 @@
return { return {
props: { props: {
...(await res.json()), ...(await res.json()),
application, application
ghToken
} }
}; };
} }
@@ -30,30 +29,35 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { buildPacks, scanningTemplates } from '$lib/components/templates'; import { buildPacks, findBuildPack, scanningTemplates } from '$lib/components/templates';
import BuildPack from './_BuildPack.svelte'; import BuildPack from './_BuildPack.svelte';
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { gitTokens } from '$lib/store';
import { browser } from '$app/env';
const { id } = $page.params;
let scanning = true; let scanning = true;
let foundConfig = null; let foundConfig = null;
let packageManager = 'npm';
export let apiUrl; export let apiUrl;
export let projectId; export let projectId;
export let repository; export let repository;
export let branch; export let branch;
export let ghToken;
export let type; export let type;
export let application; export let application;
function checkPackageJSONContents({ key, json }) { function checkPackageJSONContents({ key, json }) {
return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key); return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key);
} }
function checkTemplates({ json }) { function checkTemplates({ json, packageManager }) {
for (const [key, value] of Object.entries(scanningTemplates)) { for (const [key, value] of Object.entries(scanningTemplates)) {
if (checkPackageJSONContents({ key, json })) { if (checkPackageJSONContents({ key, json })) {
return buildPacks.find((bp) => bp.name === value.buildPack); foundConfig = findBuildPack(value.buildPack, packageManager);
break;
} }
} }
} }
@@ -61,11 +65,15 @@
try { try {
if (type === 'gitlab') { if (type === 'gitlab') {
const files = await get(`${apiUrl}/v4/projects/${projectId}/repository/tree`, { const files = await get(`${apiUrl}/v4/projects/${projectId}/repository/tree`, {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
}); });
const packageJson = files.find( const packageJson = files.find(
(file) => file.name === 'package.json' && file.type === 'blob' (file) => file.name === 'package.json' && file.type === 'blob'
); );
const yarnLock = files.find((file) => file.name === 'yarn.lock' && file.type === 'blob');
const pnpmLock = files.find(
(file) => file.name === 'pnpm-lock.yaml' && file.type === 'blob'
);
const dockerfile = files.find((file) => file.name === 'Dockerfile' && file.type === 'blob'); const dockerfile = files.find((file) => file.name === 'Dockerfile' && file.type === 'blob');
const cargoToml = files.find((file) => file.name === 'Cargo.toml' && file.type === 'blob'); const cargoToml = files.find((file) => file.name === 'Cargo.toml' && file.type === 'blob');
const requirementsTxt = files.find( const requirementsTxt = files.find(
@@ -73,35 +81,45 @@
); );
const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'blob'); const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'blob');
const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'blob'); const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'blob');
if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm';
if (dockerfile) { if (dockerfile) {
foundConfig.buildPack = 'docker'; foundConfig = findBuildPack('docker', packageManager);
} else if (packageJson) { } else if (packageJson) {
const path = packageJson.path; const path = packageJson.path;
const data = await get( const data = await get(
`${apiUrl}/v4/projects/${projectId}/repository/files/${path}/raw?ref=${branch}`, `${apiUrl}/v4/projects/${projectId}/repository/files/${path}/raw?ref=${branch}`,
{ {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
} }
); );
const json = JSON.parse(data) || {}; const json = JSON.parse(data) || {};
foundConfig = checkTemplates({ json }); checkTemplates({ json, packageManager });
} else if (cargoToml) { } else if (cargoToml) {
foundConfig = buildPacks.find((bp) => bp.name === 'rust'); foundConfig = findBuildPack('rust');
} else if (requirementsTxt) { } else if (requirementsTxt) {
foundConfig = buildPacks.find((bp) => bp.name === 'python'); foundConfig = findBuildPack('python');
} else if (indexHtml) { } else if (indexHtml) {
foundConfig = buildPacks.find((bp) => bp.name === 'static'); foundConfig = findBuildPack('static', packageManager);
} else if (indexPHP) { } else if (indexPHP) {
foundConfig = buildPacks.find((bp) => bp.name === 'php'); foundConfig = findBuildPack('php');
} else {
foundConfig = findBuildPack('node', packageManager);
} }
} else if (type === 'github') { } else if (type === 'github') {
const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, { const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, {
Authorization: `Bearer ${ghToken}`, Authorization: `Bearer ${$gitTokens.githubToken}`,
Accept: 'application/vnd.github.v2.json' Accept: 'application/vnd.github.v2.json'
}); });
const packageJson = files.find( const packageJson = files.find(
(file) => file.name === 'package.json' && file.type === 'file' (file) => file.name === 'package.json' && file.type === 'file'
); );
const yarnLock = files.find((file) => file.name === 'yarn.lock' && file.type === 'file');
const pnpmLock = files.find(
(file) => file.name === 'pnpm-lock.yaml' && file.type === 'file'
);
const dockerfile = files.find((file) => file.name === 'Dockerfile' && file.type === 'file'); const dockerfile = files.find((file) => file.name === 'Dockerfile' && file.type === 'file');
const cargoToml = files.find((file) => file.name === 'Cargo.toml' && file.type === 'file'); const cargoToml = files.find((file) => file.name === 'Cargo.toml' && file.type === 'file');
const requirementsTxt = files.find( const requirementsTxt = files.find(
@@ -109,26 +127,33 @@
); );
const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'file'); const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'file');
const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'file'); const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'file');
if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm';
if (dockerfile) { if (dockerfile) {
foundConfig.buildPack = 'docker'; foundConfig = findBuildPack('docker', packageManager);
} else if (packageJson) { } else if (packageJson) {
const data = await get(`${packageJson.git_url}`, { const data = await get(`${packageJson.git_url}`, {
Authorization: `Bearer ${ghToken}`, Authorization: `Bearer ${$gitTokens.githubToken}`,
Accept: 'application/vnd.github.v2.raw' Accept: 'application/vnd.github.v2.raw'
}); });
const json = JSON.parse(data) || {}; const json = JSON.parse(data) || {};
foundConfig = checkTemplates({ json }); checkTemplates({ json, packageManager });
} else if (cargoToml) { } else if (cargoToml) {
foundConfig = buildPacks.find((bp) => bp.name === 'rust'); foundConfig = findBuildPack('rust');
} else if (requirementsTxt) { } else if (requirementsTxt) {
foundConfig = buildPacks.find((bp) => bp.name === 'python'); foundConfig = findBuildPack('python');
} else if (indexHtml) { } else if (indexHtml) {
foundConfig = buildPacks.find((bp) => bp.name === 'static'); foundConfig = findBuildPack('static', packageManager);
} else if (indexPHP) { } else if (indexPHP) {
foundConfig = buildPacks.find((bp) => bp.name === 'php'); foundConfig = findBuildPack('php');
} else {
foundConfig = findBuildPack('node', packageManager);
} }
} }
} catch (error) { } catch (error) {
scanning = true;
if ( if (
error.error === 'invalid_token' || error.error === 'invalid_token' ||
error.error_description === error.error_description ===
@@ -156,9 +181,14 @@
}, 100); }, 100);
} }
} }
if (error.message === 'Bad credentials') {
const { token } = await get(`/applications/${id}/configuration/githubToken.json`);
$gitTokens.githubToken = token;
browser && window.location.reload();
}
return errorNotification(error); return errorNotification(error);
} finally { } finally {
if (!foundConfig) foundConfig = buildPacks.find((bp) => bp.name === 'node'); if (!foundConfig) foundConfig = findBuildPack('node', packageManager);
scanning = false; scanning = false;
} }
} }
@@ -176,10 +206,16 @@
<div class="text-xl tracking-tight">Scanning repository to suggest a build pack for you...</div> <div class="text-xl tracking-tight">Scanning repository to suggest a build pack for you...</div>
</div> </div>
{:else} {:else}
{#if packageManager === 'yarn' || packageManager === 'pnpm'}
<div class="flex justify-center p-6">
Found lock file for <span class="font-bold text-orange-500 pl-1">{packageManager}</span>.
Using it for predefined commands commands.
</div>
{/if}
<div class="max-w-7xl mx-auto flex flex-wrap justify-center"> <div class="max-w-7xl mx-auto flex flex-wrap justify-center">
{#each buildPacks as buildPack} {#each buildPacks as buildPack}
<div class="p-2"> <div class="p-2">
<BuildPack {buildPack} {scanning} bind:foundConfig /> <BuildPack {buildPack} {scanning} {packageManager} bind:foundConfig />
</div> </div>
{/each} {/each}
</div> </div>

View File

@@ -0,0 +1,45 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
import jsonwebtoken from 'jsonwebtoken';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const application = await db.getApplication({ id, teamId });
const payload = {
iat: Math.round(new Date().getTime() / 1000),
exp: Math.round(new Date().getTime() / 1000 + 60),
iss: application.gitSource.githubApp.appId
};
const githubToken = jsonwebtoken.sign(payload, application.gitSource.githubApp.privateKey, {
algorithm: 'RS256'
});
const response = await fetch(
`${application.gitSource.apiUrl}/app/installations/${application.gitSource.githubApp.installationId}/access_tokens`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${githubToken}`
}
}
);
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
const data = await response.json();
return {
status: 201,
body: { token: data.token },
headers: {
'Set-Cookie': `githubToken=${data.token}; Path=/; HttpOnly; Max-Age=15778800;`
}
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -30,14 +30,21 @@ export const post: RequestHandler = async (event) => {
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
let { repository, branch, projectId, webhookToken } = await event.request.json(); let { repository, branch, projectId, webhookToken, autodeploy } = await event.request.json();
repository = repository.toLowerCase(); repository = repository.toLowerCase();
branch = branch.toLowerCase(); branch = branch.toLowerCase();
projectId = Number(projectId); projectId = Number(projectId);
try { try {
await db.configureGitRepository({ id, repository, branch, projectId, webhookToken }); await db.configureGitRepository({
id,
repository,
branch,
projectId,
webhookToken,
autodeploy
});
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@@ -1,7 +1,7 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ params, url, stuff }) => { export const load: Load = async ({ params, url, stuff }) => {
const { application, githubToken, appId } = stuff; const { application, appId } = stuff;
if (application?.branch && application?.repository && !url.searchParams.get('from')) { if (application?.branch && application?.repository && !url.searchParams.get('from')) {
return { return {
status: 302, status: 302,
@@ -10,7 +10,6 @@
} }
return { return {
props: { props: {
githubToken,
application, application,
appId appId
} }
@@ -20,8 +19,8 @@
<script lang="ts"> <script lang="ts">
export let application; export let application;
export let githubToken;
export let appId; export let appId;
import GithubRepositories from './_GithubRepositories.svelte'; import GithubRepositories from './_GithubRepositories.svelte';
import GitlabRepositories from './_GitlabRepositories.svelte'; import GitlabRepositories from './_GitlabRepositories.svelte';
</script> </script>
@@ -31,7 +30,7 @@
</div> </div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#if application.gitSource.type === 'github'} {#if application.gitSource.type === 'github'}
<GithubRepositories {application} {githubToken} /> <GithubRepositories {application} />
{:else if application.gitSource.type === 'gitlab'} {:else if application.gitSource.type === 'gitlab'}
<GitlabRepositories {application} {appId} /> <GitlabRepositories {application} {appId} />
{/if} {/if}

View File

@@ -30,7 +30,7 @@
import type Prisma from '@prisma/client'; import type Prisma from '@prisma/client';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { enhance, errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { post } from '$lib/api'; import { post } from '$lib/api';

View File

@@ -29,6 +29,19 @@ export const post: RequestHandler = async (event) => {
.digest('hex'); .digest('hex');
await db.prisma.application.update({ where: { id }, data: { configHash } }); await db.prisma.application.update({ where: { id }, data: { configHash } });
} }
await db.prisma.application.update({ where: { id }, data: { updatedAt: new Date() } });
await db.prisma.build.create({
data: {
id: buildId,
applicationId: id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'manual'
}
});
await buildQueue.add(buildId, { build_id: buildId, type: 'manual', ...applicationFound }); await buildQueue.add(buildId, { build_id: buildId, type: 'manual', ...applicationFound });
return { return {
status: 200, status: 200,

View File

@@ -1,46 +1,36 @@
import { getTeam, getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import { getGithubToken } from '$lib/components/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import { checkContainer } from '$lib/haproxy'; import { checkContainer } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import jsonwebtoken from 'jsonwebtoken'; import jsonwebtoken from 'jsonwebtoken';
import { get as getRequest } from '$lib/api';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const appId = process.env['COOLIFY_APP_ID'];
let githubToken = null;
let ghToken = null;
let isRunning = false;
const { id } = event.params; const { id } = event.params;
const appId = process.env['COOLIFY_APP_ID'];
let isRunning = false;
let githubToken = event.locals.cookies?.githubToken || null;
let gitlabToken = event.locals.cookies?.gitlabToken || null;
try { try {
const application = await db.getApplication({ id, teamId }); const application = await db.getApplication({ id, teamId });
const { gitSource } = application;
if (gitSource?.type === 'github' && gitSource?.githubApp) {
const payload = {
iat: Math.round(new Date().getTime() / 1000),
exp: Math.round(new Date().getTime() / 1000 + 60),
iss: gitSource.githubApp.appId
};
githubToken = jsonwebtoken.sign(payload, gitSource.githubApp.privateKey, {
algorithm: 'RS256'
});
ghToken = await getGithubToken({ apiUrl: gitSource.apiUrl, application, githubToken });
}
if (application.destinationDockerId) { if (application.destinationDockerId) {
isRunning = await checkContainer(application.destinationDocker.engine, id); isRunning = await checkContainer(application.destinationDocker.engine, id);
} }
return { return {
status: 200,
body: { body: {
isRunning, isRunning,
ghToken,
githubToken,
application, application,
appId appId,
} githubToken,
gitlabToken
},
headers: {}
}; };
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@@ -62,7 +52,8 @@ export const post: RequestHandler = async (event) => {
buildCommand, buildCommand,
startCommand, startCommand,
baseDirectory, baseDirectory,
publishDirectory publishDirectory,
phpModules
} = await event.request.json(); } = await event.request.json();
if (port) port = Number(port); if (port) port = Number(port);
@@ -78,7 +69,8 @@ export const post: RequestHandler = async (event) => {
buildCommand, buildCommand,
startCommand, startCommand,
baseDirectory, baseDirectory,
publishDirectory publishDirectory,
phpModules
}); });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {

View File

@@ -42,16 +42,142 @@
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
import type Prisma from '@prisma/client'; import type Prisma from '@prisma/client';
import { getDomain, notNodeDeployments, staticDeployments } from '$lib/components/common'; import { notNodeDeployments, staticDeployments } from '$lib/components/common';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import { post } from '$lib/api'; import { post } from '$lib/api';
import cuid from 'cuid';
import { browser } from '$app/env';
import Select from 'svelte-select';
const { id } = $page.params; const { id } = $page.params;
let collection = [
'amqp',
'apcu',
'apcu_bc',
'ast',
'bcmath',
'blackfire',
'bz2',
'calendar',
'cmark',
'csv',
'dba',
'decimal',
'ds',
'enchant',
'ev',
'event',
'excimer',
'exif',
'ffi',
'gd',
'gearman',
'geoip',
'geospatial',
'gettext',
'gmagick',
'gmp',
'gnupg',
'grpc',
'http',
'igbinary',
'imagick',
'imap',
'inotify',
'interbase',
'intl',
'ioncube_loader',
'jsmin',
'json_post',
'ldap',
'lzf',
'mailparse',
'maxminddb',
'mcrypt',
'memcache',
'memcached',
'mongo',
'mongodb',
'mosquitto',
'msgpack',
'mssql',
'mysqli',
'oauth',
'oci8',
'odbc',
'opcache',
'opencensus',
'openswoole',
'parallel',
'pcntl',
'pcov',
'pdo_dblib',
'pdo_firebird',
'pdo_mysql',
'pdo_oci',
'pdo_odbc',
'pdo_pgsql',
'pdo_sqlsrv',
'pgsql',
'propro',
'protobuf',
'pspell',
'pthreads',
'raphf',
'rdkafka',
'recode',
'redis',
'seaslog',
'shmop',
'smbclient',
'snmp',
'snuffleupagus',
'soap',
'sockets',
'solr',
'sourceguardian',
'spx',
'sqlsrv',
'ssh2',
'stomp',
'swoole',
'sybase_ct',
'sysvmsg',
'sysvsem',
'sysvshm',
'tensor',
'tidy',
'timezonedb',
'uopz',
'uploadprogress',
'uuid',
'vips',
'wddx',
'xdebug',
'xhprof',
'xlswriter',
'xmldiff',
'xmlrpc',
'xsl',
'yac',
'yaml',
'yar',
'zephir_parser',
'zip',
'zookeeper',
'zstd'
];
let domainEl: HTMLInputElement; let domainEl: HTMLInputElement;
let loading = false; let loading = false;
let forceSave = false;
let debug = application.settings.debug; let debug = application.settings.debug;
let previews = application.settings.previews; let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts;
let autodeploy = application.settings.autodeploy;
if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
application.fqdn = `http://${cuid()}.demo.coolify.io`;
}
onMount(() => { onMount(() => {
domainEl.focus(); domainEl.focus();
@@ -64,20 +190,49 @@
if (name === 'previews') { if (name === 'previews') {
previews = !previews; previews = !previews;
} }
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
if (name === 'autodeploy') {
autodeploy = !autodeploy;
}
try { try {
await post(`/applications/${id}/settings.json`, { previews, debug }); await post(`/applications/${id}/settings.json`, {
previews,
debug,
dualCerts,
autodeploy,
branch: application.branch,
projectId: application.projectId
});
return toast.push('Settings saved.'); return toast.push('Settings saved.');
} catch ({ error }) { } catch ({ error }) {
if (name === 'debug') {
debug = !debug;
}
if (name === 'previews') {
previews = !previews;
}
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
if (name === 'autodeploy') {
autodeploy = !autodeploy;
}
return errorNotification(error); return errorNotification(error);
} }
} }
async function handleSubmit() { async function handleSubmit() {
loading = true; loading = true;
try { try {
await post(`/applications/${id}/check.json`, { fqdn: application.fqdn }); const tempPhpModules = application.phpModules?.map((module) => module.value).toString() || '';
await post(`/applications/${id}.json`, { ...application }); await post(`/applications/${id}/check.json`, { fqdn: application.fqdn, forceSave });
await post(`/applications/${id}.json`, { ...application, phpModules: tempPhpModules });
return window.location.reload(); return window.location.reload();
} catch ({ error }) { } catch ({ error }) {
if (error.startsWith('DNS not set')) {
forceSave = true;
}
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading = false; loading = false;
@@ -163,228 +318,251 @@
<button <button
type="submit" type="submit"
class:bg-green-600={!loading} class:bg-green-600={!loading}
class:bg-orange-600={forceSave}
class:hover:bg-green-500={!loading} class:hover:bg-green-500={!loading}
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button class:hover:bg-orange-400={forceSave}
disabled={loading}
>{loading ? 'Saving...' : forceSave ? 'Are you sure to continue?' : 'Save'}</button
> >
{/if} {/if}
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<div class="mt-2 grid grid-cols-3 items-center"> <div class="mt-2 grid grid-cols-2 items-center">
<label for="name">Name</label> <label for="name" class="text-base font-bold text-stone-100">Name</label>
<div class="col-span-2 "> <input
readonly={!$session.isAdmin}
name="name"
id="name"
bind:value={application.name}
required
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="gitSource" class="text-base font-bold text-stone-100">Git Source</label>
<a
href={$session.isAdmin
? `/applications/${id}/configuration/source?from=/applications/${id}`
: ''}
class="no-underline"
><input
value={application.gitSource.name}
id="gitSource"
disabled
class="cursor-pointer hover:bg-coolgray-500"
/></a
>
</div>
<div class="grid grid-cols-2 items-center">
<label for="repository" class="text-base font-bold text-stone-100">Git Repository</label>
<a
href={$session.isAdmin
? `/applications/${id}/configuration/repository?from=/applications/${id}&to=/applications/${id}/configuration/buildpack`
: ''}
class="no-underline"
><input
value="{application.repository}/{application.branch}"
id="repository"
disabled
class="cursor-pointer hover:bg-coolgray-500"
/></a
>
</div>
<div class="grid grid-cols-2 items-center">
<label for="buildPack" class="text-base font-bold text-stone-100">Build Pack</label>
<a
href={$session.isAdmin
? `/applications/${id}/configuration/buildpack?from=/applications/${id}`
: ''}
class="no-underline "
>
<input <input
readonly={!$session.isAdmin} value={application.buildPack}
name="name" id="buildPack"
id="name" disabled
bind:value={application.name} class="cursor-pointer hover:bg-coolgray-500"
required /></a
>
</div>
<div class="grid grid-cols-2 items-center pb-8">
<label for="destination" class="text-base font-bold text-stone-100">Destination</label>
<div class="no-underline">
<input
value={application.destinationDocker.name}
id="destination"
disabled
class="bg-transparent "
/> />
</div> </div>
</div> </div>
<div class="grid grid-cols-3 items-center">
<label for="gitSource">Git Source</label>
<div class="col-span-2">
<a
href={$session.isAdmin
? `/applications/${id}/configuration/source?from=/applications/${id}`
: ''}
class="no-underline"
><input
value={application.gitSource.name}
id="gitSource"
disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500"
/></a
>
</div>
</div>
<div class="grid grid-cols-3 items-center">
<label for="repository">Git Repository</label>
<div class="col-span-2">
<a
href={$session.isAdmin
? `/applications/${id}/configuration/repository?from=/applications/${id}&to=/applications/${id}/configuration/buildpack`
: ''}
class="no-underline"
><input
value="{application.repository}/{application.branch}"
id="repository"
disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500"
/></a
>
</div>
</div>
<div class="grid grid-cols-3 items-center">
<label for="buildPack">Build Pack</label>
<div class="col-span-2">
<a
href={$session.isAdmin
? `/applications/${id}/configuration/buildpack?from=/applications/${id}`
: ''}
class="no-underline "
>
<input
value={application.buildPack}
id="buildPack"
disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500"
/></a
>
</div>
</div>
<div class="grid grid-cols-3 items-center pb-8">
<label for="destination">Destination</label>
<div class="col-span-2">
<div class="no-underline">
<input
value={application.destinationDocker.name}
id="destination"
disabled
class="bg-transparent "
/>
</div>
</div>
</div>
</div> </div>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">Application</div> <div class="title">Application</div>
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-3"> <div class="grid grid-cols-2">
<label for="fqdn" class="pt-2">Domain (FQDN)</label> <div class="flex-col">
<div class="col-span-2"> <label for="fqdn" class="pt-2 text-base font-bold text-stone-100">Domain (FQDN)</label>
<input {#if browser && window.location.hostname === 'demo.coolify.io'}
readonly={!$session.isAdmin || isRunning} <Explainer
disabled={!$session.isAdmin || isRunning} text="<span class='text-white font-bold'>You can use the predefined random domain name or enter your own domain name.</span>"
bind:this={domainEl} />
name="fqdn" {/if}
id="fqdn"
bind:value={application.fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io"
required
/>
<Explainer <Explainer
text="If you specify <span class='text-green-600 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-green-600 font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application." text="If you specify <span class='text-green-500 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-green-500 font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-white font-bold'>You must set your DNS to point to the server IP in advance.</span>"
/> />
</div> </div>
<input
readonly={!$session.isAdmin || isRunning}
disabled={!$session.isAdmin || isRunning}
bind:this={domainEl}
name="fqdn"
id="fqdn"
bind:value={application.fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io"
required
/>
</div>
<div class="grid grid-cols-2 items-center pb-8">
<Setting
dataTooltip="Must be stopped to modify."
disabled={isRunning}
isCenter={false}
bind:setting={dualCerts}
title="Generate SSL for www and non-www?"
description="It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-green-500'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both."
on:click={() => !isRunning && changeSettings('dualCerts')}
/>
</div> </div>
{#if !staticDeployments.includes(application.buildPack)} {#if !staticDeployments.includes(application.buildPack)}
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="port">Port</label> <label for="port" class="text-base font-bold text-stone-100">Port</label>
<div class="col-span-2">
<input
readonly={!$session.isAdmin}
name="port"
id="port"
bind:value={application.port}
placeholder="default: 3000"
/>
</div>
</div>
{/if}
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-3 items-center">
<label for="installCommand">Install Command</label>
<div class="col-span-2">
<input
readonly={!$session.isAdmin}
name="installCommand"
id="installCommand"
bind:value={application.installCommand}
placeholder="default: yarn install"
/>
</div>
</div>
<div class="grid grid-cols-3 items-center">
<label for="buildCommand">Build Command</label>
<div class="col-span-2">
<input
readonly={!$session.isAdmin}
name="buildCommand"
id="buildCommand"
bind:value={application.buildCommand}
placeholder="default: yarn build"
/>
</div>
</div>
<div class="grid grid-cols-3 items-center">
<label for="startCommand" class="">Start Command</label>
<div class="col-span-2">
<input
readonly={!$session.isAdmin}
name="startCommand"
id="startCommand"
bind:value={application.startCommand}
placeholder="default: yarn start"
/>
</div>
</div>
{/if}
<div class="grid grid-cols-3">
<label for="baseDirectory" class="pt-2">Base Directory</label>
<div class="col-span-2">
<input <input
readonly={!$session.isAdmin} readonly={!$session.isAdmin}
name="baseDirectory" name="port"
id="baseDirectory" id="port"
bind:value={application.baseDirectory} bind:value={application.port}
placeholder="default: /" placeholder="default: 3000"
/>
<Explainer
text="Directory to use as the base of all commands. <br> Could be useful with monorepos."
/> />
</div> </div>
</div> {/if}
{#if !notNodeDeployments.includes(application.buildPack)} {#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-3"> <div class="grid grid-cols-2 items-center">
<label for="publishDirectory" class="pt-2">Publish Directory</label> <label for="installCommand" class="text-base font-bold text-stone-100"
<div class="col-span-2"> >Install Command</label
<input >
readonly={!$session.isAdmin} <input
name="publishDirectory" readonly={!$session.isAdmin}
id="publishDirectory" name="installCommand"
bind:value={application.publishDirectory} id="installCommand"
placeholder=" default: /" bind:value={application.installCommand}
/> placeholder="default: yarn install"
<Explainer />
text="Directory containing all the assets for deployment. <br> For example: <span class='text-green-600 font-bold'>dist</span>,<span class='text-green-600 font-bold'>_site</span> or <span class='text-green-600 font-bold'>public</span>." </div>
<div class="grid grid-cols-2 items-center">
<label for="buildCommand" class="text-base font-bold text-stone-100">Build Command</label>
<input
readonly={!$session.isAdmin}
name="buildCommand"
id="buildCommand"
bind:value={application.buildCommand}
placeholder="default: yarn build"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="startCommand" class="text-base font-bold text-stone-100">Start Command</label>
<input
readonly={!$session.isAdmin}
name="startCommand"
id="startCommand"
bind:value={application.startCommand}
placeholder="default: yarn start"
/>
</div>
{/if}
{#if application.buildPack === 'php'}
<div class="grid grid-cols-2 items-center">
<label for="startCommand" class="text-base font-bold text-stone-100">PHP Modules</label>
<div class="svelte-select">
<Select
isMulti={true}
bind:value={application.phpModules}
items={collection}
placeholder="Select PHP modules to add..."
/> />
</div> </div>
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2 items-center">
<div class="flex-col">
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"
>Base Directory</label
>
<Explainer
text="Directory to use as the base for all commands.<br>Could be useful with <span class='text-green-500 font-bold'>monorepos</span>."
/>
</div>
<input
readonly={!$session.isAdmin}
name="baseDirectory"
id="baseDirectory"
bind:value={application.baseDirectory}
placeholder="default: /"
/>
</div>
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center">
<div class="flex-col">
<label for="publishDirectory" class="pt-2 text-base font-bold text-stone-100"
>Publish Directory</label
>
<Explainer
text="Directory containing all the assets for deployment. <br> For example: <span class='text-green-500 font-bold'>dist</span>,<span class='text-green-500 font-bold'>_site</span> or <span class='text-green-500 font-bold'>public</span>."
/>
</div>
<input
readonly={!$session.isAdmin}
name="publishDirectory"
id="publishDirectory"
bind:value={application.publishDirectory}
placeholder=" default: /"
/>
</div>
{/if}
</div> </div>
</form> </form>
<div class="flex space-x-1 pb-5 font-bold"> <div class="flex space-x-1 pb-5 font-bold">
<div class="title">Features</div> <div class="title">Features</div>
</div> </div>
<div class="px-4 pb-10 sm:px-6"> <div class="px-10 pb-10">
<!-- <ul class="mt-2 divide-y divide-stone-800"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
bind:setting={forceSSL} isCenter={false}
on:click={() => changeSettings('forceSSL')} bind:setting={autodeploy}
title="Force https" on:click={() => changeSettings('autodeploy')}
description="Creates a https redirect for all requests from http and also generates a https certificate for the domain through Let's Encrypt." title="Enable Automatic Deployment"
description="Enable automatic deployment through webhooks."
/> />
</ul> --> </div>
<ul class="mt-2 divide-y divide-stone-800"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
isCenter={false}
bind:setting={previews} bind:setting={previews}
on:click={() => changeSettings('previews')} on:click={() => changeSettings('previews')}
title="Enable MR/PR Previews" title="Enable MR/PR Previews"
description="Creates previews from pull and merge requests." description="Enable preview deployments from pull or merge requests."
/> />
</ul> </div>
<ul class="mt-2 divide-y divide-stone-800"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
isCenter={false}
bind:setting={debug} bind:setting={debug}
on:click={() => changeSettings('debug')} on:click={() => changeSettings('debug')}
title="Debug Logs" title="Debug Logs"
description="Enable debug logs during build phase. <br>(<span class='text-red-500'>sensitive information</span> could be visible in logs)" description="Enable debug logs during build phase.<br><span class='text-red-500 font-bold'>Sensitive information</span> could be visible and saved in logs."
/> />
</ul> </div>
</div> </div>
</div> </div>

View File

@@ -43,11 +43,11 @@
logs = logs.concat(responseLogs.map((log) => ({ ...log, line: cleanAnsiCodes(log.line) }))); logs = logs.concat(responseLogs.map((log) => ({ ...log, line: cleanAnsiCodes(log.line) })));
loading = false; loading = false;
streamInterval = setInterval(async () => { streamInterval = setInterval(async () => {
if (status !== 'running') { if (status !== 'running' && status !== 'queued') {
clearInterval(streamInterval); clearInterval(streamInterval);
return; return;
} }
const nextSequence = logs[logs.length - 1].time; const nextSequence = logs[logs.length - 1]?.time || 0;
try { try {
const data = await get( const data = await get(
`/applications/${id}/logs/build/build.json?buildId=${buildId}&sequence=${nextSequence}` `/applications/${id}/logs/build/build.json?buildId=${buildId}&sequence=${nextSequence}`
@@ -83,37 +83,42 @@
{#if currentStatus === 'running'} {#if currentStatus === 'running'}
<LoadingLogs /> <LoadingLogs />
{/if} {/if}
<div class="flex justify-end sticky top-0 p-2"> {#if currentStatus === 'queued'}
<button <div class="text-center">Queued and waiting for execution.</div>
on:click={followBuild} {:else}
data-tooltip="Follow logs" <div class="flex justify-end sticky top-0 p-2">
class:text-green-500={followingBuild} <button
> on:click={followBuild}
<svg class="bg-transparent"
xmlns="http://www.w3.org/2000/svg" data-tooltip="Follow logs"
class="w-6 h-6" class:text-green-500={followingBuild}
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <svg
<circle cx="12" cy="12" r="9" /> xmlns="http://www.w3.org/2000/svg"
<line x1="8" y1="12" x2="12" y2="16" /> class="w-6 h-6"
<line x1="12" y1="8" x2="12" y2="16" /> viewBox="0 0 24 24"
<line x1="16" y1="12" x2="12" y2="16" /> stroke-width="1.5"
</svg> stroke="currentColor"
</button> fill="none"
</div> stroke-linecap="round"
<div stroke-linejoin="round"
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12" >
bind:this={logsEl} <path stroke="none" d="M0 0h24v24H0z" fill="none" />
> <circle cx="12" cy="12" r="9" />
{#each logs as log} <line x1="8" y1="12" x2="12" y2="16" />
<div>{log.line + '\n'}</div> <line x1="12" y1="8" x2="12" y2="16" />
{/each} <line x1="16" y1="12" x2="12" y2="16" />
</div> </svg>
</button>
</div>
<div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl}
>
{#each logs as log}
<div>{log.line + '\n'}</div>
{/each}
</div>
{/if}
</div> </div>
{/if} {/if}

View File

@@ -19,7 +19,7 @@ export const get: RequestHandler = async (event) => {
return { return {
body: { body: {
logs, logs,
status: data?.status || 'running' status: data?.status
} }
}; };
} catch (error) { } catch (error) {

View File

@@ -51,10 +51,10 @@
build.took = data.builds[0].took; build.took = data.builds[0].took;
build.since = data.builds[0].since; build.since = data.builds[0].since;
} }
window.location.reload();
return build; return build;
}); });
return; return window.location.reload();
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} }
@@ -82,7 +82,7 @@
} }
async function loadBuild(build) { async function loadBuild(build) {
buildId = build; buildId = build;
goto(`/applications/${id}/logs/build?buildId=${buildId}`); await goto(`/applications/${id}/logs/build?buildId=${buildId}`);
} }
</script> </script>
@@ -94,17 +94,19 @@
<div class="block flex-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex"> <div class="block flex-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex">
<div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 "> <div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 ">
<div class="top-4 md:sticky"> <div class="top-4 md:sticky">
{#each builds as build (build.id)} {#each builds as build, index (build.id)}
<div <div
data-tooltip={new Intl.DateTimeFormat('default', dateOptions).format( data-tooltip={new Intl.DateTimeFormat('default', dateOptions).format(
new Date(build.createdAt) new Date(build.createdAt)
) + `\n${build.status}`} ) + `\n${build.status}`}
on:click={() => loadBuild(build.id)} on:click={() => loadBuild(build.id)}
class="tooltip-top flex cursor-pointer items-center justify-center rounded-r border-l-2 border-transparent py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl " class:rounded-tr={index === 0}
class:rounded-br={index === builds.length - 1}
class="tooltip-top flex cursor-pointer items-center justify-center border-l-2 border-transparent py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl "
class:bg-coolgray-400={buildId === build.id} class:bg-coolgray-400={buildId === build.id}
class:border-red-500={build.status === 'failed'} class:border-red-500={build.status === 'failed'}
class:border-green-500={build.status === 'success'} class:border-green-500={build.status === 'success'}
class:border-yellow-500={build.status === 'inprogress'} class:border-yellow-500={build.status === 'running'}
> >
<div class="flex-col px-2"> <div class="flex-col px-2">
<div class="text-sm font-bold"> <div class="text-sm font-bold">
@@ -119,6 +121,8 @@
<div class="w-48 text-center text-xs"> <div class="w-48 text-center text-xs">
{#if build.status === 'running'} {#if build.status === 'running'}
<div class="font-bold">Running</div> <div class="font-bold">Running</div>
{:else if build.status === 'queued'}
<div class="font-bold">Queued</div>
{:else} {:else}
<div>{build.since}</div> <div>{build.since}</div>
<div>Finished in <span class="font-bold">{build.took}s</span></div> <div>Finished in <span class="font-bold">{build.took}s</span></div>

View File

@@ -77,11 +77,12 @@
{#if logs.length === 0} {#if logs.length === 0}
<div class="text-xl font-bold tracking-tighter">Waiting for the logs...</div> <div class="text-xl font-bold tracking-tighter">Waiting for the logs...</div>
{:else} {:else}
<div class="relative w-full"> <div class="relative">
<LoadingLogs /> <LoadingLogs />
<div class="flex justify-end sticky top-0 p-2"> <div class="flex justify-end sticky top-0 p-2">
<button <button
on:click={followBuild} on:click={followBuild}
class="bg-transparent"
data-tooltip="Follow logs" data-tooltip="Follow logs"
class:text-green-500={followingBuild} class:text-green-500={followingBuild}
> >
@@ -104,12 +105,14 @@
</button> </button>
</div> </div>
<div <div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 p-6 whitespace-pre-wrap break-words w-full mb-10 -mt-12" class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl} bind:this={logsEl}
> >
{#each logs as log} <div class="px-2">
{log + '\n'} {#each logs as log}
{/each} {log + '\n'}
{/each}
</div>
</div> </div>
</div> </div>
{/if} {/if}

View File

@@ -11,6 +11,9 @@ export const get: RequestHandler = async (event) => {
const { id } = event.params; const { id } = event.params;
try { try {
const secrets = await db.listSecrets(id);
const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret);
const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret);
const destinationDocker = await db.getDestinationByApplicationId({ id, teamId }); const destinationDocker = await db.getDestinationByApplicationId({ id, teamId });
const docker = dockerInstance({ destinationDocker }); const docker = dockerInstance({ destinationDocker });
const listContainers = await docker.engine.listContainers({ const listContainers = await docker.engine.listContainers({
@@ -35,7 +38,13 @@ export const get: RequestHandler = async (event) => {
}); });
return { return {
body: { body: {
containers: jsonContainers containers: jsonContainers,
applicationSecrets: applicationSecrets.sort((a, b) => {
return ('' + a.name).localeCompare(b.name);
}),
PRMRSecrets: PRMRSecrets.sort((a, b) => {
return ('' + a.name).localeCompare(b.name);
})
} }
}; };
} catch (error) { } catch (error) {

View File

@@ -22,8 +22,19 @@
<script lang="ts"> <script lang="ts">
export let containers; export let containers;
export let application; export let application;
export let PRMRSecrets;
export let applicationSecrets;
import { getDomain } from '$lib/components/common'; import { getDomain } from '$lib/components/common';
import Secret from '../secrets/_Secret.svelte';
import { get } from '$lib/api';
import { page } from '$app/stores';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params;
async function refreshSecrets() {
const data = await get(`/applications/${id}/secrets.json`);
PRMRSecrets = [...data.secrets];
}
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
@@ -32,7 +43,45 @@
</div> </div>
</div> </div>
<div class="mx-auto max-w-4xl px-6"> {#if applicationSecrets.length !== 0}
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">Name</th>
<th scope="col">Value</th>
<th scope="col" class="w-64 text-center">Need during buildtime?</th>
<th scope="col" class="w-96 text-center">Action</th>
</tr>
</thead>
<tbody>
{#each applicationSecrets as secret}
{#key secret.id}
<tr>
<Secret
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
isPRMRSecret
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
</tr>
{/key}
{/each}
</tbody>
</table>
</div>
{/if}
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={applicationSecrets.length === 0
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
: "These values overwrite application secrets in PR/MR deployments. Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
/>
</div>
<div class="mx-auto max-w-4xl py-10">
<div class="flex flex-wrap justify-center space-x-2"> <div class="flex flex-wrap justify-center space-x-2">
{#if containers.length > 0} {#if containers.length > 0}
{#each containers as container} {#each containers as container}

View File

@@ -3,14 +3,19 @@
export let value = ''; export let value = '';
export let isBuildSecret = false; export let isBuildSecret = false;
export let isNewSecret = false; export let isNewSecret = false;
export let isPRMRSecret = false;
export let PRMRSecret = {};
if (isPRMRSecret) value = PRMRSecret.value;
import { page } from '$app/stores'; import { page } from '$app/stores';
import { del, post } from '$lib/api'; import { del, post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let nameEl;
let valueEl;
const { id } = $page.params; const { id } = $page.params;
async function removeSecret() { async function removeSecret() {
try { try {
@@ -25,24 +30,24 @@
return errorNotification(error); return errorNotification(error);
} }
} }
async function saveSecret() { async function saveSecret(isNew = false) {
const nameValid = nameEl.checkValidity(); if (!name) return errorNotification('Name is required.');
const valueValid = valueEl.checkValidity(); if (!value) return errorNotification('Value is required.');
if (!nameValid) {
return nameEl.reportValidity();
}
if (!valueValid) {
return valueEl.reportValidity();
}
try { try {
await post(`/applications/${id}/secrets.json`, { name, value, isBuildSecret }); await post(`/applications/${id}/secrets.json`, {
name,
value,
isBuildSecret,
isPRMRSecret,
isNew
});
dispatch('refresh'); dispatch('refresh');
if (isNewSecret) { if (isNewSecret) {
name = ''; name = '';
value = ''; value = '';
isBuildSecret = false; isBuildSecret = false;
} }
toast.push('Secret saved.');
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} }
@@ -54,33 +59,29 @@
} }
</script> </script>
<td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white"> <td>
<input <input
id="secretName" id={isNewSecret ? 'secretName' : 'secretNameNew'}
bind:this={nameEl}
bind:value={name} bind:value={name}
required required
placeholder="EXAMPLE_VARIABLE" placeholder="EXAMPLE_VARIABLE"
class="-mx-2 w-64 border-2 border-transparent" class=" border border-dashed border-coolgray-300"
readonly={!isNewSecret} readonly={!isNewSecret}
class:bg-transparent={!isNewSecret} class:bg-transparent={!isNewSecret}
class:cursor-not-allowed={!isNewSecret} class:cursor-not-allowed={!isNewSecret}
/> />
</td> </td>
<td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white"> <td>
<input <CopyPasswordField
id="secretValue" id={isNewSecret ? 'secretValue' : 'secretValueNew'}
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
isPasswordField={true}
bind:value bind:value
bind:this={valueEl}
required required
placeholder="J$#@UIO%HO#$U%H" placeholder="J$#@UIO%HO#$U%H"
class="-mx-2 w-64 border-2 border-transparent"
class:bg-transparent={!isNewSecret}
class:cursor-not-allowed={!isNewSecret}
readonly={!isNewSecret}
/> />
</td> </td>
<td class="whitespace-nowrap px-6 py-2 text-center text-sm font-medium text-white"> <td class="text-center">
<div <div
type="button" type="button"
on:click={setSecretValue} on:click={setSecretValue}
@@ -129,14 +130,21 @@
</span> </span>
</div> </div>
</td> </td>
<td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white"> <td>
{#if isNewSecret} {#if isNewSecret}
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="w-24 bg-green-600 hover:bg-green-500" on:click={saveSecret}>Add</button> <button class="bg-green-600 hover:bg-green-500" on:click={() => saveSecret(true)}>Add</button>
</div> </div>
{:else} {:else}
<div class="flex justify-center items-end"> <div class="flex flex-row justify-center space-x-2">
<button class="w-24 bg-red-600 hover:bg-red-500" on:click={removeSecret}>Remove</button> <div class="flex items-center justify-center">
<button class="" on:click={() => saveSecret(false)}>Set</button>
</div>
{#if !isPRMRSecret}
<div class="flex justify-center items-end">
<button class="bg-red-600 hover:bg-red-500" on:click={removeSecret}>Remove</button>
</div>
{/if}
</div> </div>
{/if} {/if}
</td> </td>

View File

@@ -7,8 +7,9 @@ export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params;
try { try {
const secrets = await db.listSecrets({ applicationId: event.params.id }); const secrets = await (await db.listSecrets(id)).filter((secret) => !secret.isPRMRSecret);
return { return {
status: 200, status: 200,
body: { body: {
@@ -27,16 +28,22 @@ export const post: RequestHandler = async (event) => {
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
const { name, value, isBuildSecret } = await event.request.json(); const { name, value, isBuildSecret, isPRMRSecret, isNew } = await event.request.json();
try { try {
const found = await db.isSecretExists({ id, name }); if (isNew) {
if (found) { const found = await db.isSecretExists({ id, name, isPRMRSecret });
throw { if (found) {
error: `Secret ${name} already exists.` throw {
}; error: `Secret ${name} already exists.`
};
} else {
await db.createSecret({ id, name, value, isBuildSecret, isPRMRSecret });
return {
status: 201
};
}
} else { } else {
await db.createSecret({ id, name, value, isBuildSecret }); await db.updateSecret({ id, name, value, isBuildSecret, isPRMRSecret });
return { return {
status: 201 status: 201
}; };

View File

@@ -41,37 +41,22 @@
</div> </div>
</div> </div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4"> <div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<table class="mx-auto"> <table class="mx-auto border-separate text-left">
<thead class=" rounded-xl border-b border-coolgray-500"> <thead>
<tr> <tr class="h-12">
<th <th scope="col">Name</th>
scope="col" <th scope="col">Value</th>
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-warmGray-400" <th scope="col" class="w-64 text-center">Need during buildtime?</th>
>Name</th <th scope="col" class="w-96 text-center">Action</th>
>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-warmGray-400"
>Value</th
>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-warmGray-400"
>Need during buildtime?</th
>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-warmGray-400"
/>
</tr> </tr>
</thead> </thead>
<tbody class=""> <tbody>
{#each secrets as secret} {#each secrets as secret}
{#key secret.id} {#key secret.id}
<tr class="hover:bg-coolgray-200"> <tr>
<Secret <Secret
name={secret.name} name={secret.name}
value={secret.value ? secret.value : 'ENCRYPTED'} value={secret.value}
isBuildSecret={secret.isBuildSecret} isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets} on:refresh={refreshSecrets}
/> />

View File

@@ -8,10 +8,17 @@ export const post: RequestHandler = async (event) => {
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
const { debug, previews } = await event.request.json(); const { debug, previews, dualCerts, autodeploy, branch, projectId } = await event.request.json();
try { try {
await db.setApplicationSettings({ id, debug, previews }); const isDouble = await db.checkDoubleBranch(branch, projectId);
if (isDouble && autodeploy) {
throw {
message:
'Cannot activate automatic deployments until only one application is defined for this repository / branch.'
};
}
await db.setApplicationSettings({ id, debug, previews, dualCerts, autodeploy });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@@ -1,8 +1,7 @@
import { getDomain, getUserDetails } from '$lib/common'; import { asyncExecShell, getEngine, getUserDetails, removeDestinationDocker } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import { dockerInstance } from '$lib/docker'; import { checkContainer } from '$lib/haproxy';
import { removeProxyConfiguration } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
@@ -16,12 +15,13 @@ export const post: RequestHandler = async (event) => {
id, id,
teamId teamId
}); });
const domain = getDomain(fqdn);
if (destinationDockerId) { if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker }); const { engine } = destinationDocker;
await docker.engine.getContainer(id).stop(); const found = await checkContainer(engine, id);
if (found) {
await removeDestinationDocker({ id, engine });
}
} }
await removeProxyConfiguration({ domain });
return { return {
status: 200 status: 200
}; };

View File

@@ -13,6 +13,8 @@
import Nextjs from '$lib/components/svg/applications/Nextjs.svelte'; import Nextjs from '$lib/components/svg/applications/Nextjs.svelte';
import Gatsby from '$lib/components/svg/applications/Gatsby.svelte'; import Gatsby from '$lib/components/svg/applications/Gatsby.svelte';
import Docker from '$lib/components/svg/applications/Docker.svelte'; import Docker from '$lib/components/svg/applications/Docker.svelte';
import Astro from '$lib/components/svg/applications/Astro.svelte';
import Eleventy from '$lib/components/svg/applications/Eleventy.svelte';
const buildPack = application?.buildPack?.toLowerCase(); const buildPack = application?.buildPack?.toLowerCase();
</script> </script>
@@ -45,6 +47,10 @@
<Gatsby /> <Gatsby />
{:else if buildPack === 'docker'} {:else if buildPack === 'docker'}
<Docker /> <Docker />
{:else if buildPack === 'astro'}
<Astro />
{:else if buildPack === 'eleventy'}
<Eleventy />
{/if} {/if}
<div class="truncate text-center text-xl font-bold">{application.name}</div> <div class="truncate text-center text-xl font-bold">{application.name}</div>

View File

@@ -20,7 +20,7 @@
</script> </script>
<script lang="ts"> <script lang="ts">
export let applications: Array<Applications>; export let applications: Array<Application>;
import { session } from '$app/stores'; import { session } from '$app/stores';
import Application from './_Application.svelte'; import Application from './_Application.svelte';
</script> </script>

View File

@@ -6,73 +6,63 @@
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">CouchDB</div> <div class="title">CouchDB</div>
</div> </div>
<div class="px-10"> <div class="space-y-2 px-10">
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase">Default Database</label> <label for="defaultDatabase" class="text-base font-bold text-stone-100">Default Database</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField required
required readonly={database.defaultDatabase}
readonly={database.defaultDatabase} disabled={database.defaultDatabase}
disabled={database.defaultDatabase} placeholder="eg: mydb"
placeholder="eg: mydb" id="defaultDatabase"
id="defaultDatabase" name="defaultDatabase"
name="defaultDatabase" bind:value={database.defaultDatabase}
bind:value={database.defaultDatabase} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser">User</label> <label for="dbUser" class="text-base font-bold text-stone-100">User</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" id="dbUser"
id="dbUser" name="dbUser"
name="dbUser" value={database.dbUser}
value={database.dbUser} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword">Password</label> <label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" isPasswordField
isPasswordField id="dbUserPassword"
id="dbUserPassword" name="dbUserPassword"
name="dbUserPassword" value={database.dbUserPassword}
value={database.dbUserPassword} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser">Root User</label> <label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" id="rootUser"
id="rootUser" name="rootUser"
name="rootUser" value={database.rootUser}
value={database.rootUser} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword">Root's Password</label> <label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" isPasswordField
isPasswordField id="rootUserPassword"
id="rootUserPassword" name="rootUserPassword"
name="rootUserPassword" value={database.rootUserPassword}
value={database.rootUserPassword} />
/>
</div>
</div> </div>
</div> </div>

View File

@@ -56,9 +56,11 @@
appendOnly = !appendOnly; appendOnly = !appendOnly;
} }
try { try {
await post(`/databases/${id}/settings.json`, { isPublic, appendOnly }); const { publicPort } = await post(`/databases/${id}/settings.json`, { isPublic, appendOnly });
if (isPublic) {
database.publicPort = publicPort;
}
databaseUrl = generateUrl(); databaseUrl = generateUrl();
return;
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} }
@@ -88,70 +90,60 @@
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="name">Name</label> <label for="name" class="text-base font-bold text-stone-100">Name</label>
<div class="col-span-2 "> <input
<input readonly={!$session.isAdmin}
readonly={!$session.isAdmin} name="name"
name="name" id="name"
id="name" bind:value={database.name}
bind:value={database.name} required
required />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="destination">Destination</label> <label for="destination" class="text-base font-bold text-stone-100">Destination</label>
<div class="col-span-2"> {#if database.destinationDockerId}
{#if database.destinationDockerId} <div class="no-underline">
<div class="no-underline"> <input
<input value={database.destinationDocker.name}
value={database.destinationDocker.name} id="destination"
id="destination" disabled
disabled readonly
readonly class="bg-transparent "
class="bg-transparent " />
/> </div>
</div> {/if}
{/if}
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="version">Version</label> <label for="version" class="text-base font-bold text-stone-100">Version</label>
<div class="col-span-2 "> <input value={database.version} readonly disabled class="bg-transparent " />
<input value={database.version} readonly disabled class="bg-transparent " />
</div>
</div> </div>
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10 pt-2">
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="host">Host</label> <label for="host" class="text-base font-bold text-stone-100">Host</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField placeholder="Generated automatically after start"
placeholder="Generated automatically after start" isPasswordField={false}
isPasswordField={false} readonly
readonly disabled
disabled id="host"
id="host" name="host"
name="host" value={database.id}
value={database.id} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="publicPort">Port</label> <label for="publicPort" class="text-base font-bold text-stone-100">Port</label>
<div class="col-span-2"> <CopyPasswordField
<CopyPasswordField placeholder="Generated automatically after set to public"
placeholder="Generated automatically after start" id="publicPort"
id="publicPort" readonly
readonly disabled
disabled name="publicPort"
name="publicPort" value={isPublic ? database.publicPort : privatePort}
value={isPublic ? database.publicPort : privatePort} />
/>
</div>
</div> </div>
</div> </div>
<div class="grid grid-flow-row gap-2"> <div class="grid grid-flow-row gap-2">
@@ -166,44 +158,42 @@
{:else if database.type === 'couchdb'} {:else if database.type === 'couchdb'}
<CouchDb bind:database /> <CouchDb bind:database />
{/if} {/if}
<div class="grid grid-cols-3 items-center px-10 pb-8"> <div class="grid grid-cols-2 items-center px-10 pb-8">
<label for="url">Connection String</label> <label for="url" class="text-base font-bold text-stone-100">Connection String</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField textarea={true}
textarea={true} placeholder="Generated automatically after start"
placeholder="Generated automatically after start" isPasswordField={false}
isPasswordField={false} id="url"
id="url" name="url"
name="url" readonly
readonly disabled
disabled value={databaseUrl}
value={databaseUrl} />
/>
</div>
</div> </div>
</div> </div>
</form> </form>
<div class="flex space-x-1 pb-5 font-bold"> <div class="flex space-x-1 pb-5 font-bold">
<div class="title">Features</div> <div class="title">Features</div>
</div> </div>
<div class="px-4 pb-10 sm:px-6"> <div class="px-10 pb-10">
<ul class="mt-2 divide-y divide-stone-800"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
bind:setting={isPublic} bind:setting={isPublic}
on:click={() => changeSettings('isPublic')} on:click={() => changeSettings('isPublic')}
title="Set it public" title="Set it public"
description="Your database will be reachable over the internet. <br>Take security seriously in this case!" description="Your database will be reachable over the internet. <br>Take security seriously in this case!"
/> />
</ul> </div>
{#if database.type === 'redis'} {#if database.type === 'redis'}
<ul class="mt-2 divide-y divide-stone-800"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
bind:setting={appendOnly} bind:setting={appendOnly}
on:click={() => changeSettings('appendOnly')} on:click={() => changeSettings('appendOnly')}
title="Change append only mode" title="Change append only mode"
description="Useful if you would like to restore redis data from a backup.<br><span class='font-bold text-white'>Database restart is required.</span>" description="Useful if you would like to restore redis data from a backup.<br><span class='font-bold text-white'>Database restart is required.</span>"
/> />
</ul> </div>
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -6,32 +6,28 @@
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MongoDB</div> <div class="title">MongoDB</div>
</div> </div>
<div class="px-10"> <div class="space-y-2 px-10">
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser">Root User</label> <label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField placeholder="Generated automatically after start"
placeholder="Generated automatically after start" id="rootUser"
id="rootUser" readonly
readonly disabled
disabled name="rootUser"
name="rootUser" value={database.rootUser}
value={database.rootUser} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword">Root's Password</label> <label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField placeholder="Generated automatically after start"
placeholder="Generated automatically after start" isPasswordField={true}
isPasswordField={true} readonly
readonly disabled
disabled id="rootUserPassword"
id="rootUserPassword" name="rootUserPassword"
name="rootUserPassword" value={database.rootUserPassword}
value={database.rootUserPassword} />
/>
</div>
</div> </div>
</div> </div>

View File

@@ -6,73 +6,63 @@
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MySQL</div> <div class="title">MySQL</div>
</div> </div>
<div class=" px-10"> <div class="space-y-2 px-10">
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase">Default Database</label> <label for="defaultDatabase" class="text-base font-bold text-stone-100">Default Database</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField required
required readonly={database.defaultDatabase}
readonly={database.defaultDatabase} disabled={database.defaultDatabase}
disabled={database.defaultDatabase} placeholder="eg: mydb"
placeholder="eg: mydb" id="defaultDatabase"
id="defaultDatabase" name="defaultDatabase"
name="defaultDatabase" bind:value={database.defaultDatabase}
bind:value={database.defaultDatabase} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser">User</label> <label for="dbUser" class="text-base font-bold text-stone-100">User</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" id="dbUser"
id="dbUser" name="dbUser"
name="dbUser" value={database.dbUser}
value={database.dbUser} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword">Password</label> <label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" isPasswordField
isPasswordField id="dbUserPassword"
id="dbUserPassword" name="dbUserPassword"
name="dbUserPassword" value={database.dbUserPassword}
value={database.dbUserPassword} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser">Root User</label> <label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" id="rootUser"
id="rootUser" name="rootUser"
name="rootUser" value={database.rootUser}
value={database.rootUser} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword">Root's Password</label> <label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" isPasswordField
isPasswordField id="rootUserPassword"
id="rootUserPassword" name="rootUserPassword"
name="rootUserPassword" value={database.rootUserPassword}
value={database.rootUserPassword} />
/>
</div>
</div> </div>
</div> </div>

View File

@@ -6,46 +6,40 @@
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div> <div class="title">PostgreSQL</div>
</div> </div>
<div class="px-10"> <div class="space-y-2 px-10">
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase">Default Database</label> <label for="defaultDatabase" class="text-base font-bold text-stone-100">Default Database</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField required
required readonly={database.defaultDatabase}
readonly={database.defaultDatabase} disabled={database.defaultDatabase}
disabled={database.defaultDatabase} placeholder="eg: mydb"
placeholder="eg: mydb" id="defaultDatabase"
id="defaultDatabase" name="defaultDatabase"
name="defaultDatabase" bind:value={database.defaultDatabase}
bind:value={database.defaultDatabase} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser">User</label> <label for="dbUser" class="text-base font-bold text-stone-100">User</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" id="dbUser"
id="dbUser" name="dbUser"
name="dbUser" value={database.dbUser}
value={database.dbUser} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword">Password</label> <label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" isPasswordField
isPasswordField id="dbUserPassword"
id="dbUserPassword" name="dbUserPassword"
name="dbUserPassword" value={database.dbUserPassword}
value={database.dbUserPassword} />
/>
</div>
</div> </div>
</div> </div>

View File

@@ -6,33 +6,18 @@
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">Redis</div> <div class="title">Redis</div>
</div> </div>
<div class="px-10"> <div class="space-y-2 px-10">
<!-- <div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser">User</label> <label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField disabled
readonly readonly
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" isPasswordField
id="dbUser" id="dbUserPassword"
name="dbUser" name="dbUserPassword"
bind:value={database.dbUser} value={database.dbUserPassword}
/> />
</div>
</div> -->
<div class="grid grid-cols-3 items-center">
<label for="dbUserPassword">Password</label>
<div class="col-span-2 ">
<CopyPasswordField
disabled
readonly
placeholder="Generated automatically after start"
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
value={database.dbUserPassword}
/>
</div>
</div> </div>
<!-- <div class="grid grid-cols-3 items-center"> <!-- <div class="grid grid-cols-3 items-center">
<label for="rootUser">Root User</label> <label for="rootUser">Root User</label>

View File

@@ -120,7 +120,7 @@
title="Stop database" title="Stop database"
type="submit" type="submit"
disabled={!$session.isAdmin} disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-purple-600 hover:text-white" class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-red-500"
data-tooltip={$session.isAdmin data-tooltip={$session.isAdmin
? 'Stop database' ? 'Stop database'
: 'You do not have permission to stop the database.'} : 'You do not have permission to stop the database.'}
@@ -146,7 +146,7 @@
title="Start database" title="Start database"
type="submit" type="submit"
disabled={!$session.isAdmin} disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-purple-600 hover:text-white" class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-green-500"
data-tooltip={$session.isAdmin data-tooltip={$session.isAdmin
? 'Start database' ? 'Start database'
: 'You do not have permission to start the database.'} : 'You do not have permission to start the database.'}

View File

@@ -3,30 +3,39 @@ import * as db from '$lib/database';
import { generateDatabaseConfiguration, ErrorHandler } from '$lib/database'; import { generateDatabaseConfiguration, ErrorHandler } from '$lib/database';
import { startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy'; import { startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import getPort, { portNumbers } from 'get-port';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { status, body, teamId } = await getUserDetails(event); const { status, body, teamId } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
const { isPublic, appendOnly = true } = await event.request.json(); const { isPublic, appendOnly = true } = await event.request.json();
const publicPort = await getPort({ port: portNumbers(minPort, maxPort) });
try { try {
await db.setDatabase({ id, isPublic, appendOnly }); await db.setDatabase({ id, isPublic, appendOnly });
const database = await db.getDatabase({ id, teamId }); const database = await db.getDatabase({ id, teamId });
const { destinationDockerId, destinationDocker, publicPort } = database; const { destinationDockerId, destinationDocker, publicPort: oldPublicPort } = database;
const { privatePort } = generateDatabaseConfiguration(database); const { privatePort } = generateDatabaseConfiguration(database);
if (destinationDockerId) { if (destinationDockerId) {
if (isPublic) { if (isPublic) {
await db.prisma.database.update({ where: { id }, data: { publicPort } });
await startTcpProxy(destinationDocker, id, publicPort, privatePort); await startTcpProxy(destinationDocker, id, publicPort, privatePort);
} else { } else {
await stopTcpHttpProxy(destinationDocker, publicPort); await db.prisma.database.update({ where: { id }, data: { publicPort: null } });
await stopTcpHttpProxy(destinationDocker, oldPublicPort);
} }
} }
return { return {
status: 201 status: 201,
body: {
publicPort
}
}; };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@@ -15,6 +15,7 @@ export const post: RequestHandler = async (event) => {
const everStarted = await stopDatabase(database); const everStarted = await stopDatabase(database);
if (everStarted) await stopTcpHttpProxy(database.destinationDocker, database.publicPort); if (everStarted) await stopTcpHttpProxy(database.destinationDocker, database.publicPort);
await db.setDatabase({ id, isPublic: false }); await db.setDatabase({ id, isPublic: false });
await db.prisma.database.update({ where: { id }, data: { publicPort: null } });
return { return {
status: 200 status: 200

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