feat: Dual certificates
desing: Lots of design/css updates version++
This commit is contained in:
parent
4454287be9
commit
bf047e2a3c
@ -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": {
|
||||||
|
@ -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:
|
||||||
{
|
{
|
||||||
|
47
prisma/migrations/20220217211304_dualcerts/migration.sql
Normal file
47
prisma/migrations/20220217211304_dualcerts/migration.sql
Normal 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;
|
@ -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[]
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
|
@ -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 }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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 } });
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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'}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
19
src/routes/services/[id]/settings.json.ts
Normal file
19
src/routes/services/[id]/settings.json.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
@ -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');
|
||||||
|
@ -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>
|
||||||
|
@ -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)
|
||||||
|
@ -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>
|
||||||
|
@ -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 */
|
||||||
|
@ -31,7 +31,8 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
|
scrollbar: ['dark'],
|
||||||
extend: {}
|
extend: {}
|
||||||
},
|
},
|
||||||
plugins: []
|
plugins: [require('tailwindcss-scrollbar')]
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user