mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-29 05:09:25 +00:00
wip trpc
This commit is contained in:
22
apps/server/src/trpc/context.ts
Normal file
22
apps/server/src/trpc/context.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { inferAsyncReturnType } from '@trpc/server';
|
||||
import { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { env } from '../env';
|
||||
export interface User {
|
||||
userId: string;
|
||||
teamId: string;
|
||||
permission: string;
|
||||
isAdmin: boolean;
|
||||
iat: number;
|
||||
}
|
||||
|
||||
export function createContext({ req }: CreateFastifyContextOptions) {
|
||||
const token = req.headers.authorization;
|
||||
let user: User | null = null;
|
||||
if (token) {
|
||||
user = jwt.verify(token, env.COOLIFY_SECRET_KEY) as User;
|
||||
}
|
||||
return { user };
|
||||
}
|
||||
|
||||
export type Context = inferAsyncReturnType<typeof createContext>;
|
||||
21
apps/server/src/trpc/index.ts
Normal file
21
apps/server/src/trpc/index.ts
Normal 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;
|
||||
525
apps/server/src/trpc/routers/applications.ts
Normal file
525
apps/server/src/trpc/routers/applications.ts
Normal 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;
|
||||
}
|
||||
178
apps/server/src/trpc/routers/auth.ts
Normal file
178
apps/server/src/trpc/routers/auth.ts
Normal 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.'
|
||||
});
|
||||
})
|
||||
});
|
||||
65
apps/server/src/trpc/routers/dashboard.ts
Normal file
65
apps/server/src/trpc/routers/dashboard.ts
Normal 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
|
||||
};
|
||||
})
|
||||
});
|
||||
5
apps/server/src/trpc/routers/index.ts
Normal file
5
apps/server/src/trpc/routers/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './auth';
|
||||
export * from './dashboard';
|
||||
export * from './settings';
|
||||
export * from './applications';
|
||||
export * from './services';
|
||||
118
apps/server/src/trpc/routers/services.ts
Normal file
118
apps/server/src/trpc/routers/services.ts
Normal 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 };
|
||||
}
|
||||
80
apps/server/src/trpc/routers/settings.ts
Normal file
80
apps/server/src/trpc/routers/settings.ts
Normal 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
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
33
apps/server/src/trpc/trpc.ts
Normal file
33
apps/server/src/trpc/trpc.ts
Normal 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);
|
||||
Reference in New Issue
Block a user