* New Version: 3.0.0
This commit is contained in:
Andras Bacsai 2022-07-06 11:02:36 +02:00 committed by GitHub
parent 9137e8bc32
commit 87ba4560ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
491 changed files with 16824 additions and 20459 deletions

View File

@ -1,16 +1,11 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
/yarn.lock
/.pnpm-store
/ssl
build
.svelte-kit
package
.env
.env.prod
.env.stag
/db/*.db
/db/*.db-journal
/data/haproxy/haproxy.cfg
/data/haproxy/haproxy.cfg.lkg
.env.*
!.env.example
dist
client
apps/api/db/*.db

View File

@ -1,8 +0,0 @@
COOLIFY_APP_ID=
COOLIFY_SECRET_KEY=12341234123412341234123412341234
COOLIFY_DATABASE_URL=file:../db/dev.db
COOLIFY_SENTRY_DSN=
COOLIFY_IS_ON=docker
COOLIFY_WHITE_LABELED=false
COOLIFY_WHITE_LABELED_ICON=
COOLIFY_AUTO_UPDATE=false

21
.gitignore vendored
View File

@ -1,16 +1,11 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
/yarn.lock
/.pnpm-store
/ssl
build
.svelte-kit
package
.env
.env.prod
.env.stag
/db/*.db
/db/*.db-journal
/data/haproxy/haproxy.cfg
/data/haproxy/haproxy.cfg.lkg
.env.*
!.env.example
dist
client
apps/api/db/*.db

1
.husky/_/.gitignore vendored
View File

@ -1 +0,0 @@
*

View File

@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
pnpm lint-staged

View File

@ -1,5 +0,0 @@
{
"**/*.{js,jsx,ts,tsx,cjs,svelte,json,css,scss,md,yaml}": [
"prettier --ignore-path .gitignore --write --plugin-search-dir=."
]
}

View File

@ -2,6 +2,8 @@ # 👋 Welcome
First of all, thank you for considering contributing to my project! It means a lot 💜.
Contribution guide is for v2, not applicable for v3
## 🙋 Want to help?
If you begin in GitHub contribution, you can find the [first contribution](https://github.com/firstcontributions/first-contributions) and follow this guide.

View File

@ -1,17 +1,18 @@
FROM node:16.14.2-alpine as install
FROM node:18-alpine as build
WORKDIR /app
RUN apk add --no-cache curl
RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@6
RUN pnpm add -g pnpm
RUN curl -sL https://unpkg.com/@pnpm/self-installer | node
COPY package*.json .
COPY . .
RUN pnpm install
RUN pnpm build
FROM node:16.14.2-alpine
ARG TARGETPLATFORM
# Production build
FROM node:18-alpine
WORKDIR /app
ENV NODE_ENV production
ARG TARGETPLATFORM
ENV PRISMA_QUERY_ENGINE_BINARY=/app/prisma-engines/query-engine \
PRISMA_MIGRATION_ENGINE_BINARY=/app/prisma-engines/migration-engine \
@ -19,24 +20,24 @@ ENV PRISMA_QUERY_ENGINE_BINARY=/app/prisma-engines/query-engine \
PRISMA_FMT_BINARY=/app/prisma-engines/prisma-fmt \
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
PRISMA_CLIENT_ENGINE_TYPE=binary
COPY --from=coollabsio/prisma-engine:latest /prisma-engines/query-engine /prisma-engines/migration-engine /prisma-engines/introspection-engine /prisma-engines/prisma-fmt /app/prisma-engines/
COPY --from=install /app/node_modules ./node_modules
COPY . .
COPY --from=coollabsio/prisma-engine:3.15 /prisma-engines/query-engine /prisma-engines/migration-engine /prisma-engines/introspection-engine /prisma-engines/prisma-fmt /app/prisma-engines/
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
RUN curl -sL https://unpkg.com/@pnpm/self-installer | node
RUN mkdir -p ~/.docker/cli-plugins/
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-20.10.9 -o /usr/bin/docker
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-2.3.4 -o ~/.docker/cli-plugins/docker-compose
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker
RUN pnpm prisma generate
RUN pnpm build
COPY --from=build /app/apps/api/build/ .
COPY --from=build /app/apps/ui/build/ ./public
COPY --from=build /app/apps/api/prisma/ ./prisma
COPY --from=build /app/apps/api/package.json .
COPY --from=build /app/docker-compose.yaml .
RUN pnpm install -p
EXPOSE 3000
CMD ["pnpm", "start"]
CMD pnpm start

View File

@ -6,7 +6,7 @@ ## Live Demo
https://demo.coolify.io/
(If it is unresponsive, that means someone overloaded the server. 🙃)
(If it is unresponsive, that means someone overloaded the server. 😄)
## Feedback
@ -30,6 +30,8 @@ ## How to install
## Features
ARM support is in beta!
### Git Sources
You can use the following Git Sources to be auto-deployed to your Coolifyt instance! (Self-hosted versions are also supported.)
@ -97,7 +99,7 @@ ### One-click services
## Migration from v1
A fresh installation is necessary. v2 is not compatible with v1.
A fresh installation is necessary. v2 and v3 are not compatible with v1.
## Support

9
apps/api/.eslintignore Normal file
View File

@ -0,0 +1,9 @@
seed.js
.DS_Store
node_modules
build
.env
.env.*
!.env.example
dist
dev.db

11
apps/api/.eslintrc Normal file
View File

@ -0,0 +1,11 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "prettier"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
]
}

11
apps/api/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
.DS_Store
node_modules
build
.svelte-kit
package
.env
.env.*
!.env.example
dist
dev.db
client

0
apps/api/db/.gitkeep Normal file
View File

7
apps/api/nodemon.json Normal file
View File

@ -0,0 +1,7 @@
{
"watch": ["src"],
"ignore": ["src/**/*.test.ts"],
"ext": "ts,mjs,json,graphql",
"exec": "rimraf build && esbuild `find src \\( -name '*.ts' \\) | grep -v client/` --platform=node --outdir=build --format=cjs && node build",
"legacyWatch": true
}

67
apps/api/package.json Normal file
View File

@ -0,0 +1,67 @@
{
"name": "coolify-api",
"description": "Coolify's Fastify API",
"license": "AGPL-3.0",
"scripts": {
"db:push": "prisma db push && prisma generate",
"db:seed": "prisma db seed",
"db:studio": "prisma studio",
"dev": "nodemon",
"build": "rimraf build && esbuild `find src \\( -name '*.ts' \\)| grep -v client/` --platform=node --outdir=build --format=cjs",
"format": "prettier --write 'src/**/*.{js,ts,json,md}'",
"lint": "prettier --check 'src/**/*.{js,ts,json,md}' && eslint --ignore-path .eslintignore .",
"start": "NODE_ENV=production npx -y prisma migrate deploy && npx prisma generate && npx prisma db seed && node index.js"
},
"dependencies": {
"@breejs/ts-worker": "2.0.0",
"@fastify/autoload": "5.0.0",
"@fastify/cookie": "7.0.0",
"@fastify/cors": "8.0.0",
"@fastify/env": "4.0.0",
"@fastify/jwt": "6.2.0",
"@fastify/static": "6.4.0",
"@iarna/toml": "2.2.5",
"@prisma/client": "3.15.2",
"axios": "0.27.2",
"bcryptjs": "2.4.3",
"bree": "9.1.1",
"cabin": "9.1.2",
"compare-versions": "4.1.3",
"cuid": "2.1.8",
"dayjs": "1.11.3",
"dockerode": "3.3.2",
"dotenv-extended": "2.9.0",
"fastify": "4.2.0",
"fastify-plugin": "3.0.1",
"generate-password": "1.7.0",
"get-port": "6.1.2",
"got": "12.1.0",
"is-ip": "4.0.0",
"js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1",
"node-forge": "1.3.1",
"node-os-utils": "1.3.7",
"p-queue": "7.2.0",
"strip-ansi": "7.0.1",
"unique-names-generator": "4.7.1"
},
"devDependencies": {
"@types/node": "18.0.3",
"@types/node-os-utils": "1.3.0",
"@typescript-eslint/eslint-plugin": "5.30.5",
"@typescript-eslint/parser": "5.30.5",
"esbuild": "0.14.48",
"eslint": "8.19.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.2.1",
"nodemon": "2.0.19",
"prettier": "2.7.1",
"prisma": "3.15.2",
"rimraf": "3.0.2",
"tsconfig-paths": "4.0.0",
"typescript": "4.7.4"
},
"prisma": {
"seed": "node prisma/seed.js"
}
}

75
apps/api/prisma/seed.js Normal file
View File

@ -0,0 +1,75 @@
const dotEnvExtended = require('dotenv-extended');
dotEnvExtended.load();
const crypto = require('crypto');
const generator = require('generate-password');
const cuid = require('cuid');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
function generatePassword(length = 24) {
return generator.generate({
length,
numbers: true,
strict: true
});
}
const algorithm = 'aes-256-ctr';
async function main() {
// Enable registration for the first user
// Set initial HAProxy password
const settingsFound = await prisma.setting.findFirst({});
if (!settingsFound) {
await prisma.setting.create({
data: {
isRegistrationEnabled: true,
isTraefikUsed: true,
}
});
} else {
await prisma.setting.update({
where: {
id: settingsFound.id
},
data: {
isTraefikUsed: true,
proxyHash: null
}
});
}
const localDocker = await prisma.destinationDocker.findFirst({
where: { engine: '/var/run/docker.sock' }
});
if (!localDocker) {
await prisma.destinationDocker.create({
data: {
engine: '/var/run/docker.sock',
name: 'Local Docker',
isCoolifyProxyUsed: true,
network: 'coolify'
}
});
}
// Set auto-update based on env variable
const isAutoUpdateEnabled = process.env['COOLIFY_AUTO_UPDATE'] === 'true';
const settings = await prisma.setting.findFirst({});
if (settings) {
await prisma.setting.update({
where: {
id: settings.id
},
data: {
isAutoUpdateEnabled
}
});
}
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

132
apps/api/src/index.ts Normal file
View File

@ -0,0 +1,132 @@
import Fastify from 'fastify';
import cors from '@fastify/cors';
import serve from '@fastify/static';
import env from '@fastify/env';
import cookie from '@fastify/cookie';
import path, { join } from 'path';
import autoLoad from '@fastify/autoload';
import { asyncExecShell, isDev, prisma } from './lib/common';
import { scheduler } from './lib/scheduler';
declare module 'fastify' {
interface FastifyInstance {
config: {
COOLIFY_APP_ID: string,
COOLIFY_SECRET_KEY: string,
COOLIFY_DATABASE_URL: string,
COOLIFY_SENTRY_DSN: string,
COOLIFY_IS_ON: string,
COOLIFY_WHITE_LABELED: boolean,
COOLIFY_WHITE_LABELED_ICON: string | null,
COOLIFY_AUTO_UPDATE: boolean,
};
}
}
const port = isDev ? 3001 : 3000;
const host = '0.0.0.0';
const fastify = Fastify({
logger: false
});
const schema = {
type: 'object',
required: ['COOLIFY_SECRET_KEY', 'COOLIFY_DATABASE_URL', 'COOLIFY_IS_ON'],
properties: {
COOLIFY_APP_ID: {
type: 'string',
},
COOLIFY_SECRET_KEY: {
type: 'string',
},
COOLIFY_DATABASE_URL: {
type: 'string',
default: 'file:../db/dev.db'
},
COOLIFY_SENTRY_DSN: {
type: 'string',
default: null
},
COOLIFY_IS_ON: {
type: 'string',
default: 'docker'
},
COOLIFY_WHITE_LABELED: {
type: 'boolean',
default: false
},
COOLIFY_WHITE_LABELED_ICON: {
type: 'string',
default: null
},
COOLIFY_AUTO_UPDATE: {
type: 'boolean',
default: false
},
}
};
const options = {
schema
};
fastify.register(env, options);
if (!isDev) {
fastify.register(serve, {
root: path.join(__dirname, './public'),
preCompressed: true
});
fastify.setNotFoundHandler(function (request, reply) {
if (request.raw.url && request.raw.url.startsWith('/api')) {
return reply.status(404).send({
success: false
});
}
return reply.status(200).sendFile('index.html');
});
}
fastify.register(autoLoad, {
dir: join(__dirname, 'plugins')
});
fastify.register(autoLoad, {
dir: join(__dirname, 'routes')
});
fastify.register(cookie)
fastify.register(cors);
fastify.listen({ port, host }, async (err: any, address: any) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log(`Coolify's API is listening on ${host}:${port}`);
await initServer()
await scheduler.start('deployApplication');
await scheduler.start('cleanupStorage');
await scheduler.start('checkProxies')
// Check if no build is running, try to autoupdate.
setInterval(async () => {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) {
if (scheduler.workers.has('deployApplication')) {
scheduler.workers.get('deployApplication').postMessage("status");
}
}
}, 30000 * 10)
scheduler.on('worker deleted', async (name) => {
if (name === 'autoUpdater') {
await scheduler.start('deployApplication');
}
});
});
async function initServer() {
try {
await asyncExecShell(`docker network create --attachable coolify`);
} catch (error) { }
}

View File

@ -0,0 +1,43 @@
import axios from 'axios';
import compareVersions from 'compare-versions';
import { parentPort } from 'node:worker_threads';
import { asyncExecShell, asyncSleep, isDev, prisma, version } from '../lib/common';
(async () => {
if (parentPort) {
try {
const currentVersion = version;
const { data: versions } = await axios
.get(
`https://get.coollabs.io/versions.json`
, {
params: {
appId: process.env['COOLIFY_APP_ID'] || undefined,
version: currentVersion
}
})
const latestVersion = versions['coolify'].main.version;
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
if (isUpdateAvailable === 1) {
const activeCount = 0
if (activeCount === 0) {
if (!isDev) {
console.log(`Updating Coolify to ${latestVersion}.`);
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"`
);
} else {
console.log('Updating (not really in dev mode).');
}
}
}
} catch (error) {
console.log(error);
} finally {
await prisma.$disconnect();
}
} else process.exit(0);
})();

View File

