2
									
								
								.github/workflows/production-release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/production-release.yml
									
									
									
									
										vendored
									
									
								
							@@ -104,7 +104,9 @@ jobs:
 | 
				
			|||||||
      - name: Create & publish manifest
 | 
					      - name: Create & publish manifest
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          docker manifest create coollabsio/coolify:${{steps.package-version.outputs.current-version}} --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64
 | 
					          docker manifest create coollabsio/coolify:${{steps.package-version.outputs.current-version}} --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64
 | 
				
			||||||
 | 
					          docker manifest create coollabsio/coolify:latest --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64
 | 
				
			||||||
          docker manifest push coollabsio/coolify:${{steps.package-version.outputs.current-version}}
 | 
					          docker manifest push coollabsio/coolify:${{steps.package-version.outputs.current-version}}
 | 
				
			||||||
 | 
					           docker manifest push coollabsio/coolify:latest
 | 
				
			||||||
      - uses: sarisia/actions-status-discord@v1
 | 
					      - uses: sarisia/actions-status-discord@v1
 | 
				
			||||||
        if: always()
 | 
					        if: always()
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -141,9 +141,12 @@ import * as buildpacks from '../lib/buildPacks';
 | 
				
			|||||||
										} catch (error) {
 | 
															} catch (error) {
 | 
				
			||||||
											//
 | 
																//
 | 
				
			||||||
										}
 | 
															}
 | 
				
			||||||
										let envs = ['NODE_ENV=production', `PORT=${port}`];
 | 
															let envs = [];
 | 
				
			||||||
										if (secrets.length > 0) {
 | 
															if (secrets.length > 0) {
 | 
				
			||||||
											envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId)];
 | 
																envs = [
 | 
				
			||||||
 | 
																	...envs,
 | 
				
			||||||
 | 
																	...generateSecrets(secrets, pullmergeRequestId, false, port)
 | 
				
			||||||
 | 
																];
 | 
				
			||||||
										}
 | 
															}
 | 
				
			||||||
										await fs.writeFile(`${workdir}/Dockerfile`, simpleDockerfile);
 | 
															await fs.writeFile(`${workdir}/Dockerfile`, simpleDockerfile);
 | 
				
			||||||
										if (dockerRegistry) {
 | 
															if (dockerRegistry) {
 | 
				
			||||||
@@ -676,9 +679,12 @@ import * as buildpacks from '../lib/buildPacks';
 | 
				
			|||||||
										} catch (error) {
 | 
															} catch (error) {
 | 
				
			||||||
											//
 | 
																//
 | 
				
			||||||
										}
 | 
															}
 | 
				
			||||||
										let envs = ['NODE_ENV=production', `PORT=${port}`];
 | 
															let envs = [];
 | 
				
			||||||
										if (secrets.length > 0) {
 | 
															if (secrets.length > 0) {
 | 
				
			||||||
											envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId)];
 | 
																envs = [
 | 
				
			||||||
 | 
																	...envs,
 | 
				
			||||||
 | 
																	...generateSecrets(secrets, pullmergeRequestId, false, port)
 | 
				
			||||||
 | 
																];
 | 
				
			||||||
										}
 | 
															}
 | 
				
			||||||
										if (dockerRegistry) {
 | 
															if (dockerRegistry) {
 | 
				
			||||||
											const { url, username, password } = dockerRegistry;
 | 
																const { url, username, password } = dockerRegistry;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,9 +25,9 @@ export default async function (data) {
 | 
				
			|||||||
	if (!dockerComposeYaml.services) {
 | 
						if (!dockerComposeYaml.services) {
 | 
				
			||||||
		throw 'No Services found in docker-compose file.';
 | 
							throw 'No Services found in docker-compose file.';
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	let envs = ['NODE_ENV=production'];
 | 
						let envs = [];
 | 
				
			||||||
	if (secrets.length > 0) {
 | 
						if (secrets.length > 0) {
 | 
				
			||||||
		envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId)];
 | 
							envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, null)];
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const composeVolumes = [];
 | 
						const composeVolumes = [];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,7 +19,7 @@ import { saveBuildLog, saveDockerRegistryCredentials } from './buildPacks/common
 | 
				
			|||||||
import { scheduler } from './scheduler';
 | 
					import { scheduler } from './scheduler';
 | 
				
			||||||
import type { ExecaChildProcess } from 'execa';
 | 
					import type { ExecaChildProcess } from 'execa';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const version = '3.12.3';
 | 
					export const version = '3.12.4';
 | 
				
			||||||
export const isDev = process.env.NODE_ENV === 'development';
 | 
					export const isDev = process.env.NODE_ENV === 'development';
 | 
				
			||||||
export const sentryDSN =
 | 
					export const sentryDSN =
 | 
				
			||||||
	'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216';
 | 
						'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216';
 | 
				
			||||||
@@ -1879,7 +1879,8 @@ export async function pushToRegistry(
 | 
				
			|||||||
export function generateSecrets(
 | 
					export function generateSecrets(
 | 
				
			||||||
	secrets: Array<any>,
 | 
						secrets: Array<any>,
 | 
				
			||||||
	pullmergeRequestId: string,
 | 
						pullmergeRequestId: string,
 | 
				
			||||||
	isBuild = false
 | 
						isBuild = false,
 | 
				
			||||||
 | 
						port = null
 | 
				
			||||||
): Array<string> {
 | 
					): Array<string> {
 | 
				
			||||||
	const envs = [];
 | 
						const envs = [];
 | 
				
			||||||
	const isPRMRSecret = secrets.filter((s) => s.isPRMRSecret);
 | 
						const isPRMRSecret = secrets.filter((s) => s.isPRMRSecret);
 | 
				
			||||||
@@ -1918,5 +1919,13 @@ export function generateSecrets(
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						const portFound = envs.filter((env) => env.startsWith('PORT'));
 | 
				
			||||||
 | 
						if (portFound.length === 0 && port && !isBuild) {
 | 
				
			||||||
 | 
							envs.push(`PORT=${port}`);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const nodeEnv = envs.filter((env) => env.startsWith('NODE_ENV'));
 | 
				
			||||||
 | 
						if (nodeEnv.length === 0 && !isBuild) {
 | 
				
			||||||
 | 
							envs.push(`NODE_ENV=production`);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return envs;
 | 
						return envs;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -569,10 +569,12 @@ export async function restartApplication(
 | 
				
			|||||||
			} = application;
 | 
								} = application;
 | 
				
			||||||
			let location = null;
 | 
								let location = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			let envs = ['NODE_ENV=production', `PORT=${port}`];
 | 
								let envs = [];
 | 
				
			||||||
			if (secrets.length > 0) {
 | 
								if (secrets.length > 0) {
 | 
				
			||||||
				envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId)];
 | 
									envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, port)];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
								console.log(envs);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const { workdir } = await createDirectories({ repository, buildId });
 | 
								const { workdir } = await createDirectories({ repository, buildId });
 | 
				
			||||||
			const labels = [];
 | 
								const labels = [];
 | 
				
			||||||
			let image = null;
 | 
								let image = null;
 | 
				
			||||||
@@ -659,6 +661,7 @@ export async function restartApplication(
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
				volumes: Object.assign({}, ...composeVolumes)
 | 
									volumes: Object.assign({}, ...composeVolumes)
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
 | 
								console.log(yaml.dump(composeFile));
 | 
				
			||||||
			await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
 | 
								await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
 | 
				
			||||||
			try {
 | 
								try {
 | 
				
			||||||
				await executeCommand({ dockerId, command: `docker stop -t 0 ${id}` });
 | 
									await executeCommand({ dockerId, command: `docker stop -t 0 ${id}` });
 | 
				
			||||||
@@ -1370,9 +1373,9 @@ export async function restartPreview(
 | 
				
			|||||||
				exposePort
 | 
									exposePort
 | 
				
			||||||
			} = application;
 | 
								} = application;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			let envs = ['NODE_ENV=production', `PORT=${port}`];
 | 
								let envs = [];
 | 
				
			||||||
			if (secrets.length > 0) {
 | 
								if (secrets.length > 0) {
 | 
				
			||||||
				envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId)];
 | 
									envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, port)];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			const { workdir } = await createDirectories({ repository, buildId });
 | 
								const { workdir } = await createDirectories({ repository, buildId });
 | 
				
			||||||
			const labels = [];
 | 
								const labels = [];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,7 +44,10 @@
 | 
				
			|||||||
		"daisyui": "2.41.0",
 | 
							"daisyui": "2.41.0",
 | 
				
			||||||
		"flowbite-svelte": "0.28.0",
 | 
							"flowbite-svelte": "0.28.0",
 | 
				
			||||||
		"js-cookie": "3.0.1",
 | 
							"js-cookie": "3.0.1",
 | 
				
			||||||
 | 
							"js-yaml": "4.1.0",
 | 
				
			||||||
 | 
							"p-limit": "4.0.0",
 | 
				
			||||||
		"server": "workspace:*",
 | 
							"server": "workspace:*",
 | 
				
			||||||
		"superjson": "1.11.0"
 | 
							"superjson": "1.11.0",
 | 
				
			||||||
 | 
							"svelte-select": "4.4.7"
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										3
									
								
								apps/client/src/app.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								apps/client/src/app.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -7,3 +7,6 @@ declare namespace App {
 | 
				
			|||||||
	// interface Error {}
 | 
						// interface Error {}
 | 
				
			||||||
	// interface Platform {}
 | 
						// interface Platform {}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare const GITPOD_WORKSPACE_URL: string;
 | 
				
			||||||
 | 
					declare const CODESANDBOX_HOST: string;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,17 @@
 | 
				
			|||||||
 | 
					import { dev } from '$app/environment';
 | 
				
			||||||
import { addToast } from './store';
 | 
					import { addToast } from './store';
 | 
				
			||||||
 | 
					import Cookies from 'js-cookie';
 | 
				
			||||||
export const asyncSleep = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay));
 | 
					export const asyncSleep = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function errorNotification(error: any | { message: string }): void {
 | 
					export function errorNotification(error: any | { message: string }): void {
 | 
				
			||||||
	if (error instanceof Error) {
 | 
						if (error instanceof Error) {
 | 
				
			||||||
 | 
							console.error(error.message)
 | 
				
			||||||
		addToast({
 | 
							addToast({
 | 
				
			||||||
			message: error.message,
 | 
								message: error.message,
 | 
				
			||||||
			type: 'error'
 | 
								type: 'error'
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
 | 
							console.error(error)
 | 
				
			||||||
		addToast({
 | 
							addToast({
 | 
				
			||||||
			message: error,
 | 
								message: error,
 | 
				
			||||||
			type: 'error'
 | 
								type: 'error'
 | 
				
			||||||
@@ -18,3 +21,165 @@ export function errorNotification(error: any | { message: string }): void {
 | 
				
			|||||||
export function getRndInteger(min: number, max: number) {
 | 
					export function getRndInteger(min: number, max: number) {
 | 
				
			||||||
	return Math.floor(Math.random() * (max - min + 1)) + min;
 | 
						return Math.floor(Math.random() * (max - min + 1)) + min;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getDomain(domain: string) {
 | 
				
			||||||
 | 
						return domain?.replace('https://', '').replace('http://', '');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const notNodeDeployments = ['php', 'docker', 'rust', 'python', 'deno', 'laravel', 'heroku'];
 | 
				
			||||||
 | 
					export const staticDeployments = [
 | 
				
			||||||
 | 
						'react',
 | 
				
			||||||
 | 
						'vuejs',
 | 
				
			||||||
 | 
						'static',
 | 
				
			||||||
 | 
						'svelte',
 | 
				
			||||||
 | 
						'gatsby',
 | 
				
			||||||
 | 
						'php',
 | 
				
			||||||
 | 
						'astro',
 | 
				
			||||||
 | 
						'eleventy'
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getAPIUrl() {
 | 
				
			||||||
 | 
						if (GITPOD_WORKSPACE_URL) {
 | 
				
			||||||
 | 
							const { href } = new URL(GITPOD_WORKSPACE_URL);
 | 
				
			||||||
 | 
							const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '');
 | 
				
			||||||
 | 
							return newURL;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (CODESANDBOX_HOST) {
 | 
				
			||||||
 | 
							return `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return dev ? `http://${window.location.hostname}:3001` : 'http://localhost:3000';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export function getWebhookUrl(type: string) {
 | 
				
			||||||
 | 
						if (GITPOD_WORKSPACE_URL) {
 | 
				
			||||||
 | 
							const { href } = new URL(GITPOD_WORKSPACE_URL);
 | 
				
			||||||
 | 
							const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '');
 | 
				
			||||||
 | 
							if (type === 'github') {
 | 
				
			||||||
 | 
								return `${newURL}/webhooks/github/events`;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (type === 'gitlab') {
 | 
				
			||||||
 | 
								return `${newURL}/webhooks/gitlab/events`;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (CODESANDBOX_HOST) {
 | 
				
			||||||
 | 
							const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
 | 
				
			||||||
 | 
							if (type === 'github') {
 | 
				
			||||||
 | 
								return `${newURL}/webhooks/github/events`;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (type === 'gitlab') {
 | 
				
			||||||
 | 
								return `${newURL}/webhooks/gitlab/events`;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return `https://webhook.site/0e5beb2c-4e9b-40e2-a89e-32295e570c21/events`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function send({
 | 
				
			||||||
 | 
						method,
 | 
				
			||||||
 | 
						path,
 | 
				
			||||||
 | 
						data = null,
 | 
				
			||||||
 | 
						headers,
 | 
				
			||||||
 | 
						timeout = 120000
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
						method: string;
 | 
				
			||||||
 | 
						path: string;
 | 
				
			||||||
 | 
						data?: any;
 | 
				
			||||||
 | 
						headers?: any;
 | 
				
			||||||
 | 
						timeout?: number;
 | 
				
			||||||
 | 
					}): Promise<Record<string, unknown>> {
 | 
				
			||||||
 | 
						const token = Cookies.get('token');
 | 
				
			||||||
 | 
						const controller = new AbortController();
 | 
				
			||||||
 | 
						const id = setTimeout(() => controller.abort(), timeout);
 | 
				
			||||||
 | 
						const opts: any = { method, headers: {}, body: null, signal: controller.signal };
 | 
				
			||||||
 | 
						if (data && Object.keys(data).length > 0) {
 | 
				
			||||||
 | 
							const parsedData = data;
 | 
				
			||||||
 | 
							for (const [key, value] of Object.entries(data)) {
 | 
				
			||||||
 | 
								if (value === '') {
 | 
				
			||||||
 | 
									parsedData[key] = null;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (parsedData) {
 | 
				
			||||||
 | 
								opts.headers['Content-Type'] = 'application/json';
 | 
				
			||||||
 | 
								opts.body = JSON.stringify(parsedData);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (headers) {
 | 
				
			||||||
 | 
							opts.headers = {
 | 
				
			||||||
 | 
								...opts.headers,
 | 
				
			||||||
 | 
								...headers
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (token && !path.startsWith('https://')) {
 | 
				
			||||||
 | 
							opts.headers = {
 | 
				
			||||||
 | 
								...opts.headers,
 | 
				
			||||||
 | 
								Authorization: `Bearer ${token}`
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (!path.startsWith('https://')) {
 | 
				
			||||||
 | 
							path = `/api/v1${path}`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (dev && !path.startsWith('https://')) {
 | 
				
			||||||
 | 
							path = `${getAPIUrl()}${path}`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (method === 'POST' && data && !opts.body) {
 | 
				
			||||||
 | 
							opts.body = data;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const response = await fetch(`${path}`, opts);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						clearTimeout(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const contentType = response.headers.get('content-type');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let responseData = {};
 | 
				
			||||||
 | 
						if (contentType) {
 | 
				
			||||||
 | 
							if (contentType?.indexOf('application/json') !== -1) {
 | 
				
			||||||
 | 
								responseData = await response.json();
 | 
				
			||||||
 | 
							} else if (contentType?.indexOf('text/plain') !== -1) {
 | 
				
			||||||
 | 
								responseData = await response.text();
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return {};
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return {};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (!response.ok) {
 | 
				
			||||||
 | 
							if (
 | 
				
			||||||
 | 
								response.status === 401 &&
 | 
				
			||||||
 | 
								!path.startsWith('https://api.github') &&
 | 
				
			||||||
 | 
								!path.includes('/v4/')
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								Cookies.remove('token');
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							throw responseData;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return responseData;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function get(path: string, headers?: Record<string, unknown>): Promise<Record<string, any>> {
 | 
				
			||||||
 | 
						return send({ method: 'GET', path, headers });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function del(
 | 
				
			||||||
 | 
						path: string,
 | 
				
			||||||
 | 
						data: Record<string, unknown>,
 | 
				
			||||||
 | 
						headers?: Record<string, unknown>
 | 
				
			||||||
 | 
					): Promise<Record<string, any>> {
 | 
				
			||||||
 | 
						return send({ method: 'DELETE', path, data, headers });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function post(
 | 
				
			||||||
 | 
						path: string,
 | 
				
			||||||
 | 
						data: Record<string, unknown> | FormData,
 | 
				
			||||||
 | 
						headers?: Record<string, unknown>
 | 
				
			||||||
 | 
					): Promise<Record<string, any>> {
 | 
				
			||||||
 | 
						return send({ method: 'POST', path, data, headers });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function put(
 | 
				
			||||||
 | 
						path: string,
 | 
				
			||||||
 | 
						data: Record<string, unknown>,
 | 
				
			||||||
 | 
						headers?: Record<string, unknown>
 | 
				
			||||||
 | 
					): Promise<Record<string, any>> {
 | 
				
			||||||
 | 
						return send({ method: 'PUT', path, data, headers });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								apps/client/src/lib/components/Beta.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/client/src/lib/components/Beta.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<span class="badge bg-coollabs-gradient rounded text-white font-normal"> BETA </span>
 | 
				
			||||||
							
								
								
									
										156
									
								
								apps/client/src/lib/components/CopyPasswordField.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								apps/client/src/lib/components/CopyPasswordField.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,156 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { browser } from '$app/environment';
 | 
				
			||||||
 | 
						import { addToast } from '$lib/store';
 | 
				
			||||||
 | 
						let showPassword = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let value: string;
 | 
				
			||||||
 | 
						export let disabled = false;
 | 
				
			||||||
 | 
						export let isPasswordField = false;
 | 
				
			||||||
 | 
						export let readonly = false;
 | 
				
			||||||
 | 
						export let textarea = false;
 | 
				
			||||||
 | 
						export let required = false;
 | 
				
			||||||
 | 
						export let pattern: string | null | undefined = null;
 | 
				
			||||||
 | 
						export let id: string;
 | 
				
			||||||
 | 
						export let name: string;
 | 
				
			||||||
 | 
						export let placeholder = '';
 | 
				
			||||||
 | 
						export let inputStyle = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let disabledClass = 'input input-primary bg-coolback disabled:bg-coolblack w-full';
 | 
				
			||||||
 | 
						let isHttps = browser && window.location.protocol === 'https:';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						function copyToClipboard() {
 | 
				
			||||||
 | 
							if (isHttps && navigator.clipboard) {
 | 
				
			||||||
 | 
								navigator.clipboard.writeText(value);
 | 
				
			||||||
 | 
								addToast({
 | 
				
			||||||
 | 
									message: 'Copied to clipboard.',
 | 
				
			||||||
 | 
									type: 'success'
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="relative">
 | 
				
			||||||
 | 
						{#if !isPasswordField || showPassword}
 | 
				
			||||||
 | 
							{#if textarea}
 | 
				
			||||||
 | 
								<textarea
 | 
				
			||||||
 | 
									style={inputStyle}
 | 
				
			||||||
 | 
									rows="5"
 | 
				
			||||||
 | 
									class={disabledClass}
 | 
				
			||||||
 | 
									class:pr-10={true}
 | 
				
			||||||
 | 
									class:pr-20={value && isHttps}
 | 
				
			||||||
 | 
									class:border={required && !value}
 | 
				
			||||||
 | 
									class:border-red-500={required && !value}
 | 
				
			||||||
 | 
									{placeholder}
 | 
				
			||||||
 | 
									type="text"
 | 
				
			||||||
 | 
									{id}
 | 
				
			||||||
 | 
									{pattern}
 | 
				
			||||||
 | 
									{required}
 | 
				
			||||||
 | 
									{readonly}
 | 
				
			||||||
 | 
									{disabled}
 | 
				
			||||||
 | 
									{name}>{value}</textarea
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
 | 
							{:else}
 | 
				
			||||||
 | 
								<input
 | 
				
			||||||
 | 
									style={inputStyle}
 | 
				
			||||||
 | 
									class={disabledClass}
 | 
				
			||||||
 | 
									type="text"
 | 
				
			||||||
 | 
									class:pr-10={true}
 | 
				
			||||||
 | 
									class:pr-20={value && isHttps}
 | 
				
			||||||
 | 
									class:border={required && !value}
 | 
				
			||||||
 | 
									class:border-red-500={required && !value}
 | 
				
			||||||
 | 
									{id}
 | 
				
			||||||
 | 
									{name}
 | 
				
			||||||
 | 
									{required}
 | 
				
			||||||
 | 
									{pattern}
 | 
				
			||||||
 | 
									{readonly}
 | 
				
			||||||
 | 
									bind:value
 | 
				
			||||||
 | 
									{disabled}
 | 
				
			||||||
 | 
									{placeholder}
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
						{:else}
 | 
				
			||||||
 | 
							<input
 | 
				
			||||||
 | 
								style={inputStyle}
 | 
				
			||||||
 | 
								class={disabledClass}
 | 
				
			||||||
 | 
								class:pr-10={true}
 | 
				
			||||||
 | 
								class:pr-20={value && isHttps}
 | 
				
			||||||
 | 
								class:border={required && !value}
 | 
				
			||||||
 | 
								class:border-red-500={required && !value}
 | 
				
			||||||
 | 
								type="password"
 | 
				
			||||||
 | 
								{id}
 | 
				
			||||||
 | 
								{name}
 | 
				
			||||||
 | 
								{readonly}
 | 
				
			||||||
 | 
								{pattern}
 | 
				
			||||||
 | 
								{required}
 | 
				
			||||||
 | 
								bind:value
 | 
				
			||||||
 | 
								{disabled}
 | 
				
			||||||
 | 
								{placeholder}
 | 
				
			||||||
 | 
							/>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<div class="absolute top-0 right-0 flex justify-center items-center h-full cursor-pointer text-stone-600 hover:text-white mr-3">
 | 
				
			||||||
 | 
							<div class="flex space-x-2">
 | 
				
			||||||
 | 
								{#if isPasswordField}
 | 
				
			||||||
 | 
									<!-- svelte-ignore a11y-click-events-have-key-events -->
 | 
				
			||||||
 | 
									<div on:click={() => (showPassword = !showPassword)}>
 | 
				
			||||||
 | 
										{#if showPassword}
 | 
				
			||||||
 | 
											<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="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
 | 
				
			||||||
 | 
												/>
 | 
				
			||||||
 | 
											</svg>
 | 
				
			||||||
 | 
										{:else}
 | 
				
			||||||
 | 
											<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
 | 
				
			||||||
 | 
												/>
 | 
				
			||||||
 | 
												<path
 | 
				
			||||||
 | 
													stroke-linecap="round"
 | 
				
			||||||
 | 
													stroke-linejoin="round"
 | 
				
			||||||
 | 
													stroke-width="2"
 | 
				
			||||||
 | 
													d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
 | 
				
			||||||
 | 
												/>
 | 
				
			||||||
 | 
											</svg>
 | 
				
			||||||
 | 
										{/if}
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								{/if}
 | 
				
			||||||
 | 
								{#if value && isHttps}
 | 
				
			||||||
 | 
									<!-- svelte-ignore a11y-click-events-have-key-events -->
 | 
				
			||||||
 | 
									<div on:click={copyToClipboard}>
 | 
				
			||||||
 | 
										<svg
 | 
				
			||||||
 | 
											xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
											class="h-6 w-6"
 | 
				
			||||||
 | 
											viewBox="0 0 24 24"
 | 
				
			||||||
 | 
											stroke-width="1.5"
 | 
				
			||||||
 | 
											stroke="currentColor"
 | 
				
			||||||
 | 
											fill="none"
 | 
				
			||||||
 | 
											stroke-linecap="round"
 | 
				
			||||||
 | 
											stroke-linejoin="round"
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											<path stroke="none" d="M0 0h24v24H0z" fill="none" />
 | 
				
			||||||
 | 
											<rect x="8" y="8" width="12" height="12" rx="2" />
 | 
				
			||||||
 | 
											<path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" />
 | 
				
			||||||
 | 
										</svg>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								{/if}
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
							
								
								
									
										38
									
								
								apps/client/src/lib/components/Explainer.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								apps/client/src/lib/components/Explainer.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						// import { onMount } from 'svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// import Tooltip from './Tooltip.svelte';
 | 
				
			||||||
 | 
						export let explanation = '';
 | 
				
			||||||
 | 
						export let position = 'dropdown-right';
 | 
				
			||||||
 | 
						// let id: any;
 | 
				
			||||||
 | 
						// let self: any;
 | 
				
			||||||
 | 
						// onMount(() => {
 | 
				
			||||||
 | 
						// 	id = `info-${self.offsetLeft}-${self.offsetTop}`;
 | 
				
			||||||
 | 
						// });
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class={`dropdown dropdown-end ${position}`}>
 | 
				
			||||||
 | 
						<!-- svelte-ignore a11y-label-has-associated-control -->
 | 
				
			||||||
 | 
						<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
 | 
				
			||||||
 | 
						<label tabindex="0" class="btn btn-circle btn-ghost btn-xs text-sky-500">
 | 
				
			||||||
 | 
							<svg
 | 
				
			||||||
 | 
								xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
								fill="none"
 | 
				
			||||||
 | 
								viewBox="0 0 24 24"
 | 
				
			||||||
 | 
								class="w-4 h-4 stroke-current"
 | 
				
			||||||
 | 
								><path
 | 
				
			||||||
 | 
									stroke-linecap="round"
 | 
				
			||||||
 | 
									stroke-linejoin="round"
 | 
				
			||||||
 | 
									stroke-width="2"
 | 
				
			||||||
 | 
									d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
 | 
				
			||||||
 | 
								/></svg
 | 
				
			||||||
 | 
							>
 | 
				
			||||||
 | 
						</label>
 | 
				
			||||||
 | 
						<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
 | 
				
			||||||
 | 
						<div tabindex="0" class="card compact dropdown-content shadow bg-coolgray-400 rounded w-64">
 | 
				
			||||||
 | 
							<div class="card-body">
 | 
				
			||||||
 | 
								<!-- <h2 class="card-title">You needed more info?</h2>  -->
 | 
				
			||||||
 | 
								<p class="text-xs font-normal">{@html explanation}</p>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
							
								
								
									
										87
									
								
								apps/client/src/lib/components/Setting.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								apps/client/src/lib/components/Setting.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import Beta from './Beta.svelte';
 | 
				
			||||||
 | 
						import Explaner from './Explainer.svelte';
 | 
				
			||||||
 | 
						import Tooltip from './Tooltip.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let id: any;
 | 
				
			||||||
 | 
						export let customClass: any = null;
 | 
				
			||||||
 | 
						export let setting: any;
 | 
				
			||||||
 | 
						export let title: any;
 | 
				
			||||||
 | 
						export let isBeta: any = false;
 | 
				
			||||||
 | 
						export let description: any = null;
 | 
				
			||||||
 | 
						export let isCenter = true;
 | 
				
			||||||
 | 
						export let disabled = false;
 | 
				
			||||||
 | 
						export let dataTooltip: any = null;
 | 
				
			||||||
 | 
						export let loading = false;
 | 
				
			||||||
 | 
						let triggeredBy = `#${id}`;
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="flex items-center py-4 pr-8">
 | 
				
			||||||
 | 
						<div class="flex w-96 flex-col">
 | 
				
			||||||
 | 
							<!-- svelte-ignore a11y-label-has-associated-control -->
 | 
				
			||||||
 | 
							<label>
 | 
				
			||||||
 | 
								{title}
 | 
				
			||||||
 | 
								{#if isBeta}
 | 
				
			||||||
 | 
									<Beta />
 | 
				
			||||||
 | 
								{/if}
 | 
				
			||||||
 | 
								{#if description && description !== ''}
 | 
				
			||||||
 | 
									<Explaner explanation={description} />
 | 
				
			||||||
 | 
								{/if}
 | 
				
			||||||
 | 
							</label>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<div class:text-center={isCenter} class={`flex justify-center ${customClass}`}>
 | 
				
			||||||
 | 
						<!-- svelte-ignore a11y-click-events-have-key-events -->
 | 
				
			||||||
 | 
						<div
 | 
				
			||||||
 | 
							on:click
 | 
				
			||||||
 | 
							aria-pressed="false"
 | 
				
			||||||
 | 
							class="relative mx-20 inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out"
 | 
				
			||||||
 | 
							class:opacity-50={disabled || loading}
 | 
				
			||||||
 | 
							class:bg-green-600={!loading && setting}
 | 
				
			||||||
 | 
							class:bg-stone-700={!loading && !setting}
 | 
				
			||||||
 | 
							class:bg-yellow-500={loading}
 | 
				
			||||||
 | 
							{id}
 | 
				
			||||||
 | 
						>
 | 
				
			||||||
 | 
							<span class="sr-only">Use setting</span>
 | 
				
			||||||
 | 
							<span
 | 
				
			||||||
 | 
								class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
 | 
				
			||||||
 | 
								class:translate-x-5={setting}
 | 
				
			||||||
 | 
								class:translate-x-0={!setting}
 | 
				
			||||||
 | 
							>
 | 
				
			||||||
 | 
								<span
 | 
				
			||||||
 | 
									class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
 | 
				
			||||||
 | 
									class:opacity-0={setting}
 | 
				
			||||||
 | 
									class:opacity-100={!setting}
 | 
				
			||||||
 | 
									class:animate-spin={loading}
 | 
				
			||||||
 | 
									aria-hidden="true"
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
 | 
									<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
 | 
				
			||||||
 | 
										<path
 | 
				
			||||||
 | 
											d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
 | 
				
			||||||
 | 
											stroke="currentColor"
 | 
				
			||||||
 | 
											stroke-width="2"
 | 
				
			||||||
 | 
											stroke-linecap="round"
 | 
				
			||||||
 | 
											stroke-linejoin="round"
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									</svg>
 | 
				
			||||||
 | 
								</span>
 | 
				
			||||||
 | 
								<span
 | 
				
			||||||
 | 
									class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
 | 
				
			||||||
 | 
									aria-hidden="true"
 | 
				
			||||||
 | 
									class:opacity-100={setting}
 | 
				
			||||||
 | 
									class:opacity-0={!setting}
 | 
				
			||||||
 | 
									class:animate-spin={loading}
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
 | 
									<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
 | 
				
			||||||
 | 
										<path
 | 
				
			||||||
 | 
											d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									</svg>
 | 
				
			||||||
 | 
								</span>
 | 
				
			||||||
 | 
							</span>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{#if dataTooltip}
 | 
				
			||||||
 | 
						<Tooltip {triggeredBy} placement="top">{dataTooltip}</Tooltip>
 | 
				
			||||||
 | 
					{/if}
 | 
				
			||||||
@@ -21,7 +21,8 @@ export const trpc = createTRPCProxyClient<AppRouter>({
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
	]
 | 
						]
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					export const disabledButton: Writable<boolean> = writable(false);
 | 
				
			||||||
 | 
					export const location: Writable<null | string> = writable(null)
 | 
				
			||||||
interface AppSession {
 | 
					interface AppSession {
 | 
				
			||||||
	isRegistrationEnabled: boolean;
 | 
						isRegistrationEnabled: boolean;
 | 
				
			||||||
	token?: string;
 | 
						token?: string;
 | 
				
			||||||
@@ -139,3 +140,33 @@ export const status: Writable<any> = writable({
 | 
				
			|||||||
		isPublic: false
 | 
							isPublic: false
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) {
 | 
				
			||||||
 | 
						return !!(
 | 
				
			||||||
 | 
							(isAdmin && application.buildPack === 'compose') ||
 | 
				
			||||||
 | 
							((application.fqdn || application.settings.isBot) &&
 | 
				
			||||||
 | 
								((application.gitSource && application.repository && application.buildPack) ||
 | 
				
			||||||
 | 
									application.simpleDockerfile) &&
 | 
				
			||||||
 | 
								application.destinationDocker)
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export const setLocation = (resource: any, settings?: any) => {
 | 
				
			||||||
 | 
						if (resource.settings.isBot && resource.exposePort) {
 | 
				
			||||||
 | 
							disabledButton.set(false);
 | 
				
			||||||
 | 
							return location.set(`http://${dev ? 'localhost' : settings.ipv4}:${resource.exposePort}`);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (GITPOD_WORKSPACE_URL && resource.exposePort) {
 | 
				
			||||||
 | 
							const { href } = new URL(GITPOD_WORKSPACE_URL);
 | 
				
			||||||
 | 
							const newURL = href.replace('https://', `https://${resource.exposePort}-`).replace(/\/$/, '');
 | 
				
			||||||
 | 
							return location.set(newURL);
 | 
				
			||||||
 | 
						} else if (CODESANDBOX_HOST) {
 | 
				
			||||||
 | 
							const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/, resource.exposePort)}`;
 | 
				
			||||||
 | 
							return location.set(newURL);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (resource.fqdn) {
 | 
				
			||||||
 | 
							return location.set(resource.fqdn);
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							location.set(null);
 | 
				
			||||||
 | 
							disabledButton.set(false);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,8 @@
 | 
				
			|||||||
import { error } from '@sveltejs/kit';
 | 
					import { error } from '@sveltejs/kit';
 | 
				
			||||||
import { trpc } from '$lib/store';
 | 
					import { trpc } from '$lib/store';
 | 
				
			||||||
import type { LayoutLoad } from './$types';
 | 
					 | 
				
			||||||
import { redirect } from '@sveltejs/kit';
 | 
					 | 
				
			||||||
import Cookies from 'js-cookie';
 | 
					 | 
				
			||||||
export const ssr = false;
 | 
					export const ssr = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const load: LayoutLoad = async ({ url }) => {
 | 
					export const load = async () => {
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
		return await trpc.dashboard.resources.query();
 | 
							return await trpc.dashboard.resources.query();
 | 
				
			||||||
	} catch (err) {
 | 
						} catch (err) {
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										118
									
								
								apps/client/src/routes/applications/[id]/features/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								apps/client/src/routes/applications/[id]/features/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,118 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import type { PageParentData } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let data: PageParentData;
 | 
				
			||||||
 | 
						const application = data.application.data;
 | 
				
			||||||
 | 
						const settings = data.settings.data;
 | 
				
			||||||
 | 
						import { page } from '$app/stores';
 | 
				
			||||||
 | 
						const { id } = $page.params;
 | 
				
			||||||
 | 
						import {
 | 
				
			||||||
 | 
							addToast,
 | 
				
			||||||
 | 
							appSession,
 | 
				
			||||||
 | 
							checkIfDeploymentEnabledApplications,
 | 
				
			||||||
 | 
							setLocation,
 | 
				
			||||||
 | 
							status,
 | 
				
			||||||
 | 
							isDeploymentEnabled,
 | 
				
			||||||
 | 
							trpc
 | 
				
			||||||
 | 
						} from '$lib/store';
 | 
				
			||||||
 | 
						import { errorNotification } from '$lib/common';
 | 
				
			||||||
 | 
						import Setting from '$lib/components/Setting.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let previews = application.settings.previews;
 | 
				
			||||||
 | 
						let dualCerts = application.settings.dualCerts;
 | 
				
			||||||
 | 
						let autodeploy = application.settings.autodeploy;
 | 
				
			||||||
 | 
						let isBot = application.settings.isBot;
 | 
				
			||||||
 | 
						let isDBBranching = application.settings.isDBBranching;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function changeSettings(name: any) {
 | 
				
			||||||
 | 
							if (name === 'previews') {
 | 
				
			||||||
 | 
								previews = !previews;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (name === 'dualCerts') {
 | 
				
			||||||
 | 
								dualCerts = !dualCerts;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (name === 'autodeploy') {
 | 
				
			||||||
 | 
								autodeploy = !autodeploy;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (name === 'isBot') {
 | 
				
			||||||
 | 
								if ($status.application.isRunning) return;
 | 
				
			||||||
 | 
								isBot = !isBot;
 | 
				
			||||||
 | 
								application.settings.isBot = isBot;
 | 
				
			||||||
 | 
								application.fqdn = null;
 | 
				
			||||||
 | 
								setLocation(application, settings);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (name === 'isDBBranching') {
 | 
				
			||||||
 | 
								isDBBranching = !isDBBranching;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								await trpc.applications.saveSettings.mutate({
 | 
				
			||||||
 | 
									id,
 | 
				
			||||||
 | 
									previews,
 | 
				
			||||||
 | 
									dualCerts,
 | 
				
			||||||
 | 
									isBot,
 | 
				
			||||||
 | 
									autodeploy,
 | 
				
			||||||
 | 
									isDBBranching
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return addToast({
 | 
				
			||||||
 | 
									message: 'Settings saved',
 | 
				
			||||||
 | 
									type: 'success'
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
 | 
								if (name === 'previews') {
 | 
				
			||||||
 | 
									previews = !previews;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (name === 'dualCerts') {
 | 
				
			||||||
 | 
									dualCerts = !dualCerts;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (name === 'autodeploy') {
 | 
				
			||||||
 | 
									autodeploy = !autodeploy;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (name === 'isBot') {
 | 
				
			||||||
 | 
									isBot = !isBot;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (name === 'isDBBranching') {
 | 
				
			||||||
 | 
									isDBBranching = !isDBBranching;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return errorNotification(error);
 | 
				
			||||||
 | 
							} finally {
 | 
				
			||||||
 | 
								$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="w-full">
 | 
				
			||||||
 | 
						<div class="mx-auto w-full">
 | 
				
			||||||
 | 
							<div class="flex flex-row border-b border-coolgray-500 mb-6  space-x-2">
 | 
				
			||||||
 | 
								<div class="title font-bold pb-3">Features</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div class="px-4 lg:pb-10 pb-6">
 | 
				
			||||||
 | 
								{#if !application.settings.isPublicRepository}
 | 
				
			||||||
 | 
									<div class="grid grid-cols-2 items-center">
 | 
				
			||||||
 | 
										<Setting
 | 
				
			||||||
 | 
											id="autodeploy"
 | 
				
			||||||
 | 
											isCenter={false}
 | 
				
			||||||
 | 
											bind:setting={autodeploy}
 | 
				
			||||||
 | 
											on:click={() => changeSettings('autodeploy')}
 | 
				
			||||||
 | 
											title="Enable Automatic Deployment"
 | 
				
			||||||
 | 
											description="Enable automatic deployment through webhooks."
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
									{#if !application.settings.isBot && !application.simpleDockerfile}
 | 
				
			||||||
 | 
										<div class="grid grid-cols-2 items-center">
 | 
				
			||||||
 | 
											<Setting
 | 
				
			||||||
 | 
												id="previews"
 | 
				
			||||||
 | 
												isCenter={false}
 | 
				
			||||||
 | 
												bind:setting={previews}
 | 
				
			||||||
 | 
												on:click={() => changeSettings('previews')}
 | 
				
			||||||
 | 
												title="Enable MR/PR Previews"
 | 
				
			||||||
 | 
												description="Enable preview deployments from pull or merge requests."
 | 
				
			||||||
 | 
											/>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									{/if}
 | 
				
			||||||
 | 
								{:else}
 | 
				
			||||||
 | 
									No features available for this application
 | 
				
			||||||
 | 
								{/if}
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
							
								
								
									
										138
									
								
								apps/client/src/routes/applications/[id]/secrets/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								apps/client/src/routes/applications/[id]/secrets/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,138 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import type { PageData } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let data: PageData;
 | 
				
			||||||
 | 
						let secrets = data.secrets;
 | 
				
			||||||
 | 
						let previewSecrets = data.previewSecrets;
 | 
				
			||||||
 | 
						const application = data.application.data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						import pLimit from 'p-limit';
 | 
				
			||||||
 | 
						import { page } from '$app/stores';
 | 
				
			||||||
 | 
						import { addToast, trpc } from '$lib/store';
 | 
				
			||||||
 | 
						import Secret from './_components/Secret.svelte';
 | 
				
			||||||
 | 
						import PreviewSecret from './_components/PreviewSecret.svelte';
 | 
				
			||||||
 | 
						import { errorNotification } from '$lib/common';
 | 
				
			||||||
 | 
						import Explainer from '$lib/components/Explainer.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const limit = pLimit(1);
 | 
				
			||||||
 | 
						const { id } = $page.params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let batchSecrets = '';
 | 
				
			||||||
 | 
						async function refreshSecrets() {
 | 
				
			||||||
 | 
							const { data } = await trpc.applications.getSecrets.query({ id });
 | 
				
			||||||
 | 
							previewSecrets = [...data.previewSecrets];
 | 
				
			||||||
 | 
							secrets = [...data.secrets];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						async function getValues() {
 | 
				
			||||||
 | 
							if (!batchSecrets) return;
 | 
				
			||||||
 | 
							const eachValuePair = batchSecrets.split('\n');
 | 
				
			||||||
 | 
							const batchSecretsPairs = eachValuePair
 | 
				
			||||||
 | 
								.filter((secret) => !secret.startsWith('#') && secret)
 | 
				
			||||||
 | 
								.map((secret) => {
 | 
				
			||||||
 | 
									const [name, ...rest] = secret.split('=');
 | 
				
			||||||
 | 
									const value = rest.join('=');
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										name: name.trim(),
 | 
				
			||||||
 | 
										value: value.trim(),
 | 
				
			||||||
 | 
										createSecret: !secrets.find((secret: any) => name === secret.name)
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							await Promise.all(
 | 
				
			||||||
 | 
								batchSecretsPairs.map(({ name, value, createSecret }) =>
 | 
				
			||||||
 | 
									limit(async () => {
 | 
				
			||||||
 | 
										try {
 | 
				
			||||||
 | 
											if (!name || !value) return;
 | 
				
			||||||
 | 
											if (createSecret) {
 | 
				
			||||||
 | 
												await trpc.applications.newSecret.mutate({
 | 
				
			||||||
 | 
													id,
 | 
				
			||||||
 | 
													name,
 | 
				
			||||||
 | 
													value
 | 
				
			||||||
 | 
												});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
												addToast({
 | 
				
			||||||
 | 
													message: 'Secret created.',
 | 
				
			||||||
 | 
													type: 'success'
 | 
				
			||||||
 | 
												});
 | 
				
			||||||
 | 
											} else {
 | 
				
			||||||
 | 
												await trpc.applications.updateSecret.mutate({
 | 
				
			||||||
 | 
													id,
 | 
				
			||||||
 | 
													name,
 | 
				
			||||||
 | 
													value
 | 
				
			||||||
 | 
												});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
												addToast({
 | 
				
			||||||
 | 
													message: 'Secret updated.',
 | 
				
			||||||
 | 
													type: 'success'
 | 
				
			||||||
 | 
												});
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										} catch (error) {
 | 
				
			||||||
 | 
											return errorNotification(error);
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							batchSecrets = '';
 | 
				
			||||||
 | 
							await refreshSecrets();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="mx-auto w-full">
 | 
				
			||||||
 | 
						<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
 | 
				
			||||||
 | 
							<div class="title font-bold pb-3">Secrets</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						{#each secrets as secret, index}
 | 
				
			||||||
 | 
							{#key secret.id}
 | 
				
			||||||
 | 
								<Secret
 | 
				
			||||||
 | 
									{index}
 | 
				
			||||||
 | 
									length={secrets.length}
 | 
				
			||||||
 | 
									name={secret.name}
 | 
				
			||||||
 | 
									value={secret.value}
 | 
				
			||||||
 | 
									isBuildSecret={secret.isBuildSecret}
 | 
				
			||||||
 | 
									on:refresh={refreshSecrets}
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
							{/key}
 | 
				
			||||||
 | 
						{/each}
 | 
				
			||||||
 | 
						<div class="lg:pt-0 pt-10">
 | 
				
			||||||
 | 
							<Secret on:refresh={refreshSecrets} length={secrets.length} isNewSecret />
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						{#if !application.settings.isBot && !application.simpleDockerfile}
 | 
				
			||||||
 | 
							<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
 | 
				
			||||||
 | 
								<div class="title font-bold pb-3 pt-8">
 | 
				
			||||||
 | 
									Preview Secrets <Explainer
 | 
				
			||||||
 | 
										explanation="These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							{#if previewSecrets.length !== 0}
 | 
				
			||||||
 | 
								{#each previewSecrets as secret, index}
 | 
				
			||||||
 | 
									{#key index}
 | 
				
			||||||
 | 
										<PreviewSecret
 | 
				
			||||||
 | 
											{index}
 | 
				
			||||||
 | 
											length={secrets.length}
 | 
				
			||||||
 | 
											name={secret.name}
 | 
				
			||||||
 | 
											value={secret.value}
 | 
				
			||||||
 | 
											isBuildSecret={secret.isBuildSecret}
 | 
				
			||||||
 | 
											on:refresh={refreshSecrets}
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									{/key}
 | 
				
			||||||
 | 
								{/each}
 | 
				
			||||||
 | 
							{:else}
 | 
				
			||||||
 | 
								Add secrets first to see Preview Secrets.
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<form on:submit|preventDefault={getValues} class="mb-12 w-full">
 | 
				
			||||||
 | 
						<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 pt-10">
 | 
				
			||||||
 | 
							<div class="flex flex-row space-x-2">
 | 
				
			||||||
 | 
								<div class="title font-bold pb-3 ">Paste <code>.env</code> file</div>
 | 
				
			||||||
 | 
								<button type="submit" class="btn btn-sm bg-primary">Add Secrets in Batch</button>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<textarea
 | 
				
			||||||
 | 
							placeholder={`PORT=1337\nPASSWORD=supersecret`}
 | 
				
			||||||
 | 
							bind:value={batchSecrets}
 | 
				
			||||||
 | 
							class="mb-2 min-h-[200px] w-full"
 | 
				
			||||||
 | 
						/>
 | 
				
			||||||
 | 
					</form>
 | 
				
			||||||
							
								
								
									
										16
									
								
								apps/client/src/routes/applications/[id]/secrets/+page.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								apps/client/src/routes/applications/[id]/secrets/+page.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					import { error } from '@sveltejs/kit';
 | 
				
			||||||
 | 
					import { trpc } from '$lib/store';
 | 
				
			||||||
 | 
					import type { PageLoad } from './$types';
 | 
				
			||||||
 | 
					export const ssr = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const load: PageLoad = async ({ params }) => {
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							const { id } = params;
 | 
				
			||||||
 | 
							const { data } = await trpc.applications.getSecrets.query({ id });
 | 
				
			||||||
 | 
							return data;
 | 
				
			||||||
 | 
						} catch (err) {
 | 
				
			||||||
 | 
							throw error(500, {
 | 
				
			||||||
 | 
								message: 'An unexpected error occurred, please try again later.'
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,131 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						export let length = 0;
 | 
				
			||||||
 | 
						export let index: number = 0;
 | 
				
			||||||
 | 
						export let name = '';
 | 
				
			||||||
 | 
						export let value = '';
 | 
				
			||||||
 | 
						export let isBuildSecret = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						import { page } from '$app/stores';
 | 
				
			||||||
 | 
						import { errorNotification } from '$lib/common';
 | 
				
			||||||
 | 
						import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
 | 
				
			||||||
 | 
						import { addToast, trpc } from '$lib/store';
 | 
				
			||||||
 | 
						import { createEventDispatcher } from 'svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const dispatch = createEventDispatcher();
 | 
				
			||||||
 | 
						const { id } = $page.params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function updatePreviewSecret() {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								await trpc.applications.updateSecret.mutate({
 | 
				
			||||||
 | 
									id,
 | 
				
			||||||
 | 
									name: name.trim(),
 | 
				
			||||||
 | 
									value: value.trim(),
 | 
				
			||||||
 | 
									isPreview: true
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								addToast({
 | 
				
			||||||
 | 
									message: 'Secret updated.',
 | 
				
			||||||
 | 
									type: 'success'
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
 | 
								return errorNotification(error);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="w-full grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
 | 
				
			||||||
 | 
						<div class="flex flex-col">
 | 
				
			||||||
 | 
							{#if index === 0 || length === 0}
 | 
				
			||||||
 | 
								<label for="name" class="pb-2 uppercase font-bold">name</label>
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<input
 | 
				
			||||||
 | 
								id="secretName"
 | 
				
			||||||
 | 
								readonly
 | 
				
			||||||
 | 
								disabled
 | 
				
			||||||
 | 
								value={name}
 | 
				
			||||||
 | 
								required
 | 
				
			||||||
 | 
								placeholder="EXAMPLE_VARIABLE"
 | 
				
			||||||
 | 
								class=" w-full"
 | 
				
			||||||
 | 
							/>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div class="flex flex-col">
 | 
				
			||||||
 | 
							{#if index === 0 || length === 0}
 | 
				
			||||||
 | 
								<label for="value" class="pb-2 uppercase font-bold">value</label>
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<CopyPasswordField
 | 
				
			||||||
 | 
								id="secretValue"
 | 
				
			||||||
 | 
								name="secretValue"
 | 
				
			||||||
 | 
								isPasswordField={true}
 | 
				
			||||||
 | 
								bind:value
 | 
				
			||||||
 | 
								placeholder="J$#@UIO%HO#$U%H"
 | 
				
			||||||
 | 
							/>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div class="flex lg:flex-col flex-row justify-start items-center pt-3 lg:pt-0">
 | 
				
			||||||
 | 
							{#if index === 0 || length === 0}
 | 
				
			||||||
 | 
								<label for="name" class="pb-2 uppercase lg:block hidden font-bold"
 | 
				
			||||||
 | 
									>Need during buildtime?</label
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
							<label for="name" class="pb-2 uppercase lg:hidden block font-bold">Need during buildtime?</label
 | 
				
			||||||
 | 
							>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
 | 
				
			||||||
 | 
								<button
 | 
				
			||||||
 | 
									aria-pressed="false"
 | 
				
			||||||
 | 
									class="opacity-50 cursor-pointer cursor-not-allowedrelative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out "
 | 
				
			||||||
 | 
									class:bg-green-600={isBuildSecret}
 | 
				
			||||||
 | 
									class:bg-stone-700={!isBuildSecret}
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
 | 
									<span class="sr-only">Is build secret?</span>
 | 
				
			||||||
 | 
									<span
 | 
				
			||||||
 | 
										class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
 | 
				
			||||||
 | 
										class:translate-x-5={isBuildSecret}
 | 
				
			||||||
 | 
										class:translate-x-0={!isBuildSecret}
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										<span
 | 
				
			||||||
 | 
											class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
 | 
				
			||||||
 | 
											class:opacity-0={isBuildSecret}
 | 
				
			||||||
 | 
											class:opacity-100={!isBuildSecret}
 | 
				
			||||||
 | 
											aria-hidden="true"
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
 | 
				
			||||||
 | 
												<path
 | 
				
			||||||
 | 
													d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
 | 
				
			||||||
 | 
													stroke="currentColor"
 | 
				
			||||||
 | 
													stroke-width="2"
 | 
				
			||||||
 | 
													stroke-linecap="round"
 | 
				
			||||||
 | 
													stroke-linejoin="round"
 | 
				
			||||||
 | 
												/>
 | 
				
			||||||
 | 
											</svg>
 | 
				
			||||||
 | 
										</span>
 | 
				
			||||||
 | 
										<span
 | 
				
			||||||
 | 
											class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
 | 
				
			||||||
 | 
											aria-hidden="true"
 | 
				
			||||||
 | 
											class:opacity-100={isBuildSecret}
 | 
				
			||||||
 | 
											class:opacity-0={!isBuildSecret}
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
 | 
				
			||||||
 | 
												<path
 | 
				
			||||||
 | 
													d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
 | 
				
			||||||
 | 
												/>
 | 
				
			||||||
 | 
											</svg>
 | 
				
			||||||
 | 
										</span>
 | 
				
			||||||
 | 
									</span>
 | 
				
			||||||
 | 
								</button>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div class="flex flex-row lg:flex-col lg:items-center items-start">
 | 
				
			||||||
 | 
							{#if index === 0 || length === 0}
 | 
				
			||||||
 | 
								<label for="name" class="pb-5 uppercase lg:block hidden font-bold" />
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<div class="flex justify-center h-full items-center pt-3">
 | 
				
			||||||
 | 
								<div class="flex flex-row justify-center space-x-2">
 | 
				
			||||||
 | 
									<div class="flex items-center justify-center">
 | 
				
			||||||
 | 
										<button class="btn btn-sm btn-primary" on:click={updatePreviewSecret}>Update</button>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -0,0 +1,193 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						export let length = 0;
 | 
				
			||||||
 | 
						export let index: number = 0;
 | 
				
			||||||
 | 
						export let name = '';
 | 
				
			||||||
 | 
						export let value = '';
 | 
				
			||||||
 | 
						export let isBuildSecret = false;
 | 
				
			||||||
 | 
						export let isNewSecret = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						import { page } from '$app/stores';
 | 
				
			||||||
 | 
						import { errorNotification } from '$lib/common';
 | 
				
			||||||
 | 
						import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
 | 
				
			||||||
 | 
						import { addToast, trpc } from '$lib/store';
 | 
				
			||||||
 | 
						import { createEventDispatcher } from 'svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const dispatch = createEventDispatcher();
 | 
				
			||||||
 | 
						const { id } = $page.params;
 | 
				
			||||||
 | 
						function cleanupState() {
 | 
				
			||||||
 | 
							if (isNewSecret) {
 | 
				
			||||||
 | 
								name = '';
 | 
				
			||||||
 | 
								value = '';
 | 
				
			||||||
 | 
								isBuildSecret = false;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						async function removeSecret() {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								await trpc.applications.deleteSecret.mutate({ id, name });
 | 
				
			||||||
 | 
								cleanupState();
 | 
				
			||||||
 | 
								addToast({
 | 
				
			||||||
 | 
									message: 'Secret removed.',
 | 
				
			||||||
 | 
									type: 'success'
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								dispatch('refresh');
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
 | 
								return errorNotification(error);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function addNewSecret() {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								if (!name.trim()) return errorNotification({ message: 'Name is required.' });
 | 
				
			||||||
 | 
								if (!value.trim()) return errorNotification({ message: 'Value is required.' });
 | 
				
			||||||
 | 
								await trpc.applications.newSecret.mutate({
 | 
				
			||||||
 | 
									id,
 | 
				
			||||||
 | 
									name: name.trim(),
 | 
				
			||||||
 | 
									value: value.trim(),
 | 
				
			||||||
 | 
									isBuildSecret
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								cleanupState();
 | 
				
			||||||
 | 
								addToast({
 | 
				
			||||||
 | 
									message: 'Secret added.',
 | 
				
			||||||
 | 
									type: 'success'
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								dispatch('refresh');
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
 | 
								return errorNotification(error);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function updateSecret({
 | 
				
			||||||
 | 
							changeIsBuildSecret = false
 | 
				
			||||||
 | 
						}: { changeIsBuildSecret?: boolean } = {}) {
 | 
				
			||||||
 | 
							if (changeIsBuildSecret) isBuildSecret = !isBuildSecret;
 | 
				
			||||||
 | 
							if (isNewSecret) return;
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								await trpc.applications.updateSecret.mutate({
 | 
				
			||||||
 | 
									id,
 | 
				
			||||||
 | 
									name: name.trim(),
 | 
				
			||||||
 | 
									value: value.trim(),
 | 
				
			||||||
 | 
									isBuildSecret,
 | 
				
			||||||
 | 
									isPreview: false
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								addToast({
 | 
				
			||||||
 | 
									message: 'Secret updated.',
 | 
				
			||||||
 | 
									type: 'success'
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								dispatch('refresh');
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
 | 
								return errorNotification(error);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="w-full grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
 | 
				
			||||||
 | 
						<div class="flex flex-col">
 | 
				
			||||||
 | 
							{#if (index === 0 && !isNewSecret) || length === 0}
 | 
				
			||||||
 | 
								<label for="name" class="pb-2 uppercase font-bold">name</label>
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<input
 | 
				
			||||||
 | 
								id={isNewSecret ? 'secretName' : 'secretNameNew'}
 | 
				
			||||||
 | 
								bind:value={name}
 | 
				
			||||||
 | 
								required
 | 
				
			||||||
 | 
								placeholder="EXAMPLE_VARIABLE"
 | 
				
			||||||
 | 
								readonly={!isNewSecret}
 | 
				
			||||||
 | 
								class="w-full"
 | 
				
			||||||
 | 
								class:bg-coolblack={!isNewSecret}
 | 
				
			||||||
 | 
								class:border={!isNewSecret}
 | 
				
			||||||
 | 
								class:border-dashed={!isNewSecret}
 | 
				
			||||||
 | 
								class:border-coolgray-300={!isNewSecret}
 | 
				
			||||||
 | 
								class:cursor-not-allowed={!isNewSecret}
 | 
				
			||||||
 | 
							/>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div class="flex flex-col">
 | 
				
			||||||
 | 
							{#if (index === 0 && !isNewSecret) || length === 0}
 | 
				
			||||||
 | 
								<label for="value" class="pb-2 uppercase font-bold">value</label>
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<CopyPasswordField
 | 
				
			||||||
 | 
								id={isNewSecret ? 'secretValue' : 'secretValueNew'}
 | 
				
			||||||
 | 
								name={isNewSecret ? 'secretValue' : 'secretValueNew'}
 | 
				
			||||||
 | 
								isPasswordField={true}
 | 
				
			||||||
 | 
								bind:value
 | 
				
			||||||
 | 
								placeholder="J$#@UIO%HO#$U%H"
 | 
				
			||||||
 | 
							/>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div class="flex lg:flex-col flex-row justify-start items-center pt-3 lg:pt-0">
 | 
				
			||||||
 | 
							{#if (index === 0 && !isNewSecret) || length === 0}
 | 
				
			||||||
 | 
								<label for="name" class="pb-2 uppercase lg:block hidden font-bold"
 | 
				
			||||||
 | 
									>Need during buildtime?</label
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
							<label for="name" class="pb-2 uppercase lg:hidden block font-bold">Need during buildtime?</label
 | 
				
			||||||
 | 
							>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
 | 
				
			||||||
 | 
								<button
 | 
				
			||||||
 | 
									on:click={() => updateSecret({ changeIsBuildSecret: true })}
 | 
				
			||||||
 | 
									aria-pressed="false"
 | 
				
			||||||
 | 
									class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out "
 | 
				
			||||||
 | 
									class:bg-green-600={isBuildSecret}
 | 
				
			||||||
 | 
									class:bg-stone-700={!isBuildSecret}
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
 | 
									<span class="sr-only">Is build secret?</span>
 | 
				
			||||||
 | 
									<span
 | 
				
			||||||
 | 
										class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
 | 
				
			||||||
 | 
										class:translate-x-5={isBuildSecret}
 | 
				
			||||||
 | 
										class:translate-x-0={!isBuildSecret}
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										<span
 | 
				
			||||||
 | 
											class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
 | 
				
			||||||
 | 
											class:opacity-0={isBuildSecret}
 | 
				
			||||||
 | 
											class:opacity-100={!isBuildSecret}
 | 
				
			||||||
 | 
											aria-hidden="true"
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
 | 
				
			||||||
 | 
												<path
 | 
				
			||||||
 | 
													d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
 | 
				
			||||||
 | 
													stroke="currentColor"
 | 
				
			||||||
 | 
													stroke-width="2"
 | 
				
			||||||
 | 
													stroke-linecap="round"
 | 
				
			||||||
 | 
													stroke-linejoin="round"
 | 
				
			||||||
 | 
												/>
 | 
				
			||||||
 | 
											</svg>
 | 
				
			||||||
 | 
										</span>
 | 
				
			||||||
 | 
										<span
 | 
				
			||||||
 | 
											class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
 | 
				
			||||||
 | 
											aria-hidden="true"
 | 
				
			||||||
 | 
											class:opacity-100={isBuildSecret}
 | 
				
			||||||
 | 
											class:opacity-0={!isBuildSecret}
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
 | 
				
			||||||
 | 
												<path
 | 
				
			||||||
 | 
													d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
 | 
				
			||||||
 | 
												/>
 | 
				
			||||||
 | 
											</svg>
 | 
				
			||||||
 | 
										</span>
 | 
				
			||||||
 | 
									</span>
 | 
				
			||||||
 | 
								</button>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div class="flex flex-row lg:flex-col lg:items-center items-start">
 | 
				
			||||||
 | 
							{#if (index === 0 && !isNewSecret) || length === 0}
 | 
				
			||||||
 | 
								<label for="name" class="pb-5 uppercase lg:block hidden font-bold" />
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<div class="flex justify-center h-full items-center pt-3">
 | 
				
			||||||
 | 
								{#if isNewSecret}
 | 
				
			||||||
 | 
									<div class="flex items-center justify-center">
 | 
				
			||||||
 | 
										<button class="btn btn-sm btn-primary" on:click={addNewSecret}>Add</button>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								{:else}
 | 
				
			||||||
 | 
									<div class="flex flex-row justify-center space-x-2">
 | 
				
			||||||
 | 
										<div class="flex items-center justify-center">
 | 
				
			||||||
 | 
											<button class="btn btn-sm btn-primary" on:click={() => updateSecret()}>Set</button>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										<div class="flex justify-center items-end">
 | 
				
			||||||
 | 
											<button class="btn btn-sm btn-error" on:click={removeSecret}>Remove</button>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								{/if}
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -0,0 +1,78 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import type { PageData } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let data: PageData;
 | 
				
			||||||
 | 
						const application = data.application.data;
 | 
				
			||||||
 | 
						let persistentStorages = data.persistentStorages;
 | 
				
			||||||
 | 
						import { page } from '$app/stores';
 | 
				
			||||||
 | 
						import Storage from './components/Storage.svelte';
 | 
				
			||||||
 | 
						import Explainer from '$lib/components/Explainer.svelte';
 | 
				
			||||||
 | 
						import { trpc } from '$lib/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let composeJson: any = JSON.parse(application?.dockerComposeFile || '{}');
 | 
				
			||||||
 | 
						let predefinedVolumes: any[] = [];
 | 
				
			||||||
 | 
						if (composeJson?.services) {
 | 
				
			||||||
 | 
							for (const [_, service] of Object.entries(composeJson.services)) {
 | 
				
			||||||
 | 
								if (service?.volumes) {
 | 
				
			||||||
 | 
									for (const [_, volumeName] of Object.entries(service.volumes)) {
 | 
				
			||||||
 | 
										let [volume, target] = volumeName.split(':');
 | 
				
			||||||
 | 
										if (volume === '.') {
 | 
				
			||||||
 | 
											volume = target;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										if (!target) {
 | 
				
			||||||
 | 
											target = volume;
 | 
				
			||||||
 | 
											volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										predefinedVolumes.push({ id: volume, path: target, predefined: true });
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const { id } = $page.params;
 | 
				
			||||||
 | 
						async function refreshStorage() {
 | 
				
			||||||
 | 
							const { data } = await trpc.applications.getStorages.query({ id });
 | 
				
			||||||
 | 
							persistentStorages = [...data.persistentStorages];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="w-full">
 | 
				
			||||||
 | 
						<div class="mx-auto w-full">
 | 
				
			||||||
 | 
							<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
 | 
				
			||||||
 | 
								<div class="title font-bold pb-3">Persistent Volumes</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							{#if predefinedVolumes.length > 0}
 | 
				
			||||||
 | 
								<div class="title">Predefined Volumes</div>
 | 
				
			||||||
 | 
								<div class="w-full lg:px-0 px-4">
 | 
				
			||||||
 | 
									<div class="grid grid-col-1 lg:grid-cols-2 py-2 gap-2">
 | 
				
			||||||
 | 
										<div class="font-bold uppercase">Volume Id</div>
 | 
				
			||||||
 | 
										<div class="font-bold uppercase">Mount Dir</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<div class="gap-4">
 | 
				
			||||||
 | 
									{#each predefinedVolumes as storage}
 | 
				
			||||||
 | 
										{#key storage.id}
 | 
				
			||||||
 | 
											<Storage on:refresh={refreshStorage} {storage} />
 | 
				
			||||||
 | 
										{/key}
 | 
				
			||||||
 | 
									{/each}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
							{#if persistentStorages.length > 0}
 | 
				
			||||||
 | 
								<div class="title" class:pt-10={predefinedVolumes.length > 0}>Custom Volumes</div>
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
							{#each persistentStorages as storage}
 | 
				
			||||||
 | 
								{#key storage.id}
 | 
				
			||||||
 | 
									<Storage on:refresh={refreshStorage} {storage} />
 | 
				
			||||||
 | 
								{/key}
 | 
				
			||||||
 | 
							{/each}
 | 
				
			||||||
 | 
							<div class="Preview Secrets" class:pt-10={predefinedVolumes.length > 0}>
 | 
				
			||||||
 | 
								Add New Volume <Explainer
 | 
				
			||||||
 | 
									position="dropdown-bottom"
 | 
				
			||||||
 | 
									explanation="You can specify any folder that you want to be persistent across deployments.<br><br><span class='text-settings '>/example</span> means it will preserve <span class='text-settings '>/example</span> between deployments.<br><br>Your application's data is copied to <span class='text-settings '>/app</span> inside the container, you can preserve data under it as well, like <span class='text-settings '>/app/db</span>.<br><br>This is useful for storing data such as a <span class='text-settings '>database (SQLite)</span> or a <span class='text-settings '>cache</span>."
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<Storage on:refresh={refreshStorage} isNew />
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
							
								
								
									
										16
									
								
								apps/client/src/routes/applications/[id]/storages/+page.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								apps/client/src/routes/applications/[id]/storages/+page.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					import { error } from '@sveltejs/kit';
 | 
				
			||||||
 | 
					import { trpc } from '$lib/store';
 | 
				
			||||||
 | 
					import type { PageLoad } from './$types';
 | 
				
			||||||
 | 
					export const ssr = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const load: PageLoad = async ({ params }) => {
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							const { id } = params;
 | 
				
			||||||
 | 
							const { data } = await trpc.applications.getStorages.query({ id });
 | 
				
			||||||
 | 
							return data;
 | 
				
			||||||
 | 
						} catch (err) {
 | 
				
			||||||
 | 
							throw error(500, {
 | 
				
			||||||
 | 
								message: 'An unexpected error occurred, please try again later.'
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,114 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						export let isNew = false;
 | 
				
			||||||
 | 
						export let storage: any = {
 | 
				
			||||||
 | 
							id: null,
 | 
				
			||||||
 | 
							path: null
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
						import { page } from '$app/stores';
 | 
				
			||||||
 | 
						import { createEventDispatcher } from 'svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						import { errorNotification } from '$lib/common';
 | 
				
			||||||
 | 
						import { addToast, trpc } from '$lib/store';
 | 
				
			||||||
 | 
						const { id } = $page.params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const dispatch = createEventDispatcher();
 | 
				
			||||||
 | 
						async function saveStorage(newStorage = false) {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								if (!storage.path) return errorNotification('Path is required');
 | 
				
			||||||
 | 
								storage.path = storage.path.startsWith('/') ? storage.path : `/${storage.path}`;
 | 
				
			||||||
 | 
								storage.path = storage.path.endsWith('/') ? storage.path.slice(0, -1) : storage.path;
 | 
				
			||||||
 | 
								storage.path.replace(/\/\//g, '/');
 | 
				
			||||||
 | 
								await trpc.applications.updateStorage.mutate({
 | 
				
			||||||
 | 
									id,
 | 
				
			||||||
 | 
									path: storage.path,
 | 
				
			||||||
 | 
									storageId: storage.id,
 | 
				
			||||||
 | 
									newStorage
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								dispatch('refresh');
 | 
				
			||||||
 | 
								if (isNew) {
 | 
				
			||||||
 | 
									storage.path = null;
 | 
				
			||||||
 | 
									storage.id = null;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (newStorage) {
 | 
				
			||||||
 | 
									addToast({
 | 
				
			||||||
 | 
										message: 'Storage created',
 | 
				
			||||||
 | 
										type: 'success'
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									addToast({
 | 
				
			||||||
 | 
										message: 'Storage updated',
 | 
				
			||||||
 | 
										type: 'success'
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
 | 
								return errorNotification(error);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						async function removeStorage() {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								await trpc.applications.deleteStorage.mutate({
 | 
				
			||||||
 | 
									id,
 | 
				
			||||||
 | 
									path: storage.path
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								dispatch('refresh');
 | 
				
			||||||
 | 
								addToast({
 | 
				
			||||||
 | 
									message: 'Storage removed',
 | 
				
			||||||
 | 
									type: 'success'
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
 | 
								return errorNotification(error);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="w-full lg:px-0 px-4">
 | 
				
			||||||
 | 
						{#if storage.predefined}
 | 
				
			||||||
 | 
							<div class="flex flex-col lg:flex-row gap-4 pb-2">
 | 
				
			||||||
 | 
								<input disabled readonly class="w-full" value={storage.id} />
 | 
				
			||||||
 | 
								<input disabled readonly class="w-full" bind:value={storage.path} />
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						{:else}
 | 
				
			||||||
 | 
							<div class="flex gap-4 pb-2" class:pt-8={isNew}>
 | 
				
			||||||
 | 
								{#if storage.applicationId}
 | 
				
			||||||
 | 
									{#if storage.oldPath}
 | 
				
			||||||
 | 
										<input
 | 
				
			||||||
 | 
											disabled
 | 
				
			||||||
 | 
											readonly
 | 
				
			||||||
 | 
											class="w-full"
 | 
				
			||||||
 | 
											value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									{:else}
 | 
				
			||||||
 | 
										<input
 | 
				
			||||||
 | 
											disabled
 | 
				
			||||||
 | 
											readonly
 | 
				
			||||||
 | 
											class="w-full"
 | 
				
			||||||
 | 
											value="{storage.applicationId}{storage.path.replace(/\//gi, '-')}"
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									{/if}
 | 
				
			||||||
 | 
								{/if}
 | 
				
			||||||
 | 
								<input
 | 
				
			||||||
 | 
									disabled={!isNew}
 | 
				
			||||||
 | 
									readonly={!isNew}
 | 
				
			||||||
 | 
									class="w-full"
 | 
				
			||||||
 | 
									bind:value={storage.path}
 | 
				
			||||||
 | 
									required
 | 
				
			||||||
 | 
									placeholder="eg: /data"
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<div class="flex items-center justify-center">
 | 
				
			||||||
 | 
									{#if isNew}
 | 
				
			||||||
 | 
										<div class="w-full lg:w-64">
 | 
				
			||||||
 | 
											<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}
 | 
				
			||||||
 | 
												>Add</button
 | 
				
			||||||
 | 
											>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									{:else}
 | 
				
			||||||
 | 
										<div class="flex justify-center">
 | 
				
			||||||
 | 
											<button class="btn btn-sm btn-error" on:click={removeStorage}>Remove</button>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									{/if}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -2,6 +2,60 @@ import { goto } from '$app/navigation';
 | 
				
			|||||||
import { errorNotification } from '$lib/common';
 | 
					import { errorNotification } from '$lib/common';
 | 
				
			||||||
import { trpc } from '$lib/store';
 | 
					import { trpc } from '$lib/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function saveForm() {
 | 
					export async function saveForm(id, application, baseDatabaseBranch, dockerComposeConfiguration) {
 | 
				
			||||||
	return await trpc.applications.save.mutate();
 | 
						let {
 | 
				
			||||||
 | 
							name,
 | 
				
			||||||
 | 
							buildPack,
 | 
				
			||||||
 | 
							fqdn,
 | 
				
			||||||
 | 
							port,
 | 
				
			||||||
 | 
							exposePort,
 | 
				
			||||||
 | 
							installCommand,
 | 
				
			||||||
 | 
							buildCommand,
 | 
				
			||||||
 | 
							startCommand,
 | 
				
			||||||
 | 
							baseDirectory,
 | 
				
			||||||
 | 
							publishDirectory,
 | 
				
			||||||
 | 
							pythonWSGI,
 | 
				
			||||||
 | 
							pythonModule,
 | 
				
			||||||
 | 
							pythonVariable,
 | 
				
			||||||
 | 
							dockerFileLocation,
 | 
				
			||||||
 | 
							denoMainFile,
 | 
				
			||||||
 | 
							denoOptions,
 | 
				
			||||||
 | 
							gitCommitHash,
 | 
				
			||||||
 | 
							baseImage,
 | 
				
			||||||
 | 
							baseBuildImage,
 | 
				
			||||||
 | 
							deploymentType,
 | 
				
			||||||
 | 
							dockerComposeFile,
 | 
				
			||||||
 | 
							dockerComposeFileLocation,
 | 
				
			||||||
 | 
							simpleDockerfile,
 | 
				
			||||||
 | 
							dockerRegistryImageName
 | 
				
			||||||
 | 
						} = application;
 | 
				
			||||||
 | 
						return await trpc.applications.save.mutate({
 | 
				
			||||||
 | 
							id,
 | 
				
			||||||
 | 
							name,
 | 
				
			||||||
 | 
							buildPack,
 | 
				
			||||||
 | 
							fqdn,
 | 
				
			||||||
 | 
							port,
 | 
				
			||||||
 | 
							exposePort,
 | 
				
			||||||
 | 
							installCommand,
 | 
				
			||||||
 | 
							buildCommand,
 | 
				
			||||||
 | 
							startCommand,
 | 
				
			||||||
 | 
							baseDirectory,
 | 
				
			||||||
 | 
							publishDirectory,
 | 
				
			||||||
 | 
							pythonWSGI,
 | 
				
			||||||
 | 
							pythonModule,
 | 
				
			||||||
 | 
							pythonVariable,
 | 
				
			||||||
 | 
							dockerFileLocation,
 | 
				
			||||||
 | 
							denoMainFile,
 | 
				
			||||||
 | 
							denoOptions,
 | 
				
			||||||
 | 
							gitCommitHash,
 | 
				
			||||||
 | 
							baseImage,
 | 
				
			||||||
 | 
							baseBuildImage,
 | 
				
			||||||
 | 
							deploymentType,
 | 
				
			||||||
 | 
							dockerComposeFile,
 | 
				
			||||||
 | 
							dockerComposeFileLocation,
 | 
				
			||||||
 | 
							simpleDockerfile,
 | 
				
			||||||
 | 
							dockerRegistryImageName,
 | 
				
			||||||
 | 
							baseDatabaseBranch,
 | 
				
			||||||
 | 
							dockerComposeConfiguration: JSON.stringify(dockerComposeConfiguration)
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,9 +3,13 @@ import type { UserConfig } from 'vite';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const config: UserConfig = {
 | 
					const config: UserConfig = {
 | 
				
			||||||
	server: {
 | 
						server: {
 | 
				
			||||||
		host: '0.0.0.0',
 | 
							host: '0.0.0.0'
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	plugins: [sveltekit()]
 | 
						plugins: [sveltekit()],
 | 
				
			||||||
 | 
						define: {
 | 
				
			||||||
 | 
							GITPOD_WORKSPACE_URL: JSON.stringify(process.env.GITPOD_WORKSPACE_URL),
 | 
				
			||||||
 | 
							CODESANDBOX_HOST: JSON.stringify(process.env.CODESANDBOX_HOST)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default config;
 | 
					export default config;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,6 +34,7 @@
 | 
				
			|||||||
		"fastify": "4.10.2",
 | 
							"fastify": "4.10.2",
 | 
				
			||||||
		"fastify-plugin": "4.4.0",
 | 
							"fastify-plugin": "4.4.0",
 | 
				
			||||||
		"got": "^12.5.3",
 | 
							"got": "^12.5.3",
 | 
				
			||||||
 | 
							"is-ip": "5.0.0",
 | 
				
			||||||
		"is-port-reachable": "4.0.0",
 | 
							"is-port-reachable": "4.0.0",
 | 
				
			||||||
		"js-yaml": "4.1.0",
 | 
							"js-yaml": "4.1.0",
 | 
				
			||||||
		"jsonwebtoken": "8.5.1",
 | 
							"jsonwebtoken": "8.5.1",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import type { Permission, Setting, Team, TeamInvitation, User } from '@prisma/cl
 | 
				
			|||||||
import { prisma } from '../prisma';
 | 
					import { prisma } from '../prisma';
 | 
				
			||||||
import bcrypt from 'bcryptjs';
 | 
					import bcrypt from 'bcryptjs';
 | 
				
			||||||
import crypto from 'crypto';
 | 
					import crypto from 'crypto';
 | 
				
			||||||
 | 
					import { promises as dns } from 'dns';
 | 
				
			||||||
import fs from 'fs/promises';
 | 
					import fs from 'fs/promises';
 | 
				
			||||||
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
 | 
					import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
 | 
				
			||||||
import type { Config } from 'unique-names-generator';
 | 
					import type { Config } from 'unique-names-generator';
 | 
				
			||||||
@@ -181,3 +182,347 @@ export async function saveDockerRegistryCredentials({ url, username, password, w
 | 
				
			|||||||
	await fs.writeFile(`${location}/config.json`, payload);
 | 
						await fs.writeFile(`${location}/config.json`, payload);
 | 
				
			||||||
	return location;
 | 
						return location;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					export function getDomain(domain: string): string {
 | 
				
			||||||
 | 
						if (domain) {
 | 
				
			||||||
 | 
							return domain?.replace('https://', '').replace('http://', '');
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return '';
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function isDomainConfigured({
 | 
				
			||||||
 | 
						id,
 | 
				
			||||||
 | 
						fqdn,
 | 
				
			||||||
 | 
						checkOwn = false,
 | 
				
			||||||
 | 
						remoteIpAddress = undefined
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
						id: string;
 | 
				
			||||||
 | 
						fqdn: string;
 | 
				
			||||||
 | 
						checkOwn?: boolean;
 | 
				
			||||||
 | 
						remoteIpAddress?: string;
 | 
				
			||||||
 | 
					}): Promise<boolean> {
 | 
				
			||||||
 | 
						const domain = getDomain(fqdn);
 | 
				
			||||||
 | 
						const nakedDomain = domain.replace('www.', '');
 | 
				
			||||||
 | 
						const foundApp = await prisma.application.findFirst({
 | 
				
			||||||
 | 
							where: {
 | 
				
			||||||
 | 
								OR: [
 | 
				
			||||||
 | 
									{ fqdn: { endsWith: `//${nakedDomain}` } },
 | 
				
			||||||
 | 
									{ fqdn: { endsWith: `//www.${nakedDomain}` } },
 | 
				
			||||||
 | 
									{ dockerComposeConfiguration: { contains: `//${nakedDomain}` } },
 | 
				
			||||||
 | 
									{ dockerComposeConfiguration: { contains: `//www.${nakedDomain}` } }
 | 
				
			||||||
 | 
								],
 | 
				
			||||||
 | 
								id: { not: id },
 | 
				
			||||||
 | 
								destinationDocker: {
 | 
				
			||||||
 | 
									remoteIpAddress
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							select: { fqdn: true }
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						const foundService = await prisma.service.findFirst({
 | 
				
			||||||
 | 
							where: {
 | 
				
			||||||
 | 
								OR: [
 | 
				
			||||||
 | 
									{ fqdn: { endsWith: `//${nakedDomain}` } },
 | 
				
			||||||
 | 
									{ fqdn: { endsWith: `//www.${nakedDomain}` } }
 | 
				
			||||||
 | 
								],
 | 
				
			||||||
 | 
								id: { not: checkOwn ? undefined : id },
 | 
				
			||||||
 | 
								destinationDocker: {
 | 
				
			||||||
 | 
									remoteIpAddress
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							select: { fqdn: true }
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const coolifyFqdn = await prisma.setting.findFirst({
 | 
				
			||||||
 | 
							where: {
 | 
				
			||||||
 | 
								OR: [
 | 
				
			||||||
 | 
									{ fqdn: { endsWith: `//${nakedDomain}` } },
 | 
				
			||||||
 | 
									{ fqdn: { endsWith: `//www.${nakedDomain}` } }
 | 
				
			||||||
 | 
								],
 | 
				
			||||||
 | 
								id: { not: id }
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							select: { fqdn: true }
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						return !!(foundApp || foundService || coolifyFqdn);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function checkExposedPort({
 | 
				
			||||||
 | 
						id,
 | 
				
			||||||
 | 
						configuredPort,
 | 
				
			||||||
 | 
						exposePort,
 | 
				
			||||||
 | 
						engine,
 | 
				
			||||||
 | 
						remoteEngine,
 | 
				
			||||||
 | 
						remoteIpAddress
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
						id: string;
 | 
				
			||||||
 | 
						configuredPort?: number;
 | 
				
			||||||
 | 
						exposePort: number;
 | 
				
			||||||
 | 
						engine: string;
 | 
				
			||||||
 | 
						remoteEngine: boolean;
 | 
				
			||||||
 | 
						remoteIpAddress?: string;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
						if (exposePort < 1024 || exposePort > 65535) {
 | 
				
			||||||
 | 
							throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` };
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (configuredPort) {
 | 
				
			||||||
 | 
							if (configuredPort !== exposePort) {
 | 
				
			||||||
 | 
								const availablePort = await getFreeExposedPort(
 | 
				
			||||||
 | 
									id,
 | 
				
			||||||
 | 
									exposePort,
 | 
				
			||||||
 | 
									engine,
 | 
				
			||||||
 | 
									remoteEngine,
 | 
				
			||||||
 | 
									remoteIpAddress
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								if (availablePort.toString() !== exposePort.toString()) {
 | 
				
			||||||
 | 
									throw { status: 500, message: `Port ${exposePort} is already in use.` };
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							const availablePort = await getFreeExposedPort(
 | 
				
			||||||
 | 
								id,
 | 
				
			||||||
 | 
								exposePort,
 | 
				
			||||||
 | 
								engine,
 | 
				
			||||||
 | 
								remoteEngine,
 | 
				
			||||||
 | 
								remoteIpAddress
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							if (availablePort.toString() !== exposePort.toString()) {
 | 
				
			||||||
 | 
								throw { status: 500, message: `Port ${exposePort} is already in use.` };
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export async function getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress) {
 | 
				
			||||||
 | 
						const { default: checkPort } = await import('is-port-reachable');
 | 
				
			||||||
 | 
						if (remoteEngine) {
 | 
				
			||||||
 | 
							const applicationUsed = await (
 | 
				
			||||||
 | 
								await prisma.application.findMany({
 | 
				
			||||||
 | 
									where: {
 | 
				
			||||||
 | 
										exposePort: { not: null },
 | 
				
			||||||
 | 
										id: { not: id },
 | 
				
			||||||
 | 
										destinationDocker: { remoteIpAddress }
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									select: { exposePort: true }
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							).map((a) => a.exposePort);
 | 
				
			||||||
 | 
							const serviceUsed = await (
 | 
				
			||||||
 | 
								await prisma.service.findMany({
 | 
				
			||||||
 | 
									where: {
 | 
				
			||||||
 | 
										exposePort: { not: null },
 | 
				
			||||||
 | 
										id: { not: id },
 | 
				
			||||||
 | 
										destinationDocker: { remoteIpAddress }
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									select: { exposePort: true }
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							).map((a) => a.exposePort);
 | 
				
			||||||
 | 
							const usedPorts = [...applicationUsed, ...serviceUsed];
 | 
				
			||||||
 | 
							if (usedPorts.includes(exposePort)) {
 | 
				
			||||||
 | 
								return false;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							const found = await checkPort(exposePort, { host: remoteIpAddress });
 | 
				
			||||||
 | 
							if (!found) {
 | 
				
			||||||
 | 
								return exposePort;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							const applicationUsed = await (
 | 
				
			||||||
 | 
								await prisma.application.findMany({
 | 
				
			||||||
 | 
									where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { engine } },
 | 
				
			||||||
 | 
									select: { exposePort: true }
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							).map((a) => a.exposePort);
 | 
				
			||||||
 | 
							const serviceUsed = await (
 | 
				
			||||||
 | 
								await prisma.service.findMany({
 | 
				
			||||||
 | 
									where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { engine } },
 | 
				
			||||||
 | 
									select: { exposePort: true }
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							).map((a) => a.exposePort);
 | 
				
			||||||
 | 
							const usedPorts = [...applicationUsed, ...serviceUsed];
 | 
				
			||||||
 | 
							if (usedPorts.includes(exposePort)) {
 | 
				
			||||||
 | 
								return false;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							const found = await checkPort(exposePort, { host: 'localhost' });
 | 
				
			||||||
 | 
							if (!found) {
 | 
				
			||||||
 | 
								return exposePort;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }): Promise<any> {
 | 
				
			||||||
 | 
						const { isIP } = await import('is-ip');
 | 
				
			||||||
 | 
						const domain = getDomain(fqdn);
 | 
				
			||||||
 | 
						const domainDualCert = domain.includes('www.') ? domain.replace('www.', '') : `www.${domain}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const { DNSServers } = await listSettings();
 | 
				
			||||||
 | 
						if (DNSServers) {
 | 
				
			||||||
 | 
							dns.setServers([...DNSServers.split(',')]);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let resolves = [];
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							if (isIP(hostname)) {
 | 
				
			||||||
 | 
								resolves = [hostname];
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								resolves = await dns.resolve4(hostname);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							throw { status: 500, message: `Could not determine IP address for ${hostname}.` };
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (dualCerts) {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								const ipDomain = await dns.resolve4(domain);
 | 
				
			||||||
 | 
								const ipDomainDualCert = await dns.resolve4(domainDualCert);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								let ipDomainFound = false;
 | 
				
			||||||
 | 
								let ipDomainDualCertFound = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for (const ip of ipDomain) {
 | 
				
			||||||
 | 
									if (resolves.includes(ip)) {
 | 
				
			||||||
 | 
										ipDomainFound = true;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								for (const ip of ipDomainDualCert) {
 | 
				
			||||||
 | 
									if (resolves.includes(ip)) {
 | 
				
			||||||
 | 
										ipDomainDualCertFound = true;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (ipDomainFound && ipDomainDualCertFound) return { status: 200 };
 | 
				
			||||||
 | 
								throw {
 | 
				
			||||||
 | 
									status: 500,
 | 
				
			||||||
 | 
									message: `DNS not set correctly or propogated.<br>Please check your DNS settings.`
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
 | 
								throw {
 | 
				
			||||||
 | 
									status: 500,
 | 
				
			||||||
 | 
									message: `DNS not set correctly or propogated.<br>Please check your DNS settings.`
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								const ipDomain = await dns.resolve4(domain);
 | 
				
			||||||
 | 
								let ipDomainFound = false;
 | 
				
			||||||
 | 
								for (const ip of ipDomain) {
 | 
				
			||||||
 | 
									if (resolves.includes(ip)) {
 | 
				
			||||||
 | 
										ipDomainFound = true;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (ipDomainFound) return { status: 200 };
 | 
				
			||||||
 | 
								throw {
 | 
				
			||||||
 | 
									status: 500,
 | 
				
			||||||
 | 
									message: `DNS not set correctly or propogated.<br>Please check your DNS settings.`
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
 | 
								throw {
 | 
				
			||||||
 | 
									status: 500,
 | 
				
			||||||
 | 
									message: `DNS not set correctly or propogated.<br>Please check your DNS settings.`
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export const setDefaultConfiguration = async (data: any) => {
 | 
				
			||||||
 | 
						let {
 | 
				
			||||||
 | 
							buildPack,
 | 
				
			||||||
 | 
							port,
 | 
				
			||||||
 | 
							installCommand,
 | 
				
			||||||
 | 
							startCommand,
 | 
				
			||||||
 | 
							buildCommand,
 | 
				
			||||||
 | 
							publishDirectory,
 | 
				
			||||||
 | 
							baseDirectory,
 | 
				
			||||||
 | 
							dockerFileLocation,
 | 
				
			||||||
 | 
							dockerComposeFileLocation,
 | 
				
			||||||
 | 
							denoMainFile
 | 
				
			||||||
 | 
						} = data;
 | 
				
			||||||
 | 
						//@ts-ignore
 | 
				
			||||||
 | 
						const template = scanningTemplates[buildPack];
 | 
				
			||||||
 | 
						if (!port) {
 | 
				
			||||||
 | 
							port = template?.port || 3000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (buildPack === 'static') port = 80;
 | 
				
			||||||
 | 
							else if (buildPack === 'node') port = 3000;
 | 
				
			||||||
 | 
							else if (buildPack === 'php') port = 80;
 | 
				
			||||||
 | 
							else if (buildPack === 'python') port = 8000;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (!installCommand && buildPack !== 'static' && buildPack !== 'laravel')
 | 
				
			||||||
 | 
							installCommand = template?.installCommand || 'yarn install';
 | 
				
			||||||
 | 
						if (!startCommand && buildPack !== 'static' && buildPack !== 'laravel')
 | 
				
			||||||
 | 
							startCommand = template?.startCommand || 'yarn start';
 | 
				
			||||||
 | 
						if (!buildCommand && buildPack !== 'static' && buildPack !== 'laravel')
 | 
				
			||||||
 | 
							buildCommand = template?.buildCommand || null;
 | 
				
			||||||
 | 
						if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
 | 
				
			||||||
 | 
						if (baseDirectory) {
 | 
				
			||||||
 | 
							if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
 | 
				
			||||||
 | 
							if (baseDirectory.endsWith('/') && baseDirectory !== '/')
 | 
				
			||||||
 | 
								baseDirectory = baseDirectory.slice(0, -1);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (dockerFileLocation) {
 | 
				
			||||||
 | 
							if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`;
 | 
				
			||||||
 | 
							if (dockerFileLocation.endsWith('/')) dockerFileLocation = dockerFileLocation.slice(0, -1);
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							dockerFileLocation = '/Dockerfile';
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (dockerComposeFileLocation) {
 | 
				
			||||||
 | 
							if (!dockerComposeFileLocation.startsWith('/'))
 | 
				
			||||||
 | 
								dockerComposeFileLocation = `/${dockerComposeFileLocation}`;
 | 
				
			||||||
 | 
							if (dockerComposeFileLocation.endsWith('/'))
 | 
				
			||||||
 | 
								dockerComposeFileLocation = dockerComposeFileLocation.slice(0, -1);
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							dockerComposeFileLocation = '/Dockerfile';
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (!denoMainFile) {
 | 
				
			||||||
 | 
							denoMainFile = 'main.ts';
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							buildPack,
 | 
				
			||||||
 | 
							port,
 | 
				
			||||||
 | 
							installCommand,
 | 
				
			||||||
 | 
							startCommand,
 | 
				
			||||||
 | 
							buildCommand,
 | 
				
			||||||
 | 
							publishDirectory,
 | 
				
			||||||
 | 
							baseDirectory,
 | 
				
			||||||
 | 
							dockerFileLocation,
 | 
				
			||||||
 | 
							dockerComposeFileLocation,
 | 
				
			||||||
 | 
							denoMainFile
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const scanningTemplates = {
 | 
				
			||||||
 | 
						'@sveltejs/kit': {
 | 
				
			||||||
 | 
							buildPack: 'nodejs'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						astro: {
 | 
				
			||||||
 | 
							buildPack: 'astro'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						'@11ty/eleventy': {
 | 
				
			||||||
 | 
							buildPack: 'eleventy'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						svelte: {
 | 
				
			||||||
 | 
							buildPack: 'svelte'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						'@nestjs/core': {
 | 
				
			||||||
 | 
							buildPack: 'nestjs'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						next: {
 | 
				
			||||||
 | 
							buildPack: 'nextjs'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						nuxt: {
 | 
				
			||||||
 | 
							buildPack: 'nuxtjs'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						'react-scripts': {
 | 
				
			||||||
 | 
							buildPack: 'react'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						'parcel-bundler': {
 | 
				
			||||||
 | 
							buildPack: 'static'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						'@vue/cli-service': {
 | 
				
			||||||
 | 
							buildPack: 'vuejs'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						vuejs: {
 | 
				
			||||||
 | 
							buildPack: 'vuejs'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						gatsby: {
 | 
				
			||||||
 | 
							buildPack: 'gatsby'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						'preact-cli': {
 | 
				
			||||||
 | 
							buildPack: 'react'
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@ export function createContext({ req }: CreateFastifyContextOptions) {
 | 
				
			|||||||
	if (token) {
 | 
						if (token) {
 | 
				
			||||||
		user = jwt.verify(token, env.COOLIFY_SECRET_KEY) as User;
 | 
							user = jwt.verify(token, env.COOLIFY_SECRET_KEY) as User;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return { user };
 | 
						return { user, hostname: req.hostname };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type Context = inferAsyncReturnType<typeof createContext>;
 | 
					export type Context = inferAsyncReturnType<typeof createContext>;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,11 +10,306 @@ import {
 | 
				
			|||||||
	formatLabelsOnDocker,
 | 
						formatLabelsOnDocker,
 | 
				
			||||||
	removeContainer
 | 
						removeContainer
 | 
				
			||||||
} from '../../../lib/docker';
 | 
					} from '../../../lib/docker';
 | 
				
			||||||
import { deployApplication, generateConfigHash, getApplicationFromDB } from './lib';
 | 
					import {
 | 
				
			||||||
 | 
						deployApplication,
 | 
				
			||||||
 | 
						generateConfigHash,
 | 
				
			||||||
 | 
						getApplicationFromDB,
 | 
				
			||||||
 | 
						setDefaultBaseImage
 | 
				
			||||||
 | 
					} from './lib';
 | 
				
			||||||
import cuid from 'cuid';
 | 
					import cuid from 'cuid';
 | 
				
			||||||
import { createDirectories, saveDockerRegistryCredentials } from '../../../lib/common';
 | 
					import {
 | 
				
			||||||
 | 
						checkDomainsIsValidInDNS,
 | 
				
			||||||
 | 
						checkExposedPort,
 | 
				
			||||||
 | 
						createDirectories,
 | 
				
			||||||
 | 
						decrypt,
 | 
				
			||||||
 | 
						encrypt,
 | 
				
			||||||
 | 
						getDomain,
 | 
				
			||||||
 | 
						isDev,
 | 
				
			||||||
 | 
						isDomainConfigured,
 | 
				
			||||||
 | 
						saveDockerRegistryCredentials,
 | 
				
			||||||
 | 
						setDefaultConfiguration
 | 
				
			||||||
 | 
					} from '../../../lib/common';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const applicationsRouter = router({
 | 
					export const applicationsRouter = router({
 | 
				
			||||||
 | 
						getStorages: privateProcedure
 | 
				
			||||||
 | 
							.input(
 | 
				
			||||||
 | 
								z.object({
 | 
				
			||||||
 | 
									id: z.string()
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.query(async ({ input }) => {
 | 
				
			||||||
 | 
								const { id } = input;
 | 
				
			||||||
 | 
								const persistentStorages = await prisma.applicationPersistentStorage.findMany({
 | 
				
			||||||
 | 
									where: { applicationId: id }
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									success: true,
 | 
				
			||||||
 | 
									data: {
 | 
				
			||||||
 | 
										persistentStorages
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
						deleteStorage: privateProcedure
 | 
				
			||||||
 | 
							.input(
 | 
				
			||||||
 | 
								z.object({
 | 
				
			||||||
 | 
									id: z.string(),
 | 
				
			||||||
 | 
									path: z.string()
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.mutation(async ({ input }) => {
 | 
				
			||||||
 | 
								const { id, path } = input;
 | 
				
			||||||
 | 
								await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id, path } });
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
						updateStorage: privateProcedure
 | 
				
			||||||
 | 
							.input(
 | 
				
			||||||
 | 
								z.object({
 | 
				
			||||||
 | 
									id: z.string(),
 | 
				
			||||||
 | 
									path: z.string(),
 | 
				
			||||||
 | 
									storageId: z.string(),
 | 
				
			||||||
 | 
									newStorage: z.boolean().optional().default(false)
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.mutation(async ({ input }) => {
 | 
				
			||||||
 | 
								const { id, path, newStorage, storageId } = input;
 | 
				
			||||||
 | 
								if (newStorage) {
 | 
				
			||||||
 | 
									await prisma.applicationPersistentStorage.create({
 | 
				
			||||||
 | 
										data: { path, application: { connect: { id } } }
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									await prisma.applicationPersistentStorage.update({
 | 
				
			||||||
 | 
										where: { id: storageId },
 | 
				
			||||||
 | 
										data: { path }
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
						deleteSecret: privateProcedure
 | 
				
			||||||
 | 
							.input(
 | 
				
			||||||
 | 
								z.object({
 | 
				
			||||||
 | 
									id: z.string(),
 | 
				
			||||||
 | 
									name: z.string()
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.mutation(async ({ input }) => {
 | 
				
			||||||
 | 
								const { id, name } = input;
 | 
				
			||||||
 | 
								await prisma.secret.deleteMany({ where: { applicationId: id, name } });
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
						updateSecret: privateProcedure
 | 
				
			||||||
 | 
							.input(
 | 
				
			||||||
 | 
								z.object({
 | 
				
			||||||
 | 
									id: z.string(),
 | 
				
			||||||
 | 
									name: z.string(),
 | 
				
			||||||
 | 
									value: z.string(),
 | 
				
			||||||
 | 
									isBuildSecret: z.boolean().optional().default(false),
 | 
				
			||||||
 | 
									isPreview: z.boolean().optional().default(false)
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.mutation(async ({ input }) => {
 | 
				
			||||||
 | 
								const { id, name, value, isBuildSecret, isPreview } = input;
 | 
				
			||||||
 | 
								console.log({ isBuildSecret });
 | 
				
			||||||
 | 
								await prisma.secret.updateMany({
 | 
				
			||||||
 | 
									where: { applicationId: id, name, isPRMRSecret: isPreview },
 | 
				
			||||||
 | 
									data: { value: encrypt(value.trim()), isBuildSecret }
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
						newSecret: privateProcedure
 | 
				
			||||||
 | 
							.input(
 | 
				
			||||||
 | 
								z.object({
 | 
				
			||||||
 | 
									id: z.string(),
 | 
				
			||||||
 | 
									name: z.string(),
 | 
				
			||||||
 | 
									value: z.string(),
 | 
				
			||||||
 | 
									isBuildSecret: z.boolean().optional().default(false)
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.mutation(async ({ input }) => {
 | 
				
			||||||
 | 
								const { id, name, value, isBuildSecret } = input;
 | 
				
			||||||
 | 
								const found = await prisma.secret.findMany({ where: { applicationId: id, name } });
 | 
				
			||||||
 | 
								if (found.length > 0) {
 | 
				
			||||||
 | 
									throw { message: 'Secret already exists.' };
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								await prisma.secret.create({
 | 
				
			||||||
 | 
									data: {
 | 
				
			||||||
 | 
										name,
 | 
				
			||||||
 | 
										value: encrypt(value.trim()),
 | 
				
			||||||
 | 
										isBuildSecret,
 | 
				
			||||||
 | 
										isPRMRSecret: false,
 | 
				
			||||||
 | 
										application: { connect: { id } }
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								await prisma.secret.create({
 | 
				
			||||||
 | 
									data: {
 | 
				
			||||||
 | 
										name,
 | 
				
			||||||
 | 
										value: encrypt(value.trim()),
 | 
				
			||||||
 | 
										isBuildSecret,
 | 
				
			||||||
 | 
										isPRMRSecret: true,
 | 
				
			||||||
 | 
										application: { connect: { id } }
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
						getSecrets: privateProcedure
 | 
				
			||||||
 | 
							.input(
 | 
				
			||||||
 | 
								z.object({
 | 
				
			||||||
 | 
									id: z.string()
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.query(async ({ input }) => {
 | 
				
			||||||
 | 
								const { id } = input;
 | 
				
			||||||
 | 
								let secrets = await prisma.secret.findMany({
 | 
				
			||||||
 | 
									where: { applicationId: id, isPRMRSecret: false },
 | 
				
			||||||
 | 
									orderBy: { createdAt: 'asc' }
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								let previewSecrets = await prisma.secret.findMany({
 | 
				
			||||||
 | 
									where: { applicationId: id, isPRMRSecret: true },
 | 
				
			||||||
 | 
									orderBy: { createdAt: 'asc' }
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								secrets = secrets.map((secret) => {
 | 
				
			||||||
 | 
									secret.value = decrypt(secret.value);
 | 
				
			||||||
 | 
									return secret;
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								previewSecrets = previewSecrets.map((secret) => {
 | 
				
			||||||
 | 
									secret.value = decrypt(secret.value);
 | 
				
			||||||
 | 
									return secret;
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									success: true,
 | 
				
			||||||
 | 
									data: {
 | 
				
			||||||
 | 
										previewSecrets: previewSecrets.sort((a, b) => {
 | 
				
			||||||
 | 
											return ('' + a.name).localeCompare(b.name);
 | 
				
			||||||
 | 
										}),
 | 
				
			||||||
 | 
										secrets: secrets.sort((a, b) => {
 | 
				
			||||||
 | 
											return ('' + a.name).localeCompare(b.name);
 | 
				
			||||||
 | 
										})
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
						checkDomain: privateProcedure
 | 
				
			||||||
 | 
							.input(
 | 
				
			||||||
 | 
								z.object({
 | 
				
			||||||
 | 
									id: z.string(),
 | 
				
			||||||
 | 
									domain: z.string()
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.query(async ({ input, ctx }) => {
 | 
				
			||||||
 | 
								const { id, domain } = input;
 | 
				
			||||||
 | 
								const {
 | 
				
			||||||
 | 
									fqdn,
 | 
				
			||||||
 | 
									settings: { dualCerts }
 | 
				
			||||||
 | 
								} = await prisma.application.findUnique({ where: { id }, include: { settings: true } });
 | 
				
			||||||
 | 
								return await checkDomainsIsValidInDNS({ hostname: domain, fqdn, dualCerts });
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						checkDNS: privateProcedure
 | 
				
			||||||
 | 
							.input(
 | 
				
			||||||
 | 
								z.object({
 | 
				
			||||||
 | 
									id: z.string(),
 | 
				
			||||||
 | 
									fqdn: z.string(),
 | 
				
			||||||
 | 
									forceSave: z.boolean(),
 | 
				
			||||||
 | 
									dualCerts: z.boolean(),
 | 
				
			||||||
 | 
									exposePort: z.number().nullable().optional()
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.mutation(async ({ input, ctx }) => {
 | 
				
			||||||
 | 
								let { id, exposePort, fqdn, forceSave, dualCerts } = input;
 | 
				
			||||||
 | 
								if (!fqdn) {
 | 
				
			||||||
 | 
									return {};
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									fqdn = fqdn.toLowerCase();
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (exposePort) exposePort = Number(exposePort);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const {
 | 
				
			||||||
 | 
									destinationDocker: { engine, remoteIpAddress, remoteEngine },
 | 
				
			||||||
 | 
									exposePort: configuredPort
 | 
				
			||||||
 | 
								} = await prisma.application.findUnique({
 | 
				
			||||||
 | 
									where: { id },
 | 
				
			||||||
 | 
									include: { destinationDocker: true }
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const found = await isDomainConfigured({ id, fqdn, remoteIpAddress });
 | 
				
			||||||
 | 
								if (found) {
 | 
				
			||||||
 | 
									throw {
 | 
				
			||||||
 | 
										status: 500,
 | 
				
			||||||
 | 
										message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!`
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (exposePort)
 | 
				
			||||||
 | 
									await checkExposedPort({
 | 
				
			||||||
 | 
										id,
 | 
				
			||||||
 | 
										configuredPort,
 | 
				
			||||||
 | 
										exposePort,
 | 
				
			||||||
 | 
										engine,
 | 
				
			||||||
 | 
										remoteEngine,
 | 
				
			||||||
 | 
										remoteIpAddress
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								if (isDNSCheckEnabled && !isDev && !forceSave) {
 | 
				
			||||||
 | 
									let hostname = ctx.hostname.split(':')[0];
 | 
				
			||||||
 | 
									if (remoteEngine) hostname = remoteIpAddress;
 | 
				
			||||||
 | 
									return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
						saveSettings: privateProcedure
 | 
				
			||||||
 | 
							.input(
 | 
				
			||||||
 | 
								z.object({
 | 
				
			||||||
 | 
									id: z.string(),
 | 
				
			||||||
 | 
									previews: z.boolean().optional(),
 | 
				
			||||||
 | 
									debug: z.boolean().optional(),
 | 
				
			||||||
 | 
									dualCerts: z.boolean().optional(),
 | 
				
			||||||
 | 
									isBot: z.boolean().optional(),
 | 
				
			||||||
 | 
									autodeploy: z.boolean().optional(),
 | 
				
			||||||
 | 
									isDBBranching: z.boolean().optional(),
 | 
				
			||||||
 | 
									isCustomSSL: z.boolean().optional()
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.mutation(async ({ ctx, input }) => {
 | 
				
			||||||
 | 
								const { id, debug, previews, dualCerts, autodeploy, isBot, isDBBranching, isCustomSSL } =
 | 
				
			||||||
 | 
									input;
 | 
				
			||||||
 | 
								await prisma.application.update({
 | 
				
			||||||
 | 
									where: { id },
 | 
				
			||||||
 | 
									data: {
 | 
				
			||||||
 | 
										fqdn: isBot ? null : undefined,
 | 
				
			||||||
 | 
										settings: {
 | 
				
			||||||
 | 
											update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching, isCustomSSL }
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									include: { destinationDocker: true }
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
						getImages: privateProcedure
 | 
				
			||||||
 | 
							.input(z.object({ buildPack: z.string(), deploymentType: z.string().nullable() }))
 | 
				
			||||||
 | 
							.query(async ({ ctx, input }) => {
 | 
				
			||||||
 | 
								const { buildPack, deploymentType } = input;
 | 
				
			||||||
 | 
								let publishDirectory = undefined;
 | 
				
			||||||
 | 
								let port = undefined;
 | 
				
			||||||
 | 
								const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage(
 | 
				
			||||||
 | 
									buildPack,
 | 
				
			||||||
 | 
									deploymentType
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								if (buildPack === 'nextjs') {
 | 
				
			||||||
 | 
									if (deploymentType === 'static') {
 | 
				
			||||||
 | 
										publishDirectory = 'out';
 | 
				
			||||||
 | 
										port = '80';
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										publishDirectory = '';
 | 
				
			||||||
 | 
										port = '3000';
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (buildPack === 'nuxtjs') {
 | 
				
			||||||
 | 
									if (deploymentType === 'static') {
 | 
				
			||||||
 | 
										publishDirectory = 'dist';
 | 
				
			||||||
 | 
										port = '80';
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										publishDirectory = '';
 | 
				
			||||||
 | 
										port = '3000';
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									success: true,
 | 
				
			||||||
 | 
									data: { baseImage, baseImages, baseBuildImage, baseBuildImages, publishDirectory, port }
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
	getApplicationById: privateProcedure
 | 
						getApplicationById: privateProcedure
 | 
				
			||||||
		.input(z.object({ id: z.string() }))
 | 
							.input(z.object({ id: z.string() }))
 | 
				
			||||||
		.query(async ({ ctx, input }) => {
 | 
							.query(async ({ ctx, input }) => {
 | 
				
			||||||
@@ -32,17 +327,140 @@ export const applicationsRouter = router({
 | 
				
			|||||||
	save: privateProcedure
 | 
						save: privateProcedure
 | 
				
			||||||
		.input(
 | 
							.input(
 | 
				
			||||||
			z.object({
 | 
								z.object({
 | 
				
			||||||
				id: z.string()
 | 
									id: z.string(),
 | 
				
			||||||
 | 
									name: z.string(),
 | 
				
			||||||
 | 
									buildPack: z.string(),
 | 
				
			||||||
 | 
									fqdn: z.string().nullable().optional(),
 | 
				
			||||||
 | 
									port: z.number(),
 | 
				
			||||||
 | 
									exposePort: z.number().nullable().optional(),
 | 
				
			||||||
 | 
									installCommand: z.string(),
 | 
				
			||||||
 | 
									buildCommand: z.string(),
 | 
				
			||||||
 | 
									startCommand: z.string(),
 | 
				
			||||||
 | 
									baseDirectory: z.string().nullable().optional(),
 | 
				
			||||||
 | 
									publishDirectory: z.string().nullable().optional(),
 | 
				
			||||||
 | 
									pythonWSGI: z.string().nullable().optional(),
 | 
				
			||||||
 | 
									pythonModule: z.string().nullable().optional(),
 | 
				
			||||||
 | 
									pythonVariable: z.string().nullable().optional(),
 | 
				
			||||||
 | 
									dockerFileLocation: z.string(),
 | 
				
			||||||
 | 
									denoMainFile: z.string().nullable().optional(),
 | 
				
			||||||
 | 
									denoOptions: z.string().nullable().optional(),
 | 
				
			||||||
 | 
									gitCommitHash: z.string(),
 | 
				
			||||||
 | 
									baseImage: z.string(),
 | 
				
			||||||
 | 
									baseBuildImage: z.string(),
 | 
				
			||||||
 | 
									deploymentType: z.string().nullable().optional(),
 | 
				
			||||||
 | 
									baseDatabaseBranch: z.string().nullable().optional(),
 | 
				
			||||||
 | 
									dockerComposeFile: z.string().nullable().optional(),
 | 
				
			||||||
 | 
									dockerComposeFileLocation: z.string().nullable().optional(),
 | 
				
			||||||
 | 
									dockerComposeConfiguration: z.string().nullable().optional(),
 | 
				
			||||||
 | 
									simpleDockerfile: z.string().nullable().optional(),
 | 
				
			||||||
 | 
									dockerRegistryImageName: z.string().nullable().optional()
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
		.mutation(async ({ ctx, input }) => {
 | 
							.mutation(async ({ input }) => {
 | 
				
			||||||
			const { id } = input;
 | 
								let {
 | 
				
			||||||
			const teamId = ctx.user?.teamId;
 | 
									id,
 | 
				
			||||||
 | 
									name,
 | 
				
			||||||
			// const buildId = await deployApplication(id, teamId);
 | 
									buildPack,
 | 
				
			||||||
			return {
 | 
									fqdn,
 | 
				
			||||||
				// buildId
 | 
									port,
 | 
				
			||||||
			};
 | 
									exposePort,
 | 
				
			||||||
 | 
									installCommand,
 | 
				
			||||||
 | 
									buildCommand,
 | 
				
			||||||
 | 
									startCommand,
 | 
				
			||||||
 | 
									baseDirectory,
 | 
				
			||||||
 | 
									publishDirectory,
 | 
				
			||||||
 | 
									pythonWSGI,
 | 
				
			||||||
 | 
									pythonModule,
 | 
				
			||||||
 | 
									pythonVariable,
 | 
				
			||||||
 | 
									dockerFileLocation,
 | 
				
			||||||
 | 
									denoMainFile,
 | 
				
			||||||
 | 
									denoOptions,
 | 
				
			||||||
 | 
									gitCommitHash,
 | 
				
			||||||
 | 
									baseImage,
 | 
				
			||||||
 | 
									baseBuildImage,
 | 
				
			||||||
 | 
									deploymentType,
 | 
				
			||||||
 | 
									baseDatabaseBranch,
 | 
				
			||||||
 | 
									dockerComposeFile,
 | 
				
			||||||
 | 
									dockerComposeFileLocation,
 | 
				
			||||||
 | 
									dockerComposeConfiguration,
 | 
				
			||||||
 | 
									simpleDockerfile,
 | 
				
			||||||
 | 
									dockerRegistryImageName
 | 
				
			||||||
 | 
								} = input;
 | 
				
			||||||
 | 
								const {
 | 
				
			||||||
 | 
									destinationDocker: { engine, remoteEngine, remoteIpAddress },
 | 
				
			||||||
 | 
									exposePort: configuredPort
 | 
				
			||||||
 | 
								} = await prisma.application.findUnique({
 | 
				
			||||||
 | 
									where: { id },
 | 
				
			||||||
 | 
									include: { destinationDocker: true }
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								if (exposePort)
 | 
				
			||||||
 | 
									await checkExposedPort({
 | 
				
			||||||
 | 
										id,
 | 
				
			||||||
 | 
										configuredPort,
 | 
				
			||||||
 | 
										exposePort,
 | 
				
			||||||
 | 
										engine,
 | 
				
			||||||
 | 
										remoteEngine,
 | 
				
			||||||
 | 
										remoteIpAddress
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								if (denoOptions) denoOptions = denoOptions.trim();
 | 
				
			||||||
 | 
								const defaultConfiguration = await setDefaultConfiguration({
 | 
				
			||||||
 | 
									buildPack,
 | 
				
			||||||
 | 
									port,
 | 
				
			||||||
 | 
									installCommand,
 | 
				
			||||||
 | 
									startCommand,
 | 
				
			||||||
 | 
									buildCommand,
 | 
				
			||||||
 | 
									publishDirectory,
 | 
				
			||||||
 | 
									baseDirectory,
 | 
				
			||||||
 | 
									dockerFileLocation,
 | 
				
			||||||
 | 
									dockerComposeFileLocation,
 | 
				
			||||||
 | 
									denoMainFile
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								if (baseDatabaseBranch) {
 | 
				
			||||||
 | 
									await prisma.application.update({
 | 
				
			||||||
 | 
										where: { id },
 | 
				
			||||||
 | 
										data: {
 | 
				
			||||||
 | 
											name,
 | 
				
			||||||
 | 
											fqdn,
 | 
				
			||||||
 | 
											exposePort,
 | 
				
			||||||
 | 
											pythonWSGI,
 | 
				
			||||||
 | 
											pythonModule,
 | 
				
			||||||
 | 
											pythonVariable,
 | 
				
			||||||
 | 
											denoOptions,
 | 
				
			||||||
 | 
											baseImage,
 | 
				
			||||||
 | 
											gitCommitHash,
 | 
				
			||||||
 | 
											baseBuildImage,
 | 
				
			||||||
 | 
											deploymentType,
 | 
				
			||||||
 | 
											dockerComposeFile,
 | 
				
			||||||
 | 
											dockerComposeConfiguration,
 | 
				
			||||||
 | 
											simpleDockerfile,
 | 
				
			||||||
 | 
											dockerRegistryImageName,
 | 
				
			||||||
 | 
											...defaultConfiguration,
 | 
				
			||||||
 | 
											connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } }
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									await prisma.application.update({
 | 
				
			||||||
 | 
										where: { id },
 | 
				
			||||||
 | 
										data: {
 | 
				
			||||||
 | 
											name,
 | 
				
			||||||
 | 
											fqdn,
 | 
				
			||||||
 | 
											exposePort,
 | 
				
			||||||
 | 
											pythonWSGI,
 | 
				
			||||||
 | 
											pythonModule,
 | 
				
			||||||
 | 
											gitCommitHash,
 | 
				
			||||||
 | 
											pythonVariable,
 | 
				
			||||||
 | 
											denoOptions,
 | 
				
			||||||
 | 
											baseImage,
 | 
				
			||||||
 | 
											baseBuildImage,
 | 
				
			||||||
 | 
											deploymentType,
 | 
				
			||||||
 | 
											dockerComposeFile,
 | 
				
			||||||
 | 
											dockerComposeConfiguration,
 | 
				
			||||||
 | 
											simpleDockerfile,
 | 
				
			||||||
 | 
											dockerRegistryImageName,
 | 
				
			||||||
 | 
											...defaultConfiguration
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}),
 | 
							}),
 | 
				
			||||||
	status: privateProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
 | 
						status: privateProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
 | 
				
			||||||
		const id: string = input.id;
 | 
							const id: string = input.id;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "coolify",
 | 
					  "name": "coolify",
 | 
				
			||||||
  "description": "An open-source & self-hostable Heroku / Netlify alternative.",
 | 
					  "description": "An open-source & self-hostable Heroku / Netlify alternative.",
 | 
				
			||||||
  "version": "3.12.3",
 | 
					  "version": "3.12.4",
 | 
				
			||||||
  "license": "Apache-2.0",
 | 
					  "license": "Apache-2.0",
 | 
				
			||||||
  "repository": "github:coollabsio/coolify",
 | 
					  "repository": "github:coollabsio/coolify",
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -171,6 +171,8 @@ importers:
 | 
				
			|||||||
      eslint-plugin-svelte3: 4.0.0
 | 
					      eslint-plugin-svelte3: 4.0.0
 | 
				
			||||||
      flowbite-svelte: 0.28.0
 | 
					      flowbite-svelte: 0.28.0
 | 
				
			||||||
      js-cookie: 3.0.1
 | 
					      js-cookie: 3.0.1
 | 
				
			||||||
 | 
					      js-yaml: 4.1.0
 | 
				
			||||||
 | 
					      p-limit: 4.0.0
 | 
				
			||||||
      postcss: 8.4.19
 | 
					      postcss: 8.4.19
 | 
				
			||||||
      postcss-load-config: 4.0.1
 | 
					      postcss-load-config: 4.0.1
 | 
				
			||||||
      prettier: 2.8.0
 | 
					      prettier: 2.8.0
 | 
				
			||||||
@@ -180,6 +182,7 @@ importers:
 | 
				
			|||||||
      svelte: 3.53.1
 | 
					      svelte: 3.53.1
 | 
				
			||||||
      svelte-check: 2.9.2
 | 
					      svelte-check: 2.9.2
 | 
				
			||||||
      svelte-preprocess: ^4.10.7
 | 
					      svelte-preprocess: ^4.10.7
 | 
				
			||||||
 | 
					      svelte-select: 4.4.7
 | 
				
			||||||
      tailwindcss: 3.2.4
 | 
					      tailwindcss: 3.2.4
 | 
				
			||||||
      tslib: 2.4.1
 | 
					      tslib: 2.4.1
 | 
				
			||||||
      typescript: 4.9.3
 | 
					      typescript: 4.9.3
 | 
				
			||||||
@@ -191,8 +194,11 @@ importers:
 | 
				
			|||||||
      daisyui: 2.41.0_2lwn2upnx27dqeg6hqdu7sq75m
 | 
					      daisyui: 2.41.0_2lwn2upnx27dqeg6hqdu7sq75m
 | 
				
			||||||
      flowbite-svelte: 0.28.0
 | 
					      flowbite-svelte: 0.28.0
 | 
				
			||||||
      js-cookie: 3.0.1
 | 
					      js-cookie: 3.0.1
 | 
				
			||||||
 | 
					      js-yaml: 4.1.0
 | 
				
			||||||
 | 
					      p-limit: 4.0.0
 | 
				
			||||||
      server: link:../server
 | 
					      server: link:../server
 | 
				
			||||||
      superjson: 1.11.0
 | 
					      superjson: 1.11.0
 | 
				
			||||||
 | 
					      svelte-select: 4.4.7
 | 
				
			||||||
    devDependencies:
 | 
					    devDependencies:
 | 
				
			||||||
      '@playwright/test': 1.28.1
 | 
					      '@playwright/test': 1.28.1
 | 
				
			||||||
      '@sveltejs/adapter-static': 1.0.0-next.48
 | 
					      '@sveltejs/adapter-static': 1.0.0-next.48
 | 
				
			||||||
@@ -255,6 +261,7 @@ importers:
 | 
				
			|||||||
      fastify: 4.10.2
 | 
					      fastify: 4.10.2
 | 
				
			||||||
      fastify-plugin: 4.4.0
 | 
					      fastify-plugin: 4.4.0
 | 
				
			||||||
      got: ^12.5.3
 | 
					      got: ^12.5.3
 | 
				
			||||||
 | 
					      is-ip: 5.0.0
 | 
				
			||||||
      is-port-reachable: 4.0.0
 | 
					      is-port-reachable: 4.0.0
 | 
				
			||||||
      js-yaml: 4.1.0
 | 
					      js-yaml: 4.1.0
 | 
				
			||||||
      jsonwebtoken: 8.5.1
 | 
					      jsonwebtoken: 8.5.1
 | 
				
			||||||
@@ -292,6 +299,7 @@ importers:
 | 
				
			|||||||
      fastify: 4.10.2
 | 
					      fastify: 4.10.2
 | 
				
			||||||
      fastify-plugin: 4.4.0
 | 
					      fastify-plugin: 4.4.0
 | 
				
			||||||
      got: 12.5.3
 | 
					      got: 12.5.3
 | 
				
			||||||
 | 
					      is-ip: 5.0.0
 | 
				
			||||||
      is-port-reachable: 4.0.0
 | 
					      is-port-reachable: 4.0.0
 | 
				
			||||||
      js-yaml: 4.1.0
 | 
					      js-yaml: 4.1.0
 | 
				
			||||||
      jsonwebtoken: 8.5.1
 | 
					      jsonwebtoken: 8.5.1
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user