Merge branch 'main' into fix/github-token

This commit is contained in:
Andras Bacsai
2022-02-21 09:51:46 +01:00
committed by GitHub
90 changed files with 1600 additions and 1115 deletions

View File

@@ -1,74 +1,26 @@
/// <reference types="@sveltejs/kit" />
interface Cookies {
teamId?: string;
gitlabToken?: string;
'kit.session'?: string;
}
interface Locals {
gitlabToken?: string;
user: {
teamId: string;
permission: string;
isAdmin: boolean;
};
session: {
data: {
uid?: string;
teams?: string[];
expires?: string;
};
};
declare namespace App {
interface Locals {
session: import('svelte-kit-cookie-session').Session<SessionData>;
cookies: Record<string, string>;
}
interface Platform {}
interface Session extends SessionData {}
interface Stuff {}
}
type Applications = {
name: string;
domain: string;
};
interface Hash {
iv: string;
content: string;
interface SessionData {
version?: string;
userId?: string | null;
teamId?: string | null;
permission?: string;
isAdmin?: boolean;
expires?: string | null;
gitlabToken?: string | null;
ghToken?: string | null;
}
interface BuildPack {
name: string;
}
// TODO: Not used, not working what?!
enum GitSource {
Github = 'github',
Gitlab = 'gitlab',
Bitbucket = 'bitbucket'
}
type RawHaproxyConfiguration = {
_version: number;
data: string;
};
type NewTransaction = {
_version: number;
id: string;
status: string;
};
type HttpRequestRuleForceSSL = {
return_hdrs: null;
cond: string;
cond_test: string;
index: number;
redir_code: number;
redir_type: string;
redir_value: string;
type: string;
};
// TODO: No any please
type HttpRequestRule = {
_version: number;
data: Array<any>;
};
type DateTimeFormatOptions = {
localeMatcher?: 'lookup' | 'best fit';
weekday?: 'long' | 'short' | 'narrow';
@@ -84,3 +36,24 @@ type DateTimeFormatOptions = {
hour12?: boolean;
timeZone?: string;
};
interface Hash {
iv: string;
content: string;
}
type RawHaproxyConfiguration = {
_version: number;
data: string;
};
type NewTransaction = {
_version: number;
id: string;
status: string;
};
type Application = {
name: string;
domain: string;
};

View File

@@ -2,7 +2,7 @@ import dotEnvExtended from 'dotenv-extended';
dotEnvExtended.load();
import type { GetSession } from '@sveltejs/kit';
import { handleSession } from 'svelte-kit-cookie-session';
import { getUserDetails, isTeamIdTokenAvailable, sentry } from '$lib/common';
import { getUserDetails, sentry } from '$lib/common';
import { version } from '$lib/common';
import cookie from 'cookie';
import { dev } from '$app/env';
@@ -10,27 +10,38 @@ import { dev } from '$app/env';
export const handle = handleSession(
{
secret: process.env['COOLIFY_SECRET_KEY'],
expires: 30
expires: 30,
cookie: { secure: false }
},
async function ({ event, resolve }) {
let response;
try {
const cookies: Cookies = cookie.parse(event.request.headers.get('cookie') || '');
if (cookies['kit.session']) {
const { permission, teamId } = await getUserDetails(event, false);
event.locals.user = {
teamId,
permission,
isAdmin: permission === 'admin' || permission === 'owner'
};
}
if (cookies.gitlabToken) {
event.locals.gitlabToken = cookies.gitlabToken;
if (event.locals.cookies) {
let gitlabToken = event.locals.cookies.gitlabToken || null;
let ghToken = event.locals.cookies.ghToken || null;
if (event.locals.cookies['kit.session']) {
const { permission, teamId, userId } = await getUserDetails(event, false);
const newSession = {
userId,
teamId,
permission,
isAdmin: permission === 'admin' || permission === 'owner',
expires: event.locals.session.data.expires,
gitlabToken,
ghToken
};
if (JSON.stringify(event.locals.session.data) !== JSON.stringify(newSession)) {
event.locals.session.data = { ...newSession };
}
}
}
response = await resolve(event, {
ssr: !event.url.pathname.startsWith('/webhooks/success')
});
} catch (error) {
console.log(error);
response = await resolve(event, {
ssr: !event.url.pathname.startsWith('/webhooks/success')
});
@@ -61,17 +72,13 @@ export const handle = handleSession(
}
);
export const getSession: GetSession = function (request) {
export const getSession: GetSession = function ({ locals }) {
return {
version,
gitlabToken: request.locals?.gitlabToken || null,
uid: request.locals.session.data?.uid || null,
teamId: request.locals.user?.teamId || null,
permission: request.locals.user?.permission,
isAdmin: request.locals.user?.isAdmin || false
...locals.session.data
};
};
export async function handleError({ error, event }) {
if (!dev) sentry.captureException(error, { event });
if (!dev) sentry.captureException(error, event);
}

View File

@@ -9,7 +9,8 @@ export default async function ({
docker,
buildId,
baseDirectory,
secrets
secrets,
pullmergeRequestId
}) {
try {
let file = `${workdir}/Dockerfile`;
@@ -24,7 +25,15 @@ export default async function ({
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
}
}
});
}

View File

@@ -2,8 +2,16 @@ import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => {
const { workdir, port, installCommand, buildCommand, startCommand, baseDirectory, secrets } =
data;
const {
workdir,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
secrets,
pullmergeRequestId
} = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
@@ -11,7 +19,15 @@ const createDockerfile = async (data, image): Promise<void> => {
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
}
}
});
}

View File

@@ -2,8 +2,16 @@ import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => {
const { workdir, port, installCommand, buildCommand, startCommand, baseDirectory, secrets } =
data;
const {
workdir,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
secrets,
pullmergeRequestId
} = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
@@ -11,7 +19,15 @@ const createDockerfile = async (data, image): Promise<void> => {
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
}
}
});
}

View File

@@ -2,8 +2,16 @@ import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => {
const { workdir, port, installCommand, buildCommand, startCommand, baseDirectory, secrets } =
data;
const {
workdir,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
secrets,
pullmergeRequestId
} = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
@@ -11,7 +19,15 @@ const createDockerfile = async (data, image): Promise<void> => {
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
}
}
});
}

View File

@@ -2,8 +2,16 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, workdir, buildCommand, baseDirectory, publishDirectory, secrets } =
data;
const {
applicationId,
tag,
workdir,
buildCommand,
baseDirectory,
publishDirectory,
secrets,
pullmergeRequestId
} = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
@@ -11,7 +19,15 @@ const createDockerfile = async (data, image): Promise<void> => {
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
}
}
});
}

View File

