Merge branch 'main' into fix/github-token

This commit is contained in:
Andras Bacsai 2022-02-21 09:51:46 +01:00 committed by GitHub
commit 69845a020a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
90 changed files with 1600 additions and 1115 deletions

View File

@ -1,7 +1,7 @@
{ {
"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.16",
"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",
@ -25,9 +25,9 @@
"prepare": "husky install" "prepare": "husky install"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-node": "1.0.0-next.67", "@sveltejs/adapter-node": "1.0.0-next.68",
"@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.278",
"@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.18",
@ -50,7 +50,7 @@
"svelte": "3.46.4", "svelte": "3.46.4",
"svelte-check": "2.4.3", "svelte-check": "2.4.3",
"svelte-preprocess": "4.10.3", "svelte-preprocess": "4.10.3",
"tailwindcss": "3.0.22", "tailwindcss": "3.0.23",
"ts-node": "10.5.0", "ts-node": "10.5.0",
"tslib": "2.3.1", "tslib": "2.3.1",
"typescript": "4.5.5" "typescript": "4.5.5"
@ -59,9 +59,9 @@
"dependencies": { "dependencies": {
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@prisma/client": "3.9.2", "@prisma/client": "3.9.2",
"@sentry/node": "6.17.8", "@sentry/node": "6.17.9",
"bcrypt": "5.0.1", "bcrypt": "5.0.1",
"bullmq": "1.72.0", "bullmq": "1.73.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",
@ -69,14 +69,15 @@
"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.0",
"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",
"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"

145
pnpm-lock.yaml generated
View File

@ -3,10 +3,10 @@ lockfileVersion: 5.3
specifiers: specifiers:
'@iarna/toml': 2.2.5 '@iarna/toml': 2.2.5
'@prisma/client': 3.9.2 '@prisma/client': 3.9.2
'@sentry/node': 6.17.8 '@sentry/node': 6.17.9
'@sveltejs/adapter-node': 1.0.0-next.67 '@sveltejs/adapter-node': 1.0.0-next.68
'@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.278
'@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.18
@ -16,7 +16,7 @@ specifiers:
'@zerodevx/svelte-toast': 0.6.3 '@zerodevx/svelte-toast': 0.6.3
autoprefixer: 10.4.2 autoprefixer: 10.4.2
bcrypt: 5.0.1 bcrypt: 5.0.1
bullmq: 1.72.0 bullmq: 1.73.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
@ -28,7 +28,7 @@ specifiers:
eslint-config-prettier: 8.3.0 eslint-config-prettier: 8.3.0
eslint-plugin-svelte3: 3.2.1 eslint-plugin-svelte3: 3.2.1
generate-password: 1.7.0 generate-password: 1.7.0
get-port: 6.0.0 get-port: 6.1.0
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
@ -43,20 +43,21 @@ specifiers:
prisma: 3.9.2 prisma: 3.9.2
svelte: 3.46.4 svelte: 3.46.4
svelte-check: 2.4.3 svelte-check: 2.4.3
svelte-kit-cookie-session: 2.0.5 svelte-kit-cookie-session: 2.1.2
svelte-preprocess: 4.10.3 svelte-preprocess: 4.10.3
tailwindcss: 3.0.22 tailwindcss: 3.0.23
tailwindcss-scrollbar: ^0.1.0
ts-node: 10.5.0 ts-node: 10.5.0
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.5.5 typescript: 4.5.5
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.9.2_prisma@3.9.2
'@sentry/node': 6.17.8 '@sentry/node': 6.17.9
bcrypt: 5.0.1 bcrypt: 5.0.1
bullmq: 1.72.0 bullmq: 1.73.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
@ -64,19 +65,20 @@ dependencies:
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.0
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
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.68
'@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.278_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.18
@ -99,7 +101,7 @@ devDependencies:
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.3_postcss@8.4.6+svelte@3.46.4
svelte-preprocess: 4.10.3_88b359da5cac6d8f6ee1bbb7080a3fa9 svelte-preprocess: 4.10.3_88b359da5cac6d8f6ee1bbb7080a3fa9
tailwindcss: 3.0.22_c940fbabf228b85b1c73d314b43e31f1 tailwindcss: 3.0.23_c940fbabf228b85b1c73d314b43e31f1
ts-node: 10.5.0_f3bd4037939c2ed2942ba074291f8ef2 ts-node: 10.5.0_f3bd4037939c2ed2942ba074291f8ef2
tslib: 2.3.1 tslib: 2.3.1
typescript: 4.5.5 typescript: 4.5.5
@ -293,56 +295,56 @@ packages:
picomatch: 2.3.0 picomatch: 2.3.0
dev: true dev: true
/@sentry/core/6.17.8: /@sentry/core/6.17.9:
resolution: resolution:
{ {
integrity: sha512-4WTjgQom75Rvgn6XYy6e7vMIbWlj8utau1wWvr7kjqFKuuuuycRvPgVzAdVr4B3WDHHCInAZpUchsOLs2qwIEA== integrity: sha512-14KalmTholGUtgdh9TklO+jUpyQ/D3OGkhlH1rnGQGoJgFy2eYm+s+MnUEMxFdGIUCz5kOteuNqYZxaDmFagpQ==
} }
engines: { node: '>=6' } engines: { node: '>=6' }
dependencies: dependencies:
'@sentry/hub': 6.17.8 '@sentry/hub': 6.17.9
'@sentry/minimal': 6.17.8 '@sentry/minimal': 6.17.9
'@sentry/types': 6.17.8 '@sentry/types': 6.17.9
'@sentry/utils': 6.17.8 '@sentry/utils': 6.17.9
tslib: 1.14.1 tslib: 1.14.1
dev: false dev: false
/@sentry/hub/6.17.8: /@sentry/hub/6.17.9:
resolution: resolution:
{ {
integrity: sha512-GW0XYpkoQu/kSJaTLfsF4extHDOBPNRnT0qKr/YO20Z5wGxYp8LsdnAuU3njcFHcAV2F/QDTj2BPq1U385/4+A== integrity: sha512-34EdrweWDbBV9EzEFIXcO+JeoyQmKzQVJxpTKZoJA6PUwf2NrndaUdjlkDEtBEzjuLUTxhLxtOzEsYs1O6RVcg==
} }
engines: { node: '>=6' } engines: { node: '>=6' }
dependencies: dependencies:
'@sentry/types': 6.17.8 '@sentry/types': 6.17.9
'@sentry/utils': 6.17.8 '@sentry/utils': 6.17.9
tslib: 1.14.1 tslib: 1.14.1
dev: false dev: false
/@sentry/minimal/6.17.8: /@sentry/minimal/6.17.9:
resolution: resolution:
{ {
integrity: sha512-VJXFZBO/O8SViK0fdzodxpNr+pbpgczNgLpz/MNuSooV6EBesgCMVjXtxDUp1Ie1odc0GUprN/ZMLYBmYdIrKQ== integrity: sha512-T3PMCHcKk6lkZq6zKgANrYJJxXBXKOe+ousV1Fas1rVBMv7dtKfsa4itqQHszcW9shusPDiaQKIJ4zRLE5LKmg==
} }
engines: { node: '>=6' } engines: { node: '>=6' }
dependencies: dependencies:
'@sentry/hub': 6.17.8 '@sentry/hub': 6.17.9
'@sentry/types': 6.17.8 '@sentry/types': 6.17.9
tslib: 1.14.1 tslib: 1.14.1
dev: false dev: false
/@sentry/node/6.17.8: /@sentry/node/6.17.9:
resolution: resolution:
{ {
integrity: sha512-b3zg1XjKtxp7o821ENORO1CCzMM4QzKP01rzztMwyMcj28dmUq36QXoQAnwdKn7jEYkJdLnMeniIBR6U6NUJrQ== integrity: sha512-jbn+q7qPGOh6D7nYoYGaAlmuvMDpQmyMwBtUVYybuZp2AALe43O3Z4LtoJ+1+F31XowpsIPZx1mwNs4ZrILskA==
} }
engines: { node: '>=6' } engines: { node: '>=6' }
dependencies: dependencies:
'@sentry/core': 6.17.8 '@sentry/core': 6.17.9
'@sentry/hub': 6.17.8 '@sentry/hub': 6.17.9
'@sentry/tracing': 6.17.8 '@sentry/tracing': 6.17.9
'@sentry/types': 6.17.8 '@sentry/types': 6.17.9
'@sentry/utils': 6.17.8 '@sentry/utils': 6.17.9
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 +353,36 @@ packages:
- supports-color - supports-color
dev: false dev: false
/@sentry/tracing/6.17.8: /@sentry/tracing/6.17.9:
resolution: resolution:
{ {
integrity: sha512-WJ3W8O6iPI3w7MrzTnYcw3s5PGBNFqT4b9oBCl5Ndjexs8DsGlQOxjrsipo36z6TpnRHpAE4FEbOETb2R8JRJQ== integrity: sha512-5Rb/OS4ryNJLvz2nv6wyjwhifjy6veqaF9ffLrwFYij/WDy7m62ASBblxgeiI3fbPLX0aBRFWIJAq1vko26+AQ==
} }
engines: { node: '>=6' } engines: { node: '>=6' }
dependencies: dependencies:
'@sentry/hub': 6.17.8 '@sentry/hub': 6.17.9
'@sentry/minimal': 6.17.8 '@sentry/minimal': 6.17.9
'@sentry/types': 6.17.8 '@sentry/types': 6.17.9
'@sentry/utils': 6.17.8 '@sentry/utils': 6.17.9
tslib: 1.14.1 tslib: 1.14.1
dev: false dev: false
/@sentry/types/6.17.8: /@sentry/types/6.17.9:
resolution: resolution:
{ {
integrity: sha512-0i0f+dpvV62Pm5QMVBHNfEsTGIXoXRGQbeN2LGL4XbhzrzUmIrBPzrnZHv9c/JYtSJnI6A0B9OG7Bdlh3aku+Q== integrity: sha512-xuulX6qUCL14ayEOh/h6FUIvZtsi1Bx34dSOaWDrjXUOJHJAM7214uiqW1GZxPJ13YuaUIubjTSfDmSQ9CBzTw==
} }
engines: { node: '>=6' } engines: { node: '>=6' }
dev: false dev: false
/@sentry/utils/6.17.8: /@sentry/utils/6.17.9:
resolution: resolution:
{ {
integrity: sha512-cAOM53A5FHv95hpDuXKJU8rI4B1XdZ6qe3Yo+/nDS9QDpOgzvyjcItgXPvKW1wUjdHCcnwu7VBfBxB7teYOW9g== integrity: sha512-4eo9Z3JlJCGlGrQRbtZWL+L9NnlUXgTbfK3Lk7oO8D1ev8R5b5+iE6tZHTvU5rQRcq6zu+POT+tK5u9oxc/rnQ==
} }
engines: { node: '>=6' } engines: { node: '>=6' }
dependencies: dependencies:
'@sentry/types': 6.17.8 '@sentry/types': 6.17.9
tslib: 1.14.1 tslib: 1.14.1
dev: false dev: false
@ -392,28 +394,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.68:
resolution: resolution:
{ {
integrity: sha512-+LuLn91xARZsRANiQNIIDpMMncUTnP2pJc8tyL+FdpVvs5UtlvkYJpeCBPFqjjseRpIIbi8Slu89GCdrRXBDUg== integrity: sha512-MiEjtl15Aupm6bjirVlq0kkc9AL8qDXz/blsh4jYMsaiidmcEHeDgfZQFM5YiXy95DbxV30MAkhwCQiYK/J8Kw==
} }
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.278_svelte@3.46.4:
resolution: resolution:
{ {
integrity: sha512-+Tss6cQXmpi4Jno/ZP0zJ3INBLMED+WeW4UI81tmexheC76Y2p+cbInneKO/REx/8QFo1iroYrWAUkZPsOg8Ew== integrity: sha512-WT93Wnu05X9WG9BMMk/dj0gy6R7iXm9aXRDVgmIl9z8jT2ukejgmkhi5IwBYrK0OMIUALRVfukn+iy+srPc91Q==
} }
engines: { node: '>=14.13' } engines: { node: '>=14.13' }
hasBin: true hasBin: true
@ -1746,10 +1748,10 @@ packages:
ieee754: 1.2.1 ieee754: 1.2.1
dev: false dev: false
/bullmq/1.72.0: /bullmq/1.73.0:
resolution: resolution:
{ {
integrity: sha512-Q0pk6GphHyYsacpjZZFhjp/+TY+2g2FDsJS3qwIyskQL4j7vZaa1iYX3gKDEBn4C5eZMP1EOl9GWkm2bhdB0Wg== integrity: sha512-+BF7yeGagYD/iMkM3FA8Wvb3j3MyKE/OdXv404+nQjUsKXfL7PbqX5NSA9lBtFzOdyFx9ZWyKRnBwuGQsLfM0w==
} }
dependencies: dependencies:
cron-parser: 2.18.0 cron-parser: 2.18.0
@ -3114,10 +3116,10 @@ packages:
engines: { node: '>=8' } engines: { node: '>=8' }
dev: false dev: false
/get-port/6.0.0: /get-port/6.1.0:
resolution: resolution:
{ {
integrity: sha512-qSVkVF6Eq1GdL/cBNiFuP4nUHMF7OEMTqEjC6alR2N90u8BFOoO0PFhNTX2QtAUoGrz8NnrSWj85TZ8YXZ6LOA== integrity: sha512-JKnPFW/G2ZRirH/25sLK1aLBQktJfQLixzMMuMBP8A2G/ivSaIwdTnlJeO7PWeyhyIGVorezNf6+CXZU9i0cIQ==
} }
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
@ -5203,10 +5205,10 @@ 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
@ -5288,10 +5290,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_c940fbabf228b85b1c73d314b43e31f1
dev: false
/tailwindcss/3.0.23_c940fbabf228b85b1c73d314b43e31f1:
resolution:
{
integrity: sha512-+OZOV9ubyQ6oI2BXEhzw4HrqvgcARY38xv3zKcjnWtMIZstEsXdI9xftd1iB7+RbOnj2HOEzkA0OyB5BaSxPQA==
} }
engines: { node: '>=12.13.0' } engines: { node: '>=12.13.0' }
hasBin: true hasBin: true
@ -5515,10 +5528,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

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

@ -11,6 +11,7 @@ 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)
proxyPassword String proxyPassword String
proxyUser String proxyUser String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@ -97,6 +98,7 @@ 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)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@ -107,13 +109,14 @@ 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 BuildLog { model BuildLog {
@ -234,6 +237,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[]

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,38 @@ 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']) { let gitlabToken = event.locals.cookies.gitlabToken || null;
const { permission, teamId } = await getUserDetails(event, false); let ghToken = event.locals.cookies.ghToken || null;
event.locals.user = { if (event.locals.cookies['kit.session']) {
const { permission, teamId, userId } = await getUserDetails(event, false);
const newSession = {
userId,
teamId, teamId,
permission, permission,
isAdmin: permission === 'admin' || permission === 'owner' isAdmin: permission === 'admin' || permission === 'owner',
expires: event.locals.session.data.expires,
gitlabToken,
ghToken
}; };
if (JSON.stringify(event.locals.session.data) !== JSON.stringify(newSession)) {
event.locals.session.data = { ...newSession };
} }
if (cookies.gitlabToken) {
event.locals.gitlabToken = cookies.gitlabToken;
} }
}
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 +72,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

@ -9,7 +9,8 @@ export default async function ({
docker, docker,
buildId, buildId,
baseDirectory, baseDirectory,
secrets secrets,
pullmergeRequestId
}) { }) {
try { try {
let file = `${workdir}/Dockerfile`; let file = `${workdir}/Dockerfile`;
@ -24,8 +25,16 @@ export default async function ({
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`); Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
} }
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
}
}
}); });
} }
await fs.writeFile(`${file}`, Dockerfile.join('\n')); await fs.writeFile(`${file}`, Dockerfile.join('\n'));

View File

@ -2,8 +2,16 @@ import { 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 { 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> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
@ -11,8 +19,16 @@ const createDockerfile = async (data, image): Promise<void> => {
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`); Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
} }
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
}
}
}); });
} }
Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`); Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`);

