feat: WP could have custom db

This commit is contained in:
Andras Bacsai
2022-05-10 10:12:13 +02:00
parent ede37d296b
commit ce52608f19
19 changed files with 480 additions and 258 deletions

View File

@@ -0,0 +1,32 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Wordpress" (
"id" TEXT NOT NULL PRIMARY KEY,
"extraConfig" TEXT,
"tablePrefix" TEXT,
"ownMysql" BOOLEAN NOT NULL DEFAULT false,
"mysqlHost" TEXT,
"mysqlPort" INTEGER,
"mysqlUser" TEXT NOT NULL,
"mysqlPassword" TEXT NOT NULL,
"mysqlRootUser" TEXT NOT NULL,
"mysqlRootUserPassword" TEXT NOT NULL,
"mysqlDatabase" TEXT,
"mysqlPublicPort" INTEGER,
"ftpEnabled" BOOLEAN NOT NULL DEFAULT false,
"ftpUser" TEXT,
"ftpPassword" TEXT,
"ftpPublicPort" INTEGER,
"ftpHostKey" TEXT,
"ftpHostKeyPrivate" TEXT,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Wordpress_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Wordpress" ("createdAt", "extraConfig", "ftpEnabled", "ftpHostKey", "ftpHostKeyPrivate", "ftpPassword", "ftpPublicPort", "ftpUser", "id", "mysqlDatabase", "mysqlPassword", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "serviceId", "tablePrefix", "updatedAt") SELECT "createdAt", "extraConfig", "ftpEnabled", "ftpHostKey", "ftpHostKeyPrivate", "ftpPassword", "ftpPublicPort", "ftpUser", "id", "mysqlDatabase", "mysqlPassword", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "serviceId", "tablePrefix", "updatedAt" FROM "Wordpress";
DROP TABLE "Wordpress";
ALTER TABLE "new_Wordpress" RENAME TO "Wordpress";
CREATE UNIQUE INDEX "Wordpress_serviceId_key" ON "Wordpress"("serviceId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -353,6 +353,9 @@ model Wordpress {
id String @id @default(cuid()) id String @id @default(cuid())
extraConfig String? extraConfig String?
tablePrefix String? tablePrefix String?
ownMysql Boolean @default(false)
mysqlHost String?
mysqlPort Int?
mysqlUser String mysqlUser String
mysqlPassword String mysqlPassword String
mysqlRootUser String mysqlRootUser String

View File

@@ -4,6 +4,6 @@
<img <img
alt="plausible logo" alt="plausible logo"
class={isAbsolute ? 'w-10 absolute top-0 left-0 -m-5' : 'w-6 mx-auto'} class={isAbsolute ? 'w-9 absolute top-0 left-0 -m-4' : 'w-6 mx-auto'}
src="/plausible.png" src="/plausible.png"
/> />

View File

@@ -28,7 +28,7 @@ if (!dev) {
} }
export const prisma = new PrismaClient({ export const prisma = new PrismaClient({
errorFormat: 'pretty', errorFormat: 'minimal',
rejectOnNotFound: false rejectOnNotFound: false
}); });

View File

@@ -419,7 +419,9 @@ export async function updateWordpress({
name, name,
exposePort, exposePort,
mysqlDatabase, mysqlDatabase,
extraConfig extraConfig,
mysqlHost,
mysqlPort
}: { }: {
id: string; id: string;
fqdn: string; fqdn: string;
@@ -427,10 +429,24 @@ export async function updateWordpress({
exposePort?: number; exposePort?: number;
mysqlDatabase: string; mysqlDatabase: string;
extraConfig: string; extraConfig: string;
mysqlHost?: string;
mysqlPort?: number;
}): Promise<Service> { }): Promise<Service> {
return await prisma.service.update({ return await prisma.service.update({
where: { id }, where: { id },
data: { fqdn, name, exposePort, wordpress: { update: { mysqlDatabase, extraConfig } } } data: {
fqdn,
name,
exposePort,
wordpress: {
update: {
mysqlDatabase,
extraConfig,
mysqlHost,
mysqlPort
}
}
}
}); });
} }

View File

