This commit is contained in:
Andras Bacsai
2022-12-12 14:48:56 +01:00
parent 62f2196a0c
commit 2009dc11db
207 changed files with 9062 additions and 160 deletions

View File

@@ -0,0 +1,8 @@
import type { FastifyPluginAsync } from 'fastify';
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get('/', async function (_request, _reply) {
return { status: 'ok' };
});
};
export default root;

View File

@@ -1,4 +1,4 @@
import type { ServerOptions } from '../server';
import type { ServerOptions } from './server';
export const serverConfig: ServerOptions = {
dev: false,

View File

@@ -1,9 +1,17 @@
const dotenv = require('dotenv');
const isDev = process.env.NODE_ENV === 'development';
// dotenv.config({ path: isDev ? '../../.env' : '.env' });
dotenv.config();
const { z } = require('zod');
/*eslint sort-keys: "error"*/
const envSchema = z.object({
CODESANDBOX_HOST: z.string().optional(),
NODE_ENV: z.enum(['development', 'test', 'production']),
COOLIFY_SECRET_KEY: z.string()
COOLIFY_DATABASE_URL: z.string(),
COOLIFY_SECRET_KEY: z.string().length(32),
COOLIFY_WHITE_LABELED: z.string().optional(),
COOLIFY_WHITE_LABELED_ICON: z.string().optional()
});
const env = envSchema.safeParse(process.env);

View File

@@ -0,0 +1,111 @@
import type { Permission, Setting, Team, TeamInvitation, User } from '@prisma/client';
import { prisma } from '../prisma';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import fs from 'fs/promises';
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
import type { Config } from 'unique-names-generator';
import { env } from '../env';
import { day } from './dayjs';
const customConfig: Config = {
dictionaries: [adjectives, colors, animals],
style: 'capital',
separator: ' ',
length: 3
};
const algorithm = 'aes-256-ctr';
export const isDev = env.NODE_ENV === 'development';
export const version = '3.13.0';
export const sentryDSN =
'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216';
export async function listSettings(): Promise<Setting | null> {
return await prisma.setting.findUnique({ where: { id: '0' } });
}
export async function getCurrentUser(
userId: string
): Promise<(User & { permission: Permission[]; teams: Team[] }) | null> {
return await prisma.user.findUnique({
where: { id: userId },
include: { teams: true, permission: true }
});
}
export async function getTeamInvitation(userId: string): Promise<TeamInvitation[]> {
return await prisma.teamInvitation.findMany({ where: { uid: userId } });
}
export async function hashPassword(password: string): Promise<string> {
const saltRounds = 15;
return bcrypt.hash(password, saltRounds);
}
export async function comparePassword(password: string, hashedPassword: string): Promise<boolean> {
return bcrypt.compare(password, hashedPassword);
}
export const uniqueName = (): string => uniqueNamesGenerator(customConfig);
export const decrypt = (hashString: string) => {
if (hashString) {
try {
const hash = JSON.parse(hashString);
const decipher = crypto.createDecipheriv(
algorithm,
env.COOLIFY_SECRET_KEY,
Buffer.from(hash.iv, 'hex')
);
const decrpyted = Buffer.concat([
decipher.update(Buffer.from(hash.content, 'hex')),
decipher.final()
]);
return decrpyted.toString();
} catch (error) {
if (error instanceof Error) {
console.log({ decryptionError: error.message });
}
return hashString;
}
}
return false;
};
export function generateRangeArray(start, end) {
return Array.from({ length: end - start }, (v, k) => k + start);
}
export function generateTimestamp(): string {
return `${day().format('HH:mm:ss.SSS')}`;
}
export const encrypt = (text: string) => {
if (text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, env.COOLIFY_SECRET_KEY, iv);
const encrypted = Buffer.concat([cipher.update(text.trim()), cipher.final()]);
return JSON.stringify({
iv: iv.toString('hex'),
content: encrypted.toString('hex')
});
}
return false;
};
export async function getTemplates() {
const templatePath = isDev ? './templates.json' : '/app/templates.json';
const open = await fs.open(templatePath, 'r');
try {
let data = await open.readFile({ encoding: 'utf-8' });
let jsonData = JSON.parse(data);
if (isARM(process.arch)) {
jsonData = jsonData.filter((d) => d.arch !== 'amd64');
}
return jsonData;
} catch (error) {
return [];
} finally {
await open?.close();
}
}
export function isARM(arch: string) {
if (arch === 'arm' || arch === 'arm64' || arch === 'aarch' || arch === 'aarch64') {
return true;
}
return false;
}

View File

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

View File

@@ -0,0 +1,47 @@
import { executeCommand } from "./executeCommand";
export async function checkContainer({ dockerId, container, remove = false }: { dockerId: string, container: string, remove?: boolean }): Promise<{ found: boolean, status?: { isExited: boolean, isRunning: boolean, isRestarting: boolean } }> {
let containerFound = false;
try {
const { stdout } = await executeCommand({
dockerId,
command:
`docker inspect --format '{{json .State}}' ${container}`
});
containerFound = true
const parsedStdout = JSON.parse(stdout);
const status = parsedStdout.Status;
const isRunning = status === 'running';
const isRestarting = status === 'restarting'
const isExited = status === 'exited'
if (status === 'created') {
await executeCommand({
dockerId,
command:
`docker rm ${container}`
});
}
if (remove && status === 'exited') {
await executeCommand({
dockerId,
command:
`docker rm ${container}`
});
}
return {
found: containerFound,
status: {
isRunning,
isRestarting,
isExited
}
};
} catch (err) {
// Container not found
}
return {
found: false
};
}

View File

@@ -0,0 +1,188 @@
import { prisma } from '../prisma';
import os from 'os';
import fs from 'fs/promises';
import type { ExecaChildProcess } from 'execa';
import sshConfig from 'ssh-config';
import { getFreeSSHLocalPort } from './ssh';
import { env } from '../env';
import { saveBuildLog } from './logging';
import { decrypt } from './common';
export async function executeCommand({
command,
dockerId = null,
sshCommand = false,
shell = false,
stream = false,
buildId,
applicationId,
debug
}: {
command: string;
sshCommand?: boolean;
shell?: boolean;
stream?: boolean;
dockerId?: string;
buildId?: string;
applicationId?: string;
debug?: boolean;
}): Promise<ExecaChildProcess<string>> {
const { execa, execaCommand } = await import('execa');
const { parse } = await import('shell-quote');
const parsedCommand = parse(command);
const dockerCommand = parsedCommand[0];
const dockerArgs = parsedCommand.slice(1);
if (dockerId) {
const destinationDocker = await prisma.destinationDocker.findUnique({
where: { id: dockerId }
});
if (!destinationDocker) {
throw new Error('Destination docker not found');
}
let { remoteEngine, remoteIpAddress, engine } = destinationDocker;
if (remoteEngine) {
await createRemoteEngineConfiguration(dockerId);
engine = `ssh://${remoteIpAddress}-remote`;
} else {
engine = 'unix:///var/run/docker.sock';
}
if (env.CODESANDBOX_HOST) {
if (command.startsWith('docker compose')) {
command = command.replace(/docker compose/gi, 'docker-compose');
}
}
if (sshCommand) {
if (shell) {
return execaCommand(`ssh ${remoteIpAddress}-remote ${command}`);
}
return await execa('ssh', [`${remoteIpAddress}-remote`, dockerCommand, ...dockerArgs]);
}
if (stream) {
return await new Promise(async (resolve, reject) => {
let subprocess = null;
if (shell) {
subprocess = execaCommand(command, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
});
} else {
subprocess = execa(dockerCommand, dockerArgs, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
});
}
const logs: any[] = [];
if (subprocess && subprocess.stdout && subprocess.stderr) {
subprocess.stdout.on('data', async (data) => {
const stdout = data.toString();
const array = stdout.split('\n');
for (const line of array) {
if (line !== '\n' && line !== '') {
const log = {
line: `${line.replace('\n', '')}`,
buildId,
applicationId
};
logs.push(log);
if (debug) {
await saveBuildLog(log);
}
}
}
});
subprocess.stderr.on('data', async (data) => {
const stderr = data.toString();
const array = stderr.split('\n');
for (const line of array) {
if (line !== '\n' && line !== '') {
const log = {
line: `${line.replace('\n', '')}`,
buildId,
applicationId
};
logs.push(log);
if (debug) {
await saveBuildLog(log);
}
}
}
});
subprocess.on('exit', async (code) => {
if (code === 0) {
resolve('success');
} else {
if (!debug) {
for (const log of logs) {
await saveBuildLog(log);
}
}
reject(code);
}
});
}
});
} else {
if (shell) {
return await execaCommand(command, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
});
} else {
return await execa(dockerCommand, dockerArgs, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
});
}
}
} else {
if (shell) {
return execaCommand(command, { shell: true });
}
return await execa(dockerCommand, dockerArgs);
}
}
export async function createRemoteEngineConfiguration(id: string) {
const homedir = os.homedir();
const sshKeyFile = `/tmp/id_rsa-${id}`;
const localPort = await getFreeSSHLocalPort(id);
const {
sshKey: { privateKey },
network,
remoteIpAddress,
remotePort,
remoteUser
} = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } });
await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 });
const config = sshConfig.parse('');
const Host = `${remoteIpAddress}-remote`;
try {
await executeCommand({ command: `ssh-keygen -R ${Host}` });
await executeCommand({ command: `ssh-keygen -R ${remoteIpAddress}` });
await executeCommand({ command: `ssh-keygen -R localhost:${localPort}` });
} catch (error) {}
const found = config.find({ Host });
const foundIp = config.find({ Host: remoteIpAddress });
if (found) config.remove({ Host });
if (foundIp) config.remove({ Host: remoteIpAddress });
config.append({
Host,
Hostname: remoteIpAddress,
Port: remotePort.toString(),
User: remoteUser,
StrictHostKeyChecking: 'no',
IdentityFile: sshKeyFile,
ControlMaster: 'auto',
ControlPath: `${homedir}/.ssh/coolify-${remoteIpAddress}-%r@%h:%p`,
ControlPersist: '10m'
});
try {
await fs.stat(`${homedir}/.ssh/`);
} catch (error) {
await fs.mkdir(`${homedir}/.ssh/`);
}
return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config));
}

