diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 25e97c31a..eda081b6c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -26,7 +26,7 @@ // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [3000, 3001], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "cp apps/api/.env.example pps/api/.env && pnpm install && pnpm db:push && pnpm db:seed", + "postCreateCommand": "cp apps/api/.env.example apps/api/.env && pnpm install && pnpm db:push && pnpm db:seed", // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "node", "features": { diff --git a/apps/api/prisma/migrations/20220907092244_database_secrets/migration.sql b/apps/api/prisma/migrations/20220907092244_database_secrets/migration.sql new file mode 100644 index 000000000..53ff2d19e --- /dev/null +++ b/apps/api/prisma/migrations/20220907092244_database_secrets/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "DatabaseSecret" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "value" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "databaseId" TEXT NOT NULL, + CONSTRAINT "DatabaseSecret_databaseId_fkey" FOREIGN KEY ("databaseId") REFERENCES "Database" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "DatabaseSecret_name_databaseId_key" ON "DatabaseSecret"("name", "databaseId"); diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 681293b53..df7516e01 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -328,6 +328,19 @@ model Database { settings DatabaseSettings? teams Team[] applicationConnectedDatabase ApplicationConnectedDatabase[] + databaseSecret DatabaseSecret[] +} + +model DatabaseSecret { + id String @id @default(cuid()) + name String + value String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + databaseId String + database Database @relation(fields: [databaseId], references: [id]) + + @@unique([name, databaseId]) } model DatabaseSettings { diff --git a/apps/api/src/jobs/infrastructure.ts b/apps/api/src/jobs/infrastructure.ts index fbcb07616..178a06aca 100644 --- a/apps/api/src/jobs/infrastructure.ts +++ b/apps/api/src/jobs/infrastructure.ts @@ -21,14 +21,17 @@ async function autoUpdater() { const activeCount = 0 if (activeCount === 0) { if (!isDev) { - await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); - await asyncExecShell(`env | grep COOLIFY > .env`); - await asyncExecShell( - `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=true' .env` - ); - await asyncExecShell( - `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"` - ); + const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); + if (isAutoUpdateEnabled) { + await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); + await asyncExecShell(`env | grep COOLIFY > .env`); + await asyncExecShell( + `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` + ); + await asyncExecShell( + `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"` + ); + } } else { console.log('Updating (not really in dev mode).'); } diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index c0dd7d96a..50a8dd4db 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -21,7 +21,7 @@ import { scheduler } from './scheduler'; import { supportedServiceTypesAndVersions } from './services/supportedVersions'; import { includeServices } from './services/common'; -export const version = '3.9.4'; +export const version = '3.10.0'; export const isDev = process.env.NODE_ENV === 'development'; const algorithm = 'aes-256-ctr'; @@ -414,6 +414,12 @@ export const supportedDatabaseTypesAndVersions = [ baseImageARM: 'couchdb', versions: ['3.2.2', '3.1.2', '2.3.1'], versionsARM: ['3.2.2', '3.1.2', '2.3.1'] + }, + { + name: 'edgedb', + fancyName: 'EdgeDB', + baseImage: 'edgedb/edgedb', + versions: ['latest', '2.1', '2.0', '1.4'] } ]; @@ -493,8 +499,26 @@ export async function createRemoteEngineConfiguration(id: string) { } return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config)) } +export async function executeSSHCmd({ dockerId, command }) { + const { execaCommand } = await import('execa') + let { remoteEngine, remoteIpAddress, engine, remoteUser } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) + if (remoteEngine) { + await createRemoteEngineConfiguration(dockerId) + engine = `ssh://${remoteIpAddress}` + } else { + engine = 'unix:///var/run/docker.sock' + } + if (process.env.CODESANDBOX_HOST) { + if (command.startsWith('docker compose')) { + command = command.replace(/docker compose/gi, 'docker-compose') + } + } + command = `ssh ${remoteIpAddress} ${command}` + return await execaCommand(command) +} export async function executeDockerCmd({ debug, buildId, applicationId, dockerId, command }: { debug?: boolean, buildId?: string, applicationId?: string, dockerId: string, command: string }): Promise { - let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) + const { execaCommand } = await import('execa') + let { remoteEngine, remoteIpAddress, engine, remoteUser } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) if (remoteEngine) { await createRemoteEngineConfiguration(dockerId) engine = `ssh://${remoteIpAddress}` @@ -509,10 +533,7 @@ export async function executeDockerCmd({ debug, buildId, applicationId, dockerId if (command.startsWith(`docker build --progress plain`)) { return await asyncExecShellStream({ debug, buildId, applicationId, command, engine }); } - return await asyncExecShell( - `DOCKER_BUILDKIT=1 DOCKER_HOST="${engine}" ${command}` - ); - + return await execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine }, shell: true }) } export async function startTraefikProxy(id: string): Promise { const { engine, network, remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id } }) @@ -642,21 +663,20 @@ export function generatePassword({ length = 24, symbols = false, isHex = false } return password; } -export function generateDatabaseConfiguration(database: any, arch: string): - | { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - MYSQL_DATABASE: string; - MYSQL_PASSWORD: string; - MYSQL_ROOT_USER: string; - MYSQL_USER: string; - MYSQL_ROOT_PASSWORD: string; - }; - } +type DatabaseConfiguration = { + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + MYSQL_DATABASE: string; + MYSQL_PASSWORD: string; + MYSQL_ROOT_USER: string; + MYSQL_USER: string; + MYSQL_ROOT_PASSWORD: string; + }; +} | { volume: string; image: string; @@ -691,22 +711,13 @@ export function generateDatabaseConfiguration(database: any, arch: string): ulimits: Record; privatePort: number; environmentVariables: { - POSTGRESQL_POSTGRES_PASSWORD: string; - POSTGRESQL_USERNAME: string; - POSTGRESQL_PASSWORD: string; - POSTGRESQL_DATABASE: string; - }; - } - | { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - POSTGRES_USER: string; - POSTGRES_PASSWORD: string; - POSTGRES_DB: string; + POSTGRES_PASSWORD?: string; + POSTGRES_USER?: string; + POSTGRES_DB?: string; + POSTGRESQL_POSTGRES_PASSWORD?: string; + POSTGRESQL_USERNAME?: string; + POSTGRESQL_PASSWORD?: string; + POSTGRESQL_DATABASE?: string; }; } | { @@ -730,7 +741,21 @@ export function generateDatabaseConfiguration(database: any, arch: string): COUCHDB_PASSWORD: string; COUCHDB_USER: string; }; - } { + } + | { + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + EDGEDB_SERVER_PASSWORD: string; + EDGEDB_SERVER_USER: string; + EDGEDB_SERVER_DATABASE: string; + EDGEDB_SERVER_TLS_CERT_MODE: string; + }; + } +export function generateDatabaseConfiguration(database: any, arch: string): DatabaseConfiguration { const { id, dbUser, @@ -740,7 +765,6 @@ export function generateDatabaseConfiguration(database: any, arch: string): defaultDatabase, version, type, - settings: { appendOnly } } = database; const baseImage = getDatabaseImage(type, arch); if (type === 'mysql') { @@ -762,7 +786,7 @@ export function generateDatabaseConfiguration(database: any, arch: string): } return configuration } else if (type === 'mariadb') { - const configuration = { + const configuration: DatabaseConfiguration = { privatePort: 3306, environmentVariables: { MARIADB_ROOT_USER: rootUser, @@ -780,7 +804,7 @@ export function generateDatabaseConfiguration(database: any, arch: string): } return configuration } else if (type === 'mongodb') { - const configuration = { + const configuration: DatabaseConfiguration = { privatePort: 27017, environmentVariables: { MONGODB_ROOT_USER: rootUser, @@ -799,7 +823,7 @@ export function generateDatabaseConfiguration(database: any, arch: string): } return configuration } else if (type === 'postgresql') { - const configuration = { + const configuration: DatabaseConfiguration = { privatePort: 5432, environmentVariables: { POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword, @@ -821,7 +845,8 @@ export function generateDatabaseConfiguration(database: any, arch: string): } return configuration } else if (type === 'redis') { - const configuration = { + const { settings: { appendOnly } } = database; + const configuration: DatabaseConfiguration = { privatePort: 6379, command: undefined, environmentVariables: { @@ -838,7 +863,7 @@ export function generateDatabaseConfiguration(database: any, arch: string): } return configuration } else if (type === 'couchdb') { - const configuration = { + const configuration: DatabaseConfiguration = { privatePort: 5984, environmentVariables: { COUCHDB_PASSWORD: dbUserPassword, @@ -852,6 +877,20 @@ export function generateDatabaseConfiguration(database: any, arch: string): configuration.volume = `${id}-${type}-data:/opt/couchdb/data`; } return configuration + } else if (type === 'edgedb') { + const configuration: DatabaseConfiguration = { + privatePort: 5656, + environmentVariables: { + EDGEDB_SERVER_PASSWORD: rootUserPassword, + EDGEDB_SERVER_USER: rootUser, + EDGEDB_SERVER_DATABASE: defaultDatabase, + EDGEDB_SERVER_TLS_CERT_MODE: 'generate_self_signed' + }, + image: `${baseImage}:${version}`, + volume: `${id}-${type}-data:/var/lib/edgedb/data`, + ulimits: {} + }; + return configuration } } export function isARM(arch: string) { @@ -1090,90 +1129,150 @@ export async function updatePasswordInDb(database, user, newPassword, isRoot) { } } } -export async function checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress }: { id: string, configuredPort?: number, exposePort: number, dockerId: string, remoteIpAddress?: string }) { +export async function checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }: { id: string, configuredPort?: number, exposePort: number, engine: string, remoteEngine: boolean, remoteIpAddress?: string }) { if (exposePort < 1024 || exposePort > 65535) { throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` } } if (configuredPort) { if (configuredPort !== exposePort) { - const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress); + const availablePort = await getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress); if (availablePort.toString() !== exposePort.toString()) { throw { status: 500, message: `Port ${exposePort} is already in use.` } } } } else { - const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress); + const availablePort = await getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress); if (availablePort.toString() !== exposePort.toString()) { throw { status: 500, message: `Port ${exposePort} is already in use.` } } } } -export async function getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress) { +export async function getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress) { const { default: checkPort } = await import('is-port-reachable'); - const applicationUsed = await ( - await prisma.application.findMany({ - where: { exposePort: { not: null }, id: { not: id }, destinationDockerId: dockerId }, - select: { exposePort: true } - }) - ).map((a) => a.exposePort); - const serviceUsed = await ( - await prisma.service.findMany({ - where: { exposePort: { not: null }, id: { not: id }, destinationDockerId: dockerId }, - select: { exposePort: true } - }) - ).map((a) => a.exposePort); - const usedPorts = [...applicationUsed, ...serviceUsed]; - if (usedPorts.includes(exposePort)) { + if (remoteEngine) { + const applicationUsed = await ( + await prisma.application.findMany({ + where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } }, + select: { exposePort: true } + }) + ).map((a) => a.exposePort); + const serviceUsed = await ( + await prisma.service.findMany({ + where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } }, + select: { exposePort: true } + }) + ).map((a) => a.exposePort); + const usedPorts = [...applicationUsed, ...serviceUsed]; + if (usedPorts.includes(exposePort)) { + return false + } + const found = await checkPort(exposePort, { host: remoteIpAddress }); + if (!found) { + return exposePort + } + return false + } else { + const applicationUsed = await ( + await prisma.application.findMany({ + where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { engine } }, + select: { exposePort: true } + }) + ).map((a) => a.exposePort); + const serviceUsed = await ( + await prisma.service.findMany({ + where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { engine } }, + select: { exposePort: true } + }) + ).map((a) => a.exposePort); + const usedPorts = [...applicationUsed, ...serviceUsed]; + if (usedPorts.includes(exposePort)) { + return false + } + const found = await checkPort(exposePort, { host: 'localhost' }); + if (!found) { + return exposePort + } return false } - const found = await checkPort(exposePort, { host: remoteIpAddress || 'localhost' }); - if (!found) { - return exposePort - } - return false - } export function generateRangeArray(start, end) { return Array.from({ length: (end - start) }, (v, k) => k + start); } -export async function getFreePublicPort(id, dockerId) { +export async function getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress }) { const { default: isReachable } = await import('is-port-reachable'); const data = await prisma.setting.findFirst(); const { minPort, maxPort } = data; - const dbUsed = await ( - await prisma.database.findMany({ - where: { publicPort: { not: null }, id: { not: id }, destinationDockerId: dockerId }, - select: { publicPort: true } - }) - ).map((a) => a.publicPort); - const wpFtpUsed = await ( - await prisma.wordpress.findMany({ - where: { ftpPublicPort: { not: null }, id: { not: id }, service: { destinationDockerId: dockerId } }, - select: { ftpPublicPort: true } - }) - ).map((a) => a.ftpPublicPort); - const wpUsed = await ( - await prisma.wordpress.findMany({ - where: { mysqlPublicPort: { not: null }, id: { not: id }, service: { destinationDockerId: dockerId } }, - select: { mysqlPublicPort: true } - }) - ).map((a) => a.mysqlPublicPort); - const minioUsed = await ( - await prisma.minio.findMany({ - where: { publicPort: { not: null }, id: { not: id }, service: { destinationDockerId: dockerId } }, - select: { publicPort: true } - }) - ).map((a) => a.publicPort); - const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed]; - const range = generateRangeArray(minPort, maxPort) - const availablePorts = range.filter(port => !usedPorts.includes(port)) - for (const port of availablePorts) { - const found = await isReachable(port, { host: 'localhost' }) - if (!found) { - return port + if (remoteEngine) { + const dbUsed = await ( + await prisma.database.findMany({ + where: { publicPort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } }, + select: { publicPort: true } + }) + ).map((a) => a.publicPort); + const wpFtpUsed = await ( + await prisma.wordpress.findMany({ + where: { ftpPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } }, + select: { ftpPublicPort: true } + }) + ).map((a) => a.ftpPublicPort); + const wpUsed = await ( + await prisma.wordpress.findMany({ + where: { mysqlPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } }, + select: { mysqlPublicPort: true } + }) + ).map((a) => a.mysqlPublicPort); + const minioUsed = await ( + await prisma.minio.findMany({ + where: { publicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } }, + select: { publicPort: true } + }) + ).map((a) => a.publicPort); + const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed]; + const range = generateRangeArray(minPort, maxPort) + const availablePorts = range.filter(port => !usedPorts.includes(port)) + for (const port of availablePorts) { + const found = await isReachable(port, { host: remoteIpAddress }) + if (!found) { + return port + } } + return false + } else { + const dbUsed = await ( + await prisma.database.findMany({ + where: { publicPort: { not: null }, id: { not: id }, destinationDocker: { engine } }, + select: { publicPort: true } + }) + ).map((a) => a.publicPort); + const wpFtpUsed = await ( + await prisma.wordpress.findMany({ + where: { ftpPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { engine } } }, + select: { ftpPublicPort: true } + }) + ).map((a) => a.ftpPublicPort); + const wpUsed = await ( + await prisma.wordpress.findMany({ + where: { mysqlPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { engine } } }, + select: { mysqlPublicPort: true } + }) + ).map((a) => a.mysqlPublicPort); + const minioUsed = await ( + await prisma.minio.findMany({ + where: { publicPort: { not: null }, id: { not: id }, service: { destinationDocker: { engine } } }, + select: { publicPort: true } + }) + ).map((a) => a.publicPort); + const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed]; + const range = generateRangeArray(minPort, maxPort) + const availablePorts = range.filter(port => !usedPorts.includes(port)) + for (const port of availablePorts) { + const found = await isReachable(port, { host: 'localhost' }) + if (!found) { + return port + } + } + return false } - return false } export async function startTraefikTCPProxy( diff --git a/apps/api/src/lib/services/handlers.ts b/apps/api/src/lib/services/handlers.ts index f56657ce7..7f13f7219 100644 --- a/apps/api/src/lib/services/handlers.ts +++ b/apps/api/src/lib/services/handlers.ts @@ -321,8 +321,8 @@ async function startMinioService(request: FastifyRequest) { const network = destinationDockerId && destinationDocker.network; const port = getServiceMainPort('minio'); - const { service: { destinationDocker: { id: dockerId } } } = await prisma.minio.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } }) - const publicPort = await getFreePublicPort(id, dockerId); + const { service: { destinationDocker: { remoteEngine, engine, remoteIpAddress } } } = await prisma.minio.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } }) + const publicPort = await getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress }); const consolePort = 9001; const { workdir } = await createDirectories({ repository: type, buildId: id }); @@ -1979,8 +1979,8 @@ async function startGlitchTipService(request: FastifyRequest) EMAIL_PORT: emailSmtpPort, EMAIL_HOST_USER: emailSmtpUser, EMAIL_HOST_PASSWORD: emailSmtpPassword, - EMAIL_USE_TLS: emailSmtpUseTls, - EMAIL_USE_SSL: emailSmtpUseSsl, + EMAIL_USE_TLS: emailSmtpUseTls ? 'True' : 'False', + EMAIL_USE_SSL: emailSmtpUseSsl ? 'True' : 'False', EMAIL_BACKEND: emailBackend, MAILGUN_API_KEY: mailgunApiKey, SENDGRID_API_KEY: sendgridApiKey, diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index 1d00e94d1..8a390a3f5 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -252,8 +252,8 @@ export async function saveApplication(request: FastifyRequest, exposePort = Number(exposePort); } - const { destinationDocker: { id: dockerId, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) - if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress }) + const { destinationDocker: { engine, remoteEngine, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) + if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }) if (denoOptions) denoOptions = denoOptions.trim(); const defaultConfiguration = await setDefaultConfiguration({ buildPack, @@ -534,14 +534,14 @@ export async function checkDNS(request: FastifyRequest) { } if (exposePort) exposePort = Number(exposePort); - const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) + const { destinationDocker: { engine, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) const { isDNSCheckEnabled } = await prisma.setting.findFirst({}); const found = await isDomainConfigured({ id, fqdn, remoteIpAddress }); if (found) { throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` } } - if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress }) + if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }) if (isDNSCheckEnabled && !isDev && !forceSave) { let hostname = request.hostname.split(':')[0]; if (remoteEngine) hostname = remoteIpAddress; diff --git a/apps/api/src/routes/api/v1/base/index.ts b/apps/api/src/routes/api/v1/base/index.ts index 76735a032..f1b64baf3 100644 --- a/apps/api/src/routes/api/v1/base/index.ts +++ b/apps/api/src/routes/api/v1/base/index.ts @@ -11,6 +11,7 @@ const root: FastifyPluginAsync = async (fastify): Promise => { version, whiteLabeled: process.env.COOLIFY_WHITE_LABELED === 'true', whiteLabeledIcon: process.env.COOLIFY_WHITE_LABELED_ICON, + isRegistrationEnabled: settings.isRegistrationEnabled, } } catch ({ status, message }) { return errorHandler({ status, message }) diff --git a/apps/api/src/routes/api/v1/databases/handlers.ts b/apps/api/src/routes/api/v1/databases/handlers.ts index d646a036f..16d43205c 100644 --- a/apps/api/src/routes/api/v1/databases/handlers.ts +++ b/apps/api/src/routes/api/v1/databases/handlers.ts @@ -3,11 +3,11 @@ import type { FastifyRequest } from 'fastify'; import { FastifyReply } from 'fastify'; import yaml from 'js-yaml'; import fs from 'fs/promises'; -import { ComposeFile, createDirectories, decrypt, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common'; +import { ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common'; import { day } from '../../../../lib/dayjs'; -import { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types'; -import { DeleteDatabase, SaveDatabaseType } from './types'; +import type { OnlyId } from '../../../../types'; +import type { DeleteDatabase, DeleteDatabaseSecret, GetDatabaseLogs, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSecret, SaveDatabaseSettings, SaveDatabaseType, SaveVersion } from './types'; export async function listDatabases(request: FastifyRequest) { try { @@ -61,16 +61,18 @@ export async function getDatabaseStatus(request: FastifyRequest) { where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }, include: { destinationDocker: true, settings: true } }); - const { destinationDockerId, destinationDocker } = database; - if (destinationDockerId) { - try { - const { stdout } = await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker inspect --format '{{json .State}}' ${id}` }) + if (database) { + const { destinationDockerId, destinationDocker } = database; + if (destinationDockerId) { + try { + const { stdout } = await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker inspect --format '{{json .State}}' ${id}` }) - if (JSON.parse(stdout).Running) { - isRunning = true; + if (JSON.parse(stdout).Running) { + isRunning = true; + } + } catch (error) { + // } - } catch (error) { - // } } return { @@ -92,15 +94,14 @@ export async function getDatabase(request: FastifyRequest) { if (!database) { throw { status: 404, message: 'Database not found.' } } - const { arch } = await listSettings(); + const settings = await listSettings(); if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword); if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword); - const configuration = generateDatabaseConfiguration(database, arch); - const settings = await listSettings(); + const configuration = generateDatabaseConfiguration(database, settings.arch); return { privatePort: configuration?.privatePort, database, - versions: await getDatabaseVersions(database.type, arch), + versions: await getDatabaseVersions(database.type, settings.arch), settings }; } catch ({ status, message }) { @@ -220,7 +221,7 @@ export async function startDatabase(request: FastifyRequest) { const database = await prisma.database.findFirst({ where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { destinationDocker: true, settings: true } + include: { destinationDocker: true, settings: true, databaseSecret: true } }); const { arch } = await listSettings(); if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword); @@ -230,7 +231,8 @@ export async function startDatabase(request: FastifyRequest) { destinationDockerId, destinationDocker, publicPort, - settings: { isPublic } + settings: { isPublic }, + databaseSecret } = database; const { privatePort, command, environmentVariables, image, volume, ulimits } = generateDatabaseConfiguration(database, arch); @@ -240,7 +242,11 @@ export async function startDatabase(request: FastifyRequest) { const labels = await makeLabelForStandaloneDatabase({ id, image, volume }); const { workdir } = await createDirectories({ repository: type, buildId: id }); - + if (databaseSecret.length > 0) { + databaseSecret.forEach((secret) => { + environmentVariables[secret.name] = decrypt(secret.value); + }); + } const composeFile: ComposeFile = { version: '3.8', services: { @@ -248,20 +254,11 @@ export async function startDatabase(request: FastifyRequest) { container_name: id, image, command, - networks: [network], environment: environmentVariables, volumes: [volume], ulimits, labels, - restart: 'always', - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - } + ...defaultComposeConfiguration(network), } }, networks: { @@ -271,25 +268,16 @@ export async function startDatabase(request: FastifyRequest) { }, volumes: { [volumeName]: { - external: true + name: volumeName, } } }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - try { - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker volume create ${volumeName}` }) - } catch (error) { } - try { - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` }) - if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort); - return {}; - } catch (error) { - throw { - error - }; - } + await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` }) + if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort); + return {}; + } catch ({ status, message }) { return errorHandler({ status, message }) } @@ -376,6 +364,7 @@ export async function deleteDatabase(request: FastifyRequest) { } } await prisma.databaseSettings.deleteMany({ where: { databaseId: id } }); + await prisma.databaseSecret.deleteMany({ where: { databaseId: id } }); await prisma.database.delete({ where: { id } }); return {} } catch ({ status, message }) { @@ -436,10 +425,10 @@ export async function saveDatabaseSettings(request: FastifyRequest) { + try { + const { id } = request.params + let secrets = await prisma.databaseSecret.findMany({ + where: { databaseId: id }, + orderBy: { createdAt: 'desc' } + }); + secrets = secrets.map((secret) => { + secret.value = decrypt(secret.value); + return secret; + }); + + return { + secrets + } + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +export async function saveDatabaseSecret(request: FastifyRequest, reply: FastifyReply) { + try { + const { id } = request.params + let { name, value, isNew } = request.body + + if (isNew) { + const found = await prisma.databaseSecret.findFirst({ where: { name, databaseId: id } }); + if (found) { + throw `Secret ${name} already exists.` + } else { + value = encrypt(value.trim()); + await prisma.databaseSecret.create({ + data: { name, value, database: { connect: { id } } } + }); + } + } else { + value = encrypt(value.trim()); + const found = await prisma.databaseSecret.findFirst({ where: { databaseId: id, name } }); + + if (found) { + await prisma.databaseSecret.updateMany({ + where: { databaseId: id, name }, + data: { value } + }); + } else { + await prisma.databaseSecret.create({ + data: { name, value, database: { connect: { id } } } + }); + } + } + return reply.code(201).send() + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} +export async function deleteDatabaseSecret(request: FastifyRequest) { + try { + const { id } = request.params + const { name } = request.body + await prisma.databaseSecret.deleteMany({ where: { databaseId: id, name } }); + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} diff --git a/apps/api/src/routes/api/v1/databases/index.ts b/apps/api/src/routes/api/v1/databases/index.ts index f15fdd073..8f269c8b5 100644 --- a/apps/api/src/routes/api/v1/databases/index.ts +++ b/apps/api/src/routes/api/v1/databases/index.ts @@ -1,8 +1,9 @@ import { FastifyPluginAsync } from 'fastify'; -import { deleteDatabase, getDatabase, getDatabaseLogs, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers'; +import { deleteDatabase, deleteDatabaseSecret, getDatabase, getDatabaseLogs, getDatabaseSecrets, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSecret, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers'; -import type { DeleteDatabase, GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types'; -import type { SaveDatabaseType } from './types'; +import type { OnlyId } from '../../../../types'; + +import type { DeleteDatabase, SaveDatabaseType, DeleteDatabaseSecret, GetDatabaseLogs, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSecret, SaveDatabaseSettings, SaveVersion } from './types'; const root: FastifyPluginAsync = async (fastify): Promise => { fastify.addHook('onRequest', async (request) => { @@ -19,6 +20,10 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.post('/:id/settings', async (request) => await saveDatabaseSettings(request)); + fastify.get('/:id/secrets', async (request) => await getDatabaseSecrets(request)); + fastify.post('/:id/secrets', async (request, reply) => await saveDatabaseSecret(request, reply)); + fastify.delete('/:id/secrets', async (request) => await deleteDatabaseSecret(request)); + fastify.get('/:id/configuration/type', async (request) => await getDatabaseTypes(request)); fastify.post('/:id/configuration/type', async (request, reply) => await saveDatabaseType(request, reply)); diff --git a/apps/api/src/routes/api/v1/databases/types.ts b/apps/api/src/routes/api/v1/databases/types.ts index a56a45c23..ba9b0b8e9 100644 --- a/apps/api/src/routes/api/v1/databases/types.ts +++ b/apps/api/src/routes/api/v1/databases/types.ts @@ -5,4 +5,51 @@ export interface SaveDatabaseType extends OnlyId { } export interface DeleteDatabase extends OnlyId { Body: { force: string } -} \ No newline at end of file +} +export interface SaveVersion extends OnlyId { + Body: { + version: string + } +} +export interface SaveDatabaseDestination extends OnlyId { + Body: { + destinationId: string + } +} +export interface GetDatabaseLogs extends OnlyId { + Querystring: { + since: number + } +} +export interface SaveDatabase extends OnlyId { + Body: { + name: string, + defaultDatabase: string, + dbUser: string, + dbUserPassword: string, + rootUser: string, + rootUserPassword: string, + version: string, + isRunning: boolean + } +} +export interface SaveDatabaseSettings extends OnlyId { + Body: { + isPublic: boolean, + appendOnly: boolean + } +} + +export interface SaveDatabaseSecret extends OnlyId { + Body: { + name: string, + value: string, + isNew: string, + } +} +export interface DeleteDatabaseSecret extends OnlyId { + Body: { + name: string, + } +} + diff --git a/apps/api/src/routes/api/v1/handlers.ts b/apps/api/src/routes/api/v1/handlers.ts index 702ad31a8..cdb115af3 100644 --- a/apps/api/src/routes/api/v1/handlers.ts +++ b/apps/api/src/routes/api/v1/handlers.ts @@ -1,5 +1,4 @@ -import os from 'node:os'; -import osu from 'node-os-utils'; + import axios from 'axios'; import { compareVersions } from 'compare-versions'; import cuid from 'cuid'; @@ -15,9 +14,10 @@ export async function hashPassword(password: string): Promise { return bcrypt.hash(password, saltRounds); } -export async function cleanupManually() { +export async function cleanupManually(request: FastifyRequest) { try { - const destination = await prisma.destinationDocker.findFirst({ where: { engine: '/var/run/docker.sock' } }) + const { serverId } = request.body; + const destination = await prisma.destinationDocker.findUnique({ where: { id: serverId } }) await cleanupDockerStorage(destination.id, true, true) return {} } catch ({ status, message }) { @@ -52,9 +52,7 @@ export async function update(request: FastifyRequest) { const { latestVersion } = request.body; try { if (!isDev) { - const { isAutoUpdateEnabled } = (await prisma.setting.findFirst()) || { - isAutoUpdateEnabled: false - }; + const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); await asyncExecShell(`env | grep COOLIFY > .env`); await asyncExecShell( @@ -88,45 +86,38 @@ export async function restartCoolify(request: FastifyRequest) { return errorHandler({ status, message }) } } -export async function showUsage() { - try { - return { - usage: { - uptime: os.uptime(), - memory: await osu.mem.info(), - cpu: { - load: os.loadavg(), - usage: await osu.cpu.usage(), - count: os.cpus().length - }, - disk: await osu.drive.info('/') - } - }; - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} export async function showDashboard(request: FastifyRequest) { try { const userId = request.user.userId; const teamId = request.user.teamId; const applications = await prisma.application.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { settings: true } + include: { settings: true, destinationDocker: true, teams: true } }); const databases = await prisma.database.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { settings: true } + include: { settings: true, destinationDocker: true, teams: true } }); const services = await prisma.service.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } + where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, + include: { destinationDocker: true, teams: true } + }); + const gitSources = await prisma.gitSource.findMany({ + where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, + include: { teams: true } + }); + const destinations = await prisma.destinationDocker.findMany({ + where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, + include: { teams: true } }); const settings = await listSettings(); return { applications, databases, services, + gitSources, + destinations, settings, }; } catch ({ status, message }) { diff --git a/apps/api/src/routes/api/v1/index.ts b/apps/api/src/routes/api/v1/index.ts index 8f9f821f8..52310998d 100644 --- a/apps/api/src/routes/api/v1/index.ts +++ b/apps/api/src/routes/api/v1/index.ts @@ -43,17 +43,13 @@ const root: FastifyPluginAsync = async (fastify): Promise => { onRequest: [fastify.authenticate] }, async (request) => await showDashboard(request)); - fastify.get('/usage', { - onRequest: [fastify.authenticate] - }, async () => await showUsage()); - fastify.post('/internal/restart', { onRequest: [fastify.authenticate] }, async (request) => await restartCoolify(request)); fastify.post('/internal/cleanup', { onRequest: [fastify.authenticate] - }, async () => await cleanupManually()); + }, async (request) => await cleanupManually(request)); }; export default root; diff --git a/apps/api/src/routes/api/v1/servers/handlers.ts b/apps/api/src/routes/api/v1/servers/handlers.ts new file mode 100644 index 000000000..c917836d2 --- /dev/null +++ b/apps/api/src/routes/api/v1/servers/handlers.ts @@ -0,0 +1,119 @@ +import type { FastifyRequest } from 'fastify'; +import { errorHandler, executeDockerCmd, prisma, createRemoteEngineConfiguration, executeSSHCmd } from '../../../../lib/common'; +import os from 'node:os'; +import osu from 'node-os-utils'; + + +export async function listServers(request: FastifyRequest) { + try { + const userId = request.user.userId; + const teamId = request.user.teamId; + const servers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } }, remoteEngine: false }, distinct: ['engine'] }) + // const remoteServers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, distinct: ['remoteIpAddress', 'engine'] }) + + return { + servers + } + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} +const mappingTable = [ + ['K total memory', 'totalMemoryKB'], + ['K used memory', 'usedMemoryKB'], + ['K active memory', 'activeMemoryKB'], + ['K inactive memory', 'inactiveMemoryKB'], + ['K free memory', 'freeMemoryKB'], + ['K buffer memory', 'bufferMemoryKB'], + ['K swap cache', 'swapCacheKB'], + ['K total swap', 'totalSwapKB'], + ['K used swap', 'usedSwapKB'], + ['K free swap', 'freeSwapKB'], + ['non-nice user cpu ticks', 'nonNiceUserCpuTicks'], + ['nice user cpu ticks', 'niceUserCpuTicks'], + ['system cpu ticks', 'systemCpuTicks'], + ['idle cpu ticks', 'idleCpuTicks'], + ['IO-wait cpu ticks', 'ioWaitCpuTicks'], + ['IRQ cpu ticks', 'irqCpuTicks'], + ['softirq cpu ticks', 'softIrqCpuTicks'], + ['stolen cpu ticks', 'stolenCpuTicks'], + ['pages paged in', 'pagesPagedIn'], + ['pages paged out', 'pagesPagedOut'], + ['pages swapped in', 'pagesSwappedIn'], + ['pages swapped out', 'pagesSwappedOut'], + ['interrupts', 'interrupts'], + ['CPU context switches', 'cpuContextSwitches'], + ['boot time', 'bootTime'], + ['forks', 'forks'] +]; +function parseFromText(text) { + var data = {}; + var lines = text.split(/\r?\n/); + for (const line of lines) { + for (const [key, value] of mappingTable) { + if (line.indexOf(key) >= 0) { + const values = line.match(/[0-9]+/)[0]; + data[value] = parseInt(values, 10); + } + } + } + return data; +} +export async function showUsage(request: FastifyRequest) { + const { id } = request.params; + let { remoteEngine } = request.query + remoteEngine = remoteEngine === 'true' ? true : false + if (remoteEngine) { + const { stdout: stats } = await executeSSHCmd({ dockerId: id, command: `vmstat -s` }) + const { stdout: disks } = await executeSSHCmd({ dockerId: id, command: `df -m / --output=size,used,pcent|grep -v 'Used'| xargs` }) + const { stdout: cpus } = await executeSSHCmd({ dockerId: id, command: `nproc --all` }) + // const { stdout: cpuUsage } = await executeSSHCmd({ dockerId: id, command: `echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')]` }) + // console.log(cpuUsage) + const parsed: any = parseFromText(stats) + return { + usage: { + uptime: parsed.bootTime / 1024, + memory: { + totalMemMb: parsed.totalMemoryKB / 1024, + usedMemMb: parsed.usedMemoryKB / 1024, + freeMemMb: parsed.freeMemoryKB / 1024, + usedMemPercentage: (parsed.usedMemoryKB / parsed.totalMemoryKB) * 100, + freeMemPercentage: (parsed.totalMemoryKB - parsed.usedMemoryKB) / parsed.totalMemoryKB * 100 + }, + cpu: { + load: 0, + usage: 0, + count: cpus + }, + disk: { + totalGb: (disks.split(' ')[0] / 1024).toFixed(1), + usedGb: (disks.split(' ')[1] / 1024).toFixed(1), + freeGb: (disks.split(' ')[0] - disks.split(' ')[1]).toFixed(1), + usedPercentage: disks.split(' ')[2].replace('%', ''), + freePercentage: 100 - disks.split(' ')[2].replace('%', '') + } + + } + } + } else { + try { + return { + usage: { + uptime: os.uptime(), + memory: await osu.mem.info(), + cpu: { + load: os.loadavg(), + usage: await osu.cpu.usage(), + count: os.cpus().length + }, + disk: await osu.drive.info('/') + } + + }; + } catch ({ status, message }) { + return errorHandler({ status, message }) + } + } + + +} \ No newline at end of file diff --git a/apps/api/src/routes/api/v1/servers/index.ts b/apps/api/src/routes/api/v1/servers/index.ts new file mode 100644 index 000000000..7373d8cb4 --- /dev/null +++ b/apps/api/src/routes/api/v1/servers/index.ts @@ -0,0 +1,14 @@ +import { FastifyPluginAsync } from 'fastify'; +import { listServers, showUsage } from './handlers'; + + +const root: FastifyPluginAsync = async (fastify): Promise => { + fastify.addHook('onRequest', async (request) => { + return await request.jwtVerify() + }) + fastify.get('/', async (request) => await listServers(request)); + fastify.get('/usage/:id', async (request) => await showUsage(request)); + +}; + +export default root; diff --git a/apps/api/src/routes/api/v1/servers/types.ts b/apps/api/src/routes/api/v1/servers/types.ts new file mode 100644 index 000000000..26a896b8e --- /dev/null +++ b/apps/api/src/routes/api/v1/servers/types.ts @@ -0,0 +1,27 @@ +import { OnlyId } from "../../../../types" + +export interface SaveTeam extends OnlyId { + Body: { + name: string + } +} +export interface InviteToTeam { + Body: { + email: string, + permission: string, + teamId: string, + teamName: string + } +} +export interface BodyId { + Body: { + id: string + } +} +export interface SetPermission { + Body: { + userId: string, + newPermission: string, + permissionId: string + } +} \ No newline at end of file diff --git a/apps/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index cd1c996bf..c6f10cbb4 100644 --- a/apps/api/src/routes/api/v1/services/handlers.ts +++ b/apps/api/src/routes/api/v1/services/handlers.ts @@ -233,7 +233,7 @@ export async function checkService(request: FastifyRequest) { if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase()); if (exposePort) exposePort = Number(exposePort); - const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } }) + const { destinationDocker: { remoteIpAddress, remoteEngine, engine }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } }) const { isDNSCheckEnabled } = await prisma.setting.findFirst({}); let found = await isDomainConfigured({ id, fqdn, remoteIpAddress }); @@ -248,7 +248,7 @@ export async function checkService(request: FastifyRequest) { } } } - if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress }) + if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }) if (isDNSCheckEnabled && !isDev && !forceSave) { let hostname = request.hostname.split(':')[0]; if (remoteEngine) hostname = remoteIpAddress; @@ -485,9 +485,9 @@ export async function activateWordpressFtp(request: FastifyRequest -
+
{#if $appSession.teamId === '0'} {#if isUpdateAvailable} + New Version Available! {/if} {/if}
diff --git a/apps/ui/src/lib/components/Usage.svelte b/apps/ui/src/lib/components/Usage.svelte index a5128c6b6..7d4a82929 100644 --- a/apps/ui/src/lib/components/Usage.svelte +++ b/apps/ui/src/lib/components/Usage.svelte @@ -1,4 +1,5 @@ -
-
-

Hardware Details

-
- {#if $appSession.teamId === '0'} - - {/if} +
+ {#if loading.usage} + + {:else} + + {/if} + {#if server.remoteEngine} +
+ BETA
+ {/if} +
+
+

+ {server.name} +

+
+ {#if server?.remoteIpAddress} +

{server?.remoteIpAddress}

+ {:else} +

localhost

+ {/if} +
+
+ {#if $appSession.teamId === '0'} + + {/if} +
+
+
@@ -82,21 +109,21 @@
Total Memory
- {(usage?.memory.totalMemMb).toFixed(0)}MB + {(usage?.memory?.totalMemMb).toFixed(0)}MB
Used Memory
- {(usage?.memory.usedMemMb).toFixed(0)}MB + {(usage?.memory?.usedMemMb).toFixed(0)}MB
Free Memory
- {usage?.memory.freeMemPercentage}% + {(usage?.memory?.freeMemPercentage).toFixed(0)}%
@@ -105,41 +132,41 @@
Total CPU
- {usage?.cpu.count} + {usage?.cpu?.count}
CPU Usage
- {usage?.cpu.usage}% + {usage?.cpu?.usage}%
Load Average (5,10,30mins)
-
{usage?.cpu.load}
+
{usage?.cpu?.load}
Total Disk
- {usage?.disk.totalGb}GB + {usage?.disk?.totalGb}GB
Used Disk
- {usage?.disk.usedGb}GB + {usage?.disk?.usedGb}GB
Free Disk
- {usage?.disk.freePercentage}% + {usage?.disk?.freePercentage}%
diff --git a/apps/ui/src/lib/components/svg/databases/DatabaseIcons.svelte b/apps/ui/src/lib/components/svg/databases/DatabaseIcons.svelte index 1ef5271c4..4a00860af 100644 --- a/apps/ui/src/lib/components/svg/databases/DatabaseIcons.svelte +++ b/apps/ui/src/lib/components/svg/databases/DatabaseIcons.svelte @@ -16,4 +16,6 @@ {:else if type === 'couchdb'} +{:else if type === 'edgedb'} + {/if} diff --git a/apps/ui/src/lib/components/svg/databases/EdgeDB.svelte b/apps/ui/src/lib/components/svg/databases/EdgeDB.svelte new file mode 100644 index 000000000..57fdebed5 --- /dev/null +++ b/apps/ui/src/lib/components/svg/databases/EdgeDB.svelte @@ -0,0 +1,22 @@ + + + diff --git a/apps/ui/src/lib/components/svg/databases/index.ts b/apps/ui/src/lib/components/svg/databases/index.ts index 1b981d61c..e200b5311 100644 --- a/apps/ui/src/lib/components/svg/databases/index.ts +++ b/apps/ui/src/lib/components/svg/databases/index.ts @@ -6,5 +6,6 @@ export { default as MongoDB } from './MongoDB.svelte'; export { default as MySQL } from './MySQL.svelte'; export { default as PostgreSQL } from './PostgreSQL.svelte'; export { default as Redis } from './Redis.svelte'; +export { default as EdgeDB } from './EdgeDB.svelte'; diff --git a/apps/ui/src/lib/store.ts b/apps/ui/src/lib/store.ts index 4114026d5..ec841a2e5 100644 --- a/apps/ui/src/lib/store.ts +++ b/apps/ui/src/lib/store.ts @@ -3,7 +3,7 @@ import cuid from 'cuid'; import { writable, readable, type Writable } from 'svelte/store'; interface AppSession { - registrationEnabled: boolean; + isRegistrationEnabled: boolean; ipv4: string | null, ipv6: string | null, version: string | null, @@ -26,8 +26,11 @@ interface AddToast { message: string, timeout?: number | undefined } + +export const search: any = writable('') export const loginEmail: Writable = writable() export const appSession: Writable = writable({ + isRegistrationEnabled: false, ipv4: null, ipv6: null, version: null, @@ -83,7 +86,8 @@ export const status: Writable = writable({ isRunning: false, isExited: false, loading: false, - initialLoading: true + initialLoading: true, + isPublic: false } }); diff --git a/apps/ui/src/routes/_NewResource.svelte b/apps/ui/src/routes/_NewResource.svelte new file mode 100644 index 000000000..5729ae81c --- /dev/null +++ b/apps/ui/src/routes/_NewResource.svelte @@ -0,0 +1,150 @@ + + + diff --git a/apps/ui/src/routes/__layout.svelte b/apps/ui/src/routes/__layout.svelte index 901aedea5..8694dfe45 100644 --- a/apps/ui/src/routes/__layout.svelte +++ b/apps/ui/src/routes/__layout.svelte @@ -66,7 +66,7 @@ + + + +
+
{$t('index.applications')}
+ {#if $appSession.isAdmin} + + {/if} +
+ diff --git a/apps/ui/src/routes/applications/index.svelte b/apps/ui/src/routes/applications/index.svelte index 60644e16d..8136f76e8 100644 --- a/apps/ui/src/routes/applications/index.svelte +++ b/apps/ui/src/routes/applications/index.svelte @@ -1,142 +1,4 @@ - - - -
-
{$t('index.applications')}
- {#if $appSession.isAdmin} - - {/if} -
- diff --git a/apps/ui/src/routes/databases/[id]/_DatabaseLinks.svelte b/apps/ui/src/routes/databases/[id]/_DatabaseLinks.svelte index 50eed98b2..f1792cb6b 100644 --- a/apps/ui/src/routes/databases/[id]/_DatabaseLinks.svelte +++ b/apps/ui/src/routes/databases/[id]/_DatabaseLinks.svelte @@ -3,6 +3,7 @@ import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte'; import CouchDb from '$lib/components/svg/databases/CouchDB.svelte'; + import EdgeDb from '$lib/components/svg/databases/EdgeDB.svelte'; import MariaDb from '$lib/components/svg/databases/MariaDB.svelte'; import MongoDb from '$lib/components/svg/databases/MongoDB.svelte'; import MySql from '$lib/components/svg/databases/MySQL.svelte'; @@ -25,5 +26,7 @@ {:else if database.type === 'redis'} + {:else if database.type === 'edgedb'} + {/if} diff --git a/apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte b/apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte index 5cfa6025e..14a88b00b 100644 --- a/apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte +++ b/apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte @@ -12,6 +12,7 @@ import PostgreSql from './_PostgreSQL.svelte'; import Redis from './_Redis.svelte'; import CouchDb from './_CouchDb.svelte'; + import EdgeDB from './_EdgeDB.svelte'; import { post } from '$lib/api'; import { t } from '$lib/translations'; import { errorNotification } from '$lib/common'; @@ -23,7 +24,6 @@ let loading = false; let publicLoading = false; - let isPublic = database.settings.isPublic || false; let appendOnly = database.settings.appendOnly; let databaseDefault: any; @@ -36,8 +36,10 @@ databaseDefault = database.defaultDatabase; databaseDbUser = database.dbUser; databaseDbUserPassword = database.dbUserPassword; - if (database.type === 'mongodb') { - databaseDefault = '?readPreference=primary&ssl=false'; + if (database.type === 'mongodb' || database.type === 'edgedb') { + if (database.type === 'mongodb') { + databaseDefault = '?readPreference=primary&ssl=false'; + } databaseDbUser = database.rootUser; databaseDbUserPassword = database.rootUserPassword; } else if (database.type === 'redis') { @@ -49,12 +51,12 @@ return `${database.type}://${ databaseDbUser ? databaseDbUser + ':' : '' }${databaseDbUserPassword}@${ - isPublic + $status.database.isPublic ? database.destinationDocker.remoteEngine ? database.destinationDocker.remoteIpAddress : $appSession.ipv4 : database.id - }:${isPublic ? database.publicPort : privatePort}/${databaseDefault}`; + }:${$status.database.isPublic ? database.publicPort : privatePort}/${databaseDefault}`; } async function changeSettings(name: any) { @@ -63,11 +65,11 @@ } publicLoading = true; let data = { - isPublic, + isPublic: $status.database.isPublic, appendOnly }; if (name === 'isPublic') { - data.isPublic = !isPublic; + data.isPublic = !$status.database.isPublic; } if (name === 'appendOnly') { data.appendOnly = !appendOnly; @@ -77,9 +79,9 @@ isPublic: data.isPublic, appendOnly: data.appendOnly }); - isPublic = data.isPublic; + $status.database.isPublic = data.isPublic; appendOnly = data.appendOnly; - if (isPublic) { + if ($status.database.isPublic) { database.publicPort = publicPort; } } catch (error) { @@ -188,7 +190,7 @@ readonly disabled name="publicPort" - value={publicLoading ? 'Loading...' : isPublic ? database.publicPort : privatePort} + value={publicLoading ? 'Loading...' : $status.database.isPublic ? database.publicPort : privatePort} />
@@ -205,12 +207,14 @@ {:else if database.type === 'couchdb'} + {:else if database.type === 'edgedb'} + {/if}