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_SENTRY_DSN=
COOLIFY_IS_ON="docker"
COOLIFY_WHITE_LABELED="false"

View File

@ -11,7 +11,7 @@ WORKDIR /app
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 pnpm add -g pnpm

View File

@ -1,7 +1,7 @@
{
"name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.3.3",
"version": "2.4.0",
"license": "AGPL-3.0",
"scripts": {
"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
mysqlDatabase String?
mysqlPublicPort Int?
ftpEnabled Boolean @default(false)
ftpUser String?
ftpPassword String?
ftpPublicPort Int?
ftpHostKey String?
ftpHostKeyPrivate String?
serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())

3
src/app.d.ts vendored
View File

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

View File

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

View File

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

View File

@ -4,6 +4,12 @@ import { promises as fs } from 'fs';
const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
const { workdir, baseDirectory } = data;
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(`LABEL coolify.image=true`);
Dockerfile.push('WORKDIR /app');
@ -11,6 +17,10 @@ const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
if (htaccessFound) {
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(`EXPOSE 80`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
@ -21,12 +31,14 @@ export default async function (data) {
try {
let htaccessFound = false;
try {
const d = await fs.readFile(`${workdir}${baseDirectory || ''}/.htaccess`);
await fs.readFile(`${workdir}${baseDirectory || ''}/.htaccess`);
htaccessFound = true;
} 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 buildImage(data);
} catch (error) {

View File

@ -7,6 +7,7 @@
export let isCenter = true;
export let disabled = false;
export let dataTooltip = null;
export let loading = false;
</script>
<div class="flex items-center py-4 pr-8">
@ -26,9 +27,10 @@
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}
class:bg-green-600={setting}
class:bg-stone-700={!setting}
class:opacity-50={disabled || loading}
class:bg-green-600={!loading && setting}
class:bg-stone-700={!loading && !setting}
class:bg-yellow-500={loading}
>
<span class="sr-only">Use setting</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: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">
@ -57,6 +60,7 @@
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

View File

@ -43,3 +43,142 @@ export function changeQueryParams(buildId) {
queryParams.set('buildId', buildId);
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';
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({
@ -133,13 +139,7 @@ export async function getApplicationWebhook({
}
}
export async function getApplication({
id,
teamId
}: {
id: string;
teamId: string;
}): Promise<
export async function getApplication({ id, teamId }: { id: string; teamId: string }): Promise<
Application & {
destinationDocker: DestinationDocker;
settings: ApplicationSettings;
@ -148,16 +148,30 @@ export async function getApplication({
persistentStorage: ApplicationPersistentStorage[];
}
> {
const 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
}
});
let body;
if (teamId === '0') {
body = await prisma.application.findFirst({
where: { id },
include: {
destinationDocker: true,
settings: 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) {
body.gitSource.githubApp.clientSecret = decrypt(body.gitSource.githubApp.clientSecret);

View File

@ -1,5 +1,9 @@
import { dev } from '$app/env';
import { sentry } from '$lib/common';
import {
supportedDatabaseTypesAndVersions,
supportedServiceTypesAndVersions
} from '$lib/components/common';
import * as Prisma from '@prisma/client';
import { default as ProdPrisma } 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[] {
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,9 @@
import { dev } from '$app/env';
import got, { type Got } from 'got';
import mustache from 'mustache';
import crypto from 'crypto';
import * as db from '$lib/database';
import { checkContainer, checkHAProxy } from '.';
import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
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;
if (destinationDockerId) {
const { engine } = destinationDocker;
const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type);
const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) {
const port = found.ports.main;
const publicPort = service[type]?.publicPort;
@ -263,20 +262,36 @@ export async function configureHAProxy(): Promise<void> {
redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain
});
}
const output = mustache.render(template, data);
const newHash = crypto.createHash('md5').update(output).digest('hex');
const { proxyHash, id } = await db.listSettings();
if (proxyHash !== newHash) {
await db.prisma.setting.update({ where: { id }, data: { proxyHash: newHash } });
await haproxy.post(`v2/services/haproxy/configuration/raw`, {
searchParams: {
skip_version: true
},
body: output,
headers: {
'Content-Type': 'text/plain'
for (const service of services) {
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;
if (destinationDockerId) {
const { engine } = destinationDocker;
const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) {
const port = found.ports.main;
const publicPort = service[type]?.publicPort;
const isRunning = await checkContainer(engine, id);
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
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;
}
}
export async function startTcpProxy(
destinationDocker: DestinationDocker,
id: string,
publicPort: number,
privatePort: number
privatePort: number,
volume?: string
): Promise<{ stdout: string; stderr: string } | Error> {
const { network, engine } = destinationDocker;
const host = getEngine(engine);
@ -136,7 +136,9 @@ export async function startTcpProxy(
);
const ip = JSON.parse(Config)[0].Gateway;
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) {

View File

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

View File

@ -23,11 +23,9 @@ import yaml from 'js-yaml';
import type { Job } from 'bullmq';
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> {
/*
Edge cases:
1 - Change build pack and redeploy, what should happen?
*/
const {
id: applicationId,
repository,
@ -276,7 +274,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
}
};
});
const compose = {
const composeFile: ComposeFile = {
version: '3.8',
services: {
[imageId]: {
@ -285,7 +283,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
networks: [docker.network],
labels: labels,
labels,
depends_on: [],
restart: 'always'
}
@ -297,7 +295,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
},
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(
`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>
<title>Coolify</title>
{#if !$session.whiteLabeled}
<link rel="icon" href="/favicon.png" />
{/if}
</svelte:head>
<SvelteToast options={{ intro: { y: -64 }, duration: 3000, pausable: true }} />
{#if $session.userId}
<nav class="nav-main">
<div class="flex h-screen w-full flex-col items-center transition-all duration-100">
<div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div>
<div class="flex flex-col space-y-4 py-2">
{#if !$session.whiteLabeled}
<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
sveltekit:prefetch
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" />
</svg>
</a>
<div class="border-t border-stone-700" />
</div>
<div class="flex-1" />
@ -514,6 +518,12 @@
</div>
</div>
</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
class="fixed right-0 bottom-0 z-50 m-2 w-64 bg-opacity-30 p-2 px-4"
bind:value={selectedTeamId}

View File

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

View File

@ -62,7 +62,7 @@
<div class="flex flex-col justify-center">
{#if !filteredSources || filteredSources.length === 0}
<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">
<a href="/new/source" sveltekit:prefetch class="add-icon bg-orange-600 hover:bg-orange-500">
<svg

View File

@ -15,6 +15,7 @@
import Docker from '$lib/components/svg/applications/Docker.svelte';
import Astro from '$lib/components/svg/applications/Astro.svelte';
import Eleventy from '$lib/components/svg/applications/Eleventy.svelte';
import { session } from '$app/stores';
const buildPack = application?.buildPack?.toLowerCase();
</script>
@ -54,6 +55,9 @@
{/if}
<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}
<div class="truncate text-center">{application.fqdn}</div>
{/if}

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit';
import { makeLabelForStandaloneDatabase } from '$lib/buildPacks/common';
import { startTcpProxy } from '$lib/haproxy';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (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 composeFile = {
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {

View File

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

View File

@ -184,41 +184,19 @@
value={destination.network}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
title="Use Coolify Proxy?"
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${
cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
: ''
}`}
/>
</div>
</form>
<!-- <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 $session.teamId === '0'}
<div class="grid grid-cols-2 items-center">
<Setting
disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
title="Use Coolify Proxy?"
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${
cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
: ''
}`}
/>
</div>
{/if}
</div> -->
<!-- {#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} -->
</form>

View File

@ -8,7 +8,7 @@ import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
console.log(teamId);
const { id } = event.params;
try {
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">
<div class="box-selection hover:bg-sky-600">
<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>
</a>

View File

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

View File

@ -2,6 +2,7 @@
export let service;
export let isRunning;
export let readOnly;
export let settings;
import { page, session } from '$app/stores';
import { post } from '$lib/api';
@ -91,7 +92,22 @@
/>
</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">
<label for="destination" class="text-base font-bold text-stone-100">Destination</label>
<div>
@ -143,7 +159,7 @@
{:else if service.type === 'vscodeserver'}
<VsCodeServer {service} />
{:else if service.type === 'wordpress'}
<Wordpress bind:service {isRunning} {readOnly} />
<Wordpress bind:service {isRunning} {readOnly} {settings} />
{:else if service.type === 'ghost'}
<Ghost bind:service {readOnly} />
{:else if service.type === 'meilisearch'}
@ -151,17 +167,4 @@
{/if}
</div>
</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>

View File

@ -1,9 +1,58 @@
<script lang="ts">
import { post } from '$lib/api';
import { page } from '$app/stores';
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 isRunning;
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>
<div class="flex space-x-1 py-5 font-bold">
@ -28,6 +77,30 @@ define('SUBDOMAIN_INSTALL', false);`
: 'N/A'}>{service.wordpress.extraConfig}</textarea
>
</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="title">MySQL</div>
</div>

View File

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

View File

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

View File

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

View File

@ -31,11 +31,16 @@
import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
export let versions;
export let type;
let recommendedVersion = supportedServiceTypesAndVersions.find(
({ name }) => name === type
)?.recommendedVersion;
async function handleSubmit(version) {
try {
await post(`/services/${id}/configuration/version.json`, { version });
@ -49,13 +54,26 @@
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Service version</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">
{#each versions as version}
<div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(version)}>
<button type="submit" class="box-selection text-xl font-bold hover:bg-pink-600"
>{version}</button
<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>
</div>

View File

@ -11,6 +11,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (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;
});
}
const composeFile = {
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {

View File

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

View File

@ -6,7 +6,8 @@
props: {
service: stuff.service,
isRunning: stuff.isRunning,
readOnly: stuff.readOnly
readOnly: stuff.readOnly,
settings: stuff.settings
}
};
}
@ -37,6 +38,7 @@
export let service;
export let isRunning;
export let readOnly;
export let settings;
if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) {
service.fqdn = `http://${cuid()}.demo.coolify.io`;
@ -76,4 +78,4 @@
<ServiceLinks {service} />
</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();
try {
await db.updateMeiliSearchService({ id, fqdn, name });
await db.updateService({ id, fqdn, name });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (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`;
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile);
const composeFile = {
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -91,93 +91,95 @@
</script>
{#if !source.gitlabApp?.appId}
<form class="grid grid-flow-row gap-2 py-4" on:submit|preventDefault={newApp}>
<div class="grid grid-cols-2 items-center">
<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>
<form class="grid grid-flow-row gap-2 py-4" on:submit|preventDefault={newApp}>
<div class="grid grid-cols-2 items-center">
<label for="groupName">Group Name</label>
<input name="groupName" id="groupName" required bind:value={payload.groupName} />
<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}
{#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">
<button class="w-96 bg-orange-600 hover:bg-orange-500" type="submit"
>Register new OAuth application on GitLab</button
>
</div>
<div class="w-full pt-10 text-center">
<button class="w-96 bg-orange-600 hover:bg-orange-500" type="submit"
>Register new OAuth application on GitLab</button
>
</div>
<Explainer
customClass="w-full"
text="<span class='font-bold text-base text-white'>Scopes required:</span>
<Explainer
customClass="w-full"
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'>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>
<br>For extra security, you can set Expire access tokens!
<br><br>Webhook URL: <span class='text-orange-500 font-bold'>{browser
? window.location.origin
: ''}/webhooks/gitlab</span>
? window.location.origin
: ''}/webhooks/gitlab</span>
<br>But if you will set a custom domain name for Coolify, use that instead."
/>
</form>
<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="text-xl font-bold text-white">Configuration</div>
<button
type="submit"
class:bg-orange-600={!loading}
class:hover:bg-orange-500={!loading}
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
>
</div>
/>
</form>
<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="text-xl font-bold text-white">Configuration</div>
<button
type="submit"
class:bg-orange-600={!loading}
class:hover:bg-orange-500={!loading}
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
>
</div>
<div class="grid grid-cols-2 items-start">
<div class="flex-col">
<label for="oauthId" class="pt-2">OAuth ID</label>
<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."
<div class="grid grid-cols-2 items-start">
<div class="flex-col">
<label for="oauthId" class="pt-2">OAuth ID</label>
<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."
/>
</div>
<input
on:change={checkOauthId}
bind:this={oauthIdEl}
name="oauthId"
id="oauthId"
type="number"
required
bind:value={payload.oauthId}
/>
</div>
<input
on:change={checkOauthId}
bind:this={oauthIdEl}
name="oauthId"
id="oauthId"
type="number"
required
bind:value={payload.oauthId}
/>
</div>
{#if payload.applicationType === 'group'}
{#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="grid grid-cols-2 items-center">
<label for="groupName">Group Name</label>
<input name="groupName" id="groupName" required bind:value={payload.groupName} />
<label for="appId">Application ID</label>
<input name="appId" id="appId" required bind:value={payload.appId} />
</div>
{/if}
<div class="grid grid-cols-2 items-center">
<label for="appId">Application ID</label>
<input name="appId" id="appId" required bind:value={payload.appId} />
</div>
<div class="grid grid-cols-2 items-center">
<label for="appSecret">Secret</label>
<input
name="appSecret"
id="appSecret"
type="password"
required
bind:value={payload.appSecret}
/>
</div>
</form>
<div class="grid grid-cols-2 items-center">
<label for="appSecret">Secret</label>
<input
name="appSecret"
id="appSecret"
type="password"
required
bind:value={payload.appSecret}
/>
</div>
</form>
</div>
{:else}
<div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmitSave} class="py-4">

View File

@ -60,6 +60,9 @@
class:border-l-4={source.gitlabApp && !source.gitlabAppId}
>
<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)}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">
Configuration missing

View File

@ -4,14 +4,14 @@ import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
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 };
const { id } = event.params;
try {
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 }
});
if (!user) {

View File

@ -4,14 +4,15 @@ import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
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 };
try {
const teams = await db.prisma.permission.findMany({
where: { userId },
where: { userId: teamId === '0' ? undefined : teamId },
include: { team: { include: { _count: { select: { users: true } } } } }
});
const invitations = await db.prisma.teamInvitation.findMany({ where: { uid: userId } });
return {
status: 200,