View File

@@ -0,0 +1,50 @@
import { prisma } from '../prisma';
import { encrypt, generateTimestamp, isDev } from './common';
import { day } from './dayjs';
export const saveBuildLog = async ({
line,
buildId,
applicationId
}: {
line: string;
buildId: string;
applicationId: string;
}): Promise<any> => {
if (buildId === 'undefined' || buildId === 'null' || !buildId) return;
if (applicationId === 'undefined' || applicationId === 'null' || !applicationId) return;
const { default: got } = await import('got');
if (typeof line === 'object' && line) {
if (line.shortMessage) {
line = line.shortMessage + '\n' + line.stderr;
} else {
line = JSON.stringify(line);
}
}
if (line && typeof line === 'string' && line.includes('ghs_')) {
const regex = /ghs_.*@/g;
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
}
const addTimestamp = `[${generateTimestamp()}] ${line}`;
const fluentBitUrl = isDev ? 'http://localhost:24224' : 'http://coolify-fluentbit:24224';
if (isDev) {
console.debug(`[${applicationId}] ${addTimestamp}`);
}
try {
return await got.post(`${fluentBitUrl}/${applicationId}_buildlog_${buildId}.csv`, {
json: {
line: encrypt(line)
}
});
} catch (error) {
return await prisma.buildLog.create({
data: {
line: addTimestamp,
buildId,
time: Number(day().valueOf()),
applicationId
}
});
}
};

View File

@@ -0,0 +1,47 @@
import { prisma } from '../prisma';
import { generateRangeArray } from './common';
export async function getFreeSSHLocalPort(id: string): Promise<number | boolean> {
const { default: isReachable } = await import('is-port-reachable');
const { remoteIpAddress, sshLocalPort } = await prisma.destinationDocker.findUnique({
where: { id }
});
if (sshLocalPort) {
return Number(sshLocalPort);
}
const data = await prisma.setting.findFirst();
const { minPort, maxPort } = data;
const ports = await prisma.destinationDocker.findMany({
where: { sshLocalPort: { not: null }, remoteIpAddress: { not: remoteIpAddress } }
});
const alreadyConfigured = await prisma.destinationDocker.findFirst({
where: {
remoteIpAddress,
id: { not: id },
sshLocalPort: { not: null }
}
});
if (alreadyConfigured?.sshLocalPort) {
await prisma.destinationDocker.update({
where: { id },
data: { sshLocalPort: alreadyConfigured.sshLocalPort }
});
return Number(alreadyConfigured.sshLocalPort);
}
const range = generateRangeArray(minPort, maxPort);
const availablePorts = range.filter((port) => !ports.map((p) => p.sshLocalPort).includes(port));
for (const port of availablePorts) {
const found = await isReachable(port, { host: 'localhost' });
if (!found) {
await prisma.destinationDocker.update({
where: { id },
data: { sshLocalPort: Number(port) }
});
return Number(port);
}
}
return false;
}

View File

@@ -6,16 +6,15 @@ import { env } from './env';
import { PrismaClient } from '@prisma/client';
const prismaGlobal = global as typeof global & {
prisma?: PrismaClient;
prisma?: PrismaClient;
};
export const prisma: PrismaClient =
prismaGlobal.prisma ||
new PrismaClient({
log:
env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
prismaGlobal.prisma ||
new PrismaClient({
log: env.NODE_ENV === 'developments' ? ['query', 'error', 'warn'] : ['error']
});
if (env.NODE_ENV !== 'production') {
prismaGlobal.prisma = prisma;
}
prismaGlobal.prisma = prisma;
}

View File

@@ -1,8 +0,0 @@
import { apiRouter } from './routers/api';
import { router } from './trpc';
export const appRouter = router({
api: apiRouter,
});
export type AppRouter = typeof appRouter;

View File

@@ -1,18 +0,0 @@
// import { z } from 'zod';
import { publicProcedure, router } from '../trpc';
// import { prisma } from '../../prisma';
import { TRPCError } from '@trpc/server';
export const apiRouter = router({
getConnection: publicProcedure.query(async () => {
try {
return { success: true };
} catch (error) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'An unexpected error occurred, please try again later.',
cause: error
});
}
})
});

View File

@@ -1,13 +0,0 @@
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';
import { Context } from './context';
const t = initTRPC.context<Context>().create({
transformer: superjson,
errorFormatter({ shape }) {
return shape;
},
});
export const router = t.router;
export const publicProcedure = t.procedure;

View File

