wip: trpc
This commit is contained in:
parent
91c36dc810
commit
5cb9216add
6
apps/client/src/lib/components/SimpleExplainer.svelte
Normal file
6
apps/client/src/lib/components/SimpleExplainer.svelte
Normal file
@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
export let text: string;
|
||||
export let customClass = 'max-w-[24rem]';
|
||||
</script>
|
||||
|
||||
<div class="p-2 text-xs text-stone-400 {customClass}">{@html text}</div>
|
69
apps/client/src/routes/destinations/[id]/+layout.svelte
Normal file
69
apps/client/src/routes/destinations/[id]/+layout.svelte
Normal file
@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import type { LayoutData } from './$types';
|
||||
export let data: LayoutData;
|
||||
let destination = data.destination.destination;
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { appSession, trpc } from '$lib/store';
|
||||
import * as Icons from '$lib/components/icons';
|
||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||
|
||||
const isDestinationDeletable =
|
||||
(destination?.application.length === 0 &&
|
||||
destination?.database.length === 0 &&
|
||||
destination?.service.length === 0) ||
|
||||
true;
|
||||
|
||||
async function deleteDestination(destination: any) {
|
||||
if (!isDestinationDeletable) return;
|
||||
const sure = confirm("Are you sure you want to delete this destination? This can't be undone.");
|
||||
if (sure) {
|
||||
try {
|
||||
await trpc.destinations.delete.mutate({ id: destination.id });
|
||||
return await goto('/', { replaceState: true });
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
function deletable() {
|
||||
if (!isDestinationDeletable) {
|
||||
return 'Please delete all resources before deleting this.';
|
||||
}
|
||||
if ($appSession.isAdmin) {
|
||||
return "Delete this destination. This can't be undone.";
|
||||
} else {
|
||||
return "You don't have permission to delete this destination.";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $page.params.id !== 'new'}
|
||||
<nav class="header lg:flex-row flex-col-reverse">
|
||||
<div class="flex flex-row space-x-2 font-bold pt-10 lg:pt-0">
|
||||
<div class="flex flex-col items-center justify-center title">
|
||||
{#if $page.url.pathname === `/destinations/${$page.params.id}`}
|
||||
Configurations
|
||||
{:else if $page.url.pathname.startsWith(`/destinations/${$page.params.id}/configuration/sshkey`)}
|
||||
Select a SSH Key
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:block hidden flex-1" />
|
||||
<div class="flex flex-row flex-wrap space-x-3 justify-center lg:justify-start lg:py-0">
|
||||
<button
|
||||
id="delete"
|
||||
on:click={() => deleteDestination(destination)}
|
||||
type="submit"
|
||||
disabled={!$appSession.isAdmin && isDestinationDeletable}
|
||||
class:hover:text-red-500={$appSession.isAdmin && isDestinationDeletable}
|
||||
class="icons bg-transparent text-sm"
|
||||
class:text-stone-600={!isDestinationDeletable}><Icons.Delete /></button
|
||||
>
|
||||
<Tooltip triggeredBy="#delete">{deletable()}</Tooltip>
|
||||
</div>
|
||||
</nav>
|
||||
{/if}
|
||||
<slot />
|
45
apps/client/src/routes/destinations/[id]/+layout.ts
Normal file
45
apps/client/src/routes/destinations/[id]/+layout.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { trpc } from '$lib/store';
|
||||
import type { LayoutLoad } from './$types';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
function checkConfiguration(destination: any): string | null {
|
||||
let configurationPhase = null;
|
||||
if (!destination?.remoteEngine) return configurationPhase;
|
||||
if (!destination?.sshKey) {
|
||||
configurationPhase = 'sshkey';
|
||||
}
|
||||
return configurationPhase;
|
||||
}
|
||||
|
||||
export const load: LayoutLoad = async ({ params, url }) => {
|
||||
const { pathname } = new URL(url);
|
||||
const { id } = params;
|
||||
try {
|
||||
const destination = await trpc.destinations.getDestinationById.query({ id });
|
||||
if (!destination) {
|
||||
throw redirect(307, '/destinations');
|
||||
}
|
||||
const configurationPhase = checkConfiguration(destination);
|
||||
console.log({ configurationPhase });
|
||||
// if (
|
||||
// configurationPhase &&
|
||||
// pathname !== `/applications/${params.id}/configuration/${configurationPhase}`
|
||||
// ) {
|
||||
// throw redirect(302, `/applications/${params.id}/configuration/${configurationPhase}`);
|
||||
// }
|
||||
return {
|
||||
destination
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
throw error(500, {
|
||||
message: 'An unexpected error occurred, please try again later.' + '<br><br>' + err.message
|
||||
});
|
||||
}
|
||||
|
||||
throw error(500, {
|
||||
message: 'An unexpected error occurred, please try again later.'
|
||||
});
|
||||
}
|
||||
};
|
18
apps/client/src/routes/destinations/[id]/+page.svelte
Normal file
18
apps/client/src/routes/destinations/[id]/+page.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { LayoutData } from './$types';
|
||||
|
||||
export let data: LayoutData;
|
||||
let destination = data.destination.destination;
|
||||
let settings = data.destination.settings;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import New from './components/New.svelte';
|
||||
import Destination from './components/Destination.svelte';
|
||||
const { id } = $page.params;
|
||||
</script>
|
||||
|
||||
{#if id === 'new'}
|
||||
<New />
|
||||
{:else}
|
||||
<Destination bind:destination bind:settings />
|
||||
{/if}
|
@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
export let destination: any;
|
||||
export let settings: any;
|
||||
import LocalDocker from './LocalDocker.svelte';
|
||||
import RemoteDocker from './RemoteDocker.svelte';
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-6xl px-6">
|
||||
{#if destination.remoteEngine}
|
||||
<RemoteDocker bind:destination {settings} />
|
||||
{:else}
|
||||
<LocalDocker bind:destination {settings} />
|
||||
{/if}
|
||||
</div>
|
@ -0,0 +1,212 @@
|
||||
<script lang="ts">
|
||||
export let destination: any;
|
||||
export let settings: any;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { addToast, appSession, trpc } from '$lib/store';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
|
||||
let loading = {
|
||||
restart: false,
|
||||
proxy: false,
|
||||
save: false
|
||||
};
|
||||
|
||||
async function handleSubmit() {
|
||||
loading.save = true;
|
||||
try {
|
||||
await trpc.destinations.save.mutate({ ...destination });
|
||||
addToast({
|
||||
message: 'Configuration saved.',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.save = false;
|
||||
}
|
||||
}
|
||||
onMount(async () => {
|
||||
loading.proxy = true;
|
||||
const { isRunning } = await trpc.destinations.status.query({ id });
|
||||
let proxyUsed = !destination.isCoolifyProxyUsed;
|
||||
if (isRunning === false && destination.isCoolifyProxyUsed === true) {
|
||||
try {
|
||||
await trpc.destinations.saveSettings.mutate({
|
||||
id,
|
||||
isCoolifyProxyUsed: proxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
|
||||
await stopProxy();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
} else if (isRunning === true && destination.isCoolifyProxyUsed === false) {
|
||||
try {
|
||||
await trpc.destinations.saveSettings.mutate({
|
||||
id,
|
||||
isCoolifyProxyUsed: proxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
await startProxy();
|
||||
destination.isCoolifyProxyUsed = proxyUsed;
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.proxy = false;
|
||||
}
|
||||
}
|
||||
loading.proxy = false;
|
||||
});
|
||||
async function changeProxySetting() {
|
||||
if (!cannotDisable) {
|
||||
const isProxyActivated = destination.isCoolifyProxyUsed;
|
||||
if (isProxyActivated) {
|
||||
const sure = confirm(
|
||||
`Are you sure you want to ${
|
||||
destination.isCoolifyProxyUsed ? 'disable' : 'enable'
|
||||
} Coolify proxy? It will remove the proxy for all configured networks and all deployments on '${
|
||||
destination.engine
|
||||
}'! Nothing will be reachable if you do it!`
|
||||
);
|
||||
if (!sure) return;
|
||||
}
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
try {
|
||||
loading.proxy = true;
|
||||
await trpc.destinations.saveSettings.mutate({
|
||||
id,
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
|
||||
if (isProxyActivated) {
|
||||
await stopProxy();
|
||||
} else {
|
||||
await startProxy();
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.proxy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function stopProxy() {
|
||||
try {
|
||||
await trpc.destinations.stopProxy.mutate({ id });
|
||||
return addToast({
|
||||
message: 'Coolify proxy stopped.',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function startProxy() {
|
||||
try {
|
||||
await trpc.destinations.startProxy.mutate({ id });
|
||||
return addToast({
|
||||
message: ' Coolify proxy started.',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function forceRestartProxy() {
|
||||
const sure = confirm(
|
||||
"Are you sure you want to restart the proxy? It will remove the proxy for all configured networks and all deployments on '" +
|
||||
destination.engine +
|
||||
"'! Nothing will be reachable if you do it!"
|
||||
);
|
||||
if (sure) {
|
||||
try {
|
||||
loading.restart = true;
|
||||
addToast({
|
||||
message: 'Restarting proxy...',
|
||||
type: 'success'
|
||||
});
|
||||
await trpc.destinations.restartProxy.mutate({
|
||||
id
|
||||
});
|
||||
} catch (error) {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 5000);
|
||||
} finally {
|
||||
loading.restart = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={handleSubmit} class="py-4">
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-sm"
|
||||
class:bg-destinations={!loading.save}
|
||||
class:loading={loading.save}
|
||||
disabled={loading.save}
|
||||
>Save
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
class:loading={loading.restart}
|
||||
class:bg-error={!loading.restart}
|
||||
disabled={loading.restart}
|
||||
on:click|preventDefault={forceRestartProxy}>Force restart proxy</button
|
||||
>
|
||||
</div>
|
||||
<div class="grid gap-2 grid-cols-2 auto-rows-max mt-10 items-center">
|
||||
<label for="name">Name</label>
|
||||
<input
|
||||
class="w-full"
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
disabled={!$appSession.isAdmin}
|
||||
readonly={!$appSession.isAdmin}
|
||||
bind:value={destination.name}
|
||||
/>
|
||||
<label for="engine">Engine</label>
|
||||
<CopyPasswordField
|
||||
id="engine"
|
||||
readonly
|
||||
disabled
|
||||
name="engine"
|
||||
placeholder="Example: /var/run/docker.sock"
|
||||
value={destination.engine}
|
||||
/>
|
||||
<label for="network">Netwokr</label>
|
||||
<CopyPasswordField
|
||||
id="network"
|
||||
readonly
|
||||
disabled
|
||||
name="network"
|
||||
placeholder="Default: coolify"
|
||||
value={destination.network}
|
||||
/>
|
||||
{#if $appSession.teamId === '0'}
|
||||
<Setting
|
||||
id="changeProxySetting"
|
||||
loading={loading.proxy}
|
||||
disabled={cannotDisable}
|
||||
bind:setting={destination.isCoolifyProxyUsed}
|
||||
on:click={changeProxySetting}
|
||||
title="Use Coolify Proxy?"
|
||||
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.${
|
||||
cannotDisable
|
||||
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
|
||||
: ''
|
||||
}`}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import cuid from 'cuid';
|
||||
import NewLocalDocker from './NewLocalDocker.svelte';
|
||||
import NewRemoteDocker from './NewRemoteDocker.svelte';
|
||||
let payload = {};
|
||||
let selected = 'localDocker';
|
||||
function setPredefined(type: any) {
|
||||
selected = type;
|
||||
switch (type) {
|
||||
case 'localDocker':
|
||||
payload = {
|
||||
name: 'Local Docker',
|
||||
engine: '/var/run/docker.sock',
|
||||
remoteEngine: false,
|
||||
network: cuid(),
|
||||
isCoolifyProxyUsed: true
|
||||
};
|
||||
break;
|
||||
case 'remoteDocker':
|
||||
payload = {
|
||||
name: 'Remote Docker',
|
||||
remoteEngine: true,
|
||||
remoteIpAddress: null,
|
||||
remoteUser: 'root',
|
||||
remotePort: 22,
|
||||
network: cuid(),
|
||||
isCoolifyProxyUsed: true
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Add New Destination</div>
|
||||
</div>
|
||||
<div class="flex-col space-y-2 pb-10 text-center">
|
||||
<div class="text-xl font-bold text-white">Predefined destinations</div>
|
||||
<div class="flex justify-center space-x-2">
|
||||
<button class="btn btn-sm" on:click={() => setPredefined('localDocker')}>Local Docker</button>
|
||||
<button class="btn btn-sm" on:click={() => setPredefined('remoteDocker')}>Remote Docker</button>
|
||||
<!-- <button class="w-32" on:click={() => setPredefined('kubernetes')}>Kubernetes</button> -->
|
||||
</div>
|
||||
</div>
|
||||
{#if selected === 'localDocker'}
|
||||
<NewLocalDocker {payload} />
|
||||
{:else if selected === 'remoteDocker'}
|
||||
<NewRemoteDocker {payload} />
|
||||
{:else}
|
||||
<div class="text-center font-bold text-4xl py-10">Not implemented yet</div>
|
||||
{/if}
|
@ -0,0 +1,72 @@
|
||||
<script lang="ts">
|
||||
export let payload: any;
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import { appSession, trpc } from '$lib/store';
|
||||
|
||||
const from = $page.url.searchParams.get('from');
|
||||
let loading = false;
|
||||
|
||||
async function handleSubmit() {
|
||||
if (loading) return;
|
||||
try {
|
||||
loading = true;
|
||||
await trpc.destinations.check.query({ network: payload.network });
|
||||
const { id } = await trpc.destinations.save.mutate({ id: 'new', ...payload });
|
||||
return await goto(from || `/destinations/${id}`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center px-6 pb-8">
|
||||
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
|
||||
<div
|
||||
class="flex items-start lg:items-center space-x-0 lg:space-x-4 pb-5 flex-col lg:flex-row space-y-4 lg:space-y-0"
|
||||
>
|
||||
<div class="title font-bold">Configuration</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-sm bg-destinations w-full lg:w-fit"
|
||||
class:loading
|
||||
disabled={loading}
|
||||
>{loading ? (payload.isCoolifyProxyUsed ? 'Saving...' : 'Saving...') : 'Save'}</button
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2 grid grid-cols-2 items-center lg:pl-10">
|
||||
<label for="name" class="text-base font-bold text-stone-100">Name</label>
|
||||
<input required name="name" placeholder="Name" bind:value={payload.name} />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center lg:pl-10">
|
||||
<label for="engine" class="text-base font-bold text-stone-100">Engine</label>
|
||||
<input
|
||||
required
|
||||
name="engine"
|
||||
placeholder="Example: /var/run/docker.sock"
|
||||
bind:value={payload.engine}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center lg:pl-10">
|
||||
<label for="network" class="text-base font-bold text-stone-100">Network</label>
|
||||
<input required name="network" placeholder="Default: coolify" bind:value={payload.network} />
|
||||
</div>
|
||||
{#if $appSession.teamId === '0'}
|
||||
<div class="grid grid-cols-2 items-center lg:pl-10">
|
||||
<Setting
|
||||
id="changeProxySetting"
|
||||
bind:setting={payload.isCoolifyProxyUsed}
|
||||
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
|
||||
title="Use Coolify Proxy?"
|
||||
description={'This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.'}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
@ -0,0 +1,104 @@
|
||||
<script lang="ts">
|
||||
export let payload: any;
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import { trpc } from '$lib/store';
|
||||
|
||||
const from = $page.url.searchParams.get('from');
|
||||
let loading = false;
|
||||
|
||||
async function handleSubmit() {
|
||||
if (loading) return;
|
||||
try {
|
||||
loading = true;
|
||||
await trpc.destinations.check.query({ network: payload.network });
|
||||
const { id } = await trpc.destinations.save.mutate({ id: 'new', ...payload });
|
||||
return await goto(from || `/destinations/${id}`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="text-center flex justify-center">
|
||||
<SimpleExplainer
|
||||
customClass="max-w-[32rem]"
|
||||
text="Remote Docker Engines are using <span class='text-white font-bold'>SSH</span> to communicate with the remote docker engine.
|
||||
You need to setup an <span class='text-white font-bold'>SSH key</span> in advance on the server and install Docker.
|
||||
<br>See <a class='text-white' href='https://docs.coollabs.io/coolify/destinations#remote-docker-engine' target='blank'>docs</a> for more details."
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-center px-6 pb-8">
|
||||
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
|
||||
<div class="flex items-start lg:items-center space-x-0 lg:space-x-4 pb-5 flex-col lg:flex-row space-y-4 lg:space-y-0">
|
||||
<div class="title font-bold">Configuration</div>
|
||||
<button type="submit" class="btn btn-sm bg-destinations w-full lg:w-fit" class:loading disabled={loading}
|
||||
>{loading
|
||||
? payload.isCoolifyProxyUsed
|
||||
? 'Saving...'
|
||||
: 'Saving...'
|
||||
: "Save"}</button
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2 grid grid-cols-2 items-center lg:pl-10">
|
||||
<label for="name" class="text-base font-bold text-stone-100">Name</label>
|
||||
<input required name="name" placeholder="Name" bind:value={payload.name} />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center lg:pl-10">
|
||||
<label for="remoteIpAddress" class="text-base font-bold text-stone-100"
|
||||
>IP Address</label
|
||||
>
|
||||
<input
|
||||
required
|
||||
name="remoteIpAddress"
|
||||
placeholder="Example: 192.168..."
|
||||
bind:value={payload.remoteIpAddress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center lg:pl-10">
|
||||
<label for="remoteUser" class="text-base font-bold text-stone-100">User</label>
|
||||
<input
|
||||
required
|
||||
name="remoteUser"
|
||||
placeholder="Example: root"
|
||||
bind:value={payload.remoteUser}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center lg:pl-10">
|
||||
<label for="remotePort" class="text-base font-bold text-stone-100">Port</label>
|
||||
<input
|
||||
required
|
||||
name="remotePort"
|
||||
placeholder="Example: 22"
|
||||
bind:value={payload.remotePort}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center lg:pl-10">
|
||||
<label for="network" class="text-base font-bold text-stone-100">Network</label>
|
||||
<input
|
||||
required
|
||||
name="network"
|
||||
placeholder="Default: coolify"
|
||||
bind:value={payload.network}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center lg:pl-10">
|
||||
<Setting
|
||||
id="isCoolifyProxyUsed"
|
||||
bind:setting={payload.isCoolifyProxyUsed}
|
||||
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
|
||||
title="Use Coolify Proxy?"
|
||||
description={'This will install a proxy on the destination to allow you to access your applications and services without any manual configuration.'}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
@ -0,0 +1,279 @@
|
||||
<script lang="ts">
|
||||
export let destination: any;
|
||||
export let settings: any;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import { get, post } from '$lib/api';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { addToast, appSession } from '$lib/store';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
|
||||
let loading = {
|
||||
restart: false,
|
||||
proxy: true,
|
||||
save: false,
|
||||
verify: false
|
||||
};
|
||||
|
||||
$: isDisabled = !$appSession.isAdmin;
|
||||
|
||||
async function handleSubmit() {
|
||||
loading.save = true;
|
||||
try {
|
||||
await post(`/destinations/${id}`, { ...destination });
|
||||
addToast({
|
||||
message: 'Configuration saved.',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.save = false;
|
||||
}
|
||||
}
|
||||
onMount(async () => {
|
||||
if (destination.remoteEngine && destination.remoteVerified) {
|
||||
loading.proxy = true;
|
||||
const { isRunning } = await get(`/destinations/${id}/status`);
|
||||
if (isRunning === false && destination.isCoolifyProxyUsed === true) {
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
try {
|
||||
await post(`/destinations/${id}/settings`, {
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
await stopProxy();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
} else if (isRunning === true && destination.isCoolifyProxyUsed === false) {
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
try {
|
||||
await post(`/destinations/${id}/settings`, {
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
await startProxy();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loading.proxy = false;
|
||||
});
|
||||
async function changeProxySetting() {
|
||||
if (!destination.remoteVerified) return;
|
||||
loading.proxy = true;
|
||||
if (!cannotDisable) {
|
||||
const isProxyActivated = destination.isCoolifyProxyUsed;
|
||||
if (isProxyActivated) {
|
||||
const sure = confirm(
|
||||
`Are you sure you want to ${
|
||||
destination.isCoolifyProxyUsed ? 'disable' : 'enable'
|
||||
} Coolify proxy? It will remove the proxy for all configured networks and all deployments! Nothing will be reachable if you do it!`
|
||||
);
|
||||
if (!sure) {
|
||||
loading.proxy = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
let proxyUsed = !destination.isCoolifyProxyUsed;
|
||||
try {
|
||||
await post(`/destinations/${id}/settings`, {
|
||||
isCoolifyProxyUsed: proxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
if (isProxyActivated) {
|
||||
await stopProxy();
|
||||
} else {
|
||||
await startProxy();
|
||||
}
|
||||
destination.isCoolifyProxyUsed = proxyUsed;
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.proxy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function stopProxy() {
|
||||
try {
|
||||
await post(`/destinations/${id}/stop`, { engine: destination.engine });
|
||||
return addToast({
|
||||
message: $t('destination.coolify_proxy_stopped'),
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function startProxy() {
|
||||
try {
|
||||
await post(`/destinations/${id}/start`, { engine: destination.engine });
|
||||
return addToast({
|
||||
message: $t('destination.coolify_proxy_started'),
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function forceRestartProxy() {
|
||||
const sure = confirm($t('destination.confirm_restart_proxy'));
|
||||
if (sure) {
|
||||
try {
|
||||
loading.restart = true;
|
||||
addToast({
|
||||
message: $t('destination.coolify_proxy_restarting'),
|
||||
type: 'success'
|
||||
});
|
||||
await post(`/destinations/${id}/restart`, {
|
||||
engine: destination.engine,
|
||||
fqdn: settings.fqdn
|
||||
});
|
||||
} catch (error) {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 5000);
|
||||
} finally {
|
||||
loading.restart = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function verifyRemoteDocker() {
|
||||
try {
|
||||
loading.verify = true;
|
||||
await post(`/destinations/${id}/verify`, {});
|
||||
destination.remoteVerified = true;
|
||||
return addToast({
|
||||
message: 'Remote Docker Engine verified!',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.verify = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
|
||||
<div class="flex space-x-1 pb-5">
|
||||
{#if $appSession.isAdmin}
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-sm"
|
||||
class:loading={loading.save}
|
||||
class:bg-destinations={!loading.save}
|
||||
disabled={loading.save}
|
||||
>{$t('forms.save')}
|
||||
</button>
|
||||
<button
|
||||
disabled={loading.verify}
|
||||
class="btn btn-sm"
|
||||
class:loading={loading.verify}
|
||||
on:click|preventDefault|stopPropagation={verifyRemoteDocker}
|
||||
>{!destination.remoteVerified
|
||||
? 'Verify Remote Docker Engine'
|
||||
: 'Check Remote Docker Engine'}</button
|
||||
>
|
||||
{#if destination.remoteVerified}
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
class:loading={loading.restart}
|
||||
class:bg-error={!loading.restart}
|
||||
disabled={loading.restart}
|
||||
on:click|preventDefault={forceRestartProxy}
|
||||
>{$t('destination.force_restart_proxy')}</button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10 ">
|
||||
<label for="name">{$t('forms.name')}</label>
|
||||
<input
|
||||
name="name"
|
||||
class="w-full"
|
||||
placeholder={$t('forms.name')}
|
||||
disabled={!$appSession.isAdmin}
|
||||
readonly={!$appSession.isAdmin}
|
||||
bind:value={destination.name}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="network">{$t('forms.network')}</label>
|
||||
<CopyPasswordField
|
||||
id="network"
|
||||
readonly
|
||||
disabled
|
||||
name="network"
|
||||
placeholder="{$t('forms.default')}: coolify"
|
||||
value={destination.network}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="remoteIpAddress">IP Address</label>
|
||||
<CopyPasswordField
|
||||
id="remoteIpAddress"
|
||||
readonly
|
||||
disabled
|
||||
name="remoteIpAddress"
|
||||
value={destination.remoteIpAddress}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="remoteUser">User</label>
|
||||
<CopyPasswordField
|
||||
id="remoteUser"
|
||||
readonly
|
||||
disabled
|
||||
name="remoteUser"
|
||||
value={destination.remoteUser}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="remotePort">Port</label>
|
||||
<CopyPasswordField
|
||||
id="remotePort"
|
||||
readonly
|
||||
disabled
|
||||
name="remotePort"
|
||||
value={destination.remotePort}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="sshKey">SSH Key</label>
|
||||
<a
|
||||
href={!isDisabled ? `/destinations/${id}/configuration/sshkey?from=/destinations/${id}` : ''}
|
||||
class="no-underline"
|
||||
><input
|
||||
value={destination.sshKey.name}
|
||||
readonly
|
||||
id="sshKey"
|
||||
class="cursor-pointer w-full"
|
||||
/></a
|
||||
>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<Setting
|
||||
id="changeProxySetting"
|
||||
disabled={cannotDisable || !destination.remoteVerified}
|
||||
loading={loading.proxy}
|
||||
bind:setting={destination.isCoolifyProxyUsed}
|
||||
on:click={changeProxySetting}
|
||||
title={$t('destination.use_coolify_proxy')}
|
||||
description={`Install & configure a proxy (based on Traefik) on the destination to allow you to access your applications and services without any manual configuration.${
|
||||
cannotDisable
|
||||
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
|
||||
: ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
@ -823,3 +823,116 @@ export async function startTraefikTCPProxy(
|
||||
});
|
||||
}
|
||||
}
|
||||
export async function startTraefikProxy(id: string): Promise<void> {
|
||||
const { engine, network, remoteEngine, remoteIpAddress } =
|
||||
await prisma.destinationDocker.findUnique({ where: { id } });
|
||||
const { found } = await checkContainer({
|
||||
dockerId: id,
|
||||
container: 'coolify-proxy',
|
||||
remove: true
|
||||
});
|
||||
const { id: settingsId, ipv4, ipv6 } = await listSettings();
|
||||
|
||||
if (!found) {
|
||||
const { stdout: coolifyNetwork } = await executeCommand({
|
||||
dockerId: id,
|
||||
command: `docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"`
|
||||
});
|
||||
|
||||
if (!coolifyNetwork) {
|
||||
await executeCommand({
|
||||
dockerId: id,
|
||||
command: `docker network create --attachable coolify-infra`
|
||||
});
|
||||
}
|
||||
const { stdout: Config } = await executeCommand({
|
||||
dockerId: id,
|
||||
command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'`
|
||||
});
|
||||
const ip = JSON.parse(Config)[0].Gateway;
|
||||
let traefikUrl = mainTraefikEndpoint;
|
||||
if (remoteEngine) {
|
||||
let ip = null;
|
||||
if (isDev) {
|
||||
ip = getAPIUrl();
|
||||
} else {
|
||||
ip = `http://${ipv4 || ipv6}:3000`;
|
||||
}
|
||||
traefikUrl = `${ip}/webhooks/traefik/remote/${id}`;
|
||||
}
|
||||
await executeCommand({
|
||||
dockerId: id,
|
||||
command: `docker run --restart always \
|
||||
--add-host 'host.docker.internal:host-gateway' \
|
||||
${ip ? `--add-host 'host.docker.internal:${ip}'` : ''} \
|
||||
-v coolify-traefik-letsencrypt:/etc/traefik/acme \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--network coolify-infra \
|
||||
-p "80:80" \
|
||||
-p "443:443" \
|
||||
--name coolify-proxy \
|
||||
-d ${defaultTraefikImage} \
|
||||
--entrypoints.web.address=:80 \
|
||||
--entrypoints.web.forwardedHeaders.insecure=true \
|
||||
--entrypoints.websecure.address=:443 \
|
||||
--entrypoints.websecure.forwardedHeaders.insecure=true \
|
||||
--providers.docker=true \
|
||||
--providers.docker.exposedbydefault=false \
|
||||
--providers.http.endpoint=${traefikUrl} \
|
||||
--providers.http.pollTimeout=5s \
|
||||
--certificatesresolvers.letsencrypt.acme.httpchallenge=true \
|
||||
--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json \
|
||||
--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web \
|
||||
--log.level=error`
|
||||
});
|
||||
await prisma.destinationDocker.update({
|
||||
where: { id },
|
||||
data: { isCoolifyProxyUsed: true }
|
||||
});
|
||||
}
|
||||
// Configure networks for local docker engine
|
||||
if (engine) {
|
||||
const destinations = await prisma.destinationDocker.findMany({ where: { engine } });
|
||||
for (const destination of destinations) {
|
||||
await configureNetworkTraefikProxy(destination);
|
||||
}
|
||||
}
|
||||
// Configure networks for remote docker engine
|
||||
if (remoteEngine) {
|
||||
const destinations = await prisma.destinationDocker.findMany({ where: { remoteIpAddress } });
|
||||
for (const destination of destinations) {
|
||||
await configureNetworkTraefikProxy(destination);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function configureNetworkTraefikProxy(destination: any): Promise<void> {
|
||||
const { id } = destination;
|
||||
const { stdout: networks } = await executeCommand({
|
||||
dockerId: id,
|
||||
command: `docker ps -a --filter name=coolify-proxy --format '{{json .Networks}}'`
|
||||
});
|
||||
const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(',');
|
||||
if (!configuredNetworks.includes(destination.network)) {
|
||||
await executeCommand({
|
||||
dockerId: destination.id,
|
||||
command: `docker network connect ${destination.network} coolify-proxy`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopTraefikProxy(id: string): Promise<{ stdout: string; stderr: string }> {
|
||||
const { found } = await checkContainer({ dockerId: id, container: 'coolify-proxy' });
|
||||
await prisma.destinationDocker.update({
|
||||
where: { id },
|
||||
data: { isCoolifyProxyUsed: false }
|
||||
});
|
||||
if (found) {
|
||||
return await executeCommand({
|
||||
dockerId: id,
|
||||
command: `docker stop -t 0 coolify-proxy && docker rm coolify-proxy`,
|
||||
shell: true
|
||||
});
|
||||
}
|
||||
return { stdout: '', stderr: '' };
|
||||
}
|
||||
|
@ -8,7 +8,8 @@ import {
|
||||
applicationsRouter,
|
||||
servicesRouter,
|
||||
databasesRouter,
|
||||
sourcesRouter
|
||||
sourcesRouter,
|
||||
destinationsRouter
|
||||
} from './routers';
|
||||
|
||||
export const appRouter = router({
|
||||
@ -18,7 +19,8 @@ export const appRouter = router({
|
||||
applications: applicationsRouter,
|
||||
services: servicesRouter,
|
||||
databases: databasesRouter,
|
||||
sources: sourcesRouter
|
||||
sources: sourcesRouter,
|
||||
destinations: destinationsRouter
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
218
apps/server/src/trpc/routers/destinations/index.ts
Normal file
218
apps/server/src/trpc/routers/destinations/index.ts
Normal file
@ -0,0 +1,218 @@
|
||||
import { z } from 'zod';
|
||||
import { privateProcedure, router } from '../../trpc';
|
||||
import {
|
||||
listSettings,
|
||||
startTraefikProxy,
|
||||
startTraefikTCPProxy,
|
||||
stopTraefikProxy
|
||||
} from '../../../lib/common';
|
||||
import { prisma } from '../../../prisma';
|
||||
|
||||
import { executeCommand } from '../../../lib/executeCommand';
|
||||
import { checkContainer } from '../../../lib/docker';
|
||||
|
||||
export const destinationsRouter = router({
|
||||
restartProxy: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
await stopTraefikProxy(id);
|
||||
await startTraefikProxy(id);
|
||||
await prisma.destinationDocker.update({
|
||||
where: { id },
|
||||
data: { isCoolifyProxyUsed: true }
|
||||
});
|
||||
}),
|
||||
startProxy: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
await startTraefikProxy(id);
|
||||
}),
|
||||
stopProxy: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
await stopTraefikProxy(id);
|
||||
}),
|
||||
saveSettings: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
engine: z.string(),
|
||||
isCoolifyProxyUsed: z.boolean()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id, engine, isCoolifyProxyUsed } = input;
|
||||
await prisma.destinationDocker.updateMany({
|
||||
where: { engine },
|
||||
data: { isCoolifyProxyUsed }
|
||||
});
|
||||
}),
|
||||
status: privateProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
const destination = await prisma.destinationDocker.findUnique({ where: { id } });
|
||||
const { found: isRunning } = await checkContainer({
|
||||
dockerId: destination.id,
|
||||
container: 'coolify-proxy',
|
||||
remove: true
|
||||
});
|
||||
return {
|
||||
isRunning
|
||||
};
|
||||
}),
|
||||
save: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
htmlUrl: z.string(),
|
||||
apiUrl: z.string(),
|
||||
customPort: z.number(),
|
||||
customUser: z.string(),
|
||||
isSystemWide: z.boolean().default(false)
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId } = ctx.user;
|
||||
let {
|
||||
id,
|
||||
name,
|
||||
network,
|
||||
engine,
|
||||
isCoolifyProxyUsed,
|
||||
remoteIpAddress,
|
||||
remoteUser,
|
||||
remotePort
|
||||
} = input;
|
||||
if (id === 'new') {
|
||||
if (engine) {
|
||||
const { stdout } = await await executeCommand({
|
||||
command: `docker network ls --filter 'name=^${network}$' --format '{{json .}}'`
|
||||
});
|
||||
if (stdout === '') {
|
||||
await await executeCommand({
|
||||
command: `docker network create --attachable ${network}`
|
||||
});
|
||||
}
|
||||
await prisma.destinationDocker.create({
|
||||
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed }
|
||||
});
|
||||
const destinations = await prisma.destinationDocker.findMany({ where: { engine } });
|
||||
const destination = destinations.find((destination) => destination.network === network);
|
||||
if (destinations.length > 0) {
|
||||
const proxyConfigured = destinations.find(
|
||||
(destination) =>
|
||||
destination.network !== network && destination.isCoolifyProxyUsed === true
|
||||
);
|
||||
if (proxyConfigured) {
|
||||
isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed;
|
||||
}
|
||||
await prisma.destinationDocker.updateMany({
|
||||
where: { engine },
|
||||
data: { isCoolifyProxyUsed }
|
||||
});
|
||||
}
|
||||
if (isCoolifyProxyUsed) {
|
||||
await startTraefikProxy(destination.id);
|
||||
}
|
||||
return { id: destination.id };
|
||||
} else {
|
||||
const destination = await prisma.destinationDocker.create({
|
||||
data: {
|
||||
name,
|
||||
teams: { connect: { id: teamId } },
|
||||
engine,
|
||||
network,
|
||||
isCoolifyProxyUsed,
|
||||
remoteEngine: true,
|
||||
remoteIpAddress,
|
||||
remoteUser,
|
||||
remotePort: Number(remotePort)
|
||||
}
|
||||
});
|
||||
return { id: destination.id };
|
||||
}
|
||||
} else {
|
||||
await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } });
|
||||
return {};
|
||||
}
|
||||
}),
|
||||
check: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
network: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { network } = input;
|
||||
const found = await prisma.destinationDocker.findFirst({ where: { network } });
|
||||
if (found) {
|
||||
throw {
|
||||
message: `Network already exists: ${network}`
|
||||
};
|
||||
}
|
||||
}),
|
||||
delete: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
const { network, remoteVerified, engine, isCoolifyProxyUsed } =
|
||||
await prisma.destinationDocker.findUnique({ where: { id } });
|
||||
if (isCoolifyProxyUsed) {
|
||||
if (engine || remoteVerified) {
|
||||
const { stdout: found } = await executeCommand({
|
||||
dockerId: id,
|
||||
command: `docker ps -a --filter network=${network} --filter name=coolify-proxy --format '{{.}}'`
|
||||
});
|
||||
if (found) {
|
||||
await executeCommand({
|
||||
dockerId: id,
|
||||
command: `docker network disconnect ${network} coolify-proxy`
|
||||
});
|
||||
await executeCommand({ dockerId: id, command: `docker network rm ${network}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
await prisma.destinationDocker.delete({ where: { id } });
|
||||
}),
|
||||
getDestinationById: privateProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string()
|
||||
})
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
const { teamId } = ctx.user;
|
||||
const destination = await prisma.destinationDocker.findFirst({
|
||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { sshKey: true, application: true, service: true, database: true }
|
||||
});
|
||||
if (!destination && id !== 'new') {
|
||||
throw { status: 404, message: `Destination not found.` };
|
||||
}
|
||||
const settings = await listSettings();
|
||||
return {
|
||||
destination,
|
||||
settings
|
||||
};
|
||||
})
|
||||
});
|
@ -5,3 +5,4 @@ export * from './applications';
|
||||
export * from './services';
|
||||
export * from './databases';
|
||||
export * from './sources';
|
||||
export * from './destinations';
|
||||
|
Loading…
x
Reference in New Issue
Block a user