@ -0,0 +1,88 @@
import { parentPort } from 'node:worker_threads';
import { prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, asyncExecShell } from '../lib/common';
import { checkContainer, getEngine } from '../lib/docker';
(async () => {
if (parentPort) {
// Coolify Proxy
const engine = '/var/run/docker.sock';
const localDocker = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' }
});
if (localDocker && localDocker.isCoolifyProxyUsed) {
// Remove HAProxy
const found = await checkContainer(engine, 'coolify-haproxy');
const host = getEngine(engine);
if (found) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker stop -t 0 coolify-haproxy && docker rm coolify-haproxy`
);
}
await startTraefikProxy(engine);
}
// TCP Proxies
const databasesWithPublicPort = await prisma.database.findMany({
where: { publicPort: { not: null } },
include: { settings: true, destinationDocker: true }
});
for (const database of databasesWithPublicPort) {
const { destinationDockerId, destinationDocker, publicPort, id } = database;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
const { privatePort } = generateDatabaseConfiguration(database);
// Remove HAProxy
const found = await checkContainer(engine, `haproxy-for-${publicPort}`);
const host = getEngine(engine);
if (found) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker stop -t 0 haproxy-for-${publicPort} && docker rm haproxy-for-${publicPort}`
);
}
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
}
}
const wordpressWithFtp = await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const ftp of wordpressWithFtp) {
const { service, ftpPublicPort } = ftp;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
// Remove HAProxy
const found = await checkContainer(engine, `haproxy-for-${ftpPublicPort}`);
const host = getEngine(engine);
if (found) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker stop -t 0 haproxy-for-${ftpPublicPort} && docker rm haproxy-for-${ftpPublicPort} `
);
}
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
}
}
// HTTP Proxies
const minioInstances = await prisma.minio.findMany({
where: { publicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const minio of minioInstances) {
const { service, publicPort } = minio;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
// Remove HAProxy
const found = await checkContainer(engine, `${id}-${publicPort}`);
const host = getEngine(engine);
if (found) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker stop -t 0 ${id}-${publicPort} && docker rm ${id}-${publicPort}`
);
}
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
}
}
await prisma.$disconnect();
} else process.exit(0);
})();

View File

@ -0,0 +1,90 @@
import { parentPort } from 'node:worker_threads';
import { asyncExecShell, isDev, prisma, version } from '../lib/common';
import { getEngine } from '../lib/docker';
(async () => {
if (parentPort) {
const destinationDockers = await prisma.destinationDocker.findMany();
const engines = [...new Set(destinationDockers.map(({ engine }) => engine))];
for (const engine of engines) {
let lowDiskSpace = false;
const host = getEngine(engine);
// try {
// let stdout = null
// if (!isDev) {
// const output = await asyncExecShell(
// `DOCKER_HOST=${host} docker exec coolify sh -c 'df -kPT /'`
// );
// stdout = output.stdout;
// } else {
// const output = await asyncExecShell(
// `df -kPT /`
// );
// stdout = output.stdout;
// }
// let lines = stdout.trim().split('\n');
// let header = lines[0];
// let regex =
// /^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
// const boundaries = [];
// let match;
// while ((match = regex.exec(header))) {
// boundaries.push(match[0].length);
// }
// boundaries[boundaries.length - 1] = -1;
// const data = lines.slice(1).map((line) => {
// const cl = boundaries.map((boundary) => {
// const column = boundary > 0 ? line.slice(0, boundary) : line;
// line = line.slice(boundary);
// return column.trim();
// });
// return {
// capacity: Number.parseInt(cl[5], 10) / 100
// };
// });
// if (data.length > 0) {
// const { capacity } = data[0];
// if (capacity > 0.6) {
// lowDiskSpace = true;
// }
// }
// } catch (error) {
// console.log(error);
// }
if (!isDev) {
// Cleanup old coolify images
try {
let { stdout: images } = await asyncExecShell(
`DOCKER_HOST=${host} docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs `
);
images = images.trim();
if (images) {
await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`);
}
} catch (error) {
//console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`);
} catch (error) {
//console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f`);
} catch (error) {
//console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -a -f`);
} catch (error) {
//console.log(error);
}
} else {
console.log(`[DEV MODE] Low disk space: ${lowDiskSpace}`);
}
}
await prisma.$disconnect();
} else process.exit(0);
})();

View File

@ -0,0 +1,352 @@
import { parentPort } from 'node:worker_threads';
import crypto from 'crypto';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import { copyBaseConfigurationFiles, makeLabelForStandaloneApplication, saveBuildLog, setDefaultConfiguration } from '../lib/buildPacks/common';
import { asyncExecShell, createDirectories, decrypt, getDomain, prisma } from '../lib/common';
import { dockerInstance, getEngine } from '../lib/docker';
import * as importers from '../lib/importers';
import * as buildpacks from '../lib/buildPacks';
(async () => {
if (parentPort) {
const concurrency = 1
const PQueue = await import('p-queue');
const queue = new PQueue.default({ concurrency });
parentPort.on('message', async (message) => {
if (parentPort) {
if (message === 'error') throw new Error('oops');
if (message === 'cancel') {
parentPort.postMessage('cancelled');
return;
}
if (message === 'status') {
parentPort.postMessage({ size: queue.size, pending: queue.pending });
return;
}
await queue.add(async () => {
const {
id: applicationId,
repository,
name,
destinationDocker,
destinationDockerId,
gitSource,
build_id: buildId,
configHash,
fqdn,
projectId,
secrets,
phpModules,
type,
pullmergeRequestId = null,
sourceBranch = null,
settings,
persistentStorage,
pythonWSGI,
pythonModule,
pythonVariable,
denoOptions,
exposePort,
baseImage,
baseBuildImage
} = message
let {
branch,
buildPack,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory,
dockerFileLocation,
denoMainFile
} = message
try {
const { debug } = settings;
if (concurrency === 1) {
await prisma.build.updateMany({
where: {
status: { in: ['queued', 'running'] },
id: { not: buildId },
applicationId,
createdAt: { lt: new Date(new Date().getTime() - 10 * 1000) }
},
data: { status: 'failed' }
});
}
let imageId = applicationId;
let domain = getDomain(fqdn);
const volumes =
persistentStorage?.map((storage) => {
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : ''
}${storage.path}`;
}) || [];
// Previews, we need to get the source branch and set subdomain
if (pullmergeRequestId) {
branch = sourceBranch;
domain = `${pullmergeRequestId}.${domain}`;
imageId = `${applicationId}-${pullmergeRequestId}`;
}
let deployNeeded = true;
let destinationType;
if (destinationDockerId) {
destinationType = 'docker';
}
if (destinationType === 'docker') {
const docker = dockerInstance({ destinationDocker });
const host = getEngine(destinationDocker.engine);
await prisma.build.update({ where: { id: buildId }, data: { status: 'running' } });
const { workdir, repodir } = await createDirectories({ repository, buildId });
const configuration = await setDefaultConfiguration(message);
buildPack = configuration.buildPack;
port = configuration.port;
installCommand = configuration.installCommand;
startCommand = configuration.startCommand;
buildCommand = configuration.buildCommand;
publishDirectory = configuration.publishDirectory;
baseDirectory = configuration.baseDirectory;
dockerFileLocation = configuration.dockerFileLocation;
denoMainFile = configuration.denoMainFile;
const commit = await importers[gitSource.type]({
applicationId,
debug,
workdir,
repodir,
githubAppId: gitSource.githubApp?.id,
gitlabAppId: gitSource.gitlabApp?.id,
repository,
branch,
buildId,
apiUrl: gitSource.apiUrl,
htmlUrl: gitSource.htmlUrl,
projectId,
deployKeyId: gitSource.gitlabApp?.deployKeyId || null,
privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null
});
if (!commit) {
throw new Error('No commit found?');
}
let tag = commit.slice(0, 7);
if (pullmergeRequestId) {
tag = `${commit.slice(0, 7)}-${pullmergeRequestId}`;
}
try {
await prisma.build.update({ where: { id: buildId }, data: { commit } });
} catch (err) {
console.log(err);
}
if (!pullmergeRequestId) {
const currentHash = crypto
//@ts-ignore
.createHash('sha256')
.update(
JSON.stringify({
buildPack,
port,
exposePort,
installCommand,
buildCommand,
startCommand,
secrets,
branch,
repository,
fqdn
})
)
.digest('hex');
if (configHash !== currentHash) {
await prisma.application.update({
where: { id: applicationId },
data: { configHash: currentHash }
});
deployNeeded = true;
if (configHash) {
await saveBuildLog({ line: 'Configuration changed.', buildId, applicationId });
}
} else {
deployNeeded = false;
}
} else {
deployNeeded = true;
}
const image = await docker.engine.getImage(`${applicationId}:${tag}`);
let imageFound = false;
try {
await image.inspect();
imageFound = false;
} catch (error) {
//
}
if (!imageFound || deployNeeded) {
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
if (buildpacks[buildPack])
await buildpacks[buildPack]({
buildId,
applicationId,
domain,
name,
type,
pullmergeRequestId,
buildPack,
repository,
branch,
projectId,
publishDirectory,
debug,
commit,
tag,
workdir,
docker,
port: exposePort ? `${exposePort}:${port}` : port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
secrets,
phpModules,
pythonWSGI,
pythonModule,
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions,
baseImage,
baseBuildImage
});
else {
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
throw new Error(`Build pack ${buildPack} not found.`);
}
} else {
await saveBuildLog({ line: 'Nothing changed.', buildId, applicationId });
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker stop -t 0 ${imageId}`);
await asyncExecShell(`DOCKER_HOST=${host} docker rm ${imageId}`);
} catch (error) {
//
}
const envs = [];
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
}
});
}
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
const labels = makeLabelForStandaloneApplication({
applicationId,
fqdn,
name,
type,
pullmergeRequestId,
buildPack,
repository,
branch,
projectId,
port: exposePort ? `${exposePort}:${port}` : port,
commit,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
});
let envFound = false;
try {
envFound = !!(await fs.stat(`${workdir}/.env`));
} catch (error) {
//
}
try {
await saveBuildLog({ line: 'Deployment started.', buildId, applicationId });
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const composeFile = {
version: '3.8',
services: {
[imageId]: {
image: `${applicationId}:${tag}`,
container_name: imageId,
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
networks: [docker.network],
labels,
depends_on: [],
restart: 'always',
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
// logging: {
// driver: 'fluentd',
// },
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}
},
networks: {
[docker.network]: {
external: true
}
},
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await asyncExecShell(
`DOCKER_HOST=${host} docker compose --project-directory ${workdir} up -d`
);
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} catch (error) {
await saveBuildLog({ line: error, buildId, applicationId });
await prisma.build.update({
where: { id: message.build_id },
data: { status: 'failed' }
});
throw new Error(error);
}
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
await prisma.build.update({ where: { id: message.build_id }, data: { status: 'success' } });
}
}
catch (error) {
await prisma.build.update({
where: { id: message.build_id },
data: { status: 'failed' }
});
await saveBuildLog({ line: error, buildId, applicationId });
} finally {
await prisma.$disconnect();
}
});
await prisma.$disconnect();
}
});
} else process.exit(0);
})();

View File