@@ -1,10 +1,11 @@
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
import fastify from 'fastify';
import { appRouter } from './router';
import { createContext } from './router/context';
import { appRouter } from './trpc';
import { createContext } from './trpc/context';
import cors from '@fastify/cors';
import * as path from 'node:path';
import serve from '@fastify/static';
import autoLoad from '@fastify/autoload';
// import { prisma } from './prisma';
const isDev = process.env['NODE_ENV'] === 'development';
@@ -23,7 +24,16 @@ export function createServer(opts: ServerOptions) {
server.register(cors);
server.register(fastifyTRPCPlugin, {
prefix,
trpcOptions: { router: appRouter, createContext }
trpcOptions: {
router: appRouter,
createContext,
onError({ error, type, path, input, ctx, req }) {
console.error('Error:', error);
if (error.code === 'INTERNAL_SERVER_ERROR') {
// send to bug reporting
}
}
}
});
// Serve static files in production. Static files are generated by `yarn build` in the client folder by SvelteKit.
if (!isDev) {
@@ -40,14 +50,16 @@ export function createServer(opts: ServerOptions) {
return reply.status(200).sendFile('index.html');
});
}
server.get('/api', async () => {
return { status: 'ok' };
server.register(autoLoad, {
dir: path.join(__dirname, 'api'),
options: { prefix: '/api' }
});
const stop = () => server.close();
const start = async () => {
try {
await server.listen({ host: '0.0.0.0', port });
console.log('Server is listening on port', port);
console.log('Coolify server is listening on port', port, 'at 0.0.0.0 🚀');
} catch (err) {
server.log.error(err);
process.exit(1);

View File

@@ -3,17 +3,20 @@ import { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify';
import jwt from 'jsonwebtoken';
import { env } from '../env';
export interface User {
name: string | string[];
userId: string;
teamId: string;
permission: string;
isAdmin: boolean;
iat: number;
}
export function createContext({ req, res }: CreateFastifyContextOptions) {
export function createContext({ req }: CreateFastifyContextOptions) {
const token = req.headers.authorization;
let user: User | null = null;
if (token) {
const user = jwt.verify(token, env.COOLIFY_SECRET_KEY);
console.log(user);
user = jwt.verify(token, env.COOLIFY_SECRET_KEY) as User;
}
return { req, res };
return { user };
}
export type Context = inferAsyncReturnType<typeof createContext>;

View File

@@ -0,0 +1,21 @@
import { router } from './trpc';
import type { Permission } from '@prisma/client';
import {
settingsRouter,
authRouter,
dashboardRouter,
applicationsRouter,
servicesRouter
} from './routers';
export const appRouter = router({
settings: settingsRouter,
auth: authRouter,
dashboard: dashboardRouter,
applications: applicationsRouter,
services: servicesRouter
});
export type AppRouter = typeof appRouter;
export type PrismaPermission = Permission;

View File

@@ -0,0 +1,525 @@
import { z } from 'zod';
import { privateProcedure, router } from '../trpc';
import { decrypt, isARM, listSettings } from '../../lib/common';
import { prisma } from '../../prisma';
import { executeCommand } from '../../lib/executeCommand';
import { checkContainer } from '../../lib/docker';
export const applicationsRouter = router({
status: privateProcedure
.input(
z.object({
id: z.string()
})
)
.query(async ({ ctx, input }) => {
const id = input.id;
const teamId = ctx.user?.teamId;
if (!teamId) {
throw { status: 400, message: 'Team not found.' };
}
let payload = [];
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
if (application.buildPack === 'compose') {
const { stdout: containers } = await executeCommand({
dockerId: application.destinationDocker.id,
command: `docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'`
});
const containersArray = containers.trim().split('\n');
if (containersArray.length > 0 && containersArray[0] !== '') {
for (const container of containersArray) {
let isRunning = false;
let isExited = false;
let isRestarting = false;
const containerObj = JSON.parse(container);
const status = containerObj.State;
if (status === 'running') {
isRunning = true;
}
if (status === 'exited') {
isExited = true;
}
if (status === 'restarting') {
isRestarting = true;
}
payload.push({
name: containerObj.Names,
status: {
isRunning,
isExited,
isRestarting
}
});
}
}
} else {
let isRunning = false;
let isExited = false;
let isRestarting = false;
const status = await checkContainer({
dockerId: application.destinationDocker.id,
container: id
});
if (status?.found) {
isRunning = status.status.isRunning;
isExited = status.status.isExited;
isRestarting = status.status.isRestarting;
payload.push({
name: id,
status: {
isRunning,
isExited,
isRestarting
}
});
}
}
}
return payload;
})
});
export async function getApplicationFromDB(id: string, teamId: string) {
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,
connectedDatabase: true,
previewApplication: true,
dockerRegistry: 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 && !application.baseImage) {
application.baseImage = baseImage;
}
if (application && !application.baseBuildImage) {
application.baseBuildImage = baseBuildImage;
}
return { ...application, baseBuildImages, baseImages };
}
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;
}
}
const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy'];
const nodeBased = [
'react',
'preact',
'vuejs',
'svelte',
'gatsby',
'astro',
'eleventy',
'node',
'nestjs',
'nuxtjs',
'nextjs'
];
export function setDefaultBaseImage(
buildPack: string | null,
deploymentType: string | null = null
) {
const nodeVersions = [
{
value: 'node:lts',
label: 'node:lts'
},
{
value: 'node:18',
label: 'node:18'
},
{
value: 'node:17',
label: 'node:17'
},
{
value: 'node:16',
label: 'node:16'
},
{
value: 'node:14',
label: 'node:14'
},
{
value: 'node:12',
label: 'node:12'
}
];
const staticVersions = [
{
value: 'webdevops/nginx:alpine',
label: 'webdevops/nginx:alpine'
},
{
value: 'webdevops/apache:alpine',
label: 'webdevops/apache:alpine'
},
{
value: 'nginx:alpine',
label: 'nginx:alpine'
},
{
value: 'httpd:alpine',
label: 'httpd:alpine (Apache)'
}
];
const rustVersions = [
{
value: 'rust:latest',
label: 'rust:latest'
},
{
value: 'rust:1.60',
label: 'rust:1.60'
},
{
value: 'rust:1.60-buster',
label: 'rust:1.60-buster'
},
{
value: 'rust:1.60-bullseye',
label: 'rust:1.60-bullseye'
},
{
value: 'rust:1.60-slim-buster',
label: 'rust:1.60-slim-buster'
},
{
value: 'rust:1.60-slim-bullseye',
label: 'rust:1.60-slim-bullseye'
},
{
value: 'rust:1.60-alpine3.14',
label: 'rust:1.60-alpine3.14'
},
{
value: 'rust:1.60-alpine3.15',
label: 'rust:1.60-alpine3.15'
}
];
const phpVersions = [
{
value: 'webdevops/php-apache:8.2',
label: 'webdevops/php-apache:8.2'
},
{
value: 'webdevops/php-nginx:8.2',
label: 'webdevops/php-nginx:8.2'
},
{
value: 'webdevops/php-apache:8.1',
label: 'webdevops/php-apache:8.1'
},
{
value: 'webdevops/php-nginx:8.1',
label: 'webdevops/php-nginx:8.1'
},
{
value: 'webdevops/php-apache:8.0',
label: 'webdevops/php-apache:8.0'
},
{
value: 'webdevops/php-nginx:8.0',
label: 'webdevops/php-nginx:8.0'
},
{
value: 'webdevops/php-apache:7.4',
label: 'webdevops/php-apache:7.4'
},
{
value: 'webdevops/php-nginx:7.4',
label: 'webdevops/php-nginx:7.4'
},
{
value: 'webdevops/php-apache:7.3',
label: 'webdevops/php-apache:7.3'
},
{
value: 'webdevops/php-nginx:7.3',
label: 'webdevops/php-nginx:7.3'
},
{
value: 'webdevops/php-apache:7.2',
label: 'webdevops/php-apache:7.2'
},
{
value: 'webdevops/php-nginx:7.2',
label: 'webdevops/php-nginx:7.2'
},
{
value: 'webdevops/php-apache:7.1',
label: 'webdevops/php-apache:7.1'
},
{
value: 'webdevops/php-nginx:7.1',
label: 'webdevops/php-nginx:7.1'
},
{
value: 'webdevops/php-apache:7.0',
label: 'webdevops/php-apache:7.0'
},
{
value: 'webdevops/php-nginx:7.0',
label: 'webdevops/php-nginx:7.0'
},
{
value: 'webdevops/php-apache:5.6',
label: 'webdevops/php-apache:5.6'
},
{
value: 'webdevops/php-nginx:5.6',
label: 'webdevops/php-nginx:5.6'
},
{
value: 'webdevops/php-apache:8.2-alpine',
label: 'webdevops/php-apache:8.2-alpine'
},
{
value: 'webdevops/php-nginx:8.2-alpine',
label: 'webdevops/php-nginx:8.2-alpine'
},
{
value: 'webdevops/php-apache:8.1-alpine',
label: 'webdevops/php-apache:8.1-alpine'
},
{
value: 'webdevops/php-nginx:8.1-alpine',
label: 'webdevops/php-nginx:8.1-alpine'
},
{
value: 'webdevops/php-apache:8.0-alpine',
label: 'webdevops/php-apache:8.0-alpine'
},
{
value: 'webdevops/php-nginx:8.0-alpine',
label: 'webdevops/php-nginx:8.0-alpine'
},
{
value: 'webdevops/php-apache:7.4-alpine',
label: 'webdevops/php-apache:7.4-alpine'
},
{
value: 'webdevops/php-nginx:7.4-alpine',
label: 'webdevops/php-nginx:7.4-alpine'
},
{
value: 'webdevops/php-apache:7.3-alpine',
label: 'webdevops/php-apache:7.3-alpine'
},
{
value: 'webdevops/php-nginx:7.3-alpine',
label: 'webdevops/php-nginx:7.3-alpine'
},
{
value: 'webdevops/php-apache:7.2-alpine',
label: 'webdevops/php-apache:7.2-alpine'
},
{
value: 'webdevops/php-nginx:7.2-alpine',
label: 'webdevops/php-nginx:7.2-alpine'
},
{
value: 'webdevops/php-apache:7.1-alpine',
label: 'webdevops/php-apache:7.1-alpine'
},
{
value: 'php:8.1-fpm',
label: 'php:8.1-fpm'
},
{
value: 'php:8.0-fpm',
label: 'php:8.0-fpm'
},
{
value: 'php:8.1-fpm-alpine',
label: 'php:8.1-fpm-alpine'
},
{
value: 'php:8.0-fpm-alpine',
label: 'php:8.0-fpm-alpine'
}
];
const pythonVersions = [
{
value: 'python:3.10-alpine',
label: 'python:3.10-alpine'
},
{
value: 'python:3.10-buster',
label: 'python:3.10-buster'
},
{
value: 'python:3.10-bullseye',
label: 'python:3.10-bullseye'
},
{
value: 'python:3.10-slim-bullseye',
label: 'python:3.10-slim-bullseye'
},
{
value: 'python:3.9-alpine',
label: 'python:3.9-alpine'
},
{
value: 'python:3.9-buster',
label: 'python:3.9-buster'
},
{
value: 'python:3.9-bullseye',
label: 'python:3.9-bullseye'
},
{
value: 'python:3.9-slim-bullseye',
label: 'python:3.9-slim-bullseye'
},
{
value: 'python:3.8-alpine',
label: 'python:3.8-alpine'
},
{
value: 'python:3.8-buster',
label: 'python:3.8-buster'
},
{
value: 'python:3.8-bullseye',
label: 'python:3.8-bullseye'
},
{
value: 'python:3.8-slim-bullseye',
label: 'python:3.8-slim-bullseye'
},
{
value: 'python:3.7-alpine',
label: 'python:3.7-alpine'
},
{
value: 'python:3.7-buster',
label: 'python:3.7-buster'
},
{
value: 'python:3.7-bullseye',
label: 'python:3.7-bullseye'
},
{
value: 'python:3.7-slim-bullseye',
label: 'python:3.7-slim-bullseye'
}
];
const herokuVersions = [
{
value: 'heroku/builder:22',
label: 'heroku/builder:22'
},
{
value: 'heroku/buildpacks:20',
label: 'heroku/buildpacks:20'
},
{
value: 'heroku/builder-classic:22',
label: 'heroku/builder-classic:22'
}
];
let payload: any = {
baseImage: null,
baseBuildImage: null,
baseImages: [],
baseBuildImages: []
};
if (nodeBased.includes(buildPack)) {
if (deploymentType === 'static') {
payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine';
payload.baseImages = isARM(process.arch)
? staticVersions.filter((version) => !version.value.includes('webdevops'))
: staticVersions;
payload.baseBuildImage = 'node:lts';
payload.baseBuildImages = nodeVersions;
} else {
payload.baseImage = 'node:lts';
payload.baseImages = nodeVersions;
payload.baseBuildImage = 'node:lts';
payload.baseBuildImages = nodeVersions;
}
}
if (staticApps.includes(buildPack)) {
payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine';
payload.baseImages = isARM(process.arch)
? staticVersions.filter((version) => !version.value.includes('webdevops'))
: staticVersions;
payload.baseBuildImage = 'node:lts';
payload.baseBuildImages = nodeVersions;
}
if (buildPack === 'python') {
payload.baseImage = 'python:3.10-alpine';
payload.baseImages = pythonVersions;
}
if (buildPack === 'rust') {
payload.baseImage = 'rust:latest';
payload.baseBuildImage = 'rust:latest';
payload.baseImages = rustVersions;
payload.baseBuildImages = rustVersions;
}
if (buildPack === 'deno') {
payload.baseImage = 'denoland/deno:latest';
}
if (buildPack === 'php') {
payload.baseImage = isARM(process.arch)
? 'php:8.1-fpm-alpine'
: 'webdevops/php-apache:8.2-alpine';
payload.baseImages = isARM(process.arch)
? phpVersions.filter((version) => !version.value.includes('webdevops'))
: phpVersions;
}
if (buildPack === 'laravel') {
payload.baseImage = isARM(process.arch)
? 'php:8.1-fpm-alpine'
: 'webdevops/php-apache:8.2-alpine';
payload.baseImages = isARM(process.arch)
? phpVersions.filter((version) => !version.value.includes('webdevops'))
: phpVersions;
payload.baseBuildImage = 'node:18';
payload.baseBuildImages = nodeVersions;
}
if (buildPack === 'heroku') {
payload.baseImage = 'heroku/buildpacks:20';
payload.baseImages = herokuVersions;
}
return payload;
}

View File

@@ -0,0 +1,178 @@
import { z } from 'zod';
import { publicProcedure, router } from '../trpc';
import { TRPCError } from '@trpc/server';
import { comparePassword, hashPassword, listSettings, uniqueName } from '../../lib/common';
import { env } from '../../env';
import jsonwebtoken from 'jsonwebtoken';
import { prisma } from '../../prisma';
import cuid from 'cuid';
export const authRouter = router({
register: publicProcedure
.input(
z.object({
email: z.string(),
password: z.string()
})
)
.mutation(async ({ input }) => {
const { email, password } = input;
const userFound = await prisma.user.findUnique({
where: { email },
include: { teams: true, permission: true }
});
if (userFound) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'User already exists.'
});
}
const settings = await listSettings();
if (!settings?.isRegistrationEnabled) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Registration is disabled.'
});
}
const usersCount = await prisma.user.count();
const uid = usersCount === 0 ? '0' : cuid();
const permission = 'owner';
const isAdmin = true;
const hashedPassword = await hashPassword(password);
// Create the first user as the owner
if (usersCount === 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 } }
},
include: { teams: true }
});
await prisma.setting.update({
where: { id: '0' },
data: { isRegistrationEnabled: false }
});
} else {
// Create a new user and team
await prisma.user.create({
data: {
id: uid,
email,
password: hashedPassword,
type: 'email',
teams: {
create: {
id: uid,
name: uniqueName()
}
},
permission: { create: { teamId: uid, permission } }
},
include: { teams: true }
});
}
const payload = {
userId: uid,
teamId: uid,
permission,
isAdmin
};
return {
...payload,
token: jsonwebtoken.sign(payload, env.COOLIFY_SECRET_KEY)
};
}),
login: publicProcedure
.input(
z.object({
email: z.string(),
password: z.string()
})
)
.mutation(async ({ input }) => {
const { email, password } = input;
const userFound = await prisma.user.findUnique({
where: { email },
include: { teams: true, permission: true }
});
if (!userFound) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'User already exists.'
});
}
if (userFound.type === 'email') {
if (userFound.password === 'RESETME') {
const hashedPassword = await hashPassword(password);
if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) {
if (userFound.id === '0') {
await prisma.user.update({
where: { email: userFound.email },
data: { password: 'RESETME' }
});
} else {
await prisma.user.update({
where: { email: userFound.email },
data: { password: 'RESETTIMEOUT' }
});
}
} else {
await prisma.user.update({
where: { email: userFound.email },
data: { password: hashedPassword }
});
const payload = {
userId: userFound.id,
teamId: userFound.id,
permission: userFound.permission,
isAdmin: true
};
return {
...payload,
token: jsonwebtoken.sign(payload, env.COOLIFY_SECRET_KEY)
};
}
}
if (!userFound.password) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Something went wrong. Please try again later.'
});
}
const passwordMatch = comparePassword(password, userFound.password);
if (!passwordMatch) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Incorrect password.'
});
}
const payload = {
userId: userFound.id,
teamId: userFound.id,
permission: userFound.permission,
isAdmin: true
};
return {
...payload,
token: jsonwebtoken.sign(payload, env.COOLIFY_SECRET_KEY)
};
}
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Not implemented yet.'
});
})
});

