feat: custom certificate
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -41,4 +41,9 @@ export interface DeleteSSHKey {
|
||||
Body: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
export interface OnlyIdInBody {
|
||||
Body: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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>
|
||||
|
||||
20
apps/ui/src/lib/components/Upload.svelte
Normal file
20
apps/ui/src/lib/components/Upload.svelte
Normal 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>
|
||||
@@ -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')}
|
||||
>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
144
apps/ui/src/routes/settings/certificates.svelte
Normal file
144
apps/ui/src/routes/settings/certificates.svelte
Normal 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}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
145
apps/ui/src/routes/settings/ssh.svelte
Normal file
145
apps/ui/src/routes/settings/ssh.svelte
Normal 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}
|
||||
Reference in New Issue
Block a user