v1.0.12 - Sveltekit migration (#44)
Changed the whole tech stack to SvelteKit which means: - Typescript - SSR - No fastify :( - Beta, but it's fine! Other changes: - Tailwind -> Tailwind JIT - A lot more
This commit is contained in:
		
							
								
								
									
										15
									
								
								src/routes/__error.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/routes/__error.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| <script context="module"> | ||||
| 	export function load({ error, status }) { | ||||
| 		return { | ||||
| 			props: { | ||||
| 				error: `${status}: ${error.message}` | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <script> | ||||
| 	export let error; | ||||
| </script> | ||||
|  | ||||
| <h1 class="text-xl font-bold">{error}</h1> | ||||
							
								
								
									
										315
									
								
								src/routes/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										315
									
								
								src/routes/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,315 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import { publicPages } from '$lib/consts'; | ||||
| 	import { request } from '$lib/api/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: 301, | ||||
| 					redirect: '/' | ||||
| 				}; | ||||
| 			} | ||||
| 			return {}; | ||||
| 		} | ||||
| 		if (!publicPages.includes(path)) { | ||||
| 			return { | ||||
| 				status: 301, | ||||
| 				redirect: '/' | ||||
| 			}; | ||||
| 		} | ||||
| 		return {}; | ||||
| 	} | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
| 	import '../app.postcss'; | ||||
| 	export let initDashboard; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 	import { SvelteToast } from '@zerodevx/svelte-toast'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	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 } from '$store'; | ||||
| 	import { browser } from '$app/env'; | ||||
| 	$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; | ||||
| 	const options = { | ||||
| 		duration: 2000 | ||||
| 	}; | ||||
| 	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 } }); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| 	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() { | ||||
| 		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.' | ||||
| 				); | ||||
| 		} | ||||
| 	} | ||||
| 	async function logout() { | ||||
| 		await request('/api/v1/logout', $session, { body: {}, method: 'DELETE' }); | ||||
| 		location.reload(); | ||||
| 	} | ||||
| 	function reloadInAMin() { | ||||
| 		setTimeout(() => { | ||||
| 			location.reload(); | ||||
| 		}, 30000); | ||||
| 	} | ||||
| 	function ackError() { | ||||
| 		localStorage.setItem('automaticErrorReportsAck', 'true'); | ||||
| 		showAck = false; | ||||
| 	} | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <SvelteToast {options} /> | ||||
|  | ||||
| {#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 | ||||
| 				>. | ||||
| 			</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> | ||||
|  | ||||
| 				<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" | ||||
| 							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"> | ||||
| 					<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')} | ||||
| 					> | ||||
| 						<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="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> | ||||
| 				<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')} | ||||
| 					> | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							class="w-8" | ||||
| 							fill="none" | ||||
| 							viewBox="0 0 24 24" | ||||
| 							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" /> | ||||
| 				<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" | ||||
| 				> | ||||
| 					{packageJson.version} | ||||
| 				</a> | ||||
| 			</div> | ||||
| 		</nav> | ||||
| 	{/if} | ||||
| 	<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 transform 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} | ||||
							
								
								
									
										42
									
								
								src/routes/api/_index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/routes/api/_index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
|  | ||||
| // export async function api(request: Request, resource: string, data?: {}) { | ||||
| //     const base = 'https://github.com/'; | ||||
| //     if (!request.context.isLoggedIn) { | ||||
| //         return { status: 401, body: 'Unauthorized' }; | ||||
| //     } | ||||
|  | ||||
| //     const res = await fetch(`${base}${resource}`, { | ||||
| //         method: request.method, | ||||
| //         headers: { | ||||
| //             'content-type': 'application/json' | ||||
| //         }, | ||||
| //         body: data && JSON.stringify(data) | ||||
| //     }); | ||||
| //     return { | ||||
| //         status: res.status, | ||||
| //         body: await res.json() | ||||
| //     }; | ||||
| // } | ||||
|  | ||||
| export async function githubAPI( | ||||
| 	request: Request, | ||||
| 	resource: string, | ||||
| 	token?: string, | ||||
| 	data?: Record<string, unknown> | ||||
| ) { | ||||
| 	const base = 'https://api.github.com'; | ||||
| 	const res = await fetch(`${base}${resource}`, { | ||||
| 		method: request.method, | ||||
| 		headers: { | ||||
| 			'content-type': 'application/json', | ||||
| 			accept: 'application/json', | ||||
| 			authorization: token ? `token ${token}` : '' | ||||
| 		}, | ||||
| 		body: data && JSON.stringify(data) | ||||
| 	}); | ||||
| 	return { | ||||
| 		status: res.status, | ||||
| 		body: await res.json() | ||||
| 	}; | ||||
| } | ||||
							
								
								
									
										51
									
								
								src/routes/api/v1/application/check.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/routes/api/v1/application/check.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| import { setDefaultConfiguration } from '$lib/api/applications/configuration'; | ||||