View File

@@ -0,0 +1,65 @@
import { privateProcedure, router } from '../trpc';
import { listSettings } from '../../lib/common';
import { prisma } from '../../prisma';
export const dashboardRouter = router({
resources: privateProcedure.query(async ({ ctx }) => {
const id = ctx.user?.teamId === '0' ? undefined : ctx.user?.teamId;
let applications = await prisma.application.findMany({
where: { teams: { some: { id } } },
include: { settings: true, destinationDocker: true, teams: true }
});
const databases = await prisma.database.findMany({
where: { teams: { some: { id } } },
include: { settings: true, destinationDocker: true, teams: true }
});
const services = await prisma.service.findMany({
where: { teams: { some: { id } } },
include: { destinationDocker: true, teams: true }
});
const gitSources = await prisma.gitSource.findMany({
where: {
OR: [{ teams: { some: { id } } }, { isSystemWide: true }]
},
include: { teams: true }
});
const destinations = await prisma.destinationDocker.findMany({
where: { teams: { some: { id } } },
include: { teams: true }
});
const settings = await listSettings();
let foundUnconfiguredApplication = false;
for (const application of applications) {
if (
((!application.buildPack || !application.branch) && !application.simpleDockerfile) ||
!application.destinationDockerId ||
(!application.settings?.isBot && !application?.fqdn && application.buildPack !== 'compose')
) {
foundUnconfiguredApplication = true;
}
}
let foundUnconfiguredService = false;
for (const service of services) {
if (!service.fqdn) {
foundUnconfiguredService = true;
}
}
let foundUnconfiguredDatabase = false;
for (const database of databases) {
if (!database.version) {
foundUnconfiguredDatabase = true;
}
}
return {
foundUnconfiguredApplication,
foundUnconfiguredDatabase,
foundUnconfiguredService,
applications,
databases,
services,
gitSources,
destinations,
settings
};
})
});