@ -1,10 +1,7 @@
import { base64Encode } from '$lib/crypto';
import { getDomain, saveBuildLog, version } from '$lib/common';
import * as db from '$lib/database';
import { scanningTemplates } from '$lib/components/templates';
import { asyncExecShell, base64Encode, generateTimestamp, getDomain, isDev, prisma, version } from "../common";
import { scheduler } from "../scheduler";
import { promises as fs } from 'fs';
import { staticDeployments } from '$lib/components/common';
import { day } from "../dayjs";
const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy'];
const nodeBased = [
'react',
@ -20,223 +17,7 @@ const nodeBased = [
'nextjs'
];
export function makeLabelForStandaloneApplication({
applicationId,
fqdn,
name,
type,
pullmergeRequestId = null,
buildPack,
repository,
branch,
projectId,
port,
commit,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
}) {
if (pullmergeRequestId) {
const protocol = fqdn.startsWith('https://') ? 'https' : 'http';
const domain = getDomain(fqdn);
fqdn = `${protocol}://${pullmergeRequestId}.${domain}`;
}
return [
'coolify.managed=true',
`coolify.version=${version}`,
`coolify.type=standalone-application`,
`coolify.configuration=${base64Encode(
JSON.stringify({
applicationId,
fqdn,
name,
type,
pullmergeRequestId,
buildPack,
repository,
branch,
projectId,
port,
commit,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
})
)}`
];
}
export async function makeLabelForStandaloneDatabase({ id, image, volume }) {
const database = await db.prisma.database.findFirst({ where: { id } });
delete database.destinationDockerId;
delete database.createdAt;
delete database.updatedAt;
return [
'coolify.managed=true',
`coolify.version=${version}`,
`coolify.type=standalone-database`,
`coolify.configuration=${base64Encode(
JSON.stringify({
version,
image,
volume,
...database
})
)}`
];
}
export function makeLabelForServices(type) {
return [
'coolify.managed=true',
`coolify.version=${version}`,
`coolify.type=service`,
`coolify.service.type=${type}`
];
}
export const setDefaultConfiguration = async (data) => {
let {
buildPack,
port,
installCommand,
startCommand,
buildCommand,
publishDirectory,
baseDirectory,
dockerFileLocation,
denoMainFile
} = data;
const template = scanningTemplates[buildPack];
if (!port) {
port = template?.port || 3000;
if (buildPack === 'static') port = 80;
else if (buildPack === 'node') port = 3000;
else if (buildPack === 'php') port = 80;
else if (buildPack === 'python') port = 8000;
}
if (!installCommand && buildPack !== 'static' && buildPack !== 'laravel')
installCommand = template?.installCommand || 'yarn install';
if (!startCommand && buildPack !== 'static' && buildPack !== 'laravel')
startCommand = template?.startCommand || 'yarn start';
if (!buildCommand && buildPack !== 'static' && buildPack !== 'laravel')
buildCommand = template?.buildCommand || null;
if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
if (baseDirectory) {
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
if (!baseDirectory.endsWith('/')) baseDirectory = `${baseDirectory}/`;
}
if (dockerFileLocation) {
if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`;
if (dockerFileLocation.endsWith('/')) dockerFileLocation = dockerFileLocation.slice(0, -1);
} else {
dockerFileLocation = '/Dockerfile';
}
if (!denoMainFile) {
denoMainFile = 'main.ts';
}
return {
buildPack,
port,
installCommand,
startCommand,
buildCommand,
publishDirectory,
baseDirectory,
dockerFileLocation,
denoMainFile
};
};
export async function copyBaseConfigurationFiles(
buildPack,
workdir,
buildId,
applicationId,
baseImage
) {
try {
if (buildPack === 'php') {
await fs.writeFile(`${workdir}/entrypoint.sh`, `chown -R 1000 /app`);
await saveBuildLog({
line: 'Copied default configuration file for PHP.',
buildId,
applicationId
});
} else if (staticDeployments.includes(buildPack) && baseImage.includes('nginx')) {
await fs.writeFile(
`${workdir}/nginx.conf`,
`user nginx;
worker_processes auto;
error_log /docker.stdout;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /docker.stdout main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name localhost;
location / {
root /app;
index index.html;
try_files $uri $uri/index.html $uri/ /index.html =404;
}
error_page 404 /50x.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /app;
}
}
}
`
);
}
} catch (error) {
console.log(error);
throw new Error(error);
}
}
export function checkPnpm(installCommand = null, buildCommand = null, startCommand = null) {
return (
installCommand?.includes('pnpm') ||
buildCommand?.includes('pnpm') ||
startCommand?.includes('pnpm')
);
}
export function setDefaultBaseImage(buildPack) {
export function setDefaultBaseImage(buildPack: string | null) {
const nodeVersions = [
{
value: 'node:lts',
@ -471,7 +252,7 @@ export function setDefaultBaseImage(buildPack) {
label: 'python:3.7-slim-bullseye'
}
];
let payload = {
let payload: any = {
baseImage: null,
baseBuildImage: null,
baseImages: [],
@ -513,3 +294,421 @@ export function setDefaultBaseImage(buildPack) {
}
return payload;
}
export const setDefaultConfiguration = async (data: any) => {
let {
buildPack,
port,
installCommand,
startCommand,
buildCommand,
publishDirectory,
baseDirectory,
dockerFileLocation,
denoMainFile
} = data;
//@ts-ignore
const template = scanningTemplates[buildPack];
if (!port) {
port = template?.port || 3000;
if (buildPack === 'static') port = 80;
else if (buildPack === 'node') port = 3000;
else if (buildPack === 'php') port = 80;
else if (buildPack === 'python') port = 8000;
}
if (!installCommand && buildPack !== 'static' && buildPack !== 'laravel')
installCommand = template?.installCommand || 'yarn install';
if (!startCommand && buildPack !== 'static' && buildPack !== 'laravel')
startCommand = template?.startCommand || 'yarn start';
if (!buildCommand && buildPack !== 'static' && buildPack !== 'laravel')
buildCommand = template?.buildCommand || null;
if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
if (baseDirectory) {
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
if (!baseDirectory.endsWith('/')) baseDirectory = `${baseDirectory}/`;
}
if (dockerFileLocation) {
if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`;
if (dockerFileLocation.endsWith('/')) dockerFileLocation = dockerFileLocation.slice(0, -1);
} else {
dockerFileLocation = '/Dockerfile';
}
if (!denoMainFile) {
denoMainFile = 'main.ts';
}
return {
buildPack,
port,
installCommand,
startCommand,
buildCommand,
publishDirectory,
baseDirectory,
dockerFileLocation,
denoMainFile
};
};
export const scanningTemplates = {
'@sveltejs/kit': {
buildPack: 'nodejs'
},
astro: {
buildPack: 'astro'
},
'@11ty/eleventy': {
buildPack: 'eleventy'
},
svelte: {
buildPack: 'svelte'
},
'@nestjs/core': {
buildPack: 'nestjs'
},
next: {
buildPack: 'nextjs'
},
nuxt: {
buildPack: 'nuxtjs'
},
'react-scripts': {
buildPack: 'react'
},
'parcel-bundler': {
buildPack: 'static'
},
'@vue/cli-service': {
buildPack: 'vuejs'
},
vuejs: {
buildPack: 'vuejs'
},
gatsby: {
buildPack: 'gatsby'
},
'preact-cli': {
buildPack: 'react'
}
};
export const saveBuildLog = async ({
line,
buildId,
applicationId
}: {
line: string;
buildId: string;
applicationId: string;
}): Promise<any> => {
if (line && typeof line === 'string' && line.includes('ghs_')) {
const regex = /ghs_.*@/g;
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
}
const addTimestamp = `[${generateTimestamp()}] ${line}`;
if (isDev) console.debug(`[${applicationId}] ${addTimestamp}`);
return await prisma.buildLog.create({
data: {
line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId
}
});
};
export async function copyBaseConfigurationFiles(
buildPack,
workdir,
buildId,
applicationId,
baseImage
) {
try {
if (buildPack === 'php') {
await fs.writeFile(`${workdir}/entrypoint.sh`, `chown -R 1000 /app`);
await saveBuildLog({
line: 'Copied default configuration file for PHP.',
buildId,
applicationId
});
} else if (staticApps.includes(buildPack) && baseImage.includes('nginx')) {
await fs.writeFile(
`${workdir}/nginx.conf`,
`user nginx;
worker_processes auto;
error_log /docker.stdout;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /docker.stdout main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name localhost;
location / {
root /app;
index index.html;
try_files $uri $uri/index.html $uri/ /index.html =404;
}
error_page 404 /50x.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /app;
}
}
}
`
);
}
} catch (error) {
console.log(error);
throw new Error(error);
}
}
export function checkPnpm(installCommand = null, buildCommand = null, startCommand = null) {
return (
installCommand?.includes('pnpm') ||
buildCommand?.includes('pnpm') ||
startCommand?.includes('pnpm')
);
}
export async function buildImage({
applicationId,
tag,
workdir,
docker,
buildId,
isCache = false,
debug = false,
dockerFileLocation = '/Dockerfile'
}) {
if (isCache) {
await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId });
} else {
await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
}
if (!debug && isCache) {
await saveBuildLog({
line: `Debug turned off. To see more details, allow it in the configuration.`,
buildId,
applicationId
});
}
const stream = await docker.engine.buildImage(
{ src: ['.'], context: workdir },
{
dockerfile: isCache ? `${dockerFileLocation}-cache` : dockerFileLocation,
t: `${applicationId}:${tag}${isCache ? '-cache' : ''}`
}
);
await streamEvents({ stream, docker, buildId, applicationId, debug });
await saveBuildLog({ line: `Building image successful!`, buildId, applicationId });
}
export async function streamEvents({ stream, docker, buildId, applicationId, debug }) {
await new Promise((resolve, reject) => {
docker.engine.modem.followProgress(stream, onFinished, onProgress);
function onFinished(err, res) {
if (err) reject(err);
resolve(res);
}
async function onProgress(event) {
if (event.error) {
reject(event.error);
} else if (event.stream) {
if (event.stream !== '\n') {
if (debug)
await saveBuildLog({
line: `${event.stream.replace('\n', '')}`,
buildId,
applicationId
});
}
}
}
});
}
export function makeLabelForStandaloneApplication({
applicationId,
fqdn,
name,
type,
pullmergeRequestId = null,
buildPack,
repository,
branch,
projectId,
port,
commit,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
}) {
if (pullmergeRequestId) {
const protocol = fqdn.startsWith('https://') ? 'https' : 'http';
const domain = getDomain(fqdn);
fqdn = `${protocol}://${pullmergeRequestId}.${domain}`;
}
return [
'coolify.managed=true',
`coolify.version=${version}`,
`coolify.type=standalone-application`,
`coolify.configuration=${base64Encode(
JSON.stringify({
applicationId,
fqdn,
name,
type,
pullmergeRequestId,
buildPack,
repository,
branch,
projectId,
port,
commit,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
})
)}`
];
}
export async function buildCacheImageWithNode(data, imageForBuild) {
const {
applicationId,
tag,
workdir,
docker,
buildId,
baseDirectory,
installCommand,
buildCommand,
debug,
secrets,
pullmergeRequestId
} = data;
const isPnpm = checkPnpm(installCommand, buildCommand);
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
});
}
if (isPnpm) {
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7');
}
if (installCommand) {
Dockerfile.push(`COPY .${baseDirectory || ''}/package.json ./`);
Dockerfile.push(`RUN ${installCommand}`);
}
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`RUN ${buildCommand}`);
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });
}
export async function buildCacheImageForLaravel(data, imageForBuild) {
const { applicationId, tag, workdir, docker, buildId, debug, secrets, pullmergeRequestId } = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
});
}
Dockerfile.push(`COPY *.json *.mix.js /app/`);
Dockerfile.push(`COPY resources /app/resources`);
Dockerfile.push(`RUN yarn install && yarn production`);
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });
}
export async function buildCacheImageWithCargo(data, imageForBuild) {
const {
applicationId,
tag,
workdir,
docker,
buildId,
baseDirectory,
installCommand,
buildCommand,
debug,
secrets
} = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push('RUN cargo install cargo-chef');
Dockerfile.push('COPY . .');
Dockerfile.push('RUN cargo chef prepare --recipe-path recipe.json');
Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push('RUN cargo install cargo-chef');
Dockerfile.push(`COPY --from=planner-${applicationId} /app/recipe.json recipe.json`);
Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json');
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });
}

View File

