diff --git a/src/lib/components/ServiceLinks.svelte b/src/lib/components/ServiceLinks.svelte index 6097abbd2..b8bafc369 100644 --- a/src/lib/components/ServiceLinks.svelte +++ b/src/lib/components/ServiceLinks.svelte @@ -12,6 +12,7 @@ import VaultWarden from './svg/services/VaultWarden.svelte'; import VsCodeServer from './svg/services/VSCodeServer.svelte'; import Wordpress from './svg/services/Wordpress.svelte'; + import Fider from './svg/services/Fider.svelte'; {#if service.type === 'plausibleanalytics'} @@ -62,4 +63,8 @@ +{:else if service.type === 'fider'} + + + {/if} diff --git a/src/lib/components/common.ts b/src/lib/components/common.ts index 9f0d16263..d6ff2f8ea 100644 --- a/src/lib/components/common.ts +++ b/src/lib/components/common.ts @@ -202,5 +202,16 @@ export const supportedServiceTypesAndVersions = [ ports: { main: 8080 } + }, + { + name: 'fider', + fancyName: 'Fider', + baseImage: 'getfider/fider', + images: ['postgres:12-alpine'], + versions: ['stable'], + recommendedVersion: 'stable', + ports: { + main: 3000 + } } ]; diff --git a/src/routes/services/[id]/_Services/_Fider.svelte b/src/routes/services/[id]/_Services/_Fider.svelte new file mode 100644 index 000000000..df494de94 --- /dev/null +++ b/src/routes/services/[id]/_Services/_Fider.svelte @@ -0,0 +1,183 @@ + + +
+
Fider
+
+ +
+ + +
+ +
+ + +
+
+
Email
+
+
+ + +
+ +
+ + +
+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
PostgreSQL
+
+ +
+ + +
+
+ + +
+
+ + +
diff --git a/src/routes/services/[id]/_Services/_Services.svelte b/src/routes/services/[id]/_Services/_Services.svelte index 21db6a598..a2a2effac 100644 --- a/src/routes/services/[id]/_Services/_Services.svelte +++ b/src/routes/services/[id]/_Services/_Services.svelte @@ -12,6 +12,7 @@ import { errorNotification } from '$lib/form'; import { t } from '$lib/translations'; import { toast } from '@zerodevx/svelte-toast'; + import Fider from './_Fider.svelte'; import Ghost from './_Ghost.svelte'; import Hasura from './_Hasura.svelte'; import MeiliSearch from './_MeiliSearch.svelte'; @@ -175,6 +176,8 @@ {:else if service.type === 'hasura'} + {:else if service.type === 'fider'} + {/if}
diff --git a/src/routes/services/[id]/configuration/type.svelte b/src/routes/services/[id]/configuration/type.svelte index 788a53c9d..a736c7f15 100644 --- a/src/routes/services/[id]/configuration/type.svelte +++ b/src/routes/services/[id]/configuration/type.svelte @@ -45,6 +45,7 @@ import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte'; import Umami from '$lib/components/svg/services/Umami.svelte'; import Hasura from '$lib/components/svg/services/Hasura.svelte'; + import Fider from '$lib/components/svg/services/Fider.svelte'; const { id } = $page.params; const from = $page.url.searchParams.get('from'); @@ -96,6 +97,8 @@ {:else if type.name === 'hasura'} + {:else if type.name === 'fider'} + {/if}{type.fancyName} diff --git a/src/routes/services/[id]/fider/index.json.ts b/src/routes/services/[id]/fider/index.json.ts new file mode 100644 index 000000000..b561e3392 --- /dev/null +++ b/src/routes/services/[id]/fider/index.json.ts @@ -0,0 +1,57 @@ +import { getUserDetails } from '$lib/common'; +import { encrypt } from '$lib/crypto'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const post: RequestHandler = async (event) => { + const { status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + let { + name, + fqdn, + fider: { + emailNoreply, + emailMailgunApiKey, + emailMailgunDomain, + emailMailgunRegion, + emailSmtpHost, + emailSmtpPort, + emailSmtpUser, + emailSmtpPassword, + emailSmtpEnableStartTls + } + } = await event.request.json(); + + if (fqdn) fqdn = fqdn.toLowerCase(); + if (emailNoreply) emailNoreply = emailNoreply.toLowerCase(); + if (emailSmtpHost) emailSmtpHost = emailSmtpHost.toLowerCase(); + if (emailSmtpPassword) { + emailSmtpPassword = encrypt(emailSmtpPassword); + } + if (emailSmtpPort) emailSmtpPort = Number(emailSmtpPort); + if (emailSmtpEnableStartTls) emailSmtpEnableStartTls = Boolean(emailSmtpEnableStartTls); + + try { + await db.updateFiderService({ + id, + fqdn, + name, + emailNoreply, + emailMailgunApiKey, + emailMailgunDomain, + emailMailgunRegion, + emailSmtpHost, + emailSmtpPort, + emailSmtpUser, + emailSmtpPassword, + emailSmtpEnableStartTls + }); + return { status: 201 }; + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/fider/start.json.ts b/src/routes/services/[id]/fider/start.json.ts new file mode 100644 index 000000000..465b4195b --- /dev/null +++ b/src/routes/services/[id]/fider/start.json.ts @@ -0,0 +1,147 @@ +import { + asyncExecShell, + createDirectories, + getDomain, + getEngine, + getUserDetails +} from '$lib/common'; +import * as db from '$lib/database'; +import { promises as fs } from 'fs'; +import yaml from 'js-yaml'; +import type { RequestHandler } from '@sveltejs/kit'; +import { ErrorHandler, getServiceImage } from '$lib/database'; +import { makeLabelForServices } from '$lib/buildPacks/common'; +import type { ComposeFile } from '$lib/types/composeFile'; +import type { Service, DestinationDocker, Prisma } from '@prisma/client'; + +export const post: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + try { + const service: Service & Prisma.ServiceInclude & { destinationDocker: DestinationDocker } = + await db.getService({ id, teamId }); + const { + type, + version, + fqdn, + destinationDockerId, + destinationDocker, + serviceSecret, + fider: { + postgresqlUser, + postgresqlPassword, + postgresqlDatabase, + jwtSecret, + emailNoreply, + emailMailgunApiKey, + emailMailgunDomain, + emailSmtpHost, + emailSmtpPort, + emailSmtpUser, + emailSmtpPassword, + emailSmtpEnableStartTls + } + } = service; + const network = destinationDockerId && destinationDocker.network; + const host = getEngine(destinationDocker.engine); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + const domain = getDomain(fqdn); + const config = { + fider: { + image: `${image}:${version}`, + environmentVariables: { + HOST_DOMAIN: domain, + DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}?sslmode=disable`, + JWT_SECRET: `${jwtSecret.replace(/\$/g, '$$$')}`, + EMAIL_NOREPLY: emailNoreply, + EMAIL_MAILGUN_API: emailMailgunApiKey || null, + EMAIL_MAILGUN_DOMAIN: emailMailgunDomain || null + } + }, + postgresql: { + image: 'postgres:12-alpine', + volume: `${id}-postgresql-data:/var/lib/postgresql/data`, + environmentVariables: { + POSTGRES_USER: postgresqlUser, + POSTGRES_PASSWORD: postgresqlPassword, + POSTGRES_DB: postgresqlDatabase + } + } + }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.fider.environmentVariables[secret.name] = secret.value; + }); + } + + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.fider.image, + environment: config.fider.environmentVariables, + networks: [network], + volumes: [], + restart: 'always', + labels: makeLabelForServices('fider'), + deploy: { + restart_policy: { + condition: 'on-failure', + delay: '5s', + max_attempts: 3, + window: '120s' + } + }, + depends_on: [`${id}-postgresql`] + }, + [`${id}-postgresql`]: { + image: config.postgresql.image, + container_name: `${id}-postgresql`, + environment: config.postgresql.environmentVariables, + networks: [network], + volumes: [config.postgresql.volume], + restart: 'always', + deploy: { + restart_policy: { + condition: 'on-failure', + delay: '5s', + max_attempts: 3, + window: '120s' + } + } + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: { + [config.postgresql.volume.split(':')[0]]: { + name: config.postgresql.volume.split(':')[0] + } + } + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + try { + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); + return { + status: 200 + }; + } catch (error) { + console.log(error); + return ErrorHandler(error); + } + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/fider/stop.json.ts b/src/routes/services/[id]/fider/stop.json.ts new file mode 100644 index 000000000..67dd96d04 --- /dev/null +++ b/src/routes/services/[id]/fider/stop.json.ts @@ -0,0 +1,42 @@ +import { getUserDetails, removeDestinationDocker } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import { checkContainer, stopTcpHttpProxy } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const post: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + try { + const service = await db.getService({ id, teamId }); + const { destinationDockerId, destinationDocker } = service; + if (destinationDockerId) { + const engine = destinationDocker.engine; + + try { + const found = await checkContainer(engine, id); + if (found) { + await removeDestinationDocker({ id, engine }); + } + } catch (error) { + console.error(error); + } + try { + const found = await checkContainer(engine, `${id}-postgresql`); + if (found) { + await removeDestinationDocker({ id: `${id}-postgresql`, engine }); + } + } catch (error) { + console.error(error); + } + } + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/index.svelte b/src/routes/services/index.svelte index 587037c86..bab3a4322 100644 --- a/src/routes/services/index.svelte +++ b/src/routes/services/index.svelte @@ -17,6 +17,7 @@ import { getDomain } from '$lib/components/common'; import Umami from '$lib/components/svg/services/Umami.svelte'; import Hasura from '$lib/components/svg/services/Hasura.svelte'; + import Fider from '$lib/components/svg/services/Fider.svelte'; export let services; async function newService() { @@ -92,6 +93,8 @@ {:else if service.type === 'hasura'} + {:else if service.type === 'fider'} + {/if}
{service.name} @@ -143,6 +146,8 @@ {:else if service.type === 'hasura'} + {:else if service.type === 'fider'} + {/if}
{service.name}