Merged v2.4.0

This commit is contained in:
dominicbachmann 2022-04-07 01:03:13 +02:00
commit 9da08d600b
63 changed files with 926 additions and 389 deletions

View File

@ -3,3 +3,4 @@ COOLIFY_SECRET_KEY=12341234123412341234123412341234
COOLIFY_DATABASE_URL=file:../db/dev.db COOLIFY_DATABASE_URL=file:../db/dev.db
COOLIFY_SENTRY_DSN= COOLIFY_SENTRY_DSN=
COOLIFY_IS_ON="docker" COOLIFY_IS_ON="docker"
COOLIFY_WHITE_LABELED="false"

View File

@ -11,7 +11,7 @@ WORKDIR /app
LABEL coolify.managed true LABEL coolify.managed true
RUN apk add --no-cache git git-lfs openssh-client curl jq cmake sqlite RUN apk add --no-cache git git-lfs openssh-client curl jq cmake sqlite openssl
RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@6 RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@6
RUN pnpm add -g pnpm RUN pnpm add -g pnpm

View File

@ -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": "2.3.3", "version": "2.4.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev", "dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev",

View File

@ -0,0 +1,29 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Wordpress" (
"id" TEXT NOT NULL PRIMARY KEY,
"extraConfig" TEXT,
"tablePrefix" TEXT,
"mysqlUser" TEXT NOT NULL,
"mysqlPassword" TEXT NOT NULL,
"mysqlRootUser" TEXT NOT NULL,
"mysqlRootUserPassword" TEXT NOT NULL,
"mysqlDatabase" TEXT,
"mysqlPublicPort" INTEGER,
"ftpEnabled" BOOLEAN NOT NULL DEFAULT false,
"ftpUser" TEXT,
"ftpPassword" TEXT,
"ftpPublicPort" INTEGER,
"ftpHostKey" TEXT,
"ftpHostKeyPrivate" TEXT,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Wordpress_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Wordpress" ("createdAt", "extraConfig", "id", "mysqlDatabase", "mysqlPassword", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "serviceId", "tablePrefix", "updatedAt") SELECT "createdAt", "extraConfig", "id", "mysqlDatabase", "mysqlPassword", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "serviceId", "tablePrefix", "updatedAt" FROM "Wordpress";
DROP TABLE "Wordpress";
ALTER TABLE "new_Wordpress" RENAME TO "Wordpress";
CREATE UNIQUE INDEX "Wordpress_serviceId_key" ON "Wordpress"("serviceId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@ -332,6 +332,12 @@ model Wordpress {
mysqlRootUserPassword String mysqlRootUserPassword String
mysqlDatabase String? mysqlDatabase String?
mysqlPublicPort Int? mysqlPublicPort Int?
ftpEnabled Boolean @default(false)
ftpUser String?
ftpPassword String?
ftpPublicPort Int?
ftpHostKey String?
ftpHostKeyPrivate String?
serviceId String @unique serviceId String @unique
service Service @relation(fields: [serviceId], references: [id]) service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())

3
src/app.d.ts vendored
View File

