diff --git a/apps/api/src/lib/serviceFields.ts b/apps/api/src/lib/serviceFields.ts index 01f628be2..dadbc6713 100644 --- a/apps/api/src/lib/serviceFields.ts +++ b/apps/api/src/lib/serviceFields.ts @@ -173,6 +173,14 @@ export const wordpress = [{ isNumber: false, isBoolean: false, isEncrypted: false +}, +{ + name: 'ftpPassword', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: true }] export const ghost = [{ name: 'defaultEmail', diff --git a/apps/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index 68c43b37e..c5e3f5a89 100644 --- a/apps/api/src/routes/api/v1/services/handlers.ts +++ b/apps/api/src/routes/api/v1/services/handlers.ts @@ -2,9 +2,10 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; import fs from 'fs/promises'; import yaml from 'js-yaml'; import bcrypt from 'bcryptjs'; -import { prisma, uniqueName, asyncExecShell, getServiceImage, getServiceImages, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePort, getDomain, errorHandler, supportedServiceTypesAndVersions } from '../../../../lib/common'; +import { prisma, uniqueName, asyncExecShell, getServiceImage, getServiceImages, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePort, getDomain, errorHandler, supportedServiceTypesAndVersions, generatePassword, isDev, stopTcpHttpProxy } from '../../../../lib/common'; import { day } from '../../../../lib/dayjs'; import { checkContainer, dockerInstance, getEngine, removeContainer } from '../../../../lib/docker'; +import cuid from 'cuid'; export async function listServices(request: FastifyRequest) { try { @@ -259,7 +260,7 @@ export async function checkService(request: FastifyRequest) { exposePort = Number(exposePort); if (exposePort < 1024 || exposePort > 65535) { - throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` } + throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` } } const publicPort = await getPort({ port: exposePort }); @@ -2416,4 +2417,163 @@ export async function activatePlausibleUsers(request: FastifyRequest, reply: Fas } catch ({ status, message }) { return errorHandler({ status, message }) } -} \ No newline at end of file +} +export async function activateWordpressFtp(request: FastifyRequest, reply: FastifyReply) { + const { id } = request.params + const teamId = request.user.teamId; + + const { ftpEnabled } = request.body; + + const publicPort = await getFreePort(); + let ftpUser = cuid(); + let ftpPassword = generatePassword(); + + const hostkeyDir = isDev ? '/tmp/hostkeys' : '/app/ssl/hostkeys'; + try { + const data = await prisma.wordpress.update({ + where: { serviceId: id }, + data: { ftpEnabled }, + include: { service: { include: { destinationDocker: true } } } + }); + const { + service: { destinationDockerId, destinationDocker }, + ftpPublicPort, + ftpUser: user, + ftpPassword: savedPassword, + ftpHostKey, + ftpHostKeyPrivate + } = data; + const { network, engine } = destinationDocker; + const host = getEngine(engine); + if (ftpEnabled) { + if (user) ftpUser = user; + if (savedPassword) ftpPassword = decrypt(savedPassword); + + const { stdout: password } = await asyncExecShell( + `echo ${ftpPassword} | openssl passwd -1 -stdin` + ); + if (destinationDockerId) { + 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 ${hostkeyDir}/${id}.ed25519` + ); + const { stdout: ftpHostKey } = await asyncExecShell(`cat ${hostkeyDir}/${id}.ed25519`); + await prisma.wordpress.update({ + where: { serviceId: id }, + data: { ftpHostKey: encrypt(ftpHostKey) } + }); + } else { + await asyncExecShell(`echo "${decrypt(ftpHostKey)}" > ${hostkeyDir}/${id}.ed25519`); + } + if (!ftpHostKeyPrivate) { + await asyncExecShell(`ssh-keygen -t rsa -b 4096 -N "" -f ${hostkeyDir}/${id}.rsa`); + const { stdout: ftpHostKeyPrivate } = await asyncExecShell(`cat ${hostkeyDir}/${id}.rsa`); + await prisma.wordpress.update({ + where: { serviceId: id }, + data: { ftpHostKeyPrivate: encrypt(ftpHostKeyPrivate) } + }); + } else { + await asyncExecShell(`echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`); + } + + await prisma.wordpress.update({ + where: { serviceId: id }, + data: { + ftpPublicPort: publicPort, + ftpUser: user ? undefined : ftpUser, + ftpPassword: savedPassword ? undefined : 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); + // + } + const volumes = [ + `${id}-wordpress-data:/home/${ftpUser}`, + `${isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys' + }/${id}.ed25519:/etc/ssh/ssh_host_ed25519_key`, + `${isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys' + }/${id}.rsa:/etc/ssh/ssh_host_rsa_key`, + `${isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys' + }/${id}.sh:/etc/sftp.d/chmod.sh` + ]; + + const compose: ComposeFile = { + version: '3.8', + services: { + [`${id}-ftp`]: { + image: `atmoz/sftp:alpine`, + command: `'${ftpUser}:${password.replace('\n', '').replace(/\$/g, '$$$')}:e:33'`, + extra_hosts: ['host.docker.internal:host-gateway'], + container_name: `${id}-ftp`, + volumes, + networks: [network], + depends_on: [], + restart: 'always' + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: { + [`${id}-wordpress-data`]: { + external: true, + name: `${id}-wordpress-data` + } + } + }; + await fs.writeFile( + `${hostkeyDir}/${id}.sh`, + `#!/bin/bash\nchmod 600 /etc/ssh/ssh_host_ed25519_key /etc/ssh/ssh_host_rsa_key` + ); + await asyncExecShell(`chmod +x ${hostkeyDir}/${id}.sh`); + await fs.writeFile(`${hostkeyDir}/${id}-docker-compose.yml`, yaml.dump(compose)); + await asyncExecShell( + `DOCKER_HOST=${host} docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d` + ); + } + return reply.code(201).send({ + publicPort, + ftpUser, + ftpPassword + }) + } else { + await prisma.wordpress.update({ + where: { serviceId: id }, + data: { ftpPublicPort: null } + }); + try { + await asyncExecShell( + `DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp` + ); + } catch (error) { + // + } + await stopTcpHttpProxy(id, destinationDocker, ftpPublicPort); + return { + }; + } + } catch ({ status, message }) { + return errorHandler({ status, message }) + } finally { + await asyncExecShell( + `rm -f ${hostkeyDir}/${id}-docker-compose.yml ${hostkeyDir}/${id}.ed25519 ${hostkeyDir}/${id}.ed25519.pub ${hostkeyDir}/${id}.rsa ${hostkeyDir}/${id}.rsa.pub ${hostkeyDir}/${id}.sh` + ); + } + +} diff --git a/apps/api/src/routes/api/v1/services/index.ts b/apps/api/src/routes/api/v1/services/index.ts index 2a6d8dff7..be4b7964e 100644 --- a/apps/api/src/routes/api/v1/services/index.ts +++ b/apps/api/src/routes/api/v1/services/index.ts @@ -1,6 +1,7 @@ import { FastifyPluginAsync } from 'fastify'; import { activatePlausibleUsers, + activateWordpressFtp, checkService, deleteService, deleteServiceSecret, @@ -65,6 +66,7 @@ const root: FastifyPluginAsync = async (fastify, opts): Promise => { fastify.post('/:id/:type/settings', async (request, reply) => await setSettingsService(request, reply)); fastify.post('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply)); + fastify.post('/:id/wordpress/ftp', async (request, reply) => await activateWordpressFtp(request, reply)); }; export default root; diff --git a/apps/ui/src/routes/services/[id]/_Services/_Wordpress.svelte b/apps/ui/src/routes/services/[id]/_Services/_Wordpress.svelte index 845263024..9dc4fee22 100644 --- a/apps/ui/src/routes/services/[id]/_Services/_Wordpress.svelte +++ b/apps/ui/src/routes/services/[id]/_Services/_Wordpress.svelte @@ -112,7 +112,7 @@ define('SUBDOMAIN_INSTALL', false);`
- +
{/if}