diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 986a773bf..d5e322337 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -332,6 +332,12 @@ model Wordpress { mysqlRootUserPassword String mysqlDatabase String? mysqlPublicPort Int? + ftpEnabled Boolean @default(false) + ftpUser String? + ftpPassword String? + ftpPublicPort Int? + ftpHostKey String? + ftpHostKeyPrivate String? serviceId String @unique service Service @relation(fields: [serviceId], references: [id]) createdAt DateTime @default(now()) diff --git a/src/lib/components/Setting.svelte b/src/lib/components/Setting.svelte index c8764bed7..7d87dd6fb 100644 --- a/src/lib/components/Setting.svelte +++ b/src/lib/components/Setting.svelte @@ -7,6 +7,7 @@ export let isCenter = true; export let disabled = false; export let dataTooltip = null; + export let loading = false;
@@ -26,7 +27,7 @@ on:click aria-pressed="false" class="relative mx-20 inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out" - class:opacity-50={disabled} + class:opacity-50={disabled || loading} class:bg-green-600={setting} class:bg-stone-700={!setting} > @@ -40,6 +41,7 @@ class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in" class:opacity-0={setting} class:opacity-100={!setting} + class:animate-spin={loading} aria-hidden="true" > @@ -57,6 +59,7 @@ aria-hidden="true" class:opacity-100={setting} class:opacity-0={!setting} + class:animate-spin={loading} > {:else if service.type === 'wordpress'} - + {:else if service.type === 'ghost'} {:else if service.type === 'meilisearch'} @@ -151,17 +152,4 @@ {/if}
- diff --git a/src/routes/services/[id]/_Services/_Wordpress.svelte b/src/routes/services/[id]/_Services/_Wordpress.svelte index 883178a22..dc7eb4ebb 100644 --- a/src/routes/services/[id]/_Services/_Wordpress.svelte +++ b/src/routes/services/[id]/_Services/_Wordpress.svelte @@ -1,9 +1,55 @@
@@ -28,6 +74,29 @@ define('SUBDOMAIN_INSTALL', false);` : 'N/A'}>{service.wordpress.extraConfig}
+
+ changeSettings('ftpEnabled')} + title="Enable sFTP connection to WordPress data" + description="Enables an on-demand sFTP connection to the WordPress data directory. This is useful if you want to use sFTP to upload files." + /> +
+{#if service.wordpress.ftpEnabled} +
+ + +
+
+ + +
+
+ + +
+{/if}
MySQL
diff --git a/src/routes/services/[id]/__layout.svelte b/src/routes/services/[id]/__layout.svelte index 362f89786..37158d711 100644 --- a/src/routes/services/[id]/__layout.svelte +++ b/src/routes/services/[id]/__layout.svelte @@ -16,7 +16,7 @@ const endpoint = `/services/${params.id}.json`; const res = await fetch(endpoint); if (res.ok) { - const { service, isRunning } = await res.json(); + const { service, isRunning, settings } = await res.json(); if (!service || Object.entries(service).length === 0) { return { status: 302, @@ -45,7 +45,8 @@ stuff: { service, isRunning, - readOnly + readOnly, + settings } }; } diff --git a/src/routes/services/[id]/index.json.ts b/src/routes/services/[id]/index.json.ts index 676ff6405..f75ff87f6 100644 --- a/src/routes/services/[id]/index.json.ts +++ b/src/routes/services/[id]/index.json.ts @@ -17,7 +17,7 @@ export const get: RequestHandler = async (event) => { const { id } = event.params; try { const service = await db.getService({ id, teamId }); - const { destinationDockerId, destinationDocker, type, version } = service; + const { destinationDockerId, destinationDocker, type, version, settings } = service; let isRunning = false; if (destinationDockerId) { @@ -46,7 +46,8 @@ export const get: RequestHandler = async (event) => { return { body: { isRunning, - service + service, + settings } }; } catch (error) { diff --git a/src/routes/services/[id]/index.svelte b/src/routes/services/[id]/index.svelte index 9f64e039b..a16591266 100644 --- a/src/routes/services/[id]/index.svelte +++ b/src/routes/services/[id]/index.svelte @@ -6,7 +6,8 @@ props: { service: stuff.service, isRunning: stuff.isRunning, - readOnly: stuff.readOnly + readOnly: stuff.readOnly, + settings: stuff.settings } }; } @@ -37,6 +38,7 @@ export let service; export let isRunning; export let readOnly; + export let settings; if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) { service.fqdn = `http://${cuid()}.demo.coolify.io`; @@ -76,4 +78,4 @@ - + diff --git a/src/routes/services/[id]/wordpress/settings.json.ts b/src/routes/services/[id]/wordpress/settings.json.ts new file mode 100644 index 000000000..728d4919f --- /dev/null +++ b/src/routes/services/[id]/wordpress/settings.json.ts @@ -0,0 +1,138 @@ +import { dev } from '$app/env'; +import { asyncExecShell, getEngine, getUserDetails } from '$lib/common'; +import { decrypt, encrypt } from '$lib/crypto'; +import * as db from '$lib/database'; +import { generateDatabaseConfiguration, ErrorHandler, generatePassword } from '$lib/database'; +import { checkContainer, startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; +import cuid from 'cuid'; +import fs from 'fs/promises'; +import getPort, { portNumbers } from 'get-port'; + +export const post: RequestHandler = async (event) => { + const { status, body, teamId } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + const data = await db.prisma.setting.findFirst(); + const { minPort, maxPort } = data; + + const { ftpEnabled } = await event.request.json(); + const publicPort = await getPort({ port: portNumbers(minPort, maxPort) }); + let ftpUser = cuid(); + const ftpPassword = generatePassword(); + + const hostkeyDir = dev ? '/tmp/hostkeys' : '/app/ssl/hostkeys'; + try { + const { stdout: password } = await asyncExecShell( + `echo ${ftpPassword} | openssl passwd -1 -stdin` + ); + const data = await db.prisma.wordpress.update({ + where: { serviceId: id }, + data: { ftpEnabled }, + include: { service: { include: { destinationDocker: true } } } + }); + const { + service: { destinationDockerId, destinationDocker }, + ftpPublicPort: oldPublicPort, + ftpUser: user, + ftpHostKey, + ftpHostKeyPrivate + } = data; + if (user) ftpUser = user; + try { + await fs.stat(hostkeyDir); + } catch (error) { + await asyncExecShell(`mkdir -p ${hostkeyDir}`); + } + if (!ftpHostKey) { + await asyncExecShell( + `ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N "" -q -f /tmp/${id} < /dev/null` + ); + const { stdout: ftpHostKey } = await asyncExecShell(`cat ${hostkeyDir}/${id}.ed25519`); + await db.prisma.wordpress.update({ + where: { serviceId: id }, + data: { ftpHostKey: encrypt(ftpHostKey.replace('\n', '')) } + }); + } else { + await asyncExecShell(`echo ${decrypt(ftpHostKey)} > ${hostkeyDir}/${id}.ed25519`); + } + if (!ftpHostKeyPrivate) { + await asyncExecShell(`ssh-keygen -t rsa -b 4096 -N "" -f /tmp/${id}.rsa < /dev/null`); + const { stdout: ftpHostKeyPrivate } = await asyncExecShell(`cat /tmp/${id}.rsa`); + await db.prisma.wordpress.update({ + where: { serviceId: id }, + data: { ftpHostKeyPrivate: encrypt(ftpHostKeyPrivate.replace('\n', '')) } + }); + } else { + await asyncExecShell(`echo ${decrypt(ftpHostKeyPrivate)} > ${hostkeyDir}/${id}.rsa`); + } + if (destinationDockerId) { + const { network, engine } = destinationDocker; + const host = getEngine(engine); + if (ftpEnabled) { + await db.prisma.wordpress.update({ + where: { serviceId: id }, + data: { ftpPublicPort: publicPort, ftpUser, ftpPassword: encrypt(ftpPassword) } + }); + + try { + const isRunning = await checkContainer(engine, `${id}-ftp`); + if (isRunning) { + await asyncExecShell( + `DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp` + ); + } + } catch (error) { + console.log(error); + // + } + + await asyncExecShell( + `DOCKER_HOST=${host} docker run --restart always --add-host 'host.docker.internal:host-gateway' --network ${network} --name ${id}-ftp -v ${id}-wordpress-data:/home/${ftpUser} -v ${hostkeyDir}/${id}.ed25519:/etc/ssh/ssh_host_ed25519_key -v ${hostkeyDir}/${id}.rsa:/etc/ssh/ssh_host_rsa_key -d atmoz/sftp '${ftpUser}:${password.replace( + '\n', + '' + )}:e:1001'` + ); + + await startTcpProxy(destinationDocker, `${id}-ftp`, publicPort, 22); + } else { + await db.prisma.wordpress.update({ + where: { serviceId: id }, + data: { ftpPublicPort: null } + }); + try { + const isRunning = await checkContainer(engine, `${id}-ftp`); + if (isRunning) { + await asyncExecShell( + `DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp` + ); + } + } catch (error) { + console.log(error); + // + } + + await stopTcpHttpProxy(destinationDocker, oldPublicPort); + } + } + if (ftpEnabled) { + return { + status: 201, + body: { + publicPort, + ftpUser, + ftpPassword + } + }; + } else { + return { + status: 200, + body: {} + }; + } + } catch (error) { + console.log(error); + return ErrorHandler(error); + } +};