@ -19,14 +19,13 @@ declare namespace App {
} }
interface SessionData { interface SessionData {
whiteLabeled: boolean;
version?: string; version?: string;
userId?: string | null; userId?: string | null;
teamId?: string | null; teamId?: string | null;
permission?: string; permission?: string;
isAdmin?: boolean; isAdmin?: boolean;
expires?: string | null; expires?: string | null;
gitlabToken?: string | null;
ghToken?: string | null;
} }
type DateTimeFormatOptions = { type DateTimeFormatOptions = {

View File

@ -2,7 +2,6 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Coolify</title> <title>Coolify</title>
%svelte.head% %svelte.head%

View File

@ -7,6 +7,8 @@ import { version } from '$lib/common';
import cookie from 'cookie'; import cookie from 'cookie';
import { dev } from '$app/env'; import { dev } from '$app/env';
const whiteLabeled = process.env['COOLIFY_WHITE_LABELED'] === 'true';
export const handle = handleSession( export const handle = handleSession(
{ {
secret: process.env['COOLIFY_SECRET_KEY'], secret: process.env['COOLIFY_SECRET_KEY'],
@ -71,6 +73,7 @@ export const handle = handleSession(
export const getSession: GetSession = function ({ locals }) { export const getSession: GetSession = function ({ locals }) {
return { return {
version, version,
whiteLabeled,
...locals.session.data ...locals.session.data
}; };
}; };

View File

@ -4,6 +4,12 @@ import { promises as fs } from 'fs';
const createDockerfile = async (data, image, htaccessFound): Promise<void> => { const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
const { workdir, baseDirectory } = data; const { workdir, baseDirectory } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
let composerFound = false;
try {
await fs.readFile(`${workdir}${baseDirectory || ''}/composer.json`);
composerFound = true;
} catch (error) {}
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
@ -11,6 +17,10 @@ const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
if (htaccessFound) { if (htaccessFound) {
Dockerfile.push(`COPY .${baseDirectory || ''}/.htaccess ./`); Dockerfile.push(`COPY .${baseDirectory || ''}/.htaccess ./`);
} }
if (composerFound) {
Dockerfile.push(`RUN composer install`);
}
Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`); Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
@ -21,12 +31,14 @@ export default async function (data) {
try { try {
let htaccessFound = false; let htaccessFound = false;
try { try {
const d = await fs.readFile(`${workdir}${baseDirectory || ''}/.htaccess`); await fs.readFile(`${workdir}${baseDirectory || ''}/.htaccess`);
htaccessFound = true; htaccessFound = true;
} catch (e) { } catch (e) {
// //
} }
const image = htaccessFound ? 'webdevops/php-apache' : 'webdevops/php-nginx'; const image = htaccessFound
? 'webdevops/php-apache:8.0-alpine'
: 'webdevops/php-nginx:8.0-alpine';
await createDockerfile(data, image, htaccessFound); await createDockerfile(data, image, htaccessFound);
await buildImage(data); await buildImage(data);
} catch (error) { } catch (error) {

View File

@ -7,6 +7,7 @@
export let isCenter = true; export let isCenter = true;
export let disabled = false; export let disabled = false;
export let dataTooltip = null; export let dataTooltip = null;
export let loading = false;
</script> </script>
<div class="flex items-center py-4 pr-8"> <div class="flex items-center py-4 pr-8">
@ -26,9 +27,10 @@
on:click on:click
aria-pressed="false" aria-pressed="false"
class="relative mx-20 inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out" class="relative mx-20 inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out"
class:opacity-50={disabled} class:opacity-50={disabled || loading}
class:bg-green-600={setting} class:bg-green-600={!loading && setting}
class:bg-stone-700={!setting} class:bg-stone-700={!loading && !setting}
class:bg-yellow-500={loading}
> >
<span class="sr-only">Use setting</span> <span class="sr-only">Use setting</span>
<span <span
@ -40,6 +42,7 @@
class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in" class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
class:opacity-0={setting} class:opacity-0={setting}
class:opacity-100={!setting} class:opacity-100={!setting}
class:animate-spin={loading}
aria-hidden="true" aria-hidden="true"
> >
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12"> <svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
@ -57,6 +60,7 @@
aria-hidden="true" aria-hidden="true"
class:opacity-100={setting} class:opacity-100={setting}
class:opacity-0={!setting} class:opacity-0={!setting}
class:animate-spin={loading}
> >
<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12"> <svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
<path <path

View File

@ -43,3 +43,142 @@ export function changeQueryParams(buildId) {
queryParams.set('buildId', buildId); queryParams.set('buildId', buildId);
return history.pushState(null, null, '?' + queryParams.toString()); return history.pushState(null, null, '?' + queryParams.toString());
} }
export const supportedDatabaseTypesAndVersions = [
{
name: 'mongodb',
fancyName: 'MongoDB',
baseImage: 'bitnami/mongodb',
versions: ['5.0', '4.4', '4.2']
},
{ name: 'mysql', fancyName: 'MySQL', baseImage: 'bitnami/mysql', versions: ['8.0', '5.7'] },
{
name: 'postgresql',
fancyName: 'PostgreSQL',
baseImage: 'bitnami/postgresql',
versions: ['14.2', '13.6', '12.10', '11.15', '10.20']
},
{
name: 'redis',
fancyName: 'Redis',
baseImage: 'bitnami/redis',
versions: ['6.2', '6.0', '5.0']
},
{ name: 'couchdb', fancyName: 'CouchDB', baseImage: 'bitnami/couchdb', versions: ['3.2.1'] }
];
export const supportedServiceTypesAndVersions = [
{
name: 'plausibleanalytics',
fancyName: 'Plausible Analytics',
baseImage: 'plausible/analytics',
images: ['bitnami/postgresql:13.2.0', 'yandex/clickhouse-server:21.3.2.5'],
versions: ['latest', 'stable'],
recommendedVersion: 'stable',
ports: {
main: 8000
}
},
{
name: 'nocodb',
fancyName: 'NocoDB',
baseImage: 'nocodb/nocodb',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
{
name: 'minio',
fancyName: 'MinIO',
baseImage: 'minio/minio',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 9001
}
},
{
name: 'vscodeserver',
fancyName: 'VSCode Server',
baseImage: 'codercom/code-server',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
{
name: 'wordpress',
fancyName: 'Wordpress',
baseImage: 'wordpress',
images: ['bitnami/mysql:5.7'],
versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'],
recommendedVersion: 'latest',
ports: {
main: 80
}
},
{
name: 'vaultwarden',
fancyName: 'Vaultwarden',
baseImage: 'vaultwarden/server',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 80
}
},
{
name: 'languagetool',
fancyName: 'LanguageTool',
baseImage: 'silviof/docker-languagetool',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8010
}
},
{
name: 'n8n',
fancyName: 'n8n',
baseImage: 'n8nio/n8n',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 5678
}
},
{
name: 'uptimekuma',
fancyName: 'Uptime Kuma',
baseImage: 'louislam/uptime-kuma',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 3001
}
},
{
name: 'ghost',
fancyName: 'Ghost',
baseImage: 'bitnami/ghost',
images: ['bitnami/mariadb'],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 2368
}
},
{
name: 'meilisearch',
fancyName: 'Meilisearch',
baseImage: 'getmeili/meilisearch',
images: [],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 7700
}
}
];

View File

@ -14,7 +14,13 @@ import type {
} from '@prisma/client'; } from '@prisma/client';
export async function listApplications(teamId: string): Promise<Application[]> { export async function listApplications(teamId: string): Promise<Application[]> {
return await prisma.application.findMany({ where: { teams: { some: { id: teamId } } } }); if (teamId === '0') {
return await prisma.application.findMany({ include: { teams: true } });
}
return await prisma.application.findMany({
where: { teams: { some: { id: teamId } } },
include: { teams: true }
});
} }
export async function newApplication({ export async function newApplication({
@ -133,13 +139,7 @@ export async function getApplicationWebhook({
} }
} }
export async function getApplication({ export async function getApplication({ id, teamId }: { id: string; teamId: string }): Promise<
id,
teamId
}: {
id: string;
teamId: string;
}): Promise<
Application & { Application & {
destinationDocker: DestinationDocker; destinationDocker: DestinationDocker;
settings: ApplicationSettings; settings: ApplicationSettings;
@ -148,16 +148,30 @@ export async function getApplication({
persistentStorage: ApplicationPersistentStorage[]; persistentStorage: ApplicationPersistentStorage[];
} }
> { > {
const body = await prisma.application.findFirst({ let body;
where: { id, teams: { some: { id: teamId } } }, if (teamId === '0') {
include: { body = await prisma.application.findFirst({
destinationDocker: true, where: { id },
settings: true, include: {
gitSource: { include: { githubApp: true, gitlabApp: true } }, destinationDocker: true,
secrets: true, settings: true,
persistentStorage: true gitSource: { include: { githubApp: true, gitlabApp: true } },
} secrets: true,
}); persistentStorage: true
}
});
} else {
body = await prisma.application.findFirst({
where: { id, teams: { some: { id: teamId } } },
include: {
destinationDocker: true,
settings: true,
gitSource: { include: { githubApp: true, gitlabApp: true } },
secrets: true,
persistentStorage: true
}
});
}
if (body?.gitSource?.githubApp?.clientSecret) { if (body?.gitSource?.githubApp?.clientSecret) {
body.gitSource.githubApp.clientSecret = decrypt(body.gitSource.githubApp.clientSecret); body.gitSource.githubApp.clientSecret = decrypt(body.gitSource.githubApp.clientSecret);

View File

@ -1,5 +1,9 @@
import { dev } from '$app/env'; import { dev } from '$app/env';
import { sentry } from '$lib/common'; import { sentry } from '$lib/common';
import {
supportedDatabaseTypesAndVersions,
supportedServiceTypesAndVersions
} from '$lib/components/common';
import * as Prisma from '@prisma/client'; import * as Prisma from '@prisma/client';
import { default as ProdPrisma } from '@prisma/client'; import { default as ProdPrisma } from '@prisma/client';
import type { Database, DatabaseSettings } from '@prisma/client'; import type { Database, DatabaseSettings } from '@prisma/client';
@ -87,134 +91,6 @@ export async function generateSshKeyPair(): Promise<{ publicKey: string; private
}); });
} }
export const supportedDatabaseTypesAndVersions = [
{
name: 'mongodb',
fancyName: 'MongoDB',
baseImage: 'bitnami/mongodb',
versions: ['5.0.5', '4.4.11', '4.2.18', '4.0.27']
},
{ name: 'mysql', fancyName: 'MySQL', baseImage: 'bitnami/mysql', versions: ['8.0.27', '5.7.36'] },
{
name: 'postgresql',
fancyName: 'PostgreSQL',
baseImage: 'bitnami/postgresql',
versions: ['14.1.0', '13.5.0', '12.9.0', '11.14.0', '10.19.0', '9.6.24']
},
{
name: 'redis',
fancyName: 'Redis',
baseImage: 'bitnami/redis',
versions: ['6.2.6', '6.0.16', '5.0.14']
},
{ name: 'couchdb', fancyName: 'CouchDB', baseImage: 'bitnami/couchdb', versions: ['3.2.1'] }
];
export const supportedServiceTypesAndVersions = [
{
name: 'plausibleanalytics',
fancyName: 'Plausible Analytics',
baseImage: 'plausible/analytics',
images: ['bitnami/postgresql:13.2.0', 'yandex/clickhouse-server:21.3.2.5'],
versions: ['latest'],
ports: {
main: 8000
}
},
{
name: 'nocodb',
fancyName: 'NocoDB',
baseImage: 'nocodb/nocodb',
versions: ['latest'],
ports: {
main: 8080
}
},
{
name: 'minio',
fancyName: 'MinIO',
baseImage: 'minio/minio',
versions: ['latest'],
ports: {
main: 9001
}
},
{
name: 'vscodeserver',
fancyName: 'VSCode Server',
baseImage: 'codercom/code-server',
versions: ['latest'],
ports: {
main: 8080
}
},
{
name: 'wordpress',
fancyName: 'Wordpress',
baseImage: 'wordpress',
images: ['bitnami/mysql:5.7'],
versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'],
ports: {
main: 80
}
},
{
name: 'vaultwarden',
fancyName: 'Vaultwarden',
baseImage: 'vaultwarden/server',
versions: ['latest'],
ports: {
main: 80
}
},
{
name: 'languagetool',
fancyName: 'LanguageTool',
baseImage: 'silviof/docker-languagetool',
versions: ['latest'],
ports: {
main: 8010
}
},
{
name: 'n8n',
fancyName: 'n8n',
baseImage: 'n8nio/n8n',
versions: ['latest'],
ports: {
main: 5678
}
},
{
name: 'uptimekuma',
fancyName: 'Uptime Kuma',
baseImage: 'louislam/uptime-kuma',
versions: ['latest'],
ports: {
main: 3001
}
},
{
name: 'ghost',
fancyName: 'Ghost',
baseImage: 'bitnami/ghost',
images: ['bitnami/mariadb'],
versions: ['latest'],
ports: {
main: 2368
}
},
{
name: 'meilisearch',
fancyName: 'Meilisearch',
baseImage: 'getmeili/meilisearch',
images: [],
versions: ['latest'],
ports: {
main: 7700
}
}
];
export function getVersions(type: string): string[] { export function getVersions(type: string): string[] {
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type); const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) { if (found) {

View File

@ -6,7 +6,14 @@ import { asyncExecShell, getEngine, removeContainer } from '$lib/common';
import type { Database, DatabaseSettings, DestinationDocker } from '@prisma/client'; import type { Database, DatabaseSettings, DestinationDocker } from '@prisma/client';
export async function listDatabases(teamId: string): Promise<Database[]> { export async function listDatabases(teamId: string): Promise<Database[]> {
return await prisma.database.findMany({ where: { teams: { some: { id: teamId } } } }); if (teamId === '0') {
return await prisma.database.findMany({ include: { teams: true } });
} else {
return await prisma.database.findMany({
where: { teams: { some: { id: teamId } } },
include: { teams: true }
});
}
} }
export async function newDatabase({ export async function newDatabase({
@ -43,11 +50,18 @@ export async function getDatabase({
id: string; id: string;
teamId: string; teamId: string;
}): Promise<Database & { destinationDocker: DestinationDocker; settings: DatabaseSettings }> { }): Promise<Database & { destinationDocker: DestinationDocker; settings: DatabaseSettings }> {
const body = await prisma.database.findFirst({ let body;
where: { id, teams: { some: { id: teamId } } }, if (teamId === '0') {
include: { destinationDocker: true, settings: true } body = await prisma.database.findFirst({
}); where: { id },
include: { destinationDocker: true, settings: true }
});
} else {
body = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId } } },
include: { destinationDocker: true, settings: true }
});
}
if (body.dbUserPassword) body.dbUserPassword = decrypt(body.dbUserPassword); if (body.dbUserPassword) body.dbUserPassword = decrypt(body.dbUserPassword);
if (body.rootUserPassword) body.rootUserPassword = decrypt(body.rootUserPassword); if (body.rootUserPassword) body.rootUserPassword = decrypt(body.rootUserPassword);

View File

@ -1,5 +1,4 @@
import { asyncExecShell, getEngine } from '$lib/common'; import { asyncExecShell, getEngine } from '$lib/common';
import { decrypt } from '$lib/crypto';
import { dockerInstance } from '$lib/docker'; import { dockerInstance } from '$lib/docker';
import { startCoolifyProxy } from '$lib/haproxy'; import { startCoolifyProxy } from '$lib/haproxy';
import { getDatabaseImage } from '.'; import { getDatabaseImage } from '.';
@ -18,7 +17,13 @@ type FindDestinationFromTeam = {
}; };
export async function listDestinations(teamId: string): Promise<DestinationDocker[]> { export async function listDestinations(teamId: string): Promise<DestinationDocker[]> {
return await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId } } } }); if (teamId === '0') {
return await prisma.destinationDocker.findMany({ include: { teams: true } });
}
return await prisma.destinationDocker.findMany({
where: { teams: { some: { id: teamId } } },
include: { teams: true }
});
} }
export async function configureDestinationForService({ export async function configureDestinationForService({
@ -146,12 +151,17 @@ export async function getDestination({
id, id,
teamId teamId
}: FindDestinationFromTeam): Promise<DestinationDocker & { sshPrivateKey?: string }> { }: FindDestinationFromTeam): Promise<DestinationDocker & { sshPrivateKey?: string }> {
const destination = (await prisma.destinationDocker.findFirst({ let destination;
where: { id, teams: { some: { id: teamId } } } if (teamId === '0') {
})) as DestinationDocker & { sshPrivateKey?: string }; destination = await prisma.destinationDocker.findFirst({
if (destination.remoteEngine) { where: { id }
destination.sshPrivateKey = decrypt(destination.sshPrivateKey); });
} else {
destination = await prisma.destinationDocker.findFirst({
where: { id, teams: { some: { id: teamId } } }
});
} }
return destination; return destination;
} }
export async function getDestinationByApplicationId({ export async function getDestinationByApplicationId({

View File

@ -5,9 +5,14 @@ import type { GithubApp, GitlabApp, GitSource, Prisma, Application } from '@pris
export async function listSources( export async function listSources(
teamId: string | Prisma.StringFilter teamId: string | Prisma.StringFilter
): Promise<(GitSource & { githubApp?: GithubApp; gitlabApp?: GitlabApp })[]> { ): Promise<(GitSource & { githubApp?: GithubApp; gitlabApp?: GitlabApp })[]> {
if (teamId === '0') {
return await prisma.gitSource.findMany({
include: { githubApp: true, gitlabApp: true, teams: true }
});
}
return await prisma.gitSource.findMany({ return await prisma.gitSource.findMany({
where: { teams: { some: { id: teamId } } }, where: { teams: { some: { id: teamId } } },
include: { githubApp: true, gitlabApp: true } include: { githubApp: true, gitlabApp: true, teams: true }
}); });
} }
@ -54,10 +59,18 @@ export async function getSource({
id: string; id: string;
teamId: string; teamId: string;
}): Promise<GitSource & { githubApp: GithubApp; gitlabApp: GitlabApp }> { }): Promise<GitSource & { githubApp: GithubApp; gitlabApp: GitlabApp }> {
const body = await prisma.gitSource.findFirst({ let body;
where: { id, teams: { some: { id: teamId } } }, if (teamId === '0') {
include: { githubApp: true, gitlabApp: true } body = await prisma.gitSource.findFirst({
}); where: { id },
include: { githubApp: true, gitlabApp: true }
});
} else {
body = await prisma.gitSource.findFirst({
where: { id, teams: { some: { id: teamId } } },
include: { githubApp: true, gitlabApp: true }
});
}
if (body?.githubApp?.clientSecret) if (body?.githubApp?.clientSecret)
body.githubApp.clientSecret = decrypt(body.githubApp.clientSecret); body.githubApp.clientSecret = decrypt(body.githubApp.clientSecret);
if (body?.githubApp?.webhookSecret) if (body?.githubApp?.webhookSecret)

View File

@ -5,7 +5,14 @@ import { generatePassword } from '.';
import { prisma } from './common'; import { prisma } from './common';
export async function listServices(teamId: string): Promise<Service[]> { export async function listServices(teamId: string): Promise<Service[]> {
return await prisma.service.findMany({ where: { teams: { some: { id: teamId } } } }); if (teamId === '0') {
return await prisma.service.findMany({ include: { teams: true } });
} else {
return await prisma.service.findMany({
where: { teams: { some: { id: teamId } } },
include: { teams: true }
});
}
} }
export async function newService({ export async function newService({
@ -19,19 +26,28 @@ export async function newService({
} }
export async function getService({ id, teamId }: { id: string; teamId: string }): Promise<Service> { export async function getService({ id, teamId }: { id: string; teamId: string }): Promise<Service> {
const body = await prisma.service.findFirst({ let body;
where: { id, teams: { some: { id: teamId } } }, const include = {
include: { destinationDocker: true,
destinationDocker: true, plausibleAnalytics: true,
plausibleAnalytics: true, minio: true,
minio: true, vscodeserver: true,
vscodeserver: true, wordpress: true,
wordpress: true, ghost: true,
ghost: true, serviceSecret: true,
serviceSecret: true, meiliSearch: true
meiliSearch: true };
} if (teamId === '0') {
}); body = await prisma.service.findFirst({
where: { id },
include
});
} else {
body = await prisma.service.findFirst({
where: { id, teams: { some: { id: teamId } } },
include
});
}
if (body.plausibleAnalytics?.postgresqlPassword) if (body.plausibleAnalytics?.postgresqlPassword)
body.plausibleAnalytics.postgresqlPassword = decrypt( body.plausibleAnalytics.postgresqlPassword = decrypt(
@ -65,8 +81,12 @@ export async function getService({ id, teamId }: { id: string; teamId: string })
return s; return s;
}); });
} }
if (body.wordpress?.ftpPassword) {
body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
}
const settings = await prisma.setting.findFirst();
return { ...body }; return { ...body, settings };
} }
export async function configureServiceType({ export async function configureServiceType({
@ -191,6 +211,7 @@ export async function configureServiceType({
}); });
} }
} }
export async function setServiceVersion({ export async function setServiceVersion({
id, id,
version version
@ -233,6 +254,7 @@ export async function updatePlausibleAnalyticsService({
await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } }); await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } });
await prisma.service.update({ where: { id }, data: { name, fqdn } }); await prisma.service.update({ where: { id }, data: { name, fqdn } });
} }
export async function updateService({ export async function updateService({
id, id,
fqdn, fqdn,
@ -244,6 +266,7 @@ export async function updateService({
}): Promise<Service> { }): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } }); return await prisma.service.update({ where: { id }, data: { fqdn, name } });
} }
export async function updateLanguageToolService({ export async function updateLanguageToolService({
id, id,
fqdn, fqdn,
@ -255,6 +278,7 @@ export async function updateLanguageToolService({
}): Promise<Service> { }): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } }); return await prisma.service.update({ where: { id }, data: { fqdn, name } });
} }
export async function updateMeiliSearchService({ export async function updateMeiliSearchService({
id, id,
fqdn, fqdn,
@ -266,6 +290,7 @@ export async function updateMeiliSearchService({
}): Promise<Service> { }): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } }); return await prisma.service.update({ where: { id }, data: { fqdn, name } });
} }
export async function updateVaultWardenService({ export async function updateVaultWardenService({
id, id,
fqdn, fqdn,
@ -277,6 +302,7 @@ export async function updateVaultWardenService({
}): Promise<Service> { }): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } }); return await prisma.service.update({ where: { id }, data: { fqdn, name } });
} }
export async function updateVsCodeServer({ export async function updateVsCodeServer({
id, id,
fqdn, fqdn,
@ -288,6 +314,7 @@ export async function updateVsCodeServer({
}): Promise<Service> { }): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } }); return await prisma.service.update({ where: { id }, data: { fqdn, name } });
} }
export async function updateWordpress({ export async function updateWordpress({
id, id,
fqdn, fqdn,
@ -306,6 +333,7 @@ export async function updateWordpress({
data: { fqdn, name, wordpress: { update: { mysqlDatabase, extraConfig } } } data: { fqdn, name, wordpress: { update: { mysqlDatabase, extraConfig } } }
}); });
} }
export async function updateMinioService({ export async function updateMinioService({
id, id,
publicPort publicPort
@ -315,6 +343,7 @@ export async function updateMinioService({
}): Promise<Minio> { }): Promise<Minio> {
return await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } }); return await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } });
} }
export async function updateGhostService({ export async function updateGhostService({
id, id,
fqdn, fqdn,

View File

@ -1,10 +1,9 @@
import { dev } from '$app/env'; import { dev } from '$app/env';
import got, { type Got } from 'got'; import got, { type Got } from 'got';
import mustache from 'mustache';
import crypto from 'crypto';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { checkContainer, checkHAProxy } from '.'; import { checkContainer, checkHAProxy } from '.';
import { asyncExecShell, getDomain, getEngine } from '$lib/common'; import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555'; const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
@ -222,7 +221,7 @@ export async function configureHAProxy(): Promise<void> {
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service; const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;
if (destinationDockerId) { if (destinationDockerId) {
const { engine } = destinationDocker; const { engine } = destinationDocker;
const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type); const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) { if (found) {
const port = found.ports.main; const port = found.ports.main;
const publicPort = service[type]?.publicPort; const publicPort = service[type]?.publicPort;
@ -263,20 +262,36 @@ export async function configureHAProxy(): Promise<void> {
redirectValue, redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain
}); });
} for (const service of services) {
const output = mustache.render(template, data); const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;
const newHash = crypto.createHash('md5').update(output).digest('hex'); if (destinationDockerId) {
const { proxyHash, id } = await db.listSettings(); const { engine } = destinationDocker;
if (proxyHash !== newHash) { const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
await db.prisma.setting.update({ where: { id }, data: { proxyHash: newHash } }); if (found) {
await haproxy.post(`v2/services/haproxy/configuration/raw`, { const port = found.ports.main;
searchParams: { const publicPort = service[type]?.publicPort;
skip_version: true const isRunning = await checkContainer(engine, id);
}, if (fqdn) {
body: output, const domain = getDomain(fqdn);
headers: { const isHttps = fqdn.startsWith('https://');
'Content-Type': 'text/plain' const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
if (isRunning) {
data.services.push({
id,
port,
publicPort,
domain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain,
updatedAt: updatedAt.getTime()
});
}
}
}
} }
}); }
} }
} }

View File

@ -115,12 +115,12 @@ export async function stopTcpHttpProxy(
return error; return error;
} }
} }
export async function startTcpProxy( export async function startTcpProxy(
destinationDocker: DestinationDocker, destinationDocker: DestinationDocker,
id: string, id: string,
publicPort: number, publicPort: number,
privatePort: number privatePort: number,
volume?: string
): Promise<{ stdout: string; stderr: string } | Error> { ): Promise<{ stdout: string; stderr: string } | Error> {
const { network, engine } = destinationDocker; const { network, engine } = destinationDocker;
const host = getEngine(engine); const host = getEngine(engine);
@ -136,7 +136,9 @@ export async function startTcpProxy(
); );
const ip = JSON.parse(Config)[0].Gateway; const ip = JSON.parse(Config)[0].Gateway;
return await asyncExecShell( return await asyncExecShell(
`DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} -d coollabsio/${defaultProxyImageTcp}` `DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} ${
volume ? `-v ${volume}` : ''
} -d coollabsio/${defaultProxyImageTcp}`
); );
} }
} catch (error) { } catch (error) {

View File

@ -4,6 +4,7 @@ import * as db from '$lib/database';
import { dev } from '$app/env'; import { dev } from '$app/env';
import cuid from 'cuid'; import cuid from 'cuid';
import getPort, { portNumbers } from 'get-port'; import getPort, { portNumbers } from 'get-port';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
export async function letsEncrypt(domain: string, id?: string, isCoolify = false): Promise<void> { export async function letsEncrypt(domain: string, id?: string, isCoolify = false): Promise<void> {
try { try {
@ -160,7 +161,7 @@ export async function generateSSLCerts(): Promise<void> {
type, type,
destinationDocker: { engine } destinationDocker: { engine }
} = service; } = service;
const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type); const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) { if (found) {
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');

View File

@ -23,11 +23,9 @@ import yaml from 'js-yaml';
import type { Job } from 'bullmq'; import type { Job } from 'bullmq';
import type { BuilderJob } from '$lib/types/builderJob'; import type { BuilderJob } from '$lib/types/builderJob';
import type { ComposeFile } from '$lib/types/composeFile';
export default async function (job: Job<BuilderJob, void, string>): Promise<void> { export default async function (job: Job<BuilderJob, void, string>): Promise<void> {
/*
Edge cases:
1 - Change build pack and redeploy, what should happen?
*/
const { const {
id: applicationId, id: applicationId,
repository, repository,
@ -276,7 +274,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
} }
}; };
}); });
const compose = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[imageId]: { [imageId]: {
@ -285,7 +283,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
volumes, volumes,
env_file: envFound ? [`${workdir}/.env`] : [], env_file: envFound ? [`${workdir}/.env`] : [],
networks: [docker.network], networks: [docker.network],
labels: labels, labels,
depends_on: [], depends_on: [],
restart: 'always' restart: 'always'
} }
@ -297,7 +295,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
}, },
volumes: Object.assign({}, ...composeVolumes) volumes: Object.assign({}, ...composeVolumes)
}; };
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(compose)); await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST=${host} docker compose --project-directory ${workdir} up -d` `DOCKER_HOST=${host} docker compose --project-directory ${workdir} up -d`
); );

View File

@ -0,0 +1,53 @@
export type ComposeFile = {
version: ComposerFileVersion;
services: Record<string, ComposeFileService>;
networks: Record<string, ComposeFileNetwork>;
volumes?: Record<string, ComposeFileVolume>;
};
export type ComposeFileService = {
container_name: string;
image?: string;
networks: string[];
environment?: Record<string, unknown>;
volumes?: string[];
ulimits?: unknown;
labels?: string[];
env_file?: string[];
extra_hosts?: string[];
restart: ComposeFileRestartOption;
depends_on?: string[];
command?: string;
build?: {
context: string;
dockerfile: string;
args?: Record<string, unknown>;
};
};
export type ComposerFileVersion =
| '3.8'
| '3.7'
| '3.6'
| '3.5'
| '3.4'
| '3.3'
| '3.2'
| '3.1'
| '3.0'
| '2.4'
| '2.3'
| '2.2'
| '2.1'
| '2.0';
export type ComposeFileRestartOption = 'no' | 'always' | 'on-failure' | 'unless-stopped';
export type ComposeFileNetwork = {
external: boolean;
};
export type ComposeFileVolume = {
external?: boolean;
name?: string;
};

View File

@ -134,13 +134,18 @@
<svelte:head> <svelte:head>
<title>Coolify</title> <title>Coolify</title>
{#if !$session.whiteLabeled}
<link rel="icon" href="/favicon.png" />
{/if}
</svelte:head> </svelte:head>
<SvelteToast options={{ intro: { y: -64 }, duration: 3000, pausable: true }} /> <SvelteToast options={{ intro: { y: -64 }, duration: 3000, pausable: true }} />
{#if $session.userId} {#if $session.userId}
<nav class="nav-main"> <nav class="nav-main">
<div class="flex h-screen w-full flex-col items-center transition-all duration-100"> <div class="flex h-screen w-full flex-col items-center transition-all duration-100">
<div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div> {#if !$session.whiteLabeled}
<div class="flex flex-col space-y-4 py-2"> <div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div>
{/if}
<div class="flex flex-col space-y-4 py-2" class:mt-2={$session.whiteLabeled}>
<a <a
sveltekit:prefetch sveltekit:prefetch
href="/" href="/"
@ -312,7 +317,6 @@
<path d="M7 18a4.6 4.4 0 0 1 0 -9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-12" /> <path d="M7 18a4.6 4.4 0 0 1 0 -9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-12" />
</svg> </svg>
</a> </a>
<div class="border-t border-stone-700" />
</div> </div>
<div class="flex-1" /> <div class="flex-1" />
@ -514,6 +518,12 @@
</div> </div>
</div> </div>
</nav> </nav>
{#if $session.whiteLabeled}
<span class="fixed bottom-0 left-[50px] z-50 m-2 px-4 text-xs text-stone-700"
>Powered by <a href="https://coolify.io" target="_blank">Coolify</a></span
>
{/if}
<select <select
class="fixed right-0 bottom-0 z-50 m-2 w-64 bg-opacity-30 p-2 px-4" class="fixed right-0 bottom-0 z-50 m-2 w-64 bg-opacity-30 p-2 px-4"
bind:value={selectedTeamId} bind:value={selectedTeamId}

View File

@ -81,6 +81,9 @@
); );
const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'blob'); const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'blob');
const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'blob'); const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'blob');
const composerPHP = files.find(
(file) => file.name === 'composer.json' && file.type === 'blob'
);
if (yarnLock) packageManager = 'yarn'; if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm'; if (pnpmLock) packageManager = 'pnpm';
@ -103,7 +106,7 @@
foundConfig = findBuildPack('python'); foundConfig = findBuildPack('python');
} else if (indexHtml) { } else if (indexHtml) {
foundConfig = findBuildPack('static', packageManager); foundConfig = findBuildPack('static', packageManager);
} else if (indexPHP) { } else if (indexPHP || composerPHP) {
foundConfig = findBuildPack('php'); foundConfig = findBuildPack('php');
} else { } else {
foundConfig = findBuildPack('node', packageManager); foundConfig = findBuildPack('node', packageManager);
@ -127,6 +130,9 @@
); );
const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'file'); const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'file');
const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'file'); const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'file');
const composerPHP = files.find(
(file) => file.name === 'composer.json' && file.type === 'file'
);
if (yarnLock) packageManager = 'yarn'; if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm'; if (pnpmLock) packageManager = 'pnpm';
@ -146,7 +152,7 @@
foundConfig = findBuildPack('python'); foundConfig = findBuildPack('python');
} else if (indexHtml) { } else if (indexHtml) {
foundConfig = findBuildPack('static', packageManager); foundConfig = findBuildPack('static', packageManager);
} else if (indexPHP) { } else if (indexPHP || composerPHP) {
foundConfig = findBuildPack('php'); foundConfig = findBuildPack('php');
} else { } else {
foundConfig = findBuildPack('node', packageManager); foundConfig = findBuildPack('node', packageManager);

View File

@ -62,7 +62,7 @@
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
{#if !filteredSources || filteredSources.length === 0} {#if !filteredSources || filteredSources.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="pb-2">No configurable Git Source found</div> <div class="pb-2 text-center">No configurable Git Source found</div>
<div class="flex justify-center"> <div class="flex justify-center">
<a href="/new/source" sveltekit:prefetch class="add-icon bg-orange-600 hover:bg-orange-500"> <a href="/new/source" sveltekit:prefetch class="add-icon bg-orange-600 hover:bg-orange-500">
<svg <svg

View File

@ -15,6 +15,7 @@
import Docker from '$lib/components/svg/applications/Docker.svelte'; import Docker from '$lib/components/svg/applications/Docker.svelte';
import Astro from '$lib/components/svg/applications/Astro.svelte'; import Astro from '$lib/components/svg/applications/Astro.svelte';
import Eleventy from '$lib/components/svg/applications/Eleventy.svelte'; import Eleventy from '$lib/components/svg/applications/Eleventy.svelte';
import { session } from '$app/stores';
const buildPack = application?.buildPack?.toLowerCase(); const buildPack = application?.buildPack?.toLowerCase();
</script> </script>
@ -54,6 +55,9 @@
{/if} {/if}
<div class="truncate text-center text-xl font-bold">{application.name}</div> <div class="truncate text-center text-xl font-bold">{application.name}</div>
{#if $session.teamId === '0'}
<div class="truncate text-center">Team {application.teams[0].name}</div>
{/if}
{#if application.fqdn} {#if application.fqdn}
<div class="truncate text-center">{application.fqdn}</div> <div class="truncate text-center">{application.fqdn}</div>
{/if} {/if}

View File

@ -1,6 +1,7 @@
import { getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import { supportedDatabaseTypesAndVersions } from '$lib/components/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler, supportedDatabaseTypesAndVersions } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {

View File

@ -1,6 +1,7 @@
import { getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import { supportedDatabaseTypesAndVersions } from '$lib/components/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler, supportedDatabaseTypesAndVersions } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {

View File

@ -6,6 +6,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { makeLabelForStandaloneDatabase } from '$lib/buildPacks/common'; import { makeLabelForStandaloneDatabase } from '$lib/buildPacks/common';
import { startTcpProxy } from '$lib/haproxy'; import { startTcpProxy } from '$lib/haproxy';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -33,7 +34,7 @@ export const post: RequestHandler = async (event) => {
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -8,6 +8,7 @@
import Redis from '$lib/components/svg/databases/Redis.svelte'; import Redis from '$lib/components/svg/databases/Redis.svelte';
import { post } from '$lib/api'; import { post } from '$lib/api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { session } from '$app/stores';
async function newDatabase() { async function newDatabase() {
const { id } = await post('/databases/new', {}); const { id } = await post('/databases/new', {});
@ -59,6 +60,9 @@
<div class="font-bold text-xl text-center truncate"> <div class="font-bold text-xl text-center truncate">
{database.name} {database.name}
</div> </div>
{#if $session.teamId === '0'}
<div class="text-center truncate">Team {database.teams[0].name}</div>
{/if}
{#if !database.type} {#if !database.type}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white"> <div class="font-bold text-center truncate text-red-500 group-hover:text-white">
Configuration missing Configuration missing

View File

@ -184,41 +184,19 @@
value={destination.network} value={destination.network}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> {#if $session.teamId === '0'}
<Setting <div class="grid grid-cols-2 items-center">
disabled={cannotDisable} <Setting
bind:setting={destination.isCoolifyProxyUsed} disabled={cannotDisable}
on:click={changeProxySetting} bind:setting={destination.isCoolifyProxyUsed}
title="Use Coolify Proxy?" on:click={changeProxySetting}
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${ title="Use Coolify Proxy?"
cannotDisable description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>' cannotDisable
: '' ? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
}`} : ''
/> }`}
</div> />
</form> </div>
<!-- <div class="flex justify-center">
{#if payload.isCoolifyProxyUsed}
{#if state}
<button on:click={stopProxy}>Stop proxy</button>
{:else}
<button on:click={startProxy}>Start proxy</button>
{/if}
{/if} {/if}
</div> --> </form>
<!-- {#if scannedApps.length > 0}
<div class="flex justify-center px-6 pb-10">
<div class="flex space-x-2 h-8 items-center">
<div class="font-bold text-xl text-white">Found applications</div>
</div>
</div>
<div class="max-w-4xl mx-auto px-6">
<div class="flex space-x-2 justify-center">
{#each scannedApps as app}
<FoundApp {app} />
{/each}
</div>
</div>
{/if} -->

View File

@ -8,7 +8,7 @@ import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
console.log(teamId);
const { id } = event.params; const { id } = event.params;
try { try {
const destination = await db.getDestination({ id, teamId }); const destination = await db.getDestination({ id, teamId });

View File

@ -57,6 +57,9 @@
<a href="/destinations/{destination.id}" class="no-underline p-2 w-96"> <a href="/destinations/{destination.id}" class="no-underline p-2 w-96">
<div class="box-selection hover:bg-sky-600"> <div class="box-selection hover:bg-sky-600">
<div class="font-bold text-xl text-center truncate">{destination.name}</div> <div class="font-bold text-xl text-center truncate">{destination.name}</div>
{#if $session.teamId === '0'}
<div class="text-center truncate">Team {destination.teams[0].name}</div>
{/if}
<div class="text-center truncate">{destination.network}</div> <div class="text-center truncate">{destination.network}</div>
</div> </div>
</a> </a>

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { session } from '$app/stores';
export let payload; export let payload;
@ -56,13 +57,15 @@
<label for="network" class="text-base font-bold text-stone-100">Network</label> <label for="network" class="text-base font-bold text-stone-100">Network</label>
<input required name="network" placeholder="default: coolify" bind:value={payload.network} /> <input required name="network" placeholder="default: coolify" bind:value={payload.network} />
</div> </div>
<div class="grid grid-cols-2 items-center"> {#if $session.teamId === '0'}
<Setting <div class="grid grid-cols-2 items-center">
bind:setting={payload.isCoolifyProxyUsed} <Setting
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)} bind:setting={payload.isCoolifyProxyUsed}
title="Use Coolify Proxy?" on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
description="This will install a proxy on the destination to allow you to access your applications and services without any manual configuration (recommended for Docker).<br><br>Databases will have their own proxy." title="Use Coolify Proxy?"
/> description="This will install a proxy on the destination to allow you to access your applications and services without any manual configuration (recommended for Docker).<br><br>Databases will have their own proxy."
</div> />
</div>
{/if}
</form> </form>
</div> </div>

View File

@ -2,6 +2,7 @@
export let service; export let service;
export let isRunning; export let isRunning;
export let readOnly; export let readOnly;
export let settings;
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { post } from '$lib/api'; import { post } from '$lib/api';
@ -91,7 +92,22 @@
/> />
</div> </div>
</div> </div>
<div class="grid grid-cols-2 items-center px-10">
<label for="buildPack" class="text-base font-bold text-stone-100">Version / Tag</label>
<a
href={$session.isAdmin
? `/services/${id}/configuration/version?from=/services/${id}`
: ''}
class="no-underline"
>
<input
value={service.version}
id="service"
disabled
class="cursor-pointer hover:bg-coolgray-500"
/></a
>
</div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="destination" class="text-base font-bold text-stone-100">Destination</label> <label for="destination" class="text-base font-bold text-stone-100">Destination</label>
<div> <div>
@ -143,7 +159,7 @@
{:else if service.type === 'vscodeserver'} {:else if service.type === 'vscodeserver'}
<VsCodeServer {service} /> <VsCodeServer {service} />
{:else if service.type === 'wordpress'} {:else if service.type === 'wordpress'}
<Wordpress bind:service {isRunning} {readOnly} /> <Wordpress bind:service {isRunning} {readOnly} {settings} />
{:else if service.type === 'ghost'} {:else if service.type === 'ghost'}
<Ghost bind:service {readOnly} /> <Ghost bind:service {readOnly} />
{:else if service.type === 'meilisearch'} {:else if service.type === 'meilisearch'}
@ -151,17 +167,4 @@
{/if} {/if}
</div> </div>
</form> </form>
<!-- <div class="font-bold flex space-x-1 pb-5">
<div class="text-xl tracking-tight mr-4">Features</div>
</div>
<div class="px-4 sm:px-6 pb-10">
<ul class="mt-2 divide-y divide-stone-800">
<Setting
bind:setting={isPublic}
on:click={() => changeSettings('isPublic')}
title="Set it public"
description="Your database will be reachable over the internet. <br>Take security seriously in this case!"
/>
</ul>
</div> -->
</div> </div>

View File

@ -1,9 +1,58 @@
<script lang="ts"> <script lang="ts">
import { post } from '$lib/api';
import { page } from '$app/stores';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Setting from '$lib/components/Setting.svelte';
import { errorNotification } from '$lib/form';
import { browser } from '$app/env';
import { getDomain } from '$lib/components/common';
export let service; export let service;
export let isRunning; export let isRunning;
export let readOnly; export let readOnly;
export let settings;
const { id } = $page.params;
let ftpUrl = generateUrl(service.wordpress.ftpPublicPort);
let ftpUser = service.wordpress.ftpUser;
let ftpPassword = service.wordpress.ftpPassword;
let ftpLoading = false;
function generateUrl(publicPort) {
return browser
? `sftp://${
settings.fqdn ? getDomain(settings.fqdn) : window.location.hostname
}:${publicPort}`
: 'Loading...';
}
async function changeSettings(name) {
if (ftpLoading) return;
if (isRunning) {
ftpLoading = true;
let ftpEnabled = service.wordpress.ftpEnabled;
if (name === 'ftpEnabled') {
ftpEnabled = !ftpEnabled;
}
try {
const {
publicPort,
ftpUser: user,
ftpPassword: password
} = await post(`/services/${id}/wordpress/settings.json`, {
ftpEnabled
});
ftpUrl = generateUrl(publicPort);
ftpUser = user;
ftpPassword = password;
service.wordpress.ftpEnabled = ftpEnabled;
} catch ({ error }) {
return errorNotification(error);
} finally {
ftpLoading = false;
}
}
}
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
@ -28,6 +77,30 @@ define('SUBDOMAIN_INSTALL', false);`
: 'N/A'}>{service.wordpress.extraConfig}</textarea : 'N/A'}>{service.wordpress.extraConfig}</textarea
> >
</div> </div>
<div class="grid grid-cols-2 items-center px-10">
<Setting
bind:setting={service.wordpress.ftpEnabled}
loading={ftpLoading}
disabled={!isRunning}
on:click={() => changeSettings('ftpEnabled')}
title="Enable sFTP connection to WordPress data"
description="Enables an on-demand sFTP connection to the WordPress data directory. This is useful if you want to use sFTP to upload files."
/>
</div>
{#if service.wordpress.ftpEnabled}
<div class="grid grid-cols-2 items-center px-10">
<label for="ftpUrl">sFTP Connection URI</label>
<CopyPasswordField id="ftpUrl" readonly disabled name="ftpUrl" value={ftpUrl} />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="ftpUser">User</label>
<CopyPasswordField id="ftpUser" readonly disabled name="ftpUser" value={ftpUser} />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="ftpPassword">Password</label>
<CopyPasswordField id="ftpPassword" readonly disabled name="ftpPassword" value={ftpPassword} />
</div>
{/if}
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MySQL</div> <div class="title">MySQL</div>
</div> </div>

View File

@ -16,7 +16,7 @@
const endpoint = `/services/${params.id}.json`; const endpoint = `/services/${params.id}.json`;
const res = await fetch(endpoint); const res = await fetch(endpoint);
if (res.ok) { if (res.ok) {
const { service, isRunning } = await res.json(); const { service, isRunning, settings } = await res.json();
if (!service || Object.entries(service).length === 0) { if (!service || Object.entries(service).length === 0) {
return { return {
status: 302, status: 302,
@ -45,7 +45,8 @@
stuff: { stuff: {
service, service,
isRunning, isRunning,
readOnly readOnly,
settings
} }
}; };
} }

View File

@ -1,6 +1,7 @@
import { getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler, supportedServiceTypesAndVersions } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {

View File

@ -1,6 +1,7 @@
import { getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler, supportedServiceTypesAndVersions } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
@ -14,6 +15,7 @@ export const get: RequestHandler = async (event) => {
return { return {
status: 200, status: 200,
body: { body: {
type,
versions: supportedServiceTypesAndVersions.find((name) => name.name === type).versions versions: supportedServiceTypesAndVersions.find((name) => name.name === type).versions
} }
}; };

View File

@ -31,11 +31,16 @@
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { post } from '$lib/api'; import { post } from '$lib/api';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
export let versions; export let versions;
export let type;
let recommendedVersion = supportedServiceTypesAndVersions.find(
({ name }) => name === type
)?.recommendedVersion;
async function handleSubmit(version) { async function handleSubmit(version) {
try { try {
await post(`/services/${id}/configuration/version.json`, { version }); await post(`/services/${id}/configuration/version.json`, { version });
@ -49,13 +54,26 @@
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Service version</div> <div class="mr-4 text-2xl tracking-tight">Select a Service version</div>
</div> </div>
{#if from}
<div class="pb-10 text-center">
Warning: you are about to change the version of this service.<br />This could cause problem
after you restart the service,
<span class="font-bold text-pink-600">like losing your data, incompatibility issues, etc</span
>.<br />Only do if you know what you are doing.
</div>
{/if}
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each versions as version} {#each versions as version}
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(version)}> <form on:submit|preventDefault={() => handleSubmit(version)}>
<button type="submit" class="box-selection text-xl font-bold hover:bg-pink-600" <button
>{version}</button type="submit"
class:bg-pink-500={recommendedVersion === version}
class="box-selection relative flex text-xl font-bold hover:bg-pink-600"
>{version}
{#if recommendedVersion === version}
<span class="absolute bottom-0 pb-2 text-xs">recommended</span>
{/if}</button
> >
</form> </form>
</div> </div>

View File

@ -11,6 +11,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -75,7 +76,7 @@ export const post: RequestHandler = async (event) => {
config.ghost.environmentVariables[secret.name] = secret.value; config.ghost.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -17,7 +17,7 @@ export const get: RequestHandler = async (event) => {
const { id } = event.params; const { id } = event.params;
try { try {
const service = await db.getService({ id, teamId }); const service = await db.getService({ id, teamId });
const { destinationDockerId, destinationDocker, type, version } = service; const { destinationDockerId, destinationDocker, type, version, settings } = service;
let isRunning = false; let isRunning = false;
if (destinationDockerId) { if (destinationDockerId) {
@ -46,7 +46,8 @@ export const get: RequestHandler = async (event) => {
return { return {
body: { body: {
isRunning, isRunning,
service service,
settings
} }
}; };
} catch (error) { } catch (error) {

View File

@ -6,7 +6,8 @@
props: { props: {
service: stuff.service, service: stuff.service,
isRunning: stuff.isRunning, isRunning: stuff.isRunning,
readOnly: stuff.readOnly readOnly: stuff.readOnly,
settings: stuff.settings
} }
}; };
} }
@ -37,6 +38,7 @@
export let service; export let service;
export let isRunning; export let isRunning;
export let readOnly; export let readOnly;
export let settings;
if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) { if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) {
service.fqdn = `http://${cuid()}.demo.coolify.io`; service.fqdn = `http://${cuid()}.demo.coolify.io`;
@ -76,4 +78,4 @@
<ServiceLinks {service} /> <ServiceLinks {service} />
</div> </div>
<Services bind:service {isRunning} {readOnly} /> <Services bind:service {isRunning} {readOnly} {settings} />

View File

@ -13,7 +13,7 @@ export const post: RequestHandler = async (event) => {
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
try { try {
await db.updateMeiliSearchService({ id, fqdn, name }); await db.updateService({ id, fqdn, name });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -32,7 +33,7 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; config.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -13,7 +13,7 @@ export const post: RequestHandler = async (event) => {
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
try { try {
await db.updateLanguageToolService({ id, fqdn, name }); await db.updateService({ id, fqdn, name });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -37,7 +38,7 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; config.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -8,6 +8,7 @@ import getPort, { portNumbers } from 'get-port';
import { getDomain } from '$lib/components/common'; import { getDomain } from '$lib/components/common';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -55,7 +56,7 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; config.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -33,7 +34,7 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; config.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -30,7 +31,7 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; config.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -120,7 +121,7 @@ COPY ./init.query /docker-entrypoint-initdb.d/init.query
COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`; COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile);
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -31,7 +32,7 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; config.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -12,7 +12,7 @@ export const post: RequestHandler = async (event) => {
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
try { try {
await db.updateVaultWardenService({ id, fqdn, name }); await db.updateService({ id, fqdn, name });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { getServiceImage, ErrorHandler } from '$lib/database'; import { getServiceImage, ErrorHandler } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -32,7 +33,7 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; config.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -13,7 +13,7 @@ export const post: RequestHandler = async (event) => {
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
try { try {
await db.updateVsCodeServer({ id, fqdn, name }); await db.updateService({ id, fqdn, name });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -41,7 +42,7 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; config.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -0,0 +1,187 @@
import { dev } from '$app/env';
import { asyncExecShell, getEngine, getUserDetails } from '$lib/common';
import { decrypt, encrypt } from '$lib/crypto';
import * as db from '$lib/database';
import { generateDatabaseConfiguration, ErrorHandler, generatePassword } from '$lib/database';
import { checkContainer, startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy';
import type { ComposeFile } from '$lib/types/composeFile';
import type { RequestHandler } from '@sveltejs/kit';
import cuid from 'cuid';
import fs from 'fs/promises';
import getPort, { portNumbers } from 'get-port';
import yaml from 'js-yaml';
export const post: RequestHandler = async (event) => {
const { status, body, teamId } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
const { ftpEnabled } = await event.request.json();
const publicPort = await getPort({ port: portNumbers(minPort, maxPort) });
let ftpUser = cuid();
let ftpPassword = generatePassword();
const hostkeyDir = dev ? '/tmp/hostkeys' : '/app/ssl/hostkeys';
try {
const data = await db.prisma.wordpress.update({
where: { serviceId: id },
data: { ftpEnabled },
include: { service: { include: { destinationDocker: true } } }
});
const {
service: { destinationDockerId, destinationDocker },
ftpPublicPort: oldPublicPort,
ftpUser: user,
ftpPassword: savedPassword,
ftpHostKey,
ftpHostKeyPrivate
} = data;
if (user) ftpUser = user;
if (savedPassword) ftpPassword = decrypt(savedPassword);
const { stdout: password } = await asyncExecShell(
`echo ${ftpPassword} | openssl passwd -1 -stdin`
);
if (destinationDockerId) {
try {
await fs.stat(hostkeyDir);
} catch (error) {
await asyncExecShell(`mkdir -p ${hostkeyDir}`);
}
if (!ftpHostKey) {
await asyncExecShell(
`ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N "" -q -f ${hostkeyDir}/${id}.ed25519`
);
const { stdout: ftpHostKey } = await asyncExecShell(`cat ${hostkeyDir}/${id}.ed25519`);
await db.prisma.wordpress.update({
where: { serviceId: id },
data: { ftpHostKey: encrypt(ftpHostKey) }
});
} else {
await asyncExecShell(`echo "${decrypt(ftpHostKey)}" > ${hostkeyDir}/${id}.ed25519`);
}
if (!ftpHostKeyPrivate) {
await asyncExecShell(`ssh-keygen -t rsa -b 4096 -N "" -f ${hostkeyDir}/${id}.rsa`);
const { stdout: ftpHostKeyPrivate } = await asyncExecShell(`cat ${hostkeyDir}/${id}.rsa`);
await db.prisma.wordpress.update({
where: { serviceId: id },
data: { ftpHostKeyPrivate: encrypt(ftpHostKeyPrivate) }
});
} else {
await asyncExecShell(`echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`);
}
const { network, engine } = destinationDocker;
const host = getEngine(engine);
if (ftpEnabled) {
await db.prisma.wordpress.update({
where: { serviceId: id },
data: {
ftpPublicPort: publicPort,
ftpUser: user ? undefined : ftpUser,
ftpPassword: savedPassword ? undefined : encrypt(ftpPassword)
}
});
try {
const isRunning = await checkContainer(engine, `${id}-ftp`);
if (isRunning) {
await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
);
}
} catch (error) {
console.log(error);
//
}
const volumes = [
`${id}-wordpress-data:/home/${ftpUser}`,
`${
dev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
}/${id}.ed25519:/etc/ssh/ssh_host_ed25519_key`,
`${
dev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
}/${id}.rsa:/etc/ssh/ssh_host_rsa_key`,
`${
dev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
}/${id}.sh:/etc/sftp.d/chmod.sh`
];
const compose: ComposeFile = {
version: '3.8',
services: {
[`${id}-ftp`]: {
image: `atmoz/sftp:alpine`,
command: `'${ftpUser}:${password.replace('\n', '').replace(/\$/g, '$$$')}:e:1001'`,
extra_hosts: ['host.docker.internal:host-gateway'],
container_name: `${id}-ftp`,
volumes,
networks: [network],
depends_on: [],
restart: 'always'
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[`${id}-wordpress-data`]: {
external: true,
name: `${id}-wordpress-data`
}
}
};
await fs.writeFile(
`${hostkeyDir}/${id}.sh`,
`#!/bin/bash\nchmod 600 /etc/ssh/ssh_host_ed25519_key /etc/ssh/ssh_host_rsa_key`
);
await asyncExecShell(`chmod +x ${hostkeyDir}/${id}.sh`);
await fs.writeFile(`${hostkeyDir}/${id}-docker-compose.yml`, yaml.dump(compose));
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d`
);
await startTcpProxy(destinationDocker, `${id}-ftp`, publicPort, 22);
} else {
await db.prisma.wordpress.update({
where: { serviceId: id },
data: { ftpPublicPort: null }
});
try {
await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
);
} catch (error) {
//
}
await stopTcpHttpProxy(destinationDocker, oldPublicPort);
}
}
if (ftpEnabled) {
return {
status: 201,
body: {
publicPort,
ftpUser,
ftpPassword
}
};
} else {
return {
status: 200,
body: {}
};
}
} catch (error) {
console.log(error);
return ErrorHandler(error);
} finally {
await asyncExecShell(
`rm -f ${hostkeyDir}/${id}-docker-compose.yml ${hostkeyDir}/${id}.ed25519 ${hostkeyDir}/${id}.ed25519.pub ${hostkeyDir}/${id}.rsa ${hostkeyDir}/${id}.rsa.pub ${hostkeyDir}/${id}.sh`
);
}
};

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -65,7 +66,7 @@ export const post: RequestHandler = async (event) => {
config.wordpress.environmentVariables[secret.name] = secret.value; config.wordpress.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -12,6 +12,7 @@
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte'; import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
import Ghost from '$lib/components/svg/services/Ghost.svelte'; import Ghost from '$lib/components/svg/services/Ghost.svelte';
import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte'; import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte';
import { session } from '$app/stores';
export let services; export let services;
async function newService() { async function newService() {
@ -74,6 +75,9 @@
<div class="font-bold text-xl text-center truncate"> <div class="font-bold text-xl text-center truncate">
{service.name} {service.name}
</div> </div>
{#if $session.teamId === '0'}
<div class="text-center truncate">Team {service.teams[0].name}</div>
{/if}
{#if !service.type || !service.fqdn} {#if !service.type || !service.fqdn}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white"> <div class="font-bold text-center truncate text-red-500 group-hover:text-white">
Configuration missing Configuration missing

View File

@ -91,93 +91,95 @@
</script> </script>
{#if !source.gitlabApp?.appId} {#if !source.gitlabApp?.appId}
<form class="grid grid-flow-row gap-2 py-4" on:submit|preventDefault={newApp}> <div>
<div class="grid grid-cols-2 items-center"> <form class="grid grid-flow-row gap-2 py-4" on:submit|preventDefault={newApp}>
<label for="type">GitLab Application Type</label>
<select name="type" id="type" class="w-96" bind:value={payload.applicationType}>
<option value="user">User owned application</option>
<option value="group">Group owned application</option>
{#if source.htmlUrl !== 'https://gitlab.com'}
<option value="instance">Instance-wide application (self-hosted)</option>
{/if}
</select>
</div>
{#if payload.applicationType === 'group'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="groupName">Group Name</label> <label for="type">GitLab Application Type</label>
<input name="groupName" id="groupName" required bind:value={payload.groupName} /> <select name="type" id="type" class="w-96" bind:value={payload.applicationType}>
<option value="user">User owned application</option>
<option value="group">Group owned application</option>
{#if source.htmlUrl !== 'https://gitlab.com'}
<option value="instance">Instance-wide application (self-hosted)</option>
{/if}
</select>
</div> </div>
{/if} {#if payload.applicationType === 'group'}
<div class="grid grid-cols-2 items-center">
<label for="groupName">Group Name</label>
<input name="groupName" id="groupName" required bind:value={payload.groupName} />
</div>
{/if}
<div class="w-full pt-10 text-center"> <div class="w-full pt-10 text-center">
<button class="w-96 bg-orange-600 hover:bg-orange-500" type="submit" <button class="w-96 bg-orange-600 hover:bg-orange-500" type="submit"
>Register new OAuth application on GitLab</button >Register new OAuth application on GitLab</button
> >
</div> </div>
<Explainer <Explainer
customClass="w-full" customClass="w-full"
text="<span class='font-bold text-base text-white'>Scopes required:</span> text="<span class='font-bold text-base text-white'>Scopes required:</span>
<br>- <span class='text-orange-500 font-bold'>api</span> (Access the authenticated user's API) <br>- <span class='text-orange-500 font-bold'>api</span> (Access the authenticated user's API)
<br>- <span class='text-orange-500 font-bold'>read_repository</span> (Allows read-only access to the repository) <br>- <span class='text-orange-500 font-bold'>read_repository</span> (Allows read-only access to the repository)
<br>- <span class='text-orange-500 font-bold'>email</span> (Allows read-only access to the user's primary email address using OpenID Connect) <br>- <span class='text-orange-500 font-bold'>email</span> (Allows read-only access to the user's primary email address using OpenID Connect)
<br> <br>
<br>For extra security, you can set Expire access tokens! <br>For extra security, you can set Expire access tokens!
<br><br>Webhook URL: <span class='text-orange-500 font-bold'>{browser <br><br>Webhook URL: <span class='text-orange-500 font-bold'>{browser
? window.location.origin ? window.location.origin
: ''}/webhooks/gitlab</span> : ''}/webhooks/gitlab</span>
<br>But if you will set a custom domain name for Coolify, use that instead." <br>But if you will set a custom domain name for Coolify, use that instead."
/> />
</form> </form>
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4 pt-10"> <form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4 pt-10">
<div class="flex h-8 items-center space-x-2"> <div class="flex h-8 items-center space-x-2">
<div class="text-xl font-bold text-white">Configuration</div> <div class="text-xl font-bold text-white">Configuration</div>
<button <button
type="submit" type="submit"
class:bg-orange-600={!loading} class:bg-orange-600={!loading}
class:hover:bg-orange-500={!loading} class:hover:bg-orange-500={!loading}
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
> >
</div> </div>
<div class="grid grid-cols-2 items-start"> <div class="grid grid-cols-2 items-start">
<div class="flex-col"> <div class="flex-col">
<label for="oauthId" class="pt-2">OAuth ID</label> <label for="oauthId" class="pt-2">OAuth ID</label>
<Explainer <Explainer
text="The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class='font-bold text-orange-600' >in the URL</span> of your GitLab OAuth Application." text="The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class='font-bold text-orange-600' >in the URL</span> of your GitLab OAuth Application."
/>
</div>
<input
on:change={checkOauthId}
bind:this={oauthIdEl}
name="oauthId"
id="oauthId"
type="number"
required
bind:value={payload.oauthId}
/> />
</div> </div>
<input {#if payload.applicationType === 'group'}
on:change={checkOauthId} <div class="grid grid-cols-2 items-center">
bind:this={oauthIdEl} <label for="groupName">Group Name</label>
name="oauthId" <input name="groupName" id="groupName" required bind:value={payload.groupName} />
id="oauthId" </div>
type="number" {/if}
required
bind:value={payload.oauthId}
/>
</div>
{#if payload.applicationType === 'group'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="groupName">Group Name</label> <label for="appId">Application ID</label>
<input name="groupName" id="groupName" required bind:value={payload.groupName} /> <input name="appId" id="appId" required bind:value={payload.appId} />
</div> </div>
{/if} <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-2 items-center"> <label for="appSecret">Secret</label>
<label for="appId">Application ID</label> <input
<input name="appId" id="appId" required bind:value={payload.appId} /> name="appSecret"
</div> id="appSecret"
<div class="grid grid-cols-2 items-center"> type="password"
<label for="appSecret">Secret</label> required
<input bind:value={payload.appSecret}
name="appSecret" />
id="appSecret" </div>
type="password" </form>
required </div>
bind:value={payload.appSecret}
/>
</div>
</form>
{:else} {:else}
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmitSave} class="py-4"> <form on:submit|preventDefault={handleSubmitSave} class="py-4">

View File

@ -60,6 +60,9 @@
class:border-l-4={source.gitlabApp && !source.gitlabAppId} class:border-l-4={source.gitlabApp && !source.gitlabAppId}
> >
<div class="font-bold text-xl text-center truncate">{source.name}</div> <div class="font-bold text-xl text-center truncate">{source.name}</div>
{#if $session.teamId === '0'}
<div class="text-center truncate">Team {source.teams[0].name}</div>
{/if}
{#if (source.type === 'gitlab' && !source.gitlabAppId) || (source.type === 'github' && !source.githubAppId && !source.githubApp?.installationId)} {#if (source.type === 'gitlab' && !source.gitlabAppId) || (source.type === 'github' && !source.githubAppId && !source.githubApp?.installationId)}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white"> <div class="font-bold text-center truncate text-red-500 group-hover:text-white">
Configuration missing Configuration missing

View File

@ -4,14 +4,14 @@ import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
const { userId, status, body } = await getUserDetails(event, false); const { teamId, userId, status, body } = await getUserDetails(event, false);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
try { try {
const user = await db.prisma.user.findFirst({ const user = await db.prisma.user.findFirst({
where: { id: userId, teams: { some: { id } } }, where: { id: userId, teams: teamId === '0' ? undefined : { some: { id } } },
include: { permission: true } include: { permission: true }
}); });
if (!user) { if (!user) {

View File

@ -4,14 +4,15 @@ import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
const { userId, status, body } = await getUserDetails(event, false); const { teamId, userId, status, body } = await getUserDetails(event, false);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
try { try {
const teams = await db.prisma.permission.findMany({ const teams = await db.prisma.permission.findMany({
where: { userId }, where: { userId: teamId === '0' ? undefined : teamId },
include: { team: { include: { _count: { select: { users: true } } } } } include: { team: { include: { _count: { select: { users: true } } } } }
}); });
const invitations = await db.prisma.teamInvitation.findMany({ where: { uid: userId } }); const invitations = await db.prisma.teamInvitation.findMany({ where: { uid: userId } });
return { return {
status: 200, status: 200,