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/routes/api/v1/databases/handlers.ts b/apps/api/src/routes/api/v1/databases/handlers.ts index 6d62cca71..c76502be7 100644 --- a/apps/api/src/routes/api/v1/databases/handlers.ts +++ b/apps/api/src/routes/api/v1/databases/handlers.ts @@ -6,7 +6,7 @@ import fs from 'fs/promises'; 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 { DeleteDatabaseSecret, GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSecret, SaveDatabaseSettings, SaveVersion } from '../../../../types'; import { DeleteDatabase, SaveDatabaseType } from './types'; export async function listDatabases(request: FastifyRequest) { @@ -220,7 +220,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 +230,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 +241,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: { @@ -262,25 +267,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 }) } @@ -462,4 +458,69 @@ 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..5dcc20c68 100644 --- a/apps/api/src/routes/api/v1/databases/index.ts +++ b/apps/api/src/routes/api/v1/databases/index.ts @@ -1,7 +1,7 @@ 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 { DeleteDatabase, DeleteDatabaseSecret, GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSecret, SaveDatabaseSettings, SaveVersion } from '../../../../types'; import type { SaveDatabaseType } from './types'; const root: FastifyPluginAsync = async (fastify): Promise => { @@ -19,6 +19,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/types.ts b/apps/api/src/types.ts index 71f3db158..3767ab4dd 100644 --- a/apps/api/src/types.ts +++ b/apps/api/src/types.ts @@ -35,4 +35,17 @@ export interface SaveDatabaseSettings extends OnlyId { } } +export interface SaveDatabaseSecret extends OnlyId { + Body: { + name: string, + value: string, + isNew: string, + } +} +export interface DeleteDatabaseSecret extends OnlyId { + Body: { + name: string, + } +} + diff --git a/apps/ui/src/routes/databases/[id]/_Secret.svelte b/apps/ui/src/routes/databases/[id]/_Secret.svelte new file mode 100644 index 000000000..f67a377e4 --- /dev/null +++ b/apps/ui/src/routes/databases/[id]/_Secret.svelte @@ -0,0 +1,186 @@ + + + + + + + + + + + + + {#if isNewSecret} +
+ +
+ {:else} +
+
+ +
+ {#if !isPRMRSecret} +
+ +
+ {/if} +
+ {/if} + diff --git a/apps/ui/src/routes/databases/[id]/__layout.svelte b/apps/ui/src/routes/databases/[id]/__layout.svelte index 26f8bd9e7..9b690a7ba 100644 --- a/apps/ui/src/routes/databases/[id]/__layout.svelte +++ b/apps/ui/src/routes/databases/[id]/__layout.svelte @@ -58,7 +58,7 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; import { errorNotification, handlerNotFoundLoad } from '$lib/common'; - import { appSession, status, disabledButton } from '$lib/store'; + import { appSession, status, isDeploymentEnabled } from '$lib/store'; import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import { onDestroy, onMount } from 'svelte'; import Tooltip from '$lib/components/Tooltip.svelte'; @@ -67,7 +67,7 @@ let statusInterval: any = false; let forceDelete = false; - $disabledButton = !$appSession.isAdmin; + $isDeploymentEnabled = !$appSession.isAdmin; async function deleteDatabase(force: boolean) { const sure = confirm(`Are you sure you would like to delete '${database.name}'?`); @@ -148,11 +148,11 @@ {#if id !== 'new'}