View File

@@ -0,0 +1,5 @@
export * from './auth';
export * from './dashboard';
export * from './settings';
export * from './applications';
export * from './services';

View File

@@ -0,0 +1,118 @@
import { z } from 'zod';
import { privateProcedure, router } from '../trpc';
import { decrypt, getTemplates, listSettings } from '../../lib/common';
import { prisma } from '../../prisma';
import { executeCommand } from '../../lib/executeCommand';
import { checkContainer } from '../../lib/docker';
export const servicesRouter = router({
status: privateProcedure
.input(
z.object({
id: z.string()
})
)
.query(async ({ ctx, input }) => {
const id = input.id;
const teamId = ctx.user?.teamId;
if (!teamId) {
throw { status: 400, message: 'Team not found.' };
}
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId } = service;
let payload = {};
if (destinationDockerId) {
const { stdout: containers } = await executeCommand({
dockerId: service.destinationDocker.id,
command: `docker ps -a --filter "label=com.docker.compose.project=${id}" --format '{{json .}}'`
});
if (containers) {
const containersArray = containers.trim().split('\n');
if (containersArray.length > 0 && containersArray[0] !== '') {
const templates = await getTemplates();
let template = templates.find((t) => t.type === service.type);
const templateStr = JSON.stringify(template);
if (templateStr) {
template = JSON.parse(templateStr.replaceAll('$$id', service.id));
}
for (const container of containersArray) {
let isRunning = false;
let isExited = false;
let isRestarting = false;
let isExcluded = false;
const containerObj = JSON.parse(container);
const exclude = template?.services[containerObj.Names]?.exclude;
if (exclude) {
payload[containerObj.Names] = {
status: {
isExcluded: true,
isRunning: false,
isExited: false,
isRestarting: false
}
};
continue;
}
const status = containerObj.State;
if (status === 'running') {
isRunning = true;
}
if (status === 'exited') {
isExited = true;
}
if (status === 'restarting') {
isRestarting = true;
}
payload[containerObj.Names] = {
status: {
isExcluded,
isRunning,
isExited,
isRestarting
}
};
}
}
}
}
return payload;
})
});
export async function getServiceFromDB({
id,
teamId
}: {
id: string;
teamId: string;
}): Promise<any> {
const settings = await prisma.setting.findFirst();
const body = await prisma.service.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: {
destinationDocker: true,
persistentStorage: true,
serviceSecret: true,
serviceSetting: true,
wordpress: true,
plausibleAnalytics: true
}
});
if (!body) {
return null;
}
// body.type = fixType(body.type);
if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value);
return s;
});
}
if (body.wordpress) {
body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
}
return { ...body, settings };
}