View File

@ -2,8 +2,16 @@ import { 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 { 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> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
@ -11,8 +19,16 @@ const createDockerfile = async (data, image): Promise<void> => {
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`); Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
} }
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
}
}
}); });
} }
Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`); Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`);

View File

@ -2,8 +2,16 @@ import { 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 { 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> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
@ -11,8 +19,16 @@ const createDockerfile = async (data, image): Promise<void> => {
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`); Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
} }
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
}
}
}); });
} }
Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`); Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`);

View File

@ -2,8 +2,16 @@ 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}`);
@ -11,8 +19,16 @@ const createDockerfile = async (data, image): Promise<void> => {
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`); Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
} }
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
}
}
}); });
} }
if (buildCommand) { if (buildCommand) {

View File

@ -67,7 +67,7 @@ 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) {
@ -78,7 +78,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 },

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,7 +14,7 @@
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 actionsShow = false;
let isHttps = browser && window.location.protocol === 'https:'; let isHttps = browser && window.location.protocol === 'https:';
@ -29,11 +29,7 @@
} }
</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
@ -77,8 +73,7 @@
/> />
{/if} {/if}
{#if actionsShow} <div class="absolute top-0 right-0 m-3 cursor-pointer text-warmGray-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)}>
@ -141,5 +136,4 @@
{/if} {/if}
</div> </div>
</div> </div>
{/if} </div>
</span>

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="py-1 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,18 @@
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}>
<div <div
type="button" type="button"
on:click on:click
@ -58,5 +61,4 @@
</span> </span>
</span> </span>
</div> </div>
<!-- {/if} --> </div>
</li>

View File

@ -18,7 +18,6 @@ export const buildPacks = [
{ {
name: 'static', name: 'static',
...defaultBuildAndDeploy,
publishDirectory: 'dist', publishDirectory: 'dist',
port: 80, port: 80,
fancyName: 'Static', fancyName: 'Static',

View File

@ -1,5 +1,5 @@
import { decrypt, encrypt } from '$lib/crypto'; import { decrypt, encrypt } from '$lib/crypto';
import { removeProxyConfiguration, removeWwwRedirection } from '$lib/haproxy'; import { removeProxyConfiguration } 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';
@ -209,10 +209,10 @@ export async function configureApplication({
}); });
} }
export async function setApplicationSettings({ id, debug, previews }) { export async function setApplicationSettings({ id, debug, previews, dualCerts }) {
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 } } },
include: { destinationDocker: true } include: { destinationDocker: true }
}); });
} }

View File