| import { saveServerLog } from '$lib/api/applications/logging'; | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	try { | ||||
| 		const { DOMAIN } = process.env; | ||||
| 		const configuration = setDefaultConfiguration(request.body); | ||||
|  | ||||
| 		const services = (await docker.engine.listServices()).filter( | ||||
| 			(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application' | ||||
| 		); | ||||
| 		let foundDomain = false; | ||||
|  | ||||
| 		for (const service of services) { | ||||
| 			const running = JSON.parse(service.Spec.Labels.configuration); | ||||
| 			if (running) { | ||||
| 				if ( | ||||
| 					running.publish.domain === configuration.publish.domain && | ||||
| 					running.repository.id !== configuration.repository.id && | ||||
| 					running.publish.path === configuration.publish.path | ||||
| 				) { | ||||
| 					foundDomain = true; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		if (DOMAIN === configuration.publish.domain) foundDomain = true; | ||||
| 		if (foundDomain) { | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				body: { | ||||
| 					success: false, | ||||
| 					message: 'Domain already in use.' | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { success: true, message: 'OK' } | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		await saveServerLog(error); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										50
									
								
								src/routes/api/v1/application/config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/routes/api/v1/application/config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
|  | ||||
| export async function post(request: Request) { | ||||
| 	const { name, organization, branch }: any = request.body || {}; | ||||
| 	if (name && organization && branch) { | ||||
| 		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 (branch) { | ||||
| 				if ( | ||||
| 					configuration.repository.name === name && | ||||
| 					configuration.repository.organization === organization && | ||||
| 					configuration.repository.branch === branch | ||||
| 				) { | ||||
| 					return r; | ||||
| 				} | ||||
| 			} else { | ||||
| 				if ( | ||||
| 					configuration.repository.name === name && | ||||
| 					configuration.repository.organization === organization | ||||
| 				) { | ||||
| 					return r; | ||||
| 				} | ||||
| 			} | ||||
| 			return null; | ||||
| 		}); | ||||
| 		if (found) { | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				body: { | ||||
| 					success: true, | ||||
| 					...JSON.parse(found.Spec.Labels.configuration) | ||||
| 				} | ||||
| 			}; | ||||
| 		} else { | ||||
| 			return { | ||||
| 				status: 500, | ||||
| 				body: { | ||||
| 					error: 'No configuration found.' | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										90
									
								
								src/routes/api/v1/application/deploy/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/routes/api/v1/application/deploy/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import Deployment from '$models/Logs/Deployment'; | ||||
| import { docker } from '$lib/api/docker'; | ||||
| 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'; | ||||
| export async function post(request: Request) { | ||||
| 	let configuration; | ||||
| 	try { | ||||
| 		const services = (await docker.engine.listServices()).filter( | ||||
| 			(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application' | ||||
| 		); | ||||
| 		configuration = setDefaultConfiguration(request.body); | ||||
|  | ||||
| 		if (!configuration) { | ||||
| 			return { | ||||
| 				status: 500, | ||||
| 				body: { | ||||
| 					error: 'Whaaat?' | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 		await cloneRepository(configuration); | ||||
| 		const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({ | ||||
| 			services, | ||||
| 			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: configuration.repository.id, | ||||
| 			branch: configuration.repository.branch, | ||||
| 			organization: configuration.repository.organization, | ||||
| 			name: configuration.repository.name, | ||||
| 			domain: configuration.publish.domain, | ||||
| 			progress: { $in: ['queued', 'inprogress'] } | ||||
| 		}); | ||||
| 		if (alreadyQueued.length > 0) { | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				body: { | ||||
| 					success: false, | ||||
| 					message: 'Already in the queue.' | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 		queueAndBuild(configuration, imageChanged); | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				message: 'Deployment queued.', | ||||
| 				nickname: configuration.general.nickname, | ||||
| 				name: configuration.build.container.name, | ||||
| 				deployId: configuration.general.deployId | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		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 | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										35
									
								
								src/routes/api/v1/application/deploy/logs/[deployId].ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/routes/api/v1/application/deploy/logs/[deployId].ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import ApplicationLog from '$models/Logs/Application'; | ||||
| import Deployment from '$models/Logs/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') | ||||
| 			.sort({ createdAt: 'desc' }); | ||||
|  | ||||
| 		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 (e) { | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: e | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										47
									
								
								src/routes/api/v1/application/deploy/logs/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/routes/api/v1/application/deploy/logs/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| 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/Logs/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(); | ||||
|  | ||||
| 			return finalLogs; | ||||
| 		}); | ||||
| 		return { | ||||
| 			status: 200, | ||||
| 			body: { | ||||
| 				success: true, | ||||
| 				logs: finalLogs | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		console.log(error); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										27
									
								
								src/routes/api/v1/application/logs.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/routes/api/v1/application/logs.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| 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) { | ||||
| 		await saveServerLog(error); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										60
									
								
								src/routes/api/v1/application/remove.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/routes/api/v1/application/remove.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| import { purgeImagesContainers } from '$lib/api/applications/cleanup'; | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import Deployment from '$models/Logs/Deployment'; | ||||
| import ApplicationLog from '$models/Logs/Application'; | ||||
| import { delay, execShellAsync } from '$lib/api/common'; | ||||
|  | ||||
| async function call(found) { | ||||
| 	await delay(10000); | ||||
| 	await purgeImagesContainers(found, true); | ||||
| } | ||||
| export async function post(request: Request) { | ||||
| 	const { organization, name, branch } = request.body; | ||||
| 	let found = false; | ||||
| 	try { | ||||
| 		(await docker.engine.listServices()) | ||||
| 			.filter((r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application') | ||||
| 			.map((s) => { | ||||
| 				const running = JSON.parse(s.Spec.Labels.configuration); | ||||
| 				if ( | ||||
| 					running.repository.organization === organization && | ||||
| 					running.repository.name === name && | ||||
| 					running.repository.branch === branch | ||||
| 				) { | ||||
| 					found = running; | ||||
| 				} | ||||
| 				return null; | ||||
| 			}); | ||||
| 		if (found) { | ||||
| 			const deploys = await Deployment.find({ organization, branch, name }); | ||||
| 			for (const deploy of deploys) { | ||||
| 				await ApplicationLog.deleteMany({ deployId: deploy.deployId }); | ||||
| 				await Deployment.deleteMany({ deployId: deploy.deployId }); | ||||
| 			} | ||||
| 			await execShellAsync(`docker stack rm ${found.build.container.name}`); | ||||
| 			call(found); | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				body: { | ||||
| 					organization, | ||||
| 					name, | ||||
| 					branch | ||||
| 				} | ||||
| 			}; | ||||
| 		} else { | ||||
| 			return { | ||||
| 				status: 500, | ||||
| 				error: { | ||||
| 					message: 'Nothing to do.' | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 	} catch (error) { | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			error: { | ||||
| 				message: 'Nothing to do.' | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										76
									
								
								src/routes/api/v1/dashboard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/routes/api/v1/dashboard.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| import { docker } from '$lib/api/docker'; | ||||
| import LogsServer from '$models/Logs/Server'; | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
|  | ||||
| export async function get(request: Request) { | ||||
| 	const serverLogs = await LogsServer.find(); | ||||
| 	const dockerServices = await docker.engine.listServices(); | ||||
| 	let applications: any = dockerServices.filter( | ||||
| 		(r) => | ||||
| 			r.Spec.Labels.managedBy === 'coolify' && | ||||
| 			r.Spec.Labels.type === 'application' && | ||||
| 			r.Spec.Labels.configuration | ||||
| 	); | ||||
| 	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 | ||||
| 	); | ||||
| 	applications = applications.map((r) => { | ||||
| 		if (JSON.parse(r.Spec.Labels.configuration)) { | ||||
| 			return { | ||||
| 				configuration: JSON.parse(r.Spec.Labels.configuration), | ||||
| 				UpdatedAt: r.UpdatedAt | ||||
| 			}; | ||||
| 		} | ||||
| 		return {}; | ||||
| 	}); | ||||
| 	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 {}; | ||||
| 	}); | ||||
| 	applications = [ | ||||
| 		...new Map( | ||||
| 			applications.map((item) => [ | ||||
| 				item.configuration.publish.domain + item.configuration.publish.path, | ||||
| 				item | ||||
| 			]) | ||||
| 		).values() | ||||
| 	]; | ||||
| 	return { | ||||
| 		status: 200, | ||||
| 		body: { | ||||
| 			success: true, | ||||
| 			serverLogs, | ||||
| 			applications: { | ||||
| 				deployed: applications | ||||
| 			}, | ||||
| 			databases: { | ||||
| 				deployed: databases | ||||
| 			}, | ||||
| 			services: { | ||||
| 				deployed: services | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
| } | ||||
							
								
								
									
										122
									
								
								src/routes/api/v1/databases/[deployId]/backup.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								src/routes/api/v1/databases/[deployId]/backup.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| 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}`) | ||||
| 				}; | ||||
| 			} | ||||
| 		} | ||||
| 		return { | ||||
| 			status: 501, | ||||
| 			body: { | ||||
| 				error: `Backup method not implemented yet for ${type}.` | ||||
| 			} | ||||
| 		}; | ||||
| 	} catch (error) { | ||||
| 		console.log(error); | ||||
| 		await saveServerLog(error); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error | ||||
| 			} | ||||
| 		}; | ||||
| 	} finally { | ||||
| 		await execShellAsync(`rm -fr ${tmpdir}`); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										59
									
								
								src/routes/api/v1/databases/[deployId]/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/routes/api/v1/databases/[deployId]/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| 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.' | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										161
									
								
								src/routes/api/v1/databases/deploy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								src/routes/api/v1/databases/deploy.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| 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 | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		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 | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										110
									
								
								src/routes/api/v1/login/github/app.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/routes/api/v1/login/github/app.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| import { githubAPI } from '$api'; | ||||
| 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'; | ||||
|  | ||||
| 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, id } = 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 | ||||
| 				}); | ||||
| 				const defaultSettings = new Settings({ | ||||
| 					_id: new mongoose.Types.ObjectId() | ||||
| 				}); | ||||
| 				try { | ||||
| 					await newUser.save(); | ||||
| 					await defaultSettings.save(); | ||||
| 				} catch (e) { | ||||
| 					console.log(e); | ||||
| 					return { | ||||
| 						status: 500, | ||||
| 						body: e | ||||
| 					}; | ||||
| 				} | ||||
| 			} 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 | ||||
| 						}); | ||||
| 						try { | ||||
| 							await newUser.save(); | ||||
| 						} catch (e) { | ||||
| 							console.log(e); | ||||
| 							return { | ||||
| 								status: 500, | ||||
| 								body: { | ||||
| 									error: e | ||||
| 								} | ||||
| 							}; | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		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) { | ||||
| 		console.log('error happened'); | ||||
| 		console.log(error); | ||||
| 		return { status: 500, body: { ...error } }; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										10
									
								
								src/routes/api/v1/logout.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/routes/api/v1/logout.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| export async function del(request: Request) { | ||||
| 	request.locals.session.destroy = true; | ||||
|  | ||||
| 	return { | ||||
| 		body: { | ||||
| 			ok: true | ||||
| 		} | ||||
| 	}; | ||||
| } | ||||
							
								
								
									
										52
									
								
								src/routes/api/v1/services/[serviceName].ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/routes/api/v1/services/[serviceName].ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| 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) { | ||||
| 		console.log(error); | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				success: false, | ||||
| 				error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| export async function del(request: Request) { | ||||
| 	const { serviceName } = request.params; | ||||
| 	await execShellAsync(`docker stack rm ${serviceName}`); | ||||
| 	return { status: 200, body: {} }; | ||||
| } | ||||
							
								
								
									
										24
									
								
								src/routes/api/v1/services/deploy/plausible/activate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/routes/api/v1/services/deploy/plausible/activate.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| 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' } | ||||
| 	}; | ||||
| } | ||||
							
								
								
									
										187
									
								
								src/routes/api/v1/services/deploy/plausible/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								src/routes/api/v1/services/deploy/plausible/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,187 @@ | ||||
| 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' } | ||||
| 	}; | ||||
| } | ||||
							
								
								
									
										52
									
								
								src/routes/api/v1/settings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/routes/api/v1/settings.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| 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 | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| 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 | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										20
									
								
								src/routes/api/v1/upgrade.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/routes/api/v1/upgrade.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| 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)"' | ||||
| 	); | ||||
| 	// saveServerLog({ message: upgradeP2, type: 'UPGRADE-P-2' }) | ||||
| 	return { | ||||
| 		status: 200, | ||||
| 		body: { | ||||
| 			message: "I'm trying, okay?" | ||||
| 		} | ||||
| 	}; | ||||
| } | ||||
							
								
								
									
										24
									
								
								src/routes/api/v1/verify.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/routes/api/v1/verify.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| // import { deleteCookies } from '$lib/api/common'; | ||||
| // import { verifyUserId } from '$lib/api/common'; | ||||
| // import type { Request } from '@sveltejs/kit'; | ||||
| // import * as cookie from 'cookie'; | ||||
|  | ||||
| // export async function post(request: Request) { | ||||
| // 	const { coolToken } = cookie.parse(request.headers.cookie || ''); | ||||
| // 	try { | ||||
| // 		await verifyUserId(coolToken); | ||||
| // 		return { | ||||
| // 			status: 200, | ||||
| // 			body: { success: true } | ||||
| // 		}; | ||||
| // 	} catch (error) { | ||||
| // 		return { | ||||
| // 			status: 301, | ||||
| // 			headers: { | ||||
| // 				location: '/', | ||||
| // 				'set-cookie': [...deleteCookies] | ||||
| // 			}, | ||||
| // 			body: { error: 'Unauthorized' } | ||||
| // 		}; | ||||
| // 	} | ||||
| // } | ||||
							
								
								
									
										113
									
								
								src/routes/api/v1/webhooks/deploy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/routes/api/v1/webhooks/deploy.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| import type { Request } from '@sveltejs/kit'; | ||||
| import crypto from 'crypto'; | ||||
| import Deployment from '$models/Logs/Deployment'; | ||||
| import { docker } from '$lib/api/docker'; | ||||
| 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'; | ||||
| export async function post(request: Request) { | ||||
| 	let configuration; | ||||
| 	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 (request.headers['x-github-event'] !== 'push') { | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error: 'Not a push event.' | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| 	try { | ||||
| 		const services = (await docker.engine.listServices()).filter( | ||||
| 			(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application' | ||||
| 		); | ||||
|  | ||||
| 		configuration = services.find((r) => { | ||||
| 			if (request.body.ref.startsWith('refs')) { | ||||
| 				const branch = request.body.ref.split('/')[2]; | ||||
| 				if ( | ||||
| 					JSON.parse(r.Spec.Labels.configuration).repository.id === request.body.repository.id && | ||||
| 					JSON.parse(r.Spec.Labels.configuration).repository.branch === branch | ||||
| 				) { | ||||
| 					return r; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			return null; | ||||
| 		}); | ||||
| 		configuration = setDefaultConfiguration(JSON.parse(configuration.Spec.Labels.configuration)); | ||||
|  | ||||
| 		if (!configuration) { | ||||
| 			return { | ||||
| 				status: 500, | ||||
| 				body: { | ||||
| 					error: 'Whaaat?' | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 		await cloneRepository(configuration); | ||||
| 		const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({ | ||||
| 			services, | ||||
| 			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: configuration.repository.id, | ||||
| 			branch: configuration.repository.branch, | ||||
| 			organization: configuration.repository.organization, | ||||
| 			name: configuration.repository.name, | ||||
| 			domain: configuration.publish.domain, | ||||
| 			progress: { $in: ['queued', 'inprogress'] } | ||||
| 		}); | ||||
| 		if (alreadyQueued.length > 0) { | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				body: { | ||||
| 					success: false, | ||||
| 					message: 'Already in the queue.' | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 		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) { | ||||
| 		return { | ||||
| 			status: 500, | ||||
| 			body: { | ||||
| 				error | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,6 @@ | ||||
| <script> | ||||
| 	import Configuration from '$components/Application/Configuration.svelte'; | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <Configuration /> | ||||
| @@ -0,0 +1,83 @@ | ||||
| <script> | ||||
| 	import { onDestroy, onMount } from 'svelte'; | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| 	import Loading from '$components/Loading.svelte'; | ||||
| 	import { request } from '$lib/api/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} | ||||
| @@ -0,0 +1,135 @@ | ||||
| <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/api/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:bg-red-200={deployment.progress === 'failed'} | ||||
| 								class:hover:border-red-500={deployment.progress === 'failed'} | ||||
| 								on:click={() => goto(`./logs/${deployment.deployId}`)} | ||||
| 							> | ||||
| 								<div class="font-bold text-sm px-3 flex justify-center items-center"> | ||||
| 									{deployment.branch} | ||||
| 								</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> | ||||
							
								
								
									
										127
									
								
								src/routes/application/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								src/routes/application/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | ||||
| <script> | ||||
| 	import { application, initialApplication, initConf, dashboard } 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/api/request'; | ||||
|  | ||||
| 	$application.repository.organization = $page.params.organization; | ||||
| 	$application.repository.name = $page.params.name; | ||||
| 	$application.repository.branch = $page.params.branch; | ||||
|  | ||||
| 	async function setConfiguration() { | ||||
| 		try { | ||||
| 			const config = await request(`/api/v1/application/config`, $session, { | ||||
| 				body: { | ||||
| 					name: $application.repository.name, | ||||
| 					organization: $application.repository.organization, | ||||
| 					branch: $application.repository.branch | ||||
| 				} | ||||
| 			}); | ||||
| 			$application = { ...config }; | ||||
| 			$initConf = JSON.parse(JSON.stringify($application)); | ||||
| 		} 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 { organization, name, branch } = app.configuration.repository; | ||||
| 					if ( | ||||
| 						organization === $application.repository.organization && | ||||
| 						name === $application.repository.name && | ||||
| 						branch === $application.repository.branch | ||||
| 					) { | ||||
| 						return app; | ||||
| 					} | ||||
| 				}); | ||||
| 				if (found) { | ||||
| 					$application = { ...found.configuration }; | ||||
| 					$initConf = JSON.parse(JSON.stringify($application)); | ||||
| 				} else { | ||||
| 					await setConfiguration(); | ||||
| 				} | ||||
| 			} | ||||
| 		} 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} | ||||
							
								
								
									
										5
									
								
								src/routes/application/new.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/routes/application/new.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| <script> | ||||
| 	import Configuration from '$components/Application/Configuration.svelte'; | ||||
| </script> | ||||
|  | ||||
| <Configuration /> | ||||
							
								
								
									
										44
									
								
								src/routes/dashboard/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/routes/dashboard/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import { request } from '$lib/api/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> | ||||
							
								
								
									
										299
									
								
								src/routes/dashboard/applications.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										299
									
								
								src/routes/dashboard/applications.svelte
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										82
									
								
								src/routes/dashboard/databases.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/routes/dashboard/databases.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| <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'; | ||||
| </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 transform 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 == '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> | ||||
|  | ||||
							
								
								
									
										62
									
								
								src/routes/dashboard/services.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/routes/dashboard/services.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| <script> | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { dashboard } from '$store'; | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| </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={() => goto(`/service/${service.serviceName}/configuration`)} | ||||
| 					> | ||||
| 						<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 transform 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> | ||||
| 								{/if} | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				{/each} | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	{:else} | ||||
| 		<div class="text-2xl font-bold text-center">No services found</div> | ||||
| 	{/if} | ||||
| </div> | ||||
							
								
								
									
										110
									
								
								src/routes/database/[name]/configuration.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/routes/database/[name]/configuration.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| <script> | ||||
| 	import { database } from '$store'; | ||||
| 	import { page, session } from '$app/stores'; | ||||
| 	import { request } from '$lib/api/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'; | ||||
|  | ||||
| 	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" /> | ||||
| 				{/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 === '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} | ||||
| 		<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} | ||||
							
								
								
									
										78
									
								
								src/routes/database/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/routes/database/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| <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/api/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> | ||||
							
								
								
									
										11
									
								
								src/routes/database/new.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/routes/database/new.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| <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 /> | ||||
							
								
								
									
										60
									
								
								src/routes/index.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/routes/index.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| <script> | ||||
| 	import { browser } from '$app/env'; | ||||
|  | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { session } from '$app/stores'; | ||||
| 	import { request } from '$lib/api/request'; | ||||
|  | ||||
| 	async function login() { | ||||
| 		const left = screen.width / 2 - 1020 / 2; | ||||
| 		const top = screen.height / 2 - 618 / 2; | ||||
| 		const newWindow = open( | ||||
| 			`https://github.com/login/oauth/authorize?client_id=${ | ||||
| 				import.meta.env.VITE_GITHUB_APP_CLIENTID | ||||
| 			}`, | ||||
| 			'Authenticate', | ||||
| 			'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); | ||||
| 				browser && location.reload() | ||||
| 			} | ||||
| 		}, 100); | ||||
| 	} | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <div class="flex justify-center items-center h-screen w-full bg-warmGray-900"> | ||||
| 	<div class="max-w-7xl mx-auto px-4 sm:py-24 sm:px-6 lg:px-8"> | ||||
| 		<div class="text-center"> | ||||
| 			<p | ||||
| 				class="mt-1 pb-8 font-extrabold text-white text-5xl sm:tracking-tight lg:text-6xl text-center" | ||||
| 			> | ||||
| 				<span class="border-gradient">Coolify</span> | ||||
| 			</p> | ||||
| 			<h2 class="text-2xl md:text-3xl font-extrabold text-white"> | ||||
| 				An open-source, hassle-free, self-hostable<br /> | ||||
| 				<span class="text-indigo-400">Heroku</span> | ||||
| 				& <span class="text-green-400">Netlify</span> alternative | ||||
| 			</h2> | ||||
| 			<div class="text-center py-10"> | ||||
| 				{#if !$session.isLoggedIn} | ||||
| 					<button | ||||
| 						class="text-white bg-warmGray-800 hover:bg-warmGray-700 rounded p-2 px-10 font-bold" | ||||
| 						on:click={login}>Login with Github</button | ||||
| 					> | ||||
| 				{:else} | ||||
| 					<button | ||||
| 						class="text-white bg-warmGray-800 hover:bg-warmGray-700 rounded p-2 px-10 font-bold" | ||||
| 						on:click={() => goto('/dashboard/applications')}>Get Started</button | ||||
| 					> | ||||
| 				{/if} | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
							
								
								
									
										71
									
								
								src/routes/service/[name]/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/routes/service/[name]/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| <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/api/request'; | ||||
|  | ||||
| 	import { toast } from '@zerodevx/svelte-toast'; | ||||
|  | ||||
| 	async function removeService() { | ||||
| 		await request(`/api/v1/services/${$page.params.name}`, $session, { | ||||
| 			method: 'DELETE' | ||||
| 		}); | ||||
| 		if (browser) { | ||||
| 			toast.push('Service removed.'); | ||||
| 			goto(`/dashboard/services`, { replaceState: true }); | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <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={removeService}> | ||||
| 			<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 === '/service/new'} | ||||
| 			class:text-yellow-400={$page.path.startsWith('/service')} | ||||
| 			class:bg-warmGray-700={$page.path.startsWith('/service')} | ||||
| 			on:click={() => goto(`/service/${$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> | ||||
|  | ||||
| <div class="text-white"> | ||||
| 	<slot /> | ||||
| </div> | ||||
							
								
								
									
										83
									
								
								src/routes/service/[name]/configuration.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/routes/service/[name]/configuration.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| <script> | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| 	import { toast } from '@zerodevx/svelte-toast'; | ||||
|  | ||||
| 	import { page, session } from '$app/stores'; | ||||
| 	import { request } from '$lib/api/request'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import Loading from '$components/Loading.svelte'; | ||||
| 	import Plausible from '$components/Service/Plausible.svelte'; | ||||
| 	import { browser } from '$app/env'; | ||||
| 	let service = {}; | ||||
| 	async function loadServiceConfig() { | ||||
| 		if ($page.params.name) { | ||||
| 			try { | ||||
| 				service = await request(`/api/v1/services/${$page.params.name}`, $session); | ||||
| 			} catch (error) { | ||||
| 				browser && toast.push(`Cannot find service ${$page.params.name}?!`); | ||||
| 				goto(`/dashboard/services`, { replaceState: true }); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	async function activate() { | ||||
| 		try { | ||||
| 			await request(`/api/v1/services/deploy/${$page.params.name}/activate`, $session, { | ||||
| 				method: 'PATCH', | ||||
| 				body: {} | ||||
| 			}); | ||||
| 			browser && toast.push(`All users are activated for Plausible.`); | ||||
| 		} catch (error) { | ||||
| 			console.log(error); | ||||
| 			browser && toast.push(`Ooops, there was an error activating users for Plausible?!`); | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| {#await loadServiceConfig()} | ||||
| 	<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>{$page.params.name === 'plausible' ? 'Plausible Analytics' : $page.params.name}</div> | ||||
| 			<div class="px-4"> | ||||
| 				{#if $page.params.name === 'plausible'} | ||||
| 					<img | ||||
| 						alt="plausible logo" | ||||
| 						class="w-6 mx-auto" | ||||
| 						src="https://cdn.coollabs.io/assets/coolify/services/plausible/logo_sm.png" | ||||
| 					/> | ||||
| 				{/if} | ||||
| 			</div> | ||||
| 			<a | ||||
| 			target="_blank" | ||||
| 			class="icon mx-2" | ||||
| 			href={service.config.baseURL} | ||||
| 		> | ||||
| 			<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> | ||||
| 	</div> | ||||
| 	<div class="space-y-2 max-w-4xl mx-auto px-6" in:fade={{ duration: 100 }}> | ||||
| 		<div class="block text-center py-4"> | ||||
| 			{#if $page.params.name === 'plausible'} | ||||
| 				<Plausible {service} /> | ||||
| 			{/if} | ||||
| 		</div> | ||||
| 	</div> | ||||
| {/await} | ||||
							
								
								
									
										42
									
								
								src/routes/service/new/[type]/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/routes/service/new/[type]/__layout.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| <script> | ||||
| 	import { browser } from '$app/env'; | ||||
|  | ||||
| 	import { goto } from '$app/navigation'; | ||||
|  | ||||
| 	import { page, session } from '$app/stores'; | ||||
| 	import Loading from '$components/Loading.svelte'; | ||||
| 	import { request } from '$lib/api/request'; | ||||
| 	import { initialNewService, newService } from '$store'; | ||||
|  | ||||
| 	import { toast } from '@zerodevx/svelte-toast'; | ||||
| 	import { onDestroy } from 'svelte'; | ||||
|  | ||||
| 	async function checkService() { | ||||
| 		try { | ||||
| 			const data = await request(`/api/v1/services/${$page.params.type}`, $session); | ||||
| 			if (!data?.success) { | ||||
| 				if (browser) { | ||||
| 					goto(`/dashboard/services`, { replaceState: true }); | ||||
| 					toast.push( | ||||
| 						`${ | ||||
| 							$page.params.type === 'plausible' ? 'Plausible Analytics' : $page.params.type | ||||
| 						} already deployed.` | ||||
| 					); | ||||
| 				} | ||||
| 			} | ||||
| 		} catch (error) { | ||||
| 			// | ||||
| 		} | ||||
| 	} | ||||
| 	onDestroy(() => { | ||||
| 		$newService = JSON.parse(JSON.stringify(initialNewService)); | ||||
| 	}); | ||||
| </script> | ||||
|  | ||||
| {#await checkService()} | ||||
| 	<Loading /> | ||||
| {:then} | ||||
| 	<div class="text-white"> | ||||
| 		<slot /> | ||||
| 	</div> | ||||
| {/await} | ||||
							
								
								
									
										130
									
								
								src/routes/service/new/[type]/index.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								src/routes/service/new/[type]/index.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| <script> | ||||
| 	import { fade } from 'svelte/transition'; | ||||
|  | ||||
| 	import { toast } from '@zerodevx/svelte-toast'; | ||||
| 	import { newService } from '$store'; | ||||
| 	import { page, session } from '$app/stores'; | ||||
| 	import { request } from '$lib/api/request'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import Loading from '$components/Loading.svelte'; | ||||
| 	import TooltipInfo from '$components/TooltipInfo.svelte'; | ||||
| 	import { browser } from '$app/env'; | ||||
|  | ||||
| 	$: deployable = | ||||
| 		$newService.baseURL === '' || | ||||
| 		$newService.baseURL === null || | ||||
| 		$newService.email === '' || | ||||
| 		$newService.email === null || | ||||
| 		$newService.userName === '' || | ||||
| 		$newService.userName === null || | ||||
| 		$newService.userPassword === '' || | ||||
| 		$newService.userPassword === null || | ||||
| 		$newService.userPassword.length <= 6 || | ||||
| 		$newService.userPassword !== $newService.userPasswordAgain; | ||||
| 	let loading = false; | ||||
| 	async function deploy() { | ||||
| 		try { | ||||
| 			loading = true; | ||||
| 			const payload = $newService; | ||||
| 			delete payload.userPasswordAgain; | ||||
| 			await request(`/api/v1/services/deploy/${$page.params.type}`, $session, { | ||||
| 				body: payload | ||||
| 			}); | ||||
| 			if (browser) { | ||||
| 				toast.push( | ||||
| 					'Service deployment queued.<br><br><br>It could take 2-5 minutes to be ready, be patient and grab a coffee/tea!', | ||||
| 					{ duration: 4000 } | ||||
| 				); | ||||
| 				goto(`/dashboard/services`, { replaceState: true }); | ||||
| 			} | ||||
| 		} catch (error) { | ||||
| 			console.log(error); | ||||
| 			browser && toast.push('Oops something went wrong. See console.log.'); | ||||
| 		} finally { | ||||
| 			loading = false; | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <div class="min-h-full text-white"> | ||||
| 	<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold"> | ||||
| 		Deploy new | ||||
| 		{#if $page.params.type === 'plausible'} | ||||
| 			<span class="text-blue-500 px-2 capitalize">Plausible Analytics</span> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| </div> | ||||
| {#if loading} | ||||
| 	<Loading /> | ||||
| {:else} | ||||
| 	<div class="space-y-2 max-w-4xl mx-auto px-6 flex-col text-center" in:fade={{ duration: 100 }}> | ||||
| 		<div class="grid grid-flow-row"> | ||||
| 			<label for="Domain" | ||||
| 				>Domain <TooltipInfo | ||||
| 					position="right" | ||||
| 					label={`You will have your Plausible instance at here.`} | ||||
| 				/></label | ||||
| 			> | ||||
| 			<input | ||||
| 				id="Domain" | ||||
| 				class:border-red-500={$newService.baseURL == null || $newService.baseURL == ''} | ||||
| 				bind:value={$newService.baseURL} | ||||
| 				placeholder="analytics.coollabs.io" | ||||
| 			/> | ||||
| 		</div> | ||||
| 		<div class="grid grid-flow-row"> | ||||
| 			<label for="Email">Email</label> | ||||
| 			<input | ||||
| 				id="Email" | ||||
| 				class:border-red-500={$newService.email == null || $newService.email == ''} | ||||
| 				bind:value={$newService.email} | ||||
| 				placeholder="hi@coollabs.io" | ||||
| 			/> | ||||
| 		</div> | ||||
| 		<div class="grid grid-flow-row"> | ||||
| 			<label for="Username">Username </label> | ||||
| 			<input | ||||
| 				id="Username" | ||||
| 				class:border-red-500={$newService.userName == null || $newService.userName == ''} | ||||
| 				bind:value={$newService.userName} | ||||
| 				placeholder="admin" | ||||
| 			/> | ||||
| 		</div> | ||||
| 		<div class="grid grid-flow-row"> | ||||
| 			<label for="Password" | ||||
| 				>Password <TooltipInfo position="right" label={`Must be at least 7 characters.`} /></label | ||||
| 			> | ||||
| 			<input | ||||
| 				id="Password" | ||||
| 				type="password" | ||||
| 				class:border-red-500={$newService.userPassword == null || | ||||
| 					$newService.userPassword == '' || | ||||
| 					$newService.userPassword.length <= 6} | ||||
| 				bind:value={$newService.userPassword} | ||||
| 			/> | ||||
| 		</div> | ||||
| 		<div class="grid grid-flow-row pb-5"> | ||||
| 			<label for="PasswordAgain">Password again </label> | ||||
| 			<input | ||||
| 				id="PasswordAgain" | ||||
| 				type="password" | ||||
| 				class:placeholder-red-500={$newService.userPassword !== $newService.userPasswordAgain} | ||||
| 				class:border-red-500={$newService.userPassword !== $newService.userPasswordAgain} | ||||
| 				bind:value={$newService.userPasswordAgain} | ||||
| 			/> | ||||
| 		</div> | ||||
| 		<button | ||||
| 			disabled={deployable} | ||||
| 			class:cursor-not-allowed={deployable} | ||||
| 			class:bg-blue-500={!deployable} | ||||
| 			class:hover:bg-blue-400={!deployable} | ||||
| 			class:hover:bg-transparent={deployable} | ||||
| 			class:text-warmGray-700={deployable} | ||||
| 			class:text-white={!deployable} | ||||
| 			class="button p-2" | ||||
| 			on:click={deploy} | ||||
| 		> | ||||
| 			Deploy | ||||
| 		</button> | ||||
| 	</div> | ||||
| {/if} | ||||
							
								
								
									
										29
									
								
								src/routes/service/new/index.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/routes/service/new/index.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| <script> | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { page } from '$app/stores'; | ||||
|  | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| </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 service | ||||
| 	</div> | ||||
| </div> | ||||
| <div class="text-center space-y-2 max-w-4xl mx-auto px-6" in:fade={{ duration: 100 }}> | ||||
| 	{#if $page.path === '/service/new'} | ||||
| 		<div class="flex justify-center space-x-4 font-bold pb-6"> | ||||
| 			<div | ||||
| 				class="text-center flex-col items-center cursor-pointer ease-in-out transform hover:scale-105 duration-100 border-2 border-dashed border-transparent hover:border-blue-500 p-2 rounded bg-warmGray-800" | ||||
| 				on:click={() => goto('/service/new/plausible')} | ||||
| 			> | ||||
| 				<img | ||||
| 					alt="plausible logo" | ||||
| 					class="w-12 mx-auto" | ||||
| 					src="https://cdn.coollabs.io/assets/coolify/services/plausible/logo_sm.png" | ||||
| 				/> | ||||
| 				<div class="text-white">Plausible Analytics</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	{/if} | ||||
| </div> | ||||
							
								
								
									
										190
									
								
								src/routes/settings.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								src/routes/settings.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,190 @@ | ||||
| <script context="module"> | ||||
| 	/** | ||||
| 	 * @type {import('@sveltejs/kit').Load} | ||||
| 	 */ | ||||
| 	export async function load({ fetch }) { | ||||
| 		try { | ||||
| 			const { allowRegistration, sendErrors } = await (await fetch(`/api/v1/settings`)).json(); | ||||
| 			return { | ||||
| 				props: { | ||||
| 					allowRegistration, | ||||
| 					sendErrors | ||||
| 				} | ||||
| 			}; | ||||
| 		} catch (error) { | ||||
| 			return { | ||||
| 				props: { | ||||
| 					allowRegistration: null, | ||||
| 					sendErrors: null | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <script> | ||||
| 	export let allowRegistration; | ||||
| 	export let sendErrors; | ||||
| 	import { browser } from '$app/env'; | ||||
| 	import { session } from '$app/stores'; | ||||
|  | ||||
| 	import { request } from '$lib/api/request'; | ||||
| 	import { toast } from '@zerodevx/svelte-toast'; | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| 	let settings = { | ||||
| 		allowRegistration, | ||||
| 		sendErrors | ||||
| 	}; | ||||
| 	async function changeSettings(value) { | ||||
| 		try { | ||||
| 			settings[value] = !settings[value]; | ||||
| 			await request(`/api/v1/settings`, $session, { | ||||
| 				body: { | ||||
| 					...settings | ||||
| 				} | ||||
| 			}); | ||||
| 			browser && toast.push('Configuration saved.'); | ||||
| 		} catch (error) { | ||||
| 			console.log(error); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <div class="min-h-full text-white" in:fade={{ duration: 100 }}> | ||||
| 	<div class="py-5 text-left px-6 text-3xl tracking-tight font-bold flex items-center"> | ||||
| 		<div>Settings</div> | ||||
| 	</div> | ||||
| </div> | ||||
|  | ||||
| <div in:fade={{ duration: 100 }}> | ||||
| 	<div class="max-w-4xl mx-auto px-6 pb-4"> | ||||
| 		<div> | ||||
| 			<div class="text-2xl font-bold border-gradient w-32 pt-4 text-white">General</div> | ||||
| 			<div class=" pt-4"> | ||||
| 				<div class="px-4 sm:px-6"> | ||||
| 					<ul class="mt-2 divide-y divide-warmGray-800"> | ||||
| 						<li class="py-4 flex items-center justify-between"> | ||||
| 							<div class="flex flex-col"> | ||||
| 								<p class="text-base font-bold text-warmGray-100">Registration allowed?</p> | ||||
| 								<p class="text-sm font-medium text-warmGray-400"> | ||||
| 									Allow further registrations to the application. It's turned off after the first | ||||
| 									registration. | ||||
| 								</p> | ||||
| 							</div> | ||||
| 							<button | ||||
| 								type="button" | ||||
| 								on:click={() => changeSettings('allowRegistration')} | ||||
| 								aria-pressed="false" | ||||
| 								class="relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200" | ||||
| 								class:bg-green-600={settings?.allowRegistration} | ||||
| 								class:bg-warmGray-700={!settings?.allowRegistration} | ||||
| 							> | ||||
| 								<span class="sr-only">Use setting</span> | ||||
| 								<span | ||||
| 									class="pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform transition ease-in-out duration-200" | ||||
| 									class:translate-x-5={settings?.allowRegistration} | ||||
| 									class:translate-x-0={!settings?.allowRegistration} | ||||
| 								> | ||||
| 									<span | ||||
| 										class=" ease-in duration-200 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity" | ||||
| 										class:opacity-0={settings?.allowRegistration} | ||||
| 										class:opacity-100={!settings?.allowRegistration} | ||||
| 										aria-hidden="true" | ||||
| 									> | ||||
| 										<svg class="bg-white h-3 w-3 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="ease-out duration-100 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity" | ||||
| 										aria-hidden="true" | ||||
| 										class:opacity-100={settings?.allowRegistration} | ||||
| 										class:opacity-0={!settings?.allowRegistration} | ||||
| 									> | ||||
| 										<svg | ||||
| 											class="bg-white h-3 w-3 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> | ||||
| 							</button> | ||||
| 						</li> | ||||
| 						<li class="py-4 flex items-center justify-between"> | ||||
| 							<div class="flex flex-col"> | ||||
| 								<p class="text-base font-bold text-warmGray-100">Send errors automatically?</p> | ||||
| 								<p class="text-sm font-medium text-warmGray-400"> | ||||
| 									Allow to send errors automatically to developer(s) at coolLabs (<a | ||||
| 										href="https://twitter.com/andrasbacsai" | ||||
| 										target="_blank" | ||||
| 										class="underline text-white font-bold hover:text-blue-400">Andras Bacsai</a | ||||
| 									>). This will help to fix bugs quicker. 🙏 | ||||
| 								</p> | ||||
| 							</div> | ||||
| 							<button | ||||
| 								type="button" | ||||
| 								on:click={() => changeSettings('sendErrors')} | ||||
| 								aria-pressed="true" | ||||
| 								class="relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200" | ||||
| 								class:bg-green-600={settings?.sendErrors} | ||||
| 								class:bg-warmGray-700={!settings?.sendErrors} | ||||
| 							> | ||||
| 								<span class="sr-only">Use setting</span> | ||||
| 								<span | ||||
| 									class="pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform transition ease-in-out duration-200" | ||||
| 									class:translate-x-5={settings?.sendErrors} | ||||
| 									class:translate-x-0={!settings?.sendErrors} | ||||
| 								> | ||||
| 									<span | ||||
| 										class=" ease-in duration-200 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity" | ||||
| 										class:opacity-0={settings?.sendErrors} | ||||
| 										class:opacity-100={!settings?.sendErrors} | ||||
| 										aria-hidden="true" | ||||
| 									> | ||||
| 										<svg class="bg-white h-3 w-3 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="ease-out duration-100 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity" | ||||
| 										aria-hidden="true" | ||||
| 										class:opacity-100={settings?.sendErrors} | ||||
| 										class:opacity-0={!settings?.sendErrors} | ||||
| 									> | ||||
| 										<svg | ||||
| 											class="bg-white h-3 w-3 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> | ||||
| 							</button> | ||||
| 						</li> | ||||
| 					</ul> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
							
								
								
									
										13
									
								
								src/routes/success.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/routes/success.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| <script> | ||||
| 	import { browser } from '$app/env'; | ||||
| 	if (browser) { | ||||
| 		setTimeout(() => { | ||||
| 			window.close(); | ||||
| 		}, 1000); | ||||
| 	} | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <div class="flex w-full h-screen justify-center items-center"> | ||||
| 	<div class="text-3xl font-bold">Succesfully logged in! 🎉</div> | ||||
| </div> | ||||
		Reference in New Issue
	
	Block a user
	 Andras Bacsai
					Andras Bacsai