View File

@@ -0,0 +1,80 @@
// import { z } from 'zod';
import { publicProcedure, privateProcedure, router } from '../trpc';
import { TRPCError } from '@trpc/server';
import { getCurrentUser, getTeamInvitation, listSettings, version } from '../../lib/common';
import { env } from '../../env';
import type { Permission, TeamInvitation } from '@prisma/client';
import jsonwebtoken from 'jsonwebtoken';
export const settingsRouter = router({
getBaseSettings: publicProcedure.query(async () => {
const settings = await listSettings();
return {
success: true,
data: {
isRegistrationEnabled: settings?.isRegistrationEnabled
}
};
}),
getInstanceSettings: privateProcedure.query(async ({ ctx }) => {
try {
const settings = await listSettings();
let isAdmin = false;
let permission = null;
let token = null;
let pendingInvitations: TeamInvitation[] = [];
if (!settings) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'An unexpected error occurred, please try again later.'
});
}
if (ctx.user) {
const currentUser = await getCurrentUser(ctx.user.userId);
if (currentUser) {
const foundPermission = currentUser.permission.find(
(p: Permission) => p.teamId === ctx.user?.teamId
)?.permission;
if (foundPermission) {
permission = foundPermission;
isAdmin = foundPermission === 'owner' || foundPermission === 'admin';
}
const payload = {
userId: ctx.user?.userId,
teamId: ctx.user?.teamId,
permission,
isAdmin,
iat: Math.floor(Date.now() / 1000)
};
token = jsonwebtoken.sign(payload, env.COOLIFY_SECRET_KEY);
}
pendingInvitations = await getTeamInvitation(ctx.user.userId);
}
return {
success: true,
data: {
token,
userId: ctx.user?.userId,
teamId: ctx.user?.teamId,
permission,
isAdmin,
ipv4: ctx.user?.teamId ? settings.ipv4 : null,
ipv6: ctx.user?.teamId ? settings.ipv6 : null,
version,
whiteLabeled: env.COOLIFY_WHITE_LABELED === 'true',
whiteLabeledIcon: env.COOLIFY_WHITE_LABELED_ICON,
isRegistrationEnabled: settings.isRegistrationEnabled,
pendingInvitations
}
};
} catch (error) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'An unexpected error occurred, please try again later.',
cause: error
});
}
})
});

View File

@@ -0,0 +1,33 @@
import { initTRPC, TRPCError } from '@trpc/server';
import superjson from 'superjson';
import type { Context } from './context';
const t = initTRPC.context<Context>().create({
transformer: superjson,
errorFormatter({ shape }) {
return shape;
}
});
const logger = t.middleware(async ({ path, type, next }) => {
const start = Date.now();
const result = await next();
const durationMs = Date.now() - start;
result.ok
? console.log('OK request timing:', { path, type, durationMs })
: console.log('Non-OK request timing', { path, type, durationMs });
return result;
});
const isAdmin = t.middleware(async ({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
user: ctx.user
}
});
});
export const router = t.router;
export const privateProcedure = t.procedure.use(isAdmin).use(logger);
export const publicProcedure = t.procedure.use(logger);