diff --git a/apps/client/package.json b/apps/client/package.json
index 2f067f30a..b9fda399a 100644
--- a/apps/client/package.json
+++ b/apps/client/package.json
@@ -17,6 +17,7 @@
"@playwright/test": "1.28.1",
"@sveltejs/adapter-static": "1.0.0-next.48",
"@sveltejs/kit": "1.0.0-next.572",
+ "@types/js-cookie": "3.0.2",
"@typescript-eslint/eslint-plugin": "5.44.0",
"@typescript-eslint/parser": "5.44.0",
"autoprefixer": "10.4.13",
diff --git a/apps/client/src/lib/store.ts b/apps/client/src/lib/store.ts
index 0022e6c93..0b08d624e 100644
--- a/apps/client/src/lib/store.ts
+++ b/apps/client/src/lib/store.ts
@@ -1,6 +1,6 @@
-import { writable, readable, type Writable, type Readable } from 'svelte/store';
+import { writable, readable, type Writable } from 'svelte/store';
import superjson from 'superjson';
-import type { AppRouter, PrismaPermission } from 'server/src/trpc';
+import type { AppRouter } from 'server/src/trpc';
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import { browser, dev } from '$app/environment';
import Cookies from 'js-cookie';
diff --git a/apps/client/src/routes/+layout.svelte b/apps/client/src/routes/+layout.svelte
index 0664797ca..d219cd63a 100644
--- a/apps/client/src/routes/+layout.svelte
+++ b/apps/client/src/routes/+layout.svelte
@@ -6,7 +6,7 @@
import { appSession } from '$lib/store';
import Tooltip from '$lib/components/Tooltip.svelte';
import { page } from '$app/stores';
- import UpdateAvailable from '$lib/components/UpdateAvailable.svelte';
+ // import UpdateAvailable from '$lib/components/UpdateAvailable.svelte';
import Cookies from 'js-cookie';
import { errorNotification } from '$lib/common';
import Toasts from '$lib/components/Toasts.svelte';
@@ -346,7 +346,7 @@
IAM {#if $appSession.pendingInvitations.length > 0}
{pendingInvitations.length}{$appSession.pendingInvitations.length}
{/if}
diff --git a/apps/client/src/routes/+page.svelte b/apps/client/src/routes/+page.svelte
index b3e1c2879..71163b87e 100644
--- a/apps/client/src/routes/+page.svelte
+++ b/apps/client/src/routes/+page.svelte
@@ -2,6 +2,18 @@
import type { PageData } from './$types';
export let data: PageData;
+ import { dev } from '$app/environment';
+ import { onMount } from 'svelte';
+
+ import { asyncSleep, errorNotification, getRndInteger } from '$lib/common';
+ import { appSession, search, t } from '$lib/store';
+
+ import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte';
+ import DatabaseIcons from '$lib/components/svg/databases/DatabaseIcons.svelte';
+ import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
+ import NewResource from '$lib/components/NewResource.svelte';
+ import DeleteIcon from '$lib/components/DeleteIcon.svelte';
+
const {
applications,
foundUnconfiguredApplication,
@@ -14,17 +26,6 @@
settings
} = data;
let filtered: any = setInitials();
- import { asyncSleep, errorNotification, getRndInteger } from '$lib/common';
- import { appSession, search, t } from '$lib/store';
-
- import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte';
- import DatabaseIcons from '$lib/components/svg/databases/DatabaseIcons.svelte';
- import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
- import { dev } from '$app/environment';
- import NewResource from '$lib/components/NewResource.svelte';
- import { onMount } from 'svelte';
- import DeleteIcon from '$lib/components/DeleteIcon.svelte';
-
let numberOfGetStatus = 0;
let status: any = {};
let noInitialStatus: any = {
@@ -155,7 +156,7 @@
let isRunning = false;
let isDegraded = false;
if (buildPack || simpleDockerfile) {
- const response = await t.applications.status.query({ id })
+ const response = await t.applications.status.query({ id });
if (response.length === 0) {
isRunning = false;
} else if (response.length === 1) {
@@ -177,7 +178,7 @@
}
}
} else if (typeof dualCerts !== 'undefined') {
- const response = await t.services.status.query({ id })
+ const response = await t.services.status.query({ id });
if (Object.keys(response).length === 0) {
isRunning = false;
} else {
@@ -197,7 +198,7 @@
}
}
} else {
- const response = await get(`/databases/${id}/status`);
+ const response = await t.databases.status.query({ id });
isRunning = response.isRunning;
}
@@ -381,7 +382,7 @@
'Are you sure? This will delete all UNCONFIGURED applications and their data.'
);
if (sure) {
- // await post(`/applications/cleanup/unconfigured`, {});
+ await t.applications.cleanup.query();
return window.location.reload();
}
} catch (error) {
@@ -394,7 +395,7 @@
'Are you sure? This will delete all UNCONFIGURED services and their data.'
);
if (sure) {
- // await post(`/services/cleanup/unconfigured`, {});
+ await t.services.cleanup.query();
return window.location.reload();
}
} catch (error) {
@@ -407,7 +408,7 @@
'Are you sure? This will delete all UNCONFIGURED databases and their data.'
);
if (sure) {
- // await post(`/databases/cleanup/unconfigured`, {});
+ await t.databases.cleanup.query();
return window.location.reload();
}
} catch (error) {
@@ -418,7 +419,7 @@
try {
const sure = confirm('Are you sure? This will delete this application!');
if (sure) {
- // await del(`/applications/${id}`, { force: true });
+ await t.applications.delete.mutate({ id, force: true });
return window.location.reload();
}
} catch (error) {
@@ -429,6 +430,7 @@
try {
const sure = confirm('Are you sure? This will delete this service!');
if (sure) {
+ await t.services.delete.mutate({ id });
// await del(`/services/${id}`, {});
return window.location.reload();
}
@@ -440,7 +442,7 @@
try {
const sure = confirm('Are you sure? This will delete this database!');
if (sure) {
- // await del(`/databases/${id}`, { force: true });
+ await t.databases.delete.mutate({ id, force: true });
return window.location.reload();
}
} catch (error) {
diff --git a/apps/client/tsconfig.json b/apps/client/tsconfig.json
index b959b7dc5..dc270e7f0 100644
--- a/apps/client/tsconfig.json
+++ b/apps/client/tsconfig.json
@@ -1,5 +1,6 @@
{
"extends": "./.svelte-kit/tsconfig.json",
+ "exclude": ["node_modules/*", ".svelte-kit/*", "public/*"],
"compilerOptions": {
"allowJs": true,
"checkJs": true,
@@ -10,12 +11,8 @@
"sourceMap": true,
"strict": false,
"paths": {
- "$lib": [
- "src/lib"
- ],
- "$lib/*": [
- "src/lib/*"
- ],
+ "$lib": ["src/lib"],
+ "$lib/*": ["src/lib/*"]
}
}
-}
\ No newline at end of file
+}
diff --git a/apps/server/package.json b/apps/server/package.json
index 3ce4e8afa..2ae9cbb50 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -51,6 +51,7 @@
"@types/node": "18.11.9",
"@types/node-fetch": "2.6.2",
"@types/shell-quote": "^1.7.1",
+ "@types/bcryptjs": "^2.4.2",
"@types/ws": "8.5.3",
"npm-run-all": "4.1.5",
"rimraf": "3.0.2",
diff --git a/apps/server/src/env.js b/apps/server/src/env.js
index f69754110..fece0be52 100644
--- a/apps/server/src/env.js
+++ b/apps/server/src/env.js
@@ -1,5 +1,5 @@
const dotenv = require('dotenv');
-const isDev = process.env.NODE_ENV === 'development';
+// const isDev = process.env.NODE_ENV === 'development';
// dotenv.config({ path: isDev ? '../../.env' : '.env' });
dotenv.config();
const { z } = require('zod');
diff --git a/apps/server/src/lib/common.ts b/apps/server/src/lib/common.ts
index 77b73b284..f6eb14199 100644
--- a/apps/server/src/lib/common.ts
+++ b/apps/server/src/lib/common.ts
@@ -68,8 +68,8 @@ export const decrypt = (hashString: string) => {
return false;
};
-export function generateRangeArray(start, end) {
- return Array.from({ length: end - start }, (v, k) => k + start);
+export function generateRangeArray(start: number, end: number) {
+ return Array.from({ length: end - start }, (_v, k) => k + start);
}
export function generateTimestamp(): string {
return `${day().format('HH:mm:ss.SSS')}`;
@@ -94,7 +94,7 @@ export async function getTemplates() {
let data = await open.readFile({ encoding: 'utf-8' });
let jsonData = JSON.parse(data);
if (isARM(process.arch)) {
- jsonData = jsonData.filter((d) => d.arch !== 'amd64');
+ jsonData = jsonData.filter((d: { arch: string }) => d.arch !== 'amd64');
}
return jsonData;
} catch (error) {
@@ -109,3 +109,26 @@ export function isARM(arch: string) {
}
return false;
}
+
+export async function removeService({ id }: { id: string }): Promise {
+ await prisma.serviceSecret.deleteMany({ where: { serviceId: id } });
+ await prisma.serviceSetting.deleteMany({ where: { serviceId: id } });
+ await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } });
+ await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
+ await prisma.fider.deleteMany({ where: { serviceId: id } });
+ await prisma.ghost.deleteMany({ where: { serviceId: id } });
+ await prisma.umami.deleteMany({ where: { serviceId: id } });
+ await prisma.hasura.deleteMany({ where: { serviceId: id } });
+ await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } });
+ await prisma.minio.deleteMany({ where: { serviceId: id } });
+ await prisma.vscodeserver.deleteMany({ where: { serviceId: id } });
+ await prisma.wordpress.deleteMany({ where: { serviceId: id } });
+ await prisma.glitchTip.deleteMany({ where: { serviceId: id } });
+ await prisma.moodle.deleteMany({ where: { serviceId: id } });
+ await prisma.appwrite.deleteMany({ where: { serviceId: id } });
+ await prisma.searxng.deleteMany({ where: { serviceId: id } });
+ await prisma.weblate.deleteMany({ where: { serviceId: id } });
+ await prisma.taiga.deleteMany({ where: { serviceId: id } });
+
+ await prisma.service.delete({ where: { id } });
+}
diff --git a/apps/server/src/lib/docker.ts b/apps/server/src/lib/docker.ts
index 27bf380a3..85eb78e30 100644
--- a/apps/server/src/lib/docker.ts
+++ b/apps/server/src/lib/docker.ts
@@ -1,31 +1,39 @@
-import { executeCommand } from "./executeCommand";
+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 } }> {
+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}`
+ command: `docker inspect --format '{{json .State}}' ${container}`
});
- containerFound = true
+ containerFound = true;
const parsedStdout = JSON.parse(stdout);
const status = parsedStdout.Status;
const isRunning = status === 'running';
- const isRestarting = status === 'restarting'
- const isExited = status === 'exited'
+ const isRestarting = status === 'restarting';
+ const isExited = status === 'exited';
if (status === 'created') {
await executeCommand({
dockerId,
- command:
- `docker rm ${container}`
+ command: `docker rm ${container}`
});
}
if (remove && status === 'exited') {
await executeCommand({
dockerId,
- command:
- `docker rm ${container}`
+ command: `docker rm ${container}`
});
}
@@ -43,5 +51,74 @@ export async function checkContainer({ dockerId, container, remove = false }: {
return {
found: false
};
+}
-}
\ No newline at end of file
+export async function removeContainer({
+ id,
+ dockerId
+}: {
+ id: string;
+ dockerId: string;
+}): Promise {
+ try {
+ const { stdout } = await executeCommand({
+ dockerId,
+ command: `docker inspect --format '{{json .State}}' ${id}`
+ });
+ if (JSON.parse(stdout).Running) {
+ await executeCommand({ dockerId, command: `docker stop -t 0 ${id}` });
+ await executeCommand({ dockerId, command: `docker rm ${id}` });
+ }
+ if (JSON.parse(stdout).Status === 'exited') {
+ await executeCommand({ dockerId, command: `docker rm ${id}` });
+ }
+ } catch (error) {
+ throw error;
+ }
+}
+
+export async function stopDatabaseContainer(database: any): Promise {
+ let everStarted = false;
+ const {
+ id,
+ destinationDockerId,
+ destinationDocker: { engine, id: dockerId }
+ } = database;
+ if (destinationDockerId) {
+ try {
+ const { stdout } = await executeCommand({
+ dockerId,
+ command: `docker inspect --format '{{json .State}}' ${id}`
+ });
+
+ if (stdout) {
+ everStarted = true;
+ await removeContainer({ id, dockerId });
+ }
+ } catch (error) {
+ //
+ }
+ }
+ return everStarted;
+}
+export async function stopTcpHttpProxy(
+ id: string,
+ destinationDocker: any,
+ publicPort: number,
+ forceName: string | null = null
+): Promise<{ stdout: string; stderr: string } | Error | unknown> {
+ const { id: dockerId } = destinationDocker;
+ let container = `${id}-${publicPort}`;
+ if (forceName) container = forceName;
+ const { found } = await checkContainer({ dockerId, container });
+ try {
+ if (!found) return true;
+ return await executeCommand({
+ dockerId,
+ command: `docker stop -t 0 ${container} && docker rm ${container}`,
+ shell: true
+ });
+ } catch (error) {
+ return error;
+ }
+}
diff --git a/apps/server/src/lib/executeCommand.ts b/apps/server/src/lib/executeCommand.ts
index 198c4046c..8a3bf4afa 100644
--- a/apps/server/src/lib/executeCommand.ts
+++ b/apps/server/src/lib/executeCommand.ts
@@ -6,7 +6,7 @@ import sshConfig from 'ssh-config';
import { getFreeSSHLocalPort } from './ssh';
import { env } from '../env';
-import { saveBuildLog } from './logging';
+import { BuildLog, saveBuildLog } from './logging';
import { decrypt } from './common';
export async function executeCommand({
@@ -31,23 +31,26 @@ export async function executeCommand({
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);
+ const dockerCommand = parsedCommand[0]?.toString();
+ const dockerArgs = parsedCommand.slice(1).toString();
- if (dockerId) {
+ if (dockerId && dockerCommand && dockerArgs) {
const destinationDocker = await prisma.destinationDocker.findUnique({
where: { id: dockerId }
});
if (!destinationDocker) {
throw new Error('Destination docker not found');
}
- let { remoteEngine, remoteIpAddress, engine } = destinationDocker;
+ let {
+ remoteEngine,
+ remoteIpAddress,
+ engine = 'unix:///var/run/docker.sock'
+ } = 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');
@@ -73,12 +76,12 @@ export async function executeCommand({
}
const logs: any[] = [];
if (subprocess && subprocess.stdout && subprocess.stderr) {
- subprocess.stdout.on('data', async (data) => {
+ subprocess.stdout.on('data', async (data: string) => {
const stdout = data.toString();
const array = stdout.split('\n');
for (const line of array) {
if (line !== '\n' && line !== '') {
- const log = {
+ const log: BuildLog = {
line: `${line.replace('\n', '')}`,
buildId,
applicationId
@@ -90,7 +93,7 @@ export async function executeCommand({
}
}
});
- subprocess.stderr.on('data', async (data) => {
+ subprocess.stderr.on('data', async (data: string) => {
const stderr = data.toString();
const array = stderr.split('\n');
for (const line of array) {
@@ -107,7 +110,7 @@ export async function executeCommand({
}
}
});
- subprocess.on('exit', async (code) => {
+ subprocess.on('exit', async (code: number) => {
if (code === 0) {
resolve('success');
} else {
diff --git a/apps/server/src/lib/logging.ts b/apps/server/src/lib/logging.ts
index c8f460402..bb4dde7a0 100644
--- a/apps/server/src/lib/logging.ts
+++ b/apps/server/src/lib/logging.ts
@@ -2,15 +2,13 @@ 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 => {
+export type Line = string | { shortMessage: string; stderr: string };
+export type BuildLog = {
+ line: Line;
+ buildId?: string;
+ applicationId?: string;
+};
+export const saveBuildLog = async ({ line, buildId, applicationId }: BuildLog): Promise => {
if (buildId === 'undefined' || buildId === 'null' || !buildId) return;
if (applicationId === 'undefined' || applicationId === 'null' || !applicationId) return;
const { default: got } = await import('got');
diff --git a/apps/server/src/prisma.ts b/apps/server/src/prisma.ts
index e315eae42..7c818b869 100644
--- a/apps/server/src/prisma.ts
+++ b/apps/server/src/prisma.ts
@@ -12,7 +12,7 @@ const prismaGlobal = global as typeof global & {
export const prisma: PrismaClient =
prismaGlobal.prisma ||
new PrismaClient({
- log: env.NODE_ENV === 'developments' ? ['query', 'error', 'warn'] : ['error']
+ log: env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error']
});
if (env.NODE_ENV !== 'production') {
diff --git a/apps/server/tags.json b/apps/server/src/tags.json
similarity index 100%
rename from apps/server/tags.json
rename to apps/server/src/tags.json
diff --git a/apps/server/templates.json b/apps/server/src/templates.json
similarity index 100%
rename from apps/server/templates.json
rename to apps/server/src/templates.json
diff --git a/apps/server/src/trpc/context.ts b/apps/server/src/trpc/context.ts
index 8c7631f8d..e1e64c29f 100644
--- a/apps/server/src/trpc/context.ts
+++ b/apps/server/src/trpc/context.ts
@@ -1,5 +1,5 @@
-import { inferAsyncReturnType } from '@trpc/server';
-import { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify';
+import type { inferAsyncReturnType } from '@trpc/server';
+import type { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify';
import jwt from 'jsonwebtoken';
import { env } from '../env';
export interface User {
diff --git a/apps/server/src/trpc/index.ts b/apps/server/src/trpc/index.ts
index 4eddbb785..a5bc3e969 100644
--- a/apps/server/src/trpc/index.ts
+++ b/apps/server/src/trpc/index.ts
@@ -6,7 +6,8 @@ import {
authRouter,
dashboardRouter,
applicationsRouter,
- servicesRouter
+ servicesRouter,
+ databasesRouter
} from './routers';
export const appRouter = router({
@@ -14,7 +15,8 @@ export const appRouter = router({
auth: authRouter,
dashboard: dashboardRouter,
applications: applicationsRouter,
- services: servicesRouter
+ services: servicesRouter,
+ databases: databasesRouter
});
export type AppRouter = typeof appRouter;
diff --git a/apps/server/src/trpc/routers/applications.ts b/apps/server/src/trpc/routers/applications.ts
index 3a37d70e8..3277b43cc 100644
--- a/apps/server/src/trpc/routers/applications.ts
+++ b/apps/server/src/trpc/routers/applications.ts
@@ -1,72 +1,44 @@
import { z } from 'zod';
import { privateProcedure, router } from '../trpc';
-import { decrypt, isARM, listSettings } from '../../lib/common';
+import { decrypt, isARM } from '../../lib/common';
import { prisma } from '../../prisma';
import { executeCommand } from '../../lib/executeCommand';
-import { checkContainer } from '../../lib/docker';
+import { checkContainer, removeContainer } 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
- }
- });
+ status: privateProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
+ const id: string = 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;
}
- }
- } 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,
+ name: containerObj.Names,
status: {
isRunning,
isExited,
@@ -75,8 +47,108 @@ export const applicationsRouter = router({
});
}
}
+ } 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;
+ }
+ return payload;
+ }),
+ cleanup: privateProcedure.query(async ({ ctx }) => {
+ const teamId = ctx.user?.teamId;
+ let applications = await prisma.application.findMany({
+ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
+ include: { settings: true, destinationDocker: true, teams: true }
+ });
+ for (const application of applications) {
+ if (
+ !application.buildPack ||
+ !application.destinationDockerId ||
+ !application.branch ||
+ (!application.settings?.isBot && !application?.fqdn)
+ ) {
+ if (application?.destinationDockerId && application.destinationDocker?.network) {
+ const { stdout: containers } = await executeCommand({
+ dockerId: application.destinationDocker.id,
+ command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${application.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, dockerId: application.destinationDocker.id });
+ }
+ }
+ }
+ await prisma.applicationSettings.deleteMany({ where: { applicationId: application.id } });
+ await prisma.buildLog.deleteMany({ where: { applicationId: application.id } });
+ await prisma.build.deleteMany({ where: { applicationId: application.id } });
+ await prisma.secret.deleteMany({ where: { applicationId: application.id } });
+ await prisma.applicationPersistentStorage.deleteMany({
+ where: { applicationId: application.id }
+ });
+ await prisma.applicationConnectedDatabase.deleteMany({
+ where: { applicationId: application.id }
+ });
+ await prisma.application.deleteMany({ where: { id: application.id } });
+ }
+ }
+ return {};
+ }),
+ delete: privateProcedure
+ .input(z.object({ force: z.boolean(), id: z.string() }))
+ .mutation(async ({ ctx, input }) => {
+ const { id, force } = input;
+ const teamId = ctx.user?.teamId;
+ const application = await prisma.application.findUnique({
+ where: { id },
+ include: { destinationDocker: true }
+ });
+ if (!force && application?.destinationDockerId && application.destinationDocker?.network) {
+ const { stdout: containers } = await executeCommand({
+ dockerId: application.destinationDocker.id,
+ command: `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, dockerId: application.destinationDocker.id });
+ }
+ }
+ }
+ 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 } });
+ await prisma.applicationConnectedDatabase.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 {};
})
});
diff --git a/apps/server/src/trpc/routers/databases.ts b/apps/server/src/trpc/routers/databases.ts
new file mode 100644
index 000000000..8d4d8a0ee
--- /dev/null
+++ b/apps/server/src/trpc/routers/databases.ts
@@ -0,0 +1,84 @@
+import { z } from 'zod';
+import { privateProcedure, router } from '../trpc';
+import { decrypt } from '../../lib/common';
+import { prisma } from '../../prisma';
+import { executeCommand } from '../../lib/executeCommand';
+import { stopDatabaseContainer, stopTcpHttpProxy } from '../../lib/docker';
+
+export const databasesRouter = router({
+ status: privateProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
+ const id = input.id;
+ const teamId = ctx.user?.teamId;
+
+ let isRunning = false;
+ const database = await prisma.database.findFirst({
+ where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
+ include: { destinationDocker: true, settings: true }
+ });
+ if (database) {
+ const { destinationDockerId, destinationDocker } = database;
+ if (destinationDockerId) {
+ try {
+ const { stdout } = await executeCommand({
+ dockerId: destinationDocker.id,
+ command: `docker inspect --format '{{json .State}}' ${id}`
+ });
+
+ if (JSON.parse(stdout).Running) {
+ isRunning = true;
+ }
+ } catch (error) {
+ //
+ }
+ }
+ }
+ return {
+ isRunning
+ };
+ }),
+ cleanup: privateProcedure.query(async ({ ctx }) => {
+ const teamId = ctx.user?.teamId;
+ let databases = await prisma.database.findMany({
+ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
+ include: { settings: true, destinationDocker: true, teams: true }
+ });
+ for (const database of databases) {
+ if (!database?.version) {
+ const { id } = database;
+ 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.databaseSecret.deleteMany({ where: { databaseId: id } });
+ await prisma.database.delete({ where: { id } });
+ }
+ }
+ return {};
+ }),
+ delete: privateProcedure
+ .input(z.object({ id: z.string(), force: z.boolean() }))
+ .mutation(async ({ ctx, input }) => {
+ const { id, force } = input;
+ const teamId = ctx.user?.teamId;
+ const database = await prisma.database.findFirst({
+ where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
+ include: { destinationDocker: true, settings: true }
+ });
+ if (!force) {
+ 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.databaseSecret.deleteMany({ where: { databaseId: id } });
+ await prisma.database.delete({ where: { id } });
+ return {};
+ })
+});
diff --git a/apps/server/src/trpc/routers/index.ts b/apps/server/src/trpc/routers/index.ts
index 3dea86e15..0e16e9c41 100644
--- a/apps/server/src/trpc/routers/index.ts
+++ b/apps/server/src/trpc/routers/index.ts
@@ -3,3 +3,4 @@ export * from './dashboard';
export * from './settings';
export * from './applications';
export * from './services';
+export * from './databases';
diff --git a/apps/server/src/trpc/routers/services.ts b/apps/server/src/trpc/routers/services.ts
index 1e5a805ed..6368d60f1 100644
--- a/apps/server/src/trpc/routers/services.ts
+++ b/apps/server/src/trpc/routers/services.ts
@@ -1,82 +1,135 @@
import { z } from 'zod';
import { privateProcedure, router } from '../trpc';
-import { decrypt, getTemplates, listSettings } from '../../lib/common';
+import { decrypt, getTemplates, removeService } 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;
- }
+ 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: { type: string }) => 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,
- isRunning,
- isExited,
- isRestarting
+ 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;
+ }
+ return payload;
+ }),
+ cleanup: privateProcedure.query(async ({ ctx }) => {
+ const teamId = ctx.user?.teamId;
+ let services = await prisma.service.findMany({
+ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
+ include: { destinationDocker: true, teams: true }
+ });
+ for (const service of services) {
+ if (!service.fqdn) {
+ if (service.destinationDockerId) {
+ const { stdout: containers } = await executeCommand({
+ dockerId: service.destinationDockerId,
+ command: `docker ps -a --filter 'label=com.docker.compose.project=${service.id}' --format {{.ID}}`
+ });
+ if (containers) {
+ const containerArray = containers.split('\n');
+ if (containerArray.length > 0) {
+ for (const container of containerArray) {
+ await executeCommand({
+ dockerId: service.destinationDockerId,
+ command: `docker stop -t 0 ${container}`
+ });
+ await executeCommand({
+ dockerId: service.destinationDockerId,
+ command: `docker rm --force ${container}`
+ });
+ }
+ }
+ }
+ }
+ await removeService({ id: service.id });
+ }
+ }
+ }),
+ delete: privateProcedure
+ .input(z.object({ force: z.boolean(), id: z.string() }))
+ .mutation(async ({ input }) => {
+ // todo: check if user is allowed to delete service
+ const { id } = input;
+ await prisma.serviceSecret.deleteMany({ where: { serviceId: id } });
+ await prisma.serviceSetting.deleteMany({ where: { serviceId: id } });
+ await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } });
+ await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
+ await prisma.fider.deleteMany({ where: { serviceId: id } });
+ await prisma.ghost.deleteMany({ where: { serviceId: id } });
+ await prisma.umami.deleteMany({ where: { serviceId: id } });
+ await prisma.hasura.deleteMany({ where: { serviceId: id } });
+ await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } });
+ await prisma.minio.deleteMany({ where: { serviceId: id } });
+ await prisma.vscodeserver.deleteMany({ where: { serviceId: id } });
+ await prisma.wordpress.deleteMany({ where: { serviceId: id } });
+ await prisma.glitchTip.deleteMany({ where: { serviceId: id } });
+ await prisma.moodle.deleteMany({ where: { serviceId: id } });
+ await prisma.appwrite.deleteMany({ where: { serviceId: id } });
+ await prisma.searxng.deleteMany({ where: { serviceId: id } });
+ await prisma.weblate.deleteMany({ where: { serviceId: id } });
+ await prisma.taiga.deleteMany({ where: { serviceId: id } });
+
+ await prisma.service.delete({ where: { id } });
+ return {};
})
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4482f8b63..a62d53050 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -160,6 +160,7 @@ importers:
'@sveltejs/kit': 1.0.0-next.572
'@trpc/client': 10.1.0
'@trpc/server': 10.1.0
+ '@types/js-cookie': 3.0.2
'@typescript-eslint/eslint-plugin': 5.44.0
'@typescript-eslint/parser': 5.44.0
autoprefixer: 10.4.13
@@ -196,6 +197,7 @@ importers:
'@playwright/test': 1.28.1
'@sveltejs/adapter-static': 1.0.0-next.48
'@sveltejs/kit': 1.0.0-next.572_svelte@3.53.1+vite@3.2.4
+ '@types/js-cookie': 3.0.2
'@typescript-eslint/eslint-plugin': 5.44.0_fnsv2sbzcckq65bwfk7a5xwslu
'@typescript-eslint/parser': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a
autoprefixer: 10.4.13_postcss@8.4.19
@@ -237,6 +239,7 @@ importers:
'@prisma/client': 4.6.1
'@trpc/client': 10.1.0
'@trpc/server': 10.1.0
+ '@types/bcryptjs': ^2.4.2
'@types/jsonwebtoken': ^8.5.9
'@types/node': 18.11.9
'@types/node-fetch': 2.6.2
@@ -299,6 +302,7 @@ importers:
ws: 8.11.0
zod: 3.19.1
devDependencies:
+ '@types/bcryptjs': 2.4.2
'@types/jsonwebtoken': 8.5.9
'@types/node': 18.11.9
'@types/node-fetch': 2.6.2
@@ -2026,6 +2030,10 @@ packages:
resolution: {integrity: sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==}
dev: false
+ /@types/bcryptjs/2.4.2:
+ resolution: {integrity: sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==}
+ dev: true
+
/@types/cacheable-request/6.0.2:
resolution: {integrity: sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==}
dependencies: