feat: custom certificate

This commit is contained in:
Andras Bacsai
2022-09-21 15:48:32 +02:00
parent 86ac6461d1
commit 90e639f119
30 changed files with 1000 additions and 367 deletions

View File

@@ -21,6 +21,7 @@
"@fastify/env": "4.1.0",
"@fastify/jwt": "6.3.2",
"@fastify/static": "6.5.0",
"@fastify/multipart": "7.2.0",
"@iarna/toml": "2.2.5",
"@ladjs/graceful": "3.0.2",
"@prisma/client": "4.3.1",
@@ -49,6 +50,7 @@
"p-all": "4.0.0",
"p-throttle": "5.0.0",
"public-ip": "6.0.1",
"pump": "^3.0.0",
"ssh-config": "4.1.6",
"strip-ansi": "7.0.1",
"unique-names-generator": "4.7.1"

View File

@@ -8,6 +8,15 @@ datasource db {
url = env("COOLIFY_DATABASE_URL")
}
model Certificate {
id String @id @default(cuid())
key String
cert String
team Team[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Setting {
id String @id @default(cuid())
fqdn String? @unique
@@ -70,6 +79,7 @@ model Team {
gitLabApps GitlabApp[]
service Service[]
users User[]
certificate Certificate[]
}
model TeamInvitation {

View File

@@ -3,6 +3,7 @@ import cors from '@fastify/cors';
import serve from '@fastify/static';
import env from '@fastify/env';
import cookie from '@fastify/cookie';
import multipart from '@fastify/multipart';
import path, { join } from 'path';
import autoLoad from '@fastify/autoload';
import { asyncExecShell, createRemoteEngineConfiguration, getDomain, isDev, listSettings, prisma, version } from './lib/common';
@@ -31,6 +32,7 @@ prisma.setting.findFirst().then(async (settings) => {
logger: settings?.isAPIDebuggingEnabled || false,
trustProxy: true
});
const schema = {
type: 'object',
required: ['COOLIFY_SECRET_KEY', 'COOLIFY_DATABASE_URL', 'COOLIFY_IS_ON'],
@@ -88,13 +90,13 @@ prisma.setting.findFirst().then(async (settings) => {
return reply.status(200).sendFile('index.html');
});
}
fastify.register(multipart, { limits: { fileSize: 100000 } });
fastify.register(autoLoad, {
dir: join(__dirname, 'plugins')
});
fastify.register(autoLoad, {
dir: join(__dirname, 'routes')
});
fastify.register(cookie)
fastify.register(cors);
fastify.addHook('onRequest', async (request, reply) => {
@@ -145,11 +147,15 @@ prisma.setting.findFirst().then(async (settings) => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage")
}, isDev ? 6000 : 60000 * 10)
// checkProxies
// checkProxies and checkFluentBit
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies")
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkFluentBit")
}, 10000)
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:copySSLCertificates")
}, 2000)
// cleanupPrismaEngines
// setInterval(async () => {
// scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupPrismaEngines")

View File

@@ -1,8 +1,9 @@
import { parentPort } from 'node:worker_threads';
import axios from 'axios';
import { compareVersions } from 'compare-versions';
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration } from '../lib/common';
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration, decrypt } from '../lib/common';
import { checkContainer } from '../lib/docker';
import fs from 'fs/promises'
async function autoUpdater() {
try {
const currentVersion = version;
@@ -39,6 +40,46 @@ async function autoUpdater() {
}
} catch (error) { }
}
async function checkFluentBit() {
if (!isDev) {
const engine = '/var/run/docker.sock';
const { id } = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' }
});
const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit' });
if (!found) {
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(`docker compose up -d fluent-bit`);
}
}
}
async function copySSLCertificates() {
try {
const certificates = await prisma.certificate.findMany({ include: { team: true } })
const teamIds = certificates.map(c => c.team.map(t => t.id)).flat()
const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: teamIds } } } } })
for (const destination of destinations) {
if (destination.remoteEngine) {
// TODO: copy certificates to remote engine
} else {
for (const certificate of certificates) {
const { id, key, cert } = certificate
const decryptedKey = decrypt(key)
await asyncExecShell(`docker exec coolify-proxy sh -c 'mkdir -p /etc/traefik/acme/custom/'`)
await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey)
await fs.writeFile(`/tmp/${id}-cert.pem`, cert)
await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`)
await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`)
await fs.rm(`/tmp/${id}-key.pem`)
await fs.rm(`/tmp/${id}-cert.pem`)
}
}
}
} catch (error) {
}
}
async function checkProxies() {
try {
const { default: isReachable } = await import('is-port-reachable');
@@ -215,6 +256,14 @@ async function cleanupStorage() {
await checkProxies();
return;
}
if (message === 'action:checkFluentBit') {
await checkFluentBit();
return;
}
if (message === 'action:copySSLCertificates') {
await copySSLCertificates();
return;
}
if (message === 'action:autoUpdater') {
if (!status.cleanupStorage) {
status.autoUpdater = true

View File

@@ -1,6 +1,9 @@
import { FastifyPluginAsync } from 'fastify';
import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify } from './handlers';
import { GetCurrentUser } from './types';
import pump from 'pump'
import fs from 'fs'
import { asyncExecShell, encrypt, errorHandler, prisma } from '../../../lib/common';
export interface Update {
Body: { latestVersion: string }

View File

@@ -1,8 +1,9 @@
import { promises as dns } from 'dns';
import { X509Certificate } from 'node:crypto';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, getDomain, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types';
export async function listAllSettings(request: FastifyRequest) {
@@ -16,8 +17,16 @@ export async function listAllSettings(request: FastifyRequest) {
unencryptedKeys.push({ id: key.id, name: key.name, privateKey: decrypt(key.privateKey), createdAt: key.createdAt })
}
}
const certificates = await prisma.certificate.findMany({ where: { team: { every: { id: teamId } } } })
let cns = [];
for (const certificate of certificates) {
const x509 = new X509Certificate(certificate.cert);
cns.push({ commonName: x509.subject.split('\n').find((s) => s.startsWith('CN=')).replace('CN=', ''), id: certificate.id, createdAt: certificate.createdAt })
}
return {
settings,
certificates: cns,
sshKeys: unencryptedKeys
}
} catch ({ status, message }) {
@@ -118,7 +127,7 @@ export async function saveSSHKey(request: FastifyRequest<SaveSSHKey>, reply: Fas
return errorHandler({ status, message })
}
}
export async function deleteSSHKey(request: FastifyRequest<DeleteSSHKey>, reply: FastifyReply) {
export async function deleteSSHKey(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) {
try {
const { id } = request.body;
await prisma.sshKey.delete({ where: { id } })
@@ -126,4 +135,14 @@ export async function deleteSSHKey(request: FastifyRequest<DeleteSSHKey>, reply:
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deleteCertificates(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) {
try {
const { id } = request.body;
await prisma.certificate.delete({ where: { id } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@@ -1,21 +1,58 @@
import { FastifyPluginAsync } from 'fastify';
import { checkDNS, checkDomain, deleteDomain, deleteSSHKey, listAllSettings, saveSettings, saveSSHKey } from './handlers';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
import { X509Certificate } from 'node:crypto';
import { encrypt, errorHandler, prisma } from '../../../../lib/common';
import { checkDNS, checkDomain, deleteCertificates, deleteDomain, deleteSSHKey, getCertificates, listAllSettings, saveSettings, saveSSHKey } from './handlers';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => {
return await request.jwtVerify()
})
fastify.get('/', async (request) => await listAllSettings(request));
fastify.post<SaveSettings>('/', async (request, reply) => await saveSettings(request, reply));
fastify.delete<DeleteDomain>('/', async (request, reply) => await deleteDomain(request, reply));
fastify.addHook('onRequest', async (request) => {
return await request.jwtVerify()
})
fastify.get('/', async (request) => await listAllSettings(request));
fastify.post<SaveSettings>('/', async (request, reply) => await saveSettings(request, reply));
fastify.delete<DeleteDomain>('/', async (request, reply) => await deleteDomain(request, reply));
fastify.get<CheckDNS>('/check', async (request) => await checkDNS(request));
fastify.post<CheckDomain>('/check', async (request) => await checkDomain(request));
fastify.get<CheckDNS>('/check', async (request) => await checkDNS(request));
fastify.post<CheckDomain>('/check', async (request) => await checkDomain(request));
fastify.post<SaveSSHKey>('/sshKey', async (request, reply) => await saveSSHKey(request, reply));
fastify.delete<DeleteSSHKey>('/sshKey', async (request, reply) => await deleteSSHKey(request, reply));
fastify.post<SaveSSHKey>('/sshKey', async (request, reply) => await saveSSHKey(request, reply));
fastify.delete<OnlyIdInBody>('/sshKey', async (request, reply) => await deleteSSHKey(request, reply));
fastify.post('/upload', async (request) => {
try {
const teamId = request.user.teamId;
const certificates = await prisma.certificate.findMany({})
let cns = [];
for (const certificate of certificates) {
const x509 = new X509Certificate(certificate.cert);
cns.push(x509.subject.split('\n').find((s) => s.startsWith('CN=')).replace('CN=', ''))
}
const parts = await request.files()
let key = null
let cert = null
for await (const part of parts) {
const name = part.fieldname
if (name === 'key') key = (await part.toBuffer()).toString()
if (name === 'cert') cert = (await part.toBuffer()).toString()
}
const x509 = new X509Certificate(cert);
const cn = x509.subject.split('\n').find((s) => s.startsWith('CN=')).replace('CN=', '')
if (cns.includes(cn)) {
throw {
message: `A certificate with ${cn} common name already exists.`
}
}
await prisma.certificate.create({ data: { cert, key: encrypt(key), team: { connect: { id: teamId } } } })
return { message: 'Certificated uploaded' }
} catch ({ status, message }) {
return errorHandler({ status, message });
}
});
fastify.delete<OnlyIdInBody>('/certificate', async (request, reply) => await deleteCertificates(request, reply))
// fastify.get('/certificates', async (request) => await getCertificates(request))
};
export default root;

View File

@@ -41,4 +41,9 @@ export interface DeleteSSHKey {
Body: {
id: string
}
}
export interface OnlyIdInBody {
Body: {
id: string
}
}

View File

@@ -178,7 +178,19 @@ function configureMiddleware(
export async function traefikConfiguration(request, reply) {
try {
const sslpath = '/etc/traefik/acme/custom';
const certificates = await prisma.certificate.findMany()
let parsedCertificates = []
for (const certificate of certificates) {
parsedCertificates.push({
certFile: `${sslpath}/${certificate.id}-cert.pem`,
keyFile: `${sslpath}/${certificate.id}-key.pem`
})
}
const traefik = {
tls: {
certificates: parsedCertificates
},
http: {
routers: {},
services: {},

View File

@@ -42,13 +42,14 @@
},
"type": "module",
"dependencies": {
"dayjs": "1.11.5",
"@sveltejs/adapter-static": "1.0.0-next.39",
"@tailwindcss/typography": "^0.5.7",
"cuid": "2.1.8",
"daisyui": "2.24.2",
"dayjs": "1.11.5",
"js-cookie": "3.0.1",
"p-limit": "4.0.0",
"svelte-file-dropzone": "^1.0.0",
"svelte-select": "4.4.7",
"sveltekit-i18n": "2.2.2"
}

View File

@@ -39,7 +39,7 @@ export function getWebhookUrl(type: string) {
async function send({
method,
path,
data = {},
data = null,
headers,
timeout = 120000
}: {
@@ -53,7 +53,7 @@ async function send({
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const opts: any = { method, headers: {}, body: null, signal: controller.signal };
if (Object.keys(data).length > 0) {
if (data && Object.keys(data).length > 0) {
const parsedData = data;
for (const [key, value] of Object.entries(data)) {
if (value === '') {
@@ -85,7 +85,9 @@ async function send({
if (dev && !path.startsWith('https://')) {
path = `${getAPIUrl()}${path}`;
}
if (method === 'POST' && data && !opts.body) {
opts.body = data;
}
const response = await fetch(`${path}`, opts);
clearTimeout(id);
@@ -132,7 +134,7 @@ export function del(
export function post(
path: string,
data: Record<string, unknown>,
data: Record<string, unknown> | FormData,
headers?: Record<string, unknown>
): Promise<Record<string, any>> {
return send({ method: 'POST', path, data, headers });

View File

@@ -4,7 +4,7 @@
export let type = 'info';
function success() {
if (type === 'success') {
return 'bg-gradient-to-r from-purple-500 via-pink-500 to-red-500';
return 'bg-coollabs';
}
}
</script>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { post } from '$lib/api';
let cert: any;
let key: any;
async function submitForm() {
const formData = new FormData();
formData.append('cert', cert[0]);
formData.append('key', key[0]);
await post('/upload', formData);
}
</script>
<form on:submit|preventDefault={submitForm}>
<label for="cert">Certificate</label>
<input id="cert" type="file" required name="cert" bind:files={cert} />
<label for="key">Private Key</label>
<input id="key" type="file" required name="key" bind:files={key} />
<br />
<input type="submit" />
</form>

View File

@@ -226,7 +226,7 @@
<a
id="settings"
sveltekit:prefetch
href={$appSession.teamId === '0' ? '/settings/global' : '/settings/ssh-keys'}
href={$appSession.teamId === '0' ? '/settings/coolify' : '/settings/ssh'}
class="icons hover:text-settings"
class:text-settings={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
@@ -393,7 +393,7 @@
<li>
<a
class="no-underline icons hover:text-black hover:bg-settings"
href={$appSession.teamId === '0' ? '/settings/global' : '/settings/ssh-keys'}
href={$appSession.teamId === '0' ? '/settings/coolify' : '/settings/ssh'}
class:bg-settings={$page.url.pathname.startsWith('/settings')}
class:text-black={$page.url.pathname.startsWith('/settings')}
>

View File

@@ -218,7 +218,7 @@
id="git"
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-6 h-6"
class="w-6 h-6 lg:w-10 lg:h-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">

View File

@@ -61,7 +61,7 @@
<div class="pb-2 text-center font-bold">No SSH key found</div>
<div class="flex justify-center">
<a
href="/settings/ssh-keys"
href="/settings/ssh"
sveltekit:prefetch
class="add-icon bg-sky-600 hover:bg-sky-500"
>

View File

@@ -3,19 +3,29 @@
import { appSession } from '$lib/store';
</script>
<div class="flex flex-col pt-4 space-y-6 px-10">
<ul class="menu bg-coolgray-200 rounded lg:w-52">
{#if $appSession.teamId === '0'}
<a
href="/settings/global"
class="sub-menu no-underline w-full"
class:sub-menu-active={$page.routeId === 'settings/global'}
<li
class="hover:bg-coollabs duration-150"
class:bordered={$page.url.pathname === '/settings/coolify'}
class:bg-coolgray-500={$page.url.pathname === '/settings/coolify'}
>
Global Settings
</a>
<a href="/settings/coolify" class="no-underline w-full">Coolify Settings</a>
</li>
{/if}
<a
href="/settings/ssh-keys"
class="sub-menu no-underline w-full"
class:sub-menu-active={$page.routeId === 'settings/ssh-keys'}>SSH Keys</a
<li
class="hover:bg-coollabs duration-150"
class:bordered={$page.url.pathname === '/settings/ssh'}
class:bg-coolgray-500={$page.url.pathname === '/settings/ssh'}
>
</div>
<a href="/settings/ssh" class="no-underline w-full">SSH Keys</a>
</li>
<li
class="hover:bg-coollabs duration-150"
class:bordered={$page.url.pathname === '/settings/certificates'}
class:bg-coolgray-400={$page.url.pathname === '/settings/certificates'}
>
<a href="/settings/certificates" class="no-underline w-full">SSL Certificates</a>
</li>
</ul>

View File

@@ -1,7 +1,8 @@
<script context="module" lang="ts">
import { get } from '$lib/api';
import { page } from '$app/stores';
import type { Load } from '@sveltejs/kit';
import Menu from './_Menu.svelte';
export const load: Load = async () => {
try {
const response = await get(`/settings`);
@@ -19,5 +20,12 @@
};
</script>
<slot />
<div class="flex flex-col lg:flex-row ">
<nav class="header flex flex-col w-full lg:w-52">
<div class="title pb-10">Settings</div>
<Menu />
</nav>
<div class="pt-0 lg:pt-24 px-5 lg:px-0 mx-auto">
<slot />
</div>
</div>

View File

@@ -0,0 +1,144 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ stuff }) => {
try {
return {
props: {
...stuff
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<script lang="ts">
export let certificates: any;
import { del, post } from '$lib/api';
import { errorNotification } from '$lib/common';
let loading = {
save: false
};
let isModalActive = false;
let cert: any = null;
let key: any = null;
async function handleSubmit() {
try {
const formData = new FormData();
formData.append('cert', cert[0]);
formData.append('key', key[0]);
await post('/settings/upload', formData);
return window.location.reload();
} catch (error) {
errorNotification(error);
return false;
}
}
async function deleteCertificate(id: string) {
const sure = confirm('Are you sure you would like to delete this SSH key?');
if (sure) {
try {
if (!id) return;
await del(`/settings/certificate`, { id });
return window.location.reload();
} catch (error) {
errorNotification(error);
return false;
}
}
}
</script>
<div class="title font-bold pb-3">SSL Certificates</div>
<div class="w-full lg:w-[50em]">
{#if certificates.length === 0}
<div class="text-sm">No SSL Certificate found</div>
<label
for="my-modal"
class="btn btn-sm bg-settings text-black mt-6"
on:click={() => (isModalActive = true)}>Add SSL Certificate</label
>
{:else}
<div class="mx-auto w-full p-6 bg-coolgray-100 rounded border-coolgray-300 border ">
<table class="table w-full">
<thead>
<tr>
<th>Common Name</th>
<th>CreatedAt</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each certificates as cert}
<tr>
<td>{cert.commonName}</td>
<td>{cert.createdAt}</td>
<td
><button on:click={() => deleteCertificate(cert.id)} class="btn btn-sm bg-error"
>Delete</button
></td
>
</tr>
{/each}
</tbody>
</table>
<label
for="my-modal"
class="btn btn-sm bg-settings text-black mt-6"
on:click={() => (isModalActive = true)}>Add SSL Certificate</label
>
</div>
{/if}
</div>
{#if isModalActive}
<input type="checkbox" id="my-modal" class="modal-toggle" />
<div class="modal modal-bottom sm:modal-middle ">
<div class="modal-box rounded bg-coolgray-300 max-w-2xl">
<h3 class="font-bold text-lg">Add a new SSL Certificate</h3>
<p class="py-4">
SSL Certificates are used to secure your domain and allow you to use HTTPS. <br /><br />Once
you uploaded your certificate, Coolify will automatically configure it for you in the
background.
</p>
<div class="modal-action">
<form on:submit|preventDefault={handleSubmit} class="w-full">
<div class="flex flex-col justify-center">
<label for="cert">Certificate</label>
<div class="flex-1" />
<input
class="w-full bg-coolgray-100"
id="cert"
type="file"
required
name="cert"
bind:files={cert}
/>
<label for="key" class="pt-10">Private Key</label>
<input
class="w-full bg-coolgray-100"
id="key"
type="file"
required
name="key"
bind:files={key}
/>
</div>
<label for="my-modal">
<button type="submit" class="btn btn-sm bg-settings text-black mt-4">Upload</button
></label
>
<button on:click={() => (isModalActive = false)} type="button" class="btn btn-sm"
>Cancel</button
>
</form>
</div>
</div>
</div>
{/if}

View File

@@ -18,6 +18,7 @@
<script lang="ts">
export let settings: any;
export let certificates: any;
import Setting from '$lib/components/Setting.svelte';
import { del, get, post } from '$lib/api';
import { browser } from '$app/env';
@@ -26,6 +27,7 @@
import { asyncSleep, errorNotification, getDomain } from '$lib/common';
import Menu from './_Menu.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import Upload from '$lib/components/Upload.svelte';
let isAPIDebuggingEnabled = settings.isAPIDebuggingEnabled;
let isRegistrationEnabled = settings.isRegistrationEnabled;
@@ -194,180 +196,187 @@
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.settings')}</div>
</div>
<div class="mx-auto w-full">
<div class="title font-bold pb-3">Coolify Settings</div>
<div class="mx-auto w-full p-4 bg-coolgray-100 rounded border-coolgray-300 border">
<div class="flex lg:flex-row flex-col">
<Menu />
<form on:submit|preventDefault={handleSubmit}>
<div class="flex flex-col lg:flex-row flex-wrap items-center space-x-3 justify-center lg:justify-start lg:py-0 px-4">
<div class="title font-bold">{$t('index.global_settings')}</div>
<div class="flex lg:flex-row lg:space-x-4 flex-col space-y-2 lg:space-y-0 py-4">
<button
class="btn btn-sm bg-settings text-black"
type="submit"
class:bg-orange-600={forceSave}
class:hover:bg-orange-400={forceSave}
disabled={loading.save}
>{loading.save
? $t('forms.saving')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
>
{#if isFqdnSet}
<button
on:click|preventDefault={removeFqdn}
disabled={loading.remove}
class="btn btn-sm"
>{loading.remove ? $t('forms.removing') : $t('forms.remove_domain')}</button
>
{/if}
<button
on:click={restartCoolify}
class:loading={loading.restart}
class="btn btn-sm bg-red-600 hover:bg-red-500">Restart Coolify</button
>
<!-- <Upload />
{#if certificates.length > 0}
{#each certificates as cert}
<div>{cert.commonName}</div>
{/each}
{/if} -->
<form on:submit|preventDefault={handleSubmit}>
<div class="grid grid-flow-row gap-2 lg:px-10 px-2 pr-5">
<div class="grid grid-cols-2 items-center">
<div>
{$t('application.url_fqdn')}
<Explainer position="dropdown-bottom" explanation={$t('setting.ssl_explainer')} />
</div>
</div>
<div class="grid grid-flow-row gap-2 lg:px-10 px-2 pr-5">
<div class="grid grid-cols-2 items-center">
<div>
{$t('application.url_fqdn')}
<Explainer position="dropdown-bottom" explanation={$t('setting.ssl_explainer')} />
</div>
<input
class="w-full"
bind:value={fqdn}
readonly={!$appSession.isAdmin || isFqdnSet}
disabled={!$appSession.isAdmin || isFqdnSet}
on:input={resetView}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="{$t('forms.eg')}: https://coolify.io"
/>
<input
class="w-full"
bind:value={fqdn}
readonly={!$appSession.isAdmin || isFqdnSet}
disabled={!$appSession.isAdmin || isFqdnSet}
on:input={resetView}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="{$t('forms.eg')}: https://coolify.io"
/>
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="btn btn-sm bg-success"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="btn btn-sm bg-error"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{#if dualCerts}
{#if isWWWDomainOK}
<button
class="btn btn-sm bg-success"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="btn btn-sm bg-error"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{#if dualCerts}
{#if isWWWDomainOK}
<button
class="btn btn-sm bg-success"
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="btn btn-sm bg-error"
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{/if}
</div>
{/if}
</div>
<div class="grid grid-cols-2 items-center">
<div>
{$t('forms.public_port_range')}
<Explainer explanation={$t('forms.public_port_range_explainer')} />
</div>
<div class="flex flex-row items-center space-x-2">
<input
class=" w-full px-2"
type="number"
bind:value={minPort}
min="1024"
max={maxPort}
/>
<p>-</p>
<input
class="w-full px-2"
type="number"
bind:value={maxPort}
min={minPort}
max="65543"
/>
</div>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
id="isDNSCheckEnabled"
bind:setting={isDNSCheckEnabled}
title={$t('setting.is_dns_check_enabled')}
description={$t('setting.is_dns_check_enabled_explainer')}
on:click={() => changeSettings('isDNSCheckEnabled')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<div>
Custom DNS servers <Explainer
explanation="You can specify a custom DNS server to verify your domains all over Coolify.<br><br>By default, the OS defined DNS servers are used."
/>
</div>
<input class="w-full" placeholder="1.1.1.1,8.8.8.8" bind:value={DNSServers} />
</div>
<div class="grid grid-cols-2 items-center">
<Setting
id="dualCerts"
dataTooltip={$t('setting.must_remove_domain_before_changing')}
disabled={isFqdnSet}
bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')}
description={$t('setting.generate_www_non_www_ssl')}
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
id="isRegistrationEnabled"
bind:setting={isRegistrationEnabled}
title={$t('setting.registration_allowed')}
description={$t('setting.registration_allowed_explainer')}
on:click={() => changeSettings('isRegistrationEnabled')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
id="isAPIDebuggingEnabled"
bind:setting={isAPIDebuggingEnabled}
title="API Debugging"
description="Enable API debugging. This will log all API requests and responses.<br><br>You need to restart the Coolify for this to take effect."
on:click={() => changeSettings('isAPIDebuggingEnabled')}
/>
</div>
{#if browser && $features.beta}
<div class="grid grid-cols-2 items-center">
<Setting
id="isAutoUpdateEnabled"
bind:setting={isAutoUpdateEnabled}
title={$t('setting.auto_update_enabled')}
description={$t('setting.auto_update_enabled_explainer')}
on:click={() => changeSettings('isAutoUpdateEnabled')}
/>
{/if}
</div>
{/if}
</div>
</form>
<div class="grid grid-cols-2 items-center">
<Setting
id="dualCerts"
dataTooltip={$t('setting.must_remove_domain_before_changing')}
disabled={isFqdnSet}
bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')}
description={$t('setting.generate_www_non_www_ssl')}
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<div>
{$t('forms.public_port_range')}
<Explainer explanation={$t('forms.public_port_range_explainer')} />
</div>
<div class="flex flex-row items-center space-x-2">
<input
class=" w-full px-2"
type="number"
bind:value={minPort}
min="1024"
max={maxPort}
/>
<p>-</p>
<input
class="w-full px-2"
type="number"
bind:value={maxPort}
min={minPort}
max="65543"
/>
</div>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
id="isDNSCheckEnabled"
bind:setting={isDNSCheckEnabled}
title={$t('setting.is_dns_check_enabled')}
description={$t('setting.is_dns_check_enabled_explainer')}
on:click={() => changeSettings('isDNSCheckEnabled')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<div>
Custom DNS servers <Explainer
explanation="You can specify a custom DNS server to verify your domains all over Coolify.<br><br>By default, the OS defined DNS servers are used."
/>
</div>
<input class="w-full" placeholder="1.1.1.1,8.8.8.8" bind:value={DNSServers} />
</div>
<div class="grid grid-cols-2 items-center">
<Setting
id="isRegistrationEnabled"
bind:setting={isRegistrationEnabled}
title={$t('setting.registration_allowed')}
description={$t('setting.registration_allowed_explainer')}
on:click={() => changeSettings('isRegistrationEnabled')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
id="isAPIDebuggingEnabled"
bind:setting={isAPIDebuggingEnabled}
title="API Debugging"
description="Enable API debugging. This will log all API requests and responses.<br><br>You need to restart the Coolify for this to take effect."
on:click={() => changeSettings('isAPIDebuggingEnabled')}
/>
</div>
{#if browser && $features.beta}
<div class="grid grid-cols-2 items-center">
<Setting
id="isAutoUpdateEnabled"
bind:setting={isAutoUpdateEnabled}
title={$t('setting.auto_update_enabled')}
description={$t('setting.auto_update_enabled_explainer')}
on:click={() => changeSettings('isAutoUpdateEnabled')}
/>
</div>
{/if}
</div>
<div
class="flex flex-col lg:flex-row flex-wrap items-center space-x-3 justify-center lg:justify-start lg:py-4 px-4 pb-4 lg:pb-4"
>
<div class="flex lg:flex-row lg:space-x-4 flex-col space-y-2 lg:space-y-0 px-6">
<button
class="btn btn-sm bg-settings text-black"
type="submit"
class:bg-orange-600={forceSave}
class:hover:bg-orange-400={forceSave}
class:loading={loading.save}
disabled={loading.save}
>{loading.save
? $t('forms.saving')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
>
{#if isFqdnSet}
<button
on:click|preventDefault={removeFqdn}
disabled={loading.remove}
class="btn btn-sm"
>{loading.remove ? $t('forms.removing') : $t('forms.remove_domain')}</button
>
{/if}
<button
on:click={restartCoolify}
class:loading={loading.restart}
class="btn btn-sm bg-red-600 hover:bg-red-500">Restart Coolify</button
>
</div>
</div>
</form>
</div>
</div>

View File

@@ -2,7 +2,7 @@
import { goto } from '$app/navigation';
import { appSession } from '$lib/store';
if ($appSession.teamId !== '0') {
goto('/settings/ssh-keys');
goto('/settings/ssh');
}
goto('/settings/global');
goto('/settings/coolify');
</script>

View File

@@ -1,155 +0,0 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ stuff }) => {
try {
return {
props: {
...stuff
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<script lang="ts">
export let sshKeys: any;
import { del, post } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import Menu from './_Menu.svelte';
let loading = {
save: false
};
let isModalActive = false;
let newSSHKey = {
name: null,
privateKey: null
};
async function handleSubmit() {
try {
await post(`/settings/sshKey`, { ...newSSHKey });
return window.location.reload();
} catch (error) {
errorNotification(error);
return false;
}
}
async function deleteSSHKey(id: string) {
const sure = confirm('Are you sure you would like to delete this SSH key?');
if (sure) {
try {
if (!id) return;
await del(`/settings/sshKey`, { id });
return window.location.reload();
} catch (error) {
errorNotification(error);
return false;
}
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.settings')}</div>
</div>
<div class="mx-auto w-full">
<div class="flex lg:flex-row flex-col">
<Menu />
<div class="flex flex-col mt-5">
<div
class="flex flex-col lg:flex-row flex-wrap items-center space-x-3 justify-center lg:justify-start lg:py-0 px-4 pb-4 lg:pb-4"
style="min-width: 83vw"
>
<div class="title font-bold">SSH Keys</div>
<button
on:click={() => (isModalActive = true)}
class="btn btn-sm bg-settings text-black"
disabled={loading.save}>New SSH Key</button
>
</div>
<div class="grid grid-flow-col gap-2 lg:px-10 px-6">
{#if sshKeys.length === 0}
<div class="text-sm ">No SSH keys found</div>
{:else}
{#each sshKeys as key}
<div class="box-selection group relative">
<div class="text-xl font-bold">{key.name}</div>
<div class="py-3 text-stone-600">Added on {key.createdAt}</div>
<button on:click={() => deleteSSHKey(key.id)} class="btn btn-sm bg-error"
>Delete</button
>
</div>
{/each}
{/if}
</div>
</div>
</div>
</div>
{#if isModalActive}
<div class="relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="fixed inset-0 bg-coolgray-500 bg-opacity-75 transition-opacity" />
<div class="fixed z-10 inset-0 overflow-y-auto text-white">
<div class="flex items-end sm:items-center justify-center min-h-full p-4 text-center sm:p-0">
<form
on:submit|preventDefault={handleSubmit}
class="relative bg-coolblack rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:max-w-lg sm:w-full sm:p-6 border border-coolgray-500"
>
<div class="hidden sm:block absolute top-0 right-0 pt-4 pr-4">
<button
on:click={() => (isModalActive = false)}
type="button"
class=" rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<span class="sr-only">Close</span>
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium pb-4" id="modal-title">New SSH Key</h3>
<div class="text-xs text-stone-400">Add an SSH key to your Coolify instance.</div>
<div class="mt-2">
<label for="privateKey" class="pb-2">Key</label>
<textarea
id="privateKey"
required
bind:value={newSSHKey.privateKey}
class="w-full"
rows={15}
/>
</div>
<div class="mt-2">
<label for="name" class="pb-2">Name</label>
<input id="name" required bind:value={newSSHKey.name} class="w-full" />
</div>
</div>
</div>
<div class="mt-5 flex space-x-4 justify-end">
<button type="submit" class="btn btn-sm bg-success">Save</button>
<button on:click={() => (isModalActive = false)} type="button" class="btn btn-sm"
>Cancel</button
>
</div>
</form>
</div>
</div>
</div>
{/if}

View File

@@ -0,0 +1,145 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ stuff }) => {
try {
return {
props: {
...stuff
}
};
} catch (error: any) {
return {
status: 500,
error: new Error(error)
};
}
};
</script>
<script lang="ts">
export let sshKeys: any;
import { del, post } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import Menu from './_Menu.svelte';
let loading = {
save: false
};
let isModalActive = false;
let newSSHKey = {
name: null,
privateKey: null
};
async function handleSubmit() {
try {
await post(`/settings/sshKey`, { ...newSSHKey });
return window.location.reload();
} catch (error) {
errorNotification(error);
return false;
}
}
async function deleteSSHKey(id: string) {
const sure = confirm('Are you sure you would like to delete this SSH key?');
if (sure) {
try {
if (!id) return;
await del(`/settings/sshKey`, { id });
return window.location.reload();
} catch (error) {
errorNotification(error);
return false;
}
}
}
</script>
<div class="title font-bold pb-3">SSH Keys</div>
<div class="w-full lg:w-[50em]">
{#if sshKeys.length === 0}
<div class="text-sm">No SSH keys found</div>
<label
for="my-modal"
class="btn btn-sm bg-settings text-black mt-6"
on:click={() => (isModalActive = true)}>Add SSH Key</label
>
{:else}
<div
class="mx-auto w-full p-6 bg-coolgray-100 rounded border-coolgray-300 border "
>
<table class="table w-full">
<thead>
<tr>
<th>Name</th>
<th>CreatedAt</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each sshKeys as key}
<tr>
<td>{key.name}</td>
<td>{key.createdAt}</td>
<td
><button on:click={() => deleteSSHKey(key.id)} class="btn btn-sm bg-error"
>Delete</button
></td
>
</tr>
{/each}
</tbody>
</table>
<label
for="my-modal"
class="btn btn-sm bg-settings text-black mt-6"
on:click={() => (isModalActive = true)}>Add SSH Key</label
>
</div>
{/if}
</div>
{#if isModalActive}
<input type="checkbox" id="my-modal" class="modal-toggle" />
<div class="modal modal-bottom sm:modal-middle">
<div class="modal-box rounded bg-coolgray-300">
<h3 class="font-bold text-lg">Add a new SSH Key to Coolify</h3>
<p class="py-4">
SSH Keys can be used to authenticate & execute commands on remote servers.
<br /><br />You can generate a new public/private key using the following command:
<br />
<br />
<code class="bg-coolgray-100 p-2 rounded">ssh-keygen -t rsa -b 4096</code>
</p>
<div class="modal-action">
<form on:submit|preventDefault={handleSubmit}>
<label for="name" class="">Name</label>
<input
id="name"
required
bind:value={newSSHKey.name}
class="w-full bg-coolgray-100"
/>
<label for="privateKey" class="pt-4">Private Key</label>
<textarea
id="privateKey"
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
required
bind:value={newSSHKey.privateKey}
class="w-full bg-coolgray-100"
rows={15}
/>
<label for="my-modal">
<button type="submit" class="btn btn-sm bg-settings text-black mt-4">Save</button
></label
>
<button on:click={() => (isModalActive = false)} type="button" class="btn btn-sm"
>Cancel</button
>
</form>
</div>
</div>
</div>
{/if}