@ -1,5 +1,5 @@
import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import { buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const {

View File

@ -1,5 +1,5 @@
import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import { buildImage } from './common';
export default async function ({
applicationId,

View File

@ -1,5 +1,5 @@
import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import { buildCacheImageWithNode, buildImage } from './common';
const createDockerfile = async (data, imageforBuild): Promise<void> => {
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data;

View File

@ -1,5 +1,5 @@
import { buildCacheImageForLaravel, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import { buildCacheImageForLaravel, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const { workdir, applicationId, tag, buildId, port } = data;

View File

@ -1,5 +1,5 @@
import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import { buildCacheImageWithNode, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const { buildId, applicationId, tag, port, startCommand, workdir, baseDirectory } = data;

View File

@ -1,6 +1,5 @@
import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import { checkPnpm } from './common';
import { buildImage, checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const {

View File

@ -1,6 +1,5 @@
import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import { checkPnpm } from './common';
import { buildImage, checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const {
@ -50,7 +49,7 @@ const createDockerfile = async (data, image): Promise<void> => {
export default async function (data) {
try {
const { baseImage, baseBuildImage } = data;
const { baseImage } = data;
await createDockerfile(data, baseImage);
await buildImage(data);
} catch (error) {

View File

@ -1,6 +1,5 @@
import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import { checkPnpm } from './common';
import { buildImage, checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const {

View File

@ -1,8 +1,8 @@
import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import { buildImage } from './common';
const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
const { workdir, baseDirectory, buildId, port } = data;
const { workdir, baseDirectory, buildId, port, secrets, pullmergeRequestId } = data;
const Dockerfile: Array<string> = [];
let composerFound = false;
try {
@ -12,6 +12,21 @@ const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
});
}
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`COPY .${baseDirectory || ''} /app`);
if (htaccessFound) {

View File

@ -1,5 +1,5 @@
import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import { buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const {

View File

@ -1,5 +1,5 @@
import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import { buildCacheImageWithNode, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data;

View File

@ -1,7 +1,7 @@
import { asyncExecShell } from '$lib/common';
import { buildCacheImageWithCargo, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import TOML from '@iarna/toml';
import { asyncExecShell } from '../common';
import { buildCacheImageWithCargo, buildImage } from './common';
const createDockerfile = async (data, image, name): Promise<void> => {
const { workdir, port, applicationId, tag, buildId } = data;

View File

@ -1,5 +1,5 @@
import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import { buildCacheImageWithNode, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const {

View File

@ -1,5 +1,5 @@
import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import { buildCacheImageWithNode, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data;

View File

@ -1,5 +1,5 @@
import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import { buildCacheImageWithNode, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data;

1480
apps/api/src/lib/common.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -4,4 +4,4 @@ import relativeTime from 'dayjs/plugin/relativeTime.js';
dayjs.extend(utc);
dayjs.extend(relativeTime);
export { dayjs };
export { dayjs as day };

View File

@ -0,0 +1,78 @@
import { asyncExecShell } from './common';
import Dockerode from 'dockerode';
export function getEngine(engine: string): string {
return engine === '/var/run/docker.sock' ? 'unix:///var/run/docker.sock' : engine;
}
export function dockerInstance({ destinationDocker }): { engine: Dockerode; network: string } {
return {
engine: new Dockerode({
socketPath: destinationDocker.engine
}),
network: destinationDocker.network
};
}
export async function checkContainer(engine: string, container: string, remove = false): Promise<boolean> {
const host = getEngine(engine);
let containerFound = false;
try {
const { stdout } = await asyncExecShell(
`DOCKER_HOST="${host}" docker inspect --format '{{json .State}}' ${container}`
);
const parsedStdout = JSON.parse(stdout);
const status = parsedStdout.Status;
const isRunning = status === 'running';
if (status === 'created') {
await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`);
}
if (remove && status === 'exited') {
await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`);
}
if (isRunning) {
containerFound = true;
}
} catch (err) {
// Container not found
}
return containerFound;
}
export async function isContainerExited(engine: string, containerName: string): Promise<boolean> {
let isExited = false;
const host = getEngine(engine);
try {
const { stdout } = await asyncExecShell(
`DOCKER_HOST="${host}" docker inspect -f '{{.State.Status}}' ${containerName}`
);
if (stdout.trim() === 'exited') {
isExited = true;
}
} catch (error) {
//
}
return isExited;
}
export async function removeContainer({
id,
engine
}: {
id: string;
engine: string;
}): Promise<void> {
const host = getEngine(engine);
try {
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}`
);
if (JSON.parse(stdout).Running) {
await asyncExecShell(`DOCKER_HOST=${host} docker stop -t 0 ${id}`);
await asyncExecShell(`DOCKER_HOST=${host} docker rm ${id}`);
}
} catch (error) {
console.log(error);
throw error;
}
}

View File

@ -1,7 +1,7 @@
import { asyncExecShell, saveBuildLog } from '$lib/common';
import got from 'got';
import jsonwebtoken from 'jsonwebtoken';
import * as db from '$lib/database';
import { saveBuildLog } from '../buildPacks/common';
import { asyncExecShell, decrypt, prisma } from '../common';
export default async function ({
applicationId,
@ -22,9 +22,14 @@ export default async function ({
branch: string;
buildId: string;
}): Promise<string> {
const { default: got } = await import('got')
const url = htmlUrl.replace('https://', '').replace('http://', '');
await saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId });
const { privateKey, appId, installationId } = await db.getUniqueGithubApp({ githubAppId });
const body = await prisma.githubApp.findUnique({ where: { id: githubAppId } });
if (body.privateKey) body.privateKey = decrypt(body.privateKey);
const { privateKey, appId, installationId } = body
const githubPrivateKey = privateKey.replace(/\\n/g, '\n').replace(/"/g, '');
const payload = {

View File

@ -1,4 +1,5 @@
import { asyncExecShell, saveBuildLog } from '$lib/common';
import { saveBuildLog } from "../buildPacks/common";
import { asyncExecShell } from "../common";
export default async function ({
applicationId,

View File

@ -0,0 +1,44 @@
import Bree from 'bree';
import path from 'path';
import Cabin from 'cabin';
import TSBree from '@breejs/ts-worker';
import { isDev } from './common';
Bree.extend(TSBree);
const options: any = {
defaultExtension: 'js',
logger: false,
workerMessageHandler: async ({ name, message }) => {
if (name === 'deployApplication') {
if (message.pending === 0) {
if (!scheduler.workers.has('autoUpdater')) {
await scheduler.stop('deployApplication');
await scheduler.run('autoUpdater')
}
}
}
},
jobs: [
{
name: 'deployApplication'
},
{
name: 'cleanupStorage',
interval: '10m'
},
{
name: 'checkProxies',
interval: '10s'
},
{
name: 'autoUpdater',
}
],
};
if (isDev) options.root = path.join(__dirname, '../jobs');
export const scheduler = new Bree(options);

View File

@ -0,0 +1,407 @@
// Example:
// export const nocodb = [{
// name: 'postgreslUser',
// isEditable: false,
// isLowerCase: false,
// isNumber: false,
// isBoolean: false,
// isEncrypted: false
// }]
export const plausibleAnalytics = [{
name: 'email',
isEditable: true,
isLowerCase: true,
isNumber: false,
isBoolean: false,
isEncrypted: false
},{
name: 'username',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'password',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'postgresqlUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'postgresqlDatabase',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlPublicPort',
isEditable: false,
isLowerCase: false,
isNumber: true,
isBoolean: false,
isEncrypted: false
},
{
name: 'secretKeyBase',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'scriptName',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
}]
export const minio = [{
name: 'apiFqdn',
isEditable: true,
isLowerCase: true,
isNumber: false,
isBoolean: false,
isEncrypted: false
},{
name: 'rootUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'rootUserPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
}]
export const vscodeserver = [{
name: 'password',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
}]
export const wordpress = [{
name: 'extraConfig',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'mysqlHost',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'mysqlPort',
isEditable: true,
isLowerCase: false,
isNumber: true,
isBoolean: false,
isEncrypted: false
},
{
name: 'mysqlUser',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'mysqlPassword',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'mysqlRootUser',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'mysqlRootUserPassword',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'mysqlDatabase',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
}]
export const ghost = [{
name: 'defaultEmail',
isEditable: false,
isLowerCase: true,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'defaultPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'mariadbUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'mariadbPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'mariadbRootUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'mariadbRootUserPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'mariadbDatabase',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
}]
export const meiliSearch = [{
name: 'masterKey',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
}]
export const umami = [{
name: 'postgresqlUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'postgresqlDatabase',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'umamiAdminPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'hashSalt',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
}]
export const hasura = [{
name: 'postgresqlUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'postgresqlDatabase',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'graphQLAdminPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
}]
export const fider = [{
name: 'jwtSecret',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},{
name: 'postgreslUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'emailNoreply',
isEditable: true,
isLowerCase: true,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'emailSmtpHost',
isEditable: true,
isLowerCase: true,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'emailSmtpPassword',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'emailSmtpPort',
isEditable: true,
isLowerCase: false,
isNumber: true,
isBoolean: false,
isEncrypted: false
},
{
name: 'emailSmtpUser',
isEditable: true,
isLowerCase: true,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'emailSmtpEnableStartTls',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: true,
isEncrypted: false
},
{
name: 'emailMailgunApiKey',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'emailMailgunDomain',
isEditable: true,
isLowerCase: true,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'emailMailgunRegion',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
}]

View File

@ -0,0 +1,34 @@
import fp from 'fastify-plugin'
import fastifyJwt, { FastifyJWTOptions } from '@fastify/jwt'
declare module "@fastify/jwt" {
interface FastifyJWT {
user: {
userId: string,
teamId: string,
permission: string,
isAdmin: boolean
}
}
}
export default fp<FastifyJWTOptions>(async (fastify, opts) => {
fastify.register(fastifyJwt, {
secret: fastify.config.COOLIFY_SECRET_KEY
})
fastify.decorate("authenticate", async function (request, reply) {
try {
await request.jwtVerify()
} catch (err) {
console.log(err)
reply.send(err)
}
})
})
declare module 'fastify' {
export interface FastifyInstance {
authenticate(): Promise<void>
}
}

View File

@ -0,0 +1,871 @@
import cuid from 'cuid';
import crypto from 'node:crypto'
import jsonwebtoken from 'jsonwebtoken';
import axios from 'axios';
import { day } from '../../../../lib/dayjs';
import type { FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify';
import { CheckDNS, DeleteApplication, DeployApplication, GetApplication, SaveApplication, SaveApplicationSettings } from '.';
import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
import { asyncExecShell, checkDomainsIsValidInDNS, checkDoubleBranch, decrypt, encrypt, errorHandler, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, prisma, stopBuild, uniqueName } from '../../../../lib/common';
import { checkContainer, dockerInstance, getEngine, isContainerExited, removeContainer } from '../../../../lib/docker';
import { scheduler } from '../../../../lib/scheduler';
export async function listApplications(request: FastifyRequest) {
try {
const { teamId } = request.user
const applications = await prisma.application.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { teams: true }
});
const settings = await prisma.setting.findFirst()
return {
applications,
settings
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getApplication(request: FastifyRequest<GetApplication>) {
try {
const { id } = request.params
const { teamId } = request.user
const appId = process.env['COOLIFY_APP_ID'];
let isRunning = false;
let isExited = false;
const application = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId && application.destinationDocker?.engine) {
isRunning = await checkContainer(application.destinationDocker.engine, id);
isExited = await isContainerExited(application.destinationDocker.engine, id);
}
return {
isQueueActive: scheduler.workers.has('deployApplication'),
isRunning,
isExited,
application,
appId
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function newApplication(request: FastifyRequest, reply: FastifyReply) {
try {
const name = uniqueName();
const { teamId } = request.user
const { id } = await prisma.application.create({
data: {
name,
teams: { connect: { id: teamId } },
settings: { create: { debug: false, previews: false } }
}
});
return reply.code(201).send({ id });
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
function decryptApplication(application: any) {
if (application) {
if (application?.gitSource?.githubApp?.clientSecret) {
application.gitSource.githubApp.clientSecret = decrypt(application.gitSource.githubApp.clientSecret) || null;
}
if (application?.gitSource?.githubApp?.webhookSecret) {
application.gitSource.githubApp.webhookSecret = decrypt(application.gitSource.githubApp.webhookSecret) || null;
}
if (application?.gitSource?.githubApp?.privateKey) {
application.gitSource.githubApp.privateKey = decrypt(application.gitSource.githubApp.privateKey) || null;
}
if (application?.gitSource?.gitlabApp?.appSecret) {
application.gitSource.gitlabApp.appSecret = decrypt(application.gitSource.gitlabApp.appSecret) || null;
}
if (application?.secrets.length > 0) {
application.secrets = application.secrets.map((s: any) => {
s.value = decrypt(s.value) || null
return s;
});
}
return application;
}
}
export async function getApplicationFromDB(id: string, teamId: string) {
try {
let application = await prisma.application.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: {
destinationDocker: true,
settings: true,
gitSource: { include: { githubApp: true, gitlabApp: true } },
secrets: true,
persistentStorage: true
}
});
if (!application) {
throw { status: 404, message: 'Application not found.' };
}
application = decryptApplication(application);
const buildPack = application?.buildPack || null;
const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage(
buildPack
);
// Set default build images
if (!application.baseImage) {
application.baseImage = baseImage;
}
if (!application.baseBuildImage) {
application.baseBuildImage = baseBuildImage;
}
return { ...application, baseBuildImages, baseImages };
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getApplicationFromDBWebhook(projectId: number, branch: string) {
try {
let application = await prisma.application.findFirst({
where: { projectId, branch, settings: { autodeploy: true } },
include: {
destinationDocker: true,
settings: true,
gitSource: { include: { githubApp: true, gitlabApp: true } },
secrets: true,
persistentStorage: true
}
});
if (!application) {
throw { status: 500, message: 'Application not configured.' }
}
application = decryptApplication(application);
const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage(
application.buildPack
);
// Set default build images
if (!application.baseImage) {
application.baseImage = baseImage;
}
if (!application.baseBuildImage) {
application.baseBuildImage = baseBuildImage;
}
return { ...application, baseBuildImages, baseImages };
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveApplication(request: FastifyRequest<SaveApplication>, reply: FastifyReply) {
try {
const { id } = request.params
let {
name,
buildPack,
fqdn,
port,
exposePort,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory,
pythonWSGI,
pythonModule,
pythonVariable,
dockerFileLocation,
denoMainFile,
denoOptions,
baseImage,
baseBuildImage
} = request.body
if (port) port = Number(port);
if (exposePort) {
exposePort = Number(exposePort);
}
if (denoOptions) denoOptions = denoOptions.trim();
const defaultConfiguration = await setDefaultConfiguration({
buildPack,
port,
installCommand,
startCommand,
buildCommand,
publishDirectory,
baseDirectory,
dockerFileLocation,
denoMainFile
});
await prisma.application.update({
where: { id },
data: {
name,
fqdn,
exposePort,
pythonWSGI,
pythonModule,
pythonVariable,
denoOptions,
baseImage,
baseBuildImage,
...defaultConfiguration
}
});
return reply.code(201).send();
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveApplicationSettings(request: FastifyRequest<SaveApplicationSettings>, reply: FastifyReply) {
try {
const { id } = request.params
const { debug, previews, dualCerts, autodeploy, branch, projectId } = request.body
const isDouble = await checkDoubleBranch(branch, projectId);
if (isDouble && autodeploy) {
throw { status: 500, message: 'Application not configured.' }
}
await prisma.application.update({
where: { id },
data: { settings: { update: { debug, previews, dualCerts, autodeploy } } },
include: { destinationDocker: true }
});
return reply.code(201).send();
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function stopApplication(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params
const { teamId } = request.user
const application = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId && application.destinationDocker?.engine) {
const { engine } = application.destinationDocker;
const found = await checkContainer(engine, id);
if (found) {
await removeContainer({ id, engine });
}
}
return reply.code(201).send();
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deleteApplication(request: FastifyRequest<DeleteApplication>, reply: FastifyReply) {
try {
const { id } = request.params
const { teamId } = request.user
const application = await prisma.application.findUnique({
where: { id },
include: { destinationDocker: true }
});
if (application?.destinationDockerId && application.destinationDocker?.engine && application.destinationDocker?.network) {
const host = getEngine(application.destinationDocker.engine);
const { stdout: containers } = await asyncExecShell(
`DOCKER_HOST=${host} docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'`
);
if (containers) {
const containersArray = containers.trim().split('\n');
for (const container of containersArray) {
const containerObj = JSON.parse(container);
const id = containerObj.ID;
await removeContainer({ id, engine: application.destinationDocker.engine });
}
}
}
await prisma.applicationSettings.deleteMany({ where: { application: { id } } });
await prisma.buildLog.deleteMany({ where: { applicationId: id } });
await prisma.build.deleteMany({ where: { applicationId: id } });
await prisma.secret.deleteMany({ where: { applicationId: id } });
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } });
if (teamId === '0') {
await prisma.application.deleteMany({ where: { id } });
} else {
await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } });
}
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function checkDNS(request: FastifyRequest<CheckDNS>) {
try {
const { id } = request.params
let { exposePort, fqdn, forceSave, dualCerts } = request.body
fqdn = fqdn.toLowerCase();
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
const found = await isDomainConfigured({ id, fqdn });
if (found) {
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
}
if (exposePort) {
exposePort = Number(exposePort);
if (exposePort < 1024 || exposePort > 65535) {
throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` }
}
const { default: getPort } = await import('get-port');
const publicPort = await getPort({ port: exposePort });
if (publicPort !== exposePort) {
throw { status: 500, message: `Port ${exposePort} is already in use.` }
}
}
if (isDNSCheckEnabled && !isDev && !forceSave) {
return await checkDomainsIsValidInDNS({ hostname: request.hostname, fqdn, dualCerts });
}
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getUsage(request) {
try {
const { id } = request.params
const teamId = request.user?.teamId;
const application = await getApplicationFromDB(id, teamId);
let usage = {};
if (application.destinationDockerId) {
[usage] = await Promise.all([getContainerUsage(application.destinationDocker.engine, id)]);
}
return {
usage
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deployApplication(request: FastifyRequest<DeployApplication>) {
try {
const { id } = request.params
const teamId = request.user?.teamId;
const { pullmergeRequestId = null, branch } = request.body
const buildId = cuid();
const application = await getApplicationFromDB(id, teamId);
if (application) {
if (!application?.configHash) {
const configHash = crypto.createHash('sha256')
.update(
JSON.stringify({
buildPack: application.buildPack,
port: application.port,
exposePort: application.exposePort,
installCommand: application.installCommand,
buildCommand: application.buildCommand,
startCommand: application.startCommand
})
)
.digest('hex');
await prisma.application.update({ where: { id }, data: { configHash } });
}
await prisma.application.update({ where: { id }, data: { updatedAt: new Date() } });
await prisma.build.create({
data: {
id: buildId,
applicationId: id,
branch: application.branch,
destinationDockerId: application.destinationDocker?.id,
gitSourceId: application.gitSource?.id,
githubAppId: application.gitSource?.githubApp?.id,
gitlabAppId: application.gitSource?.gitlabApp?.id,
status: 'queued',
type: 'manual'
}
});
if (pullmergeRequestId) {
scheduler.workers.get('deployApplication').postMessage({
build_id: buildId,
type: 'manual',
...application,
sourceBranch: branch,
pullmergeRequestId
});
} else {
scheduler.workers.get('deployApplication').postMessage({
build_id: buildId,
type: 'manual',
...application
});
}
return {
buildId
};
}
throw { status: 500, message: 'Application not found!' }
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveApplicationSource(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params
const { gitSourceId } = request.body
await prisma.application.update({
where: { id },
data: { gitSource: { connect: { id: gitSourceId } } }
});
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getGitHubToken(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params
const { teamId } = request.user
const application = await getApplicationFromDB(id, teamId);
const payload = {
iat: Math.round(new Date().getTime() / 1000),
exp: Math.round(new Date().getTime() / 1000 + 60),
iss: application.gitSource.githubApp.appId
};
const githubToken = jsonwebtoken.sign(payload, application.gitSource.githubApp.privateKey, {
algorithm: 'RS256'
});
const { data } = await axios.post(`${application.gitSource.apiUrl}/app/installations/${application.gitSource.githubApp.installationId}/access_tokens`, {}, {
headers: {
Authorization: `Bearer ${githubToken}`
}
})
return reply.code(201).send({
token: data.token
})
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function checkRepository(request: FastifyRequest) {
try {
const { id } = request.params
const { repository, branch } = request.query
const application = await prisma.application.findUnique({
where: { id },
include: { gitSource: true }
});
const found = await prisma.application.findFirst({
where: { branch, repository, gitSource: { type: application.gitSource.type }, id: { not: id } }
});
return {
used: found ? true : false
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveRepository(request, reply) {
try {
const { id } = request.params
let { repository, branch, projectId, autodeploy, webhookToken } = request.body
repository = repository.toLowerCase();
branch = branch.toLowerCase();
projectId = Number(projectId);
if (webhookToken) {
await prisma.application.update({
where: { id },
data: { repository, branch, projectId, gitSource: { update: { gitlabApp: { update: { webhookToken: webhookToken ? webhookToken : undefined } } } }, settings: { update: { autodeploy } } }
});
} else {
await prisma.application.update({
where: { id },
data: { repository, branch, projectId, settings: { update: { autodeploy } } }
});
}
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveDestination(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params
const { destinationId } = request.body
await prisma.application.update({
where: { id },
data: { destinationDocker: { connect: { id: destinationId } } }
});
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getBuildPack(request) {
try {
const { id } = request.params
const teamId = request.user?.teamId;
const application = await getApplicationFromDB(id, teamId);
return {
type: application.gitSource.type,
projectId: application.projectId,
repository: application.repository,
branch: application.branch,
apiUrl: application.gitSource.apiUrl
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveBuildPack(request, reply) {
try {
const { id } = request.params
const { buildPack } = request.body
await prisma.application.update({ where: { id }, data: { buildPack } });
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getSecrets(request: FastifyRequest) {
try {
const { id } = request.params
let secrets = await prisma.secret.findMany({
where: { applicationId: id },
orderBy: { createdAt: 'desc' }
});
secrets = secrets.map((secret) => {
secret.value = decrypt(secret.value);
return secret;
});
secrets = secrets.filter((secret) => !secret.isPRMRSecret).sort((a, b) => {
return ('' + a.name).localeCompare(b.name);
})
return {
secrets
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveSecret(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params
let { name, value, isBuildSecret, isPRMRSecret, isNew } = request.body
if (isNew) {
const found = await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } });
if (found) {
throw { status: 500, message: `Secret ${name} already exists.` }
} else {
value = encrypt(value);
await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
});
}
} else {
value = encrypt(value);
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
if (found) {
await prisma.secret.updateMany({
where: { applicationId: id, name, isPRMRSecret },
data: { value, isBuildSecret, isPRMRSecret }
});
} else {
await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
});
}
}
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deleteSecret(request: FastifyRequest) {
try {
const { id } = request.params
const { name } = request.body
await prisma.secret.deleteMany({ where: { applicationId: id, name } });
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getStorages(request: FastifyRequest) {
try {
const { id } = request.params
const persistentStorages = await prisma.applicationPersistentStorage.findMany({ where: { applicationId: id } });
return {
persistentStorages
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveStorage(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params
const { path, newStorage, storageId } = request.body
if (newStorage) {
await prisma.applicationPersistentStorage.create({
data: { path, application: { connect: { id } } }
});
} else {
await prisma.applicationPersistentStorage.update({
where: { id: storageId },
data: { path }
});
}
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deleteStorage(request: FastifyRequest) {
try {
const { id } = request.params
const { path } = request.body
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id, path } });
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getPreviews(request: FastifyRequest) {
try {
const { id } = request.params
const { teamId } = request.user
let secrets = await prisma.secret.findMany({
where: { applicationId: id },
orderBy: { createdAt: 'desc' }
});
secrets = secrets.map((secret) => {
secret.value = decrypt(secret.value);
return secret;
});
const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret);
const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret);
const destinationDocker = await prisma.destinationDocker.findFirst({
where: { application: { some: { id } }, teams: { some: { id: teamId } } }
});
const docker = dockerInstance({ destinationDocker });
const listContainers = await docker.engine.listContainers({
filters: { network: [destinationDocker.network], name: [id] }
});
const containers = listContainers.filter((container) => {
return (
container.Labels['coolify.configuration'] &&
container.Labels['coolify.type'] === 'standalone-application'
);
});
const jsonContainers = containers
.map((container) =>
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
)
.filter((container) => {
return container.pullmergeRequestId && container.applicationId === id;
});
return {
containers: jsonContainers,
applicationSecrets: applicationSecrets.sort((a, b) => {
return ('' + a.name).localeCompare(b.name);
}),
PRMRSecrets: PRMRSecrets.sort((a, b) => {
return ('' + a.name).localeCompare(b.name);
})
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getApplicationLogs(request: FastifyRequest) {
try {
const { id } = request.params
let { since = 0 } = request.query
if (since !== 0) {
since = day(since).unix();
}
const { destinationDockerId, destinationDocker } = await prisma.application.findUnique({
where: { id },
include: { destinationDocker: true }
});
if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker });
try {
const container = await docker.engine.getContainer(id);
if (container) {
const { default: ansi } = await import('strip-ansi')
const logs = (
await container.logs({
stdout: true,
stderr: true,
timestamps: true,
since,
tail: 5000
})
)
.toString()
.split('\n')
.map((l) => ansi(l.slice(8)))
.filter((a) => a);
return {
logs
};
}
} catch (error) {
return {
logs: []
};
}
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getBuildLogs(request: FastifyRequest) {
try {
const { id } = request.params
let { buildId, skip = 0 } = request.query
if (typeof skip !== 'number') {
skip = Number(skip)
}
let builds = [];
const buildCount = await prisma.build.count({ where: { applicationId: id } });
if (buildId) {
builds = await prisma.build.findMany({ where: { applicationId: id, id: buildId } });
} else {
builds = await prisma.build.findMany({
where: { applicationId: id },
orderBy: { createdAt: 'desc' },
take: 5,
skip
});
}
builds = builds.map((build) => {
const updatedAt = day(build.updatedAt).utc();
build.took = updatedAt.diff(day(build.createdAt)) / 1000;
build.since = updatedAt.fromNow();
return build;
});
return {
builds,
buildCount
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getBuildIdLogs(request: FastifyRequest) {
try {
const { id, buildId } = request.params
let { sequence = 0 } = request.query
if (typeof sequence !== 'number') {
sequence = Number(sequence)
}
let logs = await prisma.buildLog.findMany({
where: { buildId, time: { gt: sequence } },
orderBy: { time: 'asc' }
});
const data = await prisma.build.findFirst({ where: { id: buildId } });
return {
logs,
status: data?.status || 'queued'
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getGitLabSSHKey(request: FastifyRequest) {
try {
const { id } = request.params
const application = await prisma.application.findUnique({
where: { id },
include: { gitSource: { include: { gitlabApp: true } } }
});
return { publicKey: application.gitSource.gitlabApp.publicSshKey };
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveGitLabSSHKey(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params
const application = await prisma.application.findUnique({
where: { id },
include: { gitSource: { include: { gitlabApp: true } } }
});
if (!application.gitSource?.gitlabApp?.privateSshKey) {
const keys = await generateSshKeyPair();
const encryptedPrivateKey = encrypt(keys.privateKey);
await prisma.gitlabApp.update({
where: { id: application.gitSource.gitlabApp.id },
data: { privateSshKey: encryptedPrivateKey, publicSshKey: keys.publicKey }
});
return reply.code(201).send({ publicKey: keys.publicKey })
}
return { message: 'SSH key already exists' }
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveDeployKey(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params
let { deployKeyId } = request.body;
deployKeyId = Number(deployKeyId);
const application = await prisma.application.findUnique({
where: { id },
include: { gitSource: { include: { gitlabApp: true } } }
});
await prisma.gitlabApp.update({
where: { id: application.gitSource.gitlabApp.id },
data: { deployKeyId }
});
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function cancelDeployment(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params
const { buildId, applicationId } = request.body;
if (!buildId) {
throw { status: 500, message: 'buildId is required' }
}
await stopBuild(buildId, applicationId);
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@ -0,0 +1,89 @@
import { FastifyPluginAsync } from 'fastify';
import { cancelDeployment, checkDNS, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication } from './handlers';
export interface GetApplication {
Params: { id: string; }
}
export interface SaveApplication {
Params: { id: string; },
Body: any
}
export interface SaveApplicationSettings {
Params: { id: string; };
Querystring: { domain: string; };
Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; };
}
export interface DeleteApplication {
Params: { id: string; };
Querystring: { domain: string; };
}
export interface CheckDNS {
Params: { id: string; };
Querystring: { domain: string; };
}
export interface DeployApplication {
Params: { id: string },
Querystring: { domain: string }
Body: { pullmergeRequestId: string | null, branch: string }
}
const root: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.addHook('onRequest', async (request, reply) => {
return await request.jwtVerify()
})
fastify.get('/', async (request) => await listApplications(request));
fastify.post('/new', async (request, reply) => await newApplication(request, reply));
fastify.get<GetApplication>('/:id', async (request) => await getApplication(request));
fastify.post<SaveApplication>('/:id', async (request, reply) => await saveApplication(request, reply));
fastify.delete<DeleteApplication>('/:id', async (request, reply) => await deleteApplication(request, reply));
fastify.post('/:id/stop', async (request, reply) => await stopApplication(request, reply));
fastify.post<SaveApplicationSettings>('/:id/settings', async (request, reply) => await saveApplicationSettings(request, reply));
fastify.post<SaveApplicationSettings>('/:id/check', async (request) => await checkDNS(request));
fastify.get('/:id/secrets', async (request) => await getSecrets(request));
fastify.post('/:id/secrets', async (request, reply) => await saveSecret(request, reply));
fastify.delete('/:id/secrets', async (request) => await deleteSecret(request));
fastify.get('/:id/storages', async (request) => await getStorages(request));
fastify.post('/:id/storages', async (request, reply) => await saveStorage(request, reply));
fastify.delete('/:id/storages', async (request) => await deleteStorage(request));
fastify.get('/:id/previews', async (request) => await getPreviews(request));
fastify.get('/:id/logs', async (request) => await getApplicationLogs(request));
fastify.get('/:id/logs/build', async (request) => await getBuildLogs(request));
fastify.get('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request));
fastify.get<DeployApplication>('/:id/usage', async (request) => await getUsage(request))
fastify.post<DeployApplication>('/:id/deploy', async (request) => await deployApplication(request))
fastify.post('/:id/cancel', async (request, reply) => await cancelDeployment(request, reply));
fastify.post('/:id/configuration/source', async (request, reply) => await saveApplicationSource(request, reply));
fastify.get('/:id/configuration/repository', async (request) => await checkRepository(request));
fastify.post('/:id/configuration/repository', async (request, reply) => await saveRepository(request, reply));
fastify.post('/:id/configuration/destination', async (request, reply) => await saveDestination(request, reply));
fastify.get('/:id/configuration/buildpack', async (request) => await getBuildPack(request));
fastify.post('/:id/configuration/buildpack', async (request, reply) => await saveBuildPack(request, reply));
fastify.get('/:id/configuration/sshkey', async (request) => await getGitLabSSHKey(request));
fastify.post('/:id/configuration/sshkey', async (request, reply) => await saveGitLabSSHKey(request, reply));
fastify.post('/:id/configuration/deploykey', async (request, reply) => await saveDeployKey(request, reply));
fastify.get('/:id/configuration/githubToken', async (request, reply) => await getGitHubToken(request, reply));
};
export default root;

View File

@ -0,0 +1,471 @@
import cuid from 'cuid';
import type { FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify';
import yaml from 'js-yaml';
import fs from 'fs/promises';
import { asyncExecShell, ComposeFile, createDirectories, decrypt, encrypt, errorHandler, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePort, listSettings, makeLabelForStandaloneDatabase, prisma, startTcpProxy, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common';
import { dockerInstance, getEngine } from '../../../../lib/docker';
import { day } from '../../../../lib/dayjs';
export async function listDatabases(request: FastifyRequest) {
try {
const userId = request.user.userId;
const teamId = request.user.teamId;
let databases = []
if (teamId === '0') {
databases = await prisma.database.findMany({ include: { teams: true } });
} else {
databases = await prisma.database.findMany({
where: { teams: { some: { id: teamId } } },
include: { teams: true }
});
}
return {
databases
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function newDatabase(request: FastifyRequest, reply: FastifyReply) {
try {
const teamId = request.user.teamId;
const name = uniqueName();
const dbUser = cuid();
const dbUserPassword = encrypt(generatePassword());
const rootUser = cuid();
const rootUserPassword = encrypt(generatePassword());
const defaultDatabase = cuid();
const { id } = await prisma.database.create({
data: {
name,
defaultDatabase,
dbUser,
dbUserPassword,
rootUser,
rootUserPassword,
teams: { connect: { id: teamId } },
settings: { create: { isPublic: false } }
}
});
return reply.code(201).send({ id })
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getDatabase(request: FastifyRequest) {
try {
const { id } = request.params;
const teamId = request.user.teamId;
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (!database) {
throw { status: 404, message: 'Database not found.' }
}
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
const { destinationDockerId, destinationDocker } = database;
let isRunning = false;
if (destinationDockerId) {
const host = getEngine(destinationDocker.engine);
try {
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}`
);
if (JSON.parse(stdout).Running) {
isRunning = true;
}
} catch (error) {
//
}
}
const configuration = generateDatabaseConfiguration(database);
const settings = await listSettings();
return {
privatePort: configuration?.privatePort,
database,
isRunning,
versions: await getDatabaseVersions(database.type),
settings
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getDatabaseTypes(request: FastifyRequest) {
try {
return {
types: supportedDatabaseTypesAndVersions
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveDatabaseType(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params;
const { type } = request.body;
await prisma.database.update({
where: { id },
data: { type }
});
return reply.code(201).send({})
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getVersions(request: FastifyRequest) {
try {
const teamId = request.user.teamId;
const { id } = request.params;
const { type } = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
return {
versions: supportedDatabaseTypesAndVersions.find((name) => name.name === type).versions
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveVersion(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params;
const { version } = request.body;
await prisma.database.update({
where: { id },
data: {
version,
}
});
return reply.code(201).send({})
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveDatabaseDestination(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params;
const { destinationId } = request.body;
await prisma.database.update({
where: { id },
data: { destinationDocker: { connect: { id: destinationId } } }
});
const {
destinationDockerId,
destinationDocker: { engine },
version,
type
} = await prisma.database.findUnique({ where: { id }, include: { destinationDocker: true } });
if (destinationDockerId) {
const host = getEngine(engine);
if (type && version) {
const baseImage = getDatabaseImage(type);
asyncExecShell(`DOCKER_HOST=${host} docker pull ${baseImage}:${version}`);
}
}
return reply.code(201).send({})
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getDatabaseUsage(request: FastifyRequest) {
try {
const { id } = request.params;
const teamId = request.user.teamId;
let usage = {};
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
if (database.destinationDockerId) {
[usage] = await Promise.all([getContainerUsage(database.destinationDocker.engine, id)]);
}
return {
usage
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function startDatabase(request: FastifyRequest) {
try {
const teamId = request.user.teamId;
const { id } = request.params;
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
const {
type,
destinationDockerId,
destinationDocker,
publicPort,
settings: { isPublic }
} = database;
const { privatePort, environmentVariables, image, volume, ulimits } =
generateDatabaseConfiguration(database);
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const engine = destinationDocker.engine;
const volumeName = volume.split(':')[0];
const labels = await makeLabelForStandaloneDatabase({ id, image, volume });
const { workdir } = await createDirectories({ repository: type, buildId: id });
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image,
networks: [network],
environment: environmentVariables,
volumes: [volume],
ulimits,
labels,
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[volumeName]: {
external: true
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
await asyncExecShell(`DOCKER_HOST=${host} docker volume create ${volumeName}`);
} catch (error) {
console.log(error);
}
try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
if (isPublic) await startTcpProxy(destinationDocker, id, publicPort, privatePort);
return {};
} catch (error) {
throw {
error
};
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function stopDatabase(request: FastifyRequest) {
try {
const teamId = request.user.teamId;
const { id } = request.params;
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
const everStarted = await stopDatabaseContainer(database);
if (everStarted) await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort);
await prisma.database.update({
where: { id },
data: {
settings: { upsert: { update: { isPublic: false }, create: { isPublic: false } } }
}
});
await prisma.database.update({ where: { id }, data: { publicPort: null } });
return {};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getDatabaseLogs(request: FastifyRequest) {
try {
const teamId = request.user.teamId;
const { id } = request.params;
let { since = 0 } = request.query
if (since !== 0) {
since = day(since).unix();
}
const { destinationDockerId, destinationDocker } = await prisma.database.findUnique({
where: { id },
include: { destinationDocker: true }
});
if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker });
try {
const container = await docker.engine.getContainer(id);
if (container) {
const { default: ansi } = await import('strip-ansi')
const logs = (
await container.logs({
stdout: true,
stderr: true,
timestamps: true,
since,
tail: 5000
})
)
.toString()
.split('\n')
.map((l) => ansi(l.slice(8)))
.filter((a) => a);
return {
logs
};
}
} catch (error) {
const { statusCode } = error;
if (statusCode === 404) {
return {
logs: []
};
}
}
}
return {
message: 'No logs found.'
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deleteDatabase(request: FastifyRequest) {
try {
const teamId = request.user.teamId;
const { id } = request.params;
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
if (database.destinationDockerId) {
const everStarted = await stopDatabaseContainer(database);
if (everStarted) await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort);
}
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
await prisma.database.delete({ where: { id } });
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveDatabase(request: FastifyRequest, reply: FastifyReply) {
try {
const teamId = request.user.teamId;
const { id } = request.params;
const {
name,
defaultDatabase,
dbUser,
dbUserPassword,
rootUser,
rootUserPassword,
version,
isRunning
} = request.body;
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
if (isRunning) {
if (database.dbUserPassword !== dbUserPassword) {
await updatePasswordInDb(database, dbUser, dbUserPassword, false);
} else if (database.rootUserPassword !== rootUserPassword) {
await updatePasswordInDb(database, rootUser, rootUserPassword, true);
}
}
const encryptedDbUserPassword = dbUserPassword && encrypt(dbUserPassword);
const encryptedRootUserPassword = rootUserPassword && encrypt(rootUserPassword);
await prisma.database.update({
where: { id },
data: {
name,
defaultDatabase,
dbUser,
dbUserPassword: encryptedDbUserPassword,
rootUser,
rootUserPassword: encryptedRootUserPassword,
version
}
});
return reply.code(201).send({})
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveDatabaseSettings(request: FastifyRequest) {
try {
const teamId = request.user.teamId;
const { id } = request.params;
const { isPublic, appendOnly = true } = request.body;
const publicPort = await getFreePort();
const settings = await listSettings();
await prisma.database.update({
where: { id },
data: {
settings: { upsert: { update: { isPublic, appendOnly }, create: { isPublic, appendOnly } } }
}
});
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
const { destinationDockerId, destinationDocker, publicPort: oldPublicPort } = database;
const { privatePort } = generateDatabaseConfiguration(database);
if (destinationDockerId) {
if (isPublic) {
await prisma.database.update({ where: { id }, data: { publicPort } });
if (settings.isTraefikUsed) {
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
} else {
await startTcpProxy(destinationDocker, id, publicPort, privatePort);
}
} else {
await prisma.database.update({ where: { id }, data: { publicPort: null } });
await stopTcpHttpProxy(id, destinationDocker, oldPublicPort);
}
}
return { publicPort }
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@ -0,0 +1,32 @@
import { FastifyPluginAsync } from 'fastify';
import { deleteDatabase, getDatabase, getDatabaseLogs, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
const root: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.addHook('onRequest', async (request, reply) => {
return await request.jwtVerify()
})
fastify.get('/', async (request) => await listDatabases(request));
fastify.post('/new', async (request, reply) => await newDatabase(request, reply));
fastify.get('/:id', async (request) => await getDatabase(request));
fastify.post('/:id', async (request, reply) => await saveDatabase(request, reply));
fastify.delete('/:id', async (request) => await deleteDatabase(request));
fastify.post('/:id/settings', async (request) => await saveDatabaseSettings(request));
fastify.get('/:id/configuration/type', async (request) => await getDatabaseTypes(request));
fastify.post('/:id/configuration/type', async (request, reply) => await saveDatabaseType(request, reply));
fastify.get('/:id/configuration/version', async (request) => await getVersions(request));
fastify.post('/:id/configuration/version', async (request, reply) => await saveVersion(request, reply));
fastify.post('/:id/configuration/destination', async (request, reply) => await saveDatabaseDestination(request, reply));
fastify.get('/:id/usage', async (request) => await getDatabaseUsage(request));
fastify.get('/:id/logs', async (request) => await getDatabaseLogs(request));
fastify.post('/:id/start', async (request) => await startDatabase(request));
fastify.post('/:id/stop', async (request) => await stopDatabase(request));
};
export default root;

View File

@ -0,0 +1,200 @@
import type { FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify';
import { asyncExecShell, errorHandler, listSettings, prisma, startCoolifyProxy, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common';
import { checkContainer, dockerInstance, getEngine } from '../../../../lib/docker';
export async function listDestinations(request: FastifyRequest) {
try {
const teamId = request.user.teamId;
let destinations = []
if (teamId === '0') {
destinations = await prisma.destinationDocker.findMany({ include: { teams: true } });
} else {
destinations = await prisma.destinationDocker.findMany({
where: { teams: { some: { id: teamId } } },
include: { teams: true }
});
}
return {
destinations
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function checkDestination(request: FastifyRequest) {
try {
const { network } = request.body;
const found = await prisma.destinationDocker.findFirst({ where: { network } });
if (found) {
throw {
message: `Network already exists: ${network}`
};
}
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getDestination(request: FastifyRequest) {
try {
const { id } = request.params
const teamId = request.user?.teamId;
const destination = await prisma.destinationDocker.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }
});
if (!destination) {
throw { status: 404, message: `Destination not found.` };
}
const settings = await listSettings();
let payload = {
destination,
settings,
state: false
};
if (destination?.remoteEngine) {
// const { stdout } = await asyncExecShell(
// `ssh -p ${destination.port} ${destination.user}@${destination.ipAddress} "docker ps -a"`
// );
// console.log(stdout)
// const engine = await generateRemoteEngine(destination);
// // await saveSshKey(destination);
// payload.state = await checkContainer(engine, 'coolify-haproxy');
} else {
let containerName = 'coolify-proxy';
if (!settings.isTraefikUsed) {
containerName = 'coolify-haproxy';
}
payload.state =
destination?.engine && (await checkContainer(destination.engine, containerName));
}
return {
...payload
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function newDestination(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params
let { name, network, engine, isCoolifyProxyUsed } = request.body
const teamId = request.user.teamId;
if (id === 'new') {
const host = getEngine(engine);
const docker = dockerInstance({ destinationDocker: { engine, network } });
const found = await docker.engine.listNetworks({ filters: { name: [`^${network}$`] } });
if (found.length === 0) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
}
await prisma.destinationDocker.create({
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed }
});
const destinations = await prisma.destinationDocker.findMany({ where: { engine } });
const destination = destinations.find((destination) => destination.network === network);
if (destinations.length > 0) {
const proxyConfigured = destinations.find(
(destination) => destination.network !== network && destination.isCoolifyProxyUsed === true
);
if (proxyConfigured) {
isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed;
}
await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } });
}
if (isCoolifyProxyUsed) {
const settings = await prisma.setting.findFirst();
if (settings?.isTraefikUsed) {
await startTraefikProxy(engine);
} else {
await startCoolifyProxy(engine);
}
}
return reply.code(201).send({ id: destination.id });
} else {
await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } });
return reply.code(201).send();
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deleteDestination(request: FastifyRequest) {
try {
const { id } = request.params
const destination = await prisma.destinationDocker.delete({ where: { id } });
if (destination.isCoolifyProxyUsed) {
const host = getEngine(destination.engine);
const { network } = destination;
const settings = await prisma.setting.findFirst();
const containerName = settings.isTraefikUsed ? 'coolify-proxy' : 'coolify-haproxy';
const { stdout: found } = await asyncExecShell(
`DOCKER_HOST=${host} docker ps -a --filter network=${network} --filter name=${containerName} --format '{{.}}'`
);
if (found) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker network disconnect ${network} ${containerName}`
);
await asyncExecShell(`DOCKER_HOST="${host}" docker network rm ${network}`);
}
}
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveDestinationSettings(request: FastifyRequest, reply: FastifyReply) {
try {
const { engine, isCoolifyProxyUsed } = request.body;
await prisma.destinationDocker.updateMany({
where: { engine },
data: { isCoolifyProxyUsed }
});
return {
status: 202
}
// return reply.code(201).send();
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function startProxy(request: FastifyRequest, reply: FastifyReply) {
const { engine } = request.body;
try {
await startTraefikProxy(engine);
return {}
} catch ({ status, message }) {
await stopTraefikProxy(engine);
return errorHandler({ status, message })
}
}
export async function stopProxy(request: FastifyRequest, reply: FastifyReply) {
const settings = await prisma.setting.findFirst({});
const { engine } = request.body;
try {
await stopTraefikProxy(engine);
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function restartProxy(request: FastifyRequest, reply: FastifyReply) {
const settings = await prisma.setting.findFirst({});
const { engine } = request.body;
try {
await stopTraefikProxy(engine);
await startTraefikProxy(engine);
await prisma.destinationDocker.updateMany({
where: { engine },
data: { isCoolifyProxyUsed: true }
});
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@ -0,0 +1,24 @@
import { FastifyPluginAsync } from 'fastify';
import { checkDestination, deleteDestination, getDestination, listDestinations, newDestination, restartProxy, saveDestinationSettings, startProxy, stopProxy } from './handlers';
const root: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.addHook('onRequest', async (request, reply) => {
return await request.jwtVerify()
})
fastify.get('/', async (request) => await listDestinations(request));
fastify.post('/check', async (request) => await checkDestination(request));
fastify.get('/:id', async (request) => await getDestination(request));
fastify.post('/:id', async (request, reply) => await newDestination(request, reply));
fastify.delete('/:id', async (request) => await deleteDestination(request));
fastify.post('/:id/settings', async (request, reply) => await saveDestinationSettings(request, reply));
fastify.post('/:id/start', async (request, reply) => await startProxy(request, reply));
fastify.post('/:id/stop', async (request, reply) => await stopProxy(request, reply));
fastify.post('/:id/restart', async (request, reply) => await restartProxy(request, reply));
};
export default root;

View File

@ -0,0 +1,280 @@
import os from 'node:os';
import osu from 'node-os-utils';
import axios from 'axios';
import compare from 'compare-versions';
import cuid from 'cuid';
import bcrypt from 'bcryptjs';
import { asyncExecShell, asyncSleep, errorHandler, isDev, prisma, uniqueName, version } from '../../../lib/common';
import type { FastifyReply, FastifyRequest } from 'fastify';
import type { Login, Update } from '.';
export async function hashPassword(password: string): Promise<string> {
const saltRounds = 15;
return bcrypt.hash(password, saltRounds);
}
export async function checkUpdate(request: FastifyRequest) {
try {
const currentVersion = version;
const { data: versions } = await axios.get(
`https://get.coollabs.io/versions.json?appId=${process.env['COOLIFY_APP_ID']}&version=${currentVersion}`
);
const latestVersion =
request.hostname === 'staging.coolify.io'
? versions['coolify'].next.version
: versions['coolify'].main.version;
const isUpdateAvailable = compare(latestVersion, currentVersion);
return {
isUpdateAvailable: isUpdateAvailable === 1,
latestVersion
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function update(request: FastifyRequest<Update>) {
const { latestVersion } = request.body;
try {
if (!isDev) {
const { isAutoUpdateEnabled } = (await prisma.setting.findFirst()) || {
isAutoUpdateEnabled: false
};
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"`
);
return {};
} else {
console.log(latestVersion);
await asyncSleep(2000);
return {};
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function showUsage() {
try {
return {
usage: {
uptime: os.uptime(),
memory: await osu.mem.info(),
cpu: {
load: os.loadavg(),
usage: await osu.cpu.usage(),
count: os.cpus().length
},
disk: await osu.drive.info('/')
}
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function showDashboard(request: FastifyRequest) {
try {
const userId = request.user.userId;
const teamId = request.user.teamId;
const applicationsCount = await prisma.application.count({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
});
const sourcesCount = await prisma.gitSource.count({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
});
const destinationsCount = await prisma.destinationDocker.count({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
});
const teamsCount = await prisma.permission.count({ where: { userId } });
const databasesCount = await prisma.database.count({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
});
const servicesCount = await prisma.service.count({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
});
const teams = await prisma.permission.findMany({
where: { userId },
include: { team: { include: { _count: { select: { users: true } } } } }
});
return {
teams,
applicationsCount,
sourcesCount,
destinationsCount,
teamsCount,
databasesCount,
servicesCount,
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function login(request: FastifyRequest<Login>, reply: FastifyReply) {
if (request.user) {
return reply.redirect('/dashboard');
} else {
const { email, password, isLogin } = request.body || {};
if (!email || !password) {
throw { status: 500, message: 'Email and password are required.' };
}
const users = await prisma.user.count();
const userFound = await prisma.user.findUnique({
where: { email },
include: { teams: true, permission: true },
rejectOnNotFound: false
});
if (!userFound && isLogin) {
throw { status: 500, message: 'User not found.' };
}
const { isRegistrationEnabled, id } = await prisma.setting.findFirst()
let uid = cuid();
let permission = 'read';
let isAdmin = false;
if (users === 0) {
await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } });
uid = '0';
}
if (userFound) {
if (userFound.type === 'email') {
// TODO: Review this one
if (userFound.password === 'RESETME') {
const hashedPassword = await hashPassword(password);
if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) {
await prisma.user.update({
where: { email: userFound.email },
data: { password: 'RESETTIMEOUT' }
});
throw {
status: 500,
message: 'Password reset link has expired. Please request a new one.'
};
} else {
await prisma.user.update({
where: { email: userFound.email },
data: { password: hashedPassword }
});
return {
userId: userFound.id,
teamId: userFound.id,
permission: userFound.permission,
isAdmin: true
};
}
}
const passwordMatch = await bcrypt.compare(password, userFound.password);
if (!passwordMatch) {
throw {
status: 500,
message: 'Wrong password or email address.'
};
}
uid = userFound.id;
isAdmin = true;
}
} else {
permission = 'owner';
isAdmin = true;
if (!isRegistrationEnabled) {
throw {
status: 404,
message: 'Registration disabled by administrator.'
};
}
const hashedPassword = await hashPassword(password);
if (users === 0) {
await prisma.user.create({
data: {
id: uid,
email,
password: hashedPassword,
type: 'email',
teams: {
create: {
id: uid,
name: uniqueName(),
destinationDocker: { connect: { network: 'coolify' } }
}
},
permission: { create: { teamId: uid, permission: 'owner' } }
},
include: { teams: true }
});
} else {
await prisma.user.create({
data: {
id: uid,
email,
password: hashedPassword,
type: 'email',
teams: {
create: {
id: uid,
name: uniqueName()
}
},
permission: { create: { teamId: uid, permission: 'owner' } }
},
include: { teams: true }
});
}
}
return {
userId: uid,
teamId: uid,
permission,
isAdmin
};
}
}
export async function getCurrentUser(request: FastifyRequest, fastify) {
let token = null
try {
const user = await prisma.user.findUnique({
where: { id: request.user.userId }
})
if (!user) {
throw "User not found";
}
} catch (error) {
throw { status: 401, message: error };
}
if (request.query.teamId) {
try {
const user = await prisma.user.findFirst({
where: { id: request.user.userId, teams: { some: { id: request.query.teamId } } },
include: { teams: true, permission: true }
})
if (user) {
const payload = {
...request.user,
teamId: request.query.teamId,
permission: user.permission.find(p => p.teamId === request.query.teamId).permission || null,
isAdmin: user.permission.find(p => p.teamId === request.query.teamId).permission === 'owner'
}
token = fastify.jwt.sign(payload)
}
} catch (error) {
// No new token -> not switching teams
}
}
return {
settings: await prisma.setting.findFirst(),
token,
...request.user
}
}

View File

@ -0,0 +1,453 @@
import cuid from 'cuid';
import type { FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify';
import { decrypt, errorHandler, prisma, uniqueName } from '../../../../lib/common';
import { day } from '../../../../lib/dayjs';
export async function listTeams(request: FastifyRequest) {
try {
const userId = request.user.userId;
const teamId = request.user.teamId;
const account = await prisma.user.findUnique({
where: { id: userId },
select: { id: true, email: true, teams: true }
});
let accounts = [];
let allTeams = [];
if (teamId === '0') {
accounts = await prisma.user.findMany({ select: { id: true, email: true, teams: true } });
allTeams = await prisma.team.findMany({
where: { users: { none: { id: userId } } },
include: { permissions: true }
});
}
const ownTeams = await prisma.team.findMany({
where: { users: { some: { id: userId } } },
include: { permissions: true }
});
const invitations = await prisma.teamInvitation.findMany({ where: { uid: userId } });
return {
ownTeams,
allTeams,
invitations,
account,
accounts
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deleteTeam(request: FastifyRequest, reply: FastifyReply) {
try {
const userId = request.user.userId;
const { id } = request.params;
const aloneInTeams = await prisma.team.findMany({ where: { users: { every: { id: userId } }, id } });
if (aloneInTeams.length > 0) {
for (const team of aloneInTeams) {
const applications = await prisma.application.findMany({
where: { teams: { every: { id: team.id } } }
});
if (applications.length > 0) {
for (const application of applications) {
await prisma.application.update({
where: { id: application.id },
data: { teams: { connect: { id: '0' } } }
});
}
}
const services = await prisma.service.findMany({
where: { teams: { every: { id: team.id } } }
});
if (services.length > 0) {
for (const service of services) {
await prisma.service.update({
where: { id: service.id },
data: { teams: { connect: { id: '0' } } }
});
}
}
const databases = await prisma.database.findMany({
where: { teams: { every: { id: team.id } } }
});
if (databases.length > 0) {
for (const database of databases) {
await prisma.database.update({
where: { id: database.id },
data: { teams: { connect: { id: '0' } } }
});
}
}
const sources = await prisma.gitSource.findMany({
where: { teams: { every: { id: team.id } } }
});
if (sources.length > 0) {
for (const source of sources) {
await prisma.gitSource.update({
where: { id: source.id },
data: { teams: { connect: { id: '0' } } }
});
}
}
const destinations = await prisma.destinationDocker.findMany({
where: { teams: { every: { id: team.id } } }
});
if (destinations.length > 0) {
for (const destination of destinations) {
await prisma.destinationDocker.update({
where: { id: destination.id },
data: { teams: { connect: { id: '0' } } }
});
}
}
await prisma.teamInvitation.deleteMany({ where: { teamId: team.id } });
await prisma.permission.deleteMany({ where: { teamId: team.id } });
// await prisma.user.delete({ where: { id } });
await prisma.team.delete({ where: { id: team.id } });
}
}
const notAloneInTeams = await prisma.team.findMany({ where: { users: { some: { id: userId } } } });
if (notAloneInTeams.length > 0) {
for (const team of notAloneInTeams) {
await prisma.team.update({
where: { id: team.id },
data: { users: { disconnect: { id } } }
});
}
}
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function newTeam(request: FastifyRequest, reply: FastifyReply) {
try {
const userId = request.user?.userId;
const name = uniqueName();
const { id } = await prisma.team.create({
data: {
name,
permissions: { create: { user: { connect: { id: userId } }, permission: 'owner' } },
users: { connect: { id: userId } }
}
});
return reply.code(201).send({ id })
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getTeam(request: FastifyRequest, reply: FastifyReply) {
try {
const userId = request.user.userId;
const teamId = request.user.teamId;
const { id } = request.params;
const user = await prisma.user.findFirst({
where: { id: userId, teams: teamId === '0' ? undefined : { some: { id } } },
include: { permission: true }
});
if (!user) return reply.code(401).send()
const permissions = await prisma.permission.findMany({
where: { teamId: id },
include: { user: { select: { id: true, email: true } } }
});
const team = await prisma.team.findUnique({ where: { id }, include: { permissions: true } });
const invitations = await prisma.teamInvitation.findMany({ where: { teamId: team.id } });
return {
team,
permissions,
invitations
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveTeam(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params;
const { name } = request.body;
await prisma.team.update({ where: { id }, data: { name: { set: name } } });
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
// export async function deleteUser(request: FastifyRequest, reply: FastifyReply) {
// try {
// const userId = request.user.userId;
// const { id } = request.params;
// const aloneInTeams = await prisma.team.findMany({ where: { users: { every: { id: userId } }, id } });
// if (aloneInTeams.length > 0) {
// for (const team of aloneInTeams) {
// const applications = await prisma.application.findMany({
// where: { teams: { every: { id: team.id } } }
// });
// if (applications.length > 0) {
// for (const application of applications) {
// await prisma.application.update({
// where: { id: application.id },
// data: { teams: { connect: { id: '0' } } }
// });
// }
// }
// const services = await prisma.service.findMany({
// where: { teams: { every: { id: team.id } } }
// });
// if (services.length > 0) {
// for (const service of services) {
// await prisma.service.update({
// where: { id: service.id },
// data: { teams: { connect: { id: '0' } } }
// });
// }
// }
// const databases = await prisma.database.findMany({
// where: { teams: { every: { id: team.id } } }
// });
// if (databases.length > 0) {
// for (const database of databases) {
// await prisma.database.update({
// where: { id: database.id },
// data: { teams: { connect: { id: '0' } } }
// });
// }
// }
// const sources = await prisma.gitSource.findMany({
// where: { teams: { every: { id: team.id } } }
// });
// if (sources.length > 0) {
// for (const source of sources) {
// await prisma.gitSource.update({
// where: { id: source.id },
// data: { teams: { connect: { id: '0' } } }
// });
// }
// }
// const destinations = await prisma.destinationDocker.findMany({
// where: { teams: { every: { id: team.id } } }
// });
// if (destinations.length > 0) {
// for (const destination of destinations) {
// await prisma.destinationDocker.update({
// where: { id: destination.id },
// data: { teams: { connect: { id: '0' } } }
// });
// }
// }
// await prisma.teamInvitation.deleteMany({ where: { teamId: team.id } });
// await prisma.permission.deleteMany({ where: { teamId: team.id } });
// await prisma.user.delete({ where: { id: userId } });
// await prisma.team.delete({ where: { id: team.id } });
// }
// }
// const notAloneInTeams = await prisma.team.findMany({ where: { users: { some: { id: userId } } } });
// if (notAloneInTeams.length > 0) {
// for (const team of notAloneInTeams) {
// await prisma.team.update({
// where: { id: team.id },
// data: { users: { disconnect: { id } } }
// });
// }
// }
// return reply.code(201).send()
// } catch (error) {
// console.log(error)
// throw { status: 500, message: error }
// }
// }
export async function inviteToTeam(request: FastifyRequest, reply: FastifyReply) {
try {
const userId = request.user.userId;
const { email, permission, teamId, teamName } = request.body;
const userFound = await prisma.user.findUnique({ where: { email } });
if (!userFound) {
throw `No user found with '${email}' email address.`
}
const uid = userFound.id;
if (uid === userId) {
throw `Invitation to yourself? Whaaaaat?`
}
const alreadyInTeam = await prisma.team.findFirst({
where: { id: teamId, users: { some: { id: uid } } }
});
if (alreadyInTeam) {
throw {
message: `Already in the team.`
};
}
const invitationFound = await prisma.teamInvitation.findFirst({ where: { uid, teamId } });
if (invitationFound) {
if (day().toDate() < day(invitationFound.createdAt).add(1, 'day').toDate()) {
throw 'Invitiation already pending on user confirmation.'
} else {
await prisma.teamInvitation.delete({ where: { id: invitationFound.id } });
await prisma.teamInvitation.create({
data: { email, uid, teamId, teamName, permission }
});
return reply.code(201).send({ message: 'Invitiation sent.' })
}
} else {
await prisma.teamInvitation.create({
data: { email, uid, teamId, teamName, permission }
});
return reply.code(201).send({ message: 'Invitiation sent.' })
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function acceptInvitation(request: FastifyRequest) {
try {
const userId = request.user.userId;
const { id } = request.body;
const invitation = await prisma.teamInvitation.findFirst({
where: { uid: userId },
rejectOnNotFound: true
});
await prisma.team.update({
where: { id: invitation.teamId },
data: { users: { connect: { id: userId } } }
});
await prisma.permission.create({
data: {
user: { connect: { id: userId } },
permission: invitation.permission,
team: { connect: { id: invitation.teamId } }
}
});
await prisma.teamInvitation.delete({ where: { id } });
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function revokeInvitation(request: FastifyRequest) {
try {
const { id } = request.body
await prisma.teamInvitation.delete({ where: { id } });
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function removeUser(request: FastifyRequest, reply: FastifyReply) {
try {
const { uid } = request.body;
const user = await prisma.user.findUnique({ where: { id: uid }, include: { teams: true, permission: true } });
if (user) {
const permissions = user.permission;
if (permissions.length > 0) {
for (const permission of permissions) {
await prisma.permission.deleteMany({ where: { id: permission.id, userId: uid } });
}
}
const teams = user.teams;
if (teams.length > 0) {
for (const team of teams) {
const newTeam = await prisma.team.update({
where: { id: team.id },
data: { users: { disconnect: { id: uid } } },
include: { applications: true, database: true, gitHubApps: true, gitLabApps: true, gitSources: true, destinationDocker: true, service: true, users: true }
});
if (newTeam.users.length === 0) {
if (newTeam.applications.length > 0) {
for (const application of newTeam.applications) {
await prisma.application.update({
where: { id: application.id },
data: { teams: { disconnect: { id: team.id }, connect: { id: '0' } } }
});
}
}
if (newTeam.database.length > 0) {
for (const database of newTeam.database) {
await prisma.database.update({
where: { id: database.id },
data: { teams: { disconnect: { id: team.id }, connect: { id: '0' } } }
});
}
}
if (newTeam.service.length > 0) {
for (const service of newTeam.service) {
await prisma.service.update({
where: { id: service.id },
data: { teams: { disconnect: { id: team.id }, connect: { id: '0' } } }
});
}
}
if (newTeam.gitHubApps.length > 0) {
for (const gitHubApp of newTeam.gitHubApps) {
await prisma.githubApp.update({
where: { id: gitHubApp.id },
data: { teams: { disconnect: { id: team.id }, connect: { id: '0' } } }
});
}
}
if (newTeam.gitLabApps.length > 0) {
for (const gitLabApp of newTeam.gitLabApps) {
await prisma.gitlabApp.update({
where: { id: gitLabApp.id },
data: { teams: { disconnect: { id: team.id }, connect: { id: '0' } } }
});
}
}
if (newTeam.gitSources.length > 0) {
for (const gitSource of newTeam.gitSources) {
await prisma.gitSource.update({
where: { id: gitSource.id },
data: { teams: { disconnect: { id: team.id }, connect: { id: '0' } } }
});
}
}
if (newTeam.destinationDocker.length > 0) {
for (const destinationDocker of newTeam.destinationDocker) {
await prisma.destinationDocker.update({
where: { id: destinationDocker.id },
data: { teams: { disconnect: { id: team.id }, connect: { id: '0' } } }
});
}
}
await prisma.team.delete({ where: { id: team.id } });
}
}
}
}
await prisma.user.delete({ where: { id: uid } });
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function setPermission(request: FastifyRequest, reply: FastifyReply) {
try {
const { userId, newPermission, permissionId } = request.body;
await prisma.permission.updateMany({
where: { id: permissionId, userId },
data: { permission: { set: newPermission } }
});
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function changePassword(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.body;
await prisma.user.update({ where: { id: undefined }, data: { password: 'RESETME' } });
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@ -0,0 +1,29 @@
import { FastifyPluginAsync } from 'fastify';
import { acceptInvitation, changePassword, deleteTeam, getTeam, inviteToTeam, listTeams, newTeam, removeUser, revokeInvitation, saveTeam, setPermission } from './handlers';
const root: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.addHook('onRequest', async (request, reply) => {
return await request.jwtVerify()
})
fastify.get('/', async (request) => await listTeams(request));
fastify.post('/new', async (request, reply) => await newTeam(request, reply));
fastify.get('/team/:id', async (request, reply) => await getTeam(request, reply));
fastify.post('/team/:id', async (request, reply) => await saveTeam(request, reply));
fastify.delete('/team/:id', async (request, reply) => await deleteTeam(request, reply));
fastify.post('/team/:id/invitation/invite', async (request, reply) => await inviteToTeam(request, reply))
fastify.post('/team/:id/invitation/accept', async (request) => await acceptInvitation(request));
fastify.post('/team/:id/invitation/revoke', async (request) => await revokeInvitation(request));
fastify.post('/team/:id/permission', async (request, reply) => await setPermission(request, reply));
fastify.delete('/user/remove', async (request, reply) => await removeUser(request, reply));
fastify.post('/user/password', async (request, reply) => await changePassword(request, reply));
// fastify.delete('/user', async (request, reply) => await deleteUser(request, reply));
};
export default root;

View File

@ -0,0 +1,51 @@
import { FastifyPluginAsync } from 'fastify';
import { scheduler } from '../../../lib/scheduler';
import { checkUpdate, login, showDashboard, update, showUsage, getCurrentUser } from './handlers';
export interface Update {
Body: { latestVersion: string }
}
export interface Login {
Body: { email: string, password: string, isLogin: boolean }
}
const root: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.get('/', async function (_request, reply) {
return reply.redirect(302, '/');
});
fastify.post<Login>('/login', async (request, reply) => {
const payload = await login(request, reply)
const token = fastify.jwt.sign(payload)
return { token, payload }
});
fastify.get('/user', {
onRequest: [fastify.authenticate]
}, async (request) => await getCurrentUser(request, fastify));
fastify.get('/undead', {
onRequest: [fastify.authenticate]
}, async function () {
return { message: 'nope' };
});
fastify.get('/update', {
onRequest: [fastify.authenticate]
}, async (request) => await checkUpdate(request));
fastify.post<Update>(
'/update', {
onRequest: [fastify.authenticate]
},
async (request) => await update(request)
);
fastify.get('/resources', {
onRequest: [fastify.authenticate]
}, async (request) => await showDashboard(request));
fastify.get('/usage', {
onRequest: [fastify.authenticate]
}, async () => await showUsage());
};
export default root;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,70 @@
import { FastifyPluginAsync } from 'fastify';
import {
activatePlausibleUsers,
checkService,
deleteService,
deleteServiceSecret,
deleteServiceStorage,
getService,
getServiceLogs,
getServiceSecrets,
getServiceStorages,
getServiceType,
getServiceUsage,
getServiceVersions,
listServices,
newService,
saveService,
saveServiceDestination,
saveServiceSecret,
saveServiceSettings,
saveServiceStorage,
saveServiceType,
saveServiceVersion,
setSettingsService,
startService,
stopService
} from './handlers';
const root: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.addHook('onRequest', async (request, reply) => {
return await request.jwtVerify()
})
fastify.get('/', async (request) => await listServices(request));
fastify.post('/new', async (request, reply) => await newService(request, reply));
fastify.get('/:id', async (request) => await getService(request));
fastify.post('/:id', async (request, reply) => await saveService(request, reply));
fastify.delete('/:id', async (request) => await deleteService(request));
fastify.post('/:id/check', async (request) => await checkService(request));
fastify.post('/:id/settings', async (request, reply) => await saveServiceSettings(request, reply));
fastify.get('/:id/secrets', async (request) => await getServiceSecrets(request));
fastify.post('/:id/secrets', async (request, reply) => await saveServiceSecret(request, reply));
fastify.delete('/:id/secrets', async (request) => await deleteServiceSecret(request));
fastify.get('/:id/storages', async (request) => await getServiceStorages(request));
fastify.post('/:id/storages', async (request, reply) => await saveServiceStorage(request, reply));
fastify.delete('/:id/storages', async (request) => await deleteServiceStorage(request));
fastify.get('/:id/configuration/type', async (request) => await getServiceType(request));
fastify.post('/:id/configuration/type', async (request, reply) => await saveServiceType(request, reply));
fastify.get('/:id/configuration/version', async (request) => await getServiceVersions(request));
fastify.post('/:id/configuration/version', async (request, reply) => await saveServiceVersion(request, reply));
fastify.post('/:id/configuration/destination', async (request, reply) => await saveServiceDestination(request, reply));
fastify.get('/:id/usage', async (request) => await getServiceUsage(request));
fastify.get('/:id/logs', async (request) => await getServiceLogs(request));
fastify.post('/:id/:type/start', async (request) => await startService(request));
fastify.post('/:id/:type/stop', async (request) => await stopService(request));
fastify.post('/:id/:type/settings', async (request, reply) => await setSettingsService(request, reply));
fastify.post('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply));
};
export default root;

View File

@ -0,0 +1,85 @@
import { promises as dns } from 'dns';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { checkDomainsIsValidInDNS, errorHandler, getDomain, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
export async function listAllSettings(request: FastifyRequest) {
try {
const settings = await listSettings();
return {
settings
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveSettings(request: FastifyRequest, reply: FastifyReply) {
try {
const {
fqdn,
isRegistrationEnabled,
dualCerts,
minPort,
maxPort,
isAutoUpdateEnabled,
isDNSCheckEnabled
} = request.body
const { id } = await listSettings();
await prisma.setting.update({
where: { id },
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled }
});
if (fqdn) {
await prisma.setting.update({ where: { id }, data: { fqdn } });
}
if (minPort && maxPort) {
await prisma.setting.update({ where: { id }, data: { minPort, maxPort } });
}
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deleteDomain(request: FastifyRequest, reply: FastifyReply) {
try {
const { fqdn } = request.body
let ip;
try {
ip = await dns.resolve(fqdn);
} catch (error) {
// Do not care.
}
await prisma.setting.update({ where: { fqdn }, data: { fqdn: null } });
return reply.redirect(302, ip ? `http://${ip[0]}:3000/settings` : undefined)
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function checkDomain(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params;
let { fqdn, forceSave, dualCerts, isDNSCheckEnabled } = request.body
if (fqdn) fqdn = fqdn.toLowerCase();
const found = await isDomainConfigured({ id, fqdn });
if (found) {
throw "Domain already configured";
}
if (isDNSCheckEnabled && !forceSave) {
return await checkDomainsIsValidInDNS({ hostname: request.hostname, fqdn, dualCerts });
}
return {};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function checkDNS(request: FastifyRequest, reply: FastifyReply) {
try {
const { id, domain } = request.params;
await isDNSValid(request.hostname, domain);
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@ -0,0 +1,17 @@
import { FastifyPluginAsync } from 'fastify';
import { checkDNS, checkDomain, deleteDomain, listAllSettings, saveSettings } from './handlers';
const root: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.addHook('onRequest', async (request, reply) => {
return await request.jwtVerify()
})
fastify.get('/', async (request) => await listAllSettings(request));
fastify.post('/', async (request, reply) => await saveSettings(request, reply));
fastify.delete('/', async (request, reply) => await deleteDomain(request, reply));
fastify.get('/check', async (request, reply) => await checkDNS(request, reply));
fastify.post('/check', async (request, reply) => await checkDomain(request, reply));
};
export default root;

View File

@ -0,0 +1,182 @@
import cuid from 'cuid';
import type { FastifyRequest } from 'fastify';
import { FastifyReply } from 'fastify';
import { decrypt, encrypt, errorHandler, prisma } from '../../../../lib/common';
export async function listSources(request: FastifyRequest) {
try {
const teamId = request.user?.teamId;
const sources = await prisma.gitSource.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { teams: true, githubApp: true, gitlabApp: true }
});
return {
sources
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveSource(request, reply) {
try {
const { id } = request.params
const { name, htmlUrl, apiUrl } = request.body
await prisma.gitSource.update({
where: { id },
data: { name, htmlUrl, apiUrl }
});
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getSource(request: FastifyRequest) {
try {
const { id } = request.params
const { teamId } = request.user
const settings = await prisma.setting.findFirst({});
if (settings.proxyPassword) settings.proxyPassword = decrypt(settings.proxyPassword);
if (id === 'new') {
return {
source: {
name: null,
type: null,
htmlUrl: null,
apiUrl: null,
organization: null
},
settings
}
}
const source = await prisma.gitSource.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { githubApp: true, gitlabApp: true }
});
if (!source) {
throw { status: 404, message: 'Source not found.' }
}
if (source?.githubApp?.clientSecret)
source.githubApp.clientSecret = decrypt(source.githubApp.clientSecret);
if (source?.githubApp?.webhookSecret)
source.githubApp.webhookSecret = decrypt(source.githubApp.webhookSecret);
if (source?.githubApp?.privateKey) source.githubApp.privateKey = decrypt(source.githubApp.privateKey);
if (source?.gitlabApp?.appSecret) source.gitlabApp.appSecret = decrypt(source.gitlabApp.appSecret);
return {
source,
settings
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deleteSource(request) {
try {
const { id } = request.params
const source = await prisma.gitSource.delete({
where: { id },
include: { githubApp: true, gitlabApp: true }
});
if (source.githubAppId) {
await prisma.githubApp.delete({ where: { id: source.githubAppId } });
}
if (source.gitlabAppId) {
await prisma.gitlabApp.delete({ where: { id: source.gitlabAppId } });
}
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveGitHubSource(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params
const { name, type, htmlUrl, apiUrl, organization } = request.body
const { teamId } = request.user
if (id === 'new') {
const newId = cuid()
await prisma.gitSource.create({
data: {
id: newId,
name,
htmlUrl,
apiUrl,
organization,
type: 'github',
teams: { connect: { id: teamId } }
}
});
return {
id: newId
}
}
throw { status: 500, message: 'Wrong request.' }
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveGitLabSource(request: FastifyRequest, reply: FastifyReply) {
try {
const { id } = request.params
const { teamId } = request.user
let { type, name, htmlUrl, apiUrl, oauthId, appId, appSecret, groupName } =
request.body
oauthId = Number(oauthId);
const encryptedAppSecret = encrypt(appSecret);
if (id === 'new') {
const newId = cuid()
await prisma.gitSource.create({ data: { id: newId, type, apiUrl, htmlUrl, name, teams: { connect: { id: teamId } } } });
await prisma.gitlabApp.create({
data: {
teams: { connect: { id: teamId } },
appId,
oauthId,
groupName,
appSecret: encryptedAppSecret,
gitSource: { connect: { id: newId } }
}
});
return {
status: 201,
id: newId
}
} else {
await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name } });
await prisma.gitlabApp.update({
where: { id },
data: {
appId,
oauthId,
groupName,
appSecret: encryptedAppSecret,
}
});
}
return { status: 201 };
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function checkGitLabOAuthID(request: FastifyRequest) {
try {
const { oauthId } = request.body
const found = await prisma.gitlabApp.findFirst({ where: { oauthId: Number(oauthId) } });
if (found) {
throw { status: 500, message: 'OAuthID already configured in Coolify.' }
}
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@ -0,0 +1,21 @@
import { FastifyPluginAsync } from 'fastify';
import { checkGitLabOAuthID, deleteSource, getSource, listSources, saveGitHubSource, saveGitLabSource, saveSource } from './handlers';
const root: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.addHook('onRequest', async (request, reply) => {
return await request.jwtVerify()
})
fastify.get('/', async (request) => await listSources(request));
fastify.get('/:id', async (request) => await getSource(request));
fastify.post('/:id', async (request, reply) => await saveSource(request, reply));
fastify.delete('/:id', async (request) => await deleteSource(request));
fastify.post('/:id/check', async (request) => await checkGitLabOAuthID(request));
fastify.post('/:id/github', async (request, reply) => await saveGitHubSource(request, reply));
fastify.post('/:id/gitlab', async (request, reply) => await saveGitLabSource(request, reply));
};
export default root;

Some files were not shown because too many files have changed in this diff Show More