feat: Wordpress on-demand SFTP
This commit is contained in:
parent
ca705bbf89
commit
8e9e6607e5
@ -332,6 +332,12 @@ model Wordpress {
|
|||||||
mysqlRootUserPassword String
|
mysqlRootUserPassword String
|
||||||
mysqlDatabase String?
|
mysqlDatabase String?
|
||||||
mysqlPublicPort Int?
|
mysqlPublicPort Int?
|
||||||
|
ftpEnabled Boolean @default(false)
|
||||||
|
ftpUser String?
|
||||||
|
ftpPassword String?
|
||||||
|
ftpPublicPort Int?
|
||||||
|
ftpHostKey String?
|
||||||
|
ftpHostKeyPrivate String?
|
||||||
serviceId String @unique
|
serviceId String @unique
|
||||||
service Service @relation(fields: [serviceId], references: [id])
|
service Service @relation(fields: [serviceId], references: [id])
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
export let isCenter = true;
|
export let isCenter = true;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
export let dataTooltip = null;
|
export let dataTooltip = null;
|
||||||
|
export let loading = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center py-4 pr-8">
|
<div class="flex items-center py-4 pr-8">
|
||||||
@ -26,7 +27,7 @@
|
|||||||
on:click
|
on:click
|
||||||
aria-pressed="false"
|
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="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-green-600={setting}
|
||||||
class:bg-stone-700={!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=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
|
||||||
class:opacity-0={setting}
|
class:opacity-0={setting}
|
||||||
class:opacity-100={!setting}
|
class:opacity-100={!setting}
|
||||||
|
class:animate-spin={loading}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
|
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
|
||||||
@ -57,6 +59,7 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class:opacity-100={setting}
|
class:opacity-100={setting}
|
||||||
class:opacity-0={!setting}
|
class:opacity-0={!setting}
|
||||||
|
class:animate-spin={loading}
|
||||||
>
|
>
|
||||||
<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
|
<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
|
||||||
<path
|
<path
|
||||||
|
@ -59,8 +59,12 @@ export async function getService({ id, teamId }) {
|
|||||||
return s;
|
return s;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (body.wordpress?.ftpPassword) {
|
||||||
|
body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
|
||||||
|
}
|
||||||
|
const settings = await prisma.setting.findFirst();
|
||||||
|
|
||||||
return { ...body };
|
return { ...body, settings };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function configureServiceType({ id, type }) {
|
export async function configureServiceType({ id, type }) {
|
||||||
|
@ -108,7 +108,7 @@ export async function stopTcpHttpProxy(destinationDocker, publicPort) {
|
|||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export async function startTcpProxy(destinationDocker, id, publicPort, privatePort) {
|
export async function startTcpProxy(destinationDocker, id, publicPort, privatePort, volume = null) {
|
||||||
const { network, engine } = destinationDocker;
|
const { network, engine } = destinationDocker;
|
||||||
const host = getEngine(engine);
|
const host = getEngine(engine);
|
||||||
|
|
||||||
@ -123,7 +123,9 @@ export async function startTcpProxy(destinationDocker, id, publicPort, privatePo
|
|||||||
);
|
);
|
||||||
const ip = JSON.parse(Config)[0].Gateway;
|
const ip = JSON.parse(Config)[0].Gateway;
|
||||||
return await asyncExecShell(
|
return await asyncExecShell(
|
||||||
`DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} -d coollabsio/${defaultProxyImageTcp}`
|
`DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} ${
|
||||||
|
volume ? `-v ${volume}` : ''
|
||||||
|
} -d coollabsio/${defaultProxyImageTcp}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
export let service;
|
export let service;
|
||||||
export let isRunning;
|
export let isRunning;
|
||||||
export let readOnly;
|
export let readOnly;
|
||||||
|
export let settings;
|
||||||
|
|
||||||
import { page, session } from '$app/stores';
|
import { page, session } from '$app/stores';
|
||||||
import { post } from '$lib/api';
|
import { post } from '$lib/api';
|
||||||
@ -143,7 +144,7 @@
|
|||||||
{:else if service.type === 'vscodeserver'}
|
{:else if service.type === 'vscodeserver'}
|
||||||
<VsCodeServer {service} />
|
<VsCodeServer {service} />
|
||||||
{:else if service.type === 'wordpress'}
|
{:else if service.type === 'wordpress'}
|
||||||
<Wordpress bind:service {isRunning} {readOnly} />
|
<Wordpress bind:service {isRunning} {readOnly} {settings} />
|
||||||
{:else if service.type === 'ghost'}
|
{:else if service.type === 'ghost'}
|
||||||
<Ghost bind:service {readOnly} />
|
<Ghost bind:service {readOnly} />
|
||||||
{:else if service.type === 'meilisearch'}
|
{:else if service.type === 'meilisearch'}
|
||||||
@ -151,17 +152,4 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<!-- <div class="font-bold flex space-x-1 pb-5">
|
|
||||||
<div class="text-xl tracking-tight mr-4">Features</div>
|
|
||||||
</div>
|
|
||||||
<div class="px-4 sm:px-6 pb-10">
|
|
||||||
<ul class="mt-2 divide-y divide-stone-800">
|
|
||||||
<Setting
|
|
||||||
bind:setting={isPublic}
|
|
||||||
on:click={() => changeSettings('isPublic')}
|
|
||||||
title="Set it public"
|
|
||||||
description="Your database will be reachable over the internet. <br>Take security seriously in this case!"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,55 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { post } from '$lib/api';
|
||||||
|
import { page } from '$app/stores';
|
||||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||||
|
import Setting from '$lib/components/Setting.svelte';
|
||||||
|
import { errorNotification } from '$lib/form';
|
||||||
|
import { browser } from '$app/env';
|
||||||
|
import { getDomain } from '$lib/components/common';
|
||||||
|
|
||||||
export let service;
|
export let service;
|
||||||
export let isRunning;
|
export let isRunning;
|
||||||
export let readOnly;
|
export let readOnly;
|
||||||
|
export let settings;
|
||||||
|
const { id } = $page.params;
|
||||||
|
|
||||||
|
let ftpUrl = generateUrl(service.wordpress.ftpPublicPort);
|
||||||
|
let ftpUser = service.wordpress.ftpUser;
|
||||||
|
let ftpPassword = service.wordpress.ftpPassword;
|
||||||
|
let ftpLoading = false;
|
||||||
|
|
||||||
|
function generateUrl(publicPort) {
|
||||||
|
return browser
|
||||||
|
? `sftp://${
|
||||||
|
settings.fqdn ? getDomain(settings.fqdn) : window.location.hostname
|
||||||
|
}:${publicPort}`
|
||||||
|
: 'Loading...';
|
||||||
|
}
|
||||||
|
async function changeSettings(name) {
|
||||||
|
ftpLoading = true;
|
||||||
|
let ftpEnabled = service.wordpress.ftpEnabled;
|
||||||
|
|
||||||
|
if (name === 'ftpEnabled') {
|
||||||
|
ftpEnabled = !ftpEnabled;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
publicPort,
|
||||||
|
ftpUser: user,
|
||||||
|
ftpPassword: password
|
||||||
|
} = await post(`/services/${id}/wordpress/settings.json`, {
|
||||||
|
ftpEnabled
|
||||||
|
});
|
||||||
|
ftpUrl = generateUrl(publicPort);
|
||||||
|
ftpUser = user;
|
||||||
|
ftpPassword = password;
|
||||||
|
service.wordpress.ftpEnabled = ftpEnabled;
|
||||||
|
} catch ({ error }) {
|
||||||
|
return errorNotification(error);
|
||||||
|
} finally {
|
||||||
|
ftpLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex space-x-1 py-5 font-bold">
|
<div class="flex space-x-1 py-5 font-bold">
|
||||||
@ -28,6 +74,29 @@ define('SUBDOMAIN_INSTALL', false);`
|
|||||||
: 'N/A'}>{service.wordpress.extraConfig}</textarea
|
: 'N/A'}>{service.wordpress.extraConfig}</textarea
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid grid-cols-2 items-center px-10">
|
||||||
|
<Setting
|
||||||
|
bind:setting={service.wordpress.ftpEnabled}
|
||||||
|
loading={ftpLoading}
|
||||||
|
on:click={() => 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."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if service.wordpress.ftpEnabled}
|
||||||
|
<div class="grid grid-cols-2 items-center px-10">
|
||||||
|
<label for="ftpUrl">sFTP Connection URI</label>
|
||||||
|
<CopyPasswordField id="ftpUrl" readonly disabled name="ftpUrl" value={ftpUrl} />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 items-center px-10">
|
||||||
|
<label for="ftpUser">User</label>
|
||||||
|
<CopyPasswordField id="ftpUser" readonly disabled name="ftpUser" value={ftpUser} />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 items-center px-10">
|
||||||
|
<label for="ftpPassword">Password</label>
|
||||||
|
<CopyPasswordField id="ftpPassword" readonly disabled name="ftpPassword" value={ftpPassword} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="flex space-x-1 py-5 font-bold">
|
<div class="flex space-x-1 py-5 font-bold">
|
||||||
<div class="title">MySQL</div>
|
<div class="title">MySQL</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
const endpoint = `/services/${params.id}.json`;
|
const endpoint = `/services/${params.id}.json`;
|
||||||
const res = await fetch(endpoint);
|
const res = await fetch(endpoint);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const { service, isRunning } = await res.json();
|
const { service, isRunning, settings } = await res.json();
|
||||||
if (!service || Object.entries(service).length === 0) {
|
if (!service || Object.entries(service).length === 0) {
|
||||||
return {
|
return {
|
||||||
status: 302,
|
status: 302,
|
||||||
@ -45,7 +45,8 @@
|
|||||||
stuff: {
|
stuff: {
|
||||||
service,
|
service,
|
||||||
isRunning,
|
isRunning,
|
||||||
readOnly
|
readOnly,
|
||||||
|
settings
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ export const get: RequestHandler = async (event) => {
|
|||||||
const { id } = event.params;
|
const { id } = event.params;
|
||||||
try {
|
try {
|
||||||
const service = await db.getService({ id, teamId });
|
const service = await db.getService({ id, teamId });
|
||||||
const { destinationDockerId, destinationDocker, type, version } = service;
|
const { destinationDockerId, destinationDocker, type, version, settings } = service;
|
||||||
|
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
if (destinationDockerId) {
|
if (destinationDockerId) {
|
||||||
@ -46,7 +46,8 @@ export const get: RequestHandler = async (event) => {
|
|||||||
return {
|
return {
|
||||||
body: {
|
body: {
|
||||||
isRunning,
|
isRunning,
|
||||||
service
|
service,
|
||||||
|
settings
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -6,7 +6,8 @@
|
|||||||
props: {
|
props: {
|
||||||
service: stuff.service,
|
service: stuff.service,
|
||||||
isRunning: stuff.isRunning,
|
isRunning: stuff.isRunning,
|
||||||
readOnly: stuff.readOnly
|
readOnly: stuff.readOnly,
|
||||||
|
settings: stuff.settings
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -37,6 +38,7 @@
|
|||||||
export let service;
|
export let service;
|
||||||
export let isRunning;
|
export let isRunning;
|
||||||
export let readOnly;
|
export let readOnly;
|
||||||
|
export let settings;
|
||||||
|
|
||||||
if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) {
|
if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) {
|
||||||
service.fqdn = `http://${cuid()}.demo.coolify.io`;
|
service.fqdn = `http://${cuid()}.demo.coolify.io`;
|
||||||
@ -76,4 +78,4 @@
|
|||||||
<ServiceLinks {service} />
|
<ServiceLinks {service} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Services bind:service {isRunning} {readOnly} />
|
<Services bind:service {isRunning} {readOnly} {settings} />
|
||||||
|
138
src/routes/services/[id]/wordpress/settings.json.ts
Normal file
138
src/routes/services/[id]/wordpress/settings.json.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user