@@ -67,7 +67,7 @@ export const isTeamIdTokenAvailable = (request) => {
};
export const getTeam = (event) => {
const cookies: Cookies = Cookie.parse(event.request.headers.get('cookie'));
const cookies = Cookie.parse(event.request.headers.get('cookie'));
if (cookies.teamId) {
return cookies.teamId;
} else if (event.locals.session.data.teamId) {
@@ -78,7 +78,7 @@ export const getTeam = (event) => {
export const getUserDetails = async (event, isAdminRequired = true) => {
const teamId = getTeam(event);
const userId = event.locals.session.data.uid || null;
const userId = event.locals.session.data.userId || null;
const { permission = 'read' } = await db.prisma.permission.findFirst({
where: { teamId, userId },
select: { permission: true },

View File

@@ -1,9 +1,9 @@
<script>
import { browser } from '$app/env';
import { toast } from '@zerodevx/svelte-toast';
export let value;
let showPassword = false;
export let value;
export let disabled = false;
export let isPasswordField = false;
export let readonly = false;
@@ -14,7 +14,7 @@
export let name;
export let placeholder = '';
let disabledClass = 'bg-coolback disabled:bg-coolblack select-all';
let disabledClass = 'bg-coolback disabled:bg-coolblack';
let actionsShow = false;
let isHttps = browser && window.location.protocol === 'https:';
@@ -29,11 +29,7 @@
}
</script>
<span
class="relative"
on:mouseenter={() => showActions(true)}
on:mouseleave={() => showActions(false)}
>
<div class="relative">
{#if !isPasswordField || showPassword}
{#if textarea}
<textarea
@@ -77,69 +73,67 @@
/>
{/if}
{#if actionsShow}
<div class="absolute top-0 right-0 mx-2 cursor-pointer text-warmGray-600 hover:text-white">
<div class="flex space-x-2">
{#if isPasswordField}
<div on:click={() => (showPassword = !showPassword)}>
{#if showPassword}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
/>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</svg>
{/if}
</div>
{/if}
{#if value && isHttps}
<div on:click={copyToClipboard}>
<div class="absolute top-0 right-0 m-3 cursor-pointer text-warmGray-600 hover:text-white">
<div class="flex space-x-2">
{#if isPasswordField}
<div on:click={() => (showPassword = !showPassword)}>
{#if showPassword}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="8" y="8" width="12" height="12" rx="2" />
<path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
/>
</svg>
</div>
{/if}
</div>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</svg>
{/if}
</div>
{/if}
{#if value && isHttps}
<div on:click={copyToClipboard}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="8" y="8" width="12" height="12" rx="2" />
<path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" />
</svg>
</div>
{/if}
</div>
{/if}
</span>
</div>
</div>

View File

@@ -1,6 +1,6 @@
<script>
export let text;
export let maxWidthClass = 'max-w-[24rem]';
export let customClass = 'max-w-[24rem]';
</script>
<div class="py-1 text-xs text-stone-400 {maxWidthClass}">{@html text}</div>
<div class="py-1 text-xs text-stone-400 {customClass}">{@html text}</div>

View File

@@ -11,7 +11,7 @@
<span class="loader" />
</div>
{:else}
<div class=" main h-64 py-24 left-0 top-0 flex flex-wrap content-center mx-auto">
<div class="main h-64 py-24 left-0 top-0 flex flex-wrap content-center mx-auto">
<span class="loader" />
</div>
{/if}

View File

@@ -4,15 +4,18 @@
export let setting;
export let title;
export let description;
export let isPadding = true;
export let isCenter = true;
export let disabled = false;
export let dataTooltip = null;
</script>
<li class="flex items-center py-4">
<div class="flex w-96 flex-col" class:px-4={isPadding} class:pr-32={!isPadding}>
<p class="text-xs font-bold text-stone-100 md:text-base">{title}</p>
<div class="flex items-center py-4 pr-8">
<div class="flex w-96 flex-col">
<div class="text-xs font-bold text-stone-100 md:text-base">{title}</div>
<Explainer text={description} />
</div>
</div>
<div class:tooltip={dataTooltip} class:text-center={isCenter} data-tooltip={dataTooltip}>
<div
type="button"
on:click
@@ -58,5 +61,4 @@
</span>
</span>
</div>
<!-- {/if} -->
</li>
</div>

View File

@@ -18,7 +18,6 @@ export const buildPacks = [
{
name: 'static',
...defaultBuildAndDeploy,
publishDirectory: 'dist',
port: 80,
fancyName: 'Static',

View File

@@ -1,5 +1,5 @@
import { decrypt, encrypt } from '$lib/crypto';
import { removeProxyConfiguration, removeWwwRedirection } from '$lib/haproxy';
import { removeProxyConfiguration } from '$lib/haproxy';
import { asyncExecShell, getEngine } from '$lib/common';
import { getDomain, removeDestinationDocker } from '$lib/common';
@@ -209,10 +209,10 @@ export async function configureApplication({
});
}
export async function setApplicationSettings({ id, debug, previews }) {
export async function setApplicationSettings({ id, debug, previews, dualCerts }) {
return await prisma.application.update({
where: { id },
data: { settings: { update: { debug, previews } } },
data: { settings: { update: { debug, previews, dualCerts } } },
include: { destinationDocker: true }
});
}

View File

@@ -15,22 +15,41 @@ export async function isDockerNetworkExists({ network }) {
return await prisma.destinationDocker.findFirst({ where: { network } });
}
export async function isSecretExists({ id, name }) {
return await prisma.secret.findFirst({ where: { name, applicationId: id } });
export async function isSecretExists({ id, name, isPRMRSecret }) {
return await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } });
}
export async function isDomainConfigured({ id, fqdn }) {
const domain = getDomain(fqdn);
const nakedDomain = domain.replace('www.', '');
const foundApp = await prisma.application.findFirst({
where: { fqdn: { endsWith: `//${domain}` }, id: { not: id } },
where: {
OR: [
{ fqdn: { endsWith: `//${nakedDomain}` } },
{ fqdn: { endsWith: `//www.${nakedDomain}` } }
],
id: { not: id }
},
select: { fqdn: true }
});
const foundService = await prisma.service.findFirst({
where: { fqdn: { endsWith: `//${domain}` }, id: { not: id } },
where: {
OR: [
{ fqdn: { endsWith: `//${nakedDomain}` } },
{ fqdn: { endsWith: `//www.${nakedDomain}` } }
],
id: { not: id }
},
select: { fqdn: true }
});
const coolifyFqdn = await prisma.setting.findFirst({
where: { fqdn: { endsWith: `//${domain}` }, id: { not: id } },
where: {
OR: [
{ fqdn: { endsWith: `//${nakedDomain}` } },
{ fqdn: { endsWith: `//www.${nakedDomain}` } }
],
id: { not: id }
},
select: { fqdn: true }
});
if (foundApp || foundService || coolifyFqdn) return true;

View File

@@ -2,6 +2,7 @@ import { dev } from '$app/env';
import { sentry } from '$lib/common';
import * as Prisma from '@prisma/client';
import { default as ProdPrisma } from '@prisma/client';
import type { PrismaClientOptions } from '@prisma/client/runtime';
import generator from 'generate-password';
import forge from 'node-forge';
@@ -19,28 +20,20 @@ if (!dev) {
PrismaClient = ProdPrisma.PrismaClient;
P = ProdPrisma.Prisma;
}
let prismaOptions = {
export const prisma = new PrismaClient({
errorFormat: 'pretty',
rejectOnNotFound: false
};
if (dev) {
prismaOptions = {
errorFormat: 'pretty',
rejectOnNotFound: false,
log: [
{
emit: 'event',
level: 'query'
}
]
};
}
export const prisma = new PrismaClient(prismaOptions);
});
export function ErrorHandler(e) {
if (e! instanceof Error) {
e = new Error(e.toString());
}
let truncatedError = e;
if (e.stdout) {
truncatedError = e.stdout;
}
if (e.message?.includes('docker run')) {
let truncatedArray = [];
truncatedArray = truncatedError.message.split('-').filter((line) => {

View File

@@ -1,19 +1,41 @@
import { encrypt } from '$lib/crypto';
import { encrypt, decrypt } from '$lib/crypto';
import { prisma } from './common';
export async function listSecrets({ applicationId }) {
return await prisma.secret.findMany({
export async function listSecrets(applicationId: string) {
let secrets = await prisma.secret.findMany({
where: { applicationId },
orderBy: { createdAt: 'desc' },
select: { id: true, createdAt: true, name: true, isBuildSecret: true }
orderBy: { createdAt: 'desc' }
});
secrets = secrets.map((secret) => {
secret.value = decrypt(secret.value);
return secret;
});
return secrets;
}
export async function createSecret({ id, name, value, isBuildSecret, isPRMRSecret }) {
value = encrypt(value);
return await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
});
}
export async function createSecret({ id, name, value, isBuildSecret }) {
export async function updateSecret({ id, name, value, isBuildSecret, isPRMRSecret }) {
value = encrypt(value);
return await prisma.secret.create({
data: { name, value, isBuildSecret, application: { connect: { id } } }
});
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
console.log(found);
if (found) {
return await prisma.secret.updateMany({
where: { applicationId: id, name, isPRMRSecret },
data: { value, isBuildSecret, isPRMRSecret }
});
} else {
return await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
});
}
}
export async function removeSecret({ id, name }) {

View File

@@ -107,13 +107,20 @@ export async function configureServiceType({ id, type }) {
});
}
}
export async function setService({ id, version }) {
export async function setServiceVersion({ id, version }) {
return await prisma.service.update({
where: { id },
data: { version }
});
}
export async function setServiceSettings({ id, dualCerts }) {
return await prisma.service.update({
where: { id },
data: { dualCerts }
});
}
export async function updatePlausibleAnalyticsService({ id, fqdn, email, username, name }) {
await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } });
await prisma.service.update({ where: { id }, data: { name, fqdn } });

View File

@@ -6,19 +6,23 @@ import { asyncExecShell, uniqueName } from '$lib/common';
import * as db from '$lib/database';
import { startCoolifyProxy } from '$lib/haproxy';
export async function login({ email, password }) {
export async function hashPassword(password: string) {
const saltRounds = 15;
return bcrypt.hash(password, saltRounds);
}
export async function login({ email, password }) {
const users = await prisma.user.count();
const userFound = await prisma.user.findUnique({
where: { email },
include: { teams: true },
include: { teams: true, permission: true },
rejectOnNotFound: false
});
// Registration disabled if database is not seeded properly
const { isRegistrationEnabled, id } = await db.listSettings();
let uid = cuid();
let permission = 'read';
let isAdmin = false;
// Disable registration if we are registering the first user.
if (users === 0) {
await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } });
@@ -50,6 +54,8 @@ export async function login({ email, password }) {
};
}
uid = userFound.id;
// permission = userFound.permission;
isAdmin = true;
}
} else {
// If registration disabled, return 403
@@ -59,8 +65,10 @@ export async function login({ email, password }) {
};
}
const hashedPassword = await bcrypt.hash(password, saltRounds);
const hashedPassword = await hashPassword(password);
if (users === 0) {
permission = 'owner';
isAdmin = true;
await prisma.user.create({
data: {
id: uid,
@@ -103,8 +111,10 @@ export async function login({ email, password }) {
'Set-Cookie': `teamId=${uid}; HttpOnly; Path=/; Max-Age=15778800;`
},
body: {
uid,
teamId: uid
userId: uid,
teamId: uid,
permission,
isAdmin
}
};
}

View File

@@ -13,15 +13,24 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
installCommand,
buildCommand,
debug,
secrets
secrets,
pullmergeRequestId
} = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push('WORKDIR /usr/src/app');
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (!secret.isBuildSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
}
}
}
});
}

View File

@@ -2,7 +2,6 @@ import { dev } from '$app/env';
import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import got from 'got';
import * as db from '$lib/database';
import { letsEncrypt } from '$lib/letsencrypt';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
@@ -49,7 +48,8 @@ export async function completeTransaction(transactionId) {
return await haproxy.put(`v2/services/haproxy/transactions/${transactionId}`);
}
export async function removeProxyConfiguration({ domain }) {
export async function removeProxyConfiguration(fqdn) {
const domain = getDomain(fqdn);
const haproxy = await haproxyInstance();
const backendFound = await haproxy
.get(`v2/services/haproxy/configuration/backends/${domain}`)
@@ -65,97 +65,92 @@ export async function removeProxyConfiguration({ domain }) {
.json();
await completeTransaction(transactionId);
}
await removeWwwRedirection(domain);
await forceSSLOffApplication(domain);
await removeWwwRedirection(fqdn);
}
export async function forceSSLOffApplication({ domain }) {
if (!dev) {
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
let transactionId;
try {
const rules: any = await haproxy
.get(`v2/services/haproxy/configuration/http_request_rules`, {
searchParams: {
parent_name: 'http',
parent_type: 'frontend'
}
})
.json();
if (rules.data.length > 0) {
const rule = rules.data.find((rule) => rule.cond_test.includes(`-i ${domain}`));
if (rule) {
transactionId = await getNextTransactionId();
await haproxy
.delete(`v2/services/haproxy/configuration/http_request_rules/${rule.index}`, {
searchParams: {
transaction_id: transactionId,
parent_name: 'http',
parent_type: 'frontend'
}
})
.json();
export async function forceSSLOffApplication(domain) {
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
let transactionId;
try {
const rules: any = await haproxy
.get(`v2/services/haproxy/configuration/http_request_rules`, {
searchParams: {
parent_name: 'http',
parent_type: 'frontend'
}
})
.json();
if (rules.data.length > 0) {
const rule = rules.data.find((rule) =>
rule.cond_test.includes(`{ hdr(host) -i ${domain} } !{ ssl_fc }`)
);
if (rule) {
transactionId = await getNextTransactionId();
await haproxy
.delete(`v2/services/haproxy/configuration/http_request_rules/${rule.index}`, {
searchParams: {
transaction_id: transactionId,
parent_name: 'http',
parent_type: 'frontend'
}
})
.json();
}
} catch (error) {
console.log(error);
} finally {
if (transactionId) await completeTransaction(transactionId);
}
} else {
console.log(`[DEBUG] Removing ssl for ${domain}`);
} catch (error) {
console.log(error);
} finally {
if (transactionId) await completeTransaction(transactionId);
}
}
export async function forceSSLOnApplication({ domain }) {
if (!dev) {
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
let transactionId;
try {
const rules: any = await haproxy
.get(`v2/services/haproxy/configuration/http_request_rules`, {
searchParams: {
parent_name: 'http',
parent_type: 'frontend'
}
})
.json();
let nextRule = 0;
if (rules.data.length > 0) {
const rule = rules.data.find((rule) =>
rule.cond_test.includes(`{ hdr(host) -i ${domain} } !{ ssl_fc }`)
);
if (rule) return;
nextRule = rules.data[rules.data.length - 1].index + 1;
}
transactionId = await getNextTransactionId();
await haproxy
.post(`v2/services/haproxy/configuration/http_request_rules`, {
searchParams: {
transaction_id: transactionId,
parent_name: 'http',
parent_type: 'frontend'
},
json: {
index: nextRule,
cond: 'if',
cond_test: `{ hdr(host) -i ${domain} } !{ ssl_fc }`,
type: 'redirect',
redir_type: 'scheme',
redir_value: 'https',
redir_code: 301
}
})
.json();
} catch (error) {
console.log(error);
throw error;
} finally {
if (transactionId) await completeTransaction(transactionId);
export async function forceSSLOnApplication(domain) {
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
let transactionId;
try {
const rules: any = await haproxy
.get(`v2/services/haproxy/configuration/http_request_rules`, {
searchParams: {
parent_name: 'http',
parent_type: 'frontend'
}
})
.json();
let nextRule = 0;
if (rules.data.length > 0) {
const rule = rules.data.find((rule) =>
rule.cond_test.includes(`{ hdr(host) -i ${domain} } !{ ssl_fc }`)
);
if (rule) return;
nextRule = rules.data[rules.data.length - 1].index + 1;
}
} else {
console.log(`[DEBUG] Adding ssl for ${domain}`);
transactionId = await getNextTransactionId();
await haproxy
.post(`v2/services/haproxy/configuration/http_request_rules`, {
searchParams: {
transaction_id: transactionId,
parent_name: 'http',
parent_type: 'frontend'
},
json: {
index: nextRule,
cond: 'if',
cond_test: `{ hdr(host) -i ${domain} } !{ ssl_fc }`,
type: 'redirect',
redir_type: 'scheme',
redir_value: 'https',
redir_code: dev ? 302 : 301
}
})
.json();
} catch (error) {
console.log(error);
throw error;
} finally {
if (transactionId) await completeTransaction(transactionId);
}
}
@@ -274,6 +269,7 @@ export async function configureProxyForApplication({ domain, imageId, applicatio
export async function configureCoolifyProxyOff(fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
@@ -288,10 +284,8 @@ export async function configureCoolifyProxyOff(fqdn) {
})
.json();
await completeTransaction(transactionId);
if (!dev) {
await forceSSLOffApplication({ domain });
}
await setWwwRedirection(fqdn);
if (isHttps) await forceSSLOffApplication(domain);
await removeWwwRedirection(fqdn);
} catch (error) {
throw error?.response?.body || error;
}
@@ -565,7 +559,8 @@ export async function configureSimpleServiceProxyOn({ id, domain, port }) {
await completeTransaction(transactionId);
}
export async function configureSimpleServiceProxyOff({ domain }) {
export async function configureSimpleServiceProxyOff(fqdn) {
const domain = getDomain(fqdn);
const haproxy = await haproxyInstance();
await checkHAProxy(haproxy);
try {
@@ -580,11 +575,16 @@ export async function configureSimpleServiceProxyOff({ domain }) {
.json();
await completeTransaction(transactionId);
} catch (error) {}
await removeWwwRedirection(domain);
await forceSSLOffApplication(domain);
await removeWwwRedirection(fqdn);
return;
}
export async function removeWwwRedirection(domain) {
export async function removeWwwRedirection(fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
const haproxy = await haproxyInstance();
await checkHAProxy();
const rules: any = await haproxy
@@ -596,9 +596,7 @@ export async function removeWwwRedirection(domain) {
})
.json();
if (rules.data.length > 0) {
const rule = rules.data.find((rule) =>
rule.redir_value.includes(`${domain}%[capture.req.uri]`)
);
const rule = rules.data.find((rule) => rule.redir_value.includes(redirectValue));
if (rule) {
const transactionId = await getNextTransactionId();
await haproxy
@@ -623,6 +621,7 @@ export async function setWwwRedirection(fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
const contTest = `{ req.hdr(host) -i ${isWWW ? domain.replace('www.', '') : `www.${domain}`} }`;
const rules: any = await haproxy
.get(`v2/services/haproxy/configuration/http_request_rules`, {
@@ -634,13 +633,11 @@ export async function setWwwRedirection(fqdn) {
.json();
let nextRule = 0;
if (rules.data.length > 0) {
const rule = rules.data.find((rule) =>
rule.redir_value.includes(`${domain}%[capture.req.uri]`)
);
const rule = rules.data.find((rule) => rule.redir_value.includes(redirectValue));
if (rule) return;
nextRule = rules.data[rules.data.length - 1].index + 1;
}
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
transactionId = await getNextTransactionId();
await haproxy
.post(`v2/services/haproxy/configuration/http_request_rules`, {

View File

@@ -1,50 +1,78 @@
import { dev } from '$app/env';
import { forceSSLOffApplication, forceSSLOnApplication, getNextTransactionId } from '$lib/haproxy';
import { forceSSLOffApplication, forceSSLOnApplication } from '$lib/haproxy';
import { asyncExecShell, getEngine } from './common';
import * as db from '$lib/database';
import cuid from 'cuid';
import getPort from 'get-port';
export async function letsEncrypt({ domain, isCoolify = false, id = null }) {
try {
const nakedDomain = domain.replace('www.', '');
const wwwDomain = `www.${nakedDomain}`;
const randomCuid = cuid();
if (dev) {
return await forceSSLOnApplication({ domain });
} else {
if (isCoolify) {
await asyncExecShell(
`docker run --rm --name certbot-${randomCuid} -p 9080:9080 -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port 9080 -d ${domain} --agree-tos --non-interactive --register-unsafely-without-email`
);
const randomPort = 9080;
const { stderr } = await asyncExecShell(
`docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest cat /etc/letsencrypt/live/${domain}/fullchain.pem /etc/letsencrypt/live/${domain}/privkey.pem > /app/ssl/${domain}.pem`
);
if (stderr) throw new Error(stderr);
return;
let host;
let dualCerts = false;
if (isCoolify) {
const data = await db.prisma.setting.findFirst();
dualCerts = data.dualCerts;
host = 'unix:///var/run/docker.sock';
} else {
// Check Application
const applicationData = await db.prisma.application.findUnique({
where: { id },
include: { destinationDocker: true, settings: true }
});
if (applicationData) {
if (applicationData?.destinationDockerId && applicationData?.destinationDocker) {
host = getEngine(applicationData.destinationDocker.engine);
}
if (applicationData?.settings?.dualCerts) {
dualCerts = applicationData.settings.dualCerts;
}
}
let data: any = await db.prisma.application.findUnique({
// Check Service
const serviceData = await db.prisma.service.findUnique({
where: { id },
include: { destinationDocker: true }
});
if (!data) {
data = await db.prisma.service.findUnique({
where: { id },
include: { destinationDocker: true }
});
}
// Set SSL with Let's encrypt
if (data.destinationDockerId && data.destinationDocker) {
const host = getEngine(data.destinationDocker.engine);
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:9080 -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port 9080 -d ${domain} --agree-tos --non-interactive --register-unsafely-without-email`
);
const { stderr } = await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name bash-${randomCuid} -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest cat /etc/letsencrypt/live/${domain}/fullchain.pem /etc/letsencrypt/live/${domain}/privkey.pem > /app/ssl/${domain}.pem`
);
if (stderr) throw new Error(stderr);
await forceSSLOnApplication({ domain });
if (serviceData) {
if (serviceData?.destinationDockerId && serviceData?.destinationDocker) {
host = getEngine(serviceData.destinationDocker.engine);
}
if (serviceData?.dualCerts) {
dualCerts = serviceData.dualCerts;
}
}
}
await forceSSLOffApplication(domain);
if (dualCerts) {
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${nakedDomain} -d ${wwwDomain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
dev ? '--test-cert' : ''
}`
);
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "test -d /etc/letsencrypt/live/${nakedDomain}/ && cat /etc/letsencrypt/live/${nakedDomain}/fullchain.pem /etc/letsencrypt/live/${nakedDomain}/privkey.pem > /app/ssl/${nakedDomain}.pem || cat /etc/letsencrypt/live/${wwwDomain}/fullchain.pem /etc/letsencrypt/live/${wwwDomain}/privkey.pem > /app/ssl/${wwwDomain}.pem"`
);
} else {
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${domain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${
dev ? '--test-cert' : ''
}`
);
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "cat /etc/letsencrypt/live/${domain}/fullchain.pem /etc/letsencrypt/live/${domain}/privkey.pem > /app/ssl/${domain}.pem"`
);
}
} catch (error) {
throw error;
if (error.code !== 0) {
throw error;
}
} finally {
if (!isCoolify) {
await forceSSLOnApplication(domain);
}
}
}

View File

@@ -64,7 +64,6 @@ export default async function (job) {
if (destinationDockerId) {
destinationType = 'docker';
}
if (destinationType === 'docker') {
const docker = dockerInstance({ destinationDocker });
const host = getEngine(destinationDocker.engine);
@@ -205,7 +204,15 @@ export default async function (job) {
const envs = [];
if (secrets.length > 0) {
secrets.forEach((secret) => {
envs.push(`${secret.name}=${secret.value}`);
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
}
});
}
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
@@ -239,6 +246,8 @@ export default async function (job) {
if (stderr) console.log(stderr);
saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} catch (error) {
saveBuildLog({ line: error, buildId, applicationId });
sentry.captureException(error);
throw new Error(error);
}
try {
@@ -257,7 +266,9 @@ export default async function (job) {
});
}
} catch (error) {
saveBuildLog({ line: error.stdout || error, buildId, applicationId });
sentry.captureException(error);
throw new Error(error);
}
}
}

View File

@@ -127,7 +127,6 @@ buildWorker.on('completed', async (job: Bullmq.Job) => {
});
buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => {
console.log(failedReason);
try {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } });
} catch (error) {
@@ -136,7 +135,11 @@ buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => {
const workdir = `/tmp/build-sources/${job.data.repository}`;
await asyncExecShell(`rm -fr ${workdir}`);
}
saveBuildLog({ line: 'Failed build!', buildId: job.data.build_id, applicationId: job.data.id });
saveBuildLog({
line: 'Failed to deploy!',
buildId: job.data.build_id,
applicationId: job.data.id
});
saveBuildLog({
line: `Reason: ${failedReason.toString()}`,
buildId: job.data.build_id,

View File

@@ -48,7 +48,7 @@ export default async function () {
port
});
const isHttps = fqdn.startsWith('https://');
if (isHttps) await forceSSLOnApplication({ domain });
if (isHttps) await forceSSLOnApplication(domain);
await setWwwRedirection(fqdn);
}
}
@@ -98,7 +98,7 @@ export default async function () {
await configureCoolifyProxyOn(fqdn);
await setWwwRedirection(fqdn);
const isHttps = fqdn.startsWith('https://');
if (isHttps) await forceSSLOnApplication({ domain });
if (isHttps) await forceSSLOnApplication(domain);
}
} catch (error) {
console.log(error);

View File

@@ -1,5 +1,7 @@
export const publicPaths = [
'/login',
'/reset',
'/reset/password',
'/webhooks/success',
'/webhooks/github',
'/webhooks/github/install',

View File

@@ -2,14 +2,14 @@
import type { Load } from '@sveltejs/kit';
import { publicPaths } from '$lib/settings';
export const load: Load = async ({ fetch, url, params, session }) => {
if (!session.uid && !publicPaths.includes(url.pathname)) {
export const load: Load = async ({ fetch, url, session }) => {
if (!session.userId && !publicPaths.includes(url.pathname)) {
return {
status: 302,
redirect: '/login'
};
}
if (!session.uid) {
if (!session.userId) {
return {};
}
const endpoint = `/teams.json`;
@@ -49,7 +49,7 @@
};
let latestVersion = 'latest';
onMount(async () => {
if ($session.uid) {
if ($session.userId) {
const overrideVersion = browser && window.localStorage.getItem('latestVersion');
try {
await get(`/login.json`);
@@ -84,7 +84,7 @@
}
async function switchTeam() {
try {
await post(`/index.json?from=${$page.url.pathname}`, {
await post(`/dashboard.json?from=${$page.url.pathname}`, {
cookie: 'teamId',
value: selectedTeamId
});
@@ -129,7 +129,7 @@
<title>Coolify</title>
</svelte:head>
<SvelteToast options={{ intro: { y: -64 }, duration: 3000, pausable: true }} />
{#if $session.uid}
{#if $session.userId}
<nav class="nav-main">
<div class="flex h-screen w-full flex-col items-center transition-all duration-100">
<div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div>

View File

@@ -17,7 +17,7 @@
const endpoint = `/applications/${params.id}.json`;
const res = await fetch(endpoint);
if (res.ok) {
const { application, githubToken, ghToken, isRunning, appId } = await res.json();
const { application, isRunning, appId } = await res.json();
if (!application || Object.entries(application).length === 0) {
return {
status: 302,
@@ -42,8 +42,6 @@
},
stuff: {
isRunning,
ghToken,
githubToken,
application,
appId
}

View File

@@ -16,7 +16,7 @@ export const post: RequestHandler = async (event) => {
const found = await db.isDomainConfigured({ id, fqdn });
if (found) {
throw {
message: `Domain ${getDomain(fqdn)} is already configured.`
message: `Domain ${getDomain(fqdn).replace('www.', '')} is already configured.`
};
}
return {

View File

@@ -3,13 +3,11 @@
import { goto } from '$app/navigation';
export let githubToken;
export let application;
import { page } from '$app/stores';
import { page, session } from '$app/stores';
import { get, post } from '$lib/api';
import { getGithubToken } from '$lib/components/common';
import { enhance, errorNotification } from '$lib/form';
import { errorNotification } from '$lib/form';
import { onMount } from 'svelte';
const { id } = $page.params;
@@ -32,15 +30,12 @@
branch: undefined
};
let showSave = false;
let token = null;
async function loadRepositoriesByPage(page = 0) {
return await get(`${apiUrl}/installation/repositories?per_page=100&page=${page}`, {
Authorization: `token ${$session.ghToken}`
});
}
async function loadRepositories() {
token = await getGithubToken({ apiUrl, githubToken, application });
let page = 1;
let reposCount = 0;
const loadedRepos = await loadRepositoriesByPage();
@@ -61,7 +56,7 @@
selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id;
try {
branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, {
Authorization: `token ${token}`
Authorization: `token ${$session.ghToken}`
});
return;
} catch ({ error }) {

View File

@@ -8,7 +8,6 @@
import cuid from 'cuid';
import { goto } from '$app/navigation';
import { del, get, post, put } from '$lib/api';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');

View File

@@ -1,7 +1,7 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, url, stuff }) => {
const { application, ghToken } = stuff;
const { application } = stuff;
if (application?.buildPack && !url.searchParams.get('from')) {
return {
status: 302,
@@ -14,8 +14,7 @@
return {
props: {
...(await res.json()),
application,
ghToken
application
}
};
}
@@ -43,7 +42,6 @@
export let projectId;
export let repository;
export let branch;
export let ghToken;
export let type;
export let application;
@@ -96,7 +94,7 @@
}
} else if (type === 'github') {
const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, {
Authorization: `Bearer ${ghToken}`,
Authorization: `Bearer ${$session.ghToken}`,
Accept: 'application/vnd.github.v2.json'
});
const packageJson = files.find(
@@ -113,7 +111,7 @@
foundConfig.buildPack = 'docker';
} else if (packageJson) {
const data = await get(`${packageJson.git_url}`, {
Authorization: `Bearer ${ghToken}`,
Authorization: `Bearer ${$session.ghToken}`,
Accept: 'application/vnd.github.v2.raw'
});
const json = JSON.parse(data) || {};

View File

@@ -1,7 +1,7 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ params, url, stuff }) => {
const { application, githubToken, appId } = stuff;
const { application, appId } = stuff;
if (application?.branch && application?.repository && !url.searchParams.get('from')) {
return {
status: 302,
@@ -10,7 +10,6 @@
}
return {
props: {
githubToken,
application,
appId
}
@@ -20,7 +19,6 @@
<script lang="ts">
export let application;
export let githubToken;
export let appId;
import GithubRepositories from './_GithubRepositories.svelte';
import GitlabRepositories from './_GitlabRepositories.svelte';
@@ -31,7 +29,7 @@
</div>
<div class="flex flex-wrap justify-center">
{#if application.gitSource.type === 'github'}
<GithubRepositories {application} {githubToken} />
<GithubRepositories {application} />
{:else if application.gitSource.type === 'gitlab'}
<GitlabRepositories {application} {appId} />
{/if}

View File

@@ -15,8 +15,8 @@ export const get: RequestHandler = async (event) => {
let githubToken = null;
let ghToken = null;
let isRunning = false;
const { id } = event.params;
try {
const application = await db.getApplication({ id, teamId });
const { gitSource } = application;
@@ -52,15 +52,20 @@ export const get: RequestHandler = async (event) => {
if (application.destinationDockerId) {
isRunning = await checkContainer(application.destinationDocker.engine, id);
}
return {
const payload = {
body: {
isRunning,
ghToken,
githubToken,
application,
appId
}
},
headers: {}
};
if (ghToken) {
payload.headers = {
'set-cookie': [`ghToken=${ghToken}; HttpOnly; Path=/; Max-Age=15778800;`]
};
}
return payload;
} catch (error) {
console.log(error);
return ErrorHandler(error);

View File

@@ -42,7 +42,7 @@
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import type Prisma from '@prisma/client';
import { getDomain, notNodeDeployments, staticDeployments } from '$lib/components/common';
import { notNodeDeployments, staticDeployments } from '$lib/components/common';
import { toast } from '@zerodevx/svelte-toast';
import { post } from '$lib/api';
const { id } = $page.params;
@@ -52,6 +52,7 @@
let loading = false;
let debug = application.settings.debug;
let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts;
onMount(() => {
domainEl.focus();
@@ -64,8 +65,11 @@
if (name === 'previews') {
previews = !previews;
}
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
try {
await post(`/applications/${id}/settings.json`, { previews, debug });
await post(`/applications/${id}/settings.json`, { previews, debug, dualCerts });
return toast.push('Settings saved.');
} catch ({ error }) {
return errorNotification(error);
@@ -193,7 +197,7 @@
value={application.gitSource.name}
id="gitSource"
disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500"
class="cursor-pointer hover:bg-coolgray-500"
/></a
>
</div>
@@ -210,7 +214,7 @@
value="{application.repository}/{application.branch}"
id="repository"
disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500"
class="cursor-pointer hover:bg-coolgray-500"
/></a
>
</div>
@@ -228,7 +232,7 @@
value={application.buildPack}
id="buildPack"
disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500"
class="cursor-pointer hover:bg-coolgray-500"
/></a
>
</div>
@@ -252,7 +256,7 @@
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-3">
<label for="fqdn" class="pt-2">Domain (FQDN)</label>
<label for="fqdn" class="relative pt-2">Domain (FQDN)</label>
<div class="col-span-2">
<input
readonly={!$session.isAdmin || isRunning}
@@ -266,11 +270,21 @@
required
/>
<Explainer
text="If you specify <span class='text-green-600 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-green-600 font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application."
text="If you specify <span class='text-green-500 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-green-500 font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application."
/>
</div>
</div>
<div class="grid grid-cols-2 items-center pb-8">
<Setting
dataTooltip="Must be stopped to modify."
disabled={isRunning}
isCenter={false}
bind:setting={dualCerts}
title="Generate SSL for www and non-www?"
description="It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-green-500'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both."
on:click={() => !isRunning && changeSettings('dualCerts')}
/>
</div>
{#if !staticDeployments.includes(application.buildPack)}
<div class="grid grid-cols-3 items-center">
<label for="port">Port</label>
@@ -285,6 +299,7 @@
</div>
</div>
{/if}
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-3 items-center">
<label for="installCommand">Install Command</label>
@@ -361,8 +376,7 @@
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">Features</div>
</div>
<div class="px-4 pb-10 sm:px-6">
<!-- <ul class="mt-2 divide-y divide-stone-800">
<!-- <ul class="mt-2 divide-y divide-stone-800">
<Setting
bind:setting={forceSSL}
on:click={() => changeSettings('forceSSL')}
@@ -370,21 +384,24 @@
description="Creates a https redirect for all requests from http and also generates a https certificate for the domain through Let's Encrypt."
/>
</ul> -->
<ul class="mt-2 divide-y divide-stone-800">
<div class="px-10 pb-10">
<div class="grid grid-cols-2 items-center">
<Setting
isCenter={false}
bind:setting={previews}
on:click={() => changeSettings('previews')}
title="Enable MR/PR Previews"
description="Creates previews from pull and merge requests."
/>
</ul>
<ul class="mt-2 divide-y divide-stone-800">
</div>
<div class="grid grid-cols-2 items-center">
<Setting
isCenter={false}
bind:setting={debug}
on:click={() => changeSettings('debug')}
title="Debug Logs"
description="Enable debug logs during build phase. <br>(<span class='text-red-500'>sensitive information</span> could be visible in logs)"
/>
</ul>
</div>
</div>
</div>

View File

@@ -86,6 +86,7 @@
<div class="flex justify-end sticky top-0 p-2">
<button
on:click={followBuild}
class="bg-transparent"
data-tooltip="Follow logs"
class:text-green-500={followingBuild}
>
@@ -108,7 +109,7 @@
</button>
</div>
<div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12"
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl}
>
{#each logs as log}

View File

@@ -82,7 +82,7 @@
}
async function loadBuild(build) {
buildId = build;
goto(`/applications/${id}/logs/build?buildId=${buildId}`);
await goto(`/applications/${id}/logs/build?buildId=${buildId}`);
}
</script>
@@ -94,17 +94,19 @@
<div class="block flex-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex">
<div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 ">
<div class="top-4 md:sticky">
{#each builds as build (build.id)}
{#each builds as build, index (build.id)}
<div
data-tooltip={new Intl.DateTimeFormat('default', dateOptions).format(
new Date(build.createdAt)
) + `\n${build.status}`}
on:click={() => loadBuild(build.id)}
class="tooltip-top flex cursor-pointer items-center justify-center rounded-r border-l-2 border-transparent py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl "
class:rounded-tr={index === 0}
class:rounded-br={index === builds.length - 1}
class="tooltip-top flex cursor-pointer items-center justify-center border-l-2 border-transparent py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl "
class:bg-coolgray-400={buildId === build.id}
class:border-red-500={build.status === 'failed'}
class:border-green-500={build.status === 'success'}
class:border-yellow-500={build.status === 'inprogress'}
class:border-yellow-500={build.status === 'running'}
>
<div class="flex-col px-2">
<div class="text-sm font-bold">

View File

@@ -77,11 +77,12 @@
{#if logs.length === 0}
<div class="text-xl font-bold tracking-tighter">Waiting for the logs...</div>
{:else}
<div class="relative w-full">
<div class="relative">
<LoadingLogs />
<div class="flex justify-end sticky top-0 p-2">
<button
on:click={followBuild}
class="bg-transparent"
data-tooltip="Follow logs"
class:text-green-500={followingBuild}
>
@@ -104,12 +105,14 @@
</button>
</div>
<div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 p-6 whitespace-pre-wrap break-words w-full mb-10 -mt-12"
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl}
>
{#each logs as log}
{log + '\n'}
{/each}
<div class="px-2">
{#each logs as log}
{log + '\n'}
{/each}
</div>
</div>
</div>
{/if}

View File

@@ -11,6 +11,9 @@ export const get: RequestHandler = async (event) => {
const { id } = event.params;
try {
const secrets = await db.listSecrets(id);
const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret);
const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret);
const destinationDocker = await db.getDestinationByApplicationId({ id, teamId });
const docker = dockerInstance({ destinationDocker });
const listContainers = await docker.engine.listContainers({
@@ -35,7 +38,13 @@ export const get: RequestHandler = async (event) => {
});
return {
body: {
containers: jsonContainers
containers: jsonContainers,
applicationSecrets: applicationSecrets.sort((a, b) => {
return ('' + a.name).localeCompare(b.name);
}),
PRMRSecrets: PRMRSecrets.sort((a, b) => {
return ('' + a.name).localeCompare(b.name);
})
}
};
} catch (error) {

View File

@@ -22,8 +22,19 @@
<script lang="ts">
export let containers;
export let application;
export let PRMRSecrets;
export let applicationSecrets;
import { getDomain } from '$lib/components/common';
import Secret from '../secrets/_Secret.svelte';
import { get } from '$lib/api';
import { page } from '$app/stores';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params;
async function refreshSecrets() {
const data = await get(`/applications/${id}/secrets.json`);
PRMRSecrets = [...data.secrets];
}
</script>
<div class="flex space-x-1 p-6 font-bold">
@@ -32,7 +43,57 @@
</div>
</div>
<div class="mx-auto max-w-4xl px-6">
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<table class="mx-auto">
<thead class=" rounded-xl border-b border-coolgray-500">
<tr>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-white">Name</th
>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-white"
>Value</th
>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-white"
>Need during buildtime?</th
>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-white"
/>
</tr>
</thead>
<tbody class="">
{#each applicationSecrets as secret}
{#key secret.id}
<tr class="h-20 transition duration-100 hover:bg-coolgray-400">
<Secret
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
isPRMRSecret
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
</tr>
{/key}
{/each}
</tbody>
</table>
</div>
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={applicationSecrets.length === 0
? "<span class='font-bold text-white text-xl'>Please add secrets to the application first.</span> <br><br>These values overwrite application secrets in PR/MR deployments. Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
: "These values overwrite application secrets in PR/MR deployments. Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
/>
</div>
<div class="mx-auto max-w-4xl py-10">
<div class="flex flex-wrap justify-center space-x-2">
{#if containers.length > 0}
{#each containers as container}

View File

@@ -3,14 +3,19 @@
export let value = '';
export let isBuildSecret = false;
export let isNewSecret = false;
export let isPRMRSecret = false;
export let PRMRSecret = {};
if (isPRMRSecret) value = PRMRSecret.value;
import { page } from '$app/stores';
import { del, post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
let nameEl;
let valueEl;
const { id } = $page.params;
async function removeSecret() {
try {
@@ -25,24 +30,24 @@
return errorNotification(error);
}
}
async function saveSecret() {
const nameValid = nameEl.checkValidity();
const valueValid = valueEl.checkValidity();
if (!nameValid) {
return nameEl.reportValidity();
}
if (!valueValid) {
return valueEl.reportValidity();
}
async function saveSecret(isNew = false) {
if (!name) return errorNotification('Name is required.');
if (!value) return errorNotification('Value is required.');
try {
await post(`/applications/${id}/secrets.json`, { name, value, isBuildSecret });
await post(`/applications/${id}/secrets.json`, {
name,
value,
isBuildSecret,
isPRMRSecret,
isNew
});
dispatch('refresh');
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
toast.push('Secret saved.');
} catch ({ error }) {
return errorNotification(error);
}
@@ -56,8 +61,7 @@
<td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white">
<input
id="secretName"
bind:this={nameEl}
id={isNewSecret ? 'secretName' : 'secretNameNew'}
bind:value={name}
required
placeholder="EXAMPLE_VARIABLE"
@@ -68,16 +72,13 @@
/>
</td>
<td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white">
<input
id="secretValue"
<CopyPasswordField
id={isNewSecret ? 'secretValue' : 'secretValueNew'}
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
isPasswordField={true}
bind:value
bind:this={valueEl}
required
placeholder="J$#@UIO%HO#$U%H"
class="-mx-2 w-64 border-2 border-transparent"
class:bg-transparent={!isNewSecret}
class:cursor-not-allowed={!isNewSecret}
readonly={!isNewSecret}
/>
</td>
<td class="whitespace-nowrap px-6 py-2 text-center text-sm font-medium text-white">
@@ -132,11 +133,20 @@
<td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white">
{#if isNewSecret}
<div class="flex items-center justify-center">
<button class="w-24 bg-green-600 hover:bg-green-500" on:click={saveSecret}>Add</button>
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveSecret(true)}>Add</button>
</div>
{:else}
<div class="flex justify-center items-end">
<button class="w-24 bg-red-600 hover:bg-red-500" on:click={removeSecret}>Remove</button>
<div class="flex-col space-y-2">
<div class="flex items-center justify-center">
<button class="w-24 bg-green-600 hover:bg-green-500" on:click={() => saveSecret(false)}
>Set</button
>
</div>
{#if !isPRMRSecret}
<div class="flex justify-center items-end">
<button class="w-24 bg-red-600 hover:bg-red-500" on:click={removeSecret}>Remove</button>
</div>
{/if}
</div>
{/if}
</td>

View File

@@ -7,8 +7,9 @@ export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const secrets = await db.listSecrets({ applicationId: event.params.id });
const secrets = await (await db.listSecrets(id)).filter((secret) => !secret.isPRMRSecret);
return {
status: 200,
body: {
@@ -27,16 +28,22 @@ export const post: RequestHandler = async (event) => {
if (status === 401) return { status, body };
const { id } = event.params;
const { name, value, isBuildSecret } = await event.request.json();
const { name, value, isBuildSecret, isPRMRSecret, isNew } = await event.request.json();
try {
const found = await db.isSecretExists({ id, name });
if (found) {
throw {
error: `Secret ${name} already exists.`
};
if (isNew) {
const found = await db.isSecretExists({ id, name, isPRMRSecret });
if (found) {
throw {
error: `Secret ${name} already exists.`
};
} else {
await db.createSecret({ id, name, value, isBuildSecret, isPRMRSecret });
return {
status: 201
};
}
} else {
await db.createSecret({ id, name, value, isBuildSecret });
await db.updateSecret({ id, name, value, isBuildSecret, isPRMRSecret });
return {
status: 201
};

View File

@@ -46,32 +46,31 @@
<tr>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-warmGray-400"
>Name</th
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-white">Name</th
>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-warmGray-400"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-white"
>Value</th
>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-warmGray-400"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-white"
>Need during buildtime?</th
>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-warmGray-400"
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-white"
/>
</tr>
</thead>
<tbody class="">
{#each secrets as secret}
{#key secret.id}
<tr class="hover:bg-coolgray-200">
<tr class="h-20 transition duration-100 hover:bg-coolgray-400">
<Secret
name={secret.name}
value={secret.value ? secret.value : 'ENCRYPTED'}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>

View File

@@ -8,10 +8,10 @@ export const post: RequestHandler = async (event) => {
if (status === 401) return { status, body };
const { id } = event.params;
const { debug, previews } = await event.request.json();
const { debug, previews, dualCerts } = await event.request.json();
try {
await db.setApplicationSettings({ id, debug, previews });
await db.setApplicationSettings({ id, debug, previews, dualCerts });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);

View File

@@ -16,12 +16,11 @@ export const post: RequestHandler = async (event) => {
id,
teamId
});
const domain = getDomain(fqdn);
if (destinationDockerId) {
const docker = dockerInstance({ destinationDocker });
await docker.engine.getContainer(id).stop();
}
await removeProxyConfiguration({ domain });
await removeProxyConfiguration(fqdn);
return {
status: 200
};

View File

@@ -20,7 +20,7 @@
</script>
<script lang="ts">
export let applications: Array<Applications>;
export let applications: Array<Application>;
import { session } from '$app/stores';
import Application from './_Application.svelte';
</script>

View File

@@ -6,73 +6,63 @@
<div class="flex space-x-1 py-5 font-bold">
<div class="title">CouchDB</div>
</div>
<div class="px-10">
<div class="grid grid-cols-3 items-center">
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase">Default Database</label>
<div class="col-span-2 ">
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="eg: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="eg: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center">
<label for="dbUser">User</label>
<div class="col-span-2 ">
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword">Password</label>
<div class="col-span-2 ">
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
value={database.dbUserPassword}
/>
</div>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
value={database.dbUserPassword}
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center">
<label for="rootUser">Root User</label>
<div class="col-span-2 ">
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="rootUser"
name="rootUser"
value={database.rootUser}
/>
</div>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="rootUser"
name="rootUser"
value={database.rootUser}
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword">Root's Password</label>
<div class="col-span-2 ">
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword"
name="rootUserPassword"
value={database.rootUserPassword}
/>
</div>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword"
name="rootUserPassword"
value={database.rootUserPassword}
/>
</div>
</div>

View File

@@ -88,70 +88,60 @@
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center">
<label for="name">Name</label>
<div class="col-span-2 ">
<input
readonly={!$session.isAdmin}
name="name"
id="name"
bind:value={database.name}
required
/>
</div>
<input
readonly={!$session.isAdmin}
name="name"
id="name"
bind:value={database.name}
required
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center">
<label for="destination">Destination</label>
<div class="col-span-2">
{#if database.destinationDockerId}
<div class="no-underline">
<input
value={database.destinationDocker.name}
id="destination"
disabled
readonly
class="bg-transparent "
/>
</div>
{/if}
</div>
{#if database.destinationDockerId}
<div class="no-underline">
<input
value={database.destinationDocker.name}
id="destination"
disabled
readonly
class="bg-transparent "
/>
</div>
{/if}
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center">
<label for="version">Version</label>
<div class="col-span-2 ">
<input value={database.version} readonly disabled class="bg-transparent " />
</div>
<input value={database.version} readonly disabled class="bg-transparent " />
</div>
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center">
<label for="host">Host</label>
<div class="col-span-2 ">
<CopyPasswordField
placeholder="Generated automatically after start"
isPasswordField={false}
readonly
disabled
id="host"
name="host"
value={database.id}
/>
</div>
<CopyPasswordField
placeholder="Generated automatically after start"
isPasswordField={false}
readonly
disabled
id="host"
name="host"
value={database.id}
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center">
<label for="publicPort">Port</label>
<div class="col-span-2">
<CopyPasswordField
placeholder="Generated automatically after start"
id="publicPort"
readonly
disabled
name="publicPort"
value={isPublic ? database.publicPort : privatePort}
/>
</div>
<CopyPasswordField
placeholder="Generated automatically after start"
id="publicPort"
readonly
disabled
name="publicPort"
value={isPublic ? database.publicPort : privatePort}
/>
</div>
</div>
<div class="grid grid-flow-row gap-2">
@@ -166,44 +156,42 @@
{:else if database.type === 'couchdb'}
<CouchDb bind:database />
{/if}
<div class="grid grid-cols-3 items-center px-10 pb-8">
<div class="grid grid-cols-2 items-center px-10 pb-8">
<label for="url">Connection String</label>
<div class="col-span-2 ">
<CopyPasswordField
textarea={true}
placeholder="Generated automatically after start"
isPasswordField={false}
id="url"
name="url"
readonly
disabled
value={databaseUrl}
/>
</div>
<CopyPasswordField
textarea={true}
placeholder="Generated automatically after start"
isPasswordField={false}
id="url"
name="url"
readonly
disabled
value={databaseUrl}
/>
</div>
</div>
</form>
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">Features</div>
</div>
<div class="px-4 pb-10 sm:px-6">
<ul class="mt-2 divide-y divide-stone-800">
<div class="px-10 pb-10">
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isPublic}
on:click={() => changeSettings('isPublic')}
title="Set it public"
description="Your database will be reachable over the internet. <br>Take security seriously in this case!"
/>
</ul>
</div>
{#if database.type === 'redis'}
<ul class="mt-2 divide-y divide-stone-800">
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={appendOnly}
on:click={() => changeSettings('appendOnly')}
title="Change append only mode"
description="Useful if you would like to restore redis data from a backup.<br><span class='font-bold text-white'>Database restart is required.</span>"
/>
</ul>
</div>
{/if}
</div>
</div>

View File

@@ -6,32 +6,28 @@
<div class="flex space-x-1 py-5 font-bold">
<div class="title">MongoDB</div>
</div>
<div class="px-10">
<div class="grid grid-cols-3 items-center">
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="rootUser">Root User</label>
<div class="col-span-2 ">
<CopyPasswordField
placeholder="Generated automatically after start"
id="rootUser"
readonly
disabled
name="rootUser"
value={database.rootUser}
/>
</div>
<CopyPasswordField
placeholder="Generated automatically after start"
id="rootUser"
readonly
disabled
name="rootUser"
value={database.rootUser}
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword">Root's Password</label>
<div class="col-span-2 ">
<CopyPasswordField
placeholder="Generated automatically after start"
isPasswordField={true}
readonly
disabled
id="rootUserPassword"
name="rootUserPassword"
value={database.rootUserPassword}
/>
</div>
<CopyPasswordField
placeholder="Generated automatically after start"
isPasswordField={true}
readonly
disabled
id="rootUserPassword"
name="rootUserPassword"
value={database.rootUserPassword}
/>
</div>
</div>

View File

@@ -6,73 +6,63 @@
<div class="flex space-x-1 py-5 font-bold">
<div class="title">MySQL</div>
</div>
<div class=" px-10">
<div class="grid grid-cols-3 items-center">
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase">Default Database</label>
<div class="col-span-2 ">
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="eg: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="eg: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center">
<label for="dbUser">User</label>
<div class="col-span-2 ">
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword">Password</label>
<div class="col-span-2 ">
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
value={database.dbUserPassword}
/>
</div>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
value={database.dbUserPassword}
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center">
<label for="rootUser">Root User</label>
<div class="col-span-2 ">
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="rootUser"
name="rootUser"
value={database.rootUser}
/>
</div>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="rootUser"
name="rootUser"
value={database.rootUser}
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword">Root's Password</label>
<div class="col-span-2 ">
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword"
name="rootUserPassword"
value={database.rootUserPassword}
/>
</div>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword"
name="rootUserPassword"
value={database.rootUserPassword}
/>
</div>
</div>

View File

@@ -6,46 +6,40 @@
<div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div>
</div>
<div class="px-10">
<div class="grid grid-cols-3 items-center">
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase">Default Database</label>
<div class="col-span-2 ">
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="eg: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="eg: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center">
<label for="dbUser">User</label>
<div class="col-span-2 ">
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword">Password</label>
<div class="col-span-2 ">
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
value={database.dbUserPassword}
/>
</div>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
value={database.dbUserPassword}
/>
</div>
</div>

View File

@@ -6,33 +6,18 @@
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Redis</div>
</div>
<div class="px-10">
<!-- <div class="grid grid-cols-3 items-center">
<label for="dbUser">User</label>
<div class="col-span-2 ">
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
id="dbUser"
name="dbUser"
bind:value={database.dbUser}
/>
</div>
</div> -->
<div class="grid grid-cols-3 items-center">
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword">Password</label>
<div class="col-span-2 ">
<CopyPasswordField
disabled
readonly
placeholder="Generated automatically after start"
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
value={database.dbUserPassword}
/>
</div>
<CopyPasswordField
disabled
readonly
placeholder="Generated automatically after start"
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
value={database.dbUserPassword}
/>
</div>
<!-- <div class="grid grid-cols-3 items-center">
<label for="rootUser">Root User</label>

View File

@@ -181,21 +181,18 @@
/>
</div>
</div>
<div class="flex justify-start">
<ul class="mt-2 divide-y divide-stone-800">
<Setting
disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
isPadding={false}
title="Use Coolify Proxy?"
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${
cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
: ''
}`}
/>
</ul>
<div class="grid grid-cols-2 items-center">
<Setting
disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
title="Use Coolify Proxy?"
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${
cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
: ''
}`}
/>
</div>
</form>
</div>

View File

@@ -24,7 +24,7 @@ export const post: RequestHandler = async (event) => {
await configureCoolifyProxyOn(fqdn);
await setWwwRedirection(fqdn);
const isHttps = fqdn.startsWith('https://');
if (isHttps) await forceSSLOnApplication({ domain });
if (isHttps) await forceSSLOnApplication(domain);
return {
status: 200
};

View File

@@ -1,7 +1,7 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, session }) => {
const url = `/index.json`;
const url = `/dashboard.json`;
const res = await fetch(url);
if (res.ok) {

View File

@@ -9,7 +9,7 @@
let emailEl;
let email, password;
if (browser && $session.uid) {
if (browser && $session.userId) {
goto('/');
}
onMount(() => {
@@ -34,7 +34,7 @@
</script>
<div class="flex h-screen flex-col items-center justify-center">
{#if $session.uid}
{#if $session.userId}
<div class="flex justify-center px-4 text-xl font-bold">Already logged in...</div>
{:else}
<div class="flex justify-center px-4">
@@ -67,6 +67,7 @@
class:text-stone-600={loading}
class:bg-coollabs={!loading}>{loading ? 'Authenticating...' : 'Login'}</button
>
<button on:click|preventDefault={() => goto('/reset')}>Reset password</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,26 @@
import type { RequestHandler } from '@sveltejs/kit';
import * as db from '$lib/database';
export const get: RequestHandler = async () => {
const users = await db.prisma.user.findMany({});
return {
status: 200,
body: {
users
}
};
};
export const post: RequestHandler = async (event) => {
const { secretKey } = await event.request.json();
if (secretKey !== process.env.COOLIFY_SECRET_KEY) {
return {
status: 500,
body: {
error: 'Invalid secret key.'
}
};
}
return {
status: 200
};
};

View File

@@ -0,0 +1,96 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { get, post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
let secretKey;
let password = false;
let users = [];
async function handleSubmit() {
try {
await post(`/reset.json`, { secretKey });
password = true;
const data = await get('/reset.json');
users = data.users;
return;
} catch ({ error }) {
return errorNotification(error);
}
}
async function resetPassword(user) {
try {
await post(`/reset/password.json`, { secretKey, user });
toast.push('Password reset done.');
return;
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="icons fixed top-0 left-0 m-3 cursor-pointer" on:click={() => goto('/')}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="5" y1="12" x2="19" y2="12" />
<line x1="5" y1="12" x2="11" y2="18" />
<line x1="5" y1="12" x2="11" y2="6" />
</svg>
</div>
<div class="pb-10 pt-24 text-center text-4xl font-bold">Reset Password</div>
<div class="flex items-center justify-center">
{#if password}
<table class="mx-2 text-left">
<thead class="mb-2">
<tr>
<th class="px-2">Email</th>
<th>New password</th>
</tr>
</thead>
<tbody>
{#each users as user}
<tr>
<td class="px-2">{user.email}</td>
<td class="flex space-x-2">
<input
id="newPassword"
name="newPassword"
bind:value={user.newPassword}
placeholder="Super secure new password"
/>
<button
class="mx-auto my-4 w-32 bg-coollabs hover:bg-coollabs-100"
on:click={() => resetPassword(user)}>Reset</button
></td
>
</tr>
{/each}
</tbody>
</table>
{:else}
<form class="flex flex-col" on:submit|preventDefault={handleSubmit}>
<div class="text-center text-2xl py-2 font-bold">Secret Key</div>
<CopyPasswordField
isPasswordField={true}
id="secretKey"
name="secretKey"
bind:value={secretKey}
placeholder="You can find it in ~/coolify/.env (COOLIFY_SECRET_KEY)"
/>
<button type="submit" class="bg-coollabs hover:bg-coollabs-100 mx-auto w-32 my-4"
>Submit</button
>
</form>
{/if}
</div>

View File

@@ -0,0 +1,27 @@
import type { RequestHandler } from '@sveltejs/kit';
import * as db from '$lib/database';
import { ErrorHandler, hashPassword } from '$lib/database';
export const post: RequestHandler = async (event) => {
const { secretKey, user } = await event.request.json();
if (secretKey !== process.env.COOLIFY_SECRET_KEY) {
return {
status: 500,
body: {
error: 'Invalid secret key.'
}
};
}
try {
const hashedPassword = await hashPassword(user.newPassword);
await db.prisma.user.update({
where: { email: user.email },
data: { password: hashedPassword }
});
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -7,42 +7,36 @@
<div class="flex space-x-1 py-5 font-bold">
<div class="title">MinIO Server</div>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center px-10">
<label for="rootUser">Root User</label>
<div class="col-span-2 ">
<input
name="rootUser"
id="rootUser"
placeholder="User to login"
value={service.minio.rootUser}
disabled
readonly
/>
</div>
<input
name="rootUser"
id="rootUser"
placeholder="User to login"
value={service.minio.rootUser}
disabled
readonly
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center px-10">
<label for="rootUserPassword">Root's Password</label>
<div class="col-span-2 ">
<CopyPasswordField
id="rootUserPassword"
isPasswordField
readonly
disabled
name="rootUserPassword"
value={service.minio.rootUserPassword}
/>
</div>
<CopyPasswordField
id="rootUserPassword"
isPasswordField
readonly
disabled
name="rootUserPassword"
value={service.minio.rootUserPassword}
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center px-10">
<label for="publicPort">API Port</label>
<div class="col-span-2 ">
<input
name="publicPort"
id="publicPort"
value={service.minio.publicPort}
disabled
readonly
placeholder="Generated automatically after start"
/>
</div>
<input
name="publicPort"
id="publicPort"
value={service.minio.publicPort}
disabled
readonly
placeholder="Generated automatically after start"
/>
</div>

View File

@@ -7,86 +7,74 @@
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Plausible Analytics</div>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center px-10">
<label for="email">Email Address</label>
<div class="col-span-2">
<input
name="email"
id="email"
disabled={readOnly}
readonly={readOnly}
placeholder="Email address"
bind:value={service.plausibleAnalytics.email}
required
/>
</div>
<input
name="email"
id="email"
disabled={readOnly}
readonly={readOnly}
placeholder="Email address"
bind:value={service.plausibleAnalytics.email}
required
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center px-10">
<label for="username">Username</label>
<div class="col-span-2">
<CopyPasswordField
name="username"
id="username"
disabled={readOnly}
readonly={readOnly}
placeholder="User to login"
bind:value={service.plausibleAnalytics.username}
required
/>
</div>
<CopyPasswordField
name="username"
id="username"
disabled={readOnly}
readonly={readOnly}
placeholder="User to login"
bind:value={service.plausibleAnalytics.username}
required
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center px-10">
<label for="password">Password</label>
<div class="col-span-2 ">
<CopyPasswordField
id="password"
isPasswordField
readonly
disabled
name="password"
value={service.plausibleAnalytics.password}
/>
</div>
<CopyPasswordField
id="password"
isPasswordField
readonly
disabled
name="password"
value={service.plausibleAnalytics.password}
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlUser">Username</label>
<div class="col-span-2 ">
<CopyPasswordField
name="postgresqlUser"
id="postgresqlUser"
value={service.plausibleAnalytics.postgresqlUser}
readonly
disabled
/>
</div>
<CopyPasswordField
name="postgresqlUser"
id="postgresqlUser"
value={service.plausibleAnalytics.postgresqlUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlPassword">Password</label>
<div class="col-span-2 ">
<CopyPasswordField
id="postgresqlPassword"
isPasswordField
readonly
disabled
name="postgresqlPassword"
value={service.plausibleAnalytics.postgresqlPassword}
/>
</div>
<CopyPasswordField
id="postgresqlPassword"
isPasswordField
readonly
disabled
name="postgresqlPassword"
value={service.plausibleAnalytics.postgresqlPassword}
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlDatabase">Database</label>
<div class="col-span-2 ">
<CopyPasswordField
name="postgresqlDatabase"
id="postgresqlDatabase"
value={service.plausibleAnalytics.postgresqlDatabase}
readonly
disabled
/>
</div>
<CopyPasswordField
name="postgresqlDatabase"
id="postgresqlDatabase"
value={service.plausibleAnalytics.postgresqlDatabase}
readonly
disabled
/>
</div>
<!-- <div class="grid grid-cols-3 items-center">
<label for="postgresqlPublicPort">Public Port</label>

View File

@@ -7,6 +7,7 @@
import { post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
import MinIo from './_MinIO.svelte';
@@ -18,6 +19,7 @@
let loading = false;
let loadingVerification = false;
let dualCerts = service.dualCerts;
async function handleSubmit() {
loading = true;
@@ -42,6 +44,17 @@
loadingVerification = false;
}
}
async function changeSettings(name) {
try {
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
await post(`/services/${id}/settings.json`, { dualCerts });
return toast.push('Settings saved.');
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="mx-auto max-w-4xl px-6">
@@ -67,10 +80,10 @@
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="mt-2 grid grid-cols-3 items-center">
<div class="grid grid-flow-row gap-2">
<div class="mt-2 grid grid-cols-2 items-center px-10">
<label for="name">Name</label>
<div class="col-span-2 ">
<div>
<input
readonly={!$session.isAdmin}
name="name"
@@ -81,9 +94,9 @@
</div>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center px-10">
<label for="destination">Destination</label>
<div class="col-span-2">
<div>
{#if service.destinationDockerId}
<div class="no-underline">
<input
@@ -96,9 +109,9 @@
{/if}
</div>
</div>
<div class="grid grid-cols-3">
<div class="grid grid-cols-2 px-10">
<label for="fqdn" class="pt-2">Domain (FQDN)</label>
<div class="col-span-2 ">
<div>
<CopyPasswordField
placeholder="eg: https://analytics.coollabs.io"
readonly={!$session.isAdmin && !isRunning}
@@ -114,6 +127,16 @@
/>
</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<Setting
disabled={isRunning}
dataTooltip="Must be stopped to modify."
bind:setting={dualCerts}
title="Generate SSL for www and non-www?"
description="It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-pink-600'>both DNS entries</span> set in advance.<br><br>Service needs to be restarted."
on:click={() => !isRunning && changeSettings('dualCerts')}
/>
</div>
{#if service.type === 'plausibleanalytics'}
<PlausibleAnalytics bind:service {readOnly} />
{:else if service.type === 'minio'}

View File

@@ -7,16 +7,14 @@
<div class="flex space-x-1 py-5 font-bold">
<div class="title">VSCode Server</div>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center px-10">
<label for="password">Password</label>
<div class="col-span-2 ">
<CopyPasswordField
id="password"
isPasswordField
readonly
disabled
name="password"
value={service.vscodeserver.password}
/>
</div>
<CopyPasswordField
id="password"
isPasswordField
readonly
disabled
name="password"
value={service.vscodeserver.password}
/>
</div>

View File

@@ -10,85 +10,73 @@
<div class="title">Wordpress</div>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center px-10">
<label for="extraConfig">Extra Config</label>
<div class="col-span-2 ">
<textarea
disabled={isRunning}
readonly={isRunning}
class:resize-none={isRunning}
rows={isRunning ? 1 : 5}
name="extraConfig"
id="extraConfig"
placeholder={!isRunning
? `eg:
<textarea
disabled={isRunning}
readonly={isRunning}
class:resize-none={isRunning}
rows={isRunning ? 1 : 5}
name="extraConfig"
id="extraConfig"
placeholder={!isRunning
? `eg:
define('WP_ALLOW_MULTISITE', true);
define('MULTISITE', true);
define('SUBDOMAIN_INSTALL', false);`
: null}>{service.wordpress.extraConfig}</textarea
>
</div>
: null}>{service.wordpress.extraConfig || 'N/A'}</textarea
>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">MySQL</div>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center px-10">
<label for="mysqlDatabase">Database</label>
<div class="col-span-2 ">
<input
name="mysqlDatabase"
id="mysqlDatabase"
required
readonly={readOnly}
disabled={readOnly}
bind:value={service.wordpress.mysqlDatabase}
placeholder="eg: wordpress_db"
/>
</div>
<input
name="mysqlDatabase"
id="mysqlDatabase"
required
readonly={readOnly}
disabled={readOnly}
bind:value={service.wordpress.mysqlDatabase}
placeholder="eg: wordpress_db"
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center px-10">
<label for="mysqlRootUser">Root User</label>
<div class="col-span-2 ">
<input
name="mysqlRootUser"
id="mysqlRootUser"
placeholder="MySQL Root User"
value={service.wordpress.mysqlRootUser}
disabled
readonly
/>
</div>
<input
name="mysqlRootUser"
id="mysqlRootUser"
placeholder="MySQL Root User"
value={service.wordpress.mysqlRootUser}
disabled
readonly
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center px-10">
<label for="mysqlRootUserPassword">Root's Password</label>
<div class="col-span-2 ">
<CopyPasswordField
id="mysqlRootUserPassword"
isPasswordField
readonly
disabled
name="mysqlRootUserPassword"
value={service.wordpress.mysqlRootUserPassword}
/>
</div>
<CopyPasswordField
id="mysqlRootUserPassword"
isPasswordField
readonly
disabled
name="mysqlRootUserPassword"
value={service.wordpress.mysqlRootUserPassword}
/>
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center px-10">
<label for="mysqlUser">User</label>
<div class="col-span-2 ">
<input name="mysqlUser" id="mysqlUser" value={service.wordpress.mysqlUser} disabled readonly />
</div>
<input name="mysqlUser" id="mysqlUser" value={service.wordpress.mysqlUser} disabled readonly />
</div>
<div class="grid grid-cols-3 items-center">
<div class="grid grid-cols-2 items-center px-10">
<label for="mysqlPassword">Password</label>
<div class="col-span-2 ">
<CopyPasswordField
id="mysqlPassword"
isPasswordField
readonly
disabled
name="mysqlPassword"
value={service.wordpress.mysqlPassword}
/>
</div>
<CopyPasswordField
id="mysqlPassword"
isPasswordField
readonly
disabled
name="mysqlPassword"
value={service.wordpress.mysqlPassword}
/>
</div>

View File

@@ -17,7 +17,7 @@ export const post: RequestHandler = async (event) => {
return {
status: found ? 500 : 200,
body: {
error: found && `Domain ${getDomain(fqdn)} is already configured`
error: found && `Domain ${getDomain(fqdn).replace('www.', '')} is already configured`
}
};
} catch (error) {

View File

@@ -30,7 +30,7 @@ export const post: RequestHandler = async (event) => {
const { version } = await event.request.json();
try {
await db.setService({ id, version });
await db.setServiceVersion({ id, version });
return {
status: 201
};

View File

@@ -35,7 +35,7 @@ export const post: RequestHandler = async (event) => {
}
try {
await stopTcpHttpProxy(destinationDocker, publicPort);
await configureSimpleServiceProxyOff({ domain });
await configureSimpleServiceProxyOff(fqdn);
} catch (error) {
console.log(error);
}

View File

@@ -28,7 +28,7 @@ export const post: RequestHandler = async (event) => {
console.error(error);
}
try {
await configureSimpleServiceProxyOff({ domain });
await configureSimpleServiceProxyOff(fqdn);
} catch (error) {
console.log(error);
}

View File

@@ -60,7 +60,7 @@ export const post: RequestHandler = async (event) => {
}
},
postgresql: {
volume: `${plausibleDbId}-postgresql-data:/var/lib/postgresql/data`,
volume: `${plausibleDbId}-postgresql-data:/bitnami/postgresql/`,
image: 'bitnami/postgresql:13.2.0',
environmentVariables: {
POSTGRESQL_PASSWORD: postgresqlPassword,
@@ -136,7 +136,6 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
'sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run"',
networks: [network],
environment: config.plausibleAnalytics.environmentVariables,
volumes: [config.postgresql.volume],
restart: 'always',
depends_on: [`${id}-postgresql`, `${id}-clickhouse`],
labels: makeLabelForServices('plausibleAnalytics')

View File

@@ -38,7 +38,7 @@ export const post: RequestHandler = async (event) => {
}
try {
await configureSimpleServiceProxyOff({ domain });
await configureSimpleServiceProxyOff(fqdn);
} catch (error) {
console.log(error);
}

View File

@@ -0,0 +1,19 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
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;
const { dualCerts } = await event.request.json();
try {
await db.setServiceSettings({ id, dualCerts });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -28,7 +28,7 @@ export const post: RequestHandler = async (event) => {
console.error(error);
}
try {
await configureSimpleServiceProxyOff({ domain });
await configureSimpleServiceProxyOff(fqdn);
} catch (error) {
console.log(error);
}

View File

@@ -28,7 +28,7 @@ export const post: RequestHandler = async (event) => {
console.error(error);
}
try {
await configureSimpleServiceProxyOff({ domain });
await configureSimpleServiceProxyOff(fqdn);
} catch (error) {
console.log(error);
}

View File

@@ -31,7 +31,7 @@ export const post: RequestHandler = async (event) => {
console.error(error);
}
try {
await configureSimpleServiceProxyOff({ domain });
await configureSimpleServiceProxyOff(fqdn);
} catch (error) {
console.log(error);
}

View File

@@ -16,7 +16,7 @@ export const post: RequestHandler = async (event) => {
return {
status: found ? 500 : 200,
body: {
error: found && `Domain ${fqdn} is already configured`
error: found && `Domain ${fqdn.replace('www.', '')} is already configured`
}
};
} catch (error) {

View File

@@ -3,10 +3,8 @@ import { getDomain, getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { listSettings, ErrorHandler } from '$lib/database';
import {
checkContainer,
configureCoolifyProxyOff,
configureCoolifyProxyOn,
forceSSLOffApplication,
forceSSLOnApplication,
reloadHaproxy,
removeWwwRedirection,
@@ -15,6 +13,7 @@ import {
} from '$lib/haproxy';
import { letsEncrypt } from '$lib/letsencrypt';
import type { RequestHandler } from '@sveltejs/kit';
import { promises as dns } from 'dns';
export const get: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
@@ -45,14 +44,24 @@ export const del: RequestHandler = async (event) => {
if (status === 401) return { status, body };
const { fqdn } = await event.request.json();
let ip;
console.log(fqdn);
try {
ip = await dns.resolve(fqdn);
} catch (error) {
// Do not care.
}
try {
const domain = getDomain(fqdn);
await db.prisma.setting.update({ where: { fqdn }, data: { fqdn: null } });
await configureCoolifyProxyOff(fqdn);
await removeWwwRedirection(domain);
return {
status: 201
status: 200,
body: {
message: 'Domain removed',
redirect: ip ? `http://${ip[0]}:3000/settings` : undefined
}
};
} catch (error) {
return ErrorHandler(error);
@@ -69,16 +78,20 @@ export const post: RequestHandler = async (event) => {
};
if (status === 401) return { status, body };
const { fqdn, isRegistrationEnabled } = await event.request.json();
const { fqdn, isRegistrationEnabled, dualCerts } = await event.request.json();
try {
const {
id,
fqdn: oldFqdn,
isRegistrationEnabled: oldIsRegistrationEnabled
isRegistrationEnabled: oldIsRegistrationEnabled,
dualCerts: oldDualCerts
} = await db.listSettings();
if (oldIsRegistrationEnabled !== isRegistrationEnabled) {
await db.prisma.setting.update({ where: { id }, data: { isRegistrationEnabled } });
}
if (oldDualCerts !== dualCerts) {
await db.prisma.setting.update({ where: { id }, data: { dualCerts } });
}
if (oldFqdn && oldFqdn !== fqdn) {
if (oldFqdn) {
const oldDomain = getDomain(oldFqdn);
@@ -93,9 +106,9 @@ export const post: RequestHandler = async (event) => {
if (domain) {
await configureCoolifyProxyOn(fqdn);
await setWwwRedirection(fqdn);
if (isHttps && !dev) {
if (isHttps) {
await letsEncrypt({ domain, isCoolify: true });
await forceSSLOnApplication({ domain });
await forceSSLOnApplication(domain);
await reloadHaproxy('/var/run/docker.sock');
}
}

View File

@@ -30,10 +30,13 @@
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { browser } from '$app/env';
import { getDomain } from '$lib/components/common';
import { toast } from '@zerodevx/svelte-toast';
let isRegistrationEnabled = settings.isRegistrationEnabled;
let dualCerts = settings.dualCerts;
let fqdn = settings.fqdn;
let isFqdnSet = settings.fqdn;
let isFqdnSet = !!settings.fqdn;
let loading = {
save: false,
remove: false
@@ -43,8 +46,8 @@
if (fqdn) {
loading.remove = true;
try {
await del(`/settings.json`, { fqdn });
return window.location.reload();
const { redirect } = await del(`/settings.json`, { fqdn });
return redirect ? window.location.replace(redirect) : window.location.reload();
} catch ({ error }) {
return errorNotification(error);
} finally {
@@ -57,7 +60,11 @@
if (name === 'isRegistrationEnabled') {
isRegistrationEnabled = !isRegistrationEnabled;
}
return await post(`/settings.json`, { isRegistrationEnabled });
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
await post(`/settings.json`, { isRegistrationEnabled, dualCerts });
return toast.push('Settings saved.');
} catch ({ error }) {
return errorNotification(error);
}
@@ -82,15 +89,15 @@
<div class="mr-4 text-2xl tracking-tight">Settings</div>
</div>
{#if $session.teamId === '0'}
<div class="mx-auto max-w-2xl">
<div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmit}>
<div class="flex space-x-1 p-6 font-bold">
<div class="flex space-x-1 py-6 font-bold">
<div class="title">Global Settings</div>
<button
type="submit"
disabled={loading.save}
class:bg-green-600={!loading.save}
class:hover:bg-green-500={!loading.save}
class:bg-yellow-500={!loading.save}
class:hover:bg-yellow-400={!loading.save}
class="mx-2 ">{loading.save ? 'Saving...' : 'Save'}</button
>
{#if isFqdnSet}
@@ -103,10 +110,10 @@
>
{/if}
</div>
<div class="px-4 sm:px-6">
<div class="flex space-x-4 py-4 px-4">
<p class="pt-2 text-base font-bold text-stone-100">Domain (FQDN)</p>
<div class="justify-center">
<div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-2 items-start">
<div class="pt-2 text-base font-bold text-stone-100">Domain (FQDN)</div>
<div class="justify-start text-left">
<input
bind:value={fqdn}
readonly={!$session.isAdmin || isFqdnSet}
@@ -118,57 +125,61 @@
required
/>
<Explainer
text="If you specify <span class='text-green-600 font-bold'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-green-600 font-bold'>www</span>, Coolify will be redirected (302) from non-www and vice versa."
text="If you specify <span class='text-yellow-500 font-bold'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-yellow-500 font-bold'>www</span>, Coolify will be redirected (302) from non-www and vice versa."
/>
</div>
</div>
<ul class="mt-2 divide-y divide-stone-800">
<div class="grid grid-cols-2 items-center">
<Setting
dataTooltip="Must remove the domain before you can change this setting."
disabled={isFqdnSet}
bind:setting={dualCerts}
title="Generate SSL for www and non-www?"
description="It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-yellow-400'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both."
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isRegistrationEnabled}
title="Registration allowed?"
description="Allow further registrations to the application. <br>It's turned off after the first registration. "
on:click={() => changeSettings('isRegistrationEnabled')}
/>
</ul>
</div>
</div>
</form>
<div class="mx-auto max-w-4xl px-6">
<div class="flex space-x-1 pt-5 font-bold">
<div class="title">HAProxy Settings</div>
</div>
<Explainer
text={`Credentials for <a class="text-white font-bold" href=${
fqdn
? 'http://' + getDomain(fqdn) + ':8404'
: browser && 'http://' + window.location.hostname + ':8404'
} target="_blank">stats</a> page.`}
/>
<div class="grid grid-cols-3 items-center px-4 pt-5">
<div class="flex space-x-1 pt-6 font-bold">
<div class="title">Coolify Proxy Settings</div>
</div>
<Explainer
text={`Credentials for <a class="text-white font-bold" href=${
fqdn
? 'http://' + getDomain(fqdn) + ':8404'
: browser && 'http://' + window.location.hostname + ':8404'
} target="_blank">stats</a> page.`}
/>
<div class="space-y-2 px-10 py-5">
<div class="grid grid-cols-2 items-center">
<label for="proxyUser">User</label>
<div class="col-span-2 ">
<CopyPasswordField
readonly
disabled
id="proxyUser"
name="proxyUser"
value={settings.proxyUser}
/>
</div>
<CopyPasswordField
readonly
disabled
id="proxyUser"
name="proxyUser"
value={settings.proxyUser}
/>
</div>
<div class="grid grid-cols-3 items-center px-4">
<div class="grid grid-cols-2 items-center">
<label for="proxyPassword">Password</label>
<div class="col-span-2 ">
<CopyPasswordField
readonly
disabled
id="proxyPassword"
name="proxyPassword"
isPasswordField
value={settings.proxyPassword}
/>
</div>
<CopyPasswordField
readonly
disabled
id="proxyPassword"
name="proxyPassword"
isPasswordField
value={settings.proxyPassword}
/>
</div>
</div>
</div>

View File

@@ -5,6 +5,7 @@
import { page, session } from '$app/stores';
import { onMount } from 'svelte';
import { post } from '$lib/api';
import { browser } from '$app/env';
const { id } = $page.params;
let loading = false;
@@ -120,13 +121,17 @@
</div>
<Explainer
maxWidthClass="w-full"
text="<span class='font-bold text-base'>Scopes required:</span>
<br>- api (Access the authenticated user's API)
<br>- read_repository (Allows read-only access to the repository)
<br>- email (Allows read-only access to the user's primary email address using OpenID Connect)
customClass="w-full"
text="<span class='font-bold text-base text-white'>Scopes required:</span>
<br>- <span class='text-orange-500 font-bold'>api</span> (Access the authenticated user's API)
<br>- <span class='text-orange-500 font-bold'>read_repository</span> (Allows read-only access to the repository)
<br>- <span class='text-orange-500 font-bold'>email</span> (Allows read-only access to the user's primary email address using OpenID Connect)
<br>
<br>For extra security, you can add Expire access tokens!"
<br>For extra security, you can set Expire access tokens!
<br><br>Webhook URL: <span class='text-orange-500 font-bold'>{browser
? window.location.origin
: ''}/webhooks/gitlab</span>
<br>But if you will set a custom domain name for Coolify, use that instead."
/>
</form>
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4 pt-10">

View File

@@ -22,21 +22,20 @@
<script lang="ts">
export let permissions;
export let team;
export let invitations;
export let invitations: any[];
import { page, session } from '$app/stores';
import Explainer from '$lib/components/Explainer.svelte';
import { errorNotification } from '$lib/form';
import { post } from '$lib/api';
const { id } = $page.params;
let invitation = {
teamName: team.name,
email: null,
permission: 'read'
};
let myPermission = permissions.find((u) => u.user.id === $session.uid).permission;
function isAdmin(permission = myPermission) {
if (myPermission === 'admin' || myPermission === 'owner') {
// let myPermission = permissions.find((u) => u.user.id === $session.userId).permission;
function isAdmin(permission: string) {
if (permission === 'admin' || permission === 'owner') {
return true;
}
@@ -56,7 +55,7 @@
return errorNotification(error);
}
}
async function revokeInvitation(id) {
async function revokeInvitation(id: string) {
try {
await post(`/teams/${id}/invitation/revoke.json`, { id });
return window.location.reload();
@@ -64,7 +63,7 @@
return errorNotification(error);
}
}
async function removeFromTeam(uid) {
async function removeFromTeam(uid: string) {
try {
await post(`/teams/${id}/remove/user.json`, { teamId: team.id, uid });
return window.location.reload();
@@ -72,7 +71,7 @@
return errorNotification(error);
}
}
async function changePermission(userId, permissionId, currentPermission) {
async function changePermission(userId: string, permissionId: string, currentPermission: string) {
let newPermission = 'read';
if (currentPermission === 'read') {
newPermission = 'admin';
@@ -99,7 +98,7 @@
<span class="arrow-right-applications px-1 text-cyan-500">></span>
<span class="pr-2">{team.name}</span>
</div>
<div class="mx-auto max-w-2xl">
<div class="mx-auto max-w-4xl">
<form on:submit|preventDefault={handleSubmit}>
<div class="flex space-x-1 p-6 font-bold">
<div class="title">Settings</div>
@@ -113,10 +112,10 @@
<input id="name" name="name" placeholder="name" bind:value={team.name} />
</div>
{#if team.id === '0'}
<div class="px-20 pt-4 text-center">
<div class="px-8 pt-4 text-left">
<Explainer
maxWidthClass="w-full"
text="This is the <span class='text-red-500 font-bold'>root</span> team. <br><br>That means members of this group can manage instance wide settings and have all the priviliges in Coolify. (imagine like root user on Linux)"
customClass="w-full"
text="This is the <span class='text-red-500 font-bold'>root</span> team. That means members of this group can manage instance wide settings and have all the priviliges in Coolify (imagine like root user on Linux)."
/>
</div>
{/if}
@@ -136,10 +135,11 @@
<tr class="text-xs">
<td class="py-4"
>{permission.user.email}
<span class="font-bold">{permission.user.id === $session.uid ? '(You)' : ''}</span></td
<span class="font-bold">{permission.user.id === $session.userId ? '(You)' : ''}</span
></td
>
<td class="py-4">{permission.permission}</td>
{#if $session.isAdmin && permission.user.id !== $session.uid && permission.permission !== 'owner'}
{#if $session.isAdmin && permission.user.id !== $session.userId && permission.permission !== 'owner'}
<td class="flex flex-col items-center justify-center space-y-2 py-4 text-center">
<button
class="w-52 bg-red-600 hover:bg-red-500"
@@ -178,11 +178,19 @@
</div>
</div>
{#if $session.isAdmin}
<div class="mx-auto max-w-2xl pt-8">
<div class="mx-auto max-w-4xl pt-8">
<form on:submit|preventDefault={sendInvitation}>
<div class="flex space-x-1 p-6 font-bold">
<div class="title">Invite new member</div>
<div class="text-center">
<div class="flex space-x-1 p-6">
<div>
<div class="title font-bold">Invite new member</div>
<div class="text-left">
<Explainer
customClass="w-56"
text="You can only invite registered users at the moment - will be extended soon."
/>
</div>
</div>
<div class="pt-1 text-center">
<button class="bg-cyan-600 hover:bg-cyan-500" type="submit">Send invitation</button>
</div>
</div>

View File

@@ -144,10 +144,17 @@ export const post: RequestHandler = async (event) => {
} else if (pullmergeRequestAction === 'closed') {
if (applicationFound.destinationDockerId) {
const domain = getDomain(applicationFound.fqdn);
const isHttps = applicationFound.fqdn.startsWith('https://');
const isWWW = applicationFound.fqdn.includes('www.');
const fqdn = `${isHttps ? 'https://' : 'http://'}${
isWWW ? 'www.' : ''
}${pullmergeRequestId}.${domain}`;
const id = `${applicationFound.id}-${pullmergeRequestId}`;
const engine = applicationFound.destinationDocker.engine;
await removeDestinationDocker({ id, engine });
await removeProxyConfiguration({ domain: `${pullmergeRequestId}.${domain}` });
await removeProxyConfiguration(fqdn);
}
return {
status: 200,

View File

@@ -141,10 +141,17 @@ export const post: RequestHandler = async (event) => {
} else if (action === 'close') {
if (applicationFound.destinationDockerId) {
const domain = getDomain(applicationFound.fqdn);
const isHttps = applicationFound.fqdn.startsWith('https://');
const isWWW = applicationFound.fqdn.includes('www.');
const fqdn = `${isHttps ? 'https://' : 'http://'}${
isWWW ? 'www.' : ''
}${pullmergeRequestId}.${domain}`;
const id = `${applicationFound.id}-${pullmergeRequestId}`;
const engine = applicationFound.destinationDocker.engine;
await removeProxyConfiguration({ domain: `${pullmergeRequestId}.${domain}` });
await removeDestinationDocker({ id, engine });
await removeProxyConfiguration(fqdn);
}
return {

View File

@@ -21,8 +21,8 @@ export const get: RequestHandler = async (event) => {
const code = event.url.searchParams.get('code');
const state = event.url.searchParams.get('state');
try {
const { fqdn } = await db.listSettings();
const application = await db.getApplication({ id: state, teamId });
const { fqdn } = application;
const { appId, appSecret } = application.gitSource.gitlabApp;
const { htmlUrl } = application.gitSource;

View File

@@ -22,10 +22,10 @@
}
html {
@apply h-full min-h-full;
@apply h-full min-h-full overflow-y-scroll;
}
body {
@apply min-h-screen overflow-x-hidden bg-coolblack text-sm text-white;
@apply min-h-screen overflow-x-hidden bg-coolblack text-sm text-white scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200;
}
main,
@@ -35,10 +35,10 @@ main,
}
input {
@apply h-12 w-96 select-all rounded border border-transparent bg-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:bg-transparent md:text-sm;
@apply h-12 w-96 rounded border border-transparent bg-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:border disabled:border-dashed disabled:border-coolgray-300 disabled:bg-transparent md:text-sm;
}
textarea {
@apply w-96 select-all rounded border border-transparent bg-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:bg-transparent md:text-sm;
@apply w-96 rounded border border-transparent bg-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:border disabled:border-dashed disabled:border-coolgray-300 disabled:bg-transparent md:text-sm;
}
select {