@ -15,22 +15,41 @@ 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 isSecretExists({ id, name, isPRMRSecret }) {
return await prisma.secret.findFirst({ where: { name, applicationId: id } }); 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 = {
rejectOnNotFound: false export const prisma = new PrismaClient({
};
if (dev) {
prismaOptions = {
errorFormat: 'pretty', errorFormat: 'pretty',
rejectOnNotFound: false, 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) => {

View File

@ -1,19 +1,41 @@
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 listSecrets(applicationId: string) {
return await prisma.secret.findMany({ 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 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 updateSecret({ id, name, value, isBuildSecret, isPRMRSecret }) {
value = encrypt(value); value = encrypt(value);
return await prisma.secret.create({ const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
data: { name, value, isBuildSecret, application: { connect: { id } } } console.log(found);
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 removeSecret({ id, name }) { export async function removeSecret({ id, name }) {

View File

@ -107,13 +107,20 @@ export async function configureServiceType({ id, 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 } });

View File

@ -6,19 +6,23 @@ 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 }) {
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
}); });
// 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 +54,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 +65,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 +111,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

@ -13,16 +13,25 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
installCommand, installCommand,
buildCommand, buildCommand,
debug, debug,
secrets secrets,
pullmergeRequestId
} = data; } = data;
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');
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (!secret.isBuildSecret) { if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`); 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 // TODO: If build command defined, install command should be the default yarn install

View File

@ -2,7 +2,6 @@ import { dev } from '$app/env';
import { asyncExecShell, getDomain, getEngine } from '$lib/common'; import { asyncExecShell, getDomain, 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';
@ -49,7 +48,8 @@ export async function completeTransaction(transactionId) {
return await haproxy.put(`v2/services/haproxy/transactions/${transactionId}`); return await haproxy.put(`v2/services/haproxy/transactions/${transactionId}`);
} }
export async function removeProxyConfiguration({ domain }) { export async function removeProxyConfiguration(fqdn) {
const domain = getDomain(fqdn);
const haproxy = await haproxyInstance(); const haproxy = await haproxyInstance();
const backendFound = await haproxy const backendFound = await haproxy
.get(`v2/services/haproxy/configuration/backends/${domain}`) .get(`v2/services/haproxy/configuration/backends/${domain}`)
@ -65,10 +65,10 @@ export async function removeProxyConfiguration({ domain }) {
.json(); .json();
await completeTransaction(transactionId); await completeTransaction(transactionId);
} }
await removeWwwRedirection(domain); await forceSSLOffApplication(domain);
await removeWwwRedirection(fqdn);
} }
export async function forceSSLOffApplication({ domain }) { export async function forceSSLOffApplication(domain) {
if (!dev) {
const haproxy = await haproxyInstance(); const haproxy = await haproxyInstance();
await checkHAProxy(haproxy); await checkHAProxy(haproxy);
let transactionId; let transactionId;
@ -82,7 +82,9 @@ export async function forceSSLOffApplication({ domain }) {
}) })
.json(); .json();
if (rules.data.length > 0) { if (rules.data.length > 0) {
const rule = rules.data.find((rule) => rule.cond_test.includes(`-i ${domain}`)); const rule = rules.data.find((rule) =>
rule.cond_test.includes(`{ hdr(host) -i ${domain} } !{ ssl_fc }`)
);
if (rule) { if (rule) {
transactionId = await getNextTransactionId(); transactionId = await getNextTransactionId();
@ -102,12 +104,8 @@ export async function forceSSLOffApplication({ domain }) {
} finally { } finally {
if (transactionId) await completeTransaction(transactionId); if (transactionId) await completeTransaction(transactionId);
} }
} else {
console.log(`[DEBUG] Removing ssl for ${domain}`);
}
} }
export async function forceSSLOnApplication({ domain }) { export async function forceSSLOnApplication(domain) {
if (!dev) {
const haproxy = await haproxyInstance(); const haproxy = await haproxyInstance();
await checkHAProxy(haproxy); await checkHAProxy(haproxy);
let transactionId; let transactionId;
@ -144,7 +142,7 @@ export async function forceSSLOnApplication({ domain }) {
type: 'redirect', type: 'redirect',
redir_type: 'scheme', redir_type: 'scheme',
redir_value: 'https', redir_value: 'https',
redir_code: 301 redir_code: dev ? 302 : 301
} }
}) })
.json(); .json();
@ -154,9 +152,6 @@ export async function forceSSLOnApplication({ domain }) {
} finally { } finally {
if (transactionId) await completeTransaction(transactionId); if (transactionId) await completeTransaction(transactionId);
} }
} else {
console.log(`[DEBUG] Adding ssl for ${domain}`);
}
} }
export async function deleteProxy({ id }) { export async function deleteProxy({ id }) {
@ -274,6 +269,7 @@ export async function configureProxyForApplication({ domain, imageId, applicatio
export async function configureCoolifyProxyOff(fqdn) { export async function configureCoolifyProxyOff(fqdn) {
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const haproxy = await haproxyInstance(); const haproxy = await haproxyInstance();
await checkHAProxy(haproxy); await checkHAProxy(haproxy);
@ -288,10 +284,8 @@ export async function configureCoolifyProxyOff(fqdn) {
}) })
.json(); .json();
await completeTransaction(transactionId); await completeTransaction(transactionId);
if (!dev) { if (isHttps) await forceSSLOffApplication(domain);
await forceSSLOffApplication({ domain }); await removeWwwRedirection(fqdn);
}
await setWwwRedirection(fqdn);
} catch (error) { } catch (error) {
throw error?.response?.body || error; throw error?.response?.body || error;
} }
@ -565,7 +559,8 @@ export async function configureSimpleServiceProxyOn({ id, domain, port }) {
await completeTransaction(transactionId); await completeTransaction(transactionId);
} }
export async function configureSimpleServiceProxyOff({ domain }) { export async function configureSimpleServiceProxyOff(fqdn) {
const domain = getDomain(fqdn);
const haproxy = await haproxyInstance(); const haproxy = await haproxyInstance();
await checkHAProxy(haproxy); await checkHAProxy(haproxy);
try { try {
@ -580,11 +575,16 @@ export async function configureSimpleServiceProxyOff({ domain }) {
.json(); .json();
await completeTransaction(transactionId); await completeTransaction(transactionId);
} catch (error) {} } catch (error) {}
await removeWwwRedirection(domain); await forceSSLOffApplication(domain);
await removeWwwRedirection(fqdn);
return; return;
} }
export async function removeWwwRedirection(domain) { export async function removeWwwRedirection(fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
const haproxy = await haproxyInstance(); const haproxy = await haproxyInstance();
await checkHAProxy(); await checkHAProxy();
const rules: any = await haproxy const rules: any = await haproxy
@ -596,9 +596,7 @@ export async function removeWwwRedirection(domain) {
}) })
.json(); .json();
if (rules.data.length > 0) { if (rules.data.length > 0) {
const rule = rules.data.find((rule) => const rule = rules.data.find((rule) => rule.redir_value.includes(redirectValue));
rule.redir_value.includes(`${domain}%[capture.req.uri]`)
);
if (rule) { if (rule) {
const transactionId = await getNextTransactionId(); const transactionId = await getNextTransactionId();
await haproxy await haproxy
@ -623,6 +621,7 @@ export async function setWwwRedirection(fqdn) {
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.'); const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
const contTest = `{ req.hdr(host) -i ${isWWW ? domain.replace('www.', '') : `www.${domain}`} }`; const contTest = `{ req.hdr(host) -i ${isWWW ? domain.replace('www.', '') : `www.${domain}`} }`;
const rules: any = await haproxy const rules: any = await haproxy
.get(`v2/services/haproxy/configuration/http_request_rules`, { .get(`v2/services/haproxy/configuration/http_request_rules`, {
@ -634,13 +633,11 @@ export async function setWwwRedirection(fqdn) {
.json(); .json();
let nextRule = 0; let nextRule = 0;
if (rules.data.length > 0) { if (rules.data.length > 0) {
const rule = rules.data.find((rule) => const rule = rules.data.find((rule) => rule.redir_value.includes(redirectValue));
rule.redir_value.includes(`${domain}%[capture.req.uri]`)
);
if (rule) return; if (rule) return;
nextRule = rules.data[rules.data.length - 1].index + 1; nextRule = rules.data[rules.data.length - 1].index + 1;
} }
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
transactionId = await getNextTransactionId(); transactionId = await getNextTransactionId();
await haproxy await haproxy
.post(`v2/services/haproxy/configuration/http_request_rules`, { .post(`v2/services/haproxy/configuration/http_request_rules`, {

View File

@ -1,50 +1,78 @@
import { dev } from '$app/env'; import { dev } from '$app/env';
import { forceSSLOffApplication, forceSSLOnApplication, getNextTransactionId } from '$lib/haproxy'; import { forceSSLOffApplication, forceSSLOnApplication } from '$lib/haproxy';
import { asyncExecShell, getEngine } from './common'; import { asyncExecShell, getEngine } from './common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import cuid from 'cuid'; import cuid from 'cuid';
import getPort from 'get-port';
export async function letsEncrypt({ domain, isCoolify = false, id = null }) { export async function letsEncrypt({ domain, isCoolify = false, id = null }) {
try { try {
const nakedDomain = domain.replace('www.', '');
const wwwDomain = `www.${nakedDomain}`;
const randomCuid = cuid(); const randomCuid = cuid();
if (dev) { const randomPort = 9080;
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( let host;
`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` let dualCerts = false;
); if (isCoolify) {
if (stderr) throw new Error(stderr); const data = await db.prisma.setting.findFirst();
return; dualCerts = data.dualCerts;
host = 'unix:///var/run/docker.sock';
} else {
// Check Application
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);
} }
let data: any = await db.prisma.application.findUnique({ if (applicationData?.settings?.dualCerts) {
where: { id }, dualCerts = applicationData.settings.dualCerts;
include: { destinationDocker: true } }
}); }
if (!data) { // Check Service
data = await db.prisma.service.findUnique({ const serviceData = await db.prisma.service.findUnique({
where: { id }, where: { id },
include: { destinationDocker: true } include: { destinationDocker: true }
}); });
if (serviceData) {
if (serviceData?.destinationDockerId && serviceData?.destinationDocker) {
host = getEngine(serviceData.destinationDocker.engine);
} }
// Set SSL with Let's encrypt if (serviceData?.dualCerts) {
if (data.destinationDockerId && data.destinationDocker) { dualCerts = serviceData.dualCerts;
const host = getEngine(data.destinationDocker.engine); }
}
}
await forceSSLOffApplication(domain);
if (dualCerts) {
await asyncExecShell( 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` `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' : ''
}`
); );
const { stderr } = await asyncExecShell( 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` `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"`
);
} else {
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"`
); );
if (stderr) throw new Error(stderr);
await forceSSLOnApplication({ domain });
}
} }
} catch (error) { } catch (error) {
if (error.code !== 0) {
throw error; throw error;
} }
} finally {
if (!isCoolify) {
await forceSSLOnApplication(domain);
}
}
} }

View File

@ -64,7 +64,6 @@ 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);
@ -205,7 +204,15 @@ export default async function (job) {
const envs = []; const envs = [];
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`); 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'));
@ -239,6 +246,8 @@ 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 { try {
@ -257,7 +266,9 @@ export default async function (job) {
}); });
} }
} catch (error) { } catch (error) {
saveBuildLog({ line: error.stdout || error, buildId, applicationId });
sentry.captureException(error); sentry.captureException(error);
throw new Error(error);
} }
} }
} }

View File

@ -127,7 +127,6 @@ buildWorker.on('completed', async (job: Bullmq.Job) => {
}); });
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) {
@ -136,7 +135,11 @@ buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => {
const workdir = `/tmp/build-sources/${job.data.repository}`; const workdir = `/tmp/build-sources/${job.data.repository}`;
await asyncExecShell(`rm -fr ${workdir}`); 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

@ -48,7 +48,7 @@ export default async function () {
port port
}); });
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
if (isHttps) await forceSSLOnApplication({ domain }); if (isHttps) await forceSSLOnApplication(domain);
await setWwwRedirection(fqdn); await setWwwRedirection(fqdn);
} }
} }
@ -98,7 +98,7 @@ export default async function () {
await configureCoolifyProxyOn(fqdn); await configureCoolifyProxyOn(fqdn);
await setWwwRedirection(fqdn); await setWwwRedirection(fqdn);
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
if (isHttps) await forceSSLOnApplication({ domain }); if (isHttps) await forceSSLOnApplication(domain);
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

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

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`;
@ -49,7 +49,7 @@
}; };
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`);
@ -84,7 +84,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
}); });
@ -129,7 +129,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>

View File

@ -17,7 +17,7 @@
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(); const { application, isRunning, appId } = await res.json();
if (!application || Object.entries(application).length === 0) { if (!application || Object.entries(application).length === 0) {
return { return {
status: 302, status: 302,
@ -42,8 +42,6 @@
}, },
stuff: { stuff: {
isRunning, isRunning,
ghToken,
githubToken,
application, application,
appId appId
} }

View File

@ -16,7 +16,7 @@ export const post: RequestHandler = async (event) => {
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 configured.`
}; };
} }
return { return {

View File

@ -3,13 +3,11 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
export let githubToken;
export let application; export let application;
import { page } from '$app/stores'; import { page, session } 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';
const { id } = $page.params; const { id } = $page.params;
@ -32,15 +30,12 @@
branch: undefined branch: undefined
}; };
let showSave = false; let showSave = false;
let token = null;
async function loadRepositoriesByPage(page = 0) { async function loadRepositoriesByPage(page = 0) {
return await get(`${apiUrl}/installation/repositories?per_page=100&page=${page}`, { return await get(`${apiUrl}/installation/repositories?per_page=100&page=${page}`, {
Authorization: `token ${$session.ghToken}` Authorization: `token ${$session.ghToken}`
}); });
} }
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();
@ -61,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 ${$session.ghToken}`
}); });
return; return;
} catch ({ error }) { } catch ({ error }) {

View File

@ -8,7 +8,6 @@
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';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');

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
} }
}; };
} }
@ -43,7 +42,6 @@
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;
@ -96,7 +94,7 @@
} }
} 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 ${$session.ghToken}`,
Accept: 'application/vnd.github.v2.json' Accept: 'application/vnd.github.v2.json'
}); });
const packageJson = files.find( const packageJson = files.find(
@ -113,7 +111,7 @@
foundConfig.buildPack = 'docker'; foundConfig.buildPack = 'docker';
} 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 ${$session.ghToken}`,
Accept: 'application/vnd.github.v2.raw' Accept: 'application/vnd.github.v2.raw'
}); });
const json = JSON.parse(data) || {}; const json = JSON.parse(data) || {};

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,7 +19,6 @@
<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';
@ -31,7 +29,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

@ -15,8 +15,8 @@ export const get: RequestHandler = async (event) => {
let githubToken = null; let githubToken = null;
let ghToken = null; let ghToken = null;
let isRunning = false; let isRunning = false;
const { id } = event.params; const { id } = event.params;
try { try {
const application = await db.getApplication({ id, teamId }); const application = await db.getApplication({ id, teamId });
const { gitSource } = application; const { gitSource } = application;
@ -52,15 +52,20 @@ export const get: RequestHandler = async (event) => {
if (application.destinationDockerId) { if (application.destinationDockerId) {
isRunning = await checkContainer(application.destinationDocker.engine, id); isRunning = await checkContainer(application.destinationDocker.engine, id);
} }
return { const payload = {
body: { body: {
isRunning, isRunning,
ghToken,
githubToken,
application, application,
appId appId
} },
headers: {}
}; };
if (ghToken) {
payload.headers = {
'set-cookie': [`ghToken=${ghToken}; HttpOnly; Path=/; Max-Age=15778800;`]
};
}
return payload;
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return ErrorHandler(error); return ErrorHandler(error);

View File

@ -42,7 +42,7 @@
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';
const { id } = $page.params; const { id } = $page.params;
@ -52,6 +52,7 @@
let loading = false; let loading = 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;
onMount(() => { onMount(() => {
domainEl.focus(); domainEl.focus();
@ -64,8 +65,11 @@
if (name === 'previews') { if (name === 'previews') {
previews = !previews; previews = !previews;
} }
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
try { try {
await post(`/applications/${id}/settings.json`, { previews, debug }); await post(`/applications/${id}/settings.json`, { previews, debug, dualCerts });
return toast.push('Settings saved.'); return toast.push('Settings saved.');
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
@ -193,7 +197,7 @@
value={application.gitSource.name} value={application.gitSource.name}
id="gitSource" id="gitSource"
disabled disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500" class="cursor-pointer hover:bg-coolgray-500"
/></a /></a
> >
</div> </div>
@ -210,7 +214,7 @@
value="{application.repository}/{application.branch}" value="{application.repository}/{application.branch}"
id="repository" id="repository"
disabled disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500" class="cursor-pointer hover:bg-coolgray-500"
/></a /></a
> >
</div> </div>
@ -228,7 +232,7 @@
value={application.buildPack} value={application.buildPack}
id="buildPack" id="buildPack"
disabled disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500" class="cursor-pointer hover:bg-coolgray-500"
/></a /></a
> >
</div> </div>
@ -252,7 +256,7 @@
</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-3">
<label for="fqdn" class="pt-2">Domain (FQDN)</label> <label for="fqdn" class="relative pt-2">Domain (FQDN)</label>
<div class="col-span-2"> <div class="col-span-2">
<input <input
readonly={!$session.isAdmin || isRunning} readonly={!$session.isAdmin || isRunning}
@ -266,11 +270,21 @@
required 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."
/> />
</div> </div>
</div> </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>
{#if !staticDeployments.includes(application.buildPack)} {#if !staticDeployments.includes(application.buildPack)}
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-3 items-center">
<label for="port">Port</label> <label for="port">Port</label>
@ -285,6 +299,7 @@
</div> </div>
</div> </div>
{/if} {/if}
{#if !notNodeDeployments.includes(application.buildPack)} {#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-3 items-center">
<label for="installCommand">Install Command</label> <label for="installCommand">Install Command</label>
@ -361,7 +376,6 @@
<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">
<!-- <ul class="mt-2 divide-y divide-stone-800"> <!-- <ul class="mt-2 divide-y divide-stone-800">
<Setting <Setting
bind:setting={forceSSL} bind:setting={forceSSL}
@ -370,21 +384,24 @@
description="Creates a https redirect for all requests from http and also generates a https certificate for the domain through Let's Encrypt." description="Creates a https redirect for all requests from http and also generates a https certificate for the domain through Let's Encrypt."
/> />
</ul> --> </ul> -->
<ul class="mt-2 divide-y divide-stone-800"> <div class="px-10 pb-10">
<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="Creates previews from pull and 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'>sensitive information</span> could be visible in logs)"
/> />
</ul> </div>
</div> </div>
</div> </div>

View File

@ -86,6 +86,7 @@
<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}
> >
@ -108,7 +109,7 @@
</button> </button>
</div> </div>
<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" 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} {#each logs as log}

View File

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

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,13 +105,15 @@
</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}
> >
<div class="px-2">
{#each logs as log} {#each logs as log}
{log + '\n'} {log + '\n'}
{/each} {/each}
</div> </div>
</div> </div>
</div>
{/if} {/if}
</div> </div>

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,57 @@
</div> </div>
</div> </div>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<table class="mx-auto">
<thead class=" rounded-xl border-b border-coolgray-500">
<tr>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-white">Name</th
>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-white"
>Value</th
>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-white"
>Need during buildtime?</th
>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-white"
/>
</tr>
</thead>
<tbody class="">
{#each applicationSecrets as secret}
{#key secret.id}
<tr class="h-20 transition duration-100 hover:bg-coolgray-400">
<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>
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={applicationSecrets.length === 0
? "<span class='font-bold text-white text-xl'>Please add secrets to the application first.</span> <br><br>These values overwrite application secrets in PR/MR deployments. 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);
} }
@ -56,8 +61,7 @@
<td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white"> <td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white">
<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"
@ -68,16 +72,13 @@
/> />
</td> </td>
<td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white"> <td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white">
<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="whitespace-nowrap px-6 py-2 text-center text-sm font-medium text-white">
@ -132,11 +133,20 @@
<td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white"> <td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white">
{#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-col space-y-2">
<div class="flex items-center justify-center">
<button class="w-24 bg-green-600 hover:bg-green-500" on:click={() => saveSecret(false)}
>Set</button
>
</div>
{#if !isPRMRSecret}
<div class="flex justify-center items-end"> <div class="flex justify-center items-end">
<button class="w-24 bg-red-600 hover:bg-red-500" on:click={removeSecret}>Remove</button> <button class="w-24 bg-red-600 hover:bg-red-500" on:click={removeSecret}>Remove</button>
</div> </div>
{/if} {/if}
</div>
{/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) {
const found = await db.isSecretExists({ id, name, isPRMRSecret });
if (found) { if (found) {
throw { throw {
error: `Secret ${name} already exists.` error: `Secret ${name} already exists.`
}; };
} else { } else {
await db.createSecret({ id, name, value, isBuildSecret }); await db.createSecret({ id, name, value, isBuildSecret, isPRMRSecret });
return {
status: 201
};
}
} else {
await db.updateSecret({ id, name, value, isBuildSecret, isPRMRSecret });
return { return {
status: 201 status: 201
}; };

View File

@ -46,32 +46,31 @@
<tr> <tr>
<th <th
scope="col" scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-warmGray-400" class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-white">Name</th
>Name</th
> >
<th <th
scope="col" scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-warmGray-400" class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-white"
>Value</th >Value</th
> >
<th <th
scope="col" scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-warmGray-400" class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-white"
>Need during buildtime?</th >Need during buildtime?</th
> >
<th <th
scope="col" scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-warmGray-400" class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-white"
/> />
</tr> </tr>
</thead> </thead>
<tbody class=""> <tbody class="">
{#each secrets as secret} {#each secrets as secret}
{#key secret.id} {#key secret.id}
<tr class="hover:bg-coolgray-200"> <tr class="h-20 transition duration-100 hover:bg-coolgray-400">
<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,10 @@ 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 } = await event.request.json();
try { try {
await db.setApplicationSettings({ id, debug, previews }); await db.setApplicationSettings({ id, debug, previews, dualCerts });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@ -16,12 +16,11 @@ export const post: RequestHandler = async (event) => {
id, id,
teamId teamId
}); });
const domain = getDomain(fqdn);
if (destinationDockerId) { if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker }); const docker = dockerInstance({ destinationDocker });
await docker.engine.getContainer(id).stop(); await docker.engine.getContainer(id).stop();
} }
await removeProxyConfiguration({ domain }); await removeProxyConfiguration(fqdn);
return { return {
status: 200 status: 200
}; };

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,10 +6,9 @@
<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">Default Database</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
required required
readonly={database.defaultDatabase} readonly={database.defaultDatabase}
@ -20,10 +19,8 @@
bind:value={database.defaultDatabase} bind:value={database.defaultDatabase}
/> />
</div> </div>
</div> <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-3 items-center">
<label for="dbUser">User</label> <label for="dbUser">User</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -33,10 +30,8 @@
value={database.dbUser} value={database.dbUser}
/> />
</div> </div>
</div> <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-3 items-center">
<label for="dbUserPassword">Password</label> <label for="dbUserPassword">Password</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -47,10 +42,8 @@
value={database.dbUserPassword} value={database.dbUserPassword}
/> />
</div> </div>
</div> <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-3 items-center">
<label for="rootUser">Root User</label> <label for="rootUser">Root User</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -60,10 +53,8 @@
value={database.rootUser} value={database.rootUser}
/> />
</div> </div>
</div> <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-3 items-center">
<label for="rootUserPassword">Root's Password</label> <label for="rootUserPassword">Root's Password</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -74,5 +65,4 @@
value={database.rootUserPassword} value={database.rootUserPassword}
/> />
</div> </div>
</div>
</div> </div>

View File

@ -88,9 +88,8 @@
</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">Name</label>
<div class="col-span-2 ">
<input <input
readonly={!$session.isAdmin} readonly={!$session.isAdmin}
name="name" name="name"
@ -99,10 +98,8 @@
required required
/> />
</div> </div>
</div> <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-3 items-center">
<label for="destination">Destination</label> <label for="destination">Destination</label>
<div class="col-span-2">
{#if database.destinationDockerId} {#if database.destinationDockerId}
<div class="no-underline"> <div class="no-underline">
<input <input
@ -115,20 +112,16 @@
</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">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">
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="host">Host</label> <label for="host">Host</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
isPasswordField={false} isPasswordField={false}
@ -139,10 +132,8 @@
value={database.id} value={database.id}
/> />
</div> </div>
</div> <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-3 items-center">
<label for="publicPort">Port</label> <label for="publicPort">Port</label>
<div class="col-span-2">
<CopyPasswordField <CopyPasswordField
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
id="publicPort" id="publicPort"
@ -153,7 +144,6 @@
/> />
</div> </div>
</div> </div>
</div>
<div class="grid grid-flow-row gap-2"> <div class="grid grid-flow-row gap-2">
{#if database.type === 'mysql'} {#if database.type === 'mysql'}
<MySql bind:database /> <MySql bind:database />
@ -166,9 +156,8 @@
{: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">Connection String</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
textarea={true} textarea={true}
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
@ -181,29 +170,28 @@
/> />
</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,10 +6,9 @@
<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">Root User</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
id="rootUser" id="rootUser"
@ -19,10 +18,8 @@
value={database.rootUser} value={database.rootUser}
/> />
</div> </div>
</div> <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-3 items-center">
<label for="rootUserPassword">Root's Password</label> <label for="rootUserPassword">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}
@ -33,5 +30,4 @@
value={database.rootUserPassword} value={database.rootUserPassword}
/> />
</div> </div>
</div>
</div> </div>

View File

@ -6,10 +6,9 @@
<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">Default Database</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
required required
readonly={database.defaultDatabase} readonly={database.defaultDatabase}
@ -20,10 +19,8 @@
bind:value={database.defaultDatabase} bind:value={database.defaultDatabase}
/> />
</div> </div>
</div> <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-3 items-center">
<label for="dbUser">User</label> <label for="dbUser">User</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -33,10 +30,8 @@
value={database.dbUser} value={database.dbUser}
/> />
</div> </div>
</div> <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-3 items-center">
<label for="dbUserPassword">Password</label> <label for="dbUserPassword">Password</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -47,10 +42,8 @@
value={database.dbUserPassword} value={database.dbUserPassword}
/> />
</div> </div>
</div> <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-3 items-center">
<label for="rootUser">Root User</label> <label for="rootUser">Root User</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -60,10 +53,8 @@
value={database.rootUser} value={database.rootUser}
/> />
</div> </div>
</div> <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-3 items-center">
<label for="rootUserPassword">Root's Password</label> <label for="rootUserPassword">Root's Password</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -74,5 +65,4 @@
value={database.rootUserPassword} value={database.rootUserPassword}
/> />
</div> </div>
</div>
</div> </div>

View File

@ -6,10 +6,9 @@
<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">Default Database</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
required required
readonly={database.defaultDatabase} readonly={database.defaultDatabase}
@ -20,10 +19,8 @@
bind:value={database.defaultDatabase} bind:value={database.defaultDatabase}
/> />
</div> </div>
</div> <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-3 items-center">
<label for="dbUser">User</label> <label for="dbUser">User</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -33,10 +30,8 @@
value={database.dbUser} value={database.dbUser}
/> />
</div> </div>
</div> <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-3 items-center">
<label for="dbUserPassword">Password</label> <label for="dbUserPassword">Password</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -47,5 +42,4 @@
value={database.dbUserPassword} value={database.dbUserPassword}
/> />
</div> </div>
</div>
</div> </div>

View File

@ -6,23 +6,9 @@
<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>
<div class="col-span-2 ">
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="dbUser"
name="dbUser"
bind:value={database.dbUser}
/>
</div>
</div> -->
<div class="grid grid-cols-3 items-center">
<label for="dbUserPassword">Password</label> <label for="dbUserPassword">Password</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
disabled disabled
readonly readonly
@ -33,7 +19,6 @@
value={database.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>
<div class="col-span-2 "> <div class="col-span-2 ">

View File

@ -181,13 +181,11 @@
/> />
</div> </div>
</div> </div>
<div class="flex justify-start"> <div class="grid grid-cols-2 items-center">
<ul class="mt-2 divide-y divide-stone-800">
<Setting <Setting
disabled={cannotDisable} disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed} bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting} on:click={changeProxySetting}
isPadding={false}
title="Use Coolify Proxy?" title="Use Coolify Proxy?"
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${ description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${
cannotDisable cannotDisable
@ -195,7 +193,6 @@
: '' : ''
}`} }`}
/> />
</ul>
</div> </div>
</form> </form>
</div> </div>

View File

@ -24,7 +24,7 @@ export const post: RequestHandler = async (event) => {
await configureCoolifyProxyOn(fqdn); await configureCoolifyProxyOn(fqdn);
await setWwwRedirection(fqdn); await setWwwRedirection(fqdn);
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
if (isHttps) await forceSSLOnApplication({ domain }); if (isHttps) await forceSSLOnApplication(domain);
return { return {
status: 200 status: 200
}; };

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, session }) => { export const load: Load = async ({ fetch, session }) => {
const url = `/index.json`; const url = `/dashboard.json`;
const res = await fetch(url); const res = await fetch(url);
if (res.ok) { if (res.ok) {

View File

@ -9,7 +9,7 @@
let emailEl; let emailEl;
let email, password; let email, password;
if (browser && $session.uid) { if (browser && $session.userId) {
goto('/'); goto('/');
} }
onMount(() => { onMount(() => {
@ -34,7 +34,7 @@
</script> </script>
<div class="flex h-screen flex-col items-center justify-center"> <div class="flex h-screen flex-col items-center justify-center">
{#if $session.uid} {#if $session.userId}
<div class="flex justify-center px-4 text-xl font-bold">Already logged in...</div> <div class="flex justify-center px-4 text-xl font-bold">Already logged in...</div>
{:else} {:else}
<div class="flex justify-center px-4"> <div class="flex justify-center px-4">
@ -67,6 +67,7 @@
class:text-stone-600={loading} class:text-stone-600={loading}
class:bg-coollabs={!loading}>{loading ? 'Authenticating...' : 'Login'}</button class:bg-coollabs={!loading}>{loading ? 'Authenticating...' : 'Login'}</button
> >
<button on:click|preventDefault={() => goto('/reset')}>Reset password</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -0,0 +1,26 @@
import type { RequestHandler } from '@sveltejs/kit';
import * as db from '$lib/database';
export const get: RequestHandler = async () => {
const users = await db.prisma.user.findMany({});
return {
status: 200,
body: {
users
}
};
};
export const post: RequestHandler = async (event) => {
const { secretKey } = await event.request.json();
if (secretKey !== process.env.COOLIFY_SECRET_KEY) {
return {
status: 500,
body: {
error: 'Invalid secret key.'
}
};
}
return {
status: 200
};
};

View File

@ -0,0 +1,96 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { get, post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
let secretKey;
let password = false;
let users = [];
async function handleSubmit() {
try {
await post(`/reset.json`, { secretKey });
password = true;
const data = await get('/reset.json');
users = data.users;
return;
} catch ({ error }) {
return errorNotification(error);
}
}
async function resetPassword(user) {
try {
await post(`/reset/password.json`, { secretKey, user });
toast.push('Password reset done.');
return;
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="icons fixed top-0 left-0 m-3 cursor-pointer" on:click={() => goto('/')}>
<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" />
<line x1="5" y1="12" x2="19" y2="12" />
<line x1="5" y1="12" x2="11" y2="18" />
<line x1="5" y1="12" x2="11" y2="6" />
</svg>
</div>
<div class="pb-10 pt-24 text-center text-4xl font-bold">Reset Password</div>
<div class="flex items-center justify-center">
{#if password}
<table class="mx-2 text-left">
<thead class="mb-2">
<tr>
<th class="px-2">Email</th>
<th>New password</th>
</tr>
</thead>
<tbody>
{#each users as user}
<tr>
<td class="px-2">{user.email}</td>
<td class="flex space-x-2">
<input
id="newPassword"
name="newPassword"
bind:value={user.newPassword}
placeholder="Super secure new password"
/>
<button
class="mx-auto my-4 w-32 bg-coollabs hover:bg-coollabs-100"
on:click={() => resetPassword(user)}>Reset</button
></td
>
</tr>
{/each}
</tbody>
</table>
{:else}
<form class="flex flex-col" on:submit|preventDefault={handleSubmit}>
<div class="text-center text-2xl py-2 font-bold">Secret Key</div>
<CopyPasswordField
isPasswordField={true}
id="secretKey"
name="secretKey"
bind:value={secretKey}
placeholder="You can find it in ~/coolify/.env (COOLIFY_SECRET_KEY)"
/>
<button type="submit" class="bg-coollabs hover:bg-coollabs-100 mx-auto w-32 my-4"
>Submit</button
>
</form>
{/if}
</div>

View File

@ -0,0 +1,27 @@
import type { RequestHandler } from '@sveltejs/kit';
import * as db from '$lib/database';
import { ErrorHandler, hashPassword } from '$lib/database';
export const post: RequestHandler = async (event) => {
const { secretKey, user } = await event.request.json();
if (secretKey !== process.env.COOLIFY_SECRET_KEY) {
return {
status: 500,
body: {
error: 'Invalid secret key.'
}
};
}
try {
const hashedPassword = await hashPassword(user.newPassword);
await db.prisma.user.update({
where: { email: user.email },
data: { password: hashedPassword }
});
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -7,9 +7,8 @@
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MinIO Server</div> <div class="title">MinIO Server</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="rootUser">Root User</label> <label for="rootUser">Root User</label>
<div class="col-span-2 ">
<input <input
name="rootUser" name="rootUser"
id="rootUser" id="rootUser"
@ -18,11 +17,9 @@
disabled disabled
readonly readonly
/> />
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="rootUserPassword">Root's Password</label> <label for="rootUserPassword">Root's Password</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
id="rootUserPassword" id="rootUserPassword"
isPasswordField isPasswordField
@ -31,11 +28,9 @@
name="rootUserPassword" name="rootUserPassword"
value={service.minio.rootUserPassword} value={service.minio.rootUserPassword}
/> />
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="publicPort">API Port</label> <label for="publicPort">API Port</label>
<div class="col-span-2 ">
<input <input
name="publicPort" name="publicPort"
id="publicPort" id="publicPort"
@ -44,5 +39,4 @@
readonly readonly
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
/> />
</div>
</div> </div>

View File

@ -7,9 +7,8 @@
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">Plausible Analytics</div> <div class="title">Plausible Analytics</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="email">Email Address</label> <label for="email">Email Address</label>
<div class="col-span-2">
<input <input
name="email" name="email"
id="email" id="email"
@ -19,11 +18,9 @@
bind:value={service.plausibleAnalytics.email} bind:value={service.plausibleAnalytics.email}
required required
/> />
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="username">Username</label> <label for="username">Username</label>
<div class="col-span-2">
<CopyPasswordField <CopyPasswordField
name="username" name="username"
id="username" id="username"
@ -33,11 +30,9 @@
bind:value={service.plausibleAnalytics.username} bind:value={service.plausibleAnalytics.username}
required required
/> />
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="password">Password</label> <label for="password">Password</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
id="password" id="password"
isPasswordField isPasswordField
@ -46,14 +41,12 @@
name="password" name="password"
value={service.plausibleAnalytics.password} value={service.plausibleAnalytics.password}
/> />
</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">PostgreSQL</div> <div class="title">PostgreSQL</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlUser">Username</label> <label for="postgresqlUser">Username</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
name="postgresqlUser" name="postgresqlUser"
id="postgresqlUser" id="postgresqlUser"
@ -61,11 +54,9 @@
readonly readonly
disabled disabled
/> />
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlPassword">Password</label> <label for="postgresqlPassword">Password</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
id="postgresqlPassword" id="postgresqlPassword"
isPasswordField isPasswordField
@ -74,11 +65,9 @@
name="postgresqlPassword" name="postgresqlPassword"
value={service.plausibleAnalytics.postgresqlPassword} value={service.plausibleAnalytics.postgresqlPassword}
/> />
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlDatabase">Database</label> <label for="postgresqlDatabase">Database</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
name="postgresqlDatabase" name="postgresqlDatabase"
id="postgresqlDatabase" id="postgresqlDatabase"
@ -86,7 +75,6 @@
readonly readonly
disabled disabled
/> />
</div>
</div> </div>
<!-- <div class="grid grid-cols-3 items-center"> <!-- <div class="grid grid-cols-3 items-center">
<label for="postgresqlPublicPort">Public Port</label> <label for="postgresqlPublicPort">Public Port</label>

View File

@ -7,6 +7,7 @@
import { post } from '$lib/api'; import { post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import MinIo from './_MinIO.svelte'; import MinIo from './_MinIO.svelte';
@ -18,6 +19,7 @@
let loading = false; let loading = false;
let loadingVerification = false; let loadingVerification = false;
let dualCerts = service.dualCerts;
async function handleSubmit() { async function handleSubmit() {
loading = true; loading = true;
@ -42,6 +44,17 @@
loadingVerification = false; loadingVerification = false;
} }
} }
async function changeSettings(name) {
try {
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
await post(`/services/${id}/settings.json`, { dualCerts });
return toast.push('Settings saved.');
} catch ({ error }) {
return errorNotification(error);
}
}
</script> </script>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-4xl px-6">
@ -67,10 +80,10 @@
{/if} {/if}
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2">
<div class="mt-2 grid grid-cols-3 items-center"> <div class="mt-2 grid grid-cols-2 items-center px-10">
<label for="name">Name</label> <label for="name">Name</label>
<div class="col-span-2 "> <div>
<input <input
readonly={!$session.isAdmin} readonly={!$session.isAdmin}
name="name" name="name"
@ -81,9 +94,9 @@
</div> </div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="destination">Destination</label> <label for="destination">Destination</label>
<div class="col-span-2"> <div>
{#if service.destinationDockerId} {#if service.destinationDockerId}
<div class="no-underline"> <div class="no-underline">
<input <input
@ -96,9 +109,9 @@
{/if} {/if}
</div> </div>
</div> </div>
<div class="grid grid-cols-3"> <div class="grid grid-cols-2 px-10">
<label for="fqdn" class="pt-2">Domain (FQDN)</label> <label for="fqdn" class="pt-2">Domain (FQDN)</label>
<div class="col-span-2 "> <div>
<CopyPasswordField <CopyPasswordField
placeholder="eg: https://analytics.coollabs.io" placeholder="eg: https://analytics.coollabs.io"
readonly={!$session.isAdmin && !isRunning} readonly={!$session.isAdmin && !isRunning}
@ -114,6 +127,16 @@
/> />
</div> </div>
</div> </div>
<div class="grid grid-cols-2 items-center px-10">
<Setting
disabled={isRunning}
dataTooltip="Must be stopped to modify."
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-pink-600'>both DNS entries</span> set in advance.<br><br>Service needs to be restarted."
on:click={() => !isRunning && changeSettings('dualCerts')}
/>
</div>
{#if service.type === 'plausibleanalytics'} {#if service.type === 'plausibleanalytics'}
<PlausibleAnalytics bind:service {readOnly} /> <PlausibleAnalytics bind:service {readOnly} />
{:else if service.type === 'minio'} {:else if service.type === 'minio'}

View File

@ -7,9 +7,8 @@
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">VSCode Server</div> <div class="title">VSCode Server</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="password">Password</label> <label for="password">Password</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
id="password" id="password"
isPasswordField isPasswordField
@ -18,5 +17,4 @@
name="password" name="password"
value={service.vscodeserver.password} value={service.vscodeserver.password}
/> />
</div>
</div> </div>

View File

@ -10,9 +10,8 @@
<div class="title">Wordpress</div> <div class="title">Wordpress</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="extraConfig">Extra Config</label> <label for="extraConfig">Extra Config</label>
<div class="col-span-2 ">
<textarea <textarea
disabled={isRunning} disabled={isRunning}
readonly={isRunning} readonly={isRunning}
@ -26,16 +25,14 @@
define('WP_ALLOW_MULTISITE', true); define('WP_ALLOW_MULTISITE', true);
define('MULTISITE', true); define('MULTISITE', true);
define('SUBDOMAIN_INSTALL', false);` define('SUBDOMAIN_INSTALL', false);`
: null}>{service.wordpress.extraConfig}</textarea : null}>{service.wordpress.extraConfig || 'N/A'}</textarea
> >
</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">MySQL</div> <div class="title">MySQL</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="mysqlDatabase">Database</label> <label for="mysqlDatabase">Database</label>
<div class="col-span-2 ">
<input <input
name="mysqlDatabase" name="mysqlDatabase"
id="mysqlDatabase" id="mysqlDatabase"
@ -45,11 +42,9 @@ define('SUBDOMAIN_INSTALL', false);`
bind:value={service.wordpress.mysqlDatabase} bind:value={service.wordpress.mysqlDatabase}
placeholder="eg: wordpress_db" placeholder="eg: wordpress_db"
/> />
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="mysqlRootUser">Root User</label> <label for="mysqlRootUser">Root User</label>
<div class="col-span-2 ">
<input <input
name="mysqlRootUser" name="mysqlRootUser"
id="mysqlRootUser" id="mysqlRootUser"
@ -58,11 +53,9 @@ define('SUBDOMAIN_INSTALL', false);`
disabled disabled
readonly readonly
/> />
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="mysqlRootUserPassword">Root's Password</label> <label for="mysqlRootUserPassword">Root's Password</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
id="mysqlRootUserPassword" id="mysqlRootUserPassword"
isPasswordField isPasswordField
@ -71,17 +64,13 @@ define('SUBDOMAIN_INSTALL', false);`
name="mysqlRootUserPassword" name="mysqlRootUserPassword"
value={service.wordpress.mysqlRootUserPassword} value={service.wordpress.mysqlRootUserPassword}
/> />
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="mysqlUser">User</label> <label for="mysqlUser">User</label>
<div class="col-span-2 ">
<input name="mysqlUser" id="mysqlUser" value={service.wordpress.mysqlUser} disabled readonly /> <input name="mysqlUser" id="mysqlUser" value={service.wordpress.mysqlUser} disabled readonly />
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="mysqlPassword">Password</label> <label for="mysqlPassword">Password</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
id="mysqlPassword" id="mysqlPassword"
isPasswordField isPasswordField
@ -90,5 +79,4 @@ define('SUBDOMAIN_INSTALL', false);`
name="mysqlPassword" name="mysqlPassword"
value={service.wordpress.mysqlPassword} value={service.wordpress.mysqlPassword}
/> />
</div>
</div> </div>

View File

@ -17,7 +17,7 @@ export const post: RequestHandler = async (event) => {
return { return {
status: found ? 500 : 200, status: found ? 500 : 200,
body: { body: {
error: found && `Domain ${getDomain(fqdn)} is already configured` error: found && `Domain ${getDomain(fqdn).replace('www.', '')} is already configured`
} }
}; };
} catch (error) { } catch (error) {

View File

@ -30,7 +30,7 @@ export const post: RequestHandler = async (event) => {
const { version } = await event.request.json(); const { version } = await event.request.json();
try { try {
await db.setService({ id, version }); await db.setServiceVersion({ id, version });
return { return {
status: 201 status: 201
}; };

View File

@ -35,7 +35,7 @@ export const post: RequestHandler = async (event) => {
} }
try { try {
await stopTcpHttpProxy(destinationDocker, publicPort); await stopTcpHttpProxy(destinationDocker, publicPort);
await configureSimpleServiceProxyOff({ domain }); await configureSimpleServiceProxyOff(fqdn);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@ -28,7 +28,7 @@ export const post: RequestHandler = async (event) => {
console.error(error); console.error(error);
} }
try { try {
await configureSimpleServiceProxyOff({ domain }); await configureSimpleServiceProxyOff(fqdn);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@ -60,7 +60,7 @@ export const post: RequestHandler = async (event) => {
} }
}, },
postgresql: { postgresql: {
volume: `${plausibleDbId}-postgresql-data:/var/lib/postgresql/data`, volume: `${plausibleDbId}-postgresql-data:/bitnami/postgresql/`,
image: 'bitnami/postgresql:13.2.0', image: 'bitnami/postgresql:13.2.0',
environmentVariables: { environmentVariables: {
POSTGRESQL_PASSWORD: postgresqlPassword, POSTGRESQL_PASSWORD: postgresqlPassword,
@ -136,7 +136,6 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
'sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run"', 'sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run"',
networks: [network], networks: [network],
environment: config.plausibleAnalytics.environmentVariables, environment: config.plausibleAnalytics.environmentVariables,
volumes: [config.postgresql.volume],
restart: 'always', restart: 'always',
depends_on: [`${id}-postgresql`, `${id}-clickhouse`], depends_on: [`${id}-postgresql`, `${id}-clickhouse`],
labels: makeLabelForServices('plausibleAnalytics') labels: makeLabelForServices('plausibleAnalytics')

View File

@ -38,7 +38,7 @@ export const post: RequestHandler = async (event) => {
} }
try { try {
await configureSimpleServiceProxyOff({ domain }); await configureSimpleServiceProxyOff(fqdn);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@ -0,0 +1,19 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const { dualCerts } = await event.request.json();
try {
await db.setServiceSettings({ id, dualCerts });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -28,7 +28,7 @@ export const post: RequestHandler = async (event) => {
console.error(error); console.error(error);
} }
try { try {
await configureSimpleServiceProxyOff({ domain }); await configureSimpleServiceProxyOff(fqdn);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@ -28,7 +28,7 @@ export const post: RequestHandler = async (event) => {
console.error(error); console.error(error);
} }
try { try {
await configureSimpleServiceProxyOff({ domain }); await configureSimpleServiceProxyOff(fqdn);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@ -31,7 +31,7 @@ export const post: RequestHandler = async (event) => {
console.error(error); console.error(error);
} }
try { try {
await configureSimpleServiceProxyOff({ domain }); await configureSimpleServiceProxyOff(fqdn);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@ -16,7 +16,7 @@ export const post: RequestHandler = async (event) => {
return { return {
status: found ? 500 : 200, status: found ? 500 : 200,
body: { body: {
error: found && `Domain ${fqdn} is already configured` error: found && `Domain ${fqdn.replace('www.', '')} is already configured`
} }
}; };
} catch (error) { } catch (error) {

View File

@ -3,10 +3,8 @@ import { getDomain, getUserDetails } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { listSettings, ErrorHandler } from '$lib/database'; import { listSettings, ErrorHandler } from '$lib/database';
import { import {
checkContainer,
configureCoolifyProxyOff, configureCoolifyProxyOff,
configureCoolifyProxyOn, configureCoolifyProxyOn,
forceSSLOffApplication,
forceSSLOnApplication, forceSSLOnApplication,
reloadHaproxy, reloadHaproxy,
removeWwwRedirection, removeWwwRedirection,
@ -15,6 +13,7 @@ import {
} from '$lib/haproxy'; } from '$lib/haproxy';
import { letsEncrypt } from '$lib/letsencrypt'; import { letsEncrypt } from '$lib/letsencrypt';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { promises as dns } from 'dns';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event); const { status, body } = await getUserDetails(event);
@ -45,14 +44,24 @@ export const del: RequestHandler = async (event) => {
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { fqdn } = await event.request.json(); const { fqdn } = await event.request.json();
let ip;
console.log(fqdn);
try {
ip = await dns.resolve(fqdn);
} catch (error) {
// Do not care.
}
try { try {
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
await db.prisma.setting.update({ where: { fqdn }, data: { fqdn: null } }); await db.prisma.setting.update({ where: { fqdn }, data: { fqdn: null } });
await configureCoolifyProxyOff(fqdn); await configureCoolifyProxyOff(fqdn);
await removeWwwRedirection(domain); await removeWwwRedirection(domain);
return { return {
status: 201 status: 200,
body: {
message: 'Domain removed',
redirect: ip ? `http://${ip[0]}:3000/settings` : undefined
}
}; };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);
@ -69,16 +78,20 @@ export const post: RequestHandler = async (event) => {
}; };
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { fqdn, isRegistrationEnabled } = await event.request.json(); const { fqdn, isRegistrationEnabled, dualCerts } = await event.request.json();
try { try {
const { const {
id, id,
fqdn: oldFqdn, fqdn: oldFqdn,
isRegistrationEnabled: oldIsRegistrationEnabled isRegistrationEnabled: oldIsRegistrationEnabled,
dualCerts: oldDualCerts
} = await db.listSettings(); } = await db.listSettings();
if (oldIsRegistrationEnabled !== isRegistrationEnabled) { if (oldIsRegistrationEnabled !== isRegistrationEnabled) {
await db.prisma.setting.update({ where: { id }, data: { isRegistrationEnabled } }); await db.prisma.setting.update({ where: { id }, data: { isRegistrationEnabled } });
} }
if (oldDualCerts !== dualCerts) {
await db.prisma.setting.update({ where: { id }, data: { dualCerts } });
}
if (oldFqdn && oldFqdn !== fqdn) { if (oldFqdn && oldFqdn !== fqdn) {
if (oldFqdn) { if (oldFqdn) {
const oldDomain = getDomain(oldFqdn); const oldDomain = getDomain(oldFqdn);
@ -93,9 +106,9 @@ export const post: RequestHandler = async (event) => {
if (domain) { if (domain) {
await configureCoolifyProxyOn(fqdn); await configureCoolifyProxyOn(fqdn);
await setWwwRedirection(fqdn); await setWwwRedirection(fqdn);
if (isHttps && !dev) { if (isHttps) {
await letsEncrypt({ domain, isCoolify: true }); await letsEncrypt({ domain, isCoolify: true });
await forceSSLOnApplication({ domain }); await forceSSLOnApplication(domain);
await reloadHaproxy('/var/run/docker.sock'); await reloadHaproxy('/var/run/docker.sock');
} }
} }

View File

@ -30,10 +30,13 @@
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { browser } from '$app/env'; import { browser } from '$app/env';
import { getDomain } from '$lib/components/common'; import { getDomain } from '$lib/components/common';
import { toast } from '@zerodevx/svelte-toast';
let isRegistrationEnabled = settings.isRegistrationEnabled; let isRegistrationEnabled = settings.isRegistrationEnabled;
let dualCerts = settings.dualCerts;
let fqdn = settings.fqdn; let fqdn = settings.fqdn;
let isFqdnSet = settings.fqdn; let isFqdnSet = !!settings.fqdn;
let loading = { let loading = {
save: false, save: false,
remove: false remove: false
@ -43,8 +46,8 @@
if (fqdn) { if (fqdn) {
loading.remove = true; loading.remove = true;
try { try {
await del(`/settings.json`, { fqdn }); const { redirect } = await del(`/settings.json`, { fqdn });
return window.location.reload(); return redirect ? window.location.replace(redirect) : window.location.reload();
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
@ -57,7 +60,11 @@
if (name === 'isRegistrationEnabled') { if (name === 'isRegistrationEnabled') {
isRegistrationEnabled = !isRegistrationEnabled; isRegistrationEnabled = !isRegistrationEnabled;
} }
return await post(`/settings.json`, { isRegistrationEnabled }); if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
await post(`/settings.json`, { isRegistrationEnabled, dualCerts });
return toast.push('Settings saved.');
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} }
@ -82,15 +89,15 @@
<div class="mr-4 text-2xl tracking-tight">Settings</div> <div class="mr-4 text-2xl tracking-tight">Settings</div>
</div> </div>
{#if $session.teamId === '0'} {#if $session.teamId === '0'}
<div class="mx-auto max-w-2xl"> <div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmit}> <form on:submit|preventDefault={handleSubmit}>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 py-6 font-bold">
<div class="title">Global Settings</div> <div class="title">Global Settings</div>
<button <button
type="submit" type="submit"
disabled={loading.save} disabled={loading.save}
class:bg-green-600={!loading.save} class:bg-yellow-500={!loading.save}
class:hover:bg-green-500={!loading.save} class:hover:bg-yellow-400={!loading.save}
class="mx-2 ">{loading.save ? 'Saving...' : 'Save'}</button class="mx-2 ">{loading.save ? 'Saving...' : 'Save'}</button
> >
{#if isFqdnSet} {#if isFqdnSet}
@ -103,10 +110,10 @@
> >
{/if} {/if}
</div> </div>
<div class="px-4 sm:px-6"> <div class="grid grid-flow-row gap-2 px-10">
<div class="flex space-x-4 py-4 px-4"> <div class="grid grid-cols-2 items-start">
<p class="pt-2 text-base font-bold text-stone-100">Domain (FQDN)</p> <div class="pt-2 text-base font-bold text-stone-100">Domain (FQDN)</div>
<div class="justify-center"> <div class="justify-start text-left">
<input <input
bind:value={fqdn} bind:value={fqdn}
readonly={!$session.isAdmin || isFqdnSet} readonly={!$session.isAdmin || isFqdnSet}
@ -118,23 +125,32 @@
required required
/> />
<Explainer <Explainer
text="If you specify <span class='text-green-600 font-bold'>https</span>, Coolify 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>, Coolify will be redirected (302) from non-www and vice versa." text="If you specify <span class='text-yellow-500 font-bold'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-yellow-500 font-bold'>www</span>, Coolify will be redirected (302) from non-www and vice versa."
/> />
</div> </div>
</div> </div>
<ul class="mt-2 divide-y divide-stone-800"> <div class="grid grid-cols-2 items-center">
<Setting
dataTooltip="Must remove the domain before you can change this setting."
disabled={isFqdnSet}
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-yellow-400'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both."
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting <Setting
bind:setting={isRegistrationEnabled} bind:setting={isRegistrationEnabled}
title="Registration allowed?" title="Registration allowed?"
description="Allow further registrations to the application. <br>It's turned off after the first registration. " description="Allow further registrations to the application. <br>It's turned off after the first registration. "
on:click={() => changeSettings('isRegistrationEnabled')} on:click={() => changeSettings('isRegistrationEnabled')}
/> />
</ul> </div>
</div> </div>
</form> </form>
<div class="mx-auto max-w-4xl px-6"> <div class="flex space-x-1 pt-6 font-bold">
<div class="flex space-x-1 pt-5 font-bold"> <div class="title">Coolify Proxy Settings</div>
<div class="title">HAProxy Settings</div>
</div> </div>
<Explainer <Explainer
text={`Credentials for <a class="text-white font-bold" href=${ text={`Credentials for <a class="text-white font-bold" href=${
@ -143,11 +159,9 @@
: browser && 'http://' + window.location.hostname + ':8404' : browser && 'http://' + window.location.hostname + ':8404'
} target="_blank">stats</a> page.`} } target="_blank">stats</a> page.`}
/> />
<div class="space-y-2 px-10 py-5">
<div class="grid grid-cols-3 items-center px-4 pt-5"> <div class="grid grid-cols-2 items-center">
<label for="proxyUser">User</label> <label for="proxyUser">User</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -156,10 +170,8 @@
value={settings.proxyUser} value={settings.proxyUser}
/> />
</div> </div>
</div> <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-3 items-center px-4">
<label for="proxyPassword">Password</label> <label for="proxyPassword">Password</label>
<div class="col-span-2 ">
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@ -171,5 +183,4 @@
</div> </div>
</div> </div>
</div> </div>
</div>
{/if} {/if}

View File

@ -5,6 +5,7 @@
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { post } from '$lib/api'; import { post } from '$lib/api';
import { browser } from '$app/env';
const { id } = $page.params; const { id } = $page.params;
let loading = false; let loading = false;
@ -120,13 +121,17 @@
</div> </div>
<Explainer <Explainer
maxWidthClass="w-full" customClass="w-full"
text="<span class='font-bold text-base'>Scopes required:</span> text="<span class='font-bold text-base text-white'>Scopes required:</span>
<br>- api (Access the authenticated user's API) <br>- <span class='text-orange-500 font-bold'>api</span> (Access the authenticated user's API)
<br>- read_repository (Allows read-only access to the repository) <br>- <span class='text-orange-500 font-bold'>read_repository</span> (Allows read-only access to the repository)
<br>- email (Allows read-only access to the user's primary email address using OpenID Connect) <br>- <span class='text-orange-500 font-bold'>email</span> (Allows read-only access to the user's primary email address using OpenID Connect)
<br> <br>
<br>For extra security, you can add Expire access tokens!" <br>For extra security, you can set Expire access tokens!
<br><br>Webhook URL: <span class='text-orange-500 font-bold'>{browser
? window.location.origin
: ''}/webhooks/gitlab</span>
<br>But if you will set a custom domain name for Coolify, use that instead."
/> />
</form> </form>
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4 pt-10"> <form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4 pt-10">

View File

@ -22,21 +22,20 @@
<script lang="ts"> <script lang="ts">
export let permissions; export let permissions;
export let team; export let team;
export let invitations; export let invitations: any[];
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { post } from '$lib/api'; import { post } from '$lib/api';
const { id } = $page.params; const { id } = $page.params;
let invitation = { let invitation = {
teamName: team.name, teamName: team.name,
email: null, email: null,
permission: 'read' permission: 'read'
}; };
let myPermission = permissions.find((u) => u.user.id === $session.uid).permission; // let myPermission = permissions.find((u) => u.user.id === $session.userId).permission;
function isAdmin(permission = myPermission) { function isAdmin(permission: string) {
if (myPermission === 'admin' || myPermission === 'owner') { if (permission === 'admin' || permission === 'owner') {
return true; return true;
} }
@ -56,7 +55,7 @@
return errorNotification(error); return errorNotification(error);
} }
} }
async function revokeInvitation(id) { async function revokeInvitation(id: string) {
try { try {
await post(`/teams/${id}/invitation/revoke.json`, { id }); await post(`/teams/${id}/invitation/revoke.json`, { id });
return window.location.reload(); return window.location.reload();
@ -64,7 +63,7 @@
return errorNotification(error); return errorNotification(error);
} }
} }
async function removeFromTeam(uid) { async function removeFromTeam(uid: string) {
try { try {
await post(`/teams/${id}/remove/user.json`, { teamId: team.id, uid }); await post(`/teams/${id}/remove/user.json`, { teamId: team.id, uid });
return window.location.reload(); return window.location.reload();
@ -72,7 +71,7 @@
return errorNotification(error); return errorNotification(error);
} }
} }
async function changePermission(userId, permissionId, currentPermission) { async function changePermission(userId: string, permissionId: string, currentPermission: string) {
let newPermission = 'read'; let newPermission = 'read';
if (currentPermission === 'read') { if (currentPermission === 'read') {
newPermission = 'admin'; newPermission = 'admin';
@ -99,7 +98,7 @@
<span class="arrow-right-applications px-1 text-cyan-500">></span> <span class="arrow-right-applications px-1 text-cyan-500">></span>
<span class="pr-2">{team.name}</span> <span class="pr-2">{team.name}</span>
</div> </div>
<div class="mx-auto max-w-2xl"> <div class="mx-auto max-w-4xl">
<form on:submit|preventDefault={handleSubmit}> <form on:submit|preventDefault={handleSubmit}>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="title">Settings</div> <div class="title">Settings</div>
@ -113,10 +112,10 @@
<input id="name" name="name" placeholder="name" bind:value={team.name} /> <input id="name" name="name" placeholder="name" bind:value={team.name} />
</div> </div>
{#if team.id === '0'} {#if team.id === '0'}
<div class="px-20 pt-4 text-center"> <div class="px-8 pt-4 text-left">
<Explainer <Explainer
maxWidthClass="w-full" customClass="w-full"
text="This is the <span class='text-red-500 font-bold'>root</span> team. <br><br>That means members of this group can manage instance wide settings and have all the priviliges in Coolify. (imagine like root user on Linux)" text="This is the <span class='text-red-500 font-bold'>root</span> team. That means members of this group can manage instance wide settings and have all the priviliges in Coolify (imagine like root user on Linux)."
/> />
</div> </div>
{/if} {/if}
@ -136,10 +135,11 @@
<tr class="text-xs"> <tr class="text-xs">
<td class="py-4" <td class="py-4"
>{permission.user.email} >{permission.user.email}
<span class="font-bold">{permission.user.id === $session.uid ? '(You)' : ''}</span></td <span class="font-bold">{permission.user.id === $session.userId ? '(You)' : ''}</span
></td
> >
<td class="py-4">{permission.permission}</td> <td class="py-4">{permission.permission}</td>
{#if $session.isAdmin && permission.user.id !== $session.uid && permission.permission !== 'owner'} {#if $session.isAdmin && permission.user.id !== $session.userId && permission.permission !== 'owner'}
<td class="flex flex-col items-center justify-center space-y-2 py-4 text-center"> <td class="flex flex-col items-center justify-center space-y-2 py-4 text-center">
<button <button
class="w-52 bg-red-600 hover:bg-red-500" class="w-52 bg-red-600 hover:bg-red-500"
@ -178,11 +178,19 @@
</div> </div>
</div> </div>
{#if $session.isAdmin} {#if $session.isAdmin}
<div class="mx-auto max-w-2xl pt-8"> <div class="mx-auto max-w-4xl pt-8">
<form on:submit|preventDefault={sendInvitation}> <form on:submit|preventDefault={sendInvitation}>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6">
<div class="title">Invite new member</div> <div>
<div class="text-center"> <div class="title font-bold">Invite new member</div>
<div class="text-left">
<Explainer
customClass="w-56"
text="You can only invite registered users at the moment - will be extended soon."
/>
</div>
</div>
<div class="pt-1 text-center">
<button class="bg-cyan-600 hover:bg-cyan-500" type="submit">Send invitation</button> <button class="bg-cyan-600 hover:bg-cyan-500" type="submit">Send invitation</button>
</div> </div>
</div> </div>

View File

@ -144,10 +144,17 @@ export const post: RequestHandler = async (event) => {
} else if (pullmergeRequestAction === 'closed') { } else if (pullmergeRequestAction === 'closed') {
if (applicationFound.destinationDockerId) { if (applicationFound.destinationDockerId) {
const domain = getDomain(applicationFound.fqdn); const domain = getDomain(applicationFound.fqdn);
const isHttps = applicationFound.fqdn.startsWith('https://');
const isWWW = applicationFound.fqdn.includes('www.');
const fqdn = `${isHttps ? 'https://' : 'http://'}${
isWWW ? 'www.' : ''
}${pullmergeRequestId}.${domain}`;
const id = `${applicationFound.id}-${pullmergeRequestId}`; const id = `${applicationFound.id}-${pullmergeRequestId}`;
const engine = applicationFound.destinationDocker.engine; const engine = applicationFound.destinationDocker.engine;
await removeDestinationDocker({ id, engine }); await removeDestinationDocker({ id, engine });
await removeProxyConfiguration({ domain: `${pullmergeRequestId}.${domain}` }); await removeProxyConfiguration(fqdn);
} }
return { return {
status: 200, status: 200,

View File

@ -141,10 +141,17 @@ export const post: RequestHandler = async (event) => {
} else if (action === 'close') { } else if (action === 'close') {
if (applicationFound.destinationDockerId) { if (applicationFound.destinationDockerId) {
const domain = getDomain(applicationFound.fqdn); const domain = getDomain(applicationFound.fqdn);
const isHttps = applicationFound.fqdn.startsWith('https://');
const isWWW = applicationFound.fqdn.includes('www.');
const fqdn = `${isHttps ? 'https://' : 'http://'}${
isWWW ? 'www.' : ''
}${pullmergeRequestId}.${domain}`;
const id = `${applicationFound.id}-${pullmergeRequestId}`; const id = `${applicationFound.id}-${pullmergeRequestId}`;
const engine = applicationFound.destinationDocker.engine; const engine = applicationFound.destinationDocker.engine;
await removeProxyConfiguration({ domain: `${pullmergeRequestId}.${domain}` });
await removeDestinationDocker({ id, engine }); await removeDestinationDocker({ id, engine });
await removeProxyConfiguration(fqdn);
} }
return { return {

View File

@ -21,8 +21,8 @@ export const get: RequestHandler = async (event) => {
const code = event.url.searchParams.get('code'); const code = event.url.searchParams.get('code');
const state = event.url.searchParams.get('state'); const state = event.url.searchParams.get('state');
try { try {
const { fqdn } = await db.listSettings();
const application = await db.getApplication({ id: state, teamId }); const application = await db.getApplication({ id: state, teamId });
const { fqdn } = application;
const { appId, appSecret } = application.gitSource.gitlabApp; const { appId, appSecret } = application.gitSource.gitlabApp;
const { htmlUrl } = application.gitSource; const { htmlUrl } = application.gitSource;

View File

@ -22,10 +22,10 @@
} }
html { html {
@apply h-full min-h-full; @apply h-full min-h-full overflow-y-scroll;
} }
body { body {
@apply min-h-screen overflow-x-hidden bg-coolblack text-sm text-white; @apply min-h-screen overflow-x-hidden bg-coolblack text-sm text-white scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200;
} }
main, main,
@ -35,10 +35,10 @@ main,
} }
input { input {
@apply h-12 w-96 select-all rounded border border-transparent bg-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:bg-transparent md:text-sm; @apply h-12 w-96 rounded border border-transparent bg-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:border disabled:border-dashed disabled:border-coolgray-300 disabled:bg-transparent md:text-sm;
} }
textarea { textarea {
@apply w-96 select-all rounded border border-transparent bg-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:bg-transparent md:text-sm; @apply w-96 rounded border border-transparent bg-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:border disabled:border-dashed disabled:border-coolgray-300 disabled:bg-transparent md:text-sm;
} }
select { select {

View File

@ -31,7 +31,8 @@ module.exports = {
} }
}, },
variants: { variants: {
scrollbar: ['dark'],
extend: {} extend: {}
}, },
plugins: [] plugins: [require('tailwindcss-scrollbar')]
}; };