diff --git a/apps/api/prisma/migrations/20220923122227_custom_ssl_for_applications/migration.sql b/apps/api/prisma/migrations/20220923122227_custom_ssl_for_applications/migration.sql new file mode 100644 index 000000000..b9261a955 --- /dev/null +++ b/apps/api/prisma/migrations/20220923122227_custom_ssl_for_applications/migration.sql @@ -0,0 +1,23 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +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, + "autodeploy" BOOLEAN NOT NULL DEFAULT true, + "isBot" BOOLEAN NOT NULL DEFAULT false, + "isPublicRepository" BOOLEAN NOT NULL DEFAULT false, + "isDBBranching" BOOLEAN NOT NULL DEFAULT false, + "isCustomSSL" 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", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isDBBranching", "isPublicRepository", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isDBBranching", "isPublicRepository", "previews", "updatedAt" FROM "ApplicationSettings"; +DROP TABLE "ApplicationSettings"; +ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings"; +CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 3fb93d4a1..d064f707a 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -172,6 +172,7 @@ model ApplicationSettings { isBot Boolean @default(false) isPublicRepository Boolean @default(false) isDBBranching Boolean @default(false) + isCustomSSL Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt application Application @relation(fields: [applicationId], references: [id]) diff --git a/apps/api/src/jobs/infrastructure.ts b/apps/api/src/jobs/infrastructure.ts index c1ac372d4..3f0dda214 100644 --- a/apps/api/src/jobs/infrastructure.ts +++ b/apps/api/src/jobs/infrastructure.ts @@ -60,40 +60,49 @@ async function copySSLCertificates() { const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } }) for (const destination of destinations) { if (destination.remoteEngine) { + const { id: dockerId, remoteIpAddress, remoteVerified } = destination if (!remoteVerified) { continue; } - // TODO: copy certificates to remote engine for (const certificate of certificates) { - const { id, key, cert } = certificate - const decryptedKey = decrypt(key) - await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey) - await fs.writeFile(`/tmp/${id}-cert.pem`, cert) - await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`) - await fs.rm(`/tmp/${id}-key.pem`) - await fs.rm(`/tmp/${id}-cert.pem`) - await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` }) - await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/ && rm /tmp/${id}-key.pem` }) - await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/ && rm /tmp/${id}-cert.pem` }) + try { + const { id, key, cert } = certificate + const decryptedKey = decrypt(key) + await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey) + await fs.writeFile(`/tmp/${id}-cert.pem`, cert) + await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`) + await fs.rm(`/tmp/${id}-key.pem`) + await fs.rm(`/tmp/${id}-cert.pem`) + await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` }) + await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/ && rm /tmp/${id}-key.pem` }) + await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/ && rm /tmp/${id}-cert.pem` }) + } catch (error) { + console.log('Error copying SSL certificates to remote engine', error) + } } + } else { + for (const certificate of certificates) { - const { id, key, cert } = certificate - const decryptedKey = decrypt(key) - await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || 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`) + try { + const { id, key, cert } = certificate + const decryptedKey = decrypt(key) + await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || 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) { + console.log('Error copying SSL certificates to remote engine', error) + } } } - } } catch (error) { - + console.log('Error copying SSL certificates', error) } } async function checkProxies() { diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index be7648bfe..0a2414a12 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -321,17 +321,12 @@ export async function saveApplication(request: FastifyRequest, export async function saveApplicationSettings(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params - const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot, isDBBranching } = request.body - // const isDouble = await checkDoubleBranch(branch, projectId); - // if (isDouble && autodeploy) { - // await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } }) - // throw { status: 500, message: 'Cannot activate automatic deployments until only one application is defined for this repository / branch.' } - // } + const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot, isDBBranching, isCustomSSL } = request.body await prisma.application.update({ where: { id }, - data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching } } }, + data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching, isCustomSSL } } }, include: { destinationDocker: true } - }); + }); return reply.code(201).send(); } catch ({ status, message }) { return errorHandler({ status, message }) diff --git a/apps/api/src/routes/api/v1/applications/types.ts b/apps/api/src/routes/api/v1/applications/types.ts index 121d23d75..443deb00f 100644 --- a/apps/api/src/routes/api/v1/applications/types.ts +++ b/apps/api/src/routes/api/v1/applications/types.ts @@ -26,7 +26,7 @@ export interface SaveApplication extends OnlyId { } export interface SaveApplicationSettings extends OnlyId { Querystring: { domain: string; }; - Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; isDBBranching: boolean }; + Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; isDBBranching: boolean, isCustomSSL: boolean }; } export interface DeleteApplication extends OnlyId { Querystring: { domain: string; }; diff --git a/apps/api/src/routes/api/v1/settings/index.ts b/apps/api/src/routes/api/v1/settings/index.ts index aa95558ad..45e418b34 100644 --- a/apps/api/src/routes/api/v1/settings/index.ts +++ b/apps/api/src/routes/api/v1/settings/index.ts @@ -45,6 +45,7 @@ const root: FastifyPluginAsync = async (fastify): Promise => { } } await prisma.certificate.create({ data: { cert, key: encrypt(key), team: { connect: { id: teamId } } } }) + await prisma.applicationSettings.updateMany({ where: { application: { AND: [{ fqdn: { endsWith: cn } }, { fqdn: { startsWith: 'https' } }] } }, data: { isCustomSSL: true } }) return { message: 'Certificated uploaded' } } catch ({ status, message }) { return errorHandler({ status, message }); diff --git a/apps/api/src/routes/webhooks/traefik/handlers.ts b/apps/api/src/routes/webhooks/traefik/handlers.ts index d46b23af8..381869430 100644 --- a/apps/api/src/routes/webhooks/traefik/handlers.ts +++ b/apps/api/src/routes/webhooks/traefik/handlers.ts @@ -6,7 +6,7 @@ import { TraefikOtherConfiguration } from "./types"; import { OnlyId } from "../../../types"; function configureMiddleware( - { id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type }, + { id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type, isCustomSSL }, traefik ) { if (isHttps) { @@ -55,7 +55,7 @@ function configureMiddleware( entrypoints: ['websecure'], rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`, service: `${id}`, - tls: { + tls: isCustomSSL ? true : { certresolver: 'letsencrypt' }, middlewares: [] @@ -66,7 +66,7 @@ function configureMiddleware( entrypoints: ['websecure'], rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`/\`)`, service: `${id}`, - tls: { + tls: isCustomSSL ? true : { certresolver: 'letsencrypt' }, middlewares: [] @@ -99,7 +99,7 @@ function configureMiddleware( entrypoints: ['websecure'], rule: `Host(\`${domain}\`) && PathPrefix(\`/\`)`, service: `${id}`, - tls: { + tls: isCustomSSL ? true : { certresolver: 'letsencrypt' }, middlewares: [] @@ -179,7 +179,7 @@ function configureMiddleware( export async function traefikConfiguration(request, reply) { try { const sslpath = '/etc/traefik/acme/custom'; - const certificates = await prisma.certificate.findMany() + const certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { remoteEngine: false, isCoolifyProxyUsed: true } } } } }) let parsedCertificates = [] for (const certificate of certificates) { parsedCertificates.push({ @@ -236,7 +236,7 @@ export async function traefikConfiguration(request, reply) { port, destinationDocker, destinationDockerId, - settings: { previews, dualCerts } + settings: { previews, dualCerts, isCustomSSL } } = application; if (destinationDockerId) { const { network, id: dockerId } = destinationDocker; @@ -256,7 +256,8 @@ export async function traefikConfiguration(request, reply) { isRunning, isHttps, isWWW, - isDualCerts: dualCerts + isDualCerts: dualCerts, + isCustomSSL }); } if (previews) { @@ -279,7 +280,8 @@ export async function traefikConfiguration(request, reply) { nakedDomain, isHttps, isWWW, - isDualCerts: dualCerts + isDualCerts: dualCerts, + isCustomSSL }); } } @@ -547,7 +549,7 @@ export async function remoteTraefikConfiguration(request: FastifyRequest const { id } = request.params try { const sslpath = '/etc/traefik/acme/custom'; - const certificates = await prisma.certificate.findMany({ where: { team: { destinationDocker: { some: { id, remoteEngine: true, isCoolifyProxyUsed: true, remoteVerified: true } } } } }) + const certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { id, remoteEngine: true, isCoolifyProxyUsed: true, remoteVerified: true } } } } }) let parsedCertificates = [] for (const certificate of certificates) { parsedCertificates.push({ diff --git a/apps/ui/src/routes/applications/[id]/_Menu.svelte b/apps/ui/src/routes/applications/[id]/_Menu.svelte index 19075a254..5e880724a 100644 --- a/apps/ui/src/routes/applications/[id]/_Menu.svelte +++ b/apps/ui/src/routes/applications/[id]/_Menu.svelte @@ -132,7 +132,7 @@ - Peristent VolumesPersistent Volumes
  • !$status.application.isRunning && changeSettings('dualCerts')} /> + {#if isHttps} +
    + changeSettings('isCustomSSL')} + /> +
    + {/if} {/if} {#if application.buildPack === 'python'}
    @@ -769,4 +795,4 @@
    - \ No newline at end of file + diff --git a/apps/ui/src/routes/settings/certificates.svelte b/apps/ui/src/routes/settings/certificates.svelte index eb2737914..a9fabb44c 100644 --- a/apps/ui/src/routes/settings/certificates.svelte +++ b/apps/ui/src/routes/settings/certificates.svelte @@ -41,7 +41,7 @@ } } async function deleteCertificate(id: string) { - const sure = confirm('Are you sure you would like to delete this SSH key?'); + const sure = confirm('Are you sure you would like to delete this SSL Certificate?'); if (sure) { try { if (!id) return; @@ -89,7 +89,7 @@
    No SSL Certificate found
    {/if} - + {#if isModalActive}