@@ -60,7 +60,7 @@
</div> </div>
{/if} {/if}
</div> </div>
<div class="flex flex-col flex-wrap justify-center"> <div class="flex justify-center">
{#if !applications || ownApplications.length === 0} {#if !applications || ownApplications.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div> <div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div>

View File

@@ -47,7 +47,7 @@
</div> </div>
</div> </div>
<div class="flex flex-col flex-wrap justify-center"> <div class="flex justify-center">
{#if !databases || ownDatabases.length === 0} {#if !databases || ownDatabases.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('database.no_databases_found')}</div> <div class="text-center text-xl font-bold">{$t('database.no_databases_found')}</div>

View File

@@ -39,10 +39,13 @@
import { t } from '$lib/translations'; import { t } from '$lib/translations';
</script> </script>
<div class="flex space-x-1 p-6 text-2xl font-bold"> <div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
<div class="tracking-tight">{$t('application.destination')}</div> <div class="-mb-5 flex-col">
<span class="arrow-right-applications px-1">></span> <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
<span class="pr-2">{destination.name}</span> Configuration
</div>
<span class="text-xs">{destination.name}</span>
</div>
</div> </div>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-4xl px-6">

View File

@@ -141,21 +141,21 @@
<div class="title font-bold">Server Usage</div> <div class="title font-bold">Server Usage</div>
<dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3"> <dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
<Loading /> <Loading />
<div class="overflow-hidden rounded-lg px-4 py-5 sm:p-6"> <div class="overflow-hidden rounded px-4 py-5 sm:p-6">
<dt class="truncate text-sm font-medium text-white">Total Memory</dt> <dt class="truncate text-sm font-medium text-white">Total Memory</dt>
<dd class="mt-1 text-3xl font-semibold text-white"> <dd class="mt-1 text-3xl font-semibold text-white">
{(usage?.memory.totalMemMb).toFixed(0)} {(usage?.memory.totalMemMb).toFixed(0)}
</dd> </dd>
</div> </div>
<div class="overflow-hidden rounded-lg px-4 py-5 sm:p-6"> <div class="overflow-hidden rounded px-4 py-5 sm:p-6">
<dt class="truncate text-sm font-medium text-white">Used Memory</dt> <dt class="truncate text-sm font-medium text-white">Used Memory</dt>
<dd class="mt-1 text-3xl font-semibold text-white "> <dd class="mt-1 text-3xl font-semibold text-white ">
{(usage?.memory.usedMemMb).toFixed(0)} {(usage?.memory.usedMemMb).toFixed(0)}
</dd> </dd>
</div> </div>
<div class="overflow-hidden rounded-lg px-4 py-5 sm:p-6" class:bg-red-500={memoryWarning}> <div class="overflow-hidden rounded px-4 py-5 sm:p-6" class:bg-red-500={memoryWarning}>
<dt class="truncate text-sm font-medium text-white">Free Memory</dt> <dt class="truncate text-sm font-medium text-white">Free Memory</dt>
<dd class="mt-1 flex items-center text-3xl font-semibold text-white"> <dd class="mt-1 flex items-center text-3xl font-semibold text-white">
{usage?.memory.freeMemPercentage}% {usage?.memory.freeMemPercentage}%
@@ -166,19 +166,19 @@
</div> </div>
</dl> </dl>
<dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3"> <dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
<div class="overflow-hidden rounded-lg px-4 py-5 sm:p-6"> <div class="overflow-hidden rounded px-4 py-5 sm:p-6">
<dt class="truncate text-sm font-medium text-white">Total CPUs</dt> <dt class="truncate text-sm font-medium text-white">Total CPUs</dt>
<dd class="mt-1 text-3xl font-semibold text-white"> <dd class="mt-1 text-3xl font-semibold text-white">
{usage?.cpu.count} {usage?.cpu.count}
</dd> </dd>
</div> </div>
<div class="overflow-hidden rounded-lg px-4 py-5 sm:p-6"> <div class="overflow-hidden rounded px-4 py-5 sm:p-6">
<dt class="truncate text-sm font-medium text-white">Load Average (5/10/30mins)</dt> <dt class="truncate text-sm font-medium text-white">Load Average (5/10/30mins)</dt>
<dd class="mt-1 text-3xl font-semibold text-white"> <dd class="mt-1 text-3xl font-semibold text-white">
{usage?.cpu.load.join('/')} {usage?.cpu.load.join('/')}
</dd> </dd>
</div> </div>
<div class="overflow-hidden rounded-lg px-4 py-5 sm:p-6" class:bg-red-500={cpuWarning}> <div class="overflow-hidden rounded px-4 py-5 sm:p-6" class:bg-red-500={cpuWarning}>
<dt class="truncate text-sm font-medium text-white">CPU Usage</dt> <dt class="truncate text-sm font-medium text-white">CPU Usage</dt>
<dd class="mt-1 flex items-center text-3xl font-semibold text-white"> <dd class="mt-1 flex items-center text-3xl font-semibold text-white">
{usage?.cpu.usage}% {usage?.cpu.usage}%
@@ -189,19 +189,19 @@
</div> </div>
</dl> </dl>
<dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3"> <dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
<div class="overflow-hidden rounded-lg px-4 py-5 sm:p-6"> <div class="overflow-hidden rounded px-4 py-5 sm:p-6">
<dt class="truncate text-sm font-medium text-white">Total Disk</dt> <dt class="truncate text-sm font-medium text-white">Total Disk</dt>
<dd class="mt-1 text-3xl font-semibold text-white"> <dd class="mt-1 text-3xl font-semibold text-white">
{usage?.disk.totalGb}GB {usage?.disk.totalGb}GB
</dd> </dd>
</div> </div>
<div class="overflow-hidden rounded-lg px-4 py-5 sm:p-6"> <div class="overflow-hidden rounded px-4 py-5 sm:p-6">
<dt class="truncate text-sm font-medium text-white">Used Disk</dt> <dt class="truncate text-sm font-medium text-white">Used Disk</dt>
<dd class="mt-1 text-3xl font-semibold text-white"> <dd class="mt-1 text-3xl font-semibold text-white">
{usage?.disk.usedGb}GB {usage?.disk.usedGb}GB
</dd> </dd>
</div> </div>
<div class="overflow-hidden rounded-lg px-4 py-5 sm:p-6" class:bg-red-500={diskWarning}> <div class="overflow-hidden rounded px-4 py-5 sm:p-6" class:bg-red-500={diskWarning}>
<dt class="truncate text-sm font-medium text-white">Free Disk</dt> <dt class="truncate text-sm font-medium text-white">Free Disk</dt>
<dd class="mt-1 flex items-center text-3xl font-semibold text-white"> <dd class="mt-1 flex items-center text-3xl font-semibold text-white">
{usage?.disk.freePercentage}% {usage?.disk.freePercentage}%
@@ -217,7 +217,7 @@
<a <a
href="/applications" href="/applications"
sveltekit:prefetch sveltekit:prefetch
class="overflow-hidden rounded-lg px-4 py-5 text-green-500 no-underline transition-all duration-100 hover:bg-green-500 hover:text-white sm:p-6" class="overflow-hidden rounded px-4 py-5 text-green-500 no-underline transition-all duration-100 hover:bg-green-500 hover:text-white sm:p-6"
> >
<dt class="truncate text-sm font-medium text-white">{$t('index.applications')}</dt> <dt class="truncate text-sm font-medium text-white">{$t('index.applications')}</dt>
<dd class="mt-1 text-3xl font-semibold "> <dd class="mt-1 text-3xl font-semibold ">
@@ -227,7 +227,7 @@
<a <a
href="/destinations" href="/destinations"
sveltekit:prefetch sveltekit:prefetch
class="overflow-hidden rounded-lg px-4 py-5 text-sky-500 no-underline transition-all duration-100 hover:bg-sky-500 hover:text-white sm:p-6" class="overflow-hidden rounded px-4 py-5 text-sky-500 no-underline transition-all duration-100 hover:bg-sky-500 hover:text-white sm:p-6"
> >
<dt class="truncate text-sm font-medium text-white">{$t('index.destinations')}</dt> <dt class="truncate text-sm font-medium text-white">{$t('index.destinations')}</dt>
<dd class="mt-1 text-3xl font-semibold "> <dd class="mt-1 text-3xl font-semibold ">
@@ -238,7 +238,7 @@
<a <a
href="/sources" href="/sources"
sveltekit:prefetch sveltekit:prefetch
class="overflow-hidden rounded-lg px-4 py-5 text-orange-500 no-underline transition-all duration-100 hover:bg-orange-500 hover:text-white sm:p-6" class="overflow-hidden rounded px-4 py-5 text-orange-500 no-underline transition-all duration-100 hover:bg-orange-500 hover:text-white sm:p-6"
> >
<dt class="truncate text-sm font-medium text-white">{$t('index.git_sources')}</dt> <dt class="truncate text-sm font-medium text-white">{$t('index.git_sources')}</dt>
<dd class="mt-1 text-3xl font-semibold"> <dd class="mt-1 text-3xl font-semibold">
@@ -250,7 +250,7 @@
<a <a
href="/databases" href="/databases"
sveltekit:prefetch sveltekit:prefetch
class="overflow-hidden rounded-lg px-4 py-5 text-purple-500 no-underline transition-all duration-100 hover:bg-purple-500 hover:text-white sm:p-6" class="overflow-hidden rounded px-4 py-5 text-purple-500 no-underline transition-all duration-100 hover:bg-purple-500 hover:text-white sm:p-6"
> >
<dt class="truncate text-sm font-medium text-white">{$t('index.databases')}</dt> <dt class="truncate text-sm font-medium text-white">{$t('index.databases')}</dt>
<dd class="mt-1 text-3xl font-semibold "> <dd class="mt-1 text-3xl font-semibold ">
@@ -261,7 +261,7 @@
<a <a
href="/services" href="/services"
sveltekit:prefetch sveltekit:prefetch
class="overflow-hidden rounded-lg px-4 py-5 text-pink-500 no-underline transition-all duration-100 hover:bg-pink-500 hover:text-white sm:p-6" class="overflow-hidden rounded px-4 py-5 text-pink-500 no-underline transition-all duration-100 hover:bg-pink-500 hover:text-white sm:p-6"
> >
<dt class="truncate text-sm font-medium text-white">{$t('index.services')}</dt> <dt class="truncate text-sm font-medium text-white">{$t('index.services')}</dt>
<dd class="mt-1 text-3xl font-semibold "> <dd class="mt-1 text-3xl font-semibold ">
@@ -272,7 +272,7 @@
<a <a
href="/iam" href="/iam"
sveltekit:prefetch sveltekit:prefetch
class="overflow-hidden rounded-lg px-4 py-5 text-cyan-500 no-underline transition-all duration-100 hover:bg-cyan-500 hover:text-white sm:p-6" class="overflow-hidden rounded px-4 py-5 text-cyan-500 no-underline transition-all duration-100 hover:bg-cyan-500 hover:text-white sm:p-6"
> >
<dt class="truncate text-sm font-medium text-white">{$t('index.teams')}</dt> <dt class="truncate text-sm font-medium text-white">{$t('index.teams')}</dt>
<dd class="mt-1 text-3xl font-semibold "> <dd class="mt-1 text-3xl font-semibold ">

View File

@@ -96,16 +96,3 @@
disabled disabled
/> />
</div> </div>
<!-- <div class="grid grid-cols-3 items-center">
<label for="postgresqlPublicPort">Public Port</label>
<div class="col-span-2 ">
<CopyPasswordField
placeholder="{ $t('forms.generated_automatically_after_start') }"
readonly
disabled
id="postgresqlPublicPort"
name="postgresqlPublicPort"
value={service.plausibleAnalytics.postgresqlPublicPort}
/>
</div>
</div> -->

View File

@@ -18,6 +18,7 @@
let ftpUser = service.wordpress.ftpUser; let ftpUser = service.wordpress.ftpUser;
let ftpPassword = service.wordpress.ftpPassword; let ftpPassword = service.wordpress.ftpPassword;
let ftpLoading = false; let ftpLoading = false;
let ownMysql = service.wordpress.ownMysql;
function generateUrl(publicPort) { function generateUrl(publicPort) {
return browser return browser
@@ -40,7 +41,7 @@
publicPort, publicPort,
ftpUser: user, ftpUser: user,
ftpPassword: password ftpPassword: password
} = await post(`/services/${id}/wordpress/settings.json`, { } = await post(`/services/${id}/wordpress/ftp.json`, {
ftpEnabled ftpEnabled
}); });
ftpUrl = generateUrl(publicPort); ftpUrl = generateUrl(publicPort);
@@ -52,6 +53,18 @@
} finally { } finally {
ftpLoading = false; ftpLoading = false;
} }
} else {
try {
if (name === 'ownMysql') {
ownMysql = !ownMysql;
}
await post(`/services/${id}/wordpress/settings.json`, {
ownMysql
});
service.wordpress.ownMysql = ownMysql;
} catch ({ error }) {
return errorNotification(error);
}
} }
} }
</script> </script>
@@ -106,51 +119,95 @@ define('SUBDOMAIN_INSTALL', false);`
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MySQL</div> <div class="title">MySQL</div>
</div> </div>
<div class="grid grid-cols-2 items-center px-10">
<Setting
dataTooltip={$t('forms.must_be_stopped_to_modify')}
bind:setting={service.wordpress.ownMysql}
disabled={isRunning}
on:click={() => !isRunning && changeSettings('ownMysql')}
title="Use your own MySQL server"
description="Enables the use of your own MySQL server. If you don't have one, you can use the one provided by Coolify."
/>
</div>
{#if service.wordpress.ownMysql}
<div class="grid grid-cols-2 items-center px-10">
<label for="mysqlHost">Host</label>
<input
name="mysqlHost"
id="mysqlHost"
required
readonly={isRunning}
disabled={isRunning}
bind:value={service.wordpress.mysqlHost}
placeholder="{$t('forms.eg')}: db.coolify.io"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mysqlPort">Port</label>
<input
name="mysqlPort"
id="mysqlPort"
required
readonly={isRunning}
disabled={isRunning}
bind:value={service.wordpress.mysqlPort}
placeholder="{$t('forms.eg')}: 3306"
/>
</div>
{/if}
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="mysqlDatabase">{$t('index.database')}</label> <label for="mysqlDatabase">{$t('index.database')}</label>
<input <input
name="mysqlDatabase" name="mysqlDatabase"
id="mysqlDatabase" id="mysqlDatabase"
required required
readonly={readOnly} readonly={readOnly && !service.wordpress.ownMysql}
disabled={readOnly} disabled={readOnly && !service.wordpress.ownMysql}
bind:value={service.wordpress.mysqlDatabase} bind:value={service.wordpress.mysqlDatabase}
placeholder="{$t('forms.eg')}: wordpress_db" placeholder="{$t('forms.eg')}: wordpress_db"
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> {#if !service.wordpress.ownMysql}
<label for="mysqlRootUser">{$t('forms.root_user')}</label> <div class="grid grid-cols-2 items-center px-10">
<input <label for="mysqlRootUser">{$t('forms.root_user')}</label>
name="mysqlRootUser" <input
id="mysqlRootUser" name="mysqlRootUser"
placeholder="MySQL {$t('forms.root_user')}" id="mysqlRootUser"
value={service.wordpress.mysqlRootUser} placeholder="MySQL {$t('forms.root_user')}"
disabled value={service.wordpress.mysqlRootUser}
readonly readonly={isRunning || !service.wordpress.ownMysq}
/> disabled={isRunning || !service.wordpress.ownMysq}
</div> />
<div class="grid grid-cols-2 items-center px-10"> </div>
<label for="mysqlRootUserPassword">{$t('forms.roots_password')}</label> <div class="grid grid-cols-2 items-center px-10">
<CopyPasswordField <label for="mysqlRootUserPassword">{$t('forms.roots_password')}</label>
id="mysqlRootUserPassword" <CopyPasswordField
isPasswordField id="mysqlRootUserPassword"
readonly isPasswordField
disabled readonly={isRunning || !service.wordpress.ownMysq}
name="mysqlRootUserPassword" disabled={isRunning || !service.wordpress.ownMysq}
value={service.wordpress.mysqlRootUserPassword} name="mysqlRootUserPassword"
/> value={service.wordpress.mysqlRootUserPassword}
</div> />
</div>
{/if}
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="mysqlUser">{$t('forms.user')}</label> <label for="mysqlUser">{$t('forms.user')}</label>
<input name="mysqlUser" id="mysqlUser" value={service.wordpress.mysqlUser} disabled readonly /> <input
name="mysqlUser"
id="mysqlUser"
value={service.wordpress.mysqlUser}
readonly={isRunning || !service.wordpress.ownMysql}
disabled={isRunning || !service.wordpress.ownMysql}
/>
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="mysqlPassword">{$t('forms.password')}</label> <label for="mysqlPassword">{$t('forms.password')}</label>
<CopyPasswordField <CopyPasswordField
id="mysqlPassword" id="mysqlPassword"
isPasswordField isPasswordField
readonly readonly={isRunning || !service.wordpress.ownMysql}
disabled disabled={isRunning || !service.wordpress.ownMysql}
name="mysqlPassword" name="mysqlPassword"
value={service.wordpress.mysqlPassword} value={service.wordpress.mysqlPassword}
/> />

View File

@@ -0,0 +1,185 @@
import { dev } from '$app/env';
import { asyncExecShell, getEngine, getUserDetails } from '$lib/common';
import { decrypt, encrypt } from '$lib/crypto';
import * as db from '$lib/database';
import { ErrorHandler, generatePassword, getFreePort } from '$lib/database';
import { checkContainer, startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy';
import type { ComposeFile } from '$lib/types/composeFile';
import type { RequestHandler } from '@sveltejs/kit';
import cuid from 'cuid';
import fs from 'fs/promises';
import yaml from 'js-yaml';
export const post: RequestHandler = async (event) => {
const { status, body, teamId } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const { ftpEnabled } = await event.request.json();
const publicPort = await getFreePort();
let ftpUser = cuid();
let ftpPassword = generatePassword();
const hostkeyDir = dev ? '/tmp/hostkeys' : '/app/ssl/hostkeys';
try {
const data = await db.prisma.wordpress.update({
where: { serviceId: id },
data: { ftpEnabled },
include: { service: { include: { destinationDocker: true } } }
});
const {
service: { destinationDockerId, destinationDocker },
ftpPublicPort: oldPublicPort,
ftpUser: user,
ftpPassword: savedPassword,
ftpHostKey,
ftpHostKeyPrivate
} = data;
if (user) ftpUser = user;
if (savedPassword) ftpPassword = decrypt(savedPassword);
const { stdout: password } = await asyncExecShell(
`echo ${ftpPassword} | openssl passwd -1 -stdin`
);
if (destinationDockerId) {
try {
await fs.stat(hostkeyDir);
} catch (error) {
await asyncExecShell(`mkdir -p ${hostkeyDir}`);
}
if (!ftpHostKey) {
await asyncExecShell(
`ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N "" -q -f ${hostkeyDir}/${id}.ed25519`
);
const { stdout: ftpHostKey } = await asyncExecShell(`cat ${hostkeyDir}/${id}.ed25519`);
await db.prisma.wordpress.update({
where: { serviceId: id },
data: { ftpHostKey: encrypt(ftpHostKey) }
});
} else {
await asyncExecShell(`echo "${decrypt(ftpHostKey)}" > ${hostkeyDir}/${id}.ed25519`);
}
if (!ftpHostKeyPrivate) {
await asyncExecShell(`ssh-keygen -t rsa -b 4096 -N "" -f ${hostkeyDir}/${id}.rsa`);
const { stdout: ftpHostKeyPrivate } = await asyncExecShell(`cat ${hostkeyDir}/${id}.rsa`);
await db.prisma.wordpress.update({
where: { serviceId: id },
data: { ftpHostKeyPrivate: encrypt(ftpHostKeyPrivate) }
});
} else {
await asyncExecShell(`echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`);
}
const { network, engine } = destinationDocker;
const host = getEngine(engine);
if (ftpEnabled) {
await db.prisma.wordpress.update({
where: { serviceId: id },
data: {
ftpPublicPort: publicPort,
ftpUser: user ? undefined : ftpUser,
ftpPassword: savedPassword ? undefined : encrypt(ftpPassword)
}
});
try {
const isRunning = await checkContainer(engine, `${id}-ftp`);
if (isRunning) {
await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
);
}
} catch (error) {
console.log(error);
//
}
const volumes = [
`${id}-wordpress-data:/home/${ftpUser}`,
`${
dev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
}/${id}.ed25519:/etc/ssh/ssh_host_ed25519_key`,
`${
dev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
}/${id}.rsa:/etc/ssh/ssh_host_rsa_key`,
`${
dev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
}/${id}.sh:/etc/sftp.d/chmod.sh`
];
const compose: ComposeFile = {
version: '3.8',
services: {
[`${id}-ftp`]: {
image: `atmoz/sftp:alpine`,
command: `'${ftpUser}:${password.replace('\n', '').replace(/\$/g, '$$$')}:e:33'`,
extra_hosts: ['host.docker.internal:host-gateway'],
container_name: `${id}-ftp`,
volumes,
networks: [network],
depends_on: [],
restart: 'always'
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[`${id}-wordpress-data`]: {
external: true,
name: `${id}-wordpress-data`
}
}
};
await fs.writeFile(
`${hostkeyDir}/${id}.sh`,
`#!/bin/bash\nchmod 600 /etc/ssh/ssh_host_ed25519_key /etc/ssh/ssh_host_rsa_key`
);
await asyncExecShell(`chmod +x ${hostkeyDir}/${id}.sh`);
await fs.writeFile(`${hostkeyDir}/${id}-docker-compose.yml`, yaml.dump(compose));
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d`
);
await startTcpProxy(destinationDocker, `${id}-ftp`, publicPort, 22);
} else {
await db.prisma.wordpress.update({
where: { serviceId: id },
data: { ftpPublicPort: null }
});
try {
await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
);
} catch (error) {
//
}
await stopTcpHttpProxy(destinationDocker, oldPublicPort);
}
}
if (ftpEnabled) {
return {
status: 201,
body: {
publicPort,
ftpUser,
ftpPassword
}
};
} else {
return {
status: 200,
body: {}
};
}
} catch (error) {
console.log(error);
return ErrorHandler(error);
} finally {
await asyncExecShell(
`rm -f ${hostkeyDir}/${id}-docker-compose.yml ${hostkeyDir}/${id}.ed25519 ${hostkeyDir}/${id}.ed25519.pub ${hostkeyDir}/${id}.rsa ${hostkeyDir}/${id}.rsa.pub ${hostkeyDir}/${id}.sh`
);
}
};

View File

@@ -12,13 +12,24 @@ export const post: RequestHandler = async (event) => {
name, name,
fqdn, fqdn,
exposePort, exposePort,
wordpress: { extraConfig, mysqlDatabase } wordpress: { extraConfig, mysqlDatabase, mysqlHost, mysqlPort }
} = await event.request.json(); } = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
if (exposePort) exposePort = Number(exposePort); if (exposePort) exposePort = Number(exposePort);
if (mysqlPort) mysqlPort = Number(mysqlPort);
try { try {
await db.updateWordpress({ id, fqdn, name, extraConfig, mysqlDatabase, exposePort }); await db.updateWordpress({
id,
fqdn,
name,
extraConfig,
mysqlDatabase,
exposePort,
mysqlHost,
mysqlPort
});
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@@ -16,170 +16,17 @@ export const post: RequestHandler = async (event) => {
const { id } = event.params; const { id } = event.params;
const { ftpEnabled } = await event.request.json(); const { ownMysql } = await event.request.json();
const publicPort = await getFreePort();
let ftpUser = cuid();
let ftpPassword = generatePassword();
const hostkeyDir = dev ? '/tmp/hostkeys' : '/app/ssl/hostkeys';
try { try {
const data = await db.prisma.wordpress.update({ await db.prisma.wordpress.update({
where: { serviceId: id }, where: { serviceId: id },
data: { ftpEnabled }, data: { ownMysql }
include: { service: { include: { destinationDocker: true } } }
}); });
const { return {
service: { destinationDockerId, destinationDocker }, status: 201
ftpPublicPort: oldPublicPort, };
ftpUser: user,
ftpPassword: savedPassword,
ftpHostKey,
ftpHostKeyPrivate
} = data;
if (user) ftpUser = user;
if (savedPassword) ftpPassword = decrypt(savedPassword);
const { stdout: password } = await asyncExecShell(
`echo ${ftpPassword} | openssl passwd -1 -stdin`
);
if (destinationDockerId) {
try {
await fs.stat(hostkeyDir);
} catch (error) {
await asyncExecShell(`mkdir -p ${hostkeyDir}`);
}
if (!ftpHostKey) {
await asyncExecShell(
`ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N "" -q -f ${hostkeyDir}/${id}.ed25519`
);
const { stdout: ftpHostKey } = await asyncExecShell(`cat ${hostkeyDir}/${id}.ed25519`);
await db.prisma.wordpress.update({
where: { serviceId: id },
data: { ftpHostKey: encrypt(ftpHostKey) }
});
} else {
await asyncExecShell(`echo "${decrypt(ftpHostKey)}" > ${hostkeyDir}/${id}.ed25519`);
}
if (!ftpHostKeyPrivate) {
await asyncExecShell(`ssh-keygen -t rsa -b 4096 -N "" -f ${hostkeyDir}/${id}.rsa`);
const { stdout: ftpHostKeyPrivate } = await asyncExecShell(`cat ${hostkeyDir}/${id}.rsa`);
await db.prisma.wordpress.update({
where: { serviceId: id },
data: { ftpHostKeyPrivate: encrypt(ftpHostKeyPrivate) }
});
} else {
await asyncExecShell(`echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`);
}
const { network, engine } = destinationDocker;
const host = getEngine(engine);
if (ftpEnabled) {
await db.prisma.wordpress.update({
where: { serviceId: id },
data: {
ftpPublicPort: publicPort,
ftpUser: user ? undefined : ftpUser,
ftpPassword: savedPassword ? undefined : encrypt(ftpPassword)
}
});
try {
const isRunning = await checkContainer(engine, `${id}-ftp`);
if (isRunning) {
await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
);
}
} catch (error) {
console.log(error);
//
}
const volumes = [
`${id}-wordpress-data:/home/${ftpUser}`,
`${
dev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
}/${id}.ed25519:/etc/ssh/ssh_host_ed25519_key`,
`${
dev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
}/${id}.rsa:/etc/ssh/ssh_host_rsa_key`,
`${
dev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
}/${id}.sh:/etc/sftp.d/chmod.sh`
];
const compose: ComposeFile = {
version: '3.8',
services: {
[`${id}-ftp`]: {
image: `atmoz/sftp:alpine`,
command: `'${ftpUser}:${password.replace('\n', '').replace(/\$/g, '$$$')}:e:33'`,
extra_hosts: ['host.docker.internal:host-gateway'],
container_name: `${id}-ftp`,
volumes,
networks: [network],
depends_on: [],
restart: 'always'
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[`${id}-wordpress-data`]: {
external: true,
name: `${id}-wordpress-data`
}
}
};
await fs.writeFile(
`${hostkeyDir}/${id}.sh`,
`#!/bin/bash\nchmod 600 /etc/ssh/ssh_host_ed25519_key /etc/ssh/ssh_host_rsa_key`
);
await asyncExecShell(`chmod +x ${hostkeyDir}/${id}.sh`);
await fs.writeFile(`${hostkeyDir}/${id}-docker-compose.yml`, yaml.dump(compose));
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d`
);
await startTcpProxy(destinationDocker, `${id}-ftp`, publicPort, 22);
} else {
await db.prisma.wordpress.update({
where: { serviceId: id },
data: { ftpPublicPort: null }
});
try {
await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
);
} catch (error) {
//
}
await stopTcpHttpProxy(destinationDocker, oldPublicPort);
}
}
if (ftpEnabled) {
return {
status: 201,
body: {
publicPort,
ftpUser,
ftpPassword
}
};
} else {
return {
status: 200,
body: {}
};
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return ErrorHandler(error); return ErrorHandler(error);
} finally {
await asyncExecShell(
`rm -f ${hostkeyDir}/${id}-docker-compose.yml ${hostkeyDir}/${id}.ed25519 ${hostkeyDir}/${id}.ed25519.pub ${hostkeyDir}/${id}.rsa ${hostkeyDir}/${id}.rsa.pub ${hostkeyDir}/${id}.sh`
);
} }
}; };

View File

@@ -26,11 +26,14 @@ export const post: RequestHandler = async (event) => {
exposePort, exposePort,
wordpress: { wordpress: {
mysqlDatabase, mysqlDatabase,
mysqlHost,
mysqlPort,
mysqlUser, mysqlUser,
mysqlPassword, mysqlPassword,
extraConfig, extraConfig,
mysqlRootUser, mysqlRootUser,
mysqlRootUserPassword mysqlRootUserPassword,
ownMysql
} }
} = service; } = service;
@@ -45,7 +48,7 @@ export const post: RequestHandler = async (event) => {
image: `${image}:${version}`, image: `${image}:${version}`,
volume: `${id}-wordpress-data:/var/www/html`, volume: `${id}-wordpress-data:/var/www/html`,
environmentVariables: { environmentVariables: {
WORDPRESS_DB_HOST: `${id}-mysql`, WORDPRESS_DB_HOST: ownMysql ? `${mysqlHost}:${mysqlPort}` : `${id}-mysql`,
WORDPRESS_DB_USER: mysqlUser, WORDPRESS_DB_USER: mysqlUser,
WORDPRESS_DB_PASSWORD: mysqlPassword, WORDPRESS_DB_PASSWORD: mysqlPassword,
WORDPRESS_DB_NAME: mysqlDatabase, WORDPRESS_DB_NAME: mysqlDatabase,
@@ -69,7 +72,7 @@ export const post: RequestHandler = async (event) => {
config.wordpress.environmentVariables[secret.name] = secret.value; config.wordpress.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile: ComposeFile = { let composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {
@@ -80,7 +83,6 @@ export const post: RequestHandler = async (event) => {
networks: [network], networks: [network],
restart: 'always', restart: 'always',
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
depends_on: [`${id}-mysql`],
labels: makeLabelForServices('wordpress'), labels: makeLabelForServices('wordpress'),
deploy: { deploy: {
restart_policy: { restart_policy: {
@@ -90,22 +92,6 @@ export const post: RequestHandler = async (event) => {
window: '120s' window: '120s'
} }
} }
},
[`${id}-mysql`]: {
container_name: `${id}-mysql`,
image: config.mysql.image,
volumes: [config.mysql.volume],
environment: config.mysql.environmentVariables,
networks: [network],
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {
@@ -116,12 +102,32 @@ export const post: RequestHandler = async (event) => {
volumes: { volumes: {
[config.wordpress.volume.split(':')[0]]: { [config.wordpress.volume.split(':')[0]]: {
name: config.wordpress.volume.split(':')[0] name: config.wordpress.volume.split(':')[0]
},
[config.mysql.volume.split(':')[0]]: {
name: config.mysql.volume.split(':')[0]
} }
} }
}; };
if (!ownMysql) {
composeFile.services[id].depends_on = [`${id}-mysql`];
composeFile.services[`${id}-mysql`] = {
container_name: `${id}-mysql`,
image: config.mysql.image,
volumes: [config.mysql.volume],
environment: config.mysql.environmentVariables,
networks: [network],
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
};
composeFile.volumes[config.mysql.volume.split(':')[0]] = {
name: config.mysql.volume.split(':')[0]
};
}
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {

View File

@@ -55,7 +55,7 @@
</div> </div>
</div> </div>
<div class="flex flex-col flex-wrap justify-center"> <div class="flex justify-center">
{#if !services || ownServices.length === 0} {#if !services || ownServices.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('service.no_service')}</div> <div class="text-center text-xl font-bold">{$t('service.no_service')}</div>

View File

@@ -68,10 +68,48 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 px-6 text-2xl font-bold"> <div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
<div class="tracking-tight">{$t('application.git_source')}</div> <div class="-mb-5 flex-col">
<span class="arrow-right-applications px-1 text-orange-500">></span> <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
<span class="pr-2">{source.name}</span> Configuration
</div>
<span class="text-xs">{source.name}</span>
</div>
{#if source?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="w-8">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if source?.type === 'github'}
<svg viewBox="0 0 128 128" class="w-8">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</div> </div>
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">

View File

@@ -62,7 +62,7 @@
</button> </button>
{/if} {/if}
</div> </div>
<div class="flex flex-col flex-wrap justify-center"> <div class="flex justify-center">
{#if !sources || ownSources.length === 0} {#if !sources || ownSources.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('source.no_git_sources_found')}</div> <div class="text-center text-xl font-bold">{$t('source.no_git_sources_found')}</div>
@@ -74,11 +74,48 @@
{#each ownSources as source} {#each ownSources as source}
<a href="/sources/{source.id}" class="w-96 p-2 no-underline"> <a href="/sources/{source.id}" class="w-96 p-2 no-underline">
<div <div
class="box-selection group hover:bg-orange-600" class="box-selection group relative hover:bg-orange-600"
class:border-red-500={source.gitlabApp && !source.gitlabAppId} class:border-red-500={source.gitlabApp && !source.gitlabAppId}
class:border-0={source.gitlabApp && !source.gitlabAppId} class:border-0={source.gitlabApp && !source.gitlabAppId}
class:border-l-4={source.gitlabApp && !source.gitlabAppId} class:border-l-4={source.gitlabApp && !source.gitlabAppId}
> >
<div class="absolute top-0 left-0 -m-5 h-10 w-10">
{#if source?.type === 'gitlab'}
<svg viewBox="0 0 128 128">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if source?.type === 'github'}
<svg viewBox="0 0 128 128">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</div>
<div class="truncate text-center text-xl font-bold">{source.name}</div> <div class="truncate text-center text-xl font-bold">{source.name}</div>
{#if $session.teamId === '0' && otherSources.length > 0} {#if $session.teamId === '0' && otherSources.length > 0}
<div class="truncate text-center">{source.teams[0].name}</div> <div class="truncate text-center">{source.teams[0].name}</div>

View File

@@ -356,7 +356,7 @@ a {
} }
.box-selection { .box-selection {
@apply min-w-[16rem] max-w-[24rem] justify-center rounded border-transparent bg-coolgray-200 p-6 shadow-lg transition duration-150 hover:scale-105 hover:border-transparent hover:bg-coolgray-400; @apply min-w-[16rem] max-w-[24rem] justify-center rounded border-transparent bg-coolgray-200 p-6 hover:border-transparent hover:bg-coolgray-400;
} }
._toastBar { ._toastBar {