| @@ -1,15 +1,29 @@ | ||||
| <script context="module"> | ||||
| 	/** @type {import('@sveltejs/kit').ErrorLoad} */ | ||||
| 	export function load({ error, status }) { | ||||
| 		console.log(error); | ||||
| 		return { | ||||
| 			props: { | ||||
| 				error: `${status}: ${error.message}` | ||||
| 				error, | ||||
| 				status | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <script> | ||||
| 	export let status; | ||||
| 	export let error; | ||||
| </script> | ||||
|  | ||||
| <h1 class="text-xl font-bold">{error}</h1> | ||||
| <div class="mx-auto flex h-screen flex-col items-center justify-center px-4"> | ||||
| 	<div class="pb-10 text-7xl font-bold">{status}</div> | ||||
| 	<div class="text-3xl font-bold">Ooops you are lost! But don't be afraid!</div> | ||||
| 	<div class="text-xl"> | ||||
| 		You can find your way back <a href="/" class="font-bold uppercase text-sky-400">here</a> | ||||
| 	</div> | ||||
| 	<div class="py-10 text-xs font-bold"> | ||||
| 		<pre | ||||
| 			class="w-full whitespace-pre-wrap break-words text-left text-xs tracking-tighter">{error.message} {error.stack}</pre> | ||||
| 	</div> | ||||
| </div> | ||||
|   | ||||
| @@ -1,408 +1,568 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import { publicPages } from '$lib/consts'; | ||||
| 	import { request } from '$lib/request'; | ||||
| 	/** | ||||
| 	 * @type {import('@sveltejs/kit').Load} | ||||
| 	 */ | ||||
| 	export async function load(session) { | ||||
| 		const { path } = session.page; | ||||
| 		if (!publicPages.includes(path)) { | ||||
| 			if (!session.session.isLoggedIn) { | ||||
| 				return { | ||||
| 					status: 302, | ||||
| 					redirect: '/' | ||||
| 				}; | ||||
| 			} | ||||
| 			return {}; | ||||
| 		} | ||||
| 		if (!publicPages.includes(path)) { | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	import { publicPaths } from '$lib/settings'; | ||||
|  | ||||
| 	export const load: Load = async ({ fetch, url, params, session }) => { | ||||
| 		if (!session.uid && !publicPaths.includes(url.pathname)) { | ||||
| 			return { | ||||
| 				status: 302, | ||||
| 				redirect: '/' | ||||
| 				redirect: '/login' | ||||
| 			}; | ||||
| 		} | ||||
| 		if (!session.uid) { | ||||
| 			return {}; | ||||
| 		} | ||||
| 		const endpoint = `/teams.json`; | ||||
| 		const res = await fetch(endpoint); | ||||
|  | ||||
| 		if (res.ok) { | ||||
| 			return { | ||||
| 				props: { | ||||
| 					selectedTeamId: session.teamId, | ||||
| 					...(await res.json()) | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 		return {}; | ||||
| 	} | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	import 'microtip/microtip.css'; | ||||
| 	import '../app.postcss'; | ||||
| 	export let initDashboard; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 	import { SvelteToast } from '@zerodevx/svelte-toast'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| <script> | ||||
| 	export let teams; | ||||
| 	export let selectedTeamId; | ||||
| 	import { fade } from 'svelte/transition'; | ||||
|  | ||||
| 	import '../tailwind.css'; | ||||
| 	import { SvelteToast, toast } from '@zerodevx/svelte-toast'; | ||||
| 	import { page, session } from '$app/stores'; | ||||
| 	import { toast } from '@zerodevx/svelte-toast'; | ||||
| 	import Tooltip from '$components/Tooltip.svelte'; | ||||
| 	import compareVersions from 'compare-versions'; | ||||
| 	import packageJson from '../../package.json'; | ||||
| 	import { dashboard, settings } from '$store'; | ||||
| 	import { browser } from '$app/env'; | ||||
| 	$settings.clientId = import.meta.env.VITE_GITHUB_APP_CLIENTID !== 'null' ? import.meta.env.VITE_GITHUB_APP_CLIENTID : null | ||||
| 	$dashboard = initDashboard; | ||||
| 	const branch = | ||||
| 		process.env.NODE_ENV === 'production' && | ||||
| 		browser && | ||||
| 		window.location.hostname !== 'test.andrasbacsai.dev' | ||||
| 			? 'main' | ||||
| 			: 'next'; | ||||
| 	let latest = { | ||||
| 		coolify: {} | ||||
| 	}; | ||||
| 	let upgradeAvailable = false; | ||||
| 	let upgradeDisabled = false; | ||||
| 	let upgradeDone = false; | ||||
| 	let showAck = false; | ||||
| 	let globalFeatureFlag = browser && localStorage.getItem('globalFeatureFlag'); | ||||
| 	const options = { | ||||
| 		duration: 2000 | ||||
| 	import { onMount } from 'svelte'; | ||||
| 	import { errorNotification } from '$lib/form'; | ||||
| 	import { asyncSleep } from '$lib/components/common'; | ||||
| 	import { del, get, post } from '$lib/api'; | ||||
| 	import { dev } from '$app/env'; | ||||
| 	import Loading from '$lib/components/Loading.svelte'; | ||||
|  | ||||
| 	let isUpdateAvailable = false; | ||||
| 	let updateStatus = { | ||||
| 		loading: false, | ||||
| 		checking: false, | ||||
| 		success: null | ||||
| 	}; | ||||
|  | ||||
| 	onMount(async () => { | ||||
| 		upgradeAvailable = await checkUpgrade(); | ||||
| 		browser && localStorage.removeItem('token'); | ||||
| 		if (!localStorage.getItem('automaticErrorReportsAck')) { | ||||
| 			showAck = true; | ||||
| 			if (latest?.coolify[branch]?.settings?.sendErrors) { | ||||
| 				const settings = { | ||||
| 					sendErrors: true | ||||
| 				}; | ||||
| 				await request('/api/v1/settings', $session, { body: { ...settings } }); | ||||
| 		if ($session.uid) { | ||||
| 			try { | ||||
| 				await get(`/login.json`); | ||||
| 			} catch ({ error }) { | ||||
| 				await del(`/logout.json`, {}); | ||||
| 				window.location.reload(); | ||||
| 				return errorNotification(error); | ||||
| 			} | ||||
| 			if ($session.teamId === '0') { | ||||
| 				updateStatus.checking = true; | ||||
| 				try { | ||||
| 					const data = await get(`/update.json`); | ||||
| 					if (data?.isUpdateAvailable) { | ||||
| 						await post(`/update.json`, { type: 'pull' }); | ||||
| 					} | ||||
| 					isUpdateAvailable = data?.isUpdateAvailable; | ||||
| 				} catch (error) { | ||||
| 				} finally { | ||||
| 					updateStatus.checking = false; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| 	async function checkUpgrade() { | ||||
| 		latest = await fetch(`https://get.coollabs.io/version.json`, { | ||||
| 			cache: 'no-cache' | ||||
| 		}).then((r) => r.json()); | ||||
|  | ||||
| 		return compareVersions(latest.coolify[branch].version, packageJson.version) === 1 | ||||
| 			? true | ||||
| 			: false; | ||||
| 	} | ||||
| 	async function upgrade() { | ||||
| 	async function logout() { | ||||
| 		try { | ||||
| 			upgradeDisabled = true; | ||||
| 			await request('/api/v1/upgrade', $session); | ||||
| 			upgradeDone = true; | ||||
| 		} catch (error) { | ||||
| 			browser && | ||||
| 				toast.push( | ||||
| 					'Something happened during update. Ooops. Automatic error reporting will happen soon.' | ||||
| 				); | ||||
| 			await del(`/logout.json`, {}); | ||||
| 			return window.location.reload(); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| 	async function logout() { | ||||
| 		await request('/api/v1/logout', $session, { body: {}, method: 'DELETE' }); | ||||
| 		location.reload(); | ||||
| 	async function switchTeam() { | ||||
| 		try { | ||||
| 			await post(`/index.json?from=${$page.url.pathname}`, { | ||||
| 				cookie: 'teamId', | ||||
| 				value: selectedTeamId | ||||
| 			}); | ||||
| 			return window.location.reload(); | ||||
| 		} catch (error) { | ||||
| 			return window.location.reload(); | ||||
| 		} | ||||
| 	} | ||||
| 	function reloadInAMin() { | ||||
| 		setTimeout(() => { | ||||
| 			location.reload(); | ||||
| 		}, 30000); | ||||
| 	} | ||||
| 	function ackError() { | ||||
| 		localStorage.setItem('automaticErrorReportsAck', 'true'); | ||||
| 		showAck = false; | ||||
|  | ||||
| 	async function update() { | ||||
| 		updateStatus.loading = true; | ||||
| 		if (!dev) { | ||||
| 			try { | ||||
| 				await post(`/update.json`, { type: 'update' }); | ||||
| 				toast.push('Update completed. Waiting for the new version to start...'); | ||||
| 				let reachable = false; | ||||
| 				let tries = 0; | ||||
| 				do { | ||||
| 					await asyncSleep(4000); | ||||
| 					try { | ||||
| 						await get(`/undead.json`); | ||||
| 						reachable = true; | ||||
| 					} catch (error) { | ||||
| 						reachable = false; | ||||
| 					} | ||||
| 					if (reachable) break; | ||||
| 					tries++; | ||||
| 				} while (!reachable || tries < 120); | ||||
| 				toast.push('New version reachable. Reloading...'); | ||||
| 				updateStatus.loading = false; | ||||
| 				updateStatus.success = true; | ||||
| 				await asyncSleep(3000); | ||||
| 				return window.location.reload(); | ||||
| 			} catch ({ error }) { | ||||
| 				return errorNotification(error); | ||||
| 			} finally { | ||||
| 				updateStatus.success = false; | ||||
| 				updateStatus.loading = false; | ||||
| 			} | ||||
| 		} else { | ||||
| 			let reachable = false; | ||||
| 			let tries = 0; | ||||
| 			do { | ||||
| 				await asyncSleep(1000); | ||||
| 				try { | ||||
| 					await get(`/undead.json`); | ||||
| 					reachable = true; | ||||
| 				} catch (error) { | ||||
| 					console.log(error); | ||||
| 					reachable = false; | ||||
| 				} | ||||
| 				console.log(reachable); | ||||
| 				if (reachable) break; | ||||
| 				tries++; | ||||
| 			} while (!reachable || tries < 120); | ||||
| 			toast.push('New version reachable. Reloading...'); | ||||
| 			await asyncSleep(2000); | ||||
| 			window.location.reload(); | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <SvelteToast {options} /> | ||||
| <svelte:head> | ||||
| 	<title>Coolify</title> | ||||
| </svelte:head> | ||||
| <SvelteToast options={{ intro: { y: -64 }, duration: 3000, pausable: true }} /> | ||||
| {#if $session.uid} | ||||
| 	<nav class="nav-main"> | ||||
| 		<div class="flex h-screen w-full flex-col items-center transition-all duration-100"> | ||||
| 			<div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div> | ||||
| 			<div class="flex flex-col space-y-4 py-2"> | ||||
| 				<a | ||||
| 					sveltekit:prefetch | ||||
| 					href="/" | ||||
| 					class="icons tooltip-right bg-coolgray-200 hover:text-white" | ||||
| 					class:text-white={$page.url.pathname === '/'} | ||||
| 					class:bg-coolgray-500={$page.url.pathname === '/'} | ||||
| 					data-tooltip="Dashboard" | ||||
| 				> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						class="h-8 w-8" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke-width="1.5" | ||||
| 						stroke="currentColor" | ||||
| 						fill="none" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 					> | ||||
| 						<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 						<path | ||||
| 							d="M19 8.71l-5.333 -4.148a2.666 2.666 0 0 0 -3.274 0l-5.334 4.148a2.665 2.665 0 0 0 -1.029 2.105v7.2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-7.2c0 -.823 -.38 -1.6 -1.03 -2.105" | ||||
| 						/> | ||||
| 						<path d="M16 15c-2.21 1.333 -5.792 1.333 -8 0" /> | ||||
| 					</svg> | ||||
| 				</a> | ||||
| 				<div class="border-t border-stone-700" /> | ||||
|  | ||||
| {#if showAck && $page.path !== '/success' && $page.path !== '/'} | ||||
| 	<div class="p-2 fixed top-0 right-0 z-50 w-64 m-2 rounded border-gradient-full bg-black"> | ||||
| 		<div class="text-white text-xs space-y-2 text-justify font-medium"> | ||||
| 			<div>We implemented an automatic error reporting feature, which is enabled by default.</div> | ||||
| 			<div>Why? Because we would like to hunt down bugs faster and easier.</div> | ||||
| 			<div class="py-5"> | ||||
| 				If you do not like it, you can turn it off in the <button | ||||
| 					class="underline font-bold" | ||||
| 					on:click={() => goto('/settings')}>Settings menu</button | ||||
| 				>. | ||||
| 				<a | ||||
| 					sveltekit:prefetch | ||||
| 					href="/applications" | ||||
| 					class="icons tooltip-right bg-coolgray-200 hover:text-green-500" | ||||
| 					class:text-green-500={$page.url.pathname.startsWith('/applications') || | ||||
| 						$page.url.pathname.startsWith('/new/application')} | ||||
| 					class:bg-coolgray-500={$page.url.pathname.startsWith('/applications') || | ||||
| 						$page.url.pathname.startsWith('/new/application')} | ||||
| 					data-tooltip="Applications" | ||||
| 				> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						class="h-8 w-8" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke-width="1.5" | ||||
| 						stroke="currentcolor" | ||||
| 						fill="none" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 					> | ||||
| 						<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 						<rect x="4" y="4" width="6" height="6" rx="1" /> | ||||
| 						<rect x="4" y="14" width="6" height="6" rx="1" /> | ||||
| 						<rect x="14" y="14" width="6" height="6" rx="1" /> | ||||
| 						<line x1="14" y1="7" x2="20" y2="7" /> | ||||
| 						<line x1="17" y1="4" x2="17" y2="10" /> | ||||
| 					</svg> | ||||
| 				</a> | ||||
| 				<a | ||||
| 					sveltekit:prefetch | ||||
| 					href="/sources" | ||||
| 					class="icons tooltip-right bg-coolgray-200 hover:text-orange-500" | ||||
| 					class:text-orange-500={$page.url.pathname.startsWith('/sources') || | ||||
| 						$page.url.pathname.startsWith('/new/source')} | ||||
| 					class:bg-coolgray-500={$page.url.pathname.startsWith('/sources') || | ||||
| 						$page.url.pathname.startsWith('/new/source')} | ||||
| 					data-tooltip="Git Sources" | ||||
| 				> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						class="h-8 w-8" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke-width="1.5" | ||||
| 						stroke="currentColor" | ||||
| 						fill="none" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 					> | ||||
| 						<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 						<circle cx="6" cy="6" r="2" /> | ||||
| 						<circle cx="18" cy="18" r="2" /> | ||||
| 						<path d="M11 6h5a2 2 0 0 1 2 2v8" /> | ||||
| 						<polyline points="14 9 11 6 14 3" /> | ||||
| 						<path d="M13 18h-5a2 2 0 0 1 -2 -2v-8" /> | ||||
| 						<polyline points="10 15 13 18 10 21" /> | ||||
| 					</svg> | ||||
| 				</a> | ||||
| 				<div class="border-t border-stone-700" /> | ||||
| 				<a | ||||
| 					sveltekit:prefetch | ||||
| 					href="/destinations" | ||||
| 					class="icons tooltip-right bg-coolgray-200 hover:text-sky-500" | ||||
| 					class:text-sky-500={$page.url.pathname.startsWith('/destinations') || | ||||
| 						$page.url.pathname.startsWith('/new/destination')} | ||||
| 					class:bg-coolgray-500={$page.url.pathname.startsWith('/destinations') || | ||||
| 						$page.url.pathname.startsWith('/new/destination')} | ||||
| 					data-tooltip="Destinations" | ||||
| 				> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						class="h-8 w-8" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke-width="1.5" | ||||
| 						stroke="currentColor" | ||||
| 						fill="none" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 					> | ||||
| 						<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 						<path | ||||
| 							d="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z" | ||||
| 						/> | ||||
| 						<path d="M5 10h3v3h-3z" /> | ||||
| 						<path d="M8 10h3v3h-3z" /> | ||||
| 						<path d="M11 10h3v3h-3z" /> | ||||
| 						<path d="M8 7h3v3h-3z" /> | ||||
| 						<path d="M11 7h3v3h-3z" /> | ||||
| 						<path d="M11 4h3v3h-3z" /> | ||||
| 						<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" /> | ||||
| 						<line x1="10" y1="16" x2="10" y2="16.01" /> | ||||
| 					</svg> | ||||
| 				</a> | ||||
| 				<div class="border-t border-stone-700" /> | ||||
| 				<a | ||||
| 					sveltekit:prefetch | ||||
| 					href="/databases" | ||||
| 					class="icons tooltip-right bg-coolgray-200 hover:text-purple-500" | ||||
| 					class:text-purple-500={$page.url.pathname.startsWith('/databases') || | ||||
| 						$page.url.pathname.startsWith('/new/database')} | ||||
| 					class:bg-coolgray-500={$page.url.pathname.startsWith('/databases') || | ||||
| 						$page.url.pathname.startsWith('/new/database')} | ||||
| 					data-tooltip="Databases" | ||||
| 				> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						class="h-8 w-8" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke-width="1.5" | ||||
| 						stroke="currentColor" | ||||
| 						fill="none" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 					> | ||||
| 						<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 						<ellipse cx="12" cy="6" rx="8" ry="3" /> | ||||
| 						<path d="M4 6v6a8 3 0 0 0 16 0v-6" /> | ||||
| 						<path d="M4 12v6a8 3 0 0 0 16 0v-6" /> | ||||
| 					</svg> | ||||
| 				</a> | ||||
| 				<div class="border-t border-stone-700" /> | ||||
| 				<a | ||||
| 					sveltekit:prefetch | ||||
| 					href="/services" | ||||
| 					class="icons tooltip-right bg-coolgray-200 hover:text-pink-500" | ||||
| 					class:text-pink-500={$page.url.pathname.startsWith('/services') || | ||||
| 						$page.url.pathname.startsWith('/new/service')} | ||||
| 					class:bg-coolgray-500={$page.url.pathname.startsWith('/services') || | ||||
| 						$page.url.pathname.startsWith('/new/service')} | ||||
| 					data-tooltip="Services" | ||||
| 				> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						class="h-8 w-8" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke-width="1.5" | ||||
| 						stroke="currentColor" | ||||
| 						fill="none" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 					> | ||||
| 						<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 						<path d="M7 18a4.6 4.4 0 0 1 0 -9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-12" /> | ||||
| 					</svg> | ||||
| 				</a> | ||||
| 				<div class="border-t border-stone-700" /> | ||||
| 			</div> | ||||
| 			<button | ||||
| 				class="button p-2 bg-warmGray-800 w-full text-center hover:bg-warmGray-700" | ||||
| 				on:click={ackError}>OK</button | ||||
| 			> | ||||
| 		</div> | ||||
| 	</div> | ||||
| {/if} | ||||
| <main class:main={$page.path !== '/success' && $page.path !== '/'}> | ||||
| 	{#if $page.path !== '/' && $page.path !== '/success'} | ||||
| 		<nav class="w-16 bg-warmGray-800 text-white top-0 left-0 fixed min-w-4rem min-h-screen"> | ||||
| 			<div | ||||
| 				class="flex flex-col w-full h-screen items-center transition-all duration-100" | ||||
| 				class:border-green-500={$page.path === '/dashboard/applications'} | ||||
| 				class:border-purple-500={$page.path === '/dashboard/databases'} | ||||
| 			> | ||||
| 				<div class="w-10 pt-4 pb-4"><img src="/favicon.png" alt="coolLabs logo" /></div> | ||||
| 				{#if $settings.clientId} | ||||
| 					<Tooltip position="right" label="Applications"> | ||||
| 						<div | ||||
| 							class="p-2 hover:bg-warmGray-700 rounded hover:text-green-500 mt-4 transition-all duration-100 cursor-pointer" | ||||
| 							on:click={() => goto('/dashboard/applications')} | ||||
| 							class:text-green-500={$page.path === '/dashboard/applications' || | ||||
| 								$page.path.startsWith('/application')} | ||||
| 							class:bg-warmGray-700={$page.path === '/dashboard/applications' || | ||||
| 								$page.path.startsWith('/application')} | ||||
| 						> | ||||
| 							<svg | ||||
| 								class="w-8" | ||||
| 			<div class="flex-1" /> | ||||
|  | ||||
| 			<div class="flex flex-col space-y-4 py-2"> | ||||
| 				{#if $session.teamId === '0'} | ||||
| 					{#if updateStatus.checking} | ||||
| 						<button | ||||
| 							disabled | ||||
| 							in:fade={{ duration: 150 }} | ||||
| 							class="icons tooltip-right bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105" | ||||
| 							data-tooltip="Checking for updates..." | ||||
| 							><svg | ||||
| 								xmlns="http://www.w3.org/2000/svg" | ||||
| 								class="h-9 w-8 animate-spin" | ||||
| 								viewBox="0 0 24 24" | ||||
| 								fill="none" | ||||
| 								stroke-width="1.5" | ||||
| 								stroke="currentColor" | ||||
| 								stroke-width="2" | ||||
| 								fill="none" | ||||
| 								stroke-linecap="round" | ||||
| 								stroke-linejoin="round" | ||||
| 								><rect x="4" y="4" width="16" height="16" rx="2" ry="2" /><rect | ||||
| 									x="9" | ||||
| 									y="9" | ||||
| 									width="6" | ||||
| 									height="6" | ||||
| 								/><line x1="9" y1="1" x2="9" y2="4" /><line x1="15" y1="1" x2="15" y2="4" /><line | ||||
| 									x1="9" | ||||
| 									y1="20" | ||||
| 									x2="9" | ||||
| 									y2="23" | ||||
| 								/><line x1="15" y1="20" x2="15" y2="23" /><line | ||||
| 									x1="20" | ||||
| 									y1="9" | ||||
| 									x2="23" | ||||
| 									y2="9" | ||||
| 								/><line x1="20" y1="14" x2="23" y2="14" /><line x1="1" y1="9" x2="4" y2="9" /><line | ||||
| 									x1="1" | ||||
| 									y1="14" | ||||
| 									x2="4" | ||||
| 									y2="14" | ||||
| 								/></svg | ||||
| 							> | ||||
| 						</div> | ||||
| 					</Tooltip> | ||||
| 					<Tooltip position="right" label="Databases"> | ||||
| 						<div | ||||
| 							class="p-2 hover:bg-warmGray-700 rounded hover:text-purple-500 my-4 transition-all duration-100 cursor-pointer" | ||||
| 							on:click={() => goto('/dashboard/databases')} | ||||
| 							class:text-purple-500={$page.path === '/dashboard/databases' || | ||||
| 								$page.path.startsWith('/database')} | ||||
| 							class:bg-warmGray-700={$page.path === '/dashboard/databases' || | ||||
| 								$page.path.startsWith('/database')} | ||||
| 								<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 								<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" /> | ||||
| 								<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" /> | ||||
| 								<line x1="4.06" y1="11" x2="4.06" y2="11.01" /> | ||||
| 								<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" /> | ||||
| 								<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" /> | ||||
| 								<line x1="11" y1="19.94" x2="11" y2="19.95" /> | ||||
| 							</svg></button | ||||
| 						> | ||||
| 							<svg | ||||
| 								class="w-8" | ||||
| 								xmlns="http://www.w3.org/2000/svg" | ||||
| 								fill="none" | ||||
| 								viewBox="0 0 24 24" | ||||
| 								stroke="currentColor" | ||||
| 							> | ||||
| 								<path | ||||
| 					{:else if isUpdateAvailable} | ||||
| 						<button | ||||
| 							disabled={updateStatus.success === false} | ||||
| 							data-tooltip="Update available" | ||||
| 							on:click={update} | ||||
| 							class="icons tooltip-right bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105" | ||||
| 						> | ||||
| 							{#if updateStatus.loading} | ||||
| 								<svg | ||||
| 									xmlns="http://www.w3.org/2000/svg" | ||||
| 									class="w-8 h-9 lds-heart" | ||||
| 									viewBox="0 0 24 24" | ||||
| 									stroke-width="1.5" | ||||
| 									stroke="currentColor" | ||||
| 									fill="none" | ||||
| 									stroke-linecap="round" | ||||
| 									stroke-linejoin="round" | ||||
| 									stroke-width="2" | ||||
| 									d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" | ||||
| 								/> | ||||
| 							</svg> | ||||
| 						</div> | ||||
| 					</Tooltip> | ||||
| 				{:else} | ||||
| 					<Tooltip | ||||
| 						position="right" | ||||
| 						label="Applications disabled, no GitHub Integration detected" | ||||
| 						size="large" | ||||
| 					> | ||||
| 						<div class="p-2 text-warmGray-700 mt-4 transition-all duration-100 cursor-pointer"> | ||||
| 							<svg | ||||
| 								class="w-8" | ||||
| 								xmlns="http://www.w3.org/2000/svg" | ||||
| 								viewBox="0 0 24 24" | ||||
| 								fill="none" | ||||
| 								stroke="currentColor" | ||||
| 								stroke-width="2" | ||||
| 								stroke-linecap="round" | ||||
| 								stroke-linejoin="round" | ||||
| 								><rect x="4" y="4" width="16" height="16" rx="2" ry="2" /><rect | ||||
| 									x="9" | ||||
| 									y="9" | ||||
| 									width="6" | ||||
| 									height="6" | ||||
| 								/><line x1="9" y1="1" x2="9" y2="4" /><line x1="15" y1="1" x2="15" y2="4" /><line | ||||
| 									x1="9" | ||||
| 									y1="20" | ||||
| 									x2="9" | ||||
| 									y2="23" | ||||
| 								/><line x1="15" y1="20" x2="15" y2="23" /><line | ||||
| 									x1="20" | ||||
| 									y1="9" | ||||
| 									x2="23" | ||||
| 									y2="9" | ||||
| 								/><line x1="20" y1="14" x2="23" y2="14" /><line x1="1" y1="9" x2="4" y2="9" /><line | ||||
| 									x1="1" | ||||
| 									y1="14" | ||||
| 									x2="4" | ||||
| 									y2="14" | ||||
| 								/></svg | ||||
| 							> | ||||
| 						</div> | ||||
| 					</Tooltip> | ||||
| 					<Tooltip position="right" label="Databases disabled, no GitHub Integration detected" size="large"> | ||||
| 						<div | ||||
| 							class="p-2 text-warmGray-700 my-4 transition-all duration-100 cursor-pointer" | ||||
| 						> | ||||
| 							<svg | ||||
| 								class="w-8" | ||||
| 								xmlns="http://www.w3.org/2000/svg" | ||||
| 								fill="none" | ||||
| 								viewBox="0 0 24 24" | ||||
| 								stroke="currentColor" | ||||
| 							> | ||||
| 								<path | ||||
| 								> | ||||
| 									<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 									<path | ||||
| 										d="M19.5 13.572l-7.5 7.428l-7.5 -7.428m0 0a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" | ||||
| 									/> | ||||
| 								</svg> | ||||
| 							{:else if updateStatus.success === null} | ||||
| 								<svg | ||||
| 									xmlns="http://www.w3.org/2000/svg" | ||||
| 									class="w-8 h-9" | ||||
| 									viewBox="0 0 24 24" | ||||
| 									stroke-width="1.5" | ||||
| 									stroke="currentColor" | ||||
| 									fill="none" | ||||
| 									stroke-linecap="round" | ||||
| 									stroke-linejoin="round" | ||||
| 									stroke-width="2" | ||||
| 									d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" | ||||
| 								/> | ||||
| 							</svg> | ||||
| 						</div> | ||||
| 					</Tooltip> | ||||
| 								> | ||||
| 									<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 									<circle cx="12" cy="12" r="9" /> | ||||
| 									<line x1="12" y1="8" x2="8" y2="12" /> | ||||
| 									<line x1="12" y1="8" x2="12" y2="16" /> | ||||
| 									<line x1="16" y1="12" x2="12" y2="8" /> | ||||
| 								</svg> | ||||
| 							{:else if updateStatus.success} | ||||
| 								<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="w-8 h-9" | ||||
| 									><path | ||||
| 										fill="#DD2E44" | ||||
| 										d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z" | ||||
| 									/><path | ||||
| 										fill="#EA596E" | ||||
| 										d="M13 12L.416 32.506l-.282.635.011.011c-.208.403.14 1.223.853 1.937.232.232.473.408.709.557L17 17l-4-5z" | ||||
| 									/><path | ||||
| 										fill="#A0041E" | ||||
| 										d="M23.012 13.066c4.67 4.672 7.263 9.652 5.789 11.124-1.473 1.474-6.453-1.118-11.126-5.788-4.671-4.672-7.263-9.654-5.79-11.127 1.474-1.473 6.454 1.119 11.127 5.791z" | ||||
| 									/><path | ||||
| 										fill="#AA8DD8" | ||||
| 										d="M18.59 13.609c-.199.161-.459.245-.734.215-.868-.094-1.598-.396-2.109-.873-.541-.505-.808-1.183-.735-1.862.128-1.192 1.324-2.286 3.363-2.066.793.085 1.147-.17 1.159-.292.014-.121-.277-.446-1.07-.532-.868-.094-1.598-.396-2.11-.873-.541-.505-.809-1.183-.735-1.862.13-1.192 1.325-2.286 3.362-2.065.578.062.883-.057 1.012-.134.103-.063.144-.123.148-.158.012-.121-.275-.446-1.07-.532-.549-.06-.947-.552-.886-1.102.059-.549.55-.946 1.101-.886 2.037.219 2.973 1.542 2.844 2.735-.13 1.194-1.325 2.286-3.364 2.067-.578-.063-.88.057-1.01.134-.103.062-.145.123-.149.157-.013.122.276.446 1.071.532 2.037.22 2.973 1.542 2.844 2.735-.129 1.192-1.324 2.286-3.362 2.065-.578-.062-.882.058-1.012.134-.104.064-.144.124-.148.158-.013.121.276.446 1.07.532.548.06.947.553.886 1.102-.028.274-.167.511-.366.671z" | ||||
| 									/><path | ||||
| 										fill="#77B255" | ||||
| 										d="M30.661 22.857c1.973-.557 3.334.323 3.658 1.478.324 1.154-.378 2.615-2.35 3.17-.77.216-1.001.584-.97.701.034.118.425.312 1.193.095 1.972-.555 3.333.325 3.657 1.479.326 1.155-.378 2.614-2.351 3.17-.769.216-1.001.585-.967.702.033.117.423.311 1.192.095.53-.149 1.084.16 1.233.691.148.532-.161 1.084-.693 1.234-1.971.555-3.333-.323-3.659-1.479-.324-1.154.379-2.613 2.353-3.169.77-.217 1.001-.584.967-.702-.032-.117-.422-.312-1.19-.096-1.974.556-3.334-.322-3.659-1.479-.325-1.154.378-2.613 2.351-3.17.768-.215.999-.585.967-.701-.034-.118-.423-.312-1.192-.096-.532.15-1.083-.16-1.233-.691-.149-.53.161-1.082.693-1.232z" | ||||
| 									/><path | ||||
| 										fill="#AA8DD8" | ||||
| 										d="M23.001 20.16c-.294 0-.584-.129-.782-.375-.345-.432-.274-1.061.156-1.406.218-.175 5.418-4.259 12.767-3.208.547.078.927.584.849 1.131-.078.546-.58.93-1.132.848-6.493-.922-11.187 2.754-11.233 2.791-.186.148-.406.219-.625.219z" | ||||
| 									/><path | ||||
| 										fill="#77B255" | ||||
| 										d="M5.754 16c-.095 0-.192-.014-.288-.042-.529-.159-.829-.716-.67-1.245 1.133-3.773 2.16-9.794.898-11.364-.141-.178-.354-.353-.842-.316-.938.072-.849 2.051-.848 2.071.042.551-.372 1.031-.922 1.072-.559.034-1.031-.372-1.072-.923-.103-1.379.326-4.035 2.692-4.214 1.056-.08 1.933.287 2.552 1.057 2.371 2.951-.036 11.506-.542 13.192-.13.433-.528.712-.958.712z" | ||||
| 									/><circle fill="#5C913B" cx="25.5" cy="9.5" r="1.5" /><circle | ||||
| 										fill="#9266CC" | ||||
| 										cx="2" | ||||
| 										cy="18" | ||||
| 										r="2" | ||||
| 									/><circle fill="#5C913B" cx="32.5" cy="19.5" r="1.5" /><circle | ||||
| 										fill="#5C913B" | ||||
| 										cx="23.5" | ||||
| 										cy="31.5" | ||||
| 										r="1.5" | ||||
| 									/><circle fill="#FFCC4D" cx="28" cy="4" r="2" /><circle | ||||
| 										fill="#FFCC4D" | ||||
| 										cx="32.5" | ||||
| 										cy="8.5" | ||||
| 										r="1.5" | ||||
| 									/><circle fill="#FFCC4D" cx="29.5" cy="12.5" r="1.5" /><circle | ||||
| 										fill="#FFCC4D" | ||||
| 										cx="7.5" | ||||
| 										cy="23.5" | ||||
| 										r="1.5" | ||||
| 									/></svg | ||||
| 								> | ||||
| 							{:else} | ||||
| 								<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="w-8 h-9" | ||||
| 									><path | ||||
| 										fill="#FFCC4D" | ||||
| 										d="M36 18c0 9.941-8.059 18-18 18S0 27.941 0 18 8.059 0 18 0s18 8.059 18 18" | ||||
| 									/><path | ||||
| 										fill="#664500" | ||||
| 										d="M22 27c0 2.763-1.791 3-4 3-2.21 0-4-.237-4-3 0-2.761 1.79-6 4-6 2.209 0 4 3.239 4 6zm8-12c-.124 0-.25-.023-.371-.072-5.229-2.091-7.372-5.241-7.461-5.374-.307-.46-.183-1.081.277-1.387.459-.306 1.077-.184 1.385.274.019.027 1.93 2.785 6.541 4.629.513.206.763.787.558 1.3-.157.392-.533.63-.929.63zM6 15c-.397 0-.772-.238-.929-.629-.205-.513.044-1.095.557-1.3 4.612-1.844 6.523-4.602 6.542-4.629.308-.456.929-.577 1.387-.27.457.308.581.925.275 1.383-.089.133-2.232 3.283-7.46 5.374C6.25 14.977 6.124 15 6 15z" | ||||
| 									/><path fill="#5DADEC" d="M24 16h4v19l-4-.046V16zM8 35l4-.046V16H8v19z" /><path | ||||
| 										fill="#664500" | ||||
| 										d="M14.999 18c-.15 0-.303-.034-.446-.105-3.512-1.756-7.07-.018-7.105 0-.495.249-1.095.046-1.342-.447-.247-.494-.047-1.095.447-1.342.182-.09 4.498-2.197 8.895 0 .494.247.694.848.447 1.342-.176.35-.529.552-.896.552zm14 0c-.15 0-.303-.034-.446-.105-3.513-1.756-7.07-.018-7.105 0-.494.248-1.094.047-1.342-.447-.247-.494-.047-1.095.447-1.342.182-.09 4.501-2.196 8.895 0 .494.247.694.848.447 1.342-.176.35-.529.552-.896.552z" | ||||
| 									/><ellipse fill="#5DADEC" cx="18" cy="34" rx="18" ry="2" /><ellipse | ||||
| 										fill="#E75A70" | ||||
| 										cx="18" | ||||
| 										cy="27" | ||||
| 										rx="3" | ||||
| 										ry="2" | ||||
| 									/></svg | ||||
| 								> | ||||
| 							{/if} | ||||
| 						</button> | ||||
| 					{/if} | ||||
| 				{/if} | ||||
|  | ||||
| 				<Tooltip position="right" label="Services"> | ||||
| 					<div | ||||
| 						class="p-2 hover:bg-warmGray-700 rounded hover:text-blue-500 transition-all duration-100 cursor-pointer" | ||||
| 						class:text-blue-500={$page.path === '/dashboard/services' || | ||||
| 							$page.path.startsWith('/service')} | ||||
| 						class:bg-warmGray-700={$page.path === '/dashboard/services' || | ||||
| 							$page.path.startsWith('/service')} | ||||
| 						on:click={() => goto('/dashboard/services')} | ||||
| 				<a | ||||
| 					sveltekit:prefetch | ||||
| 					href="/teams" | ||||
| 					class="icons tooltip-right bg-coolgray-200 hover:text-cyan-500" | ||||
| 					class:text-cyan-500={$page.url.pathname.startsWith('/teams')} | ||||
| 					class:bg-coolgray-500={$page.url.pathname.startsWith('/teams')} | ||||
| 					data-tooltip="Teams" | ||||
| 				> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						class="h-8 w-8" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke-width="1.5" | ||||
| 						stroke="currentColor" | ||||
| 						fill="none" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 					> | ||||
| 						<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 						<circle cx="9" cy="7" r="4" /> | ||||
| 						<path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" /> | ||||
| 						<path d="M16 3.13a4 4 0 0 1 0 7.75" /> | ||||
| 						<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" /> | ||||
| 					</svg> | ||||
| 				</a> | ||||
| 				{#if $session.teamId === '0'} | ||||
| 					<a | ||||
| 						sveltekit:prefetch | ||||
| 						href="/settings" | ||||
| 						class="icons tooltip-right bg-coolgray-200 hover:text-yellow-500" | ||||
| 						class:text-yellow-500={$page.url.pathname.startsWith('/settings')} | ||||
| 						class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')} | ||||
| 						data-tooltip="Settings" | ||||
| 					> | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							class="w-8" | ||||
| 							fill="none" | ||||
| 							class="h-8 w-8" | ||||
| 							viewBox="0 0 24 24" | ||||
| 							stroke-width="1.5" | ||||
| 							stroke="currentColor" | ||||
| 						> | ||||
| 							<path | ||||
| 								stroke-linecap="round" | ||||
| 								stroke-linejoin="round" | ||||
| 								stroke-width="2" | ||||
| 								d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 					</div> | ||||
| 				</Tooltip> | ||||
| 				<div class="flex-1" /> | ||||
| 				{#if globalFeatureFlag} | ||||
| 					<Tooltip position="right" label="Servers"> | ||||
| 						<div | ||||
| 							class="p-2 hover:bg-warmGray-700 rounded hover:text-red-500 mb-4 transition-all duration-100 cursor-pointer" | ||||
| 							on:click={() => goto('/servers')} | ||||
| 							class:text-red-500={$page.path === '/servers' || $page.path.startsWith('/servers')} | ||||
| 							class:bg-warmGray-700={$page.path === '/servers' || $page.path.startsWith('/servers')} | ||||
| 						> | ||||
| 							<svg | ||||
| 								class="w-8" | ||||
| 								xmlns="http://www.w3.org/2000/svg" | ||||
| 								fill="none" | ||||
| 								viewBox="0 0 24 24" | ||||
| 								stroke="currentColor" | ||||
| 							> | ||||
| 								<path | ||||
| 									stroke-linecap="round" | ||||
| 									stroke-linejoin="round" | ||||
| 									stroke-width="2" | ||||
| 									d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" | ||||
| 								/> | ||||
| 							</svg> | ||||
| 						</div> | ||||
| 					</Tooltip> | ||||
| 				{/if} | ||||
| 				<Tooltip position="right" label="Settings"> | ||||
| 					<button | ||||
| 						class="p-2 hover:bg-warmGray-700 rounded hover:text-yellow-500 transition-all duration-100 cursor-pointer" | ||||
| 						class:text-yellow-500={$page.path === '/settings'} | ||||
| 						class:bg-warmGray-700={$page.path === '/settings'} | ||||
| 						on:click={() => goto('/settings')} | ||||
| 					> | ||||
| 						<svg | ||||
| 							class="w-8" | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							fill="none" | ||||
| 							viewBox="0 0 24 24" | ||||
| 							stroke="currentColor" | ||||
| 						> | ||||
| 							<path | ||||
| 								stroke-linecap="round" | ||||
| 								stroke-linejoin="round" | ||||
| 								stroke-width="2" | ||||
| 								d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" | ||||
| 							/> | ||||
| 							<path | ||||
| 								stroke-linecap="round" | ||||
| 								stroke-linejoin="round" | ||||
| 								stroke-width="2" | ||||
| 								d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 					</button> | ||||
| 				</Tooltip> | ||||
| 				<Tooltip position="right" label="Logout"> | ||||
| 					<button | ||||
| 						class="p-2 hover:bg-warmGray-700 rounded hover:text-red-500 my-4 transition-all duration-100 cursor-pointer" | ||||
| 						on:click={logout} | ||||
| 					> | ||||
| 						<svg | ||||
| 							class="w-7" | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							viewBox="0 0 24 24" | ||||
| 							fill="none" | ||||
| 							stroke="currentColor" | ||||
| 							stroke-width="2" | ||||
| 							stroke-linecap="round" | ||||
| 							stroke-linejoin="round" | ||||
| 							><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /><polyline | ||||
| 								points="16 17 21 12 16 7" | ||||
| 							/><line x1="21" y1="12" x2="9" y2="12" /></svg | ||||
| 						> | ||||
| 					</button> | ||||
| 				</Tooltip> | ||||
| 				<a | ||||
| 					href={`https://github.com/coollabsio/coolify/releases/tag/v${packageJson.version}`} | ||||
| 					target="_blank" | ||||
| 					class="cursor-pointer text-xs font-bold text-warmGray-400 py-2 hover:bg-warmGray-700 w-full text-center" | ||||
| 							<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 							<path | ||||
| 								d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" | ||||
| 							/> | ||||
| 							<circle cx="12" cy="12" r="3" /> | ||||
| 						</svg> | ||||
| 					</a> | ||||
| 				{/if} | ||||
| 				<div | ||||
| 					class="icons tooltip-right bg-coolgray-200 hover:text-red-500" | ||||
| 					data-tooltip="Logout" | ||||
| 					on:click={logout} | ||||
| 				> | ||||
| 					{packageJson.version} | ||||
| 				</a> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						class="ml-1 h-7 w-7" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke-width="1.5" | ||||
| 						stroke="currentColor" | ||||
| 						fill="none" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 					> | ||||
| 						<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 						<path | ||||
| 							d="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2" | ||||
| 						/> | ||||
| 						<path d="M7 12h14l-3 -3m0 6l3 -3" /> | ||||
| 					</svg> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</nav> | ||||
| 	{/if} | ||||
| 			<div | ||||
| 				class="w-full text-center font-bold text-stone-400 hover:bg-coolgray-200 hover:text-white" | ||||
| 			> | ||||
| 				<a | ||||
| 					class="text-[10px] no-underline" | ||||
| 					href={`https://github.com/coollabsio/coolify/releases/tag/v${$session.version}`} | ||||
| 					target="_blank">v{$session.version}</a | ||||
| 				> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</nav> | ||||
| 	<select | ||||
| 		class="fixed right-0 bottom-0 z-50 m-2 p-2 px-4" | ||||
| 		bind:value={selectedTeamId} | ||||
| 		on:change={switchTeam} | ||||
| 	> | ||||
| 		<option value="" disabled selected>Switch to a different team...</option> | ||||
| 		{#each teams as team} | ||||
| 			<option value={team.teamId}>{team.team.name} - {team.permission}</option> | ||||
| 		{/each} | ||||
| 	</select> | ||||
| {/if} | ||||
| <main> | ||||
| 	<slot /> | ||||
| </main> | ||||
| {#if upgradeAvailable && $page.path !== '/success' && $page.path !== '/'} | ||||
| 	<footer | ||||
| 		class="fixed bottom-0 right-0 p-4 px-6 w-auto rounded-tl text-white  hover:scale-110  transition duration-100" | ||||
| 	> | ||||
| 		<div class="flex items-center"> | ||||
| 			<div /> | ||||
| 			<div class="flex-1" /> | ||||
| 			{#if !upgradeDisabled} | ||||
| 				<button | ||||
| 					class="bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-xs font-bold rounded px-2 py-2" | ||||
| 					disabled={upgradeDisabled} | ||||
| 					on:click={upgrade}>New version available, <br />click here to upgrade!</button | ||||
| 				> | ||||
| 			{:else if upgradeDone} | ||||
| 				<button | ||||
| 					use:reloadInAMin | ||||
| 					class="font-bold text-xs rounded px-2 cursor-not-allowed" | ||||
| 					disabled={upgradeDisabled}>Upgrade done. 🎉 Automatically reloading in 30s.</button | ||||
| 				> | ||||
| 			{:else} | ||||
| 				<button | ||||
| 					class="opacity-50 tracking-tight font-bold text-xs rounded px-2 cursor-not-allowed" | ||||
| 					disabled={upgradeDisabled}>Upgrading. It could take a while, please wait...</button | ||||
| 				> | ||||
| 			{/if} | ||||
| 		</div> | ||||
| 	</footer> | ||||
| {/if} | ||||
|   | ||||
| @@ -1,36 +0,0 @@ | ||||
| import { setDefaultConfiguration } from '$lib/api/applications/configuration'; | ||||
| import { saveServerLog } from '$lib/api/applications/logging'; | ||||
| import Configuration from '$models/Configuration'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	try { | ||||
| 		const { DOMAIN } = process.env; | ||||
| 		const configuration = setDefaultConfiguration(request.body); | ||||
| 		const sameDomainAndPath = await Configuration.find({ | ||||
| 			'publish.path': configuration.publish.path, | ||||
| 			'publish.domain': configuration.publish.domain | ||||
| 		}).select('-_id -__v -createdAt -updatedAt'); | ||||
| 		if (sameDomainAndPath.length > 1 || configuration.publish.domain === DOMAIN) { | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				body: { | ||||
| 					success: false, | ||||
| 					message: 'Domain/path are already in use.' | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { success: true, message: 'OK' } | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		await saveServerLog(error); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: error.message || error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -1,49 +0,0 @@ | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import Configuration from '$models/Configuration'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	const { nickname }: any = request.body || {}; | ||||
| 	if (nickname) { | ||||
| 		const configurationFound = await Configuration.find({ | ||||
| 			'general.nickname': nickname | ||||
| 		}).select('-_id -__v -createdAt -updatedAt'); | ||||
| 		if (configurationFound) { | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				body: { | ||||
| 					configuration: [...configurationFound] | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		const services = await docker.engine.listServices(); | ||||
| 		const applications = services.filter( | ||||
| 			(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application' | ||||
| 		); | ||||
| 		const found = applications.find((r) => { | ||||
| 			const configuration = r.Spec.Labels.configuration | ||||
| 				? JSON.parse(r.Spec.Labels.configuration) | ||||
| 				: null; | ||||
|  | ||||
| 			if (configuration.general.nickname === nickname) return r; | ||||
| 			return null; | ||||
| 		}); | ||||
|  | ||||
| 		if (found) { | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				body: { | ||||
| 					success: true, | ||||
| 					...JSON.parse(found.Spec.Labels.configuration) | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: 'No configuration found.' | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -1,75 +0,0 @@ | ||||
| import { updateServiceLabels } from '$lib/api/applications/configuration'; | ||||
| import { execShellAsync } from '$lib/api/common'; | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import ApplicationLog from '$models/ApplicationLog'; | ||||
| import Configuration from '$models/Configuration'; | ||||
| import Deployment from '$models/Deployment'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	const { name, organization, branch, isPreviewDeploymentEnabled }: any = request.body || {}; | ||||
| 	if (name && organization && branch) { | ||||
| 		const configuration = await Configuration.findOneAndUpdate( | ||||
| 			{ | ||||
| 				'repository.name': name, | ||||
| 				'repository.organization': organization, | ||||
| 				'repository.branch': branch | ||||
| 			}, | ||||
| 			{ | ||||
| 				$set: { | ||||
| 					'general.isPreviewDeploymentEnabled': isPreviewDeploymentEnabled, | ||||
| 					'general.pullRequest': 0 | ||||
| 				} | ||||
| 			}, | ||||
| 			{ new: true } | ||||
| 		).select('-_id -__v -createdAt -updatedAt'); | ||||
| 		if (!isPreviewDeploymentEnabled) { | ||||
| 			const found = await Configuration.find({ | ||||
| 				'repository.name': name, | ||||
| 				'repository.organization': organization, | ||||
| 				'repository.branch': branch, | ||||
| 				'general.pullRequest': { $ne: 0 } | ||||
| 			}); | ||||
| 			for (const prDeployment of found) { | ||||
| 				await Configuration.findOneAndRemove({ | ||||
| 					'repository.name': name, | ||||
| 					'repository.organization': organization, | ||||
| 					'repository.branch': branch, | ||||
| 					'publish.domain': prDeployment.publish.domain | ||||
| 				}); | ||||
| 				const deploys = await Deployment.find({ | ||||
| 					organization, | ||||
| 					branch, | ||||
| 					name, | ||||
| 					domain: prDeployment.publish.domain | ||||
| 				}); | ||||
| 				for (const deploy of deploys) { | ||||
| 					await ApplicationLog.deleteMany({ deployId: deploy.deployId }); | ||||
| 					await Deployment.deleteMany({ deployId: deploy.deployId }); | ||||
| 				} | ||||
| 				await execShellAsync(`docker stack rm ${prDeployment.build.container.name}`); | ||||
| 			} | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				body: { | ||||
| 					organization, | ||||
| 					name, | ||||
| 					branch | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 		updateServiceLabels(configuration); | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				success: true | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| 	return { | ||||
| 		status: 500, | ||||
| 		body: { | ||||
| 			error: 'Cannot save.' | ||||
| 		} | ||||
| 	}; | ||||
| } | ||||
| @@ -1,56 +0,0 @@ | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import Deployment from '$models/Deployment'; | ||||
| import { precheckDeployment, setDefaultConfiguration } from '$lib/api/applications/configuration'; | ||||
| import cloneRepository from '$lib/api/applications/cloneRepository'; | ||||
| import { cleanupTmp } from '$lib/api/common'; | ||||
| import queueAndBuild from '$lib/api/applications/queueAndBuild'; | ||||
| import Configuration from '$models/Configuration'; | ||||
| import preChecks from '$lib/api/applications/preChecks'; | ||||
| import preTasks from '$lib/api/applications/preTasks'; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	const configuration = setDefaultConfiguration(request.body); | ||||
| 	if (!configuration) { | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: 'Whaaat?' | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| 	try { | ||||
| 		await cloneRepository(configuration); | ||||
| 		const nextStep = await preChecks(configuration); | ||||
| 		if (nextStep === 0) { | ||||
| 			cleanupTmp(configuration.general.workdir); | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				body: { | ||||
| 					success: false, | ||||
| 					message: 'Nothing changed, no need to redeploy.' | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 		await preTasks(configuration) | ||||
|  | ||||
| 		queueAndBuild(configuration, nextStep); | ||||
| 		return { | ||||
| 			status: 201, | ||||
| 			body: { | ||||
| 				message: 'Deployment queued.', | ||||
| 				nickname: configuration.general.nickname, | ||||
| 				name: configuration.build.container.name, | ||||
| 				deployId: configuration.general.deployId | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		console.log(error); | ||||
| 		await Deployment.findOneAndUpdate({ nickname: configuration.general.nickname }, { $set: { progress: 'failed' } }); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: error.message || error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -1,32 +0,0 @@ | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import ApplicationLog from '$models/ApplicationLog'; | ||||
| import Deployment from '$models/Deployment'; | ||||
| import dayjs from 'dayjs'; | ||||
|  | ||||
| export async function get(request: Request) { | ||||
| 	const { deployId } = request.params; | ||||
| 	try { | ||||
| 		const logs: any = await ApplicationLog.find({ deployId }) | ||||
| 			.select('-_id -__v') | ||||
| 			.sort({ createdAt: 'asc' }); | ||||
|  | ||||
| 		const deploy: any = await Deployment.findOne({ deployId }).select('-_id -__v'); | ||||
| 		const finalLogs: any = {}; | ||||
| 		finalLogs.progress = deploy.progress; | ||||
| 		finalLogs.events = logs.map((log) => log.event); | ||||
| 		finalLogs.human = dayjs(deploy.updatedAt).from(dayjs(deploy.updatedAt)); | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				...finalLogs | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: error.message || error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -1,42 +0,0 @@ | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import dayjs from 'dayjs'; | ||||
| import utc from 'dayjs/plugin/utc.js'; | ||||
| import relativeTime from 'dayjs/plugin/relativeTime.js'; | ||||
| import Deployment from '$models/Deployment'; | ||||
| dayjs.extend(utc); | ||||
| dayjs.extend(relativeTime); | ||||
| export async function get(request: Request) { | ||||
| 	try { | ||||
| 		const repoId = request.query.get('repoId'); | ||||
| 		const branch = request.query.get('branch'); | ||||
| 		const page = request.query.get('page'); | ||||
| 		const onePage = 5; | ||||
| 		const show = Number(page) * onePage || 5; | ||||
| 		const deploy: any = await Deployment.find({ repoId, branch }) | ||||
| 			.select('-_id -__v -repoId') | ||||
| 			.sort({ createdAt: 'desc' }) | ||||
| 			.limit(show); | ||||
| 		const finalLogs = deploy.map((d) => { | ||||
| 			const finalLogs = { ...d._doc }; | ||||
| 			const updatedAt = dayjs(d.updatedAt).utc(); | ||||
| 			finalLogs.took = updatedAt.diff(dayjs(d.createdAt)) / 1000; | ||||
| 			finalLogs.since = updatedAt.fromNow(); | ||||
| 			finalLogs.isPr = d.domain.startsWith('pr'); | ||||
| 			return finalLogs; | ||||
| 		}); | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				success: true, | ||||
| 				logs: finalLogs | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: error.message || error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -1,28 +0,0 @@ | ||||
| import { saveServerLog } from '$lib/api/applications/logging'; | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
|  | ||||
| export async function get(request: Request) { | ||||
| 	try { | ||||
| 		const name = request.query.get('name'); | ||||
| 		const service = await docker.engine.getService(`${name}_${name}`); | ||||
| 		const logs = (await service.logs({ stdout: true, stderr: true, timestamps: true })) | ||||
| 			.toString() | ||||
| 			.split('\n') | ||||
| 			.map((l) => l.slice(8)) | ||||
| 			.filter((a) => a); | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { success: true, logs } | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		console.log(error); | ||||
| 		await saveServerLog(error); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: 'No such service. Is it under deployment?' | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -1,58 +0,0 @@ | ||||
| import { purgeImagesContainers } from '$lib/api/applications/cleanup'; | ||||
| import Deployment from '$models/Deployment'; | ||||
| import ApplicationLog from '$models/ApplicationLog'; | ||||
| import { delay, execShellAsync } from '$lib/api/common'; | ||||
| import Configuration from '$models/Configuration'; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	const { nickname } = request.body; | ||||
| 	try { | ||||
| 		const configurationFound = await Configuration.findOne({ | ||||
| 			'general.nickname': nickname | ||||
| 		}); | ||||
| 		if (configurationFound) { | ||||
| 			const id = configurationFound._id; | ||||
| 			if (configurationFound?.general?.pullRequest === 0) { | ||||
| 				// Main deployment deletion request; deleting main + PRs | ||||
| 				const allConfiguration = await Configuration.find({ | ||||
| 					'publish.domain': { $regex: `.*${configurationFound.publish.domain}`, $options: 'i' }, | ||||
| 					'publish.path': configurationFound.publish.path | ||||
| 				}); | ||||
| 				for (const config of allConfiguration) { | ||||
| 					await execShellAsync(`docker stack rm ${config.build.container.name}`); | ||||
| 				} | ||||
| 				await Configuration.deleteMany({ | ||||
| 					'publish.domain': { $regex: `.*${configurationFound.publish.domain}`, $options: 'i' }, | ||||
| 					'publish.path': configurationFound.publish.path | ||||
| 				}); | ||||
| 				const deploys = await Deployment.find({ nickname }); | ||||
| 				for (const deploy of deploys) { | ||||
| 					await ApplicationLog.deleteMany({ deployId: deploy.deployId }); | ||||
| 					await Deployment.deleteMany({ deployId: deploy.deployId }); | ||||
| 				} | ||||
| 			} else { | ||||
| 				// Delete only PRs | ||||
| 				await Configuration.findByIdAndRemove(id); | ||||
| 				await execShellAsync(`docker stack rm ${configurationFound.build.container.name}`); | ||||
| 				const deploys = await Deployment.find({ nickname }); | ||||
| 				for (const deploy of deploys) { | ||||
| 					await ApplicationLog.deleteMany({ deployId: deploy.deployId }); | ||||
| 					await Deployment.deleteMany({ deployId: deploy.deployId }); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: {} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		console.log(error); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			error: { | ||||
| 				message: 'Nothing to do.' | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -1,68 +0,0 @@ | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import Configuration from '$models/Configuration'; | ||||
| export async function get(request: Request) { | ||||
| 	// Should update this to get data from mongodb and update db with the currently running services on start! | ||||
| 	const dockerServices = await docker.engine.listServices(); | ||||
| 	let databases: any = dockerServices.filter( | ||||
| 		(r) => | ||||
| 			r.Spec.Labels.managedBy === 'coolify' && | ||||
| 			r.Spec.Labels.type === 'database' && | ||||
| 			r.Spec.Labels.configuration | ||||
| 	); | ||||
| 	let services: any = dockerServices.filter( | ||||
| 		(r) => | ||||
| 			r.Spec.Labels.managedBy === 'coolify' && | ||||
| 			r.Spec.Labels.type === 'service' && | ||||
| 			r.Spec.Labels.configuration | ||||
| 	); | ||||
| 	databases = databases.map((r) => { | ||||
| 		if (JSON.parse(r.Spec.Labels.configuration)) { | ||||
| 			return { | ||||
| 				configuration: JSON.parse(r.Spec.Labels.configuration) | ||||
| 			}; | ||||
| 		} | ||||
| 		return {}; | ||||
| 	}); | ||||
| 	services = services.map((r) => { | ||||
| 		if (JSON.parse(r.Spec.Labels.configuration)) { | ||||
| 			return { | ||||
| 				serviceName: r.Spec.Labels.serviceName, | ||||
| 				configuration: JSON.parse(r.Spec.Labels.configuration) | ||||
| 			}; | ||||
| 		} | ||||
| 		return {}; | ||||
| 	}); | ||||
| 	const configurations = await Configuration.find({ | ||||
| 		'general.pullRequest': { $in: [null, 0] } | ||||
| 	}).select('-_id -__v -createdAt'); | ||||
| 	const applications = []; | ||||
| 	for (const configuration of configurations) { | ||||
| 		const foundPRDeployments = await Configuration.find({ | ||||
| 			'repository.id': configuration.repository.id, | ||||
| 			'repository.branch': configuration.repository.branch, | ||||
| 			'general.pullRequest': { $ne: 0 } | ||||
| 		}).select('-_id -__v -createdAt'); | ||||
| 		const payload = { | ||||
| 			configuration, | ||||
| 			UpdatedAt: configuration.updatedAt, | ||||
| 			prBuilds: foundPRDeployments.length > 0 ? true : false | ||||
| 		}; | ||||
| 		applications.push(payload); | ||||
| 	} | ||||
| 	return { | ||||
| 		status: 200, | ||||
| 		body: { | ||||
| 			success: true, | ||||
| 			applications: { | ||||
| 				deployed: applications | ||||
| 			}, | ||||
| 			databases: { | ||||
| 				deployed: databases | ||||
| 			}, | ||||
| 			services: { | ||||
| 				deployed: services | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
| } | ||||
| @@ -1,146 +0,0 @@ | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import { saveServerLog } from '$lib/api/applications/logging'; | ||||
| import { execShellAsync } from '$lib/api/common'; | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import fs from 'fs'; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	const tmpdir = '/tmp/backups'; | ||||
| 	const { deployId } = request.params; | ||||
| 	try { | ||||
| 		const now = new Date(); | ||||
| 		const configuration = JSON.parse( | ||||
| 			JSON.parse(await execShellAsync(`docker inspect ${deployId}_${deployId}`))[0].Spec.Labels | ||||
| 				.configuration | ||||
| 		); | ||||
| 		const type = configuration.general.type; | ||||
| 		const serviceId = configuration.general.deployId; | ||||
| 		const databaseService = (await docker.engine.listContainers()).find( | ||||
| 			(r) => r.Labels['com.docker.stack.namespace'] === serviceId && r.State === 'running' | ||||
| 		); | ||||
| 		const containerID = databaseService.Labels['com.docker.swarm.task.name']; | ||||
| 		await execShellAsync(`mkdir -p ${tmpdir}`); | ||||
| 		if (type === 'mongodb') { | ||||
| 			if (databaseService) { | ||||
| 				const username = configuration.database.usernames[0]; | ||||
| 				const password = configuration.database.passwords[1]; | ||||
| 				const databaseName = configuration.database.defaultDatabaseName; | ||||
| 				const filename = `${databaseName}_${now.getTime()}.gz`; | ||||
| 				const fullfilename = `${tmpdir}/${filename}`; | ||||
| 				await execShellAsync( | ||||
| 					`docker exec -i ${containerID} /bin/bash -c "mkdir -p ${tmpdir};mongodump --uri='mongodb://${username}:${password}@${deployId}:27017' -d ${databaseName} --gzip --archive=${fullfilename}"` | ||||
| 				); | ||||
| 				await execShellAsync(`docker cp ${containerID}:${fullfilename} ${fullfilename}`); | ||||
| 				await execShellAsync(`docker exec -i ${containerID} /bin/bash -c "rm -f ${fullfilename}"`); | ||||
| 				return { | ||||
| 					status: 200, | ||||
| 					headers: { | ||||
| 						'Content-Type': 'application/octet-stream', | ||||
| 						'Content-Transfer-Encoding': 'binary', | ||||
| 						'Content-Disposition': `attachment; filename=${filename}` | ||||
| 					}, | ||||
| 					body: fs.readFileSync(`${fullfilename}`) | ||||
| 				}; | ||||
| 			} | ||||
| 		} else if (type === 'postgresql') { | ||||
| 			if (databaseService) { | ||||
| 				const username = configuration.database.usernames[0]; | ||||
| 				const password = configuration.database.passwords[0]; | ||||
| 				const databaseName = configuration.database.defaultDatabaseName; | ||||
| 				const filename = `${databaseName}_${now.getTime()}.sql.gz`; | ||||
| 				const fullfilename = `${tmpdir}/${filename}`; | ||||
| 				await execShellAsync( | ||||
| 					`docker exec -i ${containerID} /bin/bash -c "PGPASSWORD=${password} pg_dump --username ${username} -Z 9 ${databaseName}" > ${fullfilename}` | ||||
| 				); | ||||
| 				return { | ||||
| 					status: 200, | ||||
| 					headers: { | ||||
| 						'Content-Type': 'application/octet-stream', | ||||
| 						'Content-Transfer-Encoding': 'binary', | ||||
| 						'Content-Disposition': `attachment; filename=${filename}` | ||||
| 					}, | ||||
| 					body: fs.readFileSync(`${fullfilename}`) | ||||
| 				}; | ||||
| 			} | ||||
| 		} else if (type === 'couchdb') { | ||||
| 			if (databaseService) { | ||||
| 				const databaseName = configuration.database.defaultDatabaseName; | ||||
| 				const filename = `${databaseName}_${now.getTime()}.tar.gz`; | ||||
| 				const fullfilename = `${tmpdir}/${filename}`; | ||||
| 				await execShellAsync( | ||||
| 					`docker exec -i ${containerID} /bin/bash -c "cd /bitnami/couchdb/data/ && tar -czvf - ." > ${fullfilename}` | ||||
| 				); | ||||
| 				return { | ||||
| 					status: 200, | ||||
| 					headers: { | ||||
| 						'Content-Type': 'application/octet-stream', | ||||
| 						'Content-Transfer-Encoding': 'binary', | ||||
| 						'Content-Disposition': `attachment; filename=${filename}` | ||||
| 					}, | ||||
| 					body: fs.readFileSync(`${fullfilename}`) | ||||
| 				}; | ||||
| 			} | ||||
| 		} else if (type === 'mysql') { | ||||
| 			if (databaseService) { | ||||
| 				const username = configuration.database.usernames[0]; | ||||
| 				const password = configuration.database.passwords[0]; | ||||
| 				const databaseName = configuration.database.defaultDatabaseName; | ||||
| 				const filename = `${databaseName}_${now.getTime()}.sql.gz`; | ||||
| 				const fullfilename = `${tmpdir}/${filename}`; | ||||
| 				await execShellAsync( | ||||
| 					`docker exec -i ${containerID} /bin/bash -c "mysqldump -u ${username} -p${password} ${databaseName} | gzip -9 -" > ${fullfilename}` | ||||
| 				); | ||||
| 				return { | ||||
| 					status: 200, | ||||
| 					headers: { | ||||
| 						'Content-Type': 'application/octet-stream', | ||||
| 						'Content-Transfer-Encoding': 'binary', | ||||
| 						'Content-Disposition': `attachment; filename=${filename}` | ||||
| 					}, | ||||
| 					body: fs.readFileSync(`${fullfilename}`) | ||||
| 				}; | ||||
| 			} | ||||
| 		} else if (type === 'redis') { | ||||
| 			if (databaseService) { | ||||
| 				const password = configuration.database.passwords[0]; | ||||
| 				const databaseName = configuration.database.defaultDatabaseName; | ||||
| 				const filename = `${databaseName}_${now.getTime()}.rdb`; | ||||
| 				const fullfilename = `${tmpdir}/${filename}`; | ||||
| 				await execShellAsync( | ||||
| 					`docker exec -i ${containerID} /bin/bash -c "redis-cli --pass ${password} save"` | ||||
| 				); | ||||
| 				await execShellAsync( | ||||
| 					`docker cp ${containerID}:/bitnami/redis/data/dump.rdb ${fullfilename}` | ||||
| 				); | ||||
| 				await execShellAsync( | ||||
| 					`docker exec -i ${containerID} /bin/bash -c "rm -f /bitnami/redis/data/dump.rdb"` | ||||
| 				); | ||||
| 				return { | ||||
| 					status: 200, | ||||
| 					headers: { | ||||
| 						'Content-Type': 'application/octet-stream', | ||||
| 						'Content-Transfer-Encoding': 'binary', | ||||
| 						'Content-Disposition': `attachment; filename=${filename}` | ||||
| 					}, | ||||
| 					body: fs.readFileSync(`${fullfilename}`) | ||||
| 				}; | ||||
| 			} | ||||
| 		} | ||||
| 		return { | ||||
| 			status: 501, | ||||
| 			body: { | ||||
| 				error: `Backup method not implemented yet for ${type}.` | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		await saveServerLog(error); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: error.message || error | ||||
| 			} | ||||
| 		}; | ||||
| 	} finally { | ||||
| 		await execShellAsync(`rm -fr ${tmpdir}`); | ||||
| 	} | ||||
| } | ||||
| @@ -1,59 +0,0 @@ | ||||
| import { execShellAsync } from '$lib/api/common'; | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
|  | ||||
| export async function del(request: Request) { | ||||
| 	const { deployId } = request.params; | ||||
| 	await execShellAsync(`docker stack rm ${deployId}`); | ||||
| 	return { | ||||
| 		status: 200, | ||||
| 		body: {} | ||||
| 	}; | ||||
| } | ||||
| export async function get(request: Request) { | ||||
| 	const { deployId } = request.params; | ||||
|  | ||||
| 	try { | ||||
| 		const database = (await docker.engine.listServices()).find( | ||||
| 			(r) => | ||||
| 				r.Spec.Labels.managedBy === 'coolify' && | ||||
| 				r.Spec.Labels.type === 'database' && | ||||
| 				JSON.parse(r.Spec.Labels.configuration).general.deployId === deployId | ||||
| 		); | ||||
|  | ||||
| 		if (database) { | ||||
| 			const jsonEnvs = {}; | ||||
| 			if (database.Spec.TaskTemplate.ContainerSpec.Env) { | ||||
| 				for (const d of database.Spec.TaskTemplate.ContainerSpec.Env) { | ||||
| 					const s = d.split('='); | ||||
| 					jsonEnvs[s[0]] = s[1]; | ||||
| 				} | ||||
| 			} | ||||
| 			const payload = { | ||||
| 				config: JSON.parse(database.Spec.Labels.configuration), | ||||
| 				envs: jsonEnvs || null | ||||
| 			}; | ||||
|  | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				body: { | ||||
| 					...payload | ||||
| 				} | ||||
| 			}; | ||||
| 		} else { | ||||
| 			return { | ||||
| 				status: 500, | ||||
| 				body: { | ||||
| 					error: 'No database found.' | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 	} catch (error) { | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: 'No database found.' | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -1,167 +0,0 @@ | ||||
| import { saveServerLog } from '$lib/api/applications/logging'; | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import yaml from 'js-yaml'; | ||||
| import { promises as fs } from 'fs'; | ||||
| import cuid from 'cuid'; | ||||
| import generator from 'generate-password'; | ||||
| import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator'; | ||||
| import { execShellAsync } from '$lib/api/common'; | ||||
|  | ||||
| function getUniq() { | ||||
| 	return uniqueNamesGenerator({ dictionaries: [adjectives, animals, colors], length: 2 }); | ||||
| } | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	try { | ||||
| 		const { type } = request.body; | ||||
| 		let { defaultDatabaseName } = request.body; | ||||
| 		const passwords = generator.generateMultiple(2, { | ||||
| 			length: 24, | ||||
| 			numbers: true, | ||||
| 			strict: true | ||||
| 		}); | ||||
| 		const usernames = generator.generateMultiple(2, { | ||||
| 			length: 10, | ||||
| 			numbers: true, | ||||
| 			strict: true | ||||
| 		}); | ||||
| 		// TODO: Query for existing db with the same name | ||||
| 		const nickname = getUniq(); | ||||
|  | ||||
| 		if (!defaultDatabaseName) defaultDatabaseName = nickname; | ||||
|  | ||||
| 		const deployId = cuid(); | ||||
| 		const configuration = { | ||||
| 			general: { | ||||
| 				workdir: `/tmp/${deployId}`, | ||||
| 				deployId, | ||||
| 				nickname, | ||||
| 				type | ||||
| 			}, | ||||
| 			database: { | ||||
| 				usernames, | ||||
| 				passwords, | ||||
| 				defaultDatabaseName | ||||
| 			}, | ||||
| 			deploy: { | ||||
| 				name: nickname | ||||
| 			} | ||||
| 		}; | ||||
| 		await execShellAsync(`mkdir -p ${configuration.general.workdir}`); | ||||
| 		let generateEnvs = {}; | ||||
| 		let image = null; | ||||
| 		let volume = null; | ||||
| 		let ulimits = {}; | ||||
| 		if (type === 'mongodb') { | ||||
| 			generateEnvs = { | ||||
| 				MONGODB_ROOT_PASSWORD: passwords[0], | ||||
| 				MONGODB_USERNAME: usernames[0], | ||||
| 				MONGODB_PASSWORD: passwords[1], | ||||
| 				MONGODB_DATABASE: defaultDatabaseName | ||||
| 			}; | ||||
| 			image = 'bitnami/mongodb:4.4'; | ||||
| 			volume = `${configuration.general.deployId}-${type}-data:/bitnami/mongodb`; | ||||
| 		} else if (type === 'postgresql') { | ||||
| 			generateEnvs = { | ||||
| 				POSTGRESQL_PASSWORD: passwords[0], | ||||
| 				POSTGRESQL_USERNAME: usernames[0], | ||||
| 				POSTGRESQL_DATABASE: defaultDatabaseName | ||||
| 			}; | ||||
| 			image = 'bitnami/postgresql:13.2.0'; | ||||
| 			volume = `${configuration.general.deployId}-${type}-data:/bitnami/postgresql`; | ||||
| 		} else if (type === 'couchdb') { | ||||
| 			generateEnvs = { | ||||
| 				COUCHDB_PASSWORD: passwords[0], | ||||
| 				COUCHDB_USER: usernames[0] | ||||
| 			}; | ||||
| 			image = 'bitnami/couchdb:3'; | ||||
| 			volume = `${configuration.general.deployId}-${type}-data:/bitnami/couchdb`; | ||||
| 		} else if (type === 'mysql') { | ||||
| 			generateEnvs = { | ||||
| 				MYSQL_ROOT_PASSWORD: passwords[0], | ||||
| 				MYSQL_ROOT_USER: usernames[0], | ||||
| 				MYSQL_USER: usernames[1], | ||||
| 				MYSQL_PASSWORD: passwords[1], | ||||
| 				MYSQL_DATABASE: defaultDatabaseName | ||||
| 			}; | ||||
| 			image = 'bitnami/mysql:8.0'; | ||||
| 			volume = `${configuration.general.deployId}-${type}-data:/bitnami/mysql/data`; | ||||
| 		} else if (type === 'clickhouse') { | ||||
| 			image = 'yandex/clickhouse-server'; | ||||
| 			volume = `${configuration.general.deployId}-${type}-data:/var/lib/clickhouse`; | ||||
| 			ulimits = { | ||||
| 				nofile: { | ||||
| 					soft: 262144, | ||||
| 					hard: 262144 | ||||
| 				} | ||||
| 			}; | ||||
| 		} else if (type === 'redis') { | ||||
| 			image = 'bitnami/redis'; | ||||
| 			volume = `${configuration.general.deployId}-${type}-data:/bitnami/redis/data`; | ||||
| 			generateEnvs = { | ||||
| 				REDIS_PASSWORD: passwords[0] | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		const stack = { | ||||
| 			version: '3.8', | ||||
| 			services: { | ||||
| 				[configuration.general.deployId]: { | ||||
| 					image, | ||||
| 					networks: [`${docker.network}`], | ||||
| 					environment: generateEnvs, | ||||
| 					volumes: [volume], | ||||
| 					ulimits, | ||||
| 					deploy: { | ||||
| 						replicas: 1, | ||||
| 						update_config: { | ||||
| 							parallelism: 0, | ||||
| 							delay: '10s', | ||||
| 							order: 'start-first' | ||||
| 						}, | ||||
| 						rollback_config: { | ||||
| 							parallelism: 0, | ||||
| 							delay: '10s', | ||||
| 							order: 'start-first' | ||||
| 						}, | ||||
| 						labels: [ | ||||
| 							'managedBy=coolify', | ||||
| 							'type=database', | ||||
| 							'configuration=' + JSON.stringify(configuration) | ||||
| 						] | ||||
| 					} | ||||
| 				} | ||||
| 			}, | ||||
| 			networks: { | ||||
| 				[`${docker.network}`]: { | ||||
| 					external: true | ||||
| 				} | ||||
| 			}, | ||||
| 			volumes: { | ||||
| 				[`${configuration.general.deployId}-${type}-data`]: { | ||||
| 					external: true | ||||
| 				} | ||||
| 			} | ||||
| 		}; | ||||
| 		await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack)); | ||||
| 		await execShellAsync( | ||||
| 			`cat ${configuration.general.workdir}/stack.yml | docker stack deploy -c - ${configuration.general.deployId}` | ||||
| 		); | ||||
| 		return { | ||||
| 			status: 201, | ||||
| 			body: { | ||||
| 				message: 'Deployed.' | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		console.log(error); | ||||
| 		await saveServerLog(error); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -1,104 +0,0 @@ | ||||
| import mongoose from 'mongoose'; | ||||
| import Settings from '$models/Settings'; | ||||
| import User from '$models/User'; | ||||
| import bcrypt from 'bcrypt'; | ||||
| import cuid from 'cuid'; | ||||
| import jsonwebtoken from 'jsonwebtoken'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
|  | ||||
| const saltRounds = 15; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	const { email, password } = request.body; | ||||
| 	const { JWT_SIGN_KEY } = process.env; | ||||
| 	const settings = await Settings.findOne({ applicationName: 'coolify' }); | ||||
| 	const registeredUsers = await User.find().countDocuments(); | ||||
| 	const foundUser = await User.findOne({ email }); | ||||
| 	try { | ||||
| 		let uid = cuid(); | ||||
| 		if (foundUser) { | ||||
| 			if (foundUser.type === 'github') { | ||||
| 				return { | ||||
| 					status: 500, | ||||
| 					body: { | ||||
| 						error: 'Wrong password or email address.' | ||||
| 					} | ||||
| 				}; | ||||
| 			} | ||||
| 			uid = foundUser.uid; | ||||
| 			if (!(await bcrypt.compare(password, foundUser.password))) { | ||||
| 				return { | ||||
| 					status: 500, | ||||
| 					body: { | ||||
| 						error: 'Wrong password or email address.' | ||||
| 					} | ||||
| 				}; | ||||
| 			} | ||||
| 		} else { | ||||
| 			if (registeredUsers === 0) { | ||||
| 				const newUser = new User({ | ||||
| 					_id: new mongoose.Types.ObjectId(), | ||||
| 					email, | ||||
| 					uid, | ||||
| 					type: 'email', | ||||
| 					password: await bcrypt.hash(password, saltRounds) | ||||
| 				}); | ||||
| 				const defaultSettings = new Settings({ | ||||
| 					_id: new mongoose.Types.ObjectId() | ||||
| 				}); | ||||
| 				try { | ||||
| 					await newUser.save(); | ||||
| 					await defaultSettings.save(); | ||||
| 				} catch (error) { | ||||
| 					return { | ||||
| 						status: 500, | ||||
| 						error: error.message || error | ||||
| 					}; | ||||
| 				} | ||||
| 			} else { | ||||
| 				if (!settings?.allowRegistration) { | ||||
| 					return { | ||||
| 						status: 500, | ||||
| 						body: { | ||||
| 							error: 'Registration disabled, enable it in settings.' | ||||
| 						} | ||||
| 					}; | ||||
| 				} else { | ||||
| 					const newUser = new User({ | ||||
| 						_id: new mongoose.Types.ObjectId(), | ||||
| 						email, | ||||
| 						uid, | ||||
| 						type: 'email', | ||||
| 						password: await bcrypt.hash(password, saltRounds) | ||||
| 					}); | ||||
| 					try { | ||||
| 						await newUser.save(); | ||||
| 					} catch (error) { | ||||
| 						return { | ||||
| 							status: 500, | ||||
| 							error: error.message || error | ||||
| 						}; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		const coolToken = jsonwebtoken.sign({}, JWT_SIGN_KEY, { | ||||
| 			expiresIn: 15778800, | ||||
| 			algorithm: 'HS256', | ||||
| 			audience: 'coolLabs', | ||||
| 			issuer: 'coolLabs', | ||||
| 			jwtid: uid, | ||||
| 			subject: `User:${uid}`, | ||||
| 			notBefore: -1000 | ||||
| 		}); | ||||
| 		request.locals.session.data = { coolToken, ghToken: null }; | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				message: 'Successfully logged in.' | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return { status: 500, body: { error: error.message || error } }; | ||||
| 	} | ||||
| } | ||||
| @@ -1,108 +0,0 @@ | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import mongoose from 'mongoose'; | ||||
| import User from '$models/User'; | ||||
| import Settings from '$models/Settings'; | ||||
| import cuid from 'cuid'; | ||||
| import jsonwebtoken from 'jsonwebtoken'; | ||||
| import { githubAPI } from '$lib/api/github'; | ||||
|  | ||||
| export async function get(request: Request) { | ||||
| 	const code = request.query.get('code'); | ||||
| 	const { GITHUB_APP_CLIENT_SECRET, JWT_SIGN_KEY, VITE_GITHUB_APP_CLIENTID } = process.env; | ||||
| 	try { | ||||
| 		let uid = cuid(); | ||||
| 		const { access_token } = await ( | ||||
| 			await fetch( | ||||
| 				`https://github.com/login/oauth/access_token?client_id=${VITE_GITHUB_APP_CLIENTID}&client_secret=${GITHUB_APP_CLIENT_SECRET}&code=${code}`, | ||||
| 				{ headers: { accept: 'application/json' } } | ||||
| 			) | ||||
| 		).json(); | ||||
| 		const { avatar_url } = await (await githubAPI(request, '/user', access_token)).body; | ||||
| 		const email = (await githubAPI(request, '/user/emails', access_token)).body.filter( | ||||
| 			(e) => e.primary | ||||
| 		)[0].email; | ||||
| 		const settings = await Settings.findOne({ applicationName: 'coolify' }); | ||||
| 		const registeredUsers = await User.find().countDocuments(); | ||||
| 		const foundUser = await User.findOne({ email }); | ||||
| 		if (foundUser) { | ||||
| 			await User.findOneAndUpdate({ email }, { avatar: avatar_url }, { upsert: true, new: true }); | ||||
| 			uid = foundUser.uid; | ||||
| 		} else { | ||||
| 			if (registeredUsers === 0) { | ||||
| 				const newUser = new User({ | ||||
| 					_id: new mongoose.Types.ObjectId(), | ||||
| 					email, | ||||
| 					avatar: avatar_url, | ||||
| 					uid, | ||||
| 					type: 'github' | ||||
| 				}); | ||||
| 				const defaultSettings = new Settings({ | ||||
| 					_id: new mongoose.Types.ObjectId() | ||||
| 				}); | ||||
| 				try { | ||||
| 					await newUser.save(); | ||||
| 					await defaultSettings.save(); | ||||
| 				} catch (error) { | ||||
| 					return { | ||||
| 						status: 500, | ||||
| 						error: error.message || error | ||||
| 					}; | ||||
| 				} | ||||
| 			} else { | ||||
| 				if (!settings && registeredUsers > 0) { | ||||
| 					return { | ||||
| 						status: 500, | ||||
| 						body: { | ||||
| 							error: 'Registration disabled, enable it in settings.' | ||||
| 						} | ||||
| 					}; | ||||
| 				} else { | ||||
| 					if (!settings.allowRegistration) { | ||||
| 						return { | ||||
| 							status: 500, | ||||
| 							body: { | ||||
| 								error: 'You are not allowed here!' | ||||
| 							} | ||||
| 						}; | ||||
| 					} else { | ||||
| 						const newUser = new User({ | ||||
| 							_id: new mongoose.Types.ObjectId(), | ||||
| 							email, | ||||
| 							avatar: avatar_url, | ||||
| 							uid, | ||||
| 							type: 'github' | ||||
| 						}); | ||||
| 						try { | ||||
| 							await newUser.save(); | ||||
| 						} catch (error) { | ||||
| 							return { | ||||
| 								status: 500, | ||||
| 								body: { | ||||
| 									error: error.message || error | ||||
| 								} | ||||
| 							}; | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		const coolToken = jsonwebtoken.sign({}, JWT_SIGN_KEY, { | ||||
| 			expiresIn: 15778800, | ||||
| 			algorithm: 'HS256', | ||||
| 			audience: 'coolLabs', | ||||
| 			issuer: 'coolLabs', | ||||
| 			jwtid: uid, | ||||
| 			subject: `User:${uid}`, | ||||
| 			notBefore: -1000 | ||||
| 		}); | ||||
| 		request.locals.session.data = { coolToken, ghToken: access_token }; | ||||
| 		return { | ||||
| 			status: 302, | ||||
| 			headers: { | ||||
| 				location: `/success` | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return { status: 500, body: { error: error.message || error } }; | ||||
| 	} | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| export async function del(request: Request) { | ||||
| 	request.locals.session.destroy() | ||||
| 	return { | ||||
| 		body: { | ||||
| 			ok: true | ||||
| 		} | ||||
| 	}; | ||||
| } | ||||
| @@ -1,27 +0,0 @@ | ||||
| import { saveServerLog } from '$lib/api/applications/logging'; | ||||
| import { execShellAsync } from '$lib/api/common'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	try { | ||||
| 		const output = await execShellAsync('docker builder prune -af'); | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				message: 'OK', | ||||
| 				output: output | ||||
| 					.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, '') | ||||
| 					.split('\n') | ||||
| 					.pop() | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		await saveServerLog(error); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: error.message || error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -1,27 +0,0 @@ | ||||
| import { saveServerLog } from '$lib/api/applications/logging'; | ||||
| import { execShellAsync } from '$lib/api/common'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	try { | ||||
| 		const output = await execShellAsync('docker container prune -f'); | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				message: 'OK', | ||||
| 				output: output | ||||
| 					.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, '') | ||||
| 					.split('\n') | ||||
| 					.pop() | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		await saveServerLog(error); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: error.message || error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -1,27 +0,0 @@ | ||||
| import { saveServerLog } from '$lib/api/applications/logging'; | ||||
| import { execShellAsync } from '$lib/api/common'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	try { | ||||
| 		const output = await execShellAsync('docker image prune -af'); | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				message: 'OK', | ||||
| 				output: output | ||||
| 					.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, '') | ||||
| 					.split('\n') | ||||
| 					.pop() | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		await saveServerLog(error); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: error.message || error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -1,27 +0,0 @@ | ||||
| import { saveServerLog } from '$lib/api/applications/logging'; | ||||
| import { execShellAsync } from '$lib/api/common'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	try { | ||||
| 		const output = await execShellAsync('docker volume prune -f'); | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				message: 'OK', | ||||
| 				output: output | ||||
| 					.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, '') | ||||
| 					.split('\n') | ||||
| 					.pop() | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		await saveServerLog(error); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: error.message || error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -1,34 +0,0 @@ | ||||
| import { saveServerLog } from '$lib/api/applications/logging'; | ||||
| import { execShellAsync } from '$lib/api/common'; | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import systeminformation from 'systeminformation'; | ||||
|  | ||||
| export async function get(request: Request) { | ||||
| 	try { | ||||
| 		const df = await execShellAsync(`docker system df  --format '{{ json . }}'`); | ||||
| 		const dockerReclaimable = df | ||||
| 			.split('\n') | ||||
| 			.filter((n) => n) | ||||
| 			.map((s) => JSON.parse(s)); | ||||
|  | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				hostname: await (await systeminformation.osInfo()).hostname, | ||||
| 				filesystems: await ( | ||||
| 					await systeminformation.fsSize() | ||||
| 				).filter((fs) => !fs.fs.match('/dev/loop') || !fs.fs.match('/var/lib/docker/')), | ||||
| 				dockerReclaimable | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		await saveServerLog(error); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: error.message || error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -1,52 +0,0 @@ | ||||
| import { execShellAsync } from '$lib/api/common'; | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
|  | ||||
| export async function get(request: Request) { | ||||
| 	const { serviceName } = request.params; | ||||
|  | ||||
| 	try { | ||||
| 		const service = (await docker.engine.listServices()).find( | ||||
| 			(r) => | ||||
| 				r.Spec.Labels.managedBy === 'coolify' && | ||||
| 				r.Spec.Labels.type === 'service' && | ||||
| 				r.Spec.Labels.serviceName === serviceName && | ||||
| 				r.Spec.Name === `${serviceName}_${serviceName}` | ||||
| 		); | ||||
| 		if (service) { | ||||
| 			const payload = { | ||||
| 				config: JSON.parse(service.Spec.Labels.configuration) | ||||
| 			}; | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				body: { | ||||
| 					success: true, | ||||
| 					...payload | ||||
| 				} | ||||
| 			}; | ||||
| 		} else { | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				body: { | ||||
| 					success: false, | ||||
| 					showToast: false, | ||||
| 					message: 'Not found' | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 	} catch (error) { | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				success: false, | ||||
| 				error: error.message || error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| export async function del(request: Request) { | ||||
| 	const { serviceName } = request.params; | ||||
| 	await execShellAsync(`docker stack rm ${serviceName}`); | ||||
| 	return { status: 200, body: {} }; | ||||
| } | ||||
| @@ -1,74 +0,0 @@ | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import yaml from 'js-yaml'; | ||||
| import { promises as fs } from 'fs'; | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import { baseServiceConfiguration } from '$lib/api/applications/common'; | ||||
| import { cleanupTmp, execShellAsync } from '$lib/api/common'; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	let { baseURL } = request.body; | ||||
| 	const traefikURL = baseURL; | ||||
| 	baseURL = `https://${baseURL}`; | ||||
| 	const workdir = '/tmp/code-server'; | ||||
| 	const deployId = 'code-server'; | ||||
| 	// const environment = [ | ||||
| 	// 	{ name: 'DOCKER_USER', value: 'root' } | ||||
|  | ||||
| 	// ]; | ||||
| 	// const generateEnvsCodeServer = {}; | ||||
| 	// for (const env of environment) generateEnvsCodeServer[env.name] = env.value; | ||||
|  | ||||
| 	const stack = { | ||||
| 		version: '3.8', | ||||
| 		services: { | ||||
| 			[deployId]: { | ||||
| 				image: 'codercom/code-server', | ||||
| 				command: 'code-server --disable-telemetry', | ||||
| 				networks: [`${docker.network}`], | ||||
| 				volumes: [`${deployId}-code-server-data:/home/coder`], | ||||
| 				// environment: generateEnvsCodeServer, | ||||
| 				deploy: { | ||||
| 					...baseServiceConfiguration, | ||||
| 					labels: [ | ||||
| 						'managedBy=coolify', | ||||
| 						'type=service', | ||||
| 						'serviceName=code-server', | ||||
| 						'configuration=' + | ||||
| 							JSON.stringify({ | ||||
| 								baseURL | ||||
| 							}), | ||||
| 						'traefik.enable=true', | ||||
| 						'traefik.http.services.' + deployId + '.loadbalancer.server.port=8080', | ||||
| 						'traefik.http.routers.' + deployId + '.entrypoints=websecure', | ||||
| 						'traefik.http.routers.' + | ||||
| 							deployId + | ||||
| 							'.rule=Host(`' + | ||||
| 							traefikURL + | ||||
| 							'`) && PathPrefix(`/`)', | ||||
| 						'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt', | ||||
| 						'traefik.http.routers.' + deployId + '.middlewares=global-compress' | ||||
| 					] | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		networks: { | ||||
| 			[`${docker.network}`]: { | ||||
| 				external: true | ||||
| 			} | ||||
| 		}, | ||||
| 		volumes: { | ||||
| 			[`${deployId}-code-server-data`]: { | ||||
| 				external: true | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
| 	await execShellAsync(`mkdir -p ${workdir}`); | ||||
| 	await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack)); | ||||
| 	await execShellAsync('docker stack rm code-server'); | ||||
| 	await execShellAsync(`cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`); | ||||
| 	cleanupTmp(workdir); | ||||
| 	return { | ||||
| 		status: 200, | ||||
| 		body: { message: 'OK' } | ||||
| 	}; | ||||
| } | ||||
| @@ -1,27 +0,0 @@ | ||||
| import { execShellAsync } from '$lib/api/common'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import yaml from 'js-yaml'; | ||||
|  | ||||
| export async function get(request: Request) { | ||||
| 	// const { POSTGRESQL_USERNAME, POSTGRESQL_PASSWORD, POSTGRESQL_DATABASE } = JSON.parse( | ||||
| 	// 	JSON.parse( | ||||
| 	// 		await execShellAsync( | ||||
| 	// 			"docker service inspect code-server_code-server --format='{{json .Spec.Labels.configuration}}'" | ||||
| 	// 		) | ||||
| 	// 	) | ||||
| 	// ).generateEnvsPostgres; | ||||
| 	const containers = (await execShellAsync("docker ps -a --format='{{json .Names}}'")) | ||||
| 		.replace(/"/g, '') | ||||
| 		.trim() | ||||
| 		.split('\n'); | ||||
| 	const codeServer = containers.find((container) => container.startsWith('code-server')); | ||||
| 	const configYaml = yaml.load( | ||||
| 		await execShellAsync( | ||||
| 			`docker exec ${codeServer} cat /home/coder/.config/code-server/config.yaml` | ||||
| 		) | ||||
| 	); | ||||
| 	return { | ||||
| 		status: 200, | ||||
| 		body: { message: 'OK', password: configYaml.password } | ||||
| 	}; | ||||
| } | ||||
| @@ -1,82 +0,0 @@ | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import yaml from 'js-yaml'; | ||||
| import generator from 'generate-password'; | ||||
| import { promises as fs } from 'fs'; | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import { baseServiceConfiguration } from '$lib/api/applications/common'; | ||||
| import { cleanupTmp, execShellAsync } from '$lib/api/common'; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	let { baseURL } = request.body; | ||||
| 	const traefikURL = baseURL; | ||||
| 	baseURL = `https://${baseURL}`; | ||||
| 	const workdir = '/tmp/minio'; | ||||
| 	const deployId = 'minio'; | ||||
| 	const secrets = [ | ||||
| 		{ | ||||
| 			name: 'MINIO_ROOT_USER', | ||||
| 			value: generator.generate({ length: 12, numbers: true, strict: true }) | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: 'MINIO_ROOT_PASSWORD', | ||||
| 			value: generator.generate({ length: 24, numbers: true, strict: true }) | ||||
| 		} | ||||
| 	]; | ||||
| 	const generateEnvsMinIO = {}; | ||||
| 	for (const secret of secrets) generateEnvsMinIO[secret.name] = secret.value; | ||||
|  | ||||
| 	const stack = { | ||||
| 		version: '3.8', | ||||
| 		services: { | ||||
| 			[deployId]: { | ||||
| 				image: 'minio/minio', | ||||
| 				command: 'server /data', | ||||
| 				networks: [`${docker.network}`], | ||||
| 				environment: generateEnvsMinIO, | ||||
| 				volumes: [`${deployId}-minio-data:/data`], | ||||
| 				deploy: { | ||||
| 					...baseServiceConfiguration, | ||||
| 					labels: [ | ||||
| 						'managedBy=coolify', | ||||
| 						'type=service', | ||||
| 						'serviceName=minio', | ||||
| 						'configuration=' + | ||||
| 							JSON.stringify({ | ||||
| 								baseURL, | ||||
| 								generateEnvsMinIO | ||||
| 							}), | ||||
| 						'traefik.enable=true', | ||||
| 						'traefik.http.services.' + deployId + '.loadbalancer.server.port=9000', | ||||
| 						'traefik.http.routers.' + deployId + '.entrypoints=websecure', | ||||
| 						'traefik.http.routers.' + | ||||
| 							deployId + | ||||
| 							'.rule=Host(`' + | ||||
| 							traefikURL + | ||||
| 							'`) && PathPrefix(`/`)', | ||||
| 						'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt', | ||||
| 						'traefik.http.routers.' + deployId + '.middlewares=global-compress' | ||||
| 					] | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		networks: { | ||||
| 			[`${docker.network}`]: { | ||||
| 				external: true | ||||
| 			} | ||||
| 		}, | ||||
| 		volumes: { | ||||
| 			[`${deployId}-minio-data`]: { | ||||
| 				external: true | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
| 	await execShellAsync(`mkdir -p ${workdir}`); | ||||
| 	await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack)); | ||||
| 	await execShellAsync('docker stack rm minio'); | ||||
| 	await execShellAsync(`cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`); | ||||
| 	cleanupTmp(workdir); | ||||
| 	return { | ||||
| 		status: 200, | ||||
| 		body: { message: 'OK' } | ||||
| 	}; | ||||
| } | ||||
| @@ -1,59 +0,0 @@ | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import yaml from 'js-yaml'; | ||||
| import { promises as fs } from 'fs'; | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import { baseServiceConfiguration } from '$lib/api/applications/common'; | ||||
| import { cleanupTmp, execShellAsync } from '$lib/api/common'; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	let { baseURL } = request.body; | ||||
| 	const traefikURL = baseURL; | ||||
| 	baseURL = `https://${baseURL}`; | ||||
| 	const workdir = '/tmp/nocodb'; | ||||
| 	const deployId = 'nocodb'; | ||||
| 	const stack = { | ||||
| 		version: '3.8', | ||||
| 		services: { | ||||
| 			[deployId]: { | ||||
| 				image: 'nocodb/nocodb', | ||||
| 				networks: [`${docker.network}`], | ||||
| 				deploy: { | ||||
| 					...baseServiceConfiguration, | ||||
| 					labels: [ | ||||
| 						'managedBy=coolify', | ||||
| 						'type=service', | ||||
| 						'serviceName=nocodb', | ||||
| 						'configuration=' + | ||||
| 							JSON.stringify({ | ||||
| 								baseURL | ||||
| 							}), | ||||
| 						'traefik.enable=true', | ||||
| 						'traefik.http.services.' + deployId + '.loadbalancer.server.port=8080', | ||||
| 						'traefik.http.routers.' + deployId + '.entrypoints=websecure', | ||||
| 						'traefik.http.routers.' + | ||||
| 							deployId + | ||||
| 							'.rule=Host(`' + | ||||
| 							traefikURL + | ||||
| 							'`) && PathPrefix(`/`)', | ||||
| 						'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt', | ||||
| 						'traefik.http.routers.' + deployId + '.middlewares=global-compress' | ||||
| 					] | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		networks: { | ||||
| 			[`${docker.network}`]: { | ||||
| 				external: true | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
| 	await execShellAsync(`mkdir -p ${workdir}`); | ||||
| 	await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack)); | ||||
| 	await execShellAsync('docker stack rm nocodb'); | ||||
| 	await execShellAsync(`cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`); | ||||
| 	cleanupTmp(workdir); | ||||
| 	return { | ||||
| 		status: 200, | ||||
| 		body: { message: 'OK' } | ||||
| 	}; | ||||
| } | ||||
| @@ -1,24 +0,0 @@ | ||||
| import { execShellAsync } from '$lib/api/common'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
|  | ||||
| export async function patch(request: Request) { | ||||
| 	const { POSTGRESQL_USERNAME, POSTGRESQL_PASSWORD, POSTGRESQL_DATABASE } = JSON.parse( | ||||
| 		JSON.parse( | ||||
| 			await execShellAsync( | ||||
| 				"docker service inspect plausible_plausible --format='{{json .Spec.Labels.configuration}}'" | ||||
| 			) | ||||
| 		) | ||||
| 	).generateEnvsPostgres; | ||||
| 	const containers = (await execShellAsync("docker ps -a --format='{{json .Names}}'")) | ||||
| 		.replace(/"/g, '') | ||||
| 		.trim() | ||||
| 		.split('\n'); | ||||
| 	const postgresDB = containers.find((container) => container.startsWith('plausible_plausible_db')); | ||||
| 	await execShellAsync( | ||||
| 		`docker exec ${postgresDB} psql -H postgresql://${POSTGRESQL_USERNAME}:${POSTGRESQL_PASSWORD}@localhost:5432/${POSTGRESQL_DATABASE} -c "UPDATE users SET email_verified = true;"` | ||||
| 	); | ||||
| 	return { | ||||
| 		status: 200, | ||||
| 		body: { message: 'OK' } | ||||
| 	}; | ||||
| } | ||||
| @@ -1,187 +0,0 @@ | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import generator from 'generate-password'; | ||||
| import { promises as fs } from 'fs'; | ||||
| import yaml from 'js-yaml'; | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import { baseServiceConfiguration } from '$lib/api/applications/common'; | ||||
| import { cleanupTmp, execShellAsync } from '$lib/api/common'; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	const { email, userName, userPassword } = request.body; | ||||
| 	let { baseURL } = request.body; | ||||
| 	const traefikURL = baseURL; | ||||
| 	baseURL = `https://${baseURL}`; | ||||
| 	const deployId = 'plausible'; | ||||
| 	const workdir = '/tmp/plausible'; | ||||
| 	const secretKey = generator.generate({ length: 64, numbers: true, strict: true }); | ||||
| 	const generateEnvsPostgres = { | ||||
| 		POSTGRESQL_PASSWORD: generator.generate({ length: 24, numbers: true, strict: true }), | ||||
| 		POSTGRESQL_USERNAME: generator.generate({ length: 10, numbers: true, strict: true }), | ||||
| 		POSTGRESQL_DATABASE: 'plausible' | ||||
| 	}; | ||||
|  | ||||
| 	const secrets = [ | ||||
| 		{ name: 'ADMIN_USER_EMAIL', value: email }, | ||||
| 		{ name: 'ADMIN_USER_NAME', value: userName }, | ||||
| 		{ name: 'ADMIN_USER_PWD', value: userPassword }, | ||||
| 		{ name: 'BASE_URL', value: baseURL }, | ||||
| 		{ name: 'SECRET_KEY_BASE', value: secretKey }, | ||||
| 		{ name: 'DISABLE_AUTH', value: 'false' }, | ||||
| 		{ name: 'DISABLE_REGISTRATION', value: 'true' }, | ||||
| 		{ | ||||
| 			name: 'DATABASE_URL', | ||||
| 			value: `postgresql://${generateEnvsPostgres.POSTGRESQL_USERNAME}:${generateEnvsPostgres.POSTGRESQL_PASSWORD}@plausible_db:5432/${generateEnvsPostgres.POSTGRESQL_DATABASE}` | ||||
| 		}, | ||||
| 		{ name: 'CLICKHOUSE_DATABASE_URL', value: 'http://plausible_events_db:8123/plausible' } | ||||
| 	]; | ||||
|  | ||||
| 	const generateEnvsClickhouse = {}; | ||||
| 	for (const secret of secrets) generateEnvsClickhouse[secret.name] = secret.value; | ||||
|  | ||||
| 	const clickhouseConfigXml = ` | ||||
|       <yandex> | ||||
|         <logger> | ||||
|             <level>warning</level> | ||||
|             <console>true</console> | ||||
|         </logger> | ||||
|    | ||||
|         <!-- Stop all the unnecessary logging --> | ||||
|         <query_thread_log remove="remove"/> | ||||
|         <query_log remove="remove"/> | ||||
|         <text_log remove="remove"/> | ||||
|         <trace_log remove="remove"/> | ||||
|         <metric_log remove="remove"/> | ||||
|         <asynchronous_metric_log remove="remove"/> | ||||
|     </yandex>`; | ||||
| 	const clickhouseUserConfigXml = ` | ||||
|       <yandex> | ||||
|         <profiles> | ||||
|             <default> | ||||
|                 <log_queries>0</log_queries> | ||||
|                 <log_query_threads>0</log_query_threads> | ||||
|             </default> | ||||
|         </profiles> | ||||
|     </yandex>`; | ||||
|  | ||||
| 	const clickhouseConfigs = [ | ||||
| 		{ | ||||
| 			source: 'plausible-clickhouse-user-config.xml', | ||||
| 			target: '/etc/clickhouse-server/users.d/logging.xml' | ||||
| 		}, | ||||
| 		{ | ||||
| 			source: 'plausible-clickhouse-config.xml', | ||||
| 			target: '/etc/clickhouse-server/config.d/logging.xml' | ||||
| 		}, | ||||
| 		{ source: 'plausible-init.query', target: '/docker-entrypoint-initdb.d/init.query' }, | ||||
| 		{ source: 'plausible-init-db.sh', target: '/docker-entrypoint-initdb.d/init-db.sh' } | ||||
| 	]; | ||||
|  | ||||
| 	const initQuery = 'CREATE DATABASE IF NOT EXISTS plausible;'; | ||||
| 	const initScript = 'clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query'; | ||||
| 	await execShellAsync(`mkdir -p ${workdir}`); | ||||
| 	await fs.writeFile(`${workdir}/clickhouse-config.xml`, clickhouseConfigXml); | ||||
| 	await fs.writeFile(`${workdir}/clickhouse-user-config.xml`, clickhouseUserConfigXml); | ||||
| 	await fs.writeFile(`${workdir}/init.query`, initQuery); | ||||
| 	await fs.writeFile(`${workdir}/init-db.sh`, initScript); | ||||
| 	const stack = { | ||||
| 		version: '3.8', | ||||
| 		services: { | ||||
| 			[deployId]: { | ||||
| 				image: 'plausible/analytics:latest', | ||||
| 				command: | ||||
| 					'sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run"', | ||||
| 				networks: [`${docker.network}`], | ||||
| 				volumes: [`${deployId}-postgres-data:/var/lib/postgresql/data`], | ||||
| 				environment: generateEnvsClickhouse, | ||||
| 				deploy: { | ||||
| 					...baseServiceConfiguration, | ||||
| 					labels: [ | ||||
| 						'managedBy=coolify', | ||||
| 						'type=service', | ||||
| 						'serviceName=plausible', | ||||
| 						'configuration=' + | ||||
| 							JSON.stringify({ | ||||
| 								email, | ||||
| 								userName, | ||||
| 								userPassword, | ||||
| 								baseURL, | ||||
| 								secretKey, | ||||
| 								generateEnvsPostgres, | ||||
| 								generateEnvsClickhouse | ||||
| 							}), | ||||
| 						'traefik.enable=true', | ||||
| 						'traefik.http.services.' + deployId + '.loadbalancer.server.port=8000', | ||||
| 						'traefik.http.routers.' + deployId + '.entrypoints=websecure', | ||||
| 						'traefik.http.routers.' + | ||||
| 							deployId + | ||||
| 							'.rule=Host(`' + | ||||
| 							traefikURL + | ||||
| 							'`) && PathPrefix(`/`)', | ||||
| 						'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt', | ||||
| 						'traefik.http.routers.' + deployId + '.middlewares=global-compress' | ||||
| 					] | ||||
| 				} | ||||
| 			}, | ||||
| 			plausible_db: { | ||||
| 				image: 'bitnami/postgresql:13.2.0', | ||||
| 				networks: [`${docker.network}`], | ||||
| 				environment: generateEnvsPostgres, | ||||
| 				deploy: { | ||||
| 					...baseServiceConfiguration, | ||||
| 					labels: ['managedBy=coolify', 'type=service', 'serviceName=plausible'] | ||||
| 				} | ||||
| 			}, | ||||
| 			plausible_events_db: { | ||||
| 				image: 'yandex/clickhouse-server:21.3.2.5', | ||||
| 				networks: [`${docker.network}`], | ||||
| 				volumes: [`${deployId}-clickhouse-data:/var/lib/clickhouse`], | ||||
| 				ulimits: { | ||||
| 					nofile: { | ||||
| 						soft: 262144, | ||||
| 						hard: 262144 | ||||
| 					} | ||||
| 				}, | ||||
| 				configs: [...clickhouseConfigs], | ||||
| 				deploy: { | ||||
| 					...baseServiceConfiguration, | ||||
| 					labels: ['managedBy=coolify', 'type=service', 'serviceName=plausible'] | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		networks: { | ||||
| 			[`${docker.network}`]: { | ||||
| 				external: true | ||||
| 			} | ||||
| 		}, | ||||
| 		volumes: { | ||||
| 			[`${deployId}-clickhouse-data`]: { | ||||
| 				external: true | ||||
| 			}, | ||||
| 			[`${deployId}-postgres-data`]: { | ||||
| 				external: true | ||||
| 			} | ||||
| 		}, | ||||
| 		configs: { | ||||
| 			'plausible-clickhouse-user-config.xml': { | ||||
| 				file: `${workdir}/clickhouse-user-config.xml` | ||||
| 			}, | ||||
| 			'plausible-clickhouse-config.xml': { | ||||
| 				file: `${workdir}/clickhouse-config.xml` | ||||
| 			}, | ||||
| 			'plausible-init.query': { | ||||
| 				file: `${workdir}/init.query` | ||||
| 			}, | ||||
| 			'plausible-init-db.sh': { | ||||
| 				file: `${workdir}/init-db.sh` | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
| 	await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack)); | ||||
| 	await execShellAsync('docker stack rm plausible'); | ||||
| 	await execShellAsync(`cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`); | ||||
| 	cleanupTmp(workdir); | ||||
| 	return { | ||||
| 		status: 200, | ||||
| 		body: { message: 'OK' } | ||||
| 	}; | ||||
| } | ||||
| @@ -1,170 +0,0 @@ | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import yaml from 'js-yaml'; | ||||
| import generator from 'generate-password'; | ||||
| import { promises as fs } from 'fs'; | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import { baseServiceConfiguration } from '$lib/api/applications/common'; | ||||
| import { cleanupTmp, execShellAsync } from '$lib/api/common'; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	let { baseURL, remoteDB, database, wordpressExtraConfiguration } = request.body; | ||||
| 	const traefikURL = baseURL; | ||||
| 	baseURL = `https://${baseURL}`; | ||||
| 	const workdir = '/tmp/wordpress'; | ||||
| 	const deployId = `wp-${generator.generate({ length: 5, numbers: true, strict: true })}`; | ||||
| 	const defaultDatabaseName = generator.generate({ length: 12, numbers: true, strict: true }); | ||||
| 	const defaultDatabaseHost = `${deployId}-mysql`; | ||||
| 	const defaultDatabaseUser = generator.generate({ length: 12, numbers: true, strict: true }); | ||||
| 	const defaultDatabasePassword = generator.generate({ length: 24, numbers: true, strict: true }); | ||||
| 	const defaultDatabaseRootPassword = generator.generate({ | ||||
| 		length: 24, | ||||
| 		numbers: true, | ||||
| 		strict: true | ||||
| 	}); | ||||
| 	const defaultDatabaseRootUser = generator.generate({ length: 12, numbers: true, strict: true }); | ||||
| 	let secrets = [ | ||||
| 		{ name: 'WORDPRESS_DB_HOST', value: defaultDatabaseHost }, | ||||
| 		{ name: 'WORDPRESS_DB_USER', value: defaultDatabaseUser }, | ||||
| 		{ name: 'WORDPRESS_DB_PASSWORD', value: defaultDatabasePassword }, | ||||
| 		{ name: 'WORDPRESS_DB_NAME', value: defaultDatabaseName }, | ||||
| 		{ name: 'WORDPRESS_CONFIG_EXTRA', value: wordpressExtraConfiguration } | ||||
| 	]; | ||||
|  | ||||
| 	const generateEnvsMySQL = { | ||||
| 		MYSQL_ROOT_PASSWORD: defaultDatabaseRootPassword, | ||||
| 		MYSQL_ROOT_USER: defaultDatabaseRootUser, | ||||
| 		MYSQL_USER: defaultDatabaseUser, | ||||
| 		MYSQL_PASSWORD: defaultDatabasePassword, | ||||
| 		MYSQL_DATABASE: defaultDatabaseName | ||||
| 	}; | ||||
| 	const image = 'bitnami/mysql:8.0'; | ||||
| 	const volume = `${deployId}-mysql-data:/bitnami/mysql/data`; | ||||
|  | ||||
| 	if (remoteDB) { | ||||
| 		secrets = [ | ||||
| 			{ name: 'WORDPRESS_DB_HOST', value: database.host }, | ||||
| 			{ name: 'WORDPRESS_DB_USER', value: database.user }, | ||||
| 			{ name: 'WORDPRESS_DB_PASSWORD', value: database.password }, | ||||
| 			{ name: 'WORDPRESS_DB_NAME', value: database.name }, | ||||
| 			{ name: 'WORDPRESS_TABLE_PREFIX', value: database.tablePrefix }, | ||||
| 			{ name: 'WORDPRESS_CONFIG_EXTRA', value: wordpressExtraConfiguration } | ||||
| 		]; | ||||
| 	} | ||||
|  | ||||
| 	const generateEnvsWordpress = {}; | ||||
| 	for (const secret of secrets) generateEnvsWordpress[secret.name] = secret.value; | ||||
| 	let stack = { | ||||
| 		version: '3.8', | ||||
| 		services: { | ||||
| 			[deployId]: { | ||||
| 				image: 'wordpress', | ||||
| 				networks: [`${docker.network}`], | ||||
| 				environment: generateEnvsWordpress, | ||||
| 				volumes: [`${deployId}-wordpress-data:/var/www/html`], | ||||
| 				deploy: { | ||||
| 					...baseServiceConfiguration, | ||||
| 					labels: [ | ||||
| 						'managedBy=coolify', | ||||
| 						'type=service', | ||||
| 						'serviceName=' + deployId, | ||||
| 						'configuration=' + | ||||
| 							JSON.stringify({ | ||||
| 								deployId, | ||||
| 								baseURL, | ||||
| 								generateEnvsWordpress | ||||
| 							}), | ||||
| 						'traefik.enable=true', | ||||
| 						'traefik.http.services.' + deployId + '.loadbalancer.server.port=80', | ||||
| 						'traefik.http.routers.' + deployId + '.entrypoints=websecure', | ||||
| 						'traefik.http.routers.' + | ||||
| 							deployId + | ||||
| 							'.rule=Host(`' + | ||||
| 							traefikURL + | ||||
| 							'`) && PathPrefix(`/`)', | ||||
| 						'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt', | ||||
| 						'traefik.http.routers.' + deployId + '.middlewares=global-compress' | ||||
| 					] | ||||
| 				} | ||||
| 			}, | ||||
| 			[`${deployId}-mysql`]: { | ||||
| 				image, | ||||
| 				networks: [`${docker.network}`], | ||||
| 				environment: generateEnvsMySQL, | ||||
| 				volumes: [volume], | ||||
| 				deploy: { | ||||
| 					...baseServiceConfiguration, | ||||
| 					labels: ['managedBy=coolify', 'type=service', 'serviceName=' + deployId] | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		networks: { | ||||
| 			[`${docker.network}`]: { | ||||
| 				external: true | ||||
| 			} | ||||
| 		}, | ||||
| 		volumes: { | ||||
| 			[`${deployId}-wordpress-data`]: { | ||||
| 				external: true | ||||
| 			}, | ||||
| 			[`${deployId}-mysql-data`]: { | ||||
| 				external: true | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
| 	if (remoteDB) { | ||||
| 		stack = { | ||||
| 			version: '3.8', | ||||
| 			services: { | ||||
| 				[deployId]: { | ||||
| 					image: 'wordpress', | ||||
| 					networks: [`${docker.network}`], | ||||
| 					environment: generateEnvsWordpress, | ||||
| 					volumes: [`${deployId}-wordpress-data:/var/www/html`], | ||||
| 					deploy: { | ||||
| 						...baseServiceConfiguration, | ||||
| 						labels: [ | ||||
| 							'managedBy=coolify', | ||||
| 							'type=service', | ||||
| 							'serviceName=' + deployId, | ||||
| 							'configuration=' + | ||||
| 								JSON.stringify({ | ||||
| 									deployId, | ||||
| 									baseURL, | ||||
| 									generateEnvsWordpress | ||||
| 								}), | ||||
| 							'traefik.enable=true', | ||||
| 							'traefik.http.services.' + deployId + '.loadbalancer.server.port=80', | ||||
| 							'traefik.http.routers.' + deployId + '.entrypoints=websecure', | ||||
| 							'traefik.http.routers.' + | ||||
| 								deployId + | ||||
| 								'.rule=Host(`' + | ||||
| 								traefikURL + | ||||
| 								'`) && PathPrefix(`/`)', | ||||
| 							'traefik.http.routers.' + deployId + '.tls.certresolver=letsencrypt', | ||||
| 							'traefik.http.routers.' + deployId + '.middlewares=global-compress' | ||||
| 						] | ||||
| 					} | ||||
| 				} | ||||
| 			}, | ||||
| 			networks: { | ||||
| 				[`${docker.network}`]: { | ||||
| 					external: true | ||||
| 				} | ||||
| 			}, | ||||
| 			volumes: { | ||||
| 				[`${deployId}-wordpress-data`]: { | ||||
| 					external: true | ||||
| 				} | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| 	await execShellAsync(`mkdir -p ${workdir}`); | ||||
| 	await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack)); | ||||
| 	await execShellAsync(`docker stack rm ${deployId}`); | ||||
| 	await execShellAsync(`cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`); | ||||
| 	cleanupTmp(workdir); | ||||
| 	return { | ||||
| 		status: 200, | ||||
| 		body: { message: 'OK' } | ||||
| 	}; | ||||
| } | ||||
| @@ -1,52 +0,0 @@ | ||||
| import { saveServerLog } from '$lib/api/applications/logging'; | ||||
| import Settings from '$models/Settings'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| const applicationName = 'coolify'; | ||||
|  | ||||
| export async function get(request: Request) { | ||||
| 	try { | ||||
| 		const settings = await Settings.findOne({ applicationName }).select('-_id -__v'); | ||||
| 		const payload = { | ||||
| 			applicationName, | ||||
| 			allowRegistration: false, | ||||
| 			...settings._doc | ||||
| 		}; | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				...payload | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		await saveServerLog(error); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: error.message || error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| export async function post(request: Request) { | ||||
| 	try { | ||||
| 		const settings = await Settings.findOneAndUpdate( | ||||
| 			{ applicationName }, | ||||
| 			{ applicationName, ...request.body }, | ||||
| 			{ upsert: true, new: true } | ||||
| 		).select('-_id -__v'); | ||||
| 		return { | ||||
| 			status: 201, | ||||
| 			body: { | ||||
| 				...settings._doc | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		await saveServerLog(error); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: error.message || error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| import { saveServerLog } from '$lib/api/applications/logging'; | ||||
| import { execShellAsync } from '$lib/api/common'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
|  | ||||
| export async function get(request: Request) { | ||||
| 	const upgradeP1 = await execShellAsync( | ||||
| 		'bash -c "$(curl -fsSL https://get.coollabs.io/coolify/upgrade-p1.sh)"' | ||||
| 	); | ||||
| 	await saveServerLog({ message: upgradeP1, type: 'UPGRADE-P-1' }); | ||||
| 	execShellAsync( | ||||
| 		'docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -u root coolify bash -c "$(curl -fsSL https://get.coollabs.io/coolify/upgrade-p2.sh)"' | ||||
| 	); | ||||
| 	return { | ||||
| 		status: 200, | ||||
| 		body: { | ||||
| 			message: "I'm trying, okay?" | ||||
| 		} | ||||
| 	}; | ||||
| } | ||||
| @@ -1,230 +0,0 @@ | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import crypto from 'crypto'; | ||||
| import Deployment from '$models/Deployment'; | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import { precheckDeployment, setDefaultConfiguration } from '$lib/api/applications/configuration'; | ||||
| import cloneRepository from '$lib/api/applications/cloneRepository'; | ||||
| import { cleanupTmp, execShellAsync } from '$lib/api/common'; | ||||
| import queueAndBuild from '$lib/api/applications/queueAndBuild'; | ||||
| import Configuration from '$models/Configuration'; | ||||
| import ApplicationLog from '$models/ApplicationLog'; | ||||
| import { cleanupStuckedDeploymentsInDB } from '$lib/api/applications/cleanup'; | ||||
| export async function post(request: Request) { | ||||
| 	let configuration; | ||||
| 	const allowedGithubEvents = ['push', 'pull_request']; | ||||
| 	const allowedPRActions = ['opened', 'reopened', 'synchronize', 'closed']; | ||||
| 	const githubEvent = request.headers['x-github-event']; | ||||
| 	const { GITHUP_APP_WEBHOOK_SECRET } = process.env; | ||||
| 	const hmac = crypto.createHmac('sha256', GITHUP_APP_WEBHOOK_SECRET); | ||||
| 	const digest = Buffer.from( | ||||
| 		'sha256=' + hmac.update(JSON.stringify(request.body)).digest('hex'), | ||||
| 		'utf8' | ||||
| 	); | ||||
| 	const checksum = Buffer.from(request.headers['x-hub-signature-256'], 'utf8'); | ||||
| 	if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) { | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: 'Invalid request.' | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	if (!allowedGithubEvents.includes(githubEvent)) { | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: 'Event not allowed.' | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	// TODO: Monorepo support here. Find all configurations by id and update all deployments! Tough!  | ||||
| 	try { | ||||
| 		const applications = await Configuration.find({ | ||||
| 			'repository.id': request.body.repository.id | ||||
| 		}).select('-_id -__v -createdAt -updatedAt'); | ||||
| 		if (githubEvent === 'push') { | ||||
| 			configuration = applications.find((r) => { | ||||
| 				if (request.body.ref.startsWith('refs')) { | ||||
| 					if (r.repository.branch === request.body.ref.split('/')[2]) { | ||||
| 						return r; | ||||
| 					} | ||||
| 				} | ||||
| 				return null; | ||||
| 			}); | ||||
| 		} else if (githubEvent === 'pull_request') { | ||||
| 			if (!allowedPRActions.includes(request.body.action)) { | ||||
| 				return { | ||||
| 					status: 500, | ||||
| 					body: { | ||||
| 						error: 'PR action is not allowed.' | ||||
| 					} | ||||
| 				}; | ||||
| 			} | ||||
| 			configuration = applications.find( | ||||
| 				(r) => r.repository.branch === request.body['pull_request'].base.ref | ||||
| 			); | ||||
| 			if (configuration) { | ||||
| 				if (!configuration.general.isPreviewDeploymentEnabled) { | ||||
| 					return { | ||||
| 						status: 500, | ||||
| 						body: { | ||||
| 							error: 'PR deployments are not enabled.' | ||||
| 						} | ||||
| 					}; | ||||
| 				} | ||||
| 				configuration.general.pullRequest = request.body.number; | ||||
| 			} | ||||
| 		} | ||||
| 		if (!configuration) { | ||||
| 			return { | ||||
| 				status: 500, | ||||
| 				body: { | ||||
| 					error: 'No configuration found.' | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 		configuration = setDefaultConfiguration(configuration); | ||||
| 		const { id, organization, name, branch } = configuration.repository; | ||||
| 		const { domain } = configuration.publish; | ||||
| 		const { deployId, nickname, pullRequest } = configuration.general; | ||||
|  | ||||
| 		if (request.body.action === 'closed') { | ||||
| 			const deploys = await Deployment.find({ organization, branch, name, domain }); | ||||
| 			for (const deploy of deploys) { | ||||
| 				await ApplicationLog.deleteMany({ deployId: deploy.deployId }); | ||||
| 				await Deployment.deleteMany({ deployId: deploy.deployId }); | ||||
| 			} | ||||
| 			await Configuration.findOneAndRemove({ | ||||
| 				'repository.id': id, | ||||
| 				'repository.organization': organization, | ||||
| 				'repository.name': name, | ||||
| 				'repository.branch': branch, | ||||
| 				'general.pullRequest': pullRequest | ||||
| 			}); | ||||
| 			await execShellAsync(`docker stack rm ${configuration.build.container.name}`); | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				body: { | ||||
| 					success: true, | ||||
| 					message: 'Removed' | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 		await cloneRepository(configuration); | ||||
| 		const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment( | ||||
| 			configuration | ||||
| 		); | ||||
| 		if (foundService && !forceUpdate && !imageChanged && !configChanged) { | ||||
| 			cleanupTmp(configuration.general.workdir); | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				body: { | ||||
| 					success: false, | ||||
| 					message: 'Nothing changed, no need to redeploy.' | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 		const alreadyQueued = await Deployment.find({ | ||||
| 			repoId: id, | ||||
| 			branch: branch, | ||||
| 			organization: organization, | ||||
| 			name: name, | ||||
| 			domain: domain, | ||||
| 			progress: { $in: ['queued', 'inprogress'] } | ||||
| 		}); | ||||
| 		if (alreadyQueued.length > 0) { | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				body: { | ||||
| 					success: false, | ||||
| 					message: 'Already in the queue.' | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		await new Deployment({ | ||||
| 			repoId: id, | ||||
| 			branch, | ||||
| 			deployId, | ||||
| 			domain, | ||||
| 			organization, | ||||
| 			name, | ||||
| 			nickname | ||||
| 		}).save(); | ||||
|  | ||||
| 		if (githubEvent === 'pull_request') { | ||||
| 			await Configuration.findOneAndUpdate( | ||||
| 				{ | ||||
| 					'repository.id': id, | ||||
| 					'repository.organization': organization, | ||||
| 					'repository.name': name, | ||||
| 					'repository.branch': branch, | ||||
| 					'general.pullRequest': pullRequest | ||||
| 				}, | ||||
| 				{ ...configuration }, | ||||
| 				{ upsert: true, new: true } | ||||
| 			); | ||||
| 		} else { | ||||
| 			await Configuration.findOneAndUpdate( | ||||
| 				{ | ||||
| 					'repository.id': id, | ||||
| 					'repository.organization': organization, | ||||
| 					'repository.name': name, | ||||
| 					'repository.branch': branch, | ||||
| 					'general.pullRequest': { $in: [null, 0] } | ||||
| 				}, | ||||
| 				{ ...configuration }, | ||||
| 				{ upsert: true, new: true } | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		queueAndBuild(configuration, imageChanged); | ||||
| 		return { | ||||
| 			status: 201, | ||||
| 			body: { | ||||
| 				message: 'Deployment queued.', | ||||
| 				nickname: configuration.general.nickname, | ||||
| 				name: configuration.build.container.name, | ||||
| 				deployId: configuration.general.deployId | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		console.log(error); | ||||
| 		// console.log(configuration) | ||||
| 		if (configuration) { | ||||
| 			cleanupTmp(configuration.general.workdir); | ||||
| 			await Deployment.findOneAndUpdate( | ||||
| 				{ | ||||
| 					repoId: configuration.repository.id, | ||||
| 					branch: configuration.repository.branch, | ||||
| 					organization: configuration.repository.organization, | ||||
| 					name: configuration.repository.name, | ||||
| 					domain: configuration.publish.domain | ||||
| 				}, | ||||
| 				{ | ||||
| 					repoId: configuration.repository.id, | ||||
| 					branch: configuration.repository.branch, | ||||
| 					organization: configuration.repository.organization, | ||||
| 					name: configuration.repository.name, | ||||
| 					domain: configuration.publish.domain, | ||||
| 					progress: 'failed' | ||||
| 				} | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: error.message || error | ||||
| 			} | ||||
| 		}; | ||||
| 	} finally { | ||||
| 		try { | ||||
| 			await cleanupStuckedDeploymentsInDB(); | ||||
| 		} catch (error) { | ||||
| 			console.log(error); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,6 +0,0 @@ | ||||
| <script> | ||||
| 	import Configuration from '$components/Application/Configuration.svelte'; | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <Configuration /> | ||||
| @@ -1,83 +0,0 @@ | ||||
| <script> | ||||
| 	import { onDestroy, onMount } from 'svelte'; | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| 	import Loading from '$components/Loading.svelte'; | ||||
| 	import { request } from '$lib/request'; | ||||
| 	import { page, session } from '$app/stores'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { browser } from '$app/env'; | ||||
| 	import { application } from '$store'; | ||||
|  | ||||
| 	let loadLogsInterval; | ||||
| 	let logs = []; | ||||
|  | ||||
| 	onMount(() => { | ||||
| 		loadLogsInterval = setInterval(() => { | ||||
| 			loadLogs(); | ||||
| 		}, 500); | ||||
| 	}); | ||||
|  | ||||
| 	async function loadLogs() { | ||||
| 		try { | ||||
| 			const { events, progress } = await request( | ||||
| 				`/api/v1/application/deploy/logs/${$page.params.deployId}`, | ||||
| 				$session | ||||
| 			); | ||||
| 			logs = [...events]; | ||||
| 			if (progress === 'done' || progress === 'failed') { | ||||
| 				clearInterval(loadLogsInterval); | ||||
| 			} | ||||
| 		} catch (error) { | ||||
| 			browser && goto('/dashboard/applications', { replaceState: true }); | ||||
| 		} | ||||
| 	} | ||||
| 	onDestroy(() => { | ||||
| 		clearInterval(loadLogsInterval); | ||||
| 	}); | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <div | ||||
| 	class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center" | ||||
| 	in:fade={{ duration: 100 }} | ||||
| > | ||||
| 	<div>Deployment log</div> | ||||
| 	<a | ||||
| 		target="_blank" | ||||
| 		class="icon mx-2" | ||||
| 		href={'https://' + $application.publish.domain + $application.publish.path} | ||||
| 	> | ||||
| 		<svg | ||||
| 			xmlns="http://www.w3.org/2000/svg" | ||||
| 			class="h-6 w-6" | ||||
| 			fill="none" | ||||
| 			viewBox="0 0 24 24" | ||||
| 			stroke="currentColor" | ||||
| 		> | ||||
| 			<path | ||||
| 				stroke-linecap="round" | ||||
| 				stroke-linejoin="round" | ||||
| 				stroke-width="2" | ||||
| 				d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" | ||||
| 			/> | ||||
| 		</svg></a | ||||
| 	> | ||||
| </div> | ||||
| {#await loadLogs()} | ||||
| 	<Loading /> | ||||
| {:then} | ||||
| 	<div class="text-center px-6" in:fade={{ duration: 100 }}> | ||||
| 		<div in:fade={{ duration: 100 }}> | ||||
| 			<pre | ||||
| 				class="leading-4 text-left text-sm font-semibold tracking-tighter rounded-lg bg-black p-6 whitespace-pre-wrap"> | ||||
|       {#if logs.length > 0} | ||||
|         {#each logs as log} | ||||
|           {log + '\n'} | ||||
|         {/each} | ||||
|       {:else} | ||||
|         It's starting soon. | ||||
|       {/if} | ||||
|     </pre> | ||||
| 		</div> | ||||
| 	</div> | ||||
| {/await} | ||||
| @@ -1,137 +0,0 @@ | ||||
| <script> | ||||
| 	import { application, dateOptions } from '$store'; | ||||
| 	import { fade } from 'svelte/transition'; | ||||
|  | ||||
| 	import { onDestroy, onMount } from 'svelte'; | ||||
|  | ||||
| 	import Loading from '$components/Loading.svelte'; | ||||
| 	import { request } from '$lib/request'; | ||||
| 	import { session } from '$app/stores'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
|  | ||||
| 	let loadDeploymentsInterval = null; | ||||
| 	let loadLogsInterval = null; | ||||
| 	let deployments = []; | ||||
| 	let logs = []; | ||||
| 	let page = 1; | ||||
|  | ||||
| 	onMount(async () => { | ||||
| 		loadApplicationLogs(); | ||||
| 		loadLogsInterval = setInterval(() => { | ||||
| 			loadApplicationLogs(); | ||||
| 		}, 3000); | ||||
| 		loadDeploymentsInterval = setInterval(() => { | ||||
| 			loadDeploymentLogs(); | ||||
| 		}, 1000); | ||||
| 	}); | ||||
| 	onDestroy(() => { | ||||
| 		clearInterval(loadDeploymentsInterval); | ||||
| 		clearInterval(loadLogsInterval); | ||||
| 	}); | ||||
| 	async function loadMoreDeploymentLogs() { | ||||
| 		page = page + 1; | ||||
| 		await loadDeploymentLogs(); | ||||
| 	} | ||||
| 	async function loadDeploymentLogs() { | ||||
| 		deployments = ( | ||||
| 			await request( | ||||
| 				`/api/v1/application/deploy/logs?repoId=${$application.repository.id}&branch=${$application.repository.branch}&page=${page}`, | ||||
| 				$session | ||||
| 			) | ||||
| 		).logs; | ||||
| 	} | ||||
| 	async function loadApplicationLogs() { | ||||
| 		logs = ( | ||||
| 			await request(`/api/v1/application/logs?name=${$application.build.container.name}`, $session) | ||||
| 		).logs; | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <div | ||||
| 	class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center" | ||||
| 	in:fade={{ duration: 100 }} | ||||
| > | ||||
| 	<div>Logs</div> | ||||
| </div> | ||||
| {#await loadDeploymentLogs()} | ||||
| 	<Loading /> | ||||
| {:then} | ||||
| 	<div class="text-center px-6" in:fade={{ duration: 100 }}> | ||||
| 		<div class="flex pt-2 space-x-4 w-full"> | ||||
| 			<div class="w-full"> | ||||
| 				<div class="font-bold text-left pb-2 text-xl">Application logs</div> | ||||
| 				{#if logs.length === 0} | ||||
| 					<div class="text-xs font-semibold tracking-tighter">Waiting for the logs...</div> | ||||
| 				{:else} | ||||
| 					<pre | ||||
| 						class="leading-4 text-left text-sm font-semibold tracking-tighter rounded-lg bg-black p-6 whitespace-pre-wrap w-full"> | ||||
|             {#each logs as log} | ||||
|               {log + '\n'} | ||||
|             {/each} | ||||
|           </pre> | ||||
| 				{/if} | ||||
| 			</div> | ||||
| 			<div> | ||||
| 				<div class="font-bold text-left pb-2 text-xl w-300">Deployment logs</div> | ||||
| 				{#if deployments.length > 0} | ||||
| 					<div class="space-y-2"> | ||||
| 						{#each deployments as deployment} | ||||
| 							<div | ||||
| 								in:fade={{ duration: 100 }} | ||||
| 								class="flex space-x-4 text-md py-4 hover:shadow mx-auto cursor-pointer transition-all duration-100 border-l-4 border-transparent rounded hover:bg-warmGray-700" | ||||
| 								class:hover:border-green-500={deployment.progress === 'done'} | ||||
| 								class:border-yellow-300={deployment.progress !== 'done' && | ||||
| 									deployment.progress !== 'failed'} | ||||
| 								class:bg-warmGray-800={deployment.progress !== 'done' && | ||||
| 									deployment.progress !== 'failed'} | ||||
| 								class:hover:border-red-500={deployment.progress === 'failed'} | ||||
| 								on:click={() => goto(`./logs/${deployment.deployId}`)} | ||||
| 							> | ||||
| 							<div class="flex space-x-2 px-2"> | ||||
| 								<div class="font-bold text-sm flex justify-center items-center"> | ||||
| 									{deployment.branch}  | ||||
| 								</div> | ||||
| 								<div class="font-bold text-xs flex justify-center items-center text-warmGray-500">{deployment.isPr ? 'PR' : ''}</div> | ||||
| 							</div> | ||||
| 								<div class="flex-1" /> | ||||
| 								<div class="px-3 w-48"> | ||||
| 									<div | ||||
| 										class="text-xs" | ||||
| 										title={new Intl.DateTimeFormat('default', dateOptions).format( | ||||
| 											new Date(deployment.createdAt) | ||||
| 										)} | ||||
| 									> | ||||
| 										{deployment.since} | ||||
| 									</div> | ||||
| 									{#if deployment.progress === 'done'} | ||||
| 										<div class="text-xs"> | ||||
| 											Deployed in <span class="font-bold">{deployment.took}s</span> | ||||
| 										</div> | ||||
| 									{:else if deployment.progress === 'failed'} | ||||
| 										<div class="text-xs text-red-500">Failed</div> | ||||
| 									{:else} | ||||
| 										<div class="text-xs">Deploying...</div> | ||||
| 									{/if} | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						{/each} | ||||
| 					</div> | ||||
| 					<button | ||||
| 						class="text-xs bg-green-600 hover:bg-green-500 p-1 rounded text-white px-2 font-medium my-6" | ||||
| 						on:click={loadMoreDeploymentLogs}>Show more</button | ||||
| 					> | ||||
| 				{:else} | ||||
| 					<div class="text-left text-sm tracking-tight">No deployments found</div> | ||||
| 				{/if} | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| {:catch} | ||||
| 	<div class="text-center font-bold tracking-tight text-xl">No logs found</div> | ||||
| {/await} | ||||
|  | ||||
| <style lang="postcss"> | ||||
| 	.w-300 { | ||||
| 		width: 300px !important; | ||||
| 	} | ||||
| </style> | ||||
| @@ -1,137 +0,0 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	/** | ||||
| 	 * @type {import('@sveltejs/kit').Load} | ||||
| 	 */ | ||||
| 	export async function load(session) { | ||||
| 		if (!browser) { | ||||
| 			if (!import.meta.env.VITE_GITHUB_APP_CLIENTID) { | ||||
| 				return { | ||||
| 					status: 302, | ||||
| 					redirect: '/dashboard/services' | ||||
| 				}; | ||||
| 			} | ||||
| 		} | ||||
| 		return {}; | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <script> | ||||
| 	import { application, initialApplication, initConf, dashboard, prApplication, originalDomain } from '$store'; | ||||
| 	import { onDestroy } from 'svelte'; | ||||
| 	import Loading from '$components/Loading.svelte'; | ||||
| 	import Navbar from '$components/Application/Navbar.svelte'; | ||||
| 	import { page, session } from '$app/stores'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { browser } from '$app/env'; | ||||
| 	import { request } from '$lib/request'; | ||||
|  | ||||
| 	$application.general.nickname = $page.params.nickname; | ||||
| 	async function setConfiguration() { | ||||
| 		try { | ||||
| 			const { configuration } = await request(`/api/v1/application/config`, $session, { | ||||
| 				body: { | ||||
| 					nickname: $application.general.nickname | ||||
| 				} | ||||
| 			}); | ||||
| 			$prApplication = configuration.filter((c) => c.general.pullRequest !== 0); | ||||
| 			$application = configuration.find((c) => c.general.pullRequest === 0); | ||||
| 			if (!$application) browser && goto('/dashboard/applications'); | ||||
| 		} catch (error) { | ||||
| 			browser && goto('/dashboard/applications'); | ||||
| 		} | ||||
| 	} | ||||
| 	async function loadConfiguration() { | ||||
| 		if ($page.path !== '/application/new') { | ||||
| 			if (!$dashboard) { | ||||
| 				await setConfiguration(); | ||||
| 			} else { | ||||
| 				const found = $dashboard.applications.deployed.find((app) => { | ||||
| 					const { domain } = app.configuration.publish; | ||||
| 					if (domain === $application.publish.domain) { | ||||
| 						return app; | ||||
| 					} | ||||
| 				}); | ||||
| 				if (found) { | ||||
| 					$application = { ...found.configuration }; | ||||
| 					$initConf = JSON.parse(JSON.stringify($application)); | ||||
| 				} else { | ||||
| 					await setConfiguration(); | ||||
| 				} | ||||
| 			} | ||||
| 			$originalDomain = $application.publish.domain | ||||
|  | ||||
| 		} else { | ||||
| 			$application = JSON.parse(JSON.stringify(initialApplication)); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	onDestroy(() => { | ||||
| 		$application = JSON.parse(JSON.stringify(initialApplication)); | ||||
| 	}); | ||||
| </script> | ||||
|  | ||||
| {#await loadConfiguration()} | ||||
| 	<Loading /> | ||||
| {:then} | ||||
| 	<Navbar /> | ||||
| 	<div class="text-white"> | ||||
| 		{#if $page.path.endsWith('configuration')} | ||||
| 			<div class="min-h-full text-white"> | ||||
| 				<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"> | ||||
| 					{$application.publish.domain | ||||
| 						? `${$application.publish.domain}${ | ||||
| 								$application.publish.path !== '/' ? $application.publish.path : '' | ||||
| 						  }` | ||||
| 						: 'example.com'} | ||||
| 					<a | ||||
| 						target="_blank" | ||||
| 						class="icon mx-2" | ||||
| 						href={'https://' + $application.publish.domain + $application.publish.path} | ||||
| 					> | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							class="h-6 w-6" | ||||
| 							fill="none" | ||||
| 							viewBox="0 0 24 24" | ||||
| 							stroke="currentColor" | ||||
| 						> | ||||
| 							<path | ||||
| 								stroke-linecap="round" | ||||
| 								stroke-linejoin="round" | ||||
| 								stroke-width="2" | ||||
| 								d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" | ||||
| 							/> | ||||
| 						</svg></a | ||||
| 					> | ||||
|  | ||||
| 					<a | ||||
| 						target="_blank" | ||||
| 						class="icon" | ||||
| 						href={`https://github.com/${$application.repository.organization}/${$application.repository.name}`} | ||||
| 					> | ||||
| 						<svg | ||||
| 							class="w-6" | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							viewBox="0 0 24 24" | ||||
| 							fill="none" | ||||
| 							stroke="currentColor" | ||||
| 							stroke-width="2" | ||||
| 							stroke-linecap="round" | ||||
| 							stroke-linejoin="round" | ||||
| 							><path | ||||
| 								d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" | ||||
| 							/></svg | ||||
| 						></a | ||||
| 					> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{:else if $page.path === '/application/new'} | ||||
| 			<div class="min-h-full text-white"> | ||||
| 				<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"> | ||||
| 					New Application | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 		<slot /> | ||||
| 	</div> | ||||
| {/await} | ||||
| @@ -1,5 +0,0 @@ | ||||
| <script> | ||||
| 	import Configuration from '$components/Application/Configuration.svelte'; | ||||
| </script> | ||||
|  | ||||
| <Configuration /> | ||||
							
								
								
									
										376
									
								
								src/routes/applications/[id]/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										376
									
								
								src/routes/applications/[id]/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,376 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	function checkConfiguration(application): string { | ||||
| 		let configurationPhase = null; | ||||
| 		if (!application.gitSourceId) { | ||||
| 			configurationPhase = 'source'; | ||||
| 		} else if (!application.repository && !application.branch) { | ||||
| 			configurationPhase = 'repository'; | ||||
| 		} else if (!application.destinationDockerId) { | ||||
| 			configurationPhase = 'destination'; | ||||
| 		} else if (!application.buildPack) { | ||||
| 			configurationPhase = 'buildpack'; | ||||
| 		} | ||||
| 		return configurationPhase; | ||||
| 	} | ||||
| 	export const load: Load = async ({ fetch, url, params }) => { | ||||
| 		const endpoint = `/applications/${params.id}.json`; | ||||
| 		const res = await fetch(endpoint); | ||||
| 		if (res.ok) { | ||||
| 			const { application, githubToken, ghToken, isRunning, appId } = await res.json(); | ||||
| 			if (!application || Object.entries(application).length === 0) { | ||||
| 				return { | ||||
| 					status: 302, | ||||
| 					redirect: '/applications' | ||||
| 				}; | ||||
| 			} | ||||
| 			const configurationPhase = checkConfiguration(application); | ||||
| 			if ( | ||||
| 				configurationPhase && | ||||
| 				url.pathname !== `/applications/${params.id}/configuration/${configurationPhase}` | ||||
| 			) { | ||||
| 				return { | ||||
| 					status: 302, | ||||
| 					redirect: `/applications/${params.id}/configuration/${configurationPhase}` | ||||
| 				}; | ||||
| 			} | ||||
|  | ||||
| 			return { | ||||
| 				props: { | ||||
| 					application, | ||||
| 					isRunning | ||||
| 				}, | ||||
| 				stuff: { | ||||
| 					isRunning, | ||||
| 					ghToken, | ||||
| 					githubToken, | ||||
| 					application, | ||||
| 					appId | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: 302, | ||||
| 			redirect: '/applications' | ||||
| 		}; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	export let application; | ||||
| 	export let isRunning; | ||||
| 	import { page, session } from '$app/stores'; | ||||
| 	import { errorNotification } from '$lib/form'; | ||||
| 	import DeleteIcon from '$lib/components/DeleteIcon.svelte'; | ||||
| 	import Loading from '$lib/components/Loading.svelte'; | ||||
| 	import { del, post } from '$lib/api'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
|  | ||||
| 	let loading = false; | ||||
| 	const { id } = $page.params; | ||||
|  | ||||
| 	async function handleDeploySubmit() { | ||||
| 		try { | ||||
| 			const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application }); | ||||
| 			return await goto(`/applications/${id}/logs/build?buildId=${buildId}`); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async function deleteApplication(name) { | ||||
| 		const sure = confirm(`Are you sure you would like to delete '${name}'?`); | ||||
| 		if (sure) { | ||||
| 			loading = true; | ||||
| 			try { | ||||
| 				await del(`/applications/${id}/delete.json`, { id }); | ||||
| 				return await goto(`/applications`); | ||||
| 			} catch ({ error }) { | ||||
| 				return errorNotification(error); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	async function stopApplication() { | ||||
| 		try { | ||||
| 			loading = true; | ||||
| 			await post(`/applications/${id}/stop.json`, {}); | ||||
| 			return window.location.reload(); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} finally { | ||||
| 			loading = false; | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <nav class="nav-side"> | ||||
| 	{#if loading} | ||||
| 		<Loading fullscreen cover /> | ||||
| 	{:else} | ||||
| 		{#if application.fqdn && application.gitSource && application.repository && application.destinationDocker && application.buildPack} | ||||
| 			{#if isRunning} | ||||
| 				<button | ||||
| 					on:click={stopApplication} | ||||
| 					title="Stop application" | ||||
| 					type="submit" | ||||
| 					disabled={!$session.isAdmin} | ||||
| 					class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-green-600 hover:text-white" | ||||
| 					data-tooltip={$session.isAdmin | ||||
| 						? 'Stop application' | ||||
| 						: 'You do not have permission to stop the application.'} | ||||
| 				> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						class="w-6 h-6" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke-width="1.5" | ||||
| 						stroke="currentColor" | ||||
| 						fill="none" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 					> | ||||
| 						<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 						<rect x="6" y="5" width="4" height="14" rx="1" /> | ||||
| 						<rect x="14" y="5" width="4" height="14" rx="1" /> | ||||
| 					</svg> | ||||
| 				</button> | ||||
| 				<form on:submit|preventDefault={handleDeploySubmit}> | ||||
| 					<button | ||||
| 						title="Rebuild application" | ||||
| 						type="submit" | ||||
| 						disabled={!$session.isAdmin} | ||||
| 						class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-green-600 hover:text-white" | ||||
| 						data-tooltip={$session.isAdmin | ||||
| 							? 'Rebuild application' | ||||
| 							: 'You do not have permission to rebuild application.'} | ||||
| 					> | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							class="w-6 h-6" | ||||
| 							viewBox="0 0 24 24" | ||||
| 							stroke-width="1.5" | ||||
| 							stroke="currentColor" | ||||
| 							fill="none" | ||||
| 							stroke-linecap="round" | ||||
| 							stroke-linejoin="round" | ||||
| 						> | ||||
| 							<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 							<path | ||||
| 								d="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82" | ||||
| 								transform="rotate(-45 12 12)" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 					</button> | ||||
| 				</form> | ||||
| 			{:else} | ||||
| 				<form on:submit|preventDefault={handleDeploySubmit}> | ||||
| 					<button | ||||
| 						title="Build and start application" | ||||
| 						type="submit" | ||||
| 						disabled={!$session.isAdmin} | ||||
| 						class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-green-600 hover:text-white" | ||||
| 						data-tooltip={$session.isAdmin | ||||
| 							? 'Build and start application' | ||||
| 							: 'You do not have permission to Build and start application.'} | ||||
| 					> | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							class="w-6 h-6" | ||||
| 							viewBox="0 0 24 24" | ||||
| 							stroke-width="1.5" | ||||
| 							stroke="currentColor" | ||||
| 							fill="none" | ||||
| 							stroke-linecap="round" | ||||
| 							stroke-linejoin="round" | ||||
| 						> | ||||
| 							<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 							<path d="M7 4v16l13 -8z" /> | ||||
| 						</svg> | ||||
| 					</button> | ||||
| 				</form> | ||||
| 			{/if} | ||||
|  | ||||
| 			<div class="border border-stone-700 h-8" /> | ||||
| 			<a | ||||
| 				href="/applications/{id}" | ||||
| 				sveltekit:prefetch | ||||
| 				class="hover:text-yellow-500 rounded" | ||||
| 				class:text-yellow-500={$page.url.pathname === `/applications/${id}`} | ||||
| 				class:bg-coolgray-500={$page.url.pathname === `/applications/${id}`} | ||||
| 			> | ||||
| 				<button | ||||
| 					title="Configurations" | ||||
| 					class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500" | ||||
| 					data-tooltip="Configurations" | ||||
| 				> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						class="h-6 w-6" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke-width="1.5" | ||||
| 						stroke="currentColor" | ||||
| 						fill="none" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 					> | ||||
| 						<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 						<rect x="4" y="8" width="4" height="4" /> | ||||
| 						<line x1="6" y1="4" x2="6" y2="8" /> | ||||
| 						<line x1="6" y1="12" x2="6" y2="20" /> | ||||
| 						<rect x="10" y="14" width="4" height="4" /> | ||||
| 						<line x1="12" y1="4" x2="12" y2="14" /> | ||||
| 						<line x1="12" y1="18" x2="12" y2="20" /> | ||||
| 						<rect x="16" y="5" width="4" height="4" /> | ||||
| 						<line x1="18" y1="4" x2="18" y2="5" /> | ||||
| 						<line x1="18" y1="9" x2="18" y2="20" /> | ||||
| 					</svg></button | ||||
| 				></a | ||||
| 			> | ||||
| 			<a | ||||
| 				href="/applications/{id}/secrets" | ||||
| 				sveltekit:prefetch | ||||
| 				class="hover:text-pink-500 rounded" | ||||
| 				class:text-pink-500={$page.url.pathname === `/applications/${id}/secrets`} | ||||
| 				class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`} | ||||
| 			> | ||||
| 				<button | ||||
| 					title="Secrets" | ||||
| 					class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500" | ||||
| 					data-tooltip="Secrets" | ||||
| 				> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						class="w-6 h-6" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke-width="1.5" | ||||
| 						stroke="currentColor" | ||||
| 						fill="none" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 					> | ||||
| 						<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 						<path | ||||
| 							d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3" | ||||
| 						/> | ||||
| 						<circle cx="12" cy="11" r="1" /> | ||||
| 						<line x1="12" y1="12" x2="12" y2="14.5" /> | ||||
| 					</svg></button | ||||
| 				></a | ||||
| 			> | ||||
| 			<a | ||||
| 				href="/applications/{id}/previews" | ||||
| 				sveltekit:prefetch | ||||
| 				class="hover:text-orange-500 rounded" | ||||
| 				class:text-orange-500={$page.url.pathname === `/applications/${id}/previews`} | ||||
| 				class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/previews`} | ||||
| 			> | ||||
| 				<button | ||||
| 					title="Previews" | ||||
| 					class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500" | ||||
| 					data-tooltip="Previews" | ||||
| 				> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						class="w-6 h-6" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke-width="1.5" | ||||
| 						stroke="currentColor" | ||||
| 						fill="none" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 					> | ||||
| 						<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 						<circle cx="7" cy="18" r="2" /> | ||||
| 						<circle cx="7" cy="6" r="2" /> | ||||
| 						<circle cx="17" cy="12" r="2" /> | ||||
| 						<line x1="7" y1="8" x2="7" y2="16" /> | ||||
| 						<path d="M7 8a4 4 0 0 0 4 4h4" /> | ||||
| 					</svg></button | ||||
| 				></a | ||||
| 			> | ||||
| 			<div class="border border-stone-700 h-8" /> | ||||
| 			<a | ||||
| 				href="/applications/{id}/logs" | ||||
| 				sveltekit:prefetch | ||||
| 				class="hover:text-sky-500 rounded" | ||||
| 				class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`} | ||||
| 				class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`} | ||||
| 			> | ||||
| 				<button | ||||
| 					title="Application Logs" | ||||
| 					class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500 " | ||||
| 					data-tooltip="Application Logs" | ||||
| 				> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						class="h-6 w-6" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke-width="1.5" | ||||
| 						stroke="currentColor" | ||||
| 						fill="none" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 					> | ||||
| 						<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 						<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" /> | ||||
| 						<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" /> | ||||
| 						<line x1="3" y1="6" x2="3" y2="19" /> | ||||
| 						<line x1="12" y1="6" x2="12" y2="19" /> | ||||
| 						<line x1="21" y1="6" x2="21" y2="19" /> | ||||
| 					</svg> | ||||
| 				</button></a | ||||
| 			> | ||||
| 			<a | ||||
| 				href="/applications/{id}/logs/build" | ||||
| 				sveltekit:prefetch | ||||
| 				class="hover:text-red-500 rounded" | ||||
| 				class:text-red-500={$page.url.pathname === `/applications/${id}/logs/build`} | ||||
| 				class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs/build`} | ||||
| 			> | ||||
| 				<button | ||||
| 					title="Build Logs" | ||||
| 					class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500 " | ||||
| 					data-tooltip="Build Logs" | ||||
| 				> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						class="h-6 w-6" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke-width="1.5" | ||||
| 						stroke="currentColor" | ||||
| 						fill="none" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 					> | ||||
| 						<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 						<circle cx="19" cy="13" r="2" /> | ||||
| 						<circle cx="4" cy="17" r="2" /> | ||||
| 						<circle cx="13" cy="17" r="2" /> | ||||
| 						<line x1="13" y1="19" x2="4" y2="19" /> | ||||
| 						<line x1="4" y1="15" x2="13" y2="15" /> | ||||
| 						<path d="M8 12v-5h2a3 3 0 0 1 3 3v5" /> | ||||
| 						<path d="M5 15v-2a1 1 0 0 1 1 -1h7" /> | ||||
| 						<path d="M19 11v-7l-6 7" /> | ||||
| 					</svg> | ||||
| 				</button></a | ||||
| 			> | ||||
| 			<div class="border border-stone-700 h-8" /> | ||||
| 		{/if} | ||||
|  | ||||
| 		<button | ||||
| 			on:click={() => deleteApplication(application.name)} | ||||
| 			title="Delete application" | ||||
| 			type="submit" | ||||
| 			disabled={!$session.isAdmin} | ||||
| 			class:hover:text-red-500={$session.isAdmin} | ||||
| 			class="icons bg-transparent  tooltip-bottom text-sm" | ||||
| 			data-tooltip={$session.isAdmin | ||||
| 				? 'Delete application' | ||||
| 				: 'You do not have permission to delete this application'} | ||||
| 		> | ||||
| 			<DeleteIcon /> | ||||
| 		</button> | ||||
| 	{/if} | ||||
| </nav> | ||||
| <slot /> | ||||
							
								
								
									
										28
									
								
								src/routes/applications/[id]/check.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/routes/applications/[id]/check.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| import { asyncExecShell, getDomain, getEngine, getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
|  | ||||
| 	let { fqdn } = await event.request.json(); | ||||
| 	fqdn = fqdn.toLowerCase(); | ||||
|  | ||||
| 	try { | ||||
| 		const found = await db.isDomainConfigured({ id, fqdn }); | ||||
| 		if (found) { | ||||
| 			throw { | ||||
| 				message: `Domain ${getDomain(fqdn)} is already configured.` | ||||
| 			}; | ||||
| 		} | ||||
| 		return { | ||||
| 			status: 200 | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										44
									
								
								src/routes/applications/[id]/configuration/_BuildPack.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/routes/applications/[id]/configuration/_BuildPack.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| <script lang="ts"> | ||||
| 	import { goto } from '$app/navigation'; | ||||
|  | ||||
| 	import { page } from '$app/stores'; | ||||
| 	import { post } from '$lib/api'; | ||||
| 	import { errorNotification } from '$lib/form'; | ||||
|  | ||||
| 	const { id } = $page.params; | ||||
| 	const from = $page.url.searchParams.get('from'); | ||||
|  | ||||
| 	export let buildPack; | ||||
| 	export let foundConfig; | ||||
| 	export let scanning; | ||||
|  | ||||
| 	async function handleSubmit(name) { | ||||
| 		try { | ||||
| 			const tempBuildPack = JSON.parse(JSON.stringify(buildPack)); | ||||
| 			delete tempBuildPack.name; | ||||
| 			delete tempBuildPack.fancyName; | ||||
| 			delete tempBuildPack.color; | ||||
| 			delete tempBuildPack.hoverColor; | ||||
|  | ||||
| 			if (foundConfig.buildPack !== name) { | ||||
| 				await post(`/applications/${id}.json`, { ...tempBuildPack }); | ||||
| 			} | ||||
| 			await post(`/applications/${id}/configuration/buildpack.json`, { buildPack: name }); | ||||
| 			return await goto(from || `/applications/${id}`); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <form on:submit|preventDefault={() => handleSubmit(buildPack.name)}> | ||||
| 	<button | ||||
| 		type="submit" | ||||
| 		class="box-selection relative flex text-xl font-bold {buildPack.hoverColor} {foundConfig?.name === | ||||
| 			buildPack.name && buildPack.color}" | ||||
| 		><span>{buildPack.fancyName}</span> | ||||
| 		{#if !scanning && foundConfig?.name === buildPack.name} | ||||
| 			<span class="absolute bottom-0 pb-2 text-xs">Choose this one...</span> | ||||
| 		{/if} | ||||
| 	</button> | ||||
| </form> | ||||
| @@ -0,0 +1,170 @@ | ||||
| <script lang="ts"> | ||||
| 	import { goto } from '$app/navigation'; | ||||
|  | ||||
| 	export let githubToken; | ||||
| 	export let application; | ||||
|  | ||||
| 	import { page } from '$app/stores'; | ||||
| 	import { get, post } from '$lib/api'; | ||||
| 	import { getGithubToken } from '$lib/components/common'; | ||||
| 	import { enhance, errorNotification } from '$lib/form'; | ||||
| 	import { onMount } from 'svelte'; | ||||
|  | ||||
| 	const { id } = $page.params; | ||||
| 	const from = $page.url.searchParams.get('from'); | ||||
| 	const to = $page.url.searchParams.get('to'); | ||||
|  | ||||
| 	let htmlUrl = application.gitSource.htmlUrl; | ||||
| 	let apiUrl = application.gitSource.apiUrl; | ||||
|  | ||||
| 	let loading = { | ||||
| 		repositories: true, | ||||
| 		branches: false | ||||
| 	}; | ||||
| 	let repositories = []; | ||||
| 	let branches = []; | ||||
|  | ||||
| 	let selected = { | ||||
| 		projectId: undefined, | ||||
| 		repository: undefined, | ||||
| 		branch: undefined | ||||
| 	}; | ||||
| 	let showSave = false; | ||||
| 	let token = null; | ||||
|  | ||||
| 	async function loadRepositoriesByPage(page = 0) { | ||||
| 		try { | ||||
| 			return await get(`${apiUrl}/installation/repositories?per_page=100&page=${page}`, { | ||||
| 				Authorization: `token ${token}` | ||||
| 			}); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| 	async function loadRepositories() { | ||||
| 		token = await getGithubToken({ apiUrl, githubToken, application }); | ||||
| 		let page = 1; | ||||
| 		let reposCount = 0; | ||||
| 		const loadedRepos = await loadRepositoriesByPage(); | ||||
| 		repositories = repositories.concat(loadedRepos.repositories); | ||||
| 		reposCount = loadedRepos.total_count; | ||||
| 		if (reposCount > repositories.length) { | ||||
| 			while (reposCount > repositories.length) { | ||||
| 				page = page + 1; | ||||
| 				const repos = await loadRepositoriesByPage(page); | ||||
| 				repositories = repositories.concat(repos.repositories); | ||||
| 			} | ||||
| 		} | ||||
| 		loading.repositories = false; | ||||
| 	} | ||||
| 	async function loadBranches() { | ||||
| 		loading.branches = true; | ||||
| 		selected.branch = undefined; | ||||
| 		selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id; | ||||
| 		try { | ||||
| 			branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, { | ||||
| 				Authorization: `token ${token}` | ||||
| 			}); | ||||
| 			return; | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} finally { | ||||
| 			loading.branches = false; | ||||
| 		} | ||||
| 	} | ||||
| 	async function isBranchAlreadyUsed() { | ||||
| 		try { | ||||
| 			return await get( | ||||
| 				`/applications/${id}/configuration/repository.json?repository=${selected.repository}&branch=${selected.branch}` | ||||
| 			); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} finally { | ||||
| 			showSave = true; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	onMount(async () => { | ||||
| 		await loadRepositories(); | ||||
| 	}); | ||||
| 	async function handleSubmit() { | ||||
| 		try { | ||||
| 			await post(`/applications/${id}/configuration/repository.json`, { ...selected }); | ||||
| 			if (to) { | ||||
| 				return await goto(`${to}?from=${from}`); | ||||
| 			} | ||||
| 			return await goto(from || `/applications/${id}/configuration/destination`); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| {#if repositories.length === 0 && loading.repositories === false} | ||||
| 	<div class="flex-col text-center"> | ||||
| 		<div class="pb-4">No repositories configured for your Git Application.</div> | ||||
| 		<a href={`/sources/${application.gitSource.id}`}><button>Configure it now</button></a> | ||||
| 	</div> | ||||
| {:else} | ||||
| 	<form on:submit|preventDefault={handleSubmit}> | ||||
| 		<div> | ||||
| 			{#if loading.repositories} | ||||
| 				<select name="repository" disabled class="w-96"> | ||||
| 					<option selected value="">Loading repositories...</option> | ||||
| 				</select> | ||||
| 			{:else} | ||||
| 				<select | ||||
| 					name="repository" | ||||
| 					class="w-96" | ||||
| 					bind:value={selected.repository} | ||||
| 					on:change={loadBranches} | ||||
| 				> | ||||
| 					<option value="" disabled selected>Please select a repository</option> | ||||
| 					{#each repositories as repository} | ||||
| 						<option value={repository.full_name}>{repository.name}</option> | ||||
| 					{/each} | ||||
| 				</select> | ||||
| 			{/if} | ||||
| 			<input class="hidden" bind:value={selected.projectId} name="projectId" /> | ||||
| 			{#if loading.branches} | ||||
| 				<select name="branch" disabled class="w-96"> | ||||
| 					<option selected value="">Loading branches...</option> | ||||
| 				</select> | ||||
| 			{:else} | ||||
| 				<select | ||||
| 					name="branch" | ||||
| 					class="w-96" | ||||
| 					disabled={!selected.repository} | ||||
| 					bind:value={selected.branch} | ||||
| 					on:change={isBranchAlreadyUsed} | ||||
| 				> | ||||
| 					{#if !selected.repository} | ||||
| 						<option value="" disabled selected>Select a repository first</option> | ||||
| 					{:else} | ||||
| 						<option value="" disabled selected>Please select a branch</option> | ||||
| 					{/if} | ||||
|  | ||||
| 					{#each branches as branch} | ||||
| 						<option value={branch.name}>{branch.name}</option> | ||||
| 					{/each} | ||||
| 				</select> | ||||
| 			{/if} | ||||
| 		</div> | ||||
| 		<div class="pt-5 flex-col flex justify-center items-center space-y-4"> | ||||
| 			<button | ||||
| 				class="w-40" | ||||
| 				type="submit" | ||||
| 				disabled={!showSave} | ||||
| 				class:bg-orange-600={showSave} | ||||
| 				class:hover:bg-orange-500={showSave}>Save</button | ||||
| 			> | ||||
| 			<!-- <button class="w-40" | ||||
| 				><a | ||||
| 					class="no-underline" | ||||
| 					href="{apiUrl}/apps/{application.gitSource.githubApp.name}/installations/new" | ||||
| 					>Modify Repositories</a | ||||
| 				></button | ||||
| 			> --> | ||||
| 		</div> | ||||
| 	</form> | ||||
| {/if} | ||||
| @@ -0,0 +1,334 @@ | ||||
| <script lang="ts"> | ||||
| 	export let application; | ||||
| 	export let appId; | ||||
| 	import { page, session } from '$app/stores'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 	import { errorNotification } from '$lib/form'; | ||||
| 	import { dev } from '$app/env'; | ||||
| 	import cuid from 'cuid'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { del, get, post, put } from '$lib/api'; | ||||
|  | ||||
| 	const { id } = $page.params; | ||||
| 	const from = $page.url.searchParams.get('from'); | ||||
|  | ||||
| 	const updateDeployKeyIdUrl = `/applications/${id}/configuration/deploykey.json`; | ||||
|  | ||||
| 	let loading = { | ||||
| 		base: true, | ||||
| 		projects: false, | ||||
| 		branches: false, | ||||
| 		save: false | ||||
| 	}; | ||||
|  | ||||
| 	let htmlUrl = application.gitSource.htmlUrl; | ||||
| 	let apiUrl = application.gitSource.apiUrl; | ||||
|  | ||||
| 	let username = null; | ||||
| 	let groups = []; | ||||
| 	let projects = []; | ||||
| 	let branches = []; | ||||
| 	let showSave = false; | ||||
|  | ||||
| 	let selected = { | ||||
| 		group: undefined, | ||||
| 		project: undefined, | ||||
| 		branch: undefined | ||||
| 	}; | ||||
| 	onMount(async () => { | ||||
| 		if (!$session.gitlabToken) { | ||||
| 			getGitlabToken(); | ||||
| 		} else { | ||||
| 			loading.base = true; | ||||
| 			try { | ||||
| 				const user = await get(`${apiUrl}/v4/user`, { | ||||
| 					Authorization: `Bearer ${$session.gitlabToken}` | ||||
| 				}); | ||||
| 				username = user.username; | ||||
| 			} catch (error) { | ||||
| 				return getGitlabToken(); | ||||
| 			} | ||||
| 			try { | ||||
| 				groups = await get(`${apiUrl}/v4/groups?per_page=5000`, { | ||||
| 					Authorization: `Bearer ${$session.gitlabToken}` | ||||
| 				}); | ||||
| 			} catch (error) { | ||||
| 				errorNotification(error); | ||||
| 				throw new Error(error); | ||||
| 			} finally { | ||||
| 				loading.base = false; | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	function getGitlabToken() { | ||||
| 		const left = screen.width / 2 - 1020 / 2; | ||||
| 		const top = screen.height / 2 - 618 / 2; | ||||
| 		const newWindow = open( | ||||
| 			`${htmlUrl}/oauth/authorize?client_id=${application.gitSource.gitlabApp.appId}&redirect_uri=${window.location.origin}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${$page.params.id}`, | ||||
| 			'GitLab', | ||||
| 			'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' + | ||||
| 				top + | ||||
| 				', left=' + | ||||
| 				left + | ||||
| 				', toolbar=0, menubar=0, status=0' | ||||
| 		); | ||||
| 		const timer = setInterval(() => { | ||||
| 			if (newWindow?.closed) { | ||||
| 				clearInterval(timer); | ||||
| 				window.location.reload(); | ||||
| 			} | ||||
| 		}, 100); | ||||
| 	} | ||||
|  | ||||
| 	async function loadProjects() { | ||||
| 		loading.projects = true; | ||||
| 		if (username === selected.group.name) { | ||||
| 			try { | ||||
| 				projects = await get( | ||||
| 					`${apiUrl}/v4/users/${selected.group.name}/projects?min_access_level=40&page=1&per_page=25&archived=false`, | ||||
| 					{ | ||||
| 						Authorization: `Bearer ${$session.gitlabToken}` | ||||
| 					} | ||||
| 				); | ||||
| 			} catch (error) { | ||||
| 				errorNotification(error); | ||||
| 				throw new Error(error); | ||||
| 			} finally { | ||||
| 				loading.projects = false; | ||||
| 			} | ||||
| 		} else { | ||||
| 			try { | ||||
| 				projects = await get( | ||||
| 					`${apiUrl}/v4/groups/${selected.group.id}/projects?page=1&per_page=25&archived=false`, | ||||
| 					{ | ||||
| 						Authorization: `Bearer ${$session.gitlabToken}` | ||||
| 					} | ||||
| 				); | ||||
| 			} catch (error) { | ||||
| 				errorNotification(error); | ||||
| 				throw new Error(error); | ||||
| 			} finally { | ||||
| 				loading.projects = false; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async function loadBranches() { | ||||
| 		loading.branches = true; | ||||
| 		try { | ||||
| 			branches = await get( | ||||
| 				`${apiUrl}/v4/projects/${selected.project.id}/repository/branches?per_page=100&page=1`, | ||||
| 				{ | ||||
| 					Authorization: `Bearer ${$session.gitlabToken}` | ||||
| 				} | ||||
| 			); | ||||
| 		} catch (error) { | ||||
| 			errorNotification(error); | ||||
| 			throw new Error(error); | ||||
| 		} finally { | ||||
| 			loading.branches = false; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async function isBranchAlreadyUsed() { | ||||
| 		const url = `/applications/${id}/configuration/repository.json?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}`; | ||||
|  | ||||
| 		try { | ||||
| 			await get(url); | ||||
| 			showSave = true; | ||||
| 		} catch (error) { | ||||
| 			showSave = false; | ||||
| 			return errorNotification('Branch already configured'); | ||||
| 		} | ||||
| 	} | ||||
| 	// async function saveDeployKey(deployKeyId: number) { | ||||
| 	// 	try { | ||||
| 	// 		await post(updateDeployKeyIdUrl, { deployKeyId }); | ||||
| 	// 	} catch (error) { | ||||
| 	// 		errorNotification(error); | ||||
| 	// 		throw new Error(error); | ||||
| 	// 	} | ||||
| 	// } | ||||
| 	async function checkSSHKey(sshkeyUrl) { | ||||
| 		try { | ||||
| 			return await post(sshkeyUrl, {}); | ||||
| 		} catch (error) { | ||||
| 			errorNotification(error); | ||||
| 			throw new Error(error); | ||||
| 		} | ||||
| 	} | ||||
| 	async function setWebhook(url, webhookToken) { | ||||
| 		const host = dev | ||||
| 			? 'https://webhook.site/0e5beb2c-4e9b-40e2-a89e-32295e570c21' | ||||
| 			: `${window.location.origin}/webhooks/gitlab/events`; | ||||
| 		try { | ||||
| 			await post( | ||||
| 				url, | ||||
| 				{ | ||||
| 					id: selected.project.id, | ||||
| 					url: host, | ||||
| 					token: webhookToken, | ||||
| 					push_events: true, | ||||
| 					enable_ssl_verification: true, | ||||
| 					merge_requests_events: true | ||||
| 				}, | ||||
| 				{ | ||||
| 					Authorization: `Bearer ${$session.gitlabToken}` | ||||
| 				} | ||||
| 			); | ||||
| 		} catch (error) { | ||||
| 			errorNotification(error); | ||||
| 			throw error; | ||||
| 		} | ||||
| 	} | ||||
| 	async function save() { | ||||
| 		loading.save = true; | ||||
| 		let privateSshKey = application.gitSource.gitlabApp.privateSshKey; | ||||
| 		let publicSshKey = application.gitSource.gitlabApp.publicSshKey; | ||||
|  | ||||
| 		const deployKeyUrl = `${apiUrl}/v4/projects/${selected.project.id}/deploy_keys`; | ||||
| 		const sshkeyUrl = `/applications/${id}/configuration/sshkey.json`; | ||||
| 		const webhookUrl = `${apiUrl}/v4/projects/${selected.project.id}/hooks`; | ||||
| 		const webhookToken = cuid(); | ||||
|  | ||||
| 		try { | ||||
| 			if (!privateSshKey || !publicSshKey) { | ||||
| 				const { publicKey } = await checkSSHKey(sshkeyUrl); | ||||
| 				publicSshKey = publicKey; | ||||
| 			} | ||||
| 			const deployKeys = await get(deployKeyUrl, { | ||||
| 				Authorization: `Bearer ${$session.gitlabToken}` | ||||
| 			}); | ||||
| 			const deployKeyFound = deployKeys.filter((dk) => dk.title === `${appId}-coolify-deploy-key`); | ||||
| 			if (deployKeyFound.length > 0) { | ||||
| 				for (const deployKey of deployKeyFound) { | ||||
| 					console.log(`${deployKeyUrl}/${deployKey.id}`); | ||||
| 					await del( | ||||
| 						`${deployKeyUrl}/${deployKey.id}`, | ||||
| 						{}, | ||||
| 						{ | ||||
| 							Authorization: `Bearer ${$session.gitlabToken}` | ||||
| 						} | ||||
| 					); | ||||
| 				} | ||||
| 			} | ||||
| 			const { id } = await post( | ||||
| 				deployKeyUrl, | ||||
| 				{ | ||||
| 					title: `${appId}-coolify-deploy-key`, | ||||
| 					key: publicSshKey, | ||||
| 					can_push: false | ||||
| 				}, | ||||
| 				{ | ||||
| 					Authorization: `Bearer ${$session.gitlabToken}` | ||||
| 				} | ||||
| 			); | ||||
| 			await post(updateDeployKeyIdUrl, { deployKeyId: id }); | ||||
| 		} catch (error) { | ||||
| 			console.log(error); | ||||
| 			throw new Error(error); | ||||
| 		} | ||||
|  | ||||
| 		try { | ||||
| 			await setWebhook(webhookUrl, webhookToken); | ||||
| 		} catch (err) { | ||||
| 			console.log(err); | ||||
| 			if (!dev) throw new Error(err); | ||||
| 		} | ||||
|  | ||||
| 		const url = `/applications/${id}/configuration/repository.json`; | ||||
| 		try { | ||||
| 			await post(url, { | ||||
| 				repository: `${selected.group.full_path}/${selected.project.name}`, | ||||
| 				branch: selected.branch.name, | ||||
| 				projectId: selected.project.id, | ||||
| 				webhookToken | ||||
| 			}); | ||||
| 			return await goto(from || `/applications/${id}/configuration/buildpack`); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| 	async function handleSubmit() { | ||||
| 		try { | ||||
| 			await post(`/applications/{id}/configuration/repository.json`, { ...selected }); | ||||
| 			return await goto(from || `/applications/${id}/configuration/destination`); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <form on:submit={handleSubmit}> | ||||
| 	<div class="flex flex-col space-y-2 px-4 xl:flex-row xl:space-y-0 xl:space-x-2 "> | ||||
| 		{#if loading.base} | ||||
| 			<select name="group" disabled class="w-96"> | ||||
| 				<option selected value="">Loading groups...</option> | ||||
| 			</select> | ||||
| 		{:else} | ||||
| 			<select name="group" class="w-96" bind:value={selected.group} on:change={loadProjects}> | ||||
| 				<option value="" disabled selected>Please select a group</option> | ||||
| 				{#each groups as group} | ||||
| 					<option value={group}>{group.full_name}</option> | ||||
| 				{/each} | ||||
| 			</select> | ||||
| 		{/if} | ||||
| 		{#if loading.projects} | ||||
| 			<select name="project" disabled class="w-96"> | ||||
| 				<option selected value="">Loading projects...</option> | ||||
| 			</select> | ||||
| 		{:else if !loading.projects && projects.length > 0} | ||||
| 			<select | ||||
| 				name="project" | ||||
| 				class="w-96" | ||||
| 				bind:value={selected.project} | ||||
| 				on:change={loadBranches} | ||||
| 				disabled={!selected.group} | ||||
| 			> | ||||
| 				<option value="" disabled selected>Please select a project</option> | ||||
| 				{#each projects as project} | ||||
| 					<option value={project}>{project.name}</option> | ||||
| 				{/each} | ||||
| 			</select> | ||||
| 		{:else} | ||||
| 			<select name="project" disabled class="w-96"> | ||||
| 				<option disabled selected value="">No projects found</option> | ||||
| 			</select> | ||||
| 		{/if} | ||||
|  | ||||
| 		{#if loading.branches} | ||||
| 			<select name="branch" disabled class="w-96"> | ||||
| 				<option selected value="">Loading branches...</option> | ||||
| 			</select> | ||||
| 		{:else if !loading.branches && branches.length > 0} | ||||
| 			<select | ||||
| 				name="branch" | ||||
| 				class="w-96" | ||||
| 				bind:value={selected.branch} | ||||
| 				on:change={isBranchAlreadyUsed} | ||||
| 				disabled={!selected.project} | ||||
| 			> | ||||
| 				<option value="" disabled selected>Please select a branch</option> | ||||
| 				{#each branches as branch} | ||||
| 					<option value={branch}>{branch.name}</option> | ||||
| 				{/each} | ||||
| 			</select> | ||||
| 		{:else} | ||||
| 			<select name="project" disabled class="w-96"> | ||||
| 				<option disabled selected value="">No branches found</option> | ||||
| 			</select> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 	<div class="flex flex-col items-center justify-center space-y-4 pt-5"> | ||||
| 		<button | ||||
| 			on:click|preventDefault={save} | ||||
| 			class="w-40" | ||||
| 			type="submit" | ||||
| 			disabled={!showSave || loading.save} | ||||
| 			class:bg-orange-600={showSave && !loading.save} | ||||
| 			class:hover:bg-orange-500={showSave && !loading.save} | ||||
| 			>{loading.save ? 'Saving...' : 'Save'}</button | ||||
| 		> | ||||
| 	</div> | ||||
| </form> | ||||
							
								
								
									
										41
									
								
								src/routes/applications/[id]/configuration/buildpack.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/routes/applications/[id]/configuration/buildpack.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| import { getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
|  | ||||
| export const get: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	try { | ||||
| 		const application = await db.getApplication({ id, teamId }); | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				type: application.gitSource.type, | ||||
| 				projectId: application.projectId, | ||||
| 				repository: application.repository, | ||||
| 				branch: application.branch, | ||||
| 				apiUrl: application.gitSource.apiUrl | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	const { buildPack } = await event.request.json(); | ||||
|  | ||||
| 	try { | ||||
| 		await db.configureBuildPack({ id, buildPack }); | ||||
| 		return { status: 201 }; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										186
									
								
								src/routes/applications/[id]/configuration/buildpack.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								src/routes/applications/[id]/configuration/buildpack.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,186 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	export const load: Load = async ({ fetch, params, url, stuff }) => { | ||||
| 		const { application, ghToken } = stuff; | ||||
| 		if (application?.buildPack && !url.searchParams.get('from')) { | ||||
| 			return { | ||||
| 				status: 302, | ||||
| 				redirect: `/applications/${params.id}` | ||||
| 			}; | ||||
| 		} | ||||
| 		const endpoint = `/applications/${params.id}/configuration/buildpack.json`; | ||||
| 		const res = await fetch(endpoint); | ||||
| 		if (res.ok) { | ||||
| 			return { | ||||
| 				props: { | ||||
| 					...(await res.json()), | ||||
| 					application, | ||||
| 					ghToken | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: res.status, | ||||
| 			error: new Error(`Could not load ${url}`) | ||||
| 		}; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	import { onMount } from 'svelte'; | ||||
|  | ||||
| 	import { buildPacks, scanningTemplates } from '$lib/components/templates'; | ||||
| 	import BuildPack from './_BuildPack.svelte'; | ||||
| 	import { page, session } from '$app/stores'; | ||||
| 	import { get } from '$lib/api'; | ||||
| 	import { errorNotification } from '$lib/form'; | ||||
|  | ||||
| 	let scanning = true; | ||||
| 	let foundConfig = null; | ||||
|  | ||||
| 	export let apiUrl; | ||||
| 	export let projectId; | ||||
| 	export let repository; | ||||
| 	export let branch; | ||||
| 	export let ghToken; | ||||
| 	export let type; | ||||
| 	export let application; | ||||
|  | ||||
| 	function checkPackageJSONContents({ key, json }) { | ||||
| 		return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key); | ||||
| 	} | ||||
| 	function checkTemplates({ json }) { | ||||
| 		for (const [key, value] of Object.entries(scanningTemplates)) { | ||||
| 			if (checkPackageJSONContents({ key, json })) { | ||||
| 				return buildPacks.find((bp) => bp.name === value.buildPack); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	async function scanRepository() { | ||||
| 		try { | ||||
| 			if (type === 'gitlab') { | ||||
| 				const files = await get(`${apiUrl}/v4/projects/${projectId}/repository/tree`, { | ||||
| 					Authorization: `Bearer ${$session.gitlabToken}` | ||||
| 				}); | ||||
| 				const packageJson = files.find( | ||||
| 					(file) => file.name === 'package.json' && file.type === 'blob' | ||||
| 				); | ||||
| 				const dockerfile = files.find((file) => file.name === 'Dockerfile' && file.type === 'blob'); | ||||
| 				const cargoToml = files.find((file) => file.name === 'Cargo.toml' && file.type === 'blob'); | ||||
| 				const requirementsTxt = files.find( | ||||
| 					(file) => file.name === 'requirements.txt' && file.type === 'blob' | ||||
| 				); | ||||
| 				const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'blob'); | ||||
| 				const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'blob'); | ||||
| 				if (dockerfile) { | ||||
| 					foundConfig.buildPack = 'docker'; | ||||
| 				} else if (packageJson) { | ||||
| 					const path = packageJson.path; | ||||
| 					const data = await get( | ||||
| 						`${apiUrl}/v4/projects/${projectId}/repository/files/${path}/raw?ref=${branch}`, | ||||
| 						{ | ||||
| 							Authorization: `Bearer ${$session.gitlabToken}` | ||||
| 						} | ||||
| 					); | ||||
| 					const json = JSON.parse(data) || {}; | ||||
| 					foundConfig = checkTemplates({ json }); | ||||
| 				} else if (cargoToml) { | ||||
| 					foundConfig = buildPacks.find((bp) => bp.name === 'rust'); | ||||
| 				} else if (requirementsTxt) { | ||||
| 					foundConfig = buildPacks.find((bp) => bp.name === 'python'); | ||||
| 				} else if (indexHtml) { | ||||
| 					foundConfig = buildPacks.find((bp) => bp.name === 'static'); | ||||
| 				} else if (indexPHP) { | ||||
| 					foundConfig = buildPacks.find((bp) => bp.name === 'php'); | ||||
| 				} | ||||
| 			} else if (type === 'github') { | ||||
| 				const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, { | ||||
| 					Authorization: `Bearer ${ghToken}`, | ||||
| 					Accept: 'application/vnd.github.v2.json' | ||||
| 				}); | ||||
| 				const packageJson = files.find( | ||||
| 					(file) => file.name === 'package.json' && file.type === 'file' | ||||
| 				); | ||||
| 				const dockerfile = files.find((file) => file.name === 'Dockerfile' && file.type === 'file'); | ||||
| 				const cargoToml = files.find((file) => file.name === 'Cargo.toml' && file.type === 'file'); | ||||
| 				const requirementsTxt = files.find( | ||||
| 					(file) => file.name === 'requirements.txt' && file.type === 'file' | ||||
| 				); | ||||
| 				const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'file'); | ||||
| 				const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'file'); | ||||
| 				if (dockerfile) { | ||||
| 					foundConfig.buildPack = 'docker'; | ||||
| 				} else if (packageJson) { | ||||
| 					const data = await get(`${packageJson.git_url}`, { | ||||
| 						Authorization: `Bearer ${ghToken}`, | ||||
| 						Accept: 'application/vnd.github.v2.raw' | ||||
| 					}); | ||||
| 					const json = JSON.parse(data) || {}; | ||||
| 					foundConfig = checkTemplates({ json }); | ||||
| 				} else if (cargoToml) { | ||||
| 					foundConfig = buildPacks.find((bp) => bp.name === 'rust'); | ||||
| 				} else if (requirementsTxt) { | ||||
| 					foundConfig = buildPacks.find((bp) => bp.name === 'python'); | ||||
| 				} else if (indexHtml) { | ||||
| 					foundConfig = buildPacks.find((bp) => bp.name === 'static'); | ||||
| 				} else if (indexPHP) { | ||||
| 					foundConfig = buildPacks.find((bp) => bp.name === 'php'); | ||||
| 				} | ||||
| 			} | ||||
| 		} catch (error) { | ||||
| 			if ( | ||||
| 				error.error === 'invalid_token' || | ||||
| 				error.error_description === | ||||
| 					'Token is expired. You can either do re-authorization or token refresh.' || | ||||
| 				error.message === '401 Unauthorized' | ||||
| 			) { | ||||
| 				if (application.gitSource.gitlabAppId) { | ||||
| 					let htmlUrl = application.gitSource.htmlUrl; | ||||
| 					const left = screen.width / 2 - 1020 / 2; | ||||
| 					const top = screen.height / 2 - 618 / 2; | ||||
| 					const newWindow = open( | ||||
| 						`${htmlUrl}/oauth/authorize?client_id=${application.gitSource.gitlabApp.appId}&redirect_uri=${window.location.origin}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${$page.params.id}`, | ||||
| 						'GitLab', | ||||
| 						'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' + | ||||
| 							top + | ||||
| 							', left=' + | ||||
| 							left + | ||||
| 							', toolbar=0, menubar=0, status=0' | ||||
| 					); | ||||
| 					const timer = setInterval(() => { | ||||
| 						if (newWindow?.closed) { | ||||
| 							clearInterval(timer); | ||||
| 							window.location.reload(); | ||||
| 						} | ||||
| 					}, 100); | ||||
| 				} | ||||
| 			} | ||||
| 			return errorNotification(error); | ||||
| 		} finally { | ||||
| 			if (!foundConfig) foundConfig = buildPacks.find((bp) => bp.name === 'node'); | ||||
| 			scanning = false; | ||||
| 		} | ||||
| 	} | ||||
| 	onMount(async () => { | ||||
| 		await scanRepository(); | ||||
| 	}); | ||||
| </script> | ||||
|  | ||||
| <div class="flex space-x-1 p-6 font-bold"> | ||||
| 	<div class="mr-4 text-2xl tracking-tight">Configure Build Pack</div> | ||||
| </div> | ||||
|  | ||||
| {#if scanning} | ||||
| 	<div class="flex justify-center space-x-1 p-6 font-bold"> | ||||
| 		<div class="text-xl tracking-tight">Scanning repository to suggest a build pack for you...</div> | ||||
| 	</div> | ||||
| {:else} | ||||
| 	<div class="max-w-7xl mx-auto flex flex-wrap justify-center"> | ||||
| 		{#each buildPacks as buildPack} | ||||
| 			<div class="p-2"> | ||||
| 				<BuildPack {buildPack} {scanning} bind:foundConfig /> | ||||
| 			</div> | ||||
| 		{/each} | ||||
| 	</div> | ||||
| {/if} | ||||
							
								
								
									
										17
									
								
								src/routes/applications/[id]/configuration/deploykey.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/routes/applications/[id]/configuration/deploykey.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { id } = event.params; | ||||
| 	let { deployKeyId } = await event.request.json(); | ||||
|  | ||||
| 	deployKeyId = Number(deployKeyId); | ||||
|  | ||||
| 	try { | ||||
| 		await db.updateDeployKey({ id, deployKeyId }); | ||||
| 		return { status: 201 }; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
| @@ -0,0 +1,19 @@ | ||||
| import { getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	const { destinationId } = await event.request.json(); | ||||
|  | ||||
| 	try { | ||||
| 		await db.configureDestinationForApplication({ id, destinationId }); | ||||
| 		return { status: 201 }; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
| @@ -0,0 +1,91 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	export const load: Load = async ({ fetch, params, url, stuff }) => { | ||||
| 		const { application } = stuff; | ||||
| 		if (application?.destinationDockerId && !url.searchParams.get('from')) { | ||||
| 			return { | ||||
| 				status: 302, | ||||
| 				redirect: `/applications/${params.id}` | ||||
| 			}; | ||||
| 		} | ||||
| 		const endpoint = `/destinations.json`; | ||||
| 		const res = await fetch(endpoint); | ||||
|  | ||||
| 		if (res.ok) { | ||||
| 			return { | ||||
| 				props: { | ||||
| 					...(await res.json()) | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: res.status, | ||||
| 			error: new Error(`Could not load ${url}`) | ||||
| 		}; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	import type Prisma from '@prisma/client'; | ||||
|  | ||||
| 	import { page } from '$app/stores'; | ||||
| 	import { errorNotification } from '$lib/form'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { post } from '$lib/api'; | ||||
|  | ||||
| 	const { id } = $page.params; | ||||
| 	const from = $page.url.searchParams.get('from'); | ||||
|  | ||||
| 	export let destinations: Prisma.DestinationDocker[]; | ||||
|  | ||||
| 	async function handleSubmit(destinationId) { | ||||
| 		try { | ||||
| 			await post(`/applications/${id}/configuration/destination.json`, { destinationId }); | ||||
| 			return await goto(from || `/applications/${id}/configuration/buildpack`); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <div class="flex space-x-1 p-6 font-bold"> | ||||
| 	<div class="mr-4 text-2xl tracking-tight">Configure Destination</div> | ||||
| </div> | ||||
| <div class="flex justify-center"> | ||||
| 	{#if !destinations || destinations.length === 0} | ||||
| 		<div class="flex-col"> | ||||
| 			<div class="pb-2">No configurable Destination found</div> | ||||
| 			<div class="flex justify-center"> | ||||
| 				<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500"> | ||||
| 					<svg | ||||
| 						class="w-6" | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						fill="none" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke="currentColor" | ||||
| 						><path | ||||
| 							stroke-linecap="round" | ||||
| 							stroke-linejoin="round" | ||||
| 							stroke-width="2" | ||||
| 							d="M12 6v6m0 0v6m0-6h6m-6 0H6" | ||||
| 						/></svg | ||||
| 					> | ||||
| 				</a> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	{:else} | ||||
| 		<div class="flex flex-wrap justify-center"> | ||||
| 			{#each destinations as destination} | ||||
| 				<div class="p-2"> | ||||
| 					<form on:submit|preventDefault={() => handleSubmit(destination.id)}> | ||||
| 						<button type="submit" class="box-selection hover:bg-sky-700 font-bold"> | ||||
| 							<div class="font-bold text-xl text-center truncate">{destination.name}</div> | ||||
| 							<div class="text-center truncate">{destination.network}</div> | ||||
| 						</button> | ||||
| 					</form> | ||||
| 				</div> | ||||
| 			{/each} | ||||
| 		</div> | ||||
| 	{/if} | ||||
| </div> | ||||
| @@ -0,0 +1,47 @@ | ||||
| import { getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const get: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
|  | ||||
| 	const repository = event.url.searchParams.get('repository')?.toLocaleLowerCase() || undefined; | ||||
| 	const branch = event.url.searchParams.get('branch')?.toLocaleLowerCase() || undefined; | ||||
|  | ||||
| 	try { | ||||
| 		const found = await db.isBranchAlreadyUsed({ repository, branch, id }); | ||||
| 		if (found) { | ||||
| 			throw { | ||||
| 				error: `Branch ${branch} is already used by another application` | ||||
| 			}; | ||||
| 		} | ||||
| 		return { | ||||
| 			status: 200 | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	let { repository, branch, projectId, webhookToken } = await event.request.json(); | ||||
|  | ||||
| 	repository = repository.toLowerCase(); | ||||
| 	branch = branch.toLowerCase(); | ||||
| 	projectId = Number(projectId); | ||||
|  | ||||
| 	try { | ||||
| 		await db.configureGitRepository({ id, repository, branch, projectId, webhookToken }); | ||||
| 		return { status: 201 }; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										38
									
								
								src/routes/applications/[id]/configuration/repository.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/routes/applications/[id]/configuration/repository.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	export const load: Load = async ({ params, url, stuff }) => { | ||||
| 		const { application, githubToken, appId } = stuff; | ||||
| 		if (application?.branch && application?.repository && !url.searchParams.get('from')) { | ||||
| 			return { | ||||
| 				status: 302, | ||||
| 				redirect: `/applications/${params.id}` | ||||
| 			}; | ||||
| 		} | ||||
| 		return { | ||||
| 			props: { | ||||
| 				githubToken, | ||||
| 				application, | ||||
| 				appId | ||||
| 			} | ||||
| 		}; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	export let application; | ||||
| 	export let githubToken; | ||||
| 	export let appId; | ||||
| 	import GithubRepositories from './_GithubRepositories.svelte'; | ||||
| 	import GitlabRepositories from './_GitlabRepositories.svelte'; | ||||
| </script> | ||||
|  | ||||
| <div class="flex space-x-1 p-6 font-bold"> | ||||
| 	<div class="mr-4 text-2xl tracking-tight">Select a Repository / Project</div> | ||||
| </div> | ||||
| <div class="flex flex-wrap justify-center"> | ||||
| 	{#if application.gitSource.type === 'github'} | ||||
| 		<GithubRepositories {application} {githubToken} /> | ||||
| 	{:else if application.gitSource.type === 'gitlab'} | ||||
| 		<GitlabRepositories {application} {appId} /> | ||||
| 	{/if} | ||||
| </div> | ||||
							
								
								
									
										18
									
								
								src/routes/applications/[id]/configuration/source.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/routes/applications/[id]/configuration/source.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import { getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	const { gitSourceId } = await event.request.json(); | ||||
| 	try { | ||||
| 		await db.configureGitsource({ id, gitSourceId }); | ||||
| 		return { status: 201 }; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										109
									
								
								src/routes/applications/[id]/configuration/source.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/routes/applications/[id]/configuration/source.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	export const load: Load = async ({ fetch, params, url, stuff }) => { | ||||
| 		const { application } = stuff; | ||||
| 		if (application?.gitSourceId && !url.searchParams.get('from')) { | ||||
| 			return { | ||||
| 				status: 302, | ||||
| 				redirect: `/applications/${params.id}` | ||||
| 			}; | ||||
| 		} | ||||
| 		const endpoint = `/sources.json`; | ||||
| 		const res = await fetch(endpoint); | ||||
|  | ||||
| 		if (res.ok) { | ||||
| 			return { | ||||
| 				props: { | ||||
| 					...(await res.json()) | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: res.status, | ||||
| 			error: new Error(`Could not load ${url}`) | ||||
| 		}; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	import type Prisma from '@prisma/client'; | ||||
|  | ||||
| 	import { page } from '$app/stores'; | ||||
| 	import { enhance, errorNotification } from '$lib/form'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { post } from '$lib/api'; | ||||
|  | ||||
| 	const { id } = $page.params; | ||||
| 	const from = $page.url.searchParams.get('from'); | ||||
|  | ||||
| 	export let sources: Prisma.GitSource[] & { | ||||
| 		gitlabApp: Prisma.GitlabApp; | ||||
| 		githubApp: Prisma.GithubApp; | ||||
| 	}; | ||||
| 	sources = sources.filter( | ||||
| 		(source) => | ||||
| 			(source.type === 'github' && source.githubAppId && source.githubApp.installationId) || | ||||
| 			(source.type === 'gitlab' && source.gitlabAppId) | ||||
| 	); | ||||
| 	async function handleSubmit(gitSourceId) { | ||||
| 		try { | ||||
| 			await post(`/applications/${id}/configuration/source.json`, { gitSourceId }); | ||||
| 			return await goto(from || `/applications/${id}/configuration/repository`); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <div class="flex space-x-1 p-6 font-bold"> | ||||
| 	<div class="mr-4 text-2xl tracking-tight">Select a Git Source</div> | ||||
| </div> | ||||
| <div class="flex justify-center"> | ||||
| 	{#if !sources || sources.length === 0} | ||||
| 		<div class="flex-col"> | ||||
| 			<div class="pb-2">No configurable Git Source found</div> | ||||
| 			<div class="flex justify-center"> | ||||
| 				<a href="/new/source" sveltekit:prefetch class="add-icon bg-orange-600 hover:bg-orange-500"> | ||||
| 					<svg | ||||
| 						class="w-6" | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						fill="none" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke="currentColor" | ||||
| 						><path | ||||
| 							stroke-linecap="round" | ||||
| 							stroke-linejoin="round" | ||||
| 							stroke-width="2" | ||||
| 							d="M12 6v6m0 0v6m0-6h6m-6 0H6" | ||||
| 						/></svg | ||||
| 					> | ||||
| 				</a> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	{:else} | ||||
| 		<div class="flex flex-wrap justify-center"> | ||||
| 			{#each sources as source} | ||||
| 				<div class="p-2"> | ||||
| 					<form on:submit|preventDefault={() => handleSubmit(source.id)}> | ||||
| 						<button | ||||
| 							disabled={source.gitlabApp && !source.gitlabAppId} | ||||
| 							type="submit" | ||||
| 							class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group" | ||||
| 							class:border-red-500={source.gitlabApp && !source.gitlabAppId} | ||||
| 							class:border-0={source.gitlabApp && !source.gitlabAppId} | ||||
| 							class:border-l-4={source.gitlabApp && !source.gitlabAppId} | ||||
| 						> | ||||
| 							<div class="font-bold text-xl text-center truncate">{source.name}</div> | ||||
| 							{#if source.gitlabApp && !source.gitlabAppId} | ||||
| 								<div class="font-bold text-center truncate text-red-500 group-hover:text-white"> | ||||
| 									Configuration missing | ||||
| 								</div> | ||||
| 							{/if} | ||||
| 						</button> | ||||
| 					</form> | ||||
| 				</div> | ||||
| 			{/each} | ||||
| 		</div> | ||||
| 	{/if} | ||||
| </div> | ||||
							
								
								
									
										20
									
								
								src/routes/applications/[id]/configuration/sshkey.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/routes/applications/[id]/configuration/sshkey.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const get: RequestHandler = async (event) => { | ||||
| 	const { id } = event.params; | ||||
| 	try { | ||||
| 		return await db.getSshKey({ id }); | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { id } = event.params; | ||||
| 	try { | ||||
| 		return await db.generateSshKey({ id }); | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										19
									
								
								src/routes/applications/[id]/delete.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/routes/applications/[id]/delete.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import { getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const del: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	try { | ||||
| 		await db.removeApplication({ id, teamId }); | ||||
| 		return { | ||||
| 			status: 200 | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										42
									
								
								src/routes/applications/[id]/deploy.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/routes/applications/[id]/deploy.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import * as db from '$lib/database'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
| import cuid from 'cuid'; | ||||
| import crypto from 'crypto'; | ||||
| import { buildQueue } from '$lib/queues'; | ||||
| import { getUserDetails } from '$lib/common'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
|  | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	try { | ||||
| 		const buildId = cuid(); | ||||
| 		const applicationFound = await db.getApplication({ id, teamId }); | ||||
| 		if (!applicationFound.configHash) { | ||||
| 			const configHash = crypto | ||||
| 				.createHash('sha256') | ||||
| 				.update( | ||||
| 					JSON.stringify({ | ||||
| 						buildPack: applicationFound.buildPack, | ||||
| 						port: applicationFound.port, | ||||
| 						installCommand: applicationFound.installCommand, | ||||
| 						buildCommand: applicationFound.buildCommand, | ||||
| 						startCommand: applicationFound.startCommand | ||||
| 					}) | ||||
| 				) | ||||
| 				.digest('hex'); | ||||
| 			await db.prisma.application.update({ where: { id }, data: { configHash } }); | ||||
| 		} | ||||
| 		await buildQueue.add(buildId, { build_id: buildId, type: 'manual', ...applicationFound }); | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				buildId | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										87
									
								
								src/routes/applications/[id]/index.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/routes/applications/[id]/index.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| import { getTeam, getUserDetails } from '$lib/common'; | ||||
| import { getGithubToken } from '$lib/components/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
| import { checkContainer } from '$lib/haproxy'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
| import jsonwebtoken from 'jsonwebtoken'; | ||||
|  | ||||
| export const get: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const appId = process.env['COOLIFY_APP_ID']; | ||||
| 	let githubToken = null; | ||||
| 	let ghToken = null; | ||||
| 	let isRunning = false; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	try { | ||||
| 		const application = await db.getApplication({ id, teamId }); | ||||
| 		const { gitSource } = application; | ||||
| 		if (gitSource?.type === 'github' && gitSource?.githubApp) { | ||||
| 			const payload = { | ||||
| 				iat: Math.round(new Date().getTime() / 1000), | ||||
| 				exp: Math.round(new Date().getTime() / 1000 + 60), | ||||
| 				iss: gitSource.githubApp.appId | ||||
| 			}; | ||||
| 			githubToken = jsonwebtoken.sign(payload, gitSource.githubApp.privateKey, { | ||||
| 				algorithm: 'RS256' | ||||
| 			}); | ||||
| 			ghToken = await getGithubToken({ apiUrl: gitSource.apiUrl, application, githubToken }); | ||||
| 		} | ||||
| 		if (application.destinationDockerId) { | ||||
| 			isRunning = await checkContainer(application.destinationDocker.engine, id); | ||||
| 		} | ||||
| 		return { | ||||
| 			body: { | ||||
| 				isRunning, | ||||
| 				ghToken, | ||||
| 				githubToken, | ||||
| 				application, | ||||
| 				appId | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		console.log(error); | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	let { | ||||
| 		name, | ||||
| 		buildPack, | ||||
| 		fqdn, | ||||
| 		port, | ||||
| 		installCommand, | ||||
| 		buildCommand, | ||||
| 		startCommand, | ||||
| 		baseDirectory, | ||||
| 		publishDirectory | ||||
| 	} = await event.request.json(); | ||||
|  | ||||
| 	if (port) port = Number(port); | ||||
|  | ||||
| 	try { | ||||
| 		await db.configureApplication({ | ||||
| 			id, | ||||
| 			buildPack, | ||||
| 			name, | ||||
| 			fqdn, | ||||
| 			port, | ||||
| 			installCommand, | ||||
| 			buildCommand, | ||||
| 			startCommand, | ||||
| 			baseDirectory, | ||||
| 			publishDirectory | ||||
| 		}); | ||||
| 		return { status: 201 }; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										390
									
								
								src/routes/applications/[id]/index.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										390
									
								
								src/routes/applications/[id]/index.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,390 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	export const load: Load = async ({ fetch, params, stuff }) => { | ||||
| 		if (stuff?.application?.id) { | ||||
| 			return { | ||||
| 				props: { | ||||
| 					application: stuff.application, | ||||
| 					isRunning: stuff.isRunning | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 		const endpoint = `/applications/${params.id}.json`; | ||||
| 		const res = await fetch(endpoint); | ||||
|  | ||||
| 		if (res.ok) { | ||||
| 			return { | ||||
| 				props: { | ||||
| 					...(await res.json()) | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: res.status, | ||||
| 			error: new Error(`Could not load ${endpoint}`) | ||||
| 		}; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	export let application: Prisma.Application & { | ||||
| 		settings: Prisma.ApplicationSettings; | ||||
| 		gitlabApp: Prisma.GitlabApp; | ||||
| 		gitSource: Prisma.GitSource; | ||||
| 		destinationDocker: Prisma.DestinationDocker; | ||||
| 	}; | ||||
| 	export let isRunning; | ||||
| 	import { page, session } from '$app/stores'; | ||||
| 	import { errorNotification } from '$lib/form'; | ||||
| 	import { onMount } from 'svelte'; | ||||
|  | ||||
| 	import Explainer from '$lib/components/Explainer.svelte'; | ||||
| 	import Setting from '$lib/components/Setting.svelte'; | ||||
| 	import type Prisma from '@prisma/client'; | ||||
| 	import { getDomain, notNodeDeployments, staticDeployments } from '$lib/components/common'; | ||||
| 	import { toast } from '@zerodevx/svelte-toast'; | ||||
| 	import { post } from '$lib/api'; | ||||
| 	const { id } = $page.params; | ||||
|  | ||||
| 	let domainEl: HTMLInputElement; | ||||
|  | ||||
| 	let loading = false; | ||||
| 	let debug = application.settings.debug; | ||||
| 	let previews = application.settings.previews; | ||||
|  | ||||
| 	onMount(() => { | ||||
| 		domainEl.focus(); | ||||
| 	}); | ||||
|  | ||||
| 	async function changeSettings(name) { | ||||
| 		if (name === 'debug') { | ||||
| 			debug = !debug; | ||||
| 		} | ||||
| 		if (name === 'previews') { | ||||
| 			previews = !previews; | ||||
| 		} | ||||
| 		try { | ||||
| 			await post(`/applications/${id}/settings.json`, { previews, debug }); | ||||
| 			return toast.push('Settings saved.'); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| 	async function handleSubmit() { | ||||
| 		loading = true; | ||||
| 		try { | ||||
| 			await post(`/applications/${id}/check.json`, { fqdn: application.fqdn }); | ||||
| 			await post(`/applications/${id}.json`, { ...application }); | ||||
| 			return window.location.reload(); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} finally { | ||||
| 			loading = false; | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <div class="flex items-center space-x-2 p-5 px-6 font-bold"> | ||||
| 	<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block"> | ||||
| 		{application.name} | ||||
| 	</div> | ||||
| 	{#if application.fqdn} | ||||
| 		<a | ||||
| 			href={application.fqdn} | ||||
| 			target="_blank" | ||||
| 			class="icons tooltip-bottom flex items-center bg-transparent text-sm" | ||||
| 			><svg | ||||
| 				xmlns="http://www.w3.org/2000/svg" | ||||
| 				class="h-6 w-6" | ||||
| 				viewBox="0 0 24 24" | ||||
| 				stroke-width="1.5" | ||||
| 				stroke="currentColor" | ||||
| 				fill="none" | ||||
| 				stroke-linecap="round" | ||||
| 				stroke-linejoin="round" | ||||
| 			> | ||||
| 				<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 				<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" /> | ||||
| 				<line x1="10" y1="14" x2="20" y2="4" /> | ||||
| 				<polyline points="15 4 20 4 20 9" /> | ||||
| 			</svg></a | ||||
| 		> | ||||
| 	{/if} | ||||
| 	<a | ||||
| 		href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}" | ||||
| 		target="_blank" | ||||
| 		class="w-10" | ||||
| 	> | ||||
| 		{#if application.gitSource?.type === 'gitlab'} | ||||
| 			<svg viewBox="0 0 128 128" class="icons"> | ||||
| 				<path | ||||
| 					fill="#FC6D26" | ||||
| 					d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357" | ||||
| 				/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path | ||||
| 					fill="#FC6D26" | ||||
| 					d="M64 121.894l-23.144-71.23H8.42L64 121.893z" | ||||
| 				/><path | ||||
| 					fill="#FCA326" | ||||
| 					d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z" | ||||
| 				/><path | ||||
| 					fill="#E24329" | ||||
| 					d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z" | ||||
| 				/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path | ||||
| 					fill="#FCA326" | ||||
| 					d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z" | ||||
| 				/><path | ||||
| 					fill="#E24329" | ||||
| 					d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z" | ||||
| 				/> | ||||
| 			</svg> | ||||
| 		{:else if application.gitSource?.type === 'github'} | ||||
| 			<svg viewBox="0 0 128 128" class="icons"> | ||||
| 				<g fill="#ffffff" | ||||
| 					><path | ||||
| 						fill-rule="evenodd" | ||||
| 						clip-rule="evenodd" | ||||
| 						d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z" | ||||
| 					/><path | ||||
| 						d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0" | ||||
| 					/></g | ||||
| 				> | ||||
| 			</svg> | ||||
| 		{/if} | ||||
| 	</a> | ||||
| </div> | ||||
|  | ||||
| <div class="mx-auto max-w-4xl px-6"> | ||||
| 	<!-- svelte-ignore missing-declaration --> | ||||
| 	<form on:submit|preventDefault={handleSubmit} class="py-4"> | ||||
| 		<div class="flex space-x-1 pb-5 font-bold"> | ||||
| 			<div class="title">General</div> | ||||
| 			{#if $session.isAdmin} | ||||
| 				<button | ||||
| 					type="submit" | ||||
| 					class:bg-green-600={!loading} | ||||
| 					class:hover:bg-green-500={!loading} | ||||
| 					disabled={loading}>{loading ? 'Saving...' : 'Save'}</button | ||||
| 				> | ||||
| 			{/if} | ||||
| 		</div> | ||||
| 		<div class="grid grid-flow-row gap-2 px-10"> | ||||
| 			<div class="mt-2 grid grid-cols-3 items-center"> | ||||
| 				<label for="name">Name</label> | ||||
| 				<div class="col-span-2 "> | ||||
| 					<input | ||||
| 						readonly={!$session.isAdmin} | ||||
| 						name="name" | ||||
| 						id="name" | ||||
| 						bind:value={application.name} | ||||
| 						required | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="grid grid-cols-3 items-center"> | ||||
| 				<label for="gitSource">Git Source</label> | ||||
| 				<div class="col-span-2"> | ||||
| 					<a | ||||
| 						href={$session.isAdmin | ||||
| 							? `/applications/${id}/configuration/source?from=/applications/${id}` | ||||
| 							: ''} | ||||
| 						class="no-underline" | ||||
| 						><input | ||||
| 							value={application.gitSource.name} | ||||
| 							id="gitSource" | ||||
| 							disabled | ||||
| 							class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500" | ||||
| 						/></a | ||||
| 					> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="grid grid-cols-3 items-center"> | ||||
| 				<label for="repository">Git Repository</label> | ||||
| 				<div class="col-span-2"> | ||||
| 					<a | ||||
| 						href={$session.isAdmin | ||||
| 							? `/applications/${id}/configuration/repository?from=/applications/${id}&to=/applications/${id}/configuration/buildpack` | ||||
| 							: ''} | ||||
| 						class="no-underline" | ||||
| 						><input | ||||
| 							value="{application.repository}/{application.branch}" | ||||
| 							id="repository" | ||||
| 							disabled | ||||
| 							class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500" | ||||
| 						/></a | ||||
| 					> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="grid grid-cols-3 items-center"> | ||||
| 				<label for="buildPack">Build Pack</label> | ||||
| 				<div class="col-span-2"> | ||||
| 					<a | ||||
| 						href={$session.isAdmin | ||||
| 							? `/applications/${id}/configuration/buildpack?from=/applications/${id}` | ||||
| 							: ''} | ||||
| 						class="no-underline " | ||||
| 					> | ||||
| 						<input | ||||
| 							value={application.buildPack} | ||||
| 							id="buildPack" | ||||
| 							disabled | ||||
| 							class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500" | ||||
| 						/></a | ||||
| 					> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="grid grid-cols-3 items-center pb-8"> | ||||
| 				<label for="destination">Destination</label> | ||||
| 				<div class="col-span-2"> | ||||
| 					<div class="no-underline"> | ||||
| 						<input | ||||
| 							value={application.destinationDocker.name} | ||||
| 							id="destination" | ||||
| 							disabled | ||||
| 							class="bg-transparent " | ||||
| 						/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="flex space-x-1 py-5 font-bold"> | ||||
| 			<div class="title">Application</div> | ||||
| 		</div> | ||||
| 		<div class="grid grid-flow-row gap-2 px-10"> | ||||
| 			<div class="grid grid-cols-3"> | ||||
| 				<label for="fqdn" class="pt-2">Domain (FQDN)</label> | ||||
| 				<div class="col-span-2"> | ||||
| 					<input | ||||
| 						readonly={!$session.isAdmin || isRunning} | ||||
| 						disabled={!$session.isAdmin || isRunning} | ||||
| 						bind:this={domainEl} | ||||
| 						name="fqdn" | ||||
| 						id="fqdn" | ||||
| 						bind:value={application.fqdn} | ||||
| 						pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$" | ||||
| 						placeholder="eg: https://coollabs.io" | ||||
| 						required | ||||
| 					/> | ||||
| 					<Explainer | ||||
| 						text="If you specify <span class='text-green-600 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>To modify the domain, you must first stop the application." | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
|  | ||||
| 			{#if !staticDeployments.includes(application.buildPack)} | ||||
| 				<div class="grid grid-cols-3 items-center"> | ||||
| 					<label for="port">Port</label> | ||||
| 					<div class="col-span-2"> | ||||
| 						<input | ||||
| 							readonly={!$session.isAdmin} | ||||
| 							name="port" | ||||
| 							id="port" | ||||
| 							bind:value={application.port} | ||||
| 							placeholder="default: 3000" | ||||
| 						/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			{/if} | ||||
| 			{#if !notNodeDeployments.includes(application.buildPack)} | ||||
| 				<div class="grid grid-cols-3 items-center"> | ||||
| 					<label for="installCommand">Install Command</label> | ||||
| 					<div class="col-span-2"> | ||||
| 						<input | ||||
| 							readonly={!$session.isAdmin} | ||||
| 							name="installCommand" | ||||
| 							id="installCommand" | ||||
| 							bind:value={application.installCommand} | ||||
| 							placeholder="default: yarn install" | ||||
| 						/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div class="grid grid-cols-3 items-center"> | ||||
| 					<label for="buildCommand">Build Command</label> | ||||
| 					<div class="col-span-2"> | ||||
| 						<input | ||||
| 							readonly={!$session.isAdmin} | ||||
| 							name="buildCommand" | ||||
| 							id="buildCommand" | ||||
| 							bind:value={application.buildCommand} | ||||
| 							placeholder="default: yarn build" | ||||
| 						/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div class="grid grid-cols-3 items-center"> | ||||
| 					<label for="startCommand" class="">Start Command</label> | ||||
| 					<div class="col-span-2"> | ||||
| 						<input | ||||
| 							readonly={!$session.isAdmin} | ||||
| 							name="startCommand" | ||||
| 							id="startCommand" | ||||
| 							bind:value={application.startCommand} | ||||
| 							placeholder="default: yarn start" | ||||
| 						/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			{/if} | ||||
|  | ||||
| 			<div class="grid grid-cols-3"> | ||||
| 				<label for="baseDirectory" class="pt-2">Base Directory</label> | ||||
| 				<div class="col-span-2"> | ||||
| 					<input | ||||
| 						readonly={!$session.isAdmin} | ||||
| 						name="baseDirectory" | ||||
| 						id="baseDirectory" | ||||
| 						bind:value={application.baseDirectory} | ||||
| 						placeholder="default: /" | ||||
| 					/> | ||||
| 					<Explainer | ||||
| 						text="Directory to use as the base of all commands. <br> Could be useful with monorepos." | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			{#if !notNodeDeployments.includes(application.buildPack)} | ||||
| 				<div class="grid grid-cols-3"> | ||||
| 					<label for="publishDirectory" class="pt-2">Publish Directory</label> | ||||
| 					<div class="col-span-2"> | ||||
| 						<input | ||||
| 							readonly={!$session.isAdmin} | ||||
| 							name="publishDirectory" | ||||
| 							id="publishDirectory" | ||||
| 							bind:value={application.publishDirectory} | ||||
| 							placeholder=" default: /" | ||||
| 						/> | ||||
| 						<Explainer | ||||
| 							text="Directory containing all the assets for deployment. <br> For example: <span class='text-green-600 font-bold'>dist</span>,<span class='text-green-600 font-bold'>_site</span> or <span class='text-green-600 font-bold'>public</span>." | ||||
| 						/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			{/if} | ||||
| 		</div> | ||||
| 	</form> | ||||
| 	<div class="flex space-x-1 pb-5 font-bold"> | ||||
| 		<div class="title">Features</div> | ||||
| 	</div> | ||||
| 	<div class="px-4 pb-10 sm:px-6"> | ||||
| 		<!-- <ul class="mt-2 divide-y divide-stone-800"> | ||||
| 			<Setting | ||||
| 				bind:setting={forceSSL} | ||||
| 				on:click={() => changeSettings('forceSSL')} | ||||
| 				title="Force https" | ||||
| 				description="Creates a https redirect for all requests from http and also generates a https certificate for the domain through Let's Encrypt." | ||||
| 			/> | ||||
| 		</ul> --> | ||||
| 		<ul class="mt-2 divide-y divide-stone-800"> | ||||
| 			<Setting | ||||
| 				bind:setting={previews} | ||||
| 				on:click={() => changeSettings('previews')} | ||||
| 				title="Enable MR/PR Previews" | ||||
| 				description="Creates previews from pull and merge requests." | ||||
| 			/> | ||||
| 		</ul> | ||||
| 		<ul class="mt-2 divide-y divide-stone-800"> | ||||
| 			<Setting | ||||
| 				bind:setting={debug} | ||||
| 				on:click={() => changeSettings('debug')} | ||||
| 				title="Debug Logs" | ||||
| 				description="Enable debug logs during build phase. <br>(<span class='text-red-500'>sensitive information</span> could be visible in logs)" | ||||
| 			/> | ||||
| 		</ul> | ||||
| 	</div> | ||||
| </div> | ||||
							
								
								
									
										41
									
								
								src/routes/applications/[id]/logs/_Loading.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/routes/applications/[id]/logs/_Loading.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| <div class="lds-ripple absolute left-0"> | ||||
| 	<div /> | ||||
| 	<div /> | ||||
| </div> | ||||
|  | ||||
| <style> | ||||
| 	.lds-ripple { | ||||
| 		display: inline-block; | ||||
| 		position: relative; | ||||
| 		left: -19px; | ||||
| 		top: -8px; | ||||
| 		width: 40px; | ||||
| 		height: 40px; | ||||
| 	} | ||||
| 	.lds-ripple div { | ||||
| 		position: absolute; | ||||
| 		border: 4px solid #fff; | ||||
| 		opacity: 1; | ||||
| 		border-radius: 50%; | ||||
| 		animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite; | ||||
| 	} | ||||
| 	.lds-ripple div:nth-child(2) { | ||||
| 		animation-delay: -0.5s; | ||||
| 	} | ||||
| 	@keyframes lds-ripple { | ||||
| 		0% { | ||||
| 			top: 1px; | ||||
| 			left: 1px; | ||||
| 			width: 0; | ||||
| 			height: 0; | ||||
| 			opacity: 1; | ||||
| 		} | ||||
| 		100% { | ||||
| 			top: 0px; | ||||
| 			left: 0px; | ||||
| 			width: 36px; | ||||
| 			height: 36px; | ||||
| 			opacity: 0; | ||||
| 		} | ||||
| 	} | ||||
| </style> | ||||
							
								
								
									
										75
									
								
								src/routes/applications/[id]/logs/build/_BuildLog.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/routes/applications/[id]/logs/build/_BuildLog.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| <script lang="ts"> | ||||
| 	export let buildId; | ||||
|  | ||||
| 	import { createEventDispatcher, onDestroy, onMount } from 'svelte'; | ||||
| 	const dispatch = createEventDispatcher(); | ||||
|  | ||||
| 	import { page } from '$app/stores'; | ||||
|  | ||||
| 	import Loading from '$lib/components/Loading.svelte'; | ||||
| 	import LoadingLogs from '../_Loading.svelte'; | ||||
| 	import { get } from '$lib/api'; | ||||
| 	import { errorNotification } from '$lib/form'; | ||||
|  | ||||
| 	let logs = []; | ||||
| 	let loading = true; | ||||
| 	let currentStatus; | ||||
| 	let streamInterval; | ||||
|  | ||||
| 	const { id } = $page.params; | ||||
|  | ||||
| 	async function streamLogs(sequence = 0) { | ||||
| 		try { | ||||
| 			let { logs: responseLogs, status } = await get( | ||||
| 				`/applications/${id}/logs/build/build.json?buildId=${buildId}&sequence=${sequence}` | ||||
| 			); | ||||
| 			currentStatus = status; | ||||
| 			logs = logs.concat(responseLogs); | ||||
| 			loading = false; | ||||
| 			streamInterval = setInterval(async () => { | ||||
| 				if (status !== 'running') { | ||||
| 					clearInterval(streamInterval); | ||||
| 					return; | ||||
| 				} | ||||
| 				const nextSequence = logs[logs.length - 1].time; | ||||
| 				try { | ||||
| 					const data = await get( | ||||
| 						`/applications/${id}/logs/build/build.json?buildId=${buildId}&sequence=${nextSequence}` | ||||
| 					); | ||||
| 					status = data.status; | ||||
| 					currentStatus = status; | ||||
| 					logs = logs.concat(data.logs); | ||||
| 					dispatch('updateBuildStatus', { status }); | ||||
| 				} catch ({ error }) { | ||||
| 					return errorNotification(error); | ||||
| 				} | ||||
| 			}, 1000); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| 	onDestroy(() => { | ||||
| 		clearInterval(streamInterval); | ||||
| 	}); | ||||
| 	onMount(async () => { | ||||
| 		window.scrollTo(0, 0); | ||||
| 		await streamLogs(); | ||||
| 	}); | ||||
| </script> | ||||
|  | ||||
| {#if loading} | ||||
| 	<Loading /> | ||||
| {:else} | ||||
| 	<div class="relative"> | ||||
| 		{#if currentStatus === 'running'} | ||||
| 			<LoadingLogs /> | ||||
| 		{/if} | ||||
| 		<div | ||||
| 			class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words" | ||||
| 		> | ||||
| 			{#each logs as log} | ||||
| 				<div>{log.line + '\n'}</div> | ||||
| 			{/each} | ||||
| 		</div> | ||||
| 	</div> | ||||
| {/if} | ||||
							
								
								
									
										28
									
								
								src/routes/applications/[id]/logs/build/build.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/routes/applications/[id]/logs/build/build.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| import { getTeam, getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const get: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const buildId = event.url.searchParams.get('buildId'); | ||||
| 	const sequence = Number(event.url.searchParams.get('sequence')); | ||||
| 	try { | ||||
| 		let logs = await db.prisma.buildLog.findMany({ | ||||
| 			where: { buildId, time: { gt: sequence } }, | ||||
| 			orderBy: { time: 'asc' } | ||||
| 		}); | ||||
| 		const { status } = await db.prisma.build.findFirst({ where: { id: buildId } }); | ||||
|  | ||||
| 		return { | ||||
| 			body: { | ||||
| 				logs, | ||||
| 				status | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										40
									
								
								src/routes/applications/[id]/logs/build/index.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/routes/applications/[id]/logs/build/index.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
| import { dayjs } from '$lib/dayjs'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const get: RequestHandler = async (event) => { | ||||
| 	const { id } = event.params; | ||||
| 	const buildId = event.url.searchParams.get('buildId'); | ||||
| 	const skip = Number(event.url.searchParams.get('skip')) || 0; | ||||
|  | ||||
| 	let builds = []; | ||||
| 	try { | ||||
| 		const buildCount = await db.prisma.build.count({ where: { applicationId: id } }); | ||||
| 		if (buildId) { | ||||
| 			builds = await db.prisma.build.findMany({ where: { applicationId: id, id: buildId } }); | ||||
| 		} else { | ||||
| 			builds = await db.prisma.build.findMany({ | ||||
| 				where: { applicationId: id }, | ||||
| 				orderBy: { createdAt: 'desc' }, | ||||
| 				take: 5, | ||||
| 				skip | ||||
| 			}); | ||||
| 		} | ||||
| 		builds = builds.map((build) => { | ||||
| 			const updatedAt = dayjs(build.updatedAt).utc(); | ||||
| 			build.took = updatedAt.diff(dayjs(build.createdAt)) / 1000; | ||||
| 			build.since = updatedAt.fromNow(); | ||||
| 			return build; | ||||
| 		}); | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				builds, | ||||
| 				buildCount | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										143
									
								
								src/routes/applications/[id]/logs/build/index.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								src/routes/applications/[id]/logs/build/index.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	export const load: Load = async ({ fetch, params, url, stuff }) => { | ||||
| 		let endpoint = `/applications/${params.id}/logs/build.json?skip=0`; | ||||
| 		const res = await fetch(endpoint); | ||||
| 		if (res.ok) { | ||||
| 			return { | ||||
| 				props: { | ||||
| 					application: stuff.application, | ||||
| 					...(await res.json()) | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: res.status, | ||||
| 			error: new Error(`Could not load ${url}`) | ||||
| 		}; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	import { page } from '$app/stores'; | ||||
| 	import { dateOptions, getDomain } from '$lib/components/common'; | ||||
|  | ||||
| 	import BuildLog from './_BuildLog.svelte'; | ||||
| 	import { get } from '$lib/api'; | ||||
| 	import { errorNotification } from '$lib/form'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
|  | ||||
| 	export let builds; | ||||
| 	export let application; | ||||
| 	export let buildCount; | ||||
|  | ||||
| 	let buildId; | ||||
| 	$: buildId; | ||||
|  | ||||
| 	let skip = 0; | ||||
| 	let noMoreBuilds = buildCount < 5 || buildCount <= skip; | ||||
| 	const { id } = $page.params; | ||||
| 	let preselectedBuildId = $page.url.searchParams.get('buildId'); | ||||
| 	if (preselectedBuildId) buildId = preselectedBuildId; | ||||
|  | ||||
| 	async function updateBuildStatus({ detail }) { | ||||
| 		const { status } = detail; | ||||
| 		if (status !== 'running') { | ||||
| 			try { | ||||
| 				const data = await get(`/applications/${id}/logs/build.json?buildId=${buildId}`); | ||||
| 				builds = builds.filter((build) => { | ||||
| 					if (build.id === data.builds[0].id) { | ||||
| 						build.status = data.builds[0].status; | ||||
| 						build.took = data.builds[0].took; | ||||
| 						build.since = data.builds[0].since; | ||||
| 					} | ||||
| 					window.location.reload(); | ||||
| 					return build; | ||||
| 				}); | ||||
| 				return; | ||||
| 			} catch ({ error }) { | ||||
| 				return errorNotification(error); | ||||
| 			} | ||||
| 		} else { | ||||
| 			builds = builds.filter((build) => { | ||||
| 				if (build.id === buildId) build.status = status; | ||||
| 				return build; | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 	async function loadMoreBuilds() { | ||||
| 		if (buildCount >= skip) { | ||||
| 			skip = skip + 5; | ||||
| 			noMoreBuilds = buildCount >= skip; | ||||
| 			try { | ||||
| 				const data = await get(`/applications/${id}/logs/build.json?skip=${skip}`); | ||||
| 				builds = builds.concat(data.builds); | ||||
| 				return; | ||||
| 			} catch ({ error }) { | ||||
| 				return errorNotification(error); | ||||
| 			} | ||||
| 		} else { | ||||
| 			noMoreBuilds = true; | ||||
| 		} | ||||
| 	} | ||||
| 	async function loadBuild(build) { | ||||
| 		buildId = build; | ||||
| 		goto(`/applications/${id}/logs/build?buildId=${buildId}`); | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <div class="flex space-x-1 p-6 font-bold"> | ||||
| 	<div class="mr-4 text-2xl tracking-tight"> | ||||
| 		Build logs of <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a> | ||||
| 	</div> | ||||
| </div> | ||||
| <div class="flex flex-row justify-start space-x-2 px-10 pt-6 "> | ||||
| 	<div class="min-w-[16rem] space-y-2"> | ||||
| 		{#each builds as build (build.id)} | ||||
| 			<div | ||||
| 				data-tooltip={new Intl.DateTimeFormat('default', dateOptions).format( | ||||
| 					new Date(build.createdAt) | ||||
| 				) + `\n${build.status}`} | ||||
| 				on:click={() => loadBuild(build.id)} | ||||
| 				class="tooltip-top flex cursor-pointer items-center justify-center rounded-r border-l-2 border-transparent py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl" | ||||
| 				class:bg-coolgray-400={buildId === build.id} | ||||
| 				class:border-red-500={build.status === 'failed'} | ||||
| 				class:border-green-500={build.status === 'success'} | ||||
| 				class:border-yellow-500={build.status === 'inprogress'} | ||||
| 			> | ||||
| 				<div class="flex-col px-2"> | ||||
| 					<div class="text-sm font-bold"> | ||||
| 						{application.branch} | ||||
| 					</div> | ||||
| 					<div class="text-xs"> | ||||
| 						{build.type} | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div class="flex-1" /> | ||||
|  | ||||
| 				<div class="w-48 text-center text-xs"> | ||||
| 					{#if build.status === 'running'} | ||||
| 						<div class="font-bold">Running</div> | ||||
| 					{:else} | ||||
| 						<div>{build.since}</div> | ||||
| 						<div>Finished in <span class="font-bold">{build.took}s</span></div> | ||||
| 					{/if} | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/each} | ||||
| 		{#if buildCount > 0 && !noMoreBuilds} | ||||
| 			<button class="w-full" on:click={loadMoreBuilds}>Load More</button> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 	<div class="w-96 flex-1"> | ||||
| 		{#if buildId} | ||||
| 			{#key buildId} | ||||
| 				<svelte:component this={BuildLog} {buildId} on:updateBuildStatus={updateBuildStatus} /> | ||||
| 			{/key} | ||||
| 		{/if} | ||||
| 	</div> | ||||
| </div> | ||||
| {#if buildCount === 0} | ||||
| 	<div class="text-center text-xl font-bold">No logs found</div> | ||||
| {/if} | ||||
							
								
								
									
										53
									
								
								src/routes/applications/[id]/logs/index.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/routes/applications/[id]/logs/index.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| import { getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
| import { dayjs } from '$lib/dayjs'; | ||||
| import { dockerInstance } from '$lib/docker'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const get: RequestHandler = async (event) => { | ||||
| 	const { status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	try { | ||||
| 		const { destinationDockerId, destinationDocker } = await db.prisma.application.findUnique({ | ||||
| 			where: { id }, | ||||
| 			include: { destinationDocker: true } | ||||
| 		}); | ||||
| 		if (destinationDockerId) { | ||||
| 			const docker = dockerInstance({ destinationDocker }); | ||||
| 			try { | ||||
| 				const container = await docker.engine.getContainer(id); | ||||
| 				if (container) { | ||||
| 					return { | ||||
| 						body: { | ||||
| 							logs: (await container.logs({ stdout: true, stderr: true, timestamps: true })) | ||||
| 								.toString() | ||||
| 								.split('\n') | ||||
| 								.map((l) => l.slice(8)) | ||||
| 								.filter((a) => a) | ||||
| 						} | ||||
| 					}; | ||||
| 				} | ||||
| 			} catch (error) { | ||||
| 				const { statusCode } = error; | ||||
| 				if (statusCode === 404) { | ||||
| 					return { | ||||
| 						body: { | ||||
| 							logs: [] | ||||
| 						} | ||||
| 					}; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				message: 'No logs found.' | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										75
									
								
								src/routes/applications/[id]/logs/index.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/routes/applications/[id]/logs/index.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	import { onDestroy, onMount } from 'svelte'; | ||||
| 	export const load: Load = async ({ fetch, params, url, stuff }) => { | ||||
| 		let endpoint = `/applications/${params.id}/logs.json`; | ||||
| 		const res = await fetch(endpoint); | ||||
| 		if (res.ok) { | ||||
| 			return { | ||||
| 				props: { | ||||
| 					application: stuff.application, | ||||
| 					...(await res.json()) | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: res.status, | ||||
| 			error: new Error(`Could not load ${url}`) | ||||
| 		}; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	export let application; | ||||
| 	import { page } from '$app/stores'; | ||||
| 	import LoadingLogs from './_Loading.svelte'; | ||||
| 	import { getDomain } from '$lib/components/common'; | ||||
| 	import { get } from '$lib/api'; | ||||
| 	import { errorNotification } from '$lib/form'; | ||||
| 	let loadLogsInterval = null; | ||||
| 	let logs = []; | ||||
|  | ||||
| 	const { id } = $page.params; | ||||
|  | ||||
| 	onMount(async () => { | ||||
| 		loadLogs(); | ||||
| 		loadLogsInterval = setInterval(() => { | ||||
| 			loadLogs(); | ||||
| 		}, 3000); | ||||
| 	}); | ||||
| 	onDestroy(() => { | ||||
| 		clearInterval(loadLogsInterval); | ||||
| 	}); | ||||
| 	async function loadLogs() { | ||||
| 		try { | ||||
| 			const newLogs = await get(`/applications/${id}/logs.json`); | ||||
| 			logs = newLogs.logs; | ||||
| 			return; | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <div class="flex space-x-1 p-6 font-bold"> | ||||
| 	<div class="mr-4 text-2xl tracking-tight"> | ||||
| 		Application logs of <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a> | ||||
| 	</div> | ||||
| </div> | ||||
| <div class="flex flex-row justify-center space-x-2 px-10 pt-6"> | ||||
| 	{#if logs.length === 0} | ||||
| 		<div class="text-xl font-bold tracking-tighter">Waiting for the logs...</div> | ||||
| 	{:else} | ||||
| 		<div class="relative w-full"> | ||||
| 			<LoadingLogs /> | ||||
| 			<div | ||||
| 				class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 p-6 whitespace-pre-wrap break-words w-full" | ||||
| 			> | ||||
| 				{#each logs as log} | ||||
| 					{log + '\n'} | ||||
| 				{/each} | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	{/if} | ||||
| </div> | ||||
							
								
								
									
										44
									
								
								src/routes/applications/[id]/previews/index.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/routes/applications/[id]/previews/index.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| import { getTeam, getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
| import { dockerInstance } from '$lib/docker'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
| import jsonwebtoken from 'jsonwebtoken'; | ||||
|  | ||||
| export const get: RequestHandler = async (event) => { | ||||
| 	const { status, body, teamId } = await getUserDetails(event, false); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	try { | ||||
| 		const destinationDocker = await db.getDestinationByApplicationId({ id, teamId }); | ||||
| 		const docker = dockerInstance({ destinationDocker }); | ||||
| 		const listContainers = await docker.engine.listContainers({ | ||||
| 			filters: { network: [destinationDocker.network] } | ||||
| 		}); | ||||
| 		const containers = listContainers.filter((container) => { | ||||
| 			return ( | ||||
| 				container.Labels['coolify.configuration'] && | ||||
| 				container.Labels['coolify.type'] === 'standalone-application' | ||||
| 			); | ||||
| 		}); | ||||
| 		const jsonContainers = containers | ||||
| 			.map((container) => | ||||
| 				JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString()) | ||||
| 			) | ||||
| 			.filter((container) => { | ||||
| 				return ( | ||||
| 					container.type !== 'manual' && | ||||
| 					container.type !== 'webhook_commit' && | ||||
| 					container.applicationId === id | ||||
| 				); | ||||
| 			}); | ||||
| 		return { | ||||
| 			body: { | ||||
| 				containers: jsonContainers | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										51
									
								
								src/routes/applications/[id]/previews/index.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/routes/applications/[id]/previews/index.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	export const load: Load = async ({ fetch, params, stuff }) => { | ||||
| 		let endpoint = `/applications/${params.id}/previews.json`; | ||||
| 		const res = await fetch(endpoint); | ||||
| 		if (res.ok) { | ||||
| 			return { | ||||
| 				props: { | ||||
| 					application: stuff.application, | ||||
| 					...(await res.json()) | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: res.status, | ||||
| 			error: new Error(`Could not load ${endpoint}`) | ||||
| 		}; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	export let containers; | ||||
| 	export let application; | ||||
|  | ||||
| 	import { getDomain } from '$lib/components/common'; | ||||
| </script> | ||||
|  | ||||
| <div class="flex space-x-1 p-6 font-bold"> | ||||
| 	<div class="mr-4 text-2xl tracking-tight"> | ||||
| 		Previews for <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a> | ||||
| 	</div> | ||||
| </div> | ||||
|  | ||||
| <div class="mx-auto max-w-4xl px-6"> | ||||
| 	<div class="flex flex-wrap justify-center space-x-2"> | ||||
| 		{#if containers.length > 0} | ||||
| 			{#each containers as container} | ||||
| 				<a href={container.fqdn} class="p-2 no-underline" target="_blank"> | ||||
| 					<div class="box-selection text-center hover:border-transparent hover:bg-coolgray-200"> | ||||
| 						<div class="truncate text-center text-xl font-bold">{getDomain(container.fqdn)}</div> | ||||
| 					</div> | ||||
| 				</a> | ||||
| 			{/each} | ||||
| 		{:else} | ||||
| 			<div class="flex-col"> | ||||
| 				<div class="text-center font-bold text-xl">No previews available</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| </div> | ||||
							
								
								
									
										133
									
								
								src/routes/applications/[id]/secrets/_Secret.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/routes/applications/[id]/secrets/_Secret.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| <script> | ||||
| 	export let name = ''; | ||||
| 	export let value = ''; | ||||
| 	export let isBuildSecret = false; | ||||
| 	export let isNewSecret = false; | ||||
| 	import { page } from '$app/stores'; | ||||
| 	import { del, post } from '$lib/api'; | ||||
| 	import { errorNotification } from '$lib/form'; | ||||
|  | ||||
| 	if (name) value = 'ENCRYPTED'; | ||||
| 	const { id } = $page.params; | ||||
|  | ||||
| 	async function removeSecret() { | ||||
| 		try { | ||||
| 			await del(`/applications/${id}/secrets.json`, { name }); | ||||
| 			return window.location.reload(); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| 	async function saveSecret() { | ||||
| 		try { | ||||
| 			await post(`/applications/${id}/secrets.json`, { name, value, isBuildSecret }); | ||||
| 			return window.location.reload(); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| 	function setSecretValue() { | ||||
| 		if (isNewSecret) { | ||||
| 			isBuildSecret = !isBuildSecret; | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <div class="mx-auto max-w-3xl pt-4"> | ||||
| 	<div class="flex space-x-2"> | ||||
| 		<div class="grid grid-flow-row"> | ||||
| 			<label for="secretName">Name</label> | ||||
| 			<input | ||||
| 				id="secretName" | ||||
| 				bind:value={name} | ||||
| 				placeholder="EXAMPLE_VARIABLE" | ||||
| 				class="w-64 border-2 border-transparent" | ||||
| 				readonly={!isNewSecret} | ||||
| 				class:hover:bg-coolgray-200={!isNewSecret} | ||||
| 				class:cursor-not-allowed={!isNewSecret} | ||||
| 			/> | ||||
| 		</div> | ||||
| 		<div class="grid grid-flow-row"> | ||||
| 			<label for="secretValue">Value (will be encrypted)</label> | ||||
| 			<input | ||||
| 				id="secretValue" | ||||
| 				bind:value | ||||
| 				placeholder="J$#@UIO%HO#$U%H" | ||||
| 				class="w-64 border-2 border-transparent" | ||||
| 				class:hover:bg-coolgray-200={!isNewSecret} | ||||
| 				class:cursor-not-allowed={!isNewSecret} | ||||
| 				readonly={!isNewSecret} | ||||
| 			/> | ||||
| 		</div> | ||||
|  | ||||
| 		<div class="w-32 px-2 text-center"> | ||||
| 			<div class="text-xs">Is build variable?</div> | ||||
|  | ||||
| 			<div class="mt-2"> | ||||
| 				<ul class="divide-y divide-stone-800"> | ||||
| 					<li> | ||||
| 						<div | ||||
| 							type="button" | ||||
| 							on:click={setSecretValue} | ||||
| 							aria-pressed="false" | ||||
| 							class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out" | ||||
| 							class:bg-green-600={isBuildSecret} | ||||
| 							class:bg-stone-700={!isBuildSecret} | ||||
| 							class:cursor-not-allowed={!isNewSecret} | ||||
| 							class:cursor-pointer={isNewSecret} | ||||
| 						> | ||||
| 							<span class="sr-only">Use isBuildSecret</span> | ||||
| 							<span | ||||
| 								class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out" | ||||
| 								class:translate-x-5={isBuildSecret} | ||||
| 								class:translate-x-0={!isBuildSecret} | ||||
| 							> | ||||
| 								<span | ||||
| 									class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in" | ||||
| 									class:opacity-0={isBuildSecret} | ||||
| 									class:opacity-100={!isBuildSecret} | ||||
| 									aria-hidden="true" | ||||
| 								> | ||||
| 									<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12"> | ||||
| 										<path | ||||
| 											d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2" | ||||
| 											stroke="currentColor" | ||||
| 											stroke-width="2" | ||||
| 											stroke-linecap="round" | ||||
| 											stroke-linejoin="round" | ||||
| 										/> | ||||
| 									</svg> | ||||
| 								</span> | ||||
| 								<span | ||||
| 									class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out" | ||||
| 									aria-hidden="true" | ||||
| 									class:opacity-100={isBuildSecret} | ||||
| 									class:opacity-0={!isBuildSecret} | ||||
| 								> | ||||
| 									<svg | ||||
| 										class="h-3 w-3 bg-white text-green-600" | ||||
| 										fill="currentColor" | ||||
| 										viewBox="0 0 12 12" | ||||
| 									> | ||||
| 										<path | ||||
| 											d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" | ||||
| 										/> | ||||
| 									</svg> | ||||
| 								</span> | ||||
| 							</span> | ||||
| 						</div> | ||||
| 					</li> | ||||
| 				</ul> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		{#if isNewSecret} | ||||
| 			<div class="mt-6"> | ||||
| 				<button class="w-20 bg-green-600 hover:bg-green-500" on:click={saveSecret}>Add</button> | ||||
| 			</div> | ||||
| 		{:else} | ||||
| 			<div class="mt-6"> | ||||
| 				<button class="w-20 bg-red-600 hover:bg-red-500" on:click={removeSecret}>Remove</button> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| </div> | ||||
							
								
								
									
										63
									
								
								src/routes/applications/[id]/secrets/index.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/routes/applications/[id]/secrets/index.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| import { getTeam, getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const get: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	try { | ||||
| 		const secrets = await db.listSecrets({ applicationId: event.params.id }); | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				secrets: secrets.sort((a, b) => { | ||||
| 					return ('' + a.name).localeCompare(b.name); | ||||
| 				}) | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	const { name, value, isBuildSecret } = await event.request.json(); | ||||
|  | ||||
| 	try { | ||||
| 		const found = await db.isSecretExists({ id, name }); | ||||
| 		if (found) { | ||||
| 			throw { | ||||
| 				error: `Secret ${name} already exists` | ||||
| 			}; | ||||
| 		} else { | ||||
| 			await db.createSecret({ id, name, value, isBuildSecret }); | ||||
| 			return { | ||||
| 				status: 201 | ||||
| 			}; | ||||
| 		} | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
| export const del: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	const { name } = await event.request.json(); | ||||
|  | ||||
| 	try { | ||||
| 		await db.removeSecret({ id, name }); | ||||
| 		return { | ||||
| 			status: 200 | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										41
									
								
								src/routes/applications/[id]/secrets/index.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/routes/applications/[id]/secrets/index.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	export const load: Load = async ({ fetch, params, stuff }) => { | ||||
| 		let endpoint = `/applications/${params.id}/secrets.json`; | ||||
| 		const res = await fetch(endpoint); | ||||
| 		if (res.ok) { | ||||
| 			return { | ||||
| 				props: { | ||||
| 					application: stuff.application, | ||||
| 					...(await res.json()) | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: res.status, | ||||
| 			error: new Error(`Could not load ${endpoint}`) | ||||
| 		}; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	export let secrets; | ||||
| 	export let application; | ||||
| 	import Secret from './_Secret.svelte'; | ||||
| 	import { getDomain } from '$lib/components/common'; | ||||
| </script> | ||||
|  | ||||
| <div class="flex space-x-1 p-6 font-bold"> | ||||
| 	<div class="mr-4 text-2xl tracking-tight"> | ||||
| 		Secrets for <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a> | ||||
| 	</div> | ||||
| </div> | ||||
| <div class="mx-auto max-w-4xl px-6"> | ||||
| 	<div class="flex-col justify-start space-y-1"> | ||||
| 		{#each secrets as secret} | ||||
| 			<Secret name={secret.name} value={secret.value} isBuildSecret={secret.isBuildSecret} /> | ||||
| 		{/each} | ||||
| 		<Secret isNewSecret /> | ||||
| 	</div> | ||||
| </div> | ||||
							
								
								
									
										19
									
								
								src/routes/applications/[id]/settings.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/routes/applications/[id]/settings.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import { getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	const { debug, previews } = await event.request.json(); | ||||
|  | ||||
| 	try { | ||||
| 		await db.setApplicationSettings({ id, debug, previews }); | ||||
| 		return { status: 201 }; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										31
									
								
								src/routes/applications/[id]/stop.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/routes/applications/[id]/stop.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import { getDomain, getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
| import { dockerInstance } from '$lib/docker'; | ||||
| import { removeProxyConfiguration } from '$lib/haproxy'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
|  | ||||
| 	try { | ||||
| 		const { destinationDocker, destinationDockerId, fqdn } = await db.getApplication({ | ||||
| 			id, | ||||
| 			teamId | ||||
| 		}); | ||||
| 		const domain = getDomain(fqdn); | ||||
| 		if (destinationDockerId) { | ||||
| 			const docker = dockerInstance({ destinationDocker }); | ||||
| 			await docker.engine.getContainer(id).stop(); | ||||
| 		} | ||||
| 		await removeProxyConfiguration({ domain }); | ||||
| 		return { | ||||
| 			status: 200 | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										60
									
								
								src/routes/applications/_Application.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/routes/applications/_Application.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| <script lang="ts"> | ||||
| 	export let application; | ||||
| 	import Rust from '$lib/components/svg/applications/Rust.svelte'; | ||||
| 	import Nodejs from '$lib/components/svg/applications/Nodejs.svelte'; | ||||
| 	import React from '$lib/components/svg/applications/React.svelte'; | ||||
| 	import Svelte from '$lib/components/svg/applications/Svelte.svelte'; | ||||
| 	import Vuejs from '$lib/components/svg/applications/Vuejs.svelte'; | ||||
| 	import PHP from '$lib/components/svg/applications/PHP.svelte'; | ||||
| 	import Python from '$lib/components/svg/applications/Python.svelte'; | ||||
| 	import Static from '$lib/components/svg/applications/Static.svelte'; | ||||
| 	import Nestjs from '$lib/components/svg/applications/Nestjs.svelte'; | ||||
| 	import Nuxtjs from '$lib/components/svg/applications/Nuxtjs.svelte'; | ||||
| 	import Nextjs from '$lib/components/svg/applications/Nextjs.svelte'; | ||||
| 	import Gatsby from '$lib/components/svg/applications/Gatsby.svelte'; | ||||
| 	import Docker from '$lib/components/svg/applications/Docker.svelte'; | ||||
|  | ||||
| 	const buildPack = application?.buildPack?.toLowerCase(); | ||||
| </script> | ||||
|  | ||||
| <a href="/applications/{application.id}" class="w-96 p-2 no-underline"> | ||||
| 	<div class="box-selection group relative hover:bg-green-600"> | ||||
| 		{#if buildPack === 'rust'} | ||||
| 			<Rust /> | ||||
| 		{:else if buildPack === 'node'} | ||||
| 			<Nodejs /> | ||||
| 		{:else if buildPack === 'react'} | ||||
| 			<React /> | ||||
| 		{:else if buildPack === 'svelte'} | ||||
| 			<Svelte /> | ||||
| 		{:else if buildPack === 'vuejs'} | ||||
| 			<Vuejs /> | ||||
| 		{:else if buildPack === 'php'} | ||||
| 			<PHP /> | ||||
| 		{:else if buildPack === 'python'} | ||||
| 			<Python /> | ||||
| 		{:else if buildPack === 'static'} | ||||
| 			<Static /> | ||||
| 		{:else if buildPack === 'nestjs'} | ||||
| 			<Nestjs /> | ||||
| 		{:else if buildPack === 'nuxtjs'} | ||||
| 			<Nuxtjs /> | ||||
| 		{:else if buildPack === 'nextjs'} | ||||
| 			<Nextjs /> | ||||
| 		{:else if buildPack === 'gatsby'} | ||||
| 			<Gatsby /> | ||||
| 		{:else if buildPack === 'docker'} | ||||
| 			<Docker /> | ||||
| 		{/if} | ||||
|  | ||||
| 		<div class="truncate text-center text-xl font-bold">{application.name}</div> | ||||
| 		{#if application.fqdn} | ||||
| 			<div class="truncate text-center">{application.fqdn}</div> | ||||
| 		{/if} | ||||
| 		{#if !application.gitSourceId || !application.destinationDockerId} | ||||
| 			<div class="truncate text-center font-bold text-red-500 group-hover:text-white"> | ||||
| 				Configuration missing | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| </a> | ||||
							
								
								
									
										21
									
								
								src/routes/applications/index.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/routes/applications/index.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import { getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const get: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	try { | ||||
| 		const applications = await db.listApplications(teamId); | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				applications | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										58
									
								
								src/routes/applications/index.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/routes/applications/index.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	export const load: Load = async ({ fetch }) => { | ||||
| 		const endpoint = '/applications.json'; | ||||
| 		const res = await fetch(endpoint); | ||||
|  | ||||
| 		if (res.ok) { | ||||
| 			return { | ||||
| 				props: { | ||||
| 					...(await res.json()) | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: res.status, | ||||
| 			error: new Error(`Could not load ${endpoint}`) | ||||
| 		}; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	export let applications: Array<Applications>; | ||||
| 	import { session } from '$app/stores'; | ||||
| 	import Application from './_Application.svelte'; | ||||
| </script> | ||||
|  | ||||
| <div class="flex space-x-1 p-6 font-bold"> | ||||
| 	<div class="mr-4 text-2xl ">Applications</div> | ||||
| 	{#if $session.isAdmin} | ||||
| 		<a href="/new/application" class="add-icon bg-green-600 hover:bg-green-500"> | ||||
| 			<svg | ||||
| 				class="w-6" | ||||
| 				xmlns="http://www.w3.org/2000/svg" | ||||
| 				fill="none" | ||||
| 				viewBox="0 0 24 24" | ||||
| 				stroke="currentColor" | ||||
| 				><path | ||||
| 					stroke-linecap="round" | ||||
| 					stroke-linejoin="round" | ||||
| 					stroke-width="2" | ||||
| 					d="M12 6v6m0 0v6m0-6h6m-6 0H6" | ||||
| 				/></svg | ||||
| 			> | ||||
| 		</a> | ||||
| 	{/if} | ||||
| </div> | ||||
| <div class="flex flex-wrap justify-center"> | ||||
| 	{#if !applications || applications.length === 0} | ||||
| 		<div class="flex-col"> | ||||
| 			<div class="text-center text-xl font-bold">No applications found</div> | ||||
| 		</div> | ||||
| 	{:else} | ||||
| 		{#each applications as application} | ||||
| 			<Application {application} /> | ||||
| 		{/each} | ||||
| 	{/if} | ||||
| </div> | ||||
							
								
								
									
										10
									
								
								src/routes/common/getUniqueName.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/routes/common/getUniqueName.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import { uniqueName } from '$lib/common'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const get: RequestHandler = async () => { | ||||
| 	return { | ||||
| 		body: { | ||||
| 			name: uniqueName() | ||||
| 		} | ||||
| 	}; | ||||
| }; | ||||
| @@ -1,44 +0,0 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import { request } from '$lib/request'; | ||||
| 	/** | ||||
| 	 * @type {import('@sveltejs/kit').Load} | ||||
| 	 */ | ||||
| 	export async function load(session) { | ||||
| 		return { | ||||
| 			props: { | ||||
| 				initDashboard: await request('/api/v1/dashboard', session) | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	export let initDashboard; | ||||
| 	import { dashboard } from '$store'; | ||||
| 	import { onDestroy, onMount } from 'svelte'; | ||||
| 	import { session } from '$app/stores'; | ||||
| 	$dashboard = initDashboard; | ||||
| 	let loadDashboardInterval = null; | ||||
|  | ||||
| 	async function loadDashboard() { | ||||
| 		try { | ||||
| 			$dashboard = await request('/api/v1/dashboard', $session); | ||||
| 		} catch (error) { | ||||
| 			// | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	onMount(async () => { | ||||
| 		await loadDashboard(); | ||||
| 		loadDashboardInterval = setInterval(async () => { | ||||
| 			await loadDashboard(); | ||||
| 		}, 2000); | ||||
| 	}); | ||||
| 	onDestroy(() => { | ||||
| 		clearInterval(loadDashboardInterval); | ||||
| 	}); | ||||
| </script> | ||||
|  | ||||
| <div class="min-h-full"> | ||||
| 	<slot /> | ||||
| </div> | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -1,100 +0,0 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	/** | ||||
| 	 * @type {import('@sveltejs/kit').Load} | ||||
| 	 */ | ||||
| 	export async function load(session) { | ||||
| 		if (!browser && !process.env.VITE_GITHUB_APP_CLIENTID) { | ||||
| 			return { | ||||
| 				status: 302, | ||||
| 				redirect: '/dashboard/services' | ||||
| 			}; | ||||
| 		} | ||||
| 		return {}; | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <script> | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import MongoDb from '$components/Database/SVGs/MongoDb.svelte'; | ||||
| 	import Postgresql from '$components/Database/SVGs/Postgresql.svelte'; | ||||
| 	import Clickhouse from '$components/Database/SVGs/Clickhouse.svelte'; | ||||
| 	import CouchDb from '$components/Database/SVGs/CouchDb.svelte'; | ||||
| 	import Mysql from '$components/Database/SVGs/Mysql.svelte'; | ||||
| 	import { dashboard } from '$store'; | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| 	import Redis from '$components/Database/SVGs/Redis.svelte'; | ||||
| 	import { browser } from '$app/env'; | ||||
| </script> | ||||
|  | ||||
| <div class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"> | ||||
| 	<div in:fade={{ duration: 100 }}>Databases</div> | ||||
| 	<button | ||||
| 		class="icon p-1 ml-4 bg-purple-500 hover:bg-purple-400" | ||||
| 		on:click={() => goto('/database/new')} | ||||
| 	> | ||||
| 		<svg | ||||
| 			class="w-6" | ||||
| 			xmlns="http://www.w3.org/2000/svg" | ||||
| 			fill="none" | ||||
| 			viewBox="0 0 24 24" | ||||
| 			stroke="currentColor" | ||||
| 		> | ||||
| 			<path | ||||
| 				stroke-linecap="round" | ||||
| 				stroke-linejoin="round" | ||||
| 				stroke-width="2" | ||||
| 				d="M12 6v6m0 0v6m0-6h6m-6 0H6" | ||||
| 			/> | ||||
| 		</svg> | ||||
| 	</button> | ||||
| </div> | ||||
| <div in:fade={{ duration: 100 }}> | ||||
| 	{#if $dashboard.databases?.deployed.length > 0} | ||||
| 		<div class="px-4 mx-auto py-5"> | ||||
| 			<div class="flex items-center justify-center flex-wrap"> | ||||
| 				{#each $dashboard.databases.deployed as database} | ||||
| 					<div | ||||
| 						in:fade={{ duration: 200 }} | ||||
| 						class="px-4 pb-4" | ||||
| 						on:click={() => | ||||
| 							goto(`/database/${database.configuration.general.deployId}/configuration`)} | ||||
| 					> | ||||
| 						<div | ||||
| 							class="relative rounded-xl p-6 bg-warmGray-800 border-2 border-dashed border-transparent hover:border-purple-500 text-white shadow-md cursor-pointer ease-in-out hover:scale-105 duration-100 group" | ||||
| 						> | ||||
| 							<div class="flex items-center"> | ||||
| 								{#if database.configuration.general.type == 'mongodb'} | ||||
| 									<MongoDb customClass="w-10 h-10 absolute top-0 left-0 -m-4" /> | ||||
| 								{:else if database.configuration.general.type == 'postgresql'} | ||||
| 									<Postgresql customClass="w-10 h-10 absolute top-0 left-0 -m-4" /> | ||||
| 								{:else if database.configuration.general.type == 'mysql'} | ||||
| 									<Mysql customClass="w-10 h-10 absolute top-0 left-0 -m-4" /> | ||||
| 								{:else if database.configuration.general.type == 'couchdb'} | ||||
| 									<CouchDb | ||||
| 										customClass="w-10 h-10 fill-current text-red-600 absolute top-0 left-0 -m-4" | ||||
| 									/> | ||||
| 								{:else if database.configuration.general.type == 'redis'} | ||||
| 									<Redis customClass="w-10 h-10  absolute top-0 left-0 -m-4" /> | ||||
| 								{:else if database.configuration.general.type == 'clickhouse'} | ||||
| 									<Clickhouse | ||||
| 										customClass="w-10 h-10 fill-current text-red-600 absolute top-0 left-0 -m-4" | ||||
| 									/> | ||||
| 								{/if} | ||||
| 								<div class="text-center w-full"> | ||||
| 									<div class="text-base font-bold text-white group-hover:text-white"> | ||||
| 										{database.configuration.general.nickname} | ||||
| 									</div> | ||||
| 									<div class="text-xs font-bold text-warmGray-300 "> | ||||
| 										({database.configuration.general.type}) | ||||
| 									</div> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				{/each} | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	{:else} | ||||
| 		<div class="text-2xl font-bold text-center">No databases found</div> | ||||
| 	{/if} | ||||
| </div> | ||||
| @@ -1,124 +0,0 @@ | ||||
| <script> | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { dashboard } from '$store'; | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| 	async function openConfiguration(service) { | ||||
| 		if (service.serviceName === 'wordpress') { | ||||
| 			goto(`/service/${service.configuration.deployId}/configuration`); | ||||
| 		} else { | ||||
| 			goto(`/service/${service.serviceName}/configuration`); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <div | ||||
| 	in:fade={{ duration: 100 }} | ||||
| 	class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center" | ||||
| > | ||||
| 	<div>Services</div> | ||||
| 	<button class="icon p-1 ml-4 bg-blue-500 hover:bg-blue-400" on:click={() => goto('/service/new')}> | ||||
| 		<svg | ||||
| 			class="w-6" | ||||
| 			xmlns="http://www.w3.org/2000/svg" | ||||
| 			fill="none" | ||||
| 			viewBox="0 0 24 24" | ||||
| 			stroke="currentColor" | ||||
| 		> | ||||
| 			<path | ||||
| 				stroke-linecap="round" | ||||
| 				stroke-linejoin="round" | ||||
| 				stroke-width="2" | ||||
| 				d="M12 6v6m0 0v6m0-6h6m-6 0H6" | ||||
| 			/> | ||||
| 		</svg> | ||||
| 	</button> | ||||
| </div> | ||||
| <div in:fade={{ duration: 100 }}> | ||||
| 	{#if $dashboard?.services?.deployed.length > 0} | ||||
| 		<div class="px-4 mx-auto py-5"> | ||||
| 			<div class="flex items-center justify-center flex-wrap"> | ||||
| 				{#each $dashboard?.services?.deployed as service} | ||||
| 					<div | ||||
| 						in:fade={{ duration: 200 }} | ||||
| 						class="px-4 pb-4" | ||||
| 						on:click={() => openConfiguration(service)} | ||||
| 					> | ||||
| 						<div | ||||
| 							class="relative rounded-xl p-6 bg-warmGray-800 border-2 border-dashed border-transparent hover:border-blue-500 text-white shadow-md cursor-pointer ease-in-out hover:scale-105 duration-100 group" | ||||
| 						> | ||||
| 							<div class="flex items-center"> | ||||
| 								{#if service.serviceName == 'plausible'} | ||||
| 									<div> | ||||
| 										<img | ||||
| 											alt="plausible logo" | ||||
| 											class="w-10 absolute top-0 left-0 -m-6" | ||||
| 											src="https://cdn.coollabs.io/assets/coolify/services/plausible/logo_sm.png" | ||||
| 										/> | ||||
| 										<div class="text-white font-bold">Plausible Analytics</div> | ||||
| 									</div> | ||||
| 								{:else if service.serviceName == 'nocodb'} | ||||
| 									<div> | ||||
| 										<img | ||||
| 											alt="nocodedb" | ||||
| 											class="w-10 absolute top-0 left-0 -m-6" | ||||
| 											src="https://cdn.coollabs.io/assets/coolify/services/nocodb/nocodb.png" | ||||
| 										/> | ||||
| 										<div class="text-white font-bold">NocoDB</div> | ||||
| 									</div> | ||||
| 								{:else if service.serviceName == 'code-server'} | ||||
| 									<div> | ||||
| 										<svg class="w-10 absolute top-0 left-0 -m-6" viewBox="0 0 128 128"> | ||||
| 											<path | ||||
| 												d="M3.656 45.043s-3.027-2.191.61-5.113l8.468-7.594s2.426-2.559 4.989-.328l78.175 59.328v28.45s-.039 4.468-5.757 3.976zm0 0" | ||||
| 												fill="#2489ca" | ||||
| 											/><path | ||||
| 												d="M23.809 63.379L3.656 81.742s-2.07 1.543 0 4.305l9.356 8.527s2.222 2.395 5.508-.328l21.359-16.238zm0 0" | ||||
| 												fill="#1070b3" | ||||
| 											/><path | ||||
| 												d="M59.184 63.531l36.953-28.285-.239-28.297S94.32.773 89.055 3.99L39.879 48.851zm0 0" | ||||
| 												fill="#0877b9" | ||||
| 											/><path | ||||
| 												d="M90.14 123.797c2.145 2.203 4.747 1.48 4.747 1.48l28.797-14.222c3.687-2.52 3.171-5.645 3.171-5.645V20.465c0-3.735-3.812-5.024-3.812-5.024L98.082 3.38c-5.453-3.379-9.027.61-9.027.61s4.593-3.317 6.843 2.96v112.317c0 .773-.164 1.53-.492 2.214-.656 1.332-2.086 2.57-5.504 2.051zm0 0" | ||||
| 												fill="#3c99d4" | ||||
| 											/> | ||||
| 										</svg> | ||||
|  | ||||
| 										<div class="text-white font-bold">VSCode Server</div> | ||||
| 									</div> | ||||
| 								{:else if service.serviceName == 'minio'} | ||||
| 									<div> | ||||
| 										<img | ||||
| 											alt="minio" | ||||
| 											class="w-7 absolute top-0 left-0 -my-7 -mx-3" | ||||
| 											src="https://cdn.coollabs.io/assets/coolify/services/minio/MINIO_Bird.png" | ||||
| 										/> | ||||
|  | ||||
| 										<div class="text-white font-bold">MinIO</div> | ||||
| 									</div> | ||||
| 								{:else if service.serviceName.match(/wp-/)} | ||||
| 									<svg class="w-10 absolute top-0 left-0 -m-6" viewBox="0 0 128 128"> | ||||
| 										<path | ||||
| 											fill-rule="evenodd" | ||||
| 											clip-rule="evenodd" | ||||
| 											fill="white" | ||||
| 											d="M64.094 126.224c34.275-.052 62.021-27.933 62.021-62.325 0-33.833-27.618-61.697-60.613-62.286C30.85.995 1.894 29.113 1.885 63.21c-.01 35.079 27.612 63.064 62.209 63.014zM63.993 4.63c32.907-.011 59.126 26.725 59.116 60.28-.011 31.679-26.925 58.18-59.092 58.187-32.771.007-59.125-26.563-59.124-59.608.002-32.193 26.766-58.848 59.1-58.859zM39.157 35.896c.538 1.793-.968 2.417-2.569 2.542-1.685.13-3.369.257-5.325.406 6.456 19.234 12.815 38.183 19.325 57.573.464-.759.655-.973.739-1.223 3.574-10.682 7.168-21.357 10.651-32.069.318-.977.16-2.271-.188-3.275-1.843-5.32-4.051-10.524-5.667-15.908-1.105-3.686-2.571-6.071-6.928-5.644-.742.073-1.648-1.524-2.479-2.349 1.005-.6 2.003-1.704 3.017-1.719a849.593 849.593 0 0126.618.008c1.018.017 2.016 1.15 3.021 1.765-.88.804-1.639 2.01-2.668 2.321-1.651.498-3.482.404-5.458.58l19.349 57.56c2.931-9.736 5.658-18.676 8.31-27.639 2.366-8.001.956-15.473-3.322-22.52-1.286-2.119-2.866-4.175-3.595-6.486-.828-2.629-1.516-5.622-1.077-8.259.745-4.469 4.174-6.688 8.814-7.113C74.333.881 34.431 9.317 19.728 34.922c5.66-.261 11.064-.604 16.472-.678 1.022-.013 2.717.851 2.957 1.652zm10.117 77.971c-.118.345-.125.729-.218 1.302 10.943 3.034 21.675 2.815 32.659-.886l-16.78-45.96c-5.37 15.611-10.52 30.575-15.661 45.544zm-8.456-2.078l-25.281-69.35c-11.405 22.278-2.729 56.268 25.281 69.35zm76.428-44.562c.802-10.534-2.832-25.119-5.97-27.125-.35 3.875-.106 8.186-1.218 12.114-2.617 9.255-5.817 18.349-8.899 27.468-3.35 9.912-6.832 19.779-10.257 29.666 16.092-9.539 24.935-23.618 26.344-42.123z" | ||||
| 										/> | ||||
| 									</svg> | ||||
| 									<div class="text-white font-bold text-center"> | ||||
| 										Wordpress<span | ||||
| 											class="flex text-xs items-center justify-center text-warmGray-300  group-hover:text-white" | ||||
| 											>({service.configuration.baseURL.replace('https://', '')})</span | ||||
| 										> | ||||
| 									</div> | ||||
| 								{/if} | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				{/each} | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	{:else} | ||||
| 		<div class="text-2xl font-bold text-center">No services found</div> | ||||
| 	{/if} | ||||
| </div> | ||||
| @@ -1,123 +0,0 @@ | ||||
| <script> | ||||
| 	import { database } from '$store'; | ||||
| 	import { page, session } from '$app/stores'; | ||||
| 	import { request } from '$lib/request'; | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import MongoDb from '$components/Database/SVGs/MongoDb.svelte'; | ||||
| 	import Postgresql from '$components/Database/SVGs/Postgresql.svelte'; | ||||
| 	import Mysql from '$components/Database/SVGs/Mysql.svelte'; | ||||
| 	import CouchDb from '$components/Database/SVGs/CouchDb.svelte'; | ||||
| 	import Loading from '$components/Loading.svelte'; | ||||
| 	import PasswordField from '$components/PasswordField.svelte'; | ||||
| 	import { browser } from '$app/env'; | ||||
| 	import { toast } from '@zerodevx/svelte-toast'; | ||||
| 	import Redis from '$components/Database/SVGs/Redis.svelte'; | ||||
|  | ||||
| 	async function backup() { | ||||
| 		try { | ||||
| 			await request(`/api/v1/databases/${$page.params.name}/backup`, $session, { body: {} }); | ||||
|  | ||||
| 			browser && toast.push(`Successfully created backup.`); | ||||
| 		} catch (error) { | ||||
| 			console.log(error); | ||||
| 			if (error.code === 501) { | ||||
| 				browser && toast.push(error.error); | ||||
| 			} else { | ||||
| 				browser && toast.push(`Error occured during database backup!`); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	async function loadDatabaseConfig() { | ||||
| 		if ($page.params.name) { | ||||
| 			try { | ||||
| 				$database = await request(`/api/v1/databases/${$page.params.name}`, $session); | ||||
| 			} catch (error) { | ||||
| 				browser && goto(`/dashboard/databases`, { replaceState: true }); | ||||
| 			} | ||||
| 		} else { | ||||
| 			browser && goto(`/dashboard/databases`, { replaceState: true }); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| </script> | ||||
|  | ||||
| {#await loadDatabaseConfig()} | ||||
| 	<Loading /> | ||||
| {:then} | ||||
| 	<div class="min-h-full text-white"> | ||||
| 		<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"> | ||||
| 			<div>{$database.config.general.nickname}</div> | ||||
| 			<div class="px-4"> | ||||
| 				{#if $database.config.general.type === 'mongodb'} | ||||
| 					<MongoDb customClass="w-8 h-8" /> | ||||
| 				{:else if $database.config.general.type === 'postgresql'} | ||||
| 					<Postgresql customClass="w-8 h-8" /> | ||||
| 				{:else if $database.config.general.type === 'mysql'} | ||||
| 					<Mysql customClass="w-8 h-8" /> | ||||
| 				{:else if $database.config.general.type === 'couchdb'} | ||||
| 					<CouchDb customClass="w-8 h-8 fill-current text-red-600" /> | ||||
| 				{:else if $database.config.general.type === 'redis'} | ||||
| 					<Redis customClass="w-8 h-8" /> | ||||
| 				{/if} | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="text-left max-w-6xl mx-auto px-6" in:fade={{ duration: 100 }}> | ||||
| 		<div class="pb-2 pt-5 space-y-4"> | ||||
| 			<div class="text-2xl font-bold border-gradient w-32">Database</div> | ||||
| 			<div class="flex items-center pt-4"> | ||||
| 				<div class="font-bold w-64 text-warmGray-400">Connection string</div> | ||||
| 				{#if $database.config.general.type === 'mongodb'} | ||||
| 					<PasswordField | ||||
| 						value={`mongodb://${$database.envs.MONGODB_USERNAME}:${$database.envs.MONGODB_PASSWORD}@${$database.config.general.deployId}:27017/${$database.envs.MONGODB_DATABASE}`} | ||||
| 					/> | ||||
| 				{:else if $database.config.general.type === 'postgresql'} | ||||
| 					<PasswordField | ||||
| 						value={`postgresql://${$database.envs.POSTGRESQL_USERNAME}:${$database.envs.POSTGRESQL_PASSWORD}@${$database.config.general.deployId}:5432/${$database.envs.POSTGRESQL_DATABASE}`} | ||||
| 					/> | ||||
| 				{:else if $database.config.general.type === 'mysql'} | ||||
| 					<PasswordField | ||||
| 						value={`mysql://${$database.envs.MYSQL_USER}:${$database.envs.MYSQL_PASSWORD}@${$database.config.general.deployId}:3306/${$database.envs.MYSQL_DATABASE}`} | ||||
| 					/> | ||||
| 				{:else if $database.config.general.type === 'couchdb'} | ||||
| 					<PasswordField | ||||
| 						value={`http://${$database.envs.COUCHDB_USER}:${$database.envs.COUCHDB_PASSWORD}@${$database.config.general.deployId}:5984`} | ||||
| 					/> | ||||
| 				{:else if $database.config.general.type === 'redis'} | ||||
| 					<PasswordField | ||||
| 						value={`redis://${$database.envs.REDIS_PASSWORD}@${$database.config.general.deployId}:6379`} | ||||
| 					/> | ||||
| 				{:else if $database.config.general.type === 'clickhouse'} | ||||
| 					<!-- {JSON.stringify($database)} --> | ||||
| 					<!-- <textarea | ||||
|             disabled | ||||
|             class="w-full" | ||||
|             value="{`postgresql://${$database.envs.POSTGRESQL_USERNAME}:${$database.envs.POSTGRESQL_PASSWORD}@${$database.config.general.deployId}:5432/${$database.envs.POSTGRESQL_DATABASE}`}" | ||||
|           ></textarea> --> | ||||
| 				{/if} | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		{#if $database.config.general.type === 'mongodb'} | ||||
| 			<div class="flex items-center"> | ||||
| 				<div class="font-bold w-64 text-warmGray-400">Root password</div> | ||||
| 				<PasswordField value={$database.envs.MONGODB_ROOT_PASSWORD} /> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 		{#if $database.config.general.type === 'redis'} | ||||
| 			<div class="flex items-center"> | ||||
| 				<div class="font-bold w-64 text-warmGray-400">Redis password</div> | ||||
| 				<PasswordField value={$database.envs.REDIS_PASSWORD} /> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 		<div class="pb-2 pt-5 space-y-4"> | ||||
| 			<div class="text-2xl font-bold border-gradient w-32">Backup</div> | ||||
| 			<div class="pt-4"> | ||||
| 				<button | ||||
| 					class="button hover:bg-warmGray-700 bg-warmGray-800 rounded p-2 font-bold " | ||||
| 					on:click={backup}>Download database backup</button | ||||
| 				> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| {/await} | ||||
| @@ -1,78 +0,0 @@ | ||||
| <script> | ||||
| 	import { browser } from '$app/env'; | ||||
|  | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { page, session } from '$app/stores'; | ||||
| 	import Tooltip from '$components/Tooltip.svelte'; | ||||
| 	import { request } from '$lib/request'; | ||||
| 	import { database, initialDatabase } from '$store'; | ||||
| 	import { toast } from '@zerodevx/svelte-toast'; | ||||
| 	import { onDestroy } from 'svelte'; | ||||
|  | ||||
| 	onDestroy(() => { | ||||
| 		$database = JSON.parse(JSON.stringify(initialDatabase)); | ||||
| 	}); | ||||
|  | ||||
| 	async function removeDB() { | ||||
| 		await request(`/api/v1/databases/${$page.params.name}`, $session, { | ||||
| 			method: 'DELETE' | ||||
| 		}); | ||||
| 		if (browser) { | ||||
| 			toast.push('Database removed.'); | ||||
| 			goto(`/dashboard/databases`, { replaceState: true }); | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| {#if $page.path !== '/database/new'} | ||||
| 	<nav class="flex text-white justify-end items-center m-4 fixed right-0 top-0 space-x-4"> | ||||
| 		<Tooltip position="bottom" label="Delete"> | ||||
| 			<button title="Delete" class="icon hover:text-red-500" on:click={removeDB}> | ||||
| 				<svg | ||||
| 					class="w-6" | ||||
| 					xmlns="http://www.w3.org/2000/svg" | ||||
| 					fill="none" | ||||
| 					viewBox="0 0 24 24" | ||||
| 					stroke="currentColor" | ||||
| 				> | ||||
| 					<path | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 						stroke-width="2" | ||||
| 						d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" | ||||
| 					/> | ||||
| 				</svg> | ||||
| 			</button> | ||||
| 		</Tooltip> | ||||
| 		<div class="border border-warmGray-700 h-8" /> | ||||
| 		<Tooltip position="bottom-left" label="Configuration"> | ||||
| 			<button | ||||
| 				class="icon hover:text-yellow-400" | ||||
| 				disabled={$page.path === '/database/new'} | ||||
| 				class:text-yellow-400={$page.path.endsWith('configuration') || | ||||
| 					$page.path === '/database/new'} | ||||
| 				class:bg-warmGray-700={$page.path.endsWith('configuration') || | ||||
| 					$page.path === '/database/new'} | ||||
| 				on:click={() => goto(`/database/${$page.params.name}/configuration`)} | ||||
| 			> | ||||
| 				<svg | ||||
| 					class="w-6" | ||||
| 					xmlns="http://www.w3.org/2000/svg" | ||||
| 					fill="none" | ||||
| 					viewBox="0 0 24 24" | ||||
| 					stroke="currentColor" | ||||
| 				> | ||||
| 					<path | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 						stroke-width="2" | ||||
| 						d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" | ||||
| 					/> | ||||
| 				</svg> | ||||
| 			</button> | ||||
| 		</Tooltip> | ||||
| 	</nav> | ||||
| {/if} | ||||
| <div class="text-white"> | ||||
| 	<slot /> | ||||
| </div> | ||||
| @@ -1,11 +0,0 @@ | ||||
| <script> | ||||
| 	import Configuration from '$components/Database/Configuration.svelte'; | ||||
| </script> | ||||
|  | ||||
| <div class="min-h-full text-white"> | ||||
| 	<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"> | ||||
| 		Select a database | ||||
| 	</div> | ||||
| </div> | ||||
|  | ||||
| <Configuration /> | ||||
							
								
								
									
										78
									
								
								src/routes/databases/[id]/_Databases/_CouchDb.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/routes/databases/[id]/_Databases/_CouchDb.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| <script> | ||||
| 	export let database; | ||||
| 	import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; | ||||
| </script> | ||||
|  | ||||
| <div class="flex space-x-1 py-5 font-bold"> | ||||
| 	<div class="title">CouchDB</div> | ||||
| </div> | ||||
| <div class="px-10"> | ||||
| 	<div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="defaultDatabase">Default Database</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				required | ||||
| 				readonly={database.defaultDatabase} | ||||
| 				disabled={database.defaultDatabase} | ||||
| 				placeholder="eg: mydb" | ||||
| 				id="defaultDatabase" | ||||
| 				name="defaultDatabase" | ||||
| 				bind:value={database.defaultDatabase} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="dbUser">User</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				readonly | ||||
| 				disabled | ||||
| 				placeholder="Generated automatically after start" | ||||
| 				id="dbUser" | ||||
| 				name="dbUser" | ||||
| 				value={database.dbUser} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="dbUserPassword">Password</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				readonly | ||||
| 				disabled | ||||
| 				placeholder="Generated automatically after start" | ||||
| 				isPasswordField | ||||
| 				id="dbUserPassword" | ||||
| 				name="dbUserPassword" | ||||
| 				value={database.dbUserPassword} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="rootUser">Root User</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				readonly | ||||
| 				disabled | ||||
| 				placeholder="Generated automatically after start" | ||||
| 				id="rootUser" | ||||
| 				name="rootUser" | ||||
| 				value={database.rootUser} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="rootUserPassword">Root's Password</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				readonly | ||||
| 				disabled | ||||
| 				placeholder="Generated automatically after start" | ||||
| 				isPasswordField | ||||
| 				id="rootUserPassword" | ||||
| 				name="rootUserPassword" | ||||
| 				value={database.rootUserPassword} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
							
								
								
									
										209
									
								
								src/routes/databases/[id]/_Databases/_Databases.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								src/routes/databases/[id]/_Databases/_Databases.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | ||||
| <script lang="ts"> | ||||
| 	export let database; | ||||
| 	export let privatePort; | ||||
| 	export let settings; | ||||
| 	import { page, session } from '$app/stores'; | ||||
| 	import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; | ||||
| 	import Setting from '$lib/components/Setting.svelte'; | ||||
| 	import { errorNotification } from '$lib/form'; | ||||
|  | ||||
| 	import MySql from './_MySQL.svelte'; | ||||
| 	import MongoDb from './_MongoDB.svelte'; | ||||
| 	import PostgreSql from './_PostgreSQL.svelte'; | ||||
| 	import Redis from './_Redis.svelte'; | ||||
| 	import CouchDb from './_CouchDb.svelte'; | ||||
| 	import { browser } from '$app/env'; | ||||
| 	import { post } from '$lib/api'; | ||||
| 	import { getDomain } from '$lib/components/common'; | ||||
|  | ||||
| 	const { id } = $page.params; | ||||
| 	let loading = false; | ||||
| 	let isPublic = database.settings.isPublic || false; | ||||
| 	let appendOnly = database.settings.appendOnly; | ||||
|  | ||||
| 	let databaseDefault = database.defaultDatabase; | ||||
| 	let databaseDbUser = database.dbUser; | ||||
| 	let databaseDbUserPassword = database.dbUserPassword; | ||||
| 	if (database.type === 'mongodb') { | ||||
| 		databaseDefault = '?readPreference=primary&ssl=false'; | ||||
| 		databaseDbUser = database.rootUser; | ||||
| 		databaseDbUserPassword = database.rootUserPassword; | ||||
| 	} else if (database.type === 'redis') { | ||||
| 		databaseDefault = ''; | ||||
| 		databaseDbUser = ''; | ||||
| 	} | ||||
| 	let databaseUrl = generateUrl(); | ||||
|  | ||||
| 	function generateUrl() { | ||||
| 		return browser | ||||
| 			? `${database.type}://${ | ||||
| 					databaseDbUser ? databaseDbUser + ':' : '' | ||||
| 			  }${databaseDbUserPassword}@${ | ||||
| 					isPublic | ||||
| 						? settings.fqdn | ||||
| 							? getDomain(settings.fqdn) | ||||
| 							: window.location.hostname | ||||
| 						: database.id | ||||
| 			  }:${isPublic ? database.publicPort : privatePort}/${databaseDefault}` | ||||
| 			: 'Loading...'; | ||||
| 	} | ||||
|  | ||||
| 	async function changeSettings(name) { | ||||
| 		if (name === 'isPublic') { | ||||
| 			isPublic = !isPublic; | ||||
| 		} | ||||
| 		if (name === 'appendOnly') { | ||||
| 			appendOnly = !appendOnly; | ||||
| 		} | ||||
| 		try { | ||||
| 			await post(`/databases/${id}/settings.json`, { isPublic, appendOnly }); | ||||
| 			databaseUrl = generateUrl(); | ||||
| 			return; | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| 	async function handleSubmit() { | ||||
| 		try { | ||||
| 			await post(`/databases/${id}.json`, { ...database }); | ||||
| 			return window.location.reload(); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <div class="mx-auto max-w-4xl px-6"> | ||||
| 	<form on:submit|preventDefault={handleSubmit} class="py-4"> | ||||
| 		<div class="flex space-x-1 pb-5 font-bold"> | ||||
| 			<div class="title">General</div> | ||||
| 			{#if $session.isAdmin} | ||||
| 				<button | ||||
| 					type="submit" | ||||
| 					class:bg-purple-600={!loading} | ||||
| 					class:hover:bg-purple-500={!loading} | ||||
| 					disabled={loading}>{loading ? 'Saving...' : 'Save'}</button | ||||
| 				> | ||||
| 			{/if} | ||||
| 		</div> | ||||
|  | ||||
| 		<div class="grid grid-flow-row gap-2 px-10"> | ||||
| 			<div class="grid grid-cols-3 items-center"> | ||||
| 				<label for="name">Name</label> | ||||
| 				<div class="col-span-2 "> | ||||
| 					<input | ||||
| 						readonly={!$session.isAdmin} | ||||
| 						name="name" | ||||
| 						id="name" | ||||
| 						bind:value={database.name} | ||||
| 						required | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="grid grid-cols-3 items-center"> | ||||
| 				<label for="destination">Destination</label> | ||||
| 				<div class="col-span-2"> | ||||
| 					{#if database.destinationDockerId} | ||||
| 						<div class="no-underline"> | ||||
| 							<input | ||||
| 								value={database.destinationDocker.name} | ||||
| 								id="destination" | ||||
| 								disabled | ||||
| 								readonly | ||||
| 								class="bg-transparent " | ||||
| 							/> | ||||
| 						</div> | ||||
| 					{/if} | ||||
| 				</div> | ||||
| 			</div> | ||||
|  | ||||
| 			<div class="grid grid-cols-3 items-center"> | ||||
| 				<label for="version">Version</label> | ||||
| 				<div class="col-span-2 "> | ||||
| 					<input value={database.version} readonly disabled class="bg-transparent " /> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
|  | ||||
| 		<div class="grid grid-flow-row gap-2 px-10"> | ||||
| 			<div class="grid grid-cols-3 items-center"> | ||||
| 				<label for="host">Host</label> | ||||
| 				<div class="col-span-2 "> | ||||
| 					<CopyPasswordField | ||||
| 						placeholder="Generated automatically after start" | ||||
| 						isPasswordField={false} | ||||
| 						readonly | ||||
| 						disabled | ||||
| 						id="host" | ||||
| 						name="host" | ||||
| 						value={database.id} | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="grid grid-cols-3 items-center"> | ||||
| 				<label for="publicPort">Port</label> | ||||
| 				<div class="col-span-2"> | ||||
| 					<CopyPasswordField | ||||
| 						placeholder="Generated automatically after start" | ||||
| 						id="publicPort" | ||||
| 						readonly | ||||
| 						disabled | ||||
| 						name="publicPort" | ||||
| 						value={isPublic ? database.publicPort : privatePort} | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="grid grid-flow-row gap-2"> | ||||
| 			{#if database.type === 'mysql'} | ||||
| 				<MySql bind:database /> | ||||
| 			{:else if database.type === 'postgresql'} | ||||
| 				<PostgreSql bind:database /> | ||||
| 			{:else if database.type === 'mongodb'} | ||||
| 				<MongoDb {database} /> | ||||
| 			{:else if database.type === 'redis'} | ||||
| 				<Redis {database} /> | ||||
| 			{:else if database.type === 'couchdb'} | ||||
| 				<CouchDb bind:database /> | ||||
| 			{/if} | ||||
| 			<div class="grid grid-cols-3 items-center px-10 pb-8"> | ||||
| 				<label for="url">Connection String</label> | ||||
| 				<div class="col-span-2 "> | ||||
| 					<CopyPasswordField | ||||
| 						textarea={true} | ||||
| 						placeholder="Generated automatically after start" | ||||
| 						isPasswordField={false} | ||||
| 						id="url" | ||||
| 						name="url" | ||||
| 						readonly | ||||
| 						disabled | ||||
| 						value={databaseUrl} | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</form> | ||||
| 	<div class="flex space-x-1 pb-5 font-bold"> | ||||
| 		<div class="title">Features</div> | ||||
| 	</div> | ||||
| 	<div class="px-4 pb-10 sm:px-6"> | ||||
| 		<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> | ||||
| 		{#if database.type === 'redis'} | ||||
| 			<ul class="mt-2 divide-y divide-stone-800"> | ||||
| 				<Setting | ||||
| 					bind:setting={appendOnly} | ||||
| 					on:click={() => changeSettings('appendOnly')} | ||||
| 					title="Change append only mode" | ||||
| 					description="Useful if you would like to restore redis data from a backup.<br><span class='font-bold text-white'>Database restart is required.</span>" | ||||
| 				/> | ||||
| 			</ul> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| </div> | ||||
							
								
								
									
										37
									
								
								src/routes/databases/[id]/_Databases/_MongoDB.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/routes/databases/[id]/_Databases/_MongoDB.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| <script> | ||||
| 	export let database; | ||||
| 	import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; | ||||
| </script> | ||||
|  | ||||
| <div class="flex space-x-1 py-5 font-bold"> | ||||
| 	<div class="title">MongoDB</div> | ||||
| </div> | ||||
| <div class="px-10"> | ||||
| 	<div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="rootUser">Root User</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				placeholder="Generated automatically after start" | ||||
| 				id="rootUser" | ||||
| 				readonly | ||||
| 				disabled | ||||
| 				name="rootUser" | ||||
| 				value={database.rootUser} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="rootUserPassword">Root's Password</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				placeholder="Generated automatically after start" | ||||
| 				isPasswordField={true} | ||||
| 				readonly | ||||
| 				disabled | ||||
| 				id="rootUserPassword" | ||||
| 				name="rootUserPassword" | ||||
| 				value={database.rootUserPassword} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
							
								
								
									
										78
									
								
								src/routes/databases/[id]/_Databases/_MySQL.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/routes/databases/[id]/_Databases/_MySQL.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| <script> | ||||
| 	export let database; | ||||
| 	import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; | ||||
| </script> | ||||
|  | ||||
| <div class="flex space-x-1 py-5 font-bold"> | ||||
| 	<div class="title">MySQL</div> | ||||
| </div> | ||||
| <div class=" px-10"> | ||||
| 	<div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="defaultDatabase">Default Database</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				required | ||||
| 				readonly={database.defaultDatabase} | ||||
| 				disabled={database.defaultDatabase} | ||||
| 				placeholder="eg: mydb" | ||||
| 				id="defaultDatabase" | ||||
| 				name="defaultDatabase" | ||||
| 				bind:value={database.defaultDatabase} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="dbUser">User</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				readonly | ||||
| 				disabled | ||||
| 				placeholder="Generated automatically after start" | ||||
| 				id="dbUser" | ||||
| 				name="dbUser" | ||||
| 				value={database.dbUser} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="dbUserPassword">Password</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				readonly | ||||
| 				disabled | ||||
| 				placeholder="Generated automatically after start" | ||||
| 				isPasswordField | ||||
| 				id="dbUserPassword" | ||||
| 				name="dbUserPassword" | ||||
| 				value={database.dbUserPassword} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="rootUser">Root User</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				readonly | ||||
| 				disabled | ||||
| 				placeholder="Generated automatically after start" | ||||
| 				id="rootUser" | ||||
| 				name="rootUser" | ||||
| 				value={database.rootUser} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="rootUserPassword">Root's Password</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				readonly | ||||
| 				disabled | ||||
| 				placeholder="Generated automatically after start" | ||||
| 				isPasswordField | ||||
| 				id="rootUserPassword" | ||||
| 				name="rootUserPassword" | ||||
| 				value={database.rootUserPassword} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
							
								
								
									
										51
									
								
								src/routes/databases/[id]/_Databases/_PostgreSQL.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/routes/databases/[id]/_Databases/_PostgreSQL.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| <script> | ||||
| 	export let database; | ||||
| 	import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; | ||||
| </script> | ||||
|  | ||||
| <div class="flex space-x-1 py-5 font-bold"> | ||||
| 	<div class="title">PostgreSQL</div> | ||||
| </div> | ||||
| <div class="px-10"> | ||||
| 	<div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="defaultDatabase">Default Database</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				required | ||||
| 				readonly={database.defaultDatabase} | ||||
| 				disabled={database.defaultDatabase} | ||||
| 				placeholder="eg: mydb" | ||||
| 				id="defaultDatabase" | ||||
| 				name="defaultDatabase" | ||||
| 				bind:value={database.defaultDatabase} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="dbUser">User</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				readonly | ||||
| 				disabled | ||||
| 				placeholder="Generated automatically after start" | ||||
| 				id="dbUser" | ||||
| 				name="dbUser" | ||||
| 				value={database.dbUser} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="dbUserPassword">Password</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				readonly | ||||
| 				disabled | ||||
| 				placeholder="Generated automatically after start" | ||||
| 				isPasswordField | ||||
| 				id="dbUserPassword" | ||||
| 				name="dbUserPassword" | ||||
| 				value={database.dbUserPassword} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
							
								
								
									
										64
									
								
								src/routes/databases/[id]/_Databases/_Redis.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/routes/databases/[id]/_Databases/_Redis.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| <script> | ||||
| 	export let database; | ||||
| 	import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; | ||||
| </script> | ||||
|  | ||||
| <div class="flex space-x-1 py-5 font-bold"> | ||||
| 	<div class="title">Redis</div> | ||||
| </div> | ||||
| <div class="px-10"> | ||||
| 	<!-- <div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="dbUser">User</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				readonly | ||||
| 				disabled | ||||
| 				placeholder="Generated automatically after start" | ||||
| 				id="dbUser" | ||||
| 				name="dbUser" | ||||
| 				bind:value={database.dbUser} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> --> | ||||
| 	<div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="dbUserPassword">Password</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				disabled | ||||
| 				readonly | ||||
| 				placeholder="Generated automatically after start" | ||||
| 				isPasswordField | ||||
| 				id="dbUserPassword" | ||||
| 				name="dbUserPassword" | ||||
| 				value={database.dbUserPassword} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<!-- <div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="rootUser">Root User</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				disabled | ||||
| 				readonly | ||||
| 				placeholder="Generated automatically after start" | ||||
| 				id="rootUser" | ||||
| 				name="rootUser" | ||||
| 				value={database.rootUser} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="grid grid-cols-3 items-center"> | ||||
| 		<label for="rootUserPassword">Root's Password</label> | ||||
| 		<div class="col-span-2 "> | ||||
| 			<CopyPasswordField | ||||
| 				disabled | ||||
| 				readonly | ||||
| 				placeholder="Generated automatically after start" | ||||
| 				isPasswordField | ||||
| 				id="rootUserPassword" | ||||
| 				name="rootUserPassword" | ||||
| 				value={database.rootUserPassword} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	</div> --> | ||||
| </div> | ||||
							
								
								
									
										182
									
								
								src/routes/databases/[id]/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								src/routes/databases/[id]/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	function checkConfiguration(database): string { | ||||
| 		let configurationPhase = null; | ||||
| 		if (!database.type) { | ||||
| 			configurationPhase = 'type'; | ||||
| 		} else if (!database.version) { | ||||
| 			configurationPhase = 'version'; | ||||
| 		} else if (!database.destinationDockerId) { | ||||
| 			configurationPhase = 'destination'; | ||||
| 		} | ||||
| 		return configurationPhase; | ||||
| 	} | ||||
| 	export const load: Load = async ({ fetch, params, url }) => { | ||||
| 		const endpoint = `/databases/${params.id}.json`; | ||||
| 		const res = await fetch(endpoint); | ||||
| 		if (res.ok) { | ||||
| 			const { database, state, versions, privatePort, settings } = await res.json(); | ||||
| 			if (!database || Object.entries(database).length === 0) { | ||||
| 				return { | ||||
| 					status: 302, | ||||
| 					redirect: '/databases' | ||||
| 				}; | ||||
| 			} | ||||
| 			const configurationPhase = checkConfiguration(database); | ||||
| 			if ( | ||||
| 				configurationPhase && | ||||
| 				url.pathname !== `/databases/${params.id}/configuration/${configurationPhase}` | ||||
| 			) { | ||||
| 				return { | ||||
| 					status: 302, | ||||
| 					redirect: `/databases/${params.id}/configuration/${configurationPhase}` | ||||
| 				}; | ||||
| 			} | ||||
| 			return { | ||||
| 				props: { | ||||
| 					database, | ||||
| 					state, | ||||
| 					versions, | ||||
| 					privatePort | ||||
| 				}, | ||||
| 				stuff: { | ||||
| 					database, | ||||
| 					state, | ||||
| 					versions, | ||||
| 					privatePort, | ||||
| 					settings | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: 302, | ||||
| 			redirect: '/databases' | ||||
| 		}; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script> | ||||
| 	import { session } from '$app/stores'; | ||||
| 	import { errorNotification } from '$lib/form'; | ||||
| 	import DeleteIcon from '$lib/components/DeleteIcon.svelte'; | ||||
| 	import Loading from '$lib/components/Loading.svelte'; | ||||
| 	import { del, post } from '$lib/api'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
|  | ||||
| 	export let database; | ||||
| 	export let state; | ||||
| 	let loading = false; | ||||
|  | ||||
| 	async function deleteDatabase() { | ||||
| 		const sure = confirm(`Are you sure you would like to delete '${database.name}'?`); | ||||
| 		if (sure) { | ||||
| 			loading = true; | ||||
| 			try { | ||||
| 				await del(`/databases/${database.id}/delete.json`, { id: database.id }); | ||||
| 				return await goto('/databases'); | ||||
| 			} catch ({ error }) { | ||||
| 				return errorNotification(error); | ||||
| 			} finally { | ||||
| 				loading = false; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	async function stopDatabase() { | ||||
| 		const sure = confirm(`Are you sure you would like to stop '${database.name}'?`); | ||||
| 		if (sure) { | ||||
| 			loading = true; | ||||
| 			try { | ||||
| 				await post(`/databases/${database.id}/stop.json`, {}); | ||||
| 				return window.location.reload(); | ||||
| 			} catch ({ error }) { | ||||
| 				return errorNotification(error); | ||||
| 			} finally { | ||||
| 				loading = false; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	async function startDatabase() { | ||||
| 		loading = true; | ||||
| 		try { | ||||
| 			await post(`/databases/${database.id}/start.json`, {}); | ||||
| 			return window.location.reload(); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} finally { | ||||
| 			loading = false; | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <nav class="nav-side"> | ||||
| 	{#if loading} | ||||
| 		<Loading fullscreen cover /> | ||||
| 	{:else} | ||||
| 		{#if database.type && database.destinationDockerId && database.version && database.defaultDatabase} | ||||
| 			{#if state === 'running'} | ||||
| 				<button | ||||
| 					on:click={stopDatabase} | ||||
| 					title="Stop database" | ||||
| 					type="submit" | ||||
| 					disabled={!$session.isAdmin} | ||||
| 					class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-purple-600 hover:text-white" | ||||
| 					data-tooltip={$session.isAdmin | ||||
| 						? 'Stop database' | ||||
| 						: 'You do not have permission to stop the database.'} | ||||
| 				> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						class="w-6 h-6" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke-width="1.5" | ||||
| 						stroke="currentColor" | ||||
| 						fill="none" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 					> | ||||
| 						<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 						<rect x="6" y="5" width="4" height="14" rx="1" /> | ||||
| 						<rect x="14" y="5" width="4" height="14" rx="1" /> | ||||
| 					</svg> | ||||
| 				</button> | ||||
| 			{:else if state === 'not started'} | ||||
| 				<button | ||||
| 					on:click={startDatabase} | ||||
| 					title="Start database" | ||||
| 					type="submit" | ||||
| 					disabled={!$session.isAdmin} | ||||
| 					class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:bg-purple-600 hover:text-white" | ||||
| 					data-tooltip={$session.isAdmin | ||||
| 						? 'Start database' | ||||
| 						: 'You do not have permission to start the database.'} | ||||
| 					><svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						class="w-6 h-6" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke-width="1.5" | ||||
| 						stroke="currentColor" | ||||
| 						fill="none" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 					> | ||||
| 						<path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
| 						<path d="M7 4v16l13 -8z" /> | ||||
| 					</svg> | ||||
| 				</button> | ||||
| 			{/if} | ||||
| 		{/if} | ||||
| 		<button | ||||
| 			on:click={deleteDatabase} | ||||
| 			title="Delete Database" | ||||
| 			type="submit" | ||||
| 			disabled={!$session.isAdmin} | ||||
| 			class:hover:text-red-500={$session.isAdmin} | ||||
| 			class="icons bg-transparent tooltip-bottom text-sm" | ||||
| 			data-tooltip={$session.isAdmin | ||||
| 				? 'Delete Database' | ||||
| 				: 'You do not have permission to delete a Database'}><DeleteIcon /></button | ||||
| 		> | ||||
| 	{/if} | ||||
| </nav> | ||||
| <slot /> | ||||
							
								
								
									
										19
									
								
								src/routes/databases/[id]/configuration/destination.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/routes/databases/[id]/configuration/destination.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import { getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler } from '$lib/database'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	const { destinationId } = await event.request.json(); | ||||
|  | ||||
| 	try { | ||||
| 		await db.configureDestinationForDatabase({ id, destinationId }); | ||||
| 		return { status: 201 }; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										93
									
								
								src/routes/databases/[id]/configuration/destination.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/routes/databases/[id]/configuration/destination.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	export const load: Load = async ({ fetch, params, url, stuff }) => { | ||||
| 		const { database } = stuff; | ||||
| 		if (database?.destinationDockerId && !url.searchParams.get('from')) { | ||||
| 			return { | ||||
| 				status: 302, | ||||
| 				redirect: `/database/${params.id}` | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		const endpoint = `/destinations.json`; | ||||
| 		const res = await fetch(endpoint); | ||||
|  | ||||
| 		if (res.ok) { | ||||
| 			return { | ||||
| 				props: { | ||||
| 					...(await res.json()) | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: res.status, | ||||
| 			error: new Error(`Could not load ${url}`) | ||||
| 		}; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	import type Prisma from '@prisma/client'; | ||||
|  | ||||
| 	import { page } from '$app/stores'; | ||||
| 	import { errorNotification } from '$lib/form'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { post } from '$lib/api'; | ||||
|  | ||||
| 	const { id } = $page.params; | ||||
| 	const from = $page.url.searchParams.get('from'); | ||||
|  | ||||
| 	export let destinations: Prisma.DestinationDocker[]; | ||||
| 	async function handleSubmit(destinationId) { | ||||
| 		try { | ||||
| 			await post(`/databases/${id}/configuration/destination.json`, { | ||||
| 				destinationId | ||||
| 			}); | ||||
| 			return await goto(from || `/databases/${id}`); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <div class="flex space-x-1 p-6 font-bold"> | ||||
| 	<div class="mr-4 text-2xl tracking-tight">Configure Destination</div> | ||||
| </div> | ||||
| <div class="flex justify-center"> | ||||
| 	{#if !destinations || destinations.length === 0} | ||||
| 		<div class="flex-col"> | ||||
| 			<div class="pb-2">No configurable Destination found</div> | ||||
| 			<div class="flex justify-center"> | ||||
| 				<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500"> | ||||
| 					<svg | ||||
| 						class="w-6" | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						fill="none" | ||||
| 						viewBox="0 0 24 24" | ||||
| 						stroke="currentColor" | ||||
| 						><path | ||||
| 							stroke-linecap="round" | ||||
| 							stroke-linejoin="round" | ||||
| 							stroke-width="2" | ||||
| 							d="M12 6v6m0 0v6m0-6h6m-6 0H6" | ||||
| 						/></svg | ||||
| 					> | ||||
| 				</a> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	{:else} | ||||
| 		<div class="flex flex-wrap justify-center"> | ||||
| 			{#each destinations as destination} | ||||
| 				<div class="p-2"> | ||||
| 					<form on:submit|preventDefault={() => handleSubmit(destination.id)}> | ||||
| 						<button type="submit" class="box-selection hover:bg-sky-700 font-bold"> | ||||
| 							<div class="font-bold text-xl text-center truncate">{destination.name}</div> | ||||
| 							<div class="text-center truncate">{destination.network}</div> | ||||
| 						</button> | ||||
| 					</form> | ||||
| 				</div> | ||||
| 			{/each} | ||||
| 		</div> | ||||
| 	{/if} | ||||
| </div> | ||||
							
								
								
									
										32
									
								
								src/routes/databases/[id]/configuration/type.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/routes/databases/[id]/configuration/type.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| import { getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler, supportedDatabaseTypesAndVersions } from '$lib/database'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const get: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
| 	return { | ||||
| 		status: 200, | ||||
| 		body: { | ||||
| 			types: supportedDatabaseTypesAndVersions | ||||
| 		} | ||||
| 	}; | ||||
| }; | ||||
|  | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	const { type } = await event.request.json(); | ||||
|  | ||||
| 	try { | ||||
| 		await db.configureDatabaseType({ id, type }); | ||||
| 		return { | ||||
| 			status: 201 | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										81
									
								
								src/routes/databases/[id]/configuration/type.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/routes/databases/[id]/configuration/type.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	export const load: Load = async ({ fetch, params, url, stuff }) => { | ||||
| 		const { database } = stuff; | ||||
| 		if (database?.type && !url.searchParams.get('from')) { | ||||
| 			return { | ||||
| 				status: 302, | ||||
| 				redirect: `/databases/${params.id}` | ||||
| 			}; | ||||
| 		} | ||||
| 		const endpoint = `/databases/${params.id}/configuration/type.json`; | ||||
| 		const res = await fetch(endpoint); | ||||
|  | ||||
| 		if (res.ok) { | ||||
| 			return { | ||||
| 				props: { | ||||
| 					...(await res.json()) | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: res.status, | ||||
| 			error: new Error(`Could not load ${url}`) | ||||
| 		}; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	import { page } from '$app/stores'; | ||||
| 	import { errorNotification } from '$lib/form'; | ||||
|  | ||||
| 	const { id } = $page.params; | ||||
| 	const from = $page.url.searchParams.get('from'); | ||||
|  | ||||
| 	export let types; | ||||
| 	import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte'; | ||||
| 	import CouchDB from '$lib/components/svg/databases/CouchDB.svelte'; | ||||
| 	import MongoDB from '$lib/components/svg/databases/MongoDB.svelte'; | ||||
| 	import MySQL from '$lib/components/svg/databases/MySQL.svelte'; | ||||
| 	import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte'; | ||||
| 	import Redis from '$lib/components/svg/databases/Redis.svelte'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { post } from '$lib/api'; | ||||
| 	async function handleSubmit(type) { | ||||
| 		try { | ||||
| 			await post(`/databases/${id}/configuration/type.json`, { type }); | ||||
| 			return await goto(from || `/databases/${id}/configuration/version`); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <div class="flex space-x-1 p-6 font-bold"> | ||||
| 	<div class="mr-4 text-2xl tracking-tight">Select a Database type</div> | ||||
| </div> | ||||
|  | ||||
| <div class="flex flex-wrap justify-center"> | ||||
| 	{#each types as type} | ||||
| 		<div class="p-2"> | ||||
| 			<form on:submit|preventDefault={() => handleSubmit(type.name)}> | ||||
| 				<button type="submit" class="box-selection relative text-xl font-bold hover:bg-purple-700"> | ||||
| 					{#if type.name === 'clickhouse'} | ||||
| 						<Clickhouse isAbsolute /> | ||||
| 					{:else if type.name === 'couchdb'} | ||||
| 						<CouchDB isAbsolute /> | ||||
| 					{:else if type.name === 'mongodb'} | ||||
| 						<MongoDB isAbsolute /> | ||||
| 					{:else if type.name === 'mysql'} | ||||
| 						<MySQL isAbsolute /> | ||||
| 					{:else if type.name === 'postgresql'} | ||||
| 						<PostgreSQL isAbsolute /> | ||||
| 					{:else if type.name === 'redis'} | ||||
| 						<Redis isAbsolute /> | ||||
| 					{/if}{type.fancyName} | ||||
| 				</button> | ||||
| 			</form> | ||||
| 		</div> | ||||
| 	{/each} | ||||
| </div> | ||||
							
								
								
									
										36
									
								
								src/routes/databases/[id]/configuration/version.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/routes/databases/[id]/configuration/version.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| import { getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler, supportedDatabaseTypesAndVersions } from '$lib/database'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const get: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	const { type } = await db.getDatabase({ id, teamId }); | ||||
|  | ||||
| 	return { | ||||
| 		status: 200, | ||||
| 		body: { | ||||
| 			versions: supportedDatabaseTypesAndVersions.find((name) => name.name === type).versions | ||||
| 		} | ||||
| 	}; | ||||
| }; | ||||
|  | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	const { version } = await event.request.json(); | ||||
|  | ||||
| 	try { | ||||
| 		await db.setDatabase({ id, version }); | ||||
| 		return { | ||||
| 			status: 201 | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										63
									
								
								src/routes/databases/[id]/configuration/version.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/routes/databases/[id]/configuration/version.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	export const load: Load = async ({ fetch, params, url, stuff }) => { | ||||
| 		const { database } = stuff; | ||||
| 		if (database?.version && !url.searchParams.get('from')) { | ||||
| 			return { | ||||
| 				status: 302, | ||||
| 				redirect: `/databases/${params.id}` | ||||
| 			}; | ||||
| 		} | ||||
| 		const endpoint = `/databases/${params.id}/configuration/version.json`; | ||||
| 		const res = await fetch(endpoint); | ||||
|  | ||||
| 		if (res.ok) { | ||||
| 			return { | ||||
| 				props: { | ||||
| 					...(await res.json()) | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: res.status, | ||||
| 			error: new Error(`Could not load ${url}`) | ||||
| 		}; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	import { page } from '$app/stores'; | ||||
| 	import { enhance, errorNotification } from '$lib/form'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { post } from '$lib/api'; | ||||
|  | ||||
| 	const { id } = $page.params; | ||||
| 	const from = $page.url.searchParams.get('from'); | ||||
|  | ||||
| 	export let versions; | ||||
| 	async function handleSubmit(version) { | ||||
| 		try { | ||||
| 			await post(`/databases/${id}/configuration/version.json`, { version }); | ||||
| 			return await goto(from || `/databases/${id}/configuration/destination`); | ||||
| 		} catch ({ error }) { | ||||
| 			return errorNotification(error); | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <div class="flex space-x-1 p-6 font-bold"> | ||||
| 	<div class="mr-4 text-2xl tracking-tight">Select a Database version</div> | ||||
| </div> | ||||
|  | ||||
| <div class="flex flex-wrap justify-center"> | ||||
| 	{#each versions as version} | ||||
| 		<div class="p-2"> | ||||
| 			<form on:submit|preventDefault={() => handleSubmit(version)}> | ||||
| 				<button type="submit" class="box-selection text-xl font-bold hover:bg-purple-700" | ||||
| 					>{version}</button | ||||
| 				> | ||||
| 			</form> | ||||
| 		</div> | ||||
| 	{/each} | ||||
| </div> | ||||
							
								
								
									
										22
									
								
								src/routes/databases/[id]/delete.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/routes/databases/[id]/delete.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import { getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { PrismaErrorHandler, stopDatabase } from '$lib/database'; | ||||
| import { deleteProxy } from '$lib/haproxy'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const del: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
| 	const { id } = event.params; | ||||
| 	try { | ||||
| 		const database = await db.getDatabase({ id, teamId }); | ||||
| 		if (database.destinationDockerId) { | ||||
| 			const everStarted = await stopDatabase(database); | ||||
| 			if (everStarted) await deleteProxy({ id }); | ||||
| 		} | ||||
| 		await db.removeDatabase({ id }); | ||||
| 		return { status: 200 }; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										71
									
								
								src/routes/databases/[id]/index.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/routes/databases/[id]/index.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| import { asyncExecShell, getEngine, getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { generateDatabaseConfiguration, getVersions, PrismaErrorHandler } from '$lib/database'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const get: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	try { | ||||
| 		const database = await db.getDatabase({ id, teamId }); | ||||
| 		const { destinationDockerId, destinationDocker } = database; | ||||
|  | ||||
| 		let state = 'not started'; | ||||
| 		if (destinationDockerId) { | ||||
| 			const host = getEngine(destinationDocker.engine); | ||||
|  | ||||
| 			try { | ||||
| 				const { stdout } = await asyncExecShell( | ||||
| 					`DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}` | ||||
| 				); | ||||
|  | ||||
| 				if (JSON.parse(stdout).Running) { | ||||
| 					state = 'running'; | ||||
| 				} | ||||
| 			} catch (error) { | ||||
| 				// if (!error.stderr.includes('No such object')) { | ||||
| 				//     console.log(error) | ||||
| 				// } | ||||
| 			} | ||||
| 		} | ||||
| 		const configuration = generateDatabaseConfiguration(database); | ||||
| 		const settings = await db.listSettings(); | ||||
| 		return { | ||||
| 			body: { | ||||
| 				privatePort: configuration?.privatePort, | ||||
| 				database, | ||||
| 				state, | ||||
| 				versions: getVersions(database.type), | ||||
| 				settings | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
| 	const { id } = event.params; | ||||
| 	const { name, defaultDatabase, dbUser, dbUserPassword, rootUser, rootUserPassword, version } = | ||||
| 		await event.request.json(); | ||||
|  | ||||
| 	try { | ||||
| 		await db.updateDatabase({ | ||||
| 			id, | ||||
| 			name, | ||||
| 			defaultDatabase, | ||||
| 			dbUser, | ||||
| 			dbUserPassword, | ||||
| 			rootUser, | ||||
| 			rootUserPassword, | ||||
| 			version | ||||
| 		}); | ||||
| 		return { status: 201 }; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										67
									
								
								src/routes/databases/[id]/index.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/routes/databases/[id]/index.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	import Databases from './_Databases/_Databases.svelte'; | ||||
| 	export const load: Load = async ({ fetch, params, stuff }) => { | ||||
| 		if (stuff?.database?.id) { | ||||
| 			return { | ||||
| 				props: { | ||||
| 					database: stuff.database, | ||||
| 					versions: stuff.versions, | ||||
| 					privatePort: stuff.privatePort, | ||||
| 					settings: stuff.settings | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 		const endpoint = `/databases/${params.id}.json`; | ||||
| 		const res = await fetch(endpoint); | ||||
|  | ||||
| 		if (res.ok) { | ||||
| 			return { | ||||
| 				props: { | ||||
| 					...(await res.json()) | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: res.status, | ||||
| 			error: new Error(`Could not load ${endpoint}`) | ||||
| 		}; | ||||
| 	}; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte'; | ||||
| 	import CouchDb from '$lib/components/svg/databases/CouchDB.svelte'; | ||||
| 	import MongoDb from '$lib/components/svg/databases/MongoDB.svelte'; | ||||
| 	import MySql from '$lib/components/svg/databases/MySQL.svelte'; | ||||
| 	import PostgreSql from '$lib/components/svg/databases/PostgreSQL.svelte'; | ||||
| 	import Redis from '$lib/components/svg/databases/Redis.svelte'; | ||||
|  | ||||
| 	export let database; | ||||
| 	export let settings; | ||||
| 	export let privatePort; | ||||
| </script> | ||||
|  | ||||
| <div class="flex items-center space-x-2 p-6 text-2xl font-bold"> | ||||
| 	<div class="md:max-w-64 truncate text-base tracking-tight md:block md:text-2xl"> | ||||
| 		{database.name} | ||||
| 	</div> | ||||
| 	<span class="relative"> | ||||
| 		{#if database.type === 'clickhouse'} | ||||
| 			<Clickhouse /> | ||||
| 		{:else if database.type === 'couchdb'} | ||||
| 			<CouchDb /> | ||||
| 		{:else if database.type === 'mongodb'} | ||||
| 			<MongoDb /> | ||||
| 		{:else if database.type === 'mysql'} | ||||
| 			<MySql /> | ||||
| 		{:else if database.type === 'postgresql'} | ||||
| 			<PostgreSql /> | ||||
| 		{:else if database.type === 'redis'} | ||||
| 			<Redis /> | ||||
| 		{/if} | ||||
| 	</span> | ||||
| </div> | ||||
|  | ||||
| <Databases bind:database {privatePort} {settings} /> | ||||
							
								
								
									
										34
									
								
								src/routes/databases/[id]/settings.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/routes/databases/[id]/settings.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import { getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { generateDatabaseConfiguration, PrismaErrorHandler } from '$lib/database'; | ||||
| import { startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
|  | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { status, body, teamId } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
| 	const { isPublic, appendOnly = true } = await event.request.json(); | ||||
|  | ||||
| 	try { | ||||
| 		await db.setDatabase({ id, isPublic, appendOnly }); | ||||
| 		const database = await db.getDatabase({ id, teamId }); | ||||
| 		const { destinationDockerId, destinationDocker, publicPort } = database; | ||||
| 		const { privatePort } = generateDatabaseConfiguration(database); | ||||
|  | ||||
| 		if (destinationDockerId) { | ||||
| 			if (isPublic) { | ||||
| 				await startTcpProxy(destinationDocker, id, publicPort, privatePort); | ||||
| 			} else { | ||||
| 				await stopTcpHttpProxy(destinationDocker, publicPort); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			status: 201 | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										82
									
								
								src/routes/databases/[id]/start.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/routes/databases/[id]/start.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common'; | ||||
| import * as db from '$lib/database'; | ||||
| import { generateDatabaseConfiguration, PrismaErrorHandler } from '$lib/database'; | ||||
| import { promises as fs } from 'fs'; | ||||
| import yaml from 'js-yaml'; | ||||
| import type { RequestHandler } from '@sveltejs/kit'; | ||||
| import { makeLabelForStandaloneDatabase } from '$lib/buildPacks/common'; | ||||
| import { startTcpProxy } from '$lib/haproxy'; | ||||
|  | ||||
| export const post: RequestHandler = async (event) => { | ||||
| 	const { teamId, status, body } = await getUserDetails(event); | ||||
| 	if (status === 401) return { status, body }; | ||||
|  | ||||
| 	const { id } = event.params; | ||||
|  | ||||
| 	try { | ||||
| 		const database = await db.getDatabase({ id, teamId }); | ||||
| 		const { | ||||
| 			type, | ||||
| 			destinationDockerId, | ||||
| 			destinationDocker, | ||||
| 			publicPort, | ||||
| 			settings: { isPublic } | ||||
| 		} = database; | ||||
| 		const { privatePort, environmentVariables, image, volume, ulimits } = | ||||
| 			generateDatabaseConfiguration(database); | ||||
|  | ||||
| 		const network = destinationDockerId && destinationDocker.network; | ||||
| 		const host = getEngine(destinationDocker.engine); | ||||
| 		const engine = destinationDocker.engine; | ||||
| 		const volumeName = volume.split(':')[0]; | ||||
| 		const labels = await makeLabelForStandaloneDatabase({ id, image, volume }); | ||||
|  | ||||
| 		const { workdir } = await createDirectories({ repository: type, buildId: id }); | ||||
|  | ||||
| 		const composeFile = { | ||||
| 			version: '3.8', | ||||
| 			services: { | ||||
| 				[id]: { | ||||
| 					container_name: id, | ||||
| 					image, | ||||
| 					networks: [network], | ||||
| 					environment: environmentVariables, | ||||
| 					volumes: [volume], | ||||
| 					ulimits, | ||||
| 					labels, | ||||
| 					restart: 'always' | ||||
| 				} | ||||
| 			}, | ||||
| 			networks: { | ||||
| 				[network]: { | ||||
| 					external: true | ||||
| 				} | ||||
| 			}, | ||||
| 			volumes: { | ||||
| 				[volumeName]: { | ||||
| 					external: true | ||||
| 				} | ||||
| 			} | ||||
| 		}; | ||||
| 		const composeFileDestination = `${workdir}/docker-compose.yaml`; | ||||
| 		await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); | ||||
| 		try { | ||||
| 			await asyncExecShell(`DOCKER_HOST=${host} docker volume create ${volumeName}`); | ||||
| 		} catch (error) { | ||||
| 			console.log(error); | ||||
| 		} | ||||
| 		try { | ||||
| 			await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); | ||||
| 			if (isPublic) await startTcpProxy(destinationDocker, id, publicPort, privatePort); | ||||
| 			return { | ||||
| 				status: 200 | ||||
| 			}; | ||||
| 		} catch (error) { | ||||
| 			throw { | ||||
| 				error | ||||
| 			}; | ||||
| 		} | ||||
| 	} catch (error) { | ||||
| 		return PrismaErrorHandler(error); | ||||
| 	} | ||||
| }; | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 Andras Bacsai
					Andras Bacsai