feat: Dual certificates

desing: Lots of design/css updates
version++
This commit is contained in:
Andras Bacsai 2022-02-17 22:14:06 +01:00
parent 4454287be9
commit bf047e2a3c
32 changed files with 670 additions and 587 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.14",
"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",
@ -76,6 +76,7 @@
"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.0.2",
"tailwindcss-scrollbar": "^0.1.0",
"unique-names-generator": "4.6.0" "unique-names-generator": "4.6.0"
}, },
"prisma": { "prisma": {

View File

@ -43,9 +43,10 @@ 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.0.2
svelte-preprocess: 4.10.3 svelte-preprocess: 4.10.3
tailwindcss: 3.0.22 tailwindcss: 3.0.22
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
@ -70,7 +71,8 @@ dependencies:
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.0.2
tailwindcss-scrollbar: 0.1.0_tailwindcss@3.0.22
unique-names-generator: 4.6.0 unique-names-generator: 4.6.0
devDependencies: devDependencies:
@ -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.0.2:
resolution: resolution:
{ {
integrity: sha512-IX1IXtn42UTz/isem1LqH0SAZdCx6Z6Iu2V4Q83V2EScFbXZWfeFY08Azl8ZrPKdIDhSNHBLAAumRjA6TBxCvQ== integrity: sha512-+JfunYbraIOkecOJlC1iYqH9g6YOY8MXyUdE3hTZquR1JrODmOZZ+pVPmZuVIFpM5sStJf/jF1NT5306TWE9Gw==
} }
dev: false dev: false
@ -5288,6 +5290,17 @@ packages:
strip-ansi: 6.0.1 strip-ansi: 6.0.1
dev: true dev: true
/tailwindcss-scrollbar/0.1.0_tailwindcss@3.0.22:
resolution:
{
integrity: sha512-egipxw4ooQDh94x02XQpPck0P0sfwazwoUGfA9SedPATIuYDR+6qe8d31Gl7YsSMRiOKDkkqfI0kBvEw9lT/Hg==
}
peerDependencies:
tailwindcss: '>= 2.x.x'
dependencies:
tailwindcss: 3.0.22_c940fbabf228b85b1c73d314b43e31f1
dev: false
/tailwindcss/3.0.22_c940fbabf228b85b1c73d314b43e31f1: /tailwindcss/3.0.22_c940fbabf228b85b1c73d314b43e31f1:
resolution: resolution:
{ {

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

@ -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())
@ -105,7 +107,7 @@ model ApplicationSettings {
model Secret { model Secret {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
value String value String
isBuildSecret Boolean @default(false) isBuildSecret Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@ -234,6 +236,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,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

@ -4,15 +4,17 @@
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;
</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:text-center={isCenter}>
<div <div
type="button" type="button"
on:click on:click
@ -58,5 +60,4 @@
</span> </span>
</span> </span>
</div> </div>
<!-- {/if} --> </div>
</li>

View File

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

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

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

View File

@ -3,49 +3,70 @@ import { 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 nakedDomain = domain.replace('www.', '');
const wwwDomain = `www.${nakedDomain}`; const wwwDomain = `www.${nakedDomain}`;
const randomCuid = cuid(); const randomCuid = cuid();
if (dev) { const randomPort = getPort();
return await forceSSLOnApplication({ domain });
let host;
let dualCerts = false;
if (isCoolify) {
const data = await db.prisma.setting.findFirst();
dualCerts = data.dualCerts;
host = '/var/run/docker.sock';
} else { } else {
if (isCoolify) { // Check Application
await asyncExecShell( const applicationData = await db.prisma.application.findUnique({
`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 ${nakedDomain} -d ${wwwDomain} --expand --agree-tos --non-interactive --register-unsafely-without-email` where: { id },
); include: { destinationDocker: true, settings: true }
});
const { stderr: copyError } = await asyncExecShell( if (applicationData) {
`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"` if (applicationData?.destinationDockerId && applicationData?.destinationDocker) {
); host = getEngine(applicationData.destinationDocker.engine);
}
if (copyError) throw copyError; if (applicationData?.settings?.dualCerts) {
return; dualCerts = applicationData.settings.dualCerts;
}
} }
let data: any = await db.prisma.application.findUnique({ // Check Service
const serviceData = await db.prisma.service.findUnique({
where: { id }, where: { id },
include: { destinationDocker: true } include: { destinationDocker: true }
}); });
if (!data) { if (serviceData) {
data = await db.prisma.service.findUnique({ if (serviceData?.destinationDockerId && serviceData?.destinationDocker) {
where: { id }, host = getEngine(serviceData.destinationDocker.engine);
include: { destinationDocker: true } }
}); if (serviceData?.dualCerts) {
dualCerts = serviceData.dualCerts;
}
} }
// Set SSL with Let's encrypt }
if (data.destinationDockerId && data.destinationDocker) { if (!dev) {
const host = getEngine(data.destinationDocker.engine); 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 ${nakedDomain} -d ${wwwDomain} --expand --agree-tos --non-interactive --register-unsafely-without-email` `DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p ${randomPort}:${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`
); );
const { stderr: copyError } = await asyncExecShell( await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "test -d /etc/letsencrypt/live/${nakedDomain}/ && cat /etc/letsencrypt/live/${nakedDomain}/fullchain.pem /etc/letsencrypt/live/${nakedDomain}/privkey.pem > /app/ssl/${nakedDomain}.pem || cat /etc/letsencrypt/live/${wwwDomain}/fullchain.pem /etc/letsencrypt/live/${wwwDomain}/privkey.pem > /app/ssl/${wwwDomain}.pem"` `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"`
); );
if (copyError) throw copyError; } else {
await forceSSLOnApplication({ domain }); await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p ${randomPort}:${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`
);
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"`
);
} }
} else {
console.log({ dualCerts, host, wwwDomain, nakedDomain, domain });
}
if (!isCoolify) {
await forceSSLOnApplication({ domain });
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@ -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);
@ -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,19 @@
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
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.<br>Application must be redeployed."
on:click={() => 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 +297,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,8 +374,7 @@
<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}
on:click={() => changeSettings('forceSSL')} on:click={() => changeSettings('forceSSL')}
@ -370,21 +382,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

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

@ -7,72 +7,62 @@
<div class="title">CouchDB</div> <div class="title">CouchDB</div>
</div> </div>
<div class="px-10"> <div class="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} disabled={database.defaultDatabase}
disabled={database.defaultDatabase} placeholder="eg: mydb"
placeholder="eg: mydb" id="defaultDatabase"
id="defaultDatabase" name="defaultDatabase"
name="defaultDatabase" bind:value={database.defaultDatabase}
bind:value={database.defaultDatabase} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser">User</label> <label for="dbUser">User</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" id="dbUser"
id="dbUser" name="dbUser"
name="dbUser" value={database.dbUser}
value={database.dbUser} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword">Password</label> <label for="dbUserPassword">Password</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" isPasswordField
isPasswordField id="dbUserPassword"
id="dbUserPassword" name="dbUserPassword"
name="dbUserPassword" value={database.dbUserPassword}
value={database.dbUserPassword} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser">Root User</label> <label for="rootUser">Root User</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" id="rootUser"
id="rootUser" name="rootUser"
name="rootUser" value={database.rootUser}
value={database.rootUser} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword">Root's Password</label> <label for="rootUserPassword">Root's Password</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" isPasswordField
isPasswordField id="rootUserPassword"
id="rootUserPassword" name="rootUserPassword"
name="rootUserPassword" value={database.rootUserPassword}
value={database.rootUserPassword} />
/>
</div>
</div> </div>
</div> </div>

View File

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

View File

@ -7,31 +7,27 @@
<div class="title">MongoDB</div> <div class="title">MongoDB</div>
</div> </div>
<div class="px-10"> <div class="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" readonly
readonly disabled
disabled name="rootUser"
name="rootUser" value={database.rootUser}
value={database.rootUser} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword">Root's Password</label> <label for="rootUserPassword">Root's Password</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField placeholder="Generated automatically after start"
placeholder="Generated automatically after start" isPasswordField={true}
isPasswordField={true} readonly
readonly disabled
disabled id="rootUserPassword"
id="rootUserPassword" name="rootUserPassword"
name="rootUserPassword" value={database.rootUserPassword}
value={database.rootUserPassword} />
/>
</div>
</div> </div>
</div> </div>

View File

@ -7,72 +7,62 @@
<div class="title">MySQL</div> <div class="title">MySQL</div>
</div> </div>
<div class=" px-10"> <div class=" 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} disabled={database.defaultDatabase}
disabled={database.defaultDatabase} placeholder="eg: mydb"
placeholder="eg: mydb" id="defaultDatabase"
id="defaultDatabase" name="defaultDatabase"
name="defaultDatabase" bind:value={database.defaultDatabase}
bind:value={database.defaultDatabase} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser">User</label> <label for="dbUser">User</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" id="dbUser"
id="dbUser" name="dbUser"
name="dbUser" value={database.dbUser}
value={database.dbUser} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword">Password</label> <label for="dbUserPassword">Password</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" isPasswordField
isPasswordField id="dbUserPassword"
id="dbUserPassword" name="dbUserPassword"
name="dbUserPassword" value={database.dbUserPassword}
value={database.dbUserPassword} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser">Root User</label> <label for="rootUser">Root User</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" id="rootUser"
id="rootUser" name="rootUser"
name="rootUser" value={database.rootUser}
value={database.rootUser} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword">Root's Password</label> <label for="rootUserPassword">Root's Password</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" isPasswordField
isPasswordField id="rootUserPassword"
id="rootUserPassword" name="rootUserPassword"
name="rootUserPassword" value={database.rootUserPassword}
value={database.rootUserPassword} />
/>
</div>
</div> </div>
</div> </div>

View File

@ -7,45 +7,39 @@
<div class="title">PostgreSQL</div> <div class="title">PostgreSQL</div>
</div> </div>
<div class="px-10"> <div class="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} disabled={database.defaultDatabase}
disabled={database.defaultDatabase} placeholder="eg: mydb"
placeholder="eg: mydb" id="defaultDatabase"
id="defaultDatabase" name="defaultDatabase"
name="defaultDatabase" bind:value={database.defaultDatabase}
bind:value={database.defaultDatabase} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser">User</label> <label for="dbUser">User</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" id="dbUser"
id="dbUser" name="dbUser"
name="dbUser" value={database.dbUser}
value={database.dbUser} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword">Password</label> <label for="dbUserPassword">Password</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled placeholder="Generated automatically after start"
placeholder="Generated automatically after start" isPasswordField
isPasswordField id="dbUserPassword"
id="dbUserPassword" name="dbUserPassword"
name="dbUserPassword" value={database.dbUserPassword}
value={database.dbUserPassword} />
/>
</div>
</div> </div>
</div> </div>

View File

@ -7,32 +7,17 @@
<div class="title">Redis</div> <div class="title">Redis</div>
</div> </div>
<div class="px-10"> <div class="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 placeholder="Generated automatically after start"
placeholder="Generated automatically after start" isPasswordField
isPasswordField id="dbUserPassword"
id="dbUserPassword" name="dbUserPassword"
name="dbUserPassword" value={database.dbUserPassword}
value={database.dbUserPassword} />
/>
</div>
</div> </div>
<!-- <div class="grid grid-cols-3 items-center"> <!-- <div class="grid grid-cols-3 items-center">
<label for="rootUser">Root User</label> <label for="rootUser">Root User</label>

View File

@ -181,21 +181,18 @@
/> />
</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} title="Use Coolify Proxy?"
isPadding={false} 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>${
title="Use Coolify Proxy?" cannotDisable
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>${ ? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
cannotDisable : ''
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>' }`}
: '' />
}`}
/>
</ul>
</div> </div>
</form> </form>
</div> </div>

View File

@ -7,42 +7,36 @@
<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">
<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" placeholder="User to login"
placeholder="User to login" value={service.minio.rootUser}
value={service.minio.rootUser} disabled
disabled readonly
readonly />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword">Root's Password</label> <label for="rootUserPassword">Root's Password</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField id="rootUserPassword"
id="rootUserPassword" isPasswordField
isPasswordField readonly
readonly disabled
disabled 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">
<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" value={service.minio.publicPort}
value={service.minio.publicPort} disabled
disabled readonly
readonly placeholder="Generated automatically after start"
placeholder="Generated automatically after start" />
/>
</div>
</div> </div>

View File

@ -7,86 +7,74 @@
<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" disabled={readOnly}
disabled={readOnly} readonly={readOnly}
readonly={readOnly} placeholder="Email address"
placeholder="Email address" 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" disabled={readOnly}
disabled={readOnly} readonly={readOnly}
readonly={readOnly} placeholder="User to login"
placeholder="User to login" 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 readonly
readonly disabled
disabled 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" value={service.plausibleAnalytics.postgresqlUser}
value={service.plausibleAnalytics.postgresqlUser} 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 readonly
readonly disabled
disabled 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" value={service.plausibleAnalytics.postgresqlDatabase}
value={service.plausibleAnalytics.postgresqlDatabase} 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,14 @@
/> />
</div> </div>
</div> </div>
<div class="grid grid-cols-2 items-center px-10">
<Setting
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={() => 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,16 +7,14 @@
<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 readonly
readonly disabled
disabled name="password"
name="password" value={service.vscodeserver.password}
value={service.vscodeserver.password} />
/>
</div>
</div> </div>

View File

@ -10,85 +10,73 @@
<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} class:resize-none={isRunning}
class:resize-none={isRunning} rows={isRunning ? 1 : 5}
rows={isRunning ? 1 : 5} name="extraConfig"
name="extraConfig" id="extraConfig"
id="extraConfig" placeholder={!isRunning
placeholder={!isRunning ? `eg:
? `eg:
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" required
required readonly={readOnly}
readonly={readOnly} disabled={readOnly}
disabled={readOnly} 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" placeholder="MySQL Root User"
placeholder="MySQL Root User" value={service.wordpress.mysqlRootUser}
value={service.wordpress.mysqlRootUser} 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 readonly
readonly disabled
disabled 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 readonly
readonly disabled
disabled name="mysqlPassword"
name="mysqlPassword" value={service.wordpress.mysqlPassword}
value={service.wordpress.mysqlPassword} />
/>
</div>
</div> </div>

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

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

@ -15,6 +15,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 dns from 'dns/promises';
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 +46,18 @@ 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();
const ip = await dns.resolve(event.url.hostname);
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: `http://${ip[0]}:3000/settings`
}
}; };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);
@ -69,16 +74,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,7 +102,7 @@ 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,57 +125,60 @@
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
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=${ fqdn
fqdn ? 'http://' + getDomain(fqdn) + ':8404'
? 'http://' + getDomain(fqdn) + ':8404' : browser && 'http://' + window.location.hostname + ':8404'
: browser && 'http://' + window.location.hostname + ':8404' } target="_blank">stats</a> page.`}
} target="_blank">stats</a> page.`} />
/> <div class="px-10 py-5">
<div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-3 items-center px-4 pt-5">
<label for="proxyUser">User</label> <label for="proxyUser">User</label>
<CopyPasswordField
<div class="col-span-2 "> readonly
<CopyPasswordField disabled
readonly id="proxyUser"
disabled name="proxyUser"
id="proxyUser" value={settings.proxyUser}
name="proxyUser" />
value={settings.proxyUser}
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center px-4"> <div class="grid grid-cols-2 items-center">
<label for="proxyPassword">Password</label> <label for="proxyPassword">Password</label>
<div class="col-span-2 "> <CopyPasswordField
<CopyPasswordField readonly
readonly disabled
disabled id="proxyPassword"
id="proxyPassword" name="proxyPassword"
name="proxyPassword" isPasswordField
isPasswordField value={settings.proxyPassword}
value={settings.proxyPassword} />
/>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -120,7 +120,7 @@
</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'>Scopes required:</span>
<br>- api (Access the authenticated user's API) <br>- api (Access the authenticated user's API)
<br>- read_repository (Allows read-only access to the repository) <br>- read_repository (Allows read-only access to the repository)

View File

@ -99,7 +99,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 +113,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}
@ -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

@ -22,10 +22,10 @@ @font-face {
} }
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,
@ -170,7 +170,7 @@ [data-tooltip]:after {
padding: 8px; padding: 8px;
color: #fff; color: #fff;
content: attr(data-tooltip); content: attr(data-tooltip);
@apply min-w-[100px] rounded bg-coollabs text-center font-normal; @apply min-w-[100px] rounded bg-coollabs text-center font-medium;
} }
/* Directions */ /* Directions */

View File

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