Compare commits

...

43 Commits

Author SHA1 Message Date
Andras Bacsai
c370fba9ba Merge pull request #185 from coollabsio/next
v2.0.20
2022-02-23 13:00:08 +01:00
Andras Bacsai
6e32421172 chore: Version++ 2022-02-23 12:58:46 +01:00
Andras Bacsai
6643687c0a fix: Revert default network 2022-02-23 12:58:32 +01:00
Andras Bacsai
ed01e78d77 improvement: dns check 2022-02-23 12:43:04 +01:00
Andras Bacsai
93aed52f88 Login page description for demo page 2022-02-23 11:14:04 +01:00
Andras Bacsai
bb6d1fd6a3 cleanup 2022-02-23 10:40:34 +01:00
Andras Bacsai
6e33179fc2 Update README.md 2022-02-23 10:40:21 +01:00
Andras Bacsai
277fd167cf Merge pull request #184 from coollabsio/next
v2.0.19
2022-02-23 10:29:10 +01:00
Andras Bacsai
98e8d5170b fix: Settings fqdn grr 2022-02-23 10:26:29 +01:00
Andras Bacsai
11ee1651ae fix: Random network name for demo 2022-02-23 10:22:25 +01:00
Andras Bacsai
0dfcf9b1e6 Merge pull request #159 from coollabsio/next
v2.0.18
2022-02-22 20:41:53 +01:00
Andras Bacsai
08f57ac5bc UI fix 2022-02-22 20:41:07 +01:00
Andras Bacsai
7095e781e9 small fixes 2022-02-22 20:37:11 +01:00
Andras Bacsai
df18b93809 Design day! 2022-02-22 12:56:58 +01:00
Andras Bacsai
0c2e028b38 Frontend for port range 2022-02-22 10:35:39 +01:00
Andras Bacsai
80cb1bc129 fix: Use normal docker-compose in dev 2022-02-22 09:54:23 +01:00
Andras Bacsai
74c1cb51f6 nothing here 2022-02-22 09:49:17 +01:00
Andras Bacsai
2e864bddf9 nothing important 2022-02-22 09:47:21 +01:00
Andras Bacsai
e60ae91b5d design: make copy/password visible 2022-02-22 09:45:00 +01:00
Andras Bacsai
d606cd86a0 feat: Ports range 2022-02-22 09:23:41 +01:00
Andras Bacsai
bc463c37f4 fix: Lowercase email everywhere 2022-02-22 08:12:45 +01:00
Andras Bacsai
76c1480903 fix: Email is lowercased in login 2022-02-22 08:10:33 +01:00
Andras Bacsai
6f312caf8b chore: Version++ 2022-02-21 12:46:37 +01:00
Andras Bacsai
980d8d374f Merge branch 'main' into next 2022-02-21 12:46:08 +01:00
Andras Bacsai
c49b34942f Merge pull request #164 from coollabsio/fix/github-token
v2.0.17
2022-02-21 12:41:36 +01:00
Andras Bacsai
fcfa8717a5 fix: Move tokens from session to cookie/store 2022-02-21 12:35:20 +01:00
Andras Bacsai
954a265965 chore: Version++ 2022-02-21 09:52:51 +01:00
Andras Bacsai
69845a020a Merge branch 'main' into fix/github-token 2022-02-21 09:51:46 +01:00
Andras Bacsai
22200fd8a7 fix: Github token 2022-02-21 09:50:15 +01:00
Andras Bacsai
add441675d feat: Public port range (WIP) 2022-02-20 15:12:01 +01:00
Andras Bacsai
d3d9754277 chore: Version ++ 2022-02-20 14:42:24 +01:00
Andras Bacsai
aa5e2edbc5 feat: Scan for lock files and set right commands 2022-02-20 14:40:15 +01:00
Andras Bacsai
310b099ecf Merge pull request #154 from coollabsio/next
v2.0.16
2022-02-20 00:48:08 +01:00
Andras Bacsai
1cfaef911c Small fixes 2022-02-20 00:17:44 +01:00
Andras Bacsai
b931c5f638 Migration file 2022-02-20 00:13:16 +01:00
Andras Bacsai
7c683668eb feat: Secrets for previews
UI: Some CSS changes
2022-02-20 00:00:31 +01:00
Andras Bacsai
cab7ac7d58 fix: If DNS not found, do not redirect 2022-02-19 22:37:45 +01:00
Andras Bacsai
15e69c538a feat: Preview secrets
chore: version++
2022-02-19 14:54:47 +01:00
Andras Bacsai
31ee938b66 Merge pull request #152 from coollabsio/next
v2.0.15
2022-02-19 14:52:35 +01:00
Andras Bacsai
e51a8d43d9 Browser 2022-02-19 14:03:33 +01:00
Andras Bacsai
64cd5b6e4b fix: Gitlab webhooks fixed 2022-02-19 13:57:56 +01:00
Andras Bacsai
6c9ef34905 chore: version++ 2022-02-18 23:25:43 +01:00
Andras Bacsai
aa89019236 fix: Database connection strings 2022-02-18 23:25:24 +01:00
72 changed files with 1433 additions and 984 deletions

View File

@@ -2,6 +2,12 @@
An open-source & self-hostable Heroku / Netlify alternative. An open-source & self-hostable Heroku / Netlify alternative.
## Demo instance
https://demo.coolify.io/
(If it is unresponsible, that means someone overloaded the server. 🙃)
## Installation ## Installation
Installation is automated with the following command: Installation is automated with the following command:

View File

@@ -1,12 +1,12 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.0.14", "version": "2.0.20",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"dev": "docker compose -f docker-compose-dev.yaml up -d && NODE_ENV=development svelte-kit dev --host 0.0.0.0", "dev": "docker-compose -f docker-compose-dev.yaml up -d && NODE_ENV=development svelte-kit dev --host 0.0.0.0",
"dev:stop": "docker compose -f docker-compose-dev.yaml down", "dev:stop": "docker-compose -f docker-compose-dev.yaml down",
"dev:logs": "docker compose -f docker-compose-dev.yaml logs -f --tail 10", "dev:logs": "docker-compose -f docker-compose-dev.yaml logs -f --tail 10",
"studio": "npx prisma studio", "studio": "npx prisma studio",
"start": "npx prisma migrate deploy && npx prisma generate && npx prisma db seed && node index.js", "start": "npx prisma migrate deploy && npx prisma generate && npx prisma db seed && node index.js",
"build": "svelte-kit build", "build": "svelte-kit build",

View File

@@ -0,0 +1,19 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Secret" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"value" TEXT NOT NULL,
"isPRMRSecret" BOOLEAN NOT NULL DEFAULT false,
"isBuildSecret" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"applicationId" TEXT NOT NULL,
CONSTRAINT "Secret_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Secret" ("applicationId", "createdAt", "id", "isBuildSecret", "name", "updatedAt", "value") SELECT "applicationId", "createdAt", "id", "isBuildSecret", "name", "updatedAt", "value" FROM "Secret";
DROP TABLE "Secret";
ALTER TABLE "new_Secret" RENAME TO "Secret";
CREATE UNIQUE INDEX "Secret_name_applicationId_isPRMRSecret_key" ON "Secret"("name", "applicationId", "isPRMRSecret");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,20 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Setting" (
"id" TEXT NOT NULL PRIMARY KEY,
"fqdn" TEXT,
"isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"minPort" INTEGER NOT NULL DEFAULT 9000,
"maxPort" INTEGER NOT NULL DEFAULT 9100,
"proxyPassword" TEXT NOT NULL,
"proxyUser" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Setting" ("createdAt", "dualCerts", "fqdn", "id", "isRegistrationEnabled", "proxyPassword", "proxyUser", "updatedAt") SELECT "createdAt", "dualCerts", "fqdn", "id", "isRegistrationEnabled", "proxyPassword", "proxyUser", "updatedAt" FROM "Setting";
DROP TABLE "Setting";
ALTER TABLE "new_Setting" RENAME TO "Setting";
CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -12,6 +12,8 @@ model Setting {
fqdn String? @unique fqdn String? @unique
isRegistrationEnabled Boolean @default(false) isRegistrationEnabled Boolean @default(false)
dualCerts Boolean @default(false) dualCerts Boolean @default(false)
minPort Int @default(9000)
maxPort Int @default(9100)
proxyPassword String proxyPassword String
proxyUser String proxyUser String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -109,13 +111,14 @@ model Secret {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
value String value String
isPRMRSecret Boolean @default(false)
isBuildSecret Boolean @default(false) isBuildSecret Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])
applicationId String applicationId String
@@unique([name, applicationId]) @@unique([name, applicationId, isPRMRSecret])
} }
model BuildLog { model BuildLog {

View File

@@ -17,8 +17,6 @@ export const handle = handleSession(
let response; let response;
try { try {
if (event.locals.cookies) { if (event.locals.cookies) {
let gitlabToken = event.locals.cookies.gitlabToken || null;
let ghToken = event.locals.cookies.ghToken;
if (event.locals.cookies['kit.session']) { if (event.locals.cookies['kit.session']) {
const { permission, teamId, userId } = await getUserDetails(event, false); const { permission, teamId, userId } = await getUserDetails(event, false);
const newSession = { const newSession = {
@@ -26,9 +24,7 @@ export const handle = handleSession(
teamId, teamId,
permission, permission,
isAdmin: permission === 'admin' || permission === 'owner', isAdmin: permission === 'admin' || permission === 'owner',
expires: event.locals.session.data.expires, expires: event.locals.session.data.expires
gitlabToken,
ghToken
}; };
if (JSON.stringify(event.locals.session.data) !== JSON.stringify(newSession)) { if (JSON.stringify(event.locals.session.data) !== JSON.stringify(newSession)) {

View File

@@ -9,7 +9,8 @@ export default async function ({
docker, docker,
buildId, buildId,
baseDirectory, baseDirectory,
secrets secrets,
pullmergeRequestId
}) { }) {
try { try {
let file = `${workdir}/Dockerfile`; let file = `${workdir}/Dockerfile`;
@@ -24,7 +25,15 @@ export default async function ({
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { 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'; import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { workdir, port, installCommand, buildCommand, startCommand, baseDirectory, secrets } = const {
data; workdir,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
secrets,
pullmergeRequestId
} = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
@@ -11,7 +19,15 @@ const createDockerfile = async (data, image): Promise<void> => {
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { 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'; import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { workdir, port, installCommand, buildCommand, startCommand, baseDirectory, secrets } = const {
data; workdir,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
secrets,
pullmergeRequestId
} = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
@@ -11,7 +19,15 @@ const createDockerfile = async (data, image): Promise<void> => {
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { 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'; import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { workdir, port, installCommand, buildCommand, startCommand, baseDirectory, secrets } = const {
data; workdir,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
secrets,
pullmergeRequestId
} = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
@@ -11,7 +19,15 @@ const createDockerfile = async (data, image): Promise<void> => {
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { 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'; import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, workdir, buildCommand, baseDirectory, publishDirectory, secrets } = const {
data; applicationId,
tag,
workdir,
buildCommand,
baseDirectory,
publishDirectory,
secrets,
pullmergeRequestId
} = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
@@ -11,7 +19,15 @@ const createDockerfile = async (data, image): Promise<void> => {
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { 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

@@ -1,9 +1,9 @@
<script> <script>
import { browser } from '$app/env'; import { browser } from '$app/env';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
export let value;
let showPassword = false; let showPassword = false;
export let value;
export let disabled = false; export let disabled = false;
export let isPasswordField = false; export let isPasswordField = false;
export let readonly = false; export let readonly = false;
@@ -14,30 +14,22 @@
export let name; export let name;
export let placeholder = ''; 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:'; let isHttps = browser && window.location.protocol === 'https:';
function showActions(value) {
actionsShow = value;
}
function copyToClipboard() { function copyToClipboard() {
if (isHttps && navigator.clipboard) { if (isHttps && navigator.clipboard) {
navigator.clipboard.writeText(value); navigator.clipboard.writeText(value);
toast.push('Copied to clipboard'); toast.push('Copied to clipboard.');
} }
} }
</script> </script>
<div <div class="relative">
class="relative"
on:mouseenter={() => showActions(true)}
on:mouseleave={() => showActions(false)}
>
{#if !isPasswordField || showPassword} {#if !isPasswordField || showPassword}
{#if textarea} {#if textarea}
<textarea <textarea
rows="3" rows="5"
class={disabledClass} class={disabledClass}
{placeholder} {placeholder}
type="text" type="text"
@@ -77,69 +69,67 @@
/> />
{/if} {/if}
{#if actionsShow} <div class="absolute top-0 right-0 m-3 cursor-pointer text-warmGray-600 hover:text-white">
<div class="absolute top-0 right-0 m-3 cursor-pointer text-warmGray-600 hover:text-white"> <div class="flex space-x-2">
<div class="flex space-x-2"> {#if isPasswordField}
{#if isPasswordField} <div on:click={() => (showPassword = !showPassword)}>
<div on:click={() => (showPassword = !showPassword)}> {#if 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}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none" fill="none"
stroke-linecap="round" viewBox="0 0 24 24"
stroke-linejoin="round" stroke="currentColor"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path
<rect x="8" y="8" width="12" height="12" rx="2" /> stroke-linecap="round"
<path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" /> 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> </svg>
</div> {:else}
{/if} <svg
</div> 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> </div>
{/if} </div>
</div> </div>

View File

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

View File

@@ -15,7 +15,12 @@
<Explainer text={description} /> <Explainer text={description} />
</div> </div>
</div> </div>
<div class:tooltip={dataTooltip} class:text-center={isCenter} data-tooltip={dataTooltip}> <div
class:tooltip={dataTooltip}
class:text-center={isCenter}
data-tooltip={dataTooltip}
class="flex justify-center"
>
<div <div
type="button" type="button"
on:click on:click

View File

@@ -9,22 +9,6 @@ export const dateOptions: DateTimeFormatOptions = {
hour12: false hour12: false
}; };
export async function getGithubToken({ apiUrl, application, githubToken }): Promise<void> {
const response = await fetch(
`${apiUrl}/app/installations/${application.gitSource.githubApp.installationId}/access_tokens`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${githubToken}`
}
}
);
if (!response.ok) {
throw new Error('Git Source not configured.');
}
const data = await response.json();
return data.token;
}
export const staticDeployments = ['react', 'vuejs', 'static', 'svelte', 'gatsby', 'php']; export const staticDeployments = ['react', 'vuejs', 'static', 'svelte', 'gatsby', 'php'];
export const notNodeDeployments = ['php', 'docker', 'rust']; export const notNodeDeployments = ['php', 'docker', 'rust'];

View File

@@ -1,16 +1,146 @@
const defaultBuildAndDeploy = { function defaultBuildAndDeploy(packageManager) {
installCommand: 'yarn install', return {
buildCommand: 'yarn build', installCommand:
startCommand: 'yarn start' packageManager === 'npm' ? `${packageManager} run install` : `${packageManager} install`,
}; buildCommand:
export const buildPacks = [ packageManager === 'npm' ? `${packageManager} run build` : `${packageManager} build`,
{ startCommand:
packageManager === 'npm' ? `${packageManager} run start` : `${packageManager} start`
};
}
export function findBuildPack(pack, packageManager = 'npm') {
const metaData = buildPacks.find((b) => b.name === pack);
if (pack === 'node') {
return {
...metaData,
installCommand: null,
buildCommand: null,
startCommand: null,
publishDirectory: null,
port: null
};
}
if (pack === 'static') {
return {
...metaData,
installCommand: null,
buildCommand: null,
startCommand: null,
publishDirectory: null,
port: 80
};
}
if (pack === 'docker') {
return {
...metaData,
installCommand: null,
buildCommand: null,
startCommand: null,
publishDirectory: null,
port: null
};
}
if (pack === 'svelte') {
return {
...metaData,
...defaultBuildAndDeploy(packageManager),
publishDirectory: 'public',
port: 80
};
}
if (pack === 'nestjs') {
return {
...metaData,
...defaultBuildAndDeploy(packageManager),
startCommand:
packageManager === 'npm' ? 'npm run start:prod' : `${packageManager} run start:prod`,
publishDirectory: null,
port: 3000
};
}
if (pack === 'react') {
return {
...metaData,
...defaultBuildAndDeploy(packageManager),
publishDirectory: 'build',
port: 80
};
}
if (pack === 'nextjs') {
return {
...metaData,
...defaultBuildAndDeploy(packageManager),
publishDirectory: null,
port: 3000
};
}
if (pack === 'gatsby') {
return {
...metaData,
...defaultBuildAndDeploy(packageManager),
publishDirectory: 'public',
port: 80
};
}
if (pack === 'vuejs') {
return {
...metaData,
...defaultBuildAndDeploy(packageManager),
publishDirectory: 'dist',
port: 80
};
}
if (pack === 'nuxtjs') {
return {
...metaData,
...defaultBuildAndDeploy(packageManager),
publishDirectory: null,
port: 3000
};
}
if (pack === 'preact') {
return {
...metaData,
...defaultBuildAndDeploy(packageManager),
publishDirectory: 'build',
port: 80
};
}
if (pack === 'php') {
return {
...metaData,
installCommand: null,
buildCommand: null,
startCommand: null,
publishDirectory: null,
port: 80
};
}
if (pack === 'rust') {
return {
...metaData,
installCommand: null,
buildCommand: null,
startCommand: null,
publishDirectory: null,
port: 3000
};
}
return {
name: 'node', name: 'node',
fancyName: 'Node.js',
hoverColor: 'hover:bg-green-700',
color: 'bg-green-700',
installCommand: null, installCommand: null,
buildCommand: null, buildCommand: null,
startCommand: null, startCommand: null,
publishDirectory: null, publishDirectory: null,
port: null, port: null
};
}
export const buildPacks = [
{
name: 'node',
fancyName: 'Node.js', fancyName: 'Node.js',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700' color: 'bg-green-700'
@@ -18,104 +148,72 @@ export const buildPacks = [
{ {
name: 'static', name: 'static',
...defaultBuildAndDeploy,
publishDirectory: 'dist',
port: 80,
fancyName: 'Static', fancyName: 'Static',
hoverColor: 'hover:bg-orange-700', hoverColor: 'hover:bg-orange-700',
color: 'bg-orange-700' color: 'bg-orange-700'
}, },
{ {
name: 'docker', name: 'docker',
installCommand: null,
buildCommand: null,
startCommand: null,
publishDirectory: null,
port: null,
fancyName: 'Docker', fancyName: 'Docker',
hoverColor: 'hover:bg-sky-700', hoverColor: 'hover:bg-sky-700',
color: 'bg-sky-700' color: 'bg-sky-700'
}, },
{ {
name: 'svelte', name: 'svelte',
...defaultBuildAndDeploy,
publishDirectory: 'public',
port: 80,
fancyName: 'Svelte', fancyName: 'Svelte',
hoverColor: 'hover:bg-orange-700', hoverColor: 'hover:bg-orange-700',
color: 'bg-orange-700' color: 'bg-orange-700'
}, },
{ {
name: 'nestjs', name: 'nestjs',
...defaultBuildAndDeploy,
startCommand: 'yarn start:prod',
port: 3000,
fancyName: 'NestJS', fancyName: 'NestJS',
hoverColor: 'hover:bg-red-700', hoverColor: 'hover:bg-red-700',
color: 'bg-red-700' color: 'bg-red-700'
}, },
{ {
name: 'react', name: 'react',
...defaultBuildAndDeploy,
publishDirectory: 'build',
port: 80,
fancyName: 'React', fancyName: 'React',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700' color: 'bg-blue-700'
}, },
{ {
name: 'nextjs', name: 'nextjs',
...defaultBuildAndDeploy,
port: 3000,
fancyName: 'NextJS', fancyName: 'NextJS',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700' color: 'bg-blue-700'
}, },
{ {
name: 'gatsby', name: 'gatsby',
...defaultBuildAndDeploy,
publishDirectory: 'public',
port: 80,
fancyName: 'Gatsby', fancyName: 'Gatsby',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700' color: 'bg-blue-700'
}, },
{ {
name: 'vuejs', name: 'vuejs',
...defaultBuildAndDeploy,
publishDirectory: 'dist',
port: 80,
fancyName: 'VueJS', fancyName: 'VueJS',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700' color: 'bg-green-700'
}, },
{ {
name: 'nuxtjs', name: 'nuxtjs',
...defaultBuildAndDeploy,
port: 3000,
fancyName: 'NuxtJS', fancyName: 'NuxtJS',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700' color: 'bg-green-700'
}, },
{ {
name: 'preact', name: 'preact',
...defaultBuildAndDeploy,
publishDirectory: 'build',
port: 80,
fancyName: 'Preact', fancyName: 'Preact',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700' color: 'bg-blue-700'
}, },
{ {
name: 'php', name: 'php',
port: 80,
fancyName: 'PHP', fancyName: 'PHP',
hoverColor: 'hover:bg-indigo-700', hoverColor: 'hover:bg-indigo-700',
color: 'bg-indigo-700' color: 'bg-indigo-700'
}, },
{ {
name: 'rust', name: 'rust',
port: 3000,
fancyName: 'Rust', fancyName: 'Rust',
hoverColor: 'hover:bg-pink-700', hoverColor: 'hover:bg-pink-700',
color: 'bg-pink-700' color: 'bg-pink-700'

View File

@@ -15,8 +15,8 @@ export async function isDockerNetworkExists({ network }) {
return await prisma.destinationDocker.findFirst({ where: { network } }); return await prisma.destinationDocker.findFirst({ where: { network } });
} }
export async function isSecretExists({ id, name }) { export async function isSecretExists({ id, name, isPRMRSecret }) {
return await prisma.secret.findFirst({ where: { name, applicationId: id } }); return await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } });
} }
export async function isDomainConfigured({ id, fqdn }) { export async function isDomainConfigured({ id, fqdn }) {

View File

@@ -1,9 +1,9 @@
import { decrypt, encrypt } from '$lib/crypto'; import { decrypt, encrypt } from '$lib/crypto';
import { dockerInstance } from '$lib/docker'; import * as db from '$lib/database';
import cuid from 'cuid'; import cuid from 'cuid';
import { generatePassword } from '.'; import { generatePassword } from '.';
import { prisma, ErrorHandler } from './common'; import { prisma, ErrorHandler } from './common';
import getPort from 'get-port'; import getPort, { portNumbers } from 'get-port';
import { asyncExecShell, getEngine, removeContainer } from '$lib/common'; import { asyncExecShell, getEngine, removeContainer } from '$lib/common';
export async function listDatabases(teamId) { export async function listDatabases(teamId) {
@@ -16,24 +16,9 @@ export async function newDatabase({ name, teamId }) {
const rootUserPassword = encrypt(generatePassword()); const rootUserPassword = encrypt(generatePassword());
const defaultDatabase = cuid(); const defaultDatabase = cuid();
let publicPort = await getPort();
let i = 0;
do {
const usedPorts = await prisma.database.findMany({ where: { publicPort } });
if (usedPorts.length === 0) break;
publicPort = await getPort();
i++;
} while (i < 10);
if (i === 9) {
throw {
error: 'No free port found!? Is it possible?'
};
}
return await prisma.database.create({ return await prisma.database.create({
data: { data: {
name, name,
publicPort,
defaultDatabase, defaultDatabase,
dbUser, dbUser,
dbUserPassword, dbUserPassword,

View File

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

View File

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

@@ -3,23 +3,24 @@ import { forceSSLOffApplication, forceSSLOnApplication } from '$lib/haproxy';
import { asyncExecShell, getEngine } from './common'; import { asyncExecShell, getEngine } from './common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import cuid from 'cuid'; import cuid from 'cuid';
import getPort from 'get-port'; import getPort, { portNumbers } from 'get-port';
export async function letsEncrypt({ domain, isCoolify = false, id = null }) { export async function letsEncrypt({ domain, isCoolify = false, id = null }) {
try { try {
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
const nakedDomain = domain.replace('www.', ''); const nakedDomain = domain.replace('www.', '');
const wwwDomain = `www.${nakedDomain}`; const wwwDomain = `www.${nakedDomain}`;
const randomCuid = cuid(); const randomCuid = cuid();
const randomPort = 9080; const randomPort = await getPort({ port: portNumbers(minPort, maxPort) });
let host; let host;
let dualCerts = false; let dualCerts = false;
if (isCoolify) { if (isCoolify) {
const data = await db.prisma.setting.findFirst();
dualCerts = data.dualCerts; dualCerts = data.dualCerts;
host = 'unix:///var/run/docker.sock'; host = 'unix:///var/run/docker.sock';
} else { } else {
// Check Application
const applicationData = await db.prisma.application.findUnique({ const applicationData = await db.prisma.application.findUnique({
where: { id }, where: { id },
include: { destinationDocker: true, settings: true } include: { destinationDocker: true, settings: true }

View File

@@ -64,7 +64,6 @@ export default async function (job) {
if (destinationDockerId) { if (destinationDockerId) {
destinationType = 'docker'; destinationType = 'docker';
} }
if (destinationType === 'docker') { if (destinationType === 'docker') {
const docker = dockerInstance({ destinationDocker }); const docker = dockerInstance({ destinationDocker });
const host = getEngine(destinationDocker.engine); const host = getEngine(destinationDocker.engine);
@@ -205,7 +204,15 @@ export default async function (job) {
const envs = []; const envs = [];
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { 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')); await fs.writeFile(`${workdir}/.env`, envs.join('\n'));

View File

@@ -101,7 +101,6 @@ export default async function () {
if (isHttps) await forceSSLOnApplication(domain); if (isHttps) await forceSSLOnApplication(domain);
} }
} catch (error) { } catch (error) {
console.log(error);
throw error; throw error;
} }
} }

View File

@@ -4,9 +4,16 @@ import { dockerInstance } from '$lib/docker';
import { forceSSLOnApplication } from '$lib/haproxy'; import { forceSSLOnApplication } from '$lib/haproxy';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { dev } from '$app/env'; import { dev } from '$app/env';
import getPort, { portNumbers } from 'get-port';
import cuid from 'cuid';
export default async function () { export default async function () {
try { try {
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
const publicPort = await getPort({ port: portNumbers(minPort, maxPort) });
const randomCuid = cuid();
const destinationDockers = await prisma.destinationDocker.findMany({}); const destinationDockers = await prisma.destinationDocker.findMany({});
for (const destination of destinationDockers) { for (const destination of destinationDockers) {
if (destination.isCoolifyProxyUsed) { if (destination.isCoolifyProxyUsed) {
@@ -30,10 +37,10 @@ export default async function () {
} else { } else {
const host = getEngine(destination.engine); const host = getEngine(destination.engine);
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot -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` `DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${publicPort} -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 ${publicPort} -d ${domain} --agree-tos --non-interactive --register-unsafely-without-email`
); );
const { stderr } = await asyncExecShell( const { stderr } = await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name bash -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` `DOCKER_HOST=${host} 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); if (stderr) throw new Error(stderr);
} }
@@ -52,7 +59,7 @@ export default async function () {
console.log('DEV MODE: SSL is enabled'); console.log('DEV MODE: SSL is enabled');
} else { } else {
await asyncExecShell( await asyncExecShell(
`docker run --rm --name certbot -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` `docker run --rm --name certbot-${randomCuid} -p 9080:${publicPort} -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 ${publicPort} -d ${domain} --agree-tos --non-interactive --register-unsafely-without-email`
); );
const { stderr } = await asyncExecShell( const { stderr } = await asyncExecShell(

6
src/lib/store.ts Normal file
View File

@@ -0,0 +1,6 @@
import { writable } from 'svelte/store';
export const gitTokens = writable({
githubToken: null,
gitlabToken: null
});

View File

@@ -98,7 +98,7 @@
updateStatus.loading = true; updateStatus.loading = true;
try { try {
await post(`/update.json`, { type: 'update', latestVersion }); await post(`/update.json`, { type: 'update', latestVersion });
toast.push('Update completed.<br>Waiting for the new version to start...'); toast.push('Update completed.<br><br>Waiting for the new version to start...');
let reachable = false; let reachable = false;
let tries = 0; let tries = 0;
do { do {
@@ -444,7 +444,8 @@
</button> </button>
{/if} {/if}
{/if} {/if}
</div>
<div class="flex flex-col space-y-4 py-2">
<a <a
sveltekit:prefetch sveltekit:prefetch
href="/teams" href="/teams"
@@ -519,20 +520,20 @@
<path d="M7 12h14l-3 -3m0 6l3 -3" /> <path d="M7 12h14l-3 -3m0 6l3 -3" />
</svg> </svg>
</div> </div>
</div> <div
<div class="w-full text-center font-bold text-stone-400 hover:bg-coolgray-200 hover:text-white"
class="w-full text-center font-bold text-stone-400 hover:bg-coolgray-200 hover:text-white"
>
<a
class="text-[10px] no-underline"
href={`https://github.com/coollabsio/coolify/releases/tag/v${$session.version}`}
target="_blank">v{$session.version}</a
> >
<a
class="text-[10px] no-underline"
href={`https://github.com/coollabsio/coolify/releases/tag/v${$session.version}`}
target="_blank">v{$session.version}</a
>
</div>
</div> </div>
</div> </div>
</nav> </nav>
<select <select
class="fixed right-0 bottom-0 z-50 m-2 p-2 px-4" class="fixed right-0 bottom-0 z-50 m-2 w-64 bg-opacity-30 p-2 px-4"
bind:value={selectedTeamId} bind:value={selectedTeamId}
on:change={switchTeam} on:change={switchTeam}
> >

View File

@@ -17,13 +17,20 @@
const endpoint = `/applications/${params.id}.json`; const endpoint = `/applications/${params.id}.json`;
const res = await fetch(endpoint); const res = await fetch(endpoint);
if (res.ok) { if (res.ok) {
const { application, isRunning, appId } = await res.json(); let { application, isRunning, appId, githubToken, gitlabToken } = await res.json();
if (!application || Object.entries(application).length === 0) { if (!application || Object.entries(application).length === 0) {
return { return {
status: 302, status: 302,
redirect: '/applications' redirect: '/applications'
}; };
} }
if (application.gitSource?.githubAppId && !githubToken) {
const response = await fetch(`/applications/${params.id}/configuration/githubToken.json`);
if (response.ok) {
const { token } = await response.json();
githubToken = token;
}
}
const configurationPhase = checkConfiguration(application); const configurationPhase = checkConfiguration(application);
if ( if (
configurationPhase && configurationPhase &&
@@ -38,7 +45,9 @@
return { return {
props: { props: {
application, application,
isRunning isRunning,
githubToken,
gitlabToken
}, },
stuff: { stuff: {
isRunning, isRunning,
@@ -58,12 +67,18 @@
<script lang="ts"> <script lang="ts">
export let application; export let application;
export let isRunning; export let isRunning;
export let githubToken;
export let gitlabToken;
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import Loading from '$lib/components/Loading.svelte'; import Loading from '$lib/components/Loading.svelte';
import { del, post } from '$lib/api'; import { del, post } from '$lib/api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { gitTokens } from '$lib/store';
if (githubToken) $gitTokens.githubToken = githubToken;
if (gitlabToken) $gitTokens.gitlabToken = gitlabToken;
let loading = false; let loading = false;
const { id } = $page.params; const { id } = $page.params;

View File

@@ -1,24 +1,47 @@
import { asyncExecShell, getDomain, getEngine, getUserDetails } from '$lib/common'; import { dev } from '$app/env';
import { getDomain, getUserDetails } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { promises as dns } from 'dns';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event); const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
let { fqdn, forceSave } = await event.request.json();
let { fqdn } = await event.request.json();
fqdn = fqdn.toLowerCase(); fqdn = fqdn.toLowerCase();
try { try {
const domain = getDomain(fqdn);
const found = await db.isDomainConfigured({ id, fqdn }); const found = await db.isDomainConfigured({ id, fqdn });
if (found) { if (found) {
throw { throw {
message: `Domain ${getDomain(fqdn).replace('www.', '')} is already configured.` message: `Domain ${getDomain(fqdn).replace('www.', '')} is already used.`
}; };
} }
if (!dev && !forceSave) {
let ip = [];
let localIp = [];
dns.setServers(['1.1.1.1', '8.8.8.8']);
try {
localIp = await dns.resolve4(event.url.hostname);
} catch (error) {}
try {
ip = await dns.resolve4(domain);
} catch (error) {}
if (localIp?.length > 0) {
if (ip?.length === 0 || !ip.includes(localIp[0])) {
throw {
message: `DNS not set or propogated for ${domain}.<br><br>Please check your DNS settings.`
};
}
}
}
return { return {
status: 200 status: 200
}; };

View File

@@ -3,6 +3,7 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import { post } from '$lib/api'; import { post } from '$lib/api';
import { findBuildPack } from '$lib/components/templates';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
const { id } = $page.params; const { id } = $page.params;
@@ -11,10 +12,13 @@
export let buildPack; export let buildPack;
export let foundConfig; export let foundConfig;
export let scanning; export let scanning;
export let packageManager;
async function handleSubmit(name) { async function handleSubmit(name) {
try { try {
const tempBuildPack = JSON.parse(JSON.stringify(buildPack)); const tempBuildPack = JSON.parse(
JSON.stringify(findBuildPack(buildPack.name, packageManager))
);
delete tempBuildPack.name; delete tempBuildPack.name;
delete tempBuildPack.fancyName; delete tempBuildPack.fancyName;
delete tempBuildPack.color; delete tempBuildPack.color;

View File

@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation';
export let application; export let application;
import { page, session } from '$app/stores'; import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { gitTokens } from '$lib/store';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
@@ -29,13 +29,9 @@
}; };
let showSave = false; let showSave = false;
async function loadRepositoriesByPage(page = 0) { async function loadRepositoriesByPage(page = 0) {
try { return await get(`${apiUrl}/installation/repositories?per_page=100&page=${page}`, {
return await get(`${apiUrl}/installation/repositories?per_page=100&page=${page}`, { Authorization: `token ${$gitTokens.githubToken}`
Authorization: `token ${$session.ghToken}` });
});
} catch ({ error }) {
return errorNotification(error);
}
} }
async function loadRepositories() { async function loadRepositories() {
let page = 1; let page = 1;
@@ -58,7 +54,7 @@
selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id; selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id;
try { try {
branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, { branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, {
Authorization: `token ${$session.ghToken}` Authorization: `token ${$gitTokens.githubToken}`
}); });
return; return;
} catch ({ error }) { } catch ({ error }) {
@@ -85,7 +81,47 @@
} }
onMount(async () => { onMount(async () => {
await loadRepositories(); try {
if (!$gitTokens.githubToken) {
const { token } = await get(`/applications/${id}/configuration/githubToken.json`);
$gitTokens.githubToken = token;
}
await loadRepositories();
} catch (error) {
if (
error.error === 'invalid_token' ||
error.error_description ===
'Token is expired. You can either do re-authorization or token refresh.' ||
error.message === '401 Unauthorized'
) {
if (application.gitSource.gitlabAppId) {
let htmlUrl = application.gitSource.htmlUrl;
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
`${htmlUrl}/oauth/authorize?client_id=${application.gitSource.gitlabApp.appId}&redirect_uri=${window.location.origin}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${$page.params.id}`,
'GitLab',
'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
top +
', left=' +
left +
', toolbar=0, menubar=0, status=0'
);
const timer = setInterval(() => {
if (newWindow?.closed) {
clearInterval(timer);
window.location.reload();
}
}, 100);
}
}
if (error.message === 'Bad credentials') {
const { token } = await get(`/applications/${id}/configuration/githubToken.json`);
$gitTokens.githubToken = token;
return await loadRepositories();
}
return errorNotification(error);
}
}); });
async function handleSubmit() { async function handleSubmit() {
try { try {

View File

@@ -8,6 +8,8 @@
import cuid from 'cuid'; import cuid from 'cuid';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { del, get, post, put } from '$lib/api'; import { del, get, post, put } from '$lib/api';
import { gitTokens } from '$lib/store';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
@@ -35,13 +37,13 @@
branch: undefined branch: undefined
}; };
onMount(async () => { onMount(async () => {
if (!$session.gitlabToken) { if (!$gitTokens.gitlabToken) {
getGitlabToken(); getGitlabToken();
} else { } else {
loading.base = true; loading.base = true;
try { try {
const user = await get(`${apiUrl}/v4/user`, { const user = await get(`${apiUrl}/v4/user`, {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
}); });
username = user.username; username = user.username;
} catch (error) { } catch (error) {
@@ -49,7 +51,7 @@
} }
try { try {
groups = await get(`${apiUrl}/v4/groups?per_page=5000`, { groups = await get(`${apiUrl}/v4/groups?per_page=5000`, {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
}); });
} catch (error) { } catch (error) {
errorNotification(error); errorNotification(error);
@@ -87,7 +89,7 @@
projects = await get( projects = await get(
`${apiUrl}/v4/users/${selected.group.name}/projects?min_access_level=40&page=1&per_page=25&archived=false`, `${apiUrl}/v4/users/${selected.group.name}/projects?min_access_level=40&page=1&per_page=25&archived=false`,
{ {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
} }
); );
} catch (error) { } catch (error) {
@@ -101,7 +103,7 @@
projects = await get( projects = await get(
`${apiUrl}/v4/groups/${selected.group.id}/projects?page=1&per_page=25&archived=false`, `${apiUrl}/v4/groups/${selected.group.id}/projects?page=1&per_page=25&archived=false`,
{ {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
} }
); );
} catch (error) { } catch (error) {
@@ -119,7 +121,7 @@
branches = await get( branches = await get(
`${apiUrl}/v4/projects/${selected.project.id}/repository/branches?per_page=100&page=1`, `${apiUrl}/v4/projects/${selected.project.id}/repository/branches?per_page=100&page=1`,
{ {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
} }
); );
} catch (error) { } catch (error) {
@@ -169,7 +171,7 @@
merge_requests_events: true merge_requests_events: true
}, },
{ {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
} }
); );
} catch (error) { } catch (error) {
@@ -193,7 +195,7 @@
publicSshKey = publicKey; publicSshKey = publicKey;
} }
const deployKeys = await get(deployKeyUrl, { const deployKeys = await get(deployKeyUrl, {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
}); });
const deployKeyFound = deployKeys.filter((dk) => dk.title === `${appId}-coolify-deploy-key`); const deployKeyFound = deployKeys.filter((dk) => dk.title === `${appId}-coolify-deploy-key`);
if (deployKeyFound.length > 0) { if (deployKeyFound.length > 0) {
@@ -202,7 +204,7 @@
`${deployKeyUrl}/${deployKey.id}`, `${deployKeyUrl}/${deployKey.id}`,
{}, {},
{ {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
} }
); );
} }
@@ -215,7 +217,7 @@
can_push: false can_push: false
}, },
{ {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
} }
); );
await post(updateDeployKeyIdUrl, { deployKeyId: id }); await post(updateDeployKeyIdUrl, { deployKeyId: id });

View File

@@ -29,14 +29,16 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { buildPacks, scanningTemplates } from '$lib/components/templates'; import { buildPacks, findBuildPack, scanningTemplates } from '$lib/components/templates';
import BuildPack from './_BuildPack.svelte'; import BuildPack from './_BuildPack.svelte';
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { gitTokens } from '$lib/store';
let scanning = true; let scanning = true;
let foundConfig = null; let foundConfig = null;
let packageManager = 'npm';
export let apiUrl; export let apiUrl;
export let projectId; export let projectId;
@@ -48,10 +50,11 @@
function checkPackageJSONContents({ key, json }) { function checkPackageJSONContents({ key, json }) {
return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key); return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key);
} }
function checkTemplates({ json }) { function checkTemplates({ json, packageManager }) {
for (const [key, value] of Object.entries(scanningTemplates)) { for (const [key, value] of Object.entries(scanningTemplates)) {
if (checkPackageJSONContents({ key, json })) { if (checkPackageJSONContents({ key, json })) {
return buildPacks.find((bp) => bp.name === value.buildPack); foundConfig = findBuildPack(value.buildPack, packageManager);
break;
} }
} }
} }
@@ -59,11 +62,15 @@
try { try {
if (type === 'gitlab') { if (type === 'gitlab') {
const files = await get(`${apiUrl}/v4/projects/${projectId}/repository/tree`, { const files = await get(`${apiUrl}/v4/projects/${projectId}/repository/tree`, {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
}); });
const packageJson = files.find( const packageJson = files.find(
(file) => file.name === 'package.json' && file.type === 'blob' (file) => file.name === 'package.json' && file.type === 'blob'
); );
const yarnLock = files.find((file) => file.name === 'yarn.lock' && file.type === 'blob');
const pnpmLock = files.find(
(file) => file.name === 'pnpm-lock.yaml' && file.type === 'blob'
);
const dockerfile = files.find((file) => file.name === 'Dockerfile' && file.type === 'blob'); const dockerfile = files.find((file) => file.name === 'Dockerfile' && file.type === 'blob');
const cargoToml = files.find((file) => file.name === 'Cargo.toml' && file.type === 'blob'); const cargoToml = files.find((file) => file.name === 'Cargo.toml' && file.type === 'blob');
const requirementsTxt = files.find( const requirementsTxt = files.find(
@@ -71,6 +78,10 @@
); );
const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'blob'); const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'blob');
const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'blob'); const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'blob');
if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm';
if (dockerfile) { if (dockerfile) {
foundConfig.buildPack = 'docker'; foundConfig.buildPack = 'docker';
} else if (packageJson) { } else if (packageJson) {
@@ -78,28 +89,32 @@
const data = await get( const data = await get(
`${apiUrl}/v4/projects/${projectId}/repository/files/${path}/raw?ref=${branch}`, `${apiUrl}/v4/projects/${projectId}/repository/files/${path}/raw?ref=${branch}`,
{ {
Authorization: `Bearer ${$session.gitlabToken}` Authorization: `Bearer ${$gitTokens.gitlabToken}`
} }
); );
const json = JSON.parse(data) || {}; const json = JSON.parse(data) || {};
foundConfig = checkTemplates({ json }); checkTemplates({ json, packageManager });
} else if (cargoToml) { } else if (cargoToml) {
foundConfig = buildPacks.find((bp) => bp.name === 'rust'); foundConfig = findBuildPack('rust');
} else if (requirementsTxt) { } else if (requirementsTxt) {
foundConfig = buildPacks.find((bp) => bp.name === 'python'); foundConfig = findBuildPack('python');
} else if (indexHtml) { } else if (indexHtml) {
foundConfig = buildPacks.find((bp) => bp.name === 'static'); foundConfig = findBuildPack('static', packageManager);
} else if (indexPHP) { } else if (indexPHP) {
foundConfig = buildPacks.find((bp) => bp.name === 'php'); foundConfig = findBuildPack('php');
} }
} else if (type === 'github') { } else if (type === 'github') {
const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, { const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, {
Authorization: `Bearer ${$session.ghToken || ghToken}`, Authorization: `Bearer ${$gitTokens.githubToken}`,
Accept: 'application/vnd.github.v2.json' Accept: 'application/vnd.github.v2.json'
}); });
const packageJson = files.find( const packageJson = files.find(
(file) => file.name === 'package.json' && file.type === 'file' (file) => file.name === 'package.json' && file.type === 'file'
); );
const yarnLock = files.find((file) => file.name === 'yarn.lock' && file.type === 'file');
const pnpmLock = files.find(
(file) => file.name === 'pnpm-lock.yaml' && file.type === 'file'
);
const dockerfile = files.find((file) => file.name === 'Dockerfile' && file.type === 'file'); const dockerfile = files.find((file) => file.name === 'Dockerfile' && file.type === 'file');
const cargoToml = files.find((file) => file.name === 'Cargo.toml' && file.type === 'file'); const cargoToml = files.find((file) => file.name === 'Cargo.toml' && file.type === 'file');
const requirementsTxt = files.find( const requirementsTxt = files.find(
@@ -107,26 +122,31 @@
); );
const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'file'); const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'file');
const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'file'); const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'file');
if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm';
if (dockerfile) { if (dockerfile) {
foundConfig.buildPack = 'docker'; foundConfig.buildPack = 'docker';
} else if (packageJson) { } else if (packageJson) {
const data = await get(`${packageJson.git_url}`, { const data = await get(`${packageJson.git_url}`, {
Authorization: `Bearer ${$session.ghToken}`, Authorization: `Bearer ${$gitTokens.githubToken}`,
Accept: 'application/vnd.github.v2.raw' Accept: 'application/vnd.github.v2.raw'
}); });
const json = JSON.parse(data) || {}; const json = JSON.parse(data) || {};
foundConfig = checkTemplates({ json }); checkTemplates({ json, packageManager });
} else if (cargoToml) { } else if (cargoToml) {
foundConfig = buildPacks.find((bp) => bp.name === 'rust'); foundConfig = findBuildPack('rust');
} else if (requirementsTxt) { } else if (requirementsTxt) {
foundConfig = buildPacks.find((bp) => bp.name === 'python'); foundConfig = findBuildPack('python');
} else if (indexHtml) { } else if (indexHtml) {
foundConfig = buildPacks.find((bp) => bp.name === 'static'); foundConfig = findBuildPack('static', packageManager);
} else if (indexPHP) { } else if (indexPHP) {
foundConfig = buildPacks.find((bp) => bp.name === 'php'); foundConfig = findBuildPack('php');
} }
} }
} catch (error) { } catch (error) {
scanning = true;
if ( if (
error.error === 'invalid_token' || error.error === 'invalid_token' ||
error.error_description === error.error_description ===
@@ -154,11 +174,13 @@
}, 100); }, 100);
} }
} }
if (error.message === 'Bad credentials') {
browser && window.location.reload();
}
return errorNotification(error); return errorNotification(error);
} finally {
if (!foundConfig) foundConfig = buildPacks.find((bp) => bp.name === 'node');
scanning = false;
} }
if (!foundConfig) foundConfig = findBuildPack('node', packageManager);
scanning = false;
} }
onMount(async () => { onMount(async () => {
await scanRepository(); await scanRepository();
@@ -174,10 +196,16 @@
<div class="text-xl tracking-tight">Scanning repository to suggest a build pack for you...</div> <div class="text-xl tracking-tight">Scanning repository to suggest a build pack for you...</div>
</div> </div>
{:else} {:else}
{#if packageManager === 'yarn' || packageManager === 'pnpm'}
<div class="flex justify-center p-6">
Found lock file for <span class="font-bold text-orange-500 pl-1">{packageManager}</span>.
Using it for predefined commands commands.
</div>
{/if}
<div class="max-w-7xl mx-auto flex flex-wrap justify-center"> <div class="max-w-7xl mx-auto flex flex-wrap justify-center">
{#each buildPacks as buildPack} {#each buildPacks as buildPack}
<div class="p-2"> <div class="p-2">
<BuildPack {buildPack} {scanning} bind:foundConfig /> <BuildPack {buildPack} {scanning} {packageManager} bind:foundConfig />
</div> </div>
{/each} {/each}
</div> </div>

View File

@@ -0,0 +1,45 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
import jsonwebtoken from 'jsonwebtoken';
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 application = await db.getApplication({ id, teamId });
const payload = {
iat: Math.round(new Date().getTime() / 1000),
exp: Math.round(new Date().getTime() / 1000 + 60),
iss: application.gitSource.githubApp.appId
};
const githubToken = jsonwebtoken.sign(payload, application.gitSource.githubApp.privateKey, {
algorithm: 'RS256'
});
const response = await fetch(
`${application.gitSource.apiUrl}/app/installations/${application.gitSource.githubApp.installationId}/access_tokens`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${githubToken}`
}
}
);
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
const data = await response.json();
return {
status: 201,
body: { token: data.token },
headers: {
'Set-Cookie': `githubToken=${data.token}; Path=/; HttpOnly; Max-Age=15778800;`
}
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -20,6 +20,7 @@
<script lang="ts"> <script lang="ts">
export let application; export let application;
export let appId; export let appId;
import GithubRepositories from './_GithubRepositories.svelte'; import GithubRepositories from './_GithubRepositories.svelte';
import GitlabRepositories from './_GitlabRepositories.svelte'; import GitlabRepositories from './_GitlabRepositories.svelte';
</script> </script>

View File

@@ -30,7 +30,7 @@
import type Prisma from '@prisma/client'; import type Prisma from '@prisma/client';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { enhance, errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { post } from '$lib/api'; import { post } from '$lib/api';

View File

@@ -1,54 +1,37 @@
import { getTeam, getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import { getGithubToken } from '$lib/components/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import { checkContainer } from '$lib/haproxy'; import { checkContainer } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import jsonwebtoken from 'jsonwebtoken'; import jsonwebtoken from 'jsonwebtoken';
import { get as getRequest } from '$lib/api';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const appId = process.env['COOLIFY_APP_ID'];
let githubToken = null;
let ghToken = null;
let isRunning = false;
const { id } = event.params; const { id } = event.params;
const appId = process.env['COOLIFY_APP_ID'];
let isRunning = false;
let githubToken = event.locals.cookies?.githubToken || null;
let gitlabToken = event.locals.cookies?.gitlabToken || null;
try { try {
const application = await db.getApplication({ id, teamId }); const application = await db.getApplication({ id, teamId });
const { gitSource } = application;
if (gitSource?.type === 'github' && gitSource?.githubApp) {
if (!event.locals.session.data.ghToken) {
const payload = {
iat: Math.round(new Date().getTime() / 1000),
exp: Math.round(new Date().getTime() / 1000 + 600),
iss: gitSource.githubApp.appId
};
githubToken = jsonwebtoken.sign(payload, gitSource.githubApp.privateKey, {
algorithm: 'RS256'
});
ghToken = await getGithubToken({ apiUrl: gitSource.apiUrl, application, githubToken });
}
}
if (application.destinationDockerId) { if (application.destinationDockerId) {
isRunning = await checkContainer(application.destinationDocker.engine, id); isRunning = await checkContainer(application.destinationDocker.engine, id);
} }
const payload = { return {
status: 200,
body: { body: {
isRunning, isRunning,
application, application,
appId appId,
githubToken,
gitlabToken
}, },
headers: {} headers: {}
}; };
if (ghToken) {
payload.headers = {
'set-cookie': [`ghToken=${ghToken}; HttpOnly; Path=/; Max-Age=15778800;`]
};
}
return payload;
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return ErrorHandler(error); return ErrorHandler(error);

View File

@@ -50,6 +50,7 @@
let domainEl: HTMLInputElement; let domainEl: HTMLInputElement;
let loading = false; let loading = false;
let forceSave = false;
let debug = application.settings.debug; let debug = application.settings.debug;
let previews = application.settings.previews; let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts; let dualCerts = application.settings.dualCerts;
@@ -78,10 +79,13 @@
async function handleSubmit() { async function handleSubmit() {
loading = true; loading = true;
try { try {
await post(`/applications/${id}/check.json`, { fqdn: application.fqdn }); await post(`/applications/${id}/check.json`, { fqdn: application.fqdn, forceSave });
await post(`/applications/${id}.json`, { ...application }); await post(`/applications/${id}.json`, { ...application });
return window.location.reload(); return window.location.reload();
} catch ({ error }) { } catch ({ error }) {
if (error.startsWith('DNS not set')) {
forceSave = true;
}
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading = false; loading = false;
@@ -167,112 +171,105 @@
<button <button
type="submit" type="submit"
class:bg-green-600={!loading} class:bg-green-600={!loading}
class:bg-orange-600={forceSave}
class:hover:bg-green-500={!loading} class:hover:bg-green-500={!loading}
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button class:hover:bg-orange-400={forceSave}
disabled={loading}
>{loading ? 'Saving...' : forceSave ? 'Are you sure to continue?' : 'Save'}</button
> >
{/if} {/if}
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<div class="mt-2 grid grid-cols-3 items-center"> <div class="mt-2 grid grid-cols-2 items-center">
<label for="name">Name</label> <label for="name" class="text-base font-bold text-stone-100">Name</label>
<div class="col-span-2 "> <input
readonly={!$session.isAdmin}
name="name"
id="name"
bind:value={application.name}
required
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="gitSource" class="text-base font-bold text-stone-100">Git Source</label>
<a
href={$session.isAdmin
? `/applications/${id}/configuration/source?from=/applications/${id}`
: ''}
class="no-underline"
><input
value={application.gitSource.name}
id="gitSource"
disabled
class="cursor-pointer hover:bg-coolgray-500"
/></a
>
</div>
<div class="grid grid-cols-2 items-center">
<label for="repository" class="text-base font-bold text-stone-100">Git Repository</label>
<a
href={$session.isAdmin
? `/applications/${id}/configuration/repository?from=/applications/${id}&to=/applications/${id}/configuration/buildpack`
: ''}
class="no-underline"
><input
value="{application.repository}/{application.branch}"
id="repository"
disabled
class="cursor-pointer hover:bg-coolgray-500"
/></a
>
</div>
<div class="grid grid-cols-2 items-center">
<label for="buildPack" class="text-base font-bold text-stone-100">Build Pack</label>
<a
href={$session.isAdmin
? `/applications/${id}/configuration/buildpack?from=/applications/${id}`
: ''}
class="no-underline "
>
<input <input
readonly={!$session.isAdmin} value={application.buildPack}
name="name" id="buildPack"
id="name" disabled
bind:value={application.name} class="cursor-pointer hover:bg-coolgray-500"
required /></a
>
</div>
<div class="grid grid-cols-2 items-center pb-8">
<label for="destination" class="text-base font-bold text-stone-100">Destination</label>
<div class="no-underline">
<input
value={application.destinationDocker.name}
id="destination"
disabled
class="bg-transparent "
/> />
</div> </div>
</div> </div>
<div class="grid grid-cols-3 items-center">
<label for="gitSource">Git Source</label>
<div class="col-span-2">
<a
href={$session.isAdmin
? `/applications/${id}/configuration/source?from=/applications/${id}`
: ''}
class="no-underline"
><input
value={application.gitSource.name}
id="gitSource"
disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500"
/></a
>
</div>
</div>
<div class="grid grid-cols-3 items-center">
<label for="repository">Git Repository</label>
<div class="col-span-2">
<a
href={$session.isAdmin
? `/applications/${id}/configuration/repository?from=/applications/${id}&to=/applications/${id}/configuration/buildpack`
: ''}
class="no-underline"
><input
value="{application.repository}/{application.branch}"
id="repository"
disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500"
/></a
>
</div>
</div>
<div class="grid grid-cols-3 items-center">
<label for="buildPack">Build Pack</label>
<div class="col-span-2">
<a
href={$session.isAdmin
? `/applications/${id}/configuration/buildpack?from=/applications/${id}`
: ''}
class="no-underline "
>
<input
value={application.buildPack}
id="buildPack"
disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500"
/></a
>
</div>
</div>
<div class="grid grid-cols-3 items-center pb-8">
<label for="destination">Destination</label>
<div class="col-span-2">
<div class="no-underline">
<input
value={application.destinationDocker.name}
id="destination"
disabled
class="bg-transparent "
/>
</div>
</div>
</div>
</div> </div>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">Application</div> <div class="title">Application</div>
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-3"> <div class="grid grid-cols-2">
<label for="fqdn" class="relative pt-2">Domain (FQDN)</label> <div class="flex-col">
<div class="col-span-2"> <label for="fqdn" class="pt-2 text-base font-bold text-stone-100">Domain (FQDN)</label>
<input
readonly={!$session.isAdmin || isRunning}
disabled={!$session.isAdmin || isRunning}
bind:this={domainEl}
name="fqdn"
id="fqdn"
bind:value={application.fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io"
required
/>
<Explainer <Explainer
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." 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.<br><br><span class='text-white font-bold'>You must set your DNS to point to the server IP in advance.</span>"
/> />
</div> </div>
<input
readonly={!$session.isAdmin || isRunning}
disabled={!$session.isAdmin || isRunning}
bind:this={domainEl}
name="fqdn"
id="fqdn"
bind:value={application.fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io"
required
/>
</div> </div>
<div class="grid grid-cols-2 items-center pb-8"> <div class="grid grid-cols-2 items-center pb-8">
<Setting <Setting
@@ -286,89 +283,88 @@
/> />
</div> </div>
{#if !staticDeployments.includes(application.buildPack)} {#if !staticDeployments.includes(application.buildPack)}
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center">
<label for="port">Port</label> <label for="port" class="text-base font-bold text-stone-100">Port</label>
<div class="col-span-2">
<input
readonly={!$session.isAdmin}
name="port"
id="port"
bind:value={application.port}
placeholder="default: 3000"
/>
</div>
</div>
{/if}
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-3 items-center">
<label for="installCommand">Install Command</label>
<div class="col-span-2">
<input
readonly={!$session.isAdmin}
name="installCommand"
id="installCommand"
bind:value={application.installCommand}
placeholder="default: yarn install"
/>
</div>
</div>
<div class="grid grid-cols-3 items-center">
<label for="buildCommand">Build Command</label>
<div class="col-span-2">
<input
readonly={!$session.isAdmin}
name="buildCommand"
id="buildCommand"
bind:value={application.buildCommand}
placeholder="default: yarn build"
/>
</div>
</div>
<div class="grid grid-cols-3 items-center">
<label for="startCommand" class="">Start Command</label>
<div class="col-span-2">
<input
readonly={!$session.isAdmin}
name="startCommand"
id="startCommand"
bind:value={application.startCommand}
placeholder="default: yarn start"
/>
</div>
</div>
{/if}
<div class="grid grid-cols-3">
<label for="baseDirectory" class="pt-2">Base Directory</label>
<div class="col-span-2">
<input <input
readonly={!$session.isAdmin} readonly={!$session.isAdmin}
name="baseDirectory" name="port"
id="baseDirectory" id="port"
bind:value={application.baseDirectory} bind:value={application.port}
placeholder="default: /" placeholder="default: 3000"
/>
<Explainer
text="Directory to use as the base of all commands. <br> Could be useful with monorepos."
/> />
</div> </div>
{/if}
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center">
<label for="installCommand" class="text-base font-bold text-stone-100"
>Install Command</label
>
<input
readonly={!$session.isAdmin}
name="installCommand"
id="installCommand"
bind:value={application.installCommand}
placeholder="default: yarn install"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="buildCommand" class="text-base font-bold text-stone-100">Build Command</label>
<input
readonly={!$session.isAdmin}
name="buildCommand"
id="buildCommand"
bind:value={application.buildCommand}
placeholder="default: yarn build"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="startCommand" class="text-base font-bold text-stone-100">Start Command</label>
<input
readonly={!$session.isAdmin}
name="startCommand"
id="startCommand"
bind:value={application.startCommand}
placeholder="default: yarn start"
/>
</div>
{/if}
<div class="grid grid-cols-2 items-center">
<div class="flex-col">
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"
>Base Directory</label
>
<Explainer
text="Directory to use as the base for all commands.<br>Could be useful with <span class='text-green-500 font-bold'>monorepos</span>."
/>
</div>
<input
readonly={!$session.isAdmin}
name="baseDirectory"
id="baseDirectory"
bind:value={application.baseDirectory}
placeholder="default: /"
/>
</div> </div>
{#if !notNodeDeployments.includes(application.buildPack)} {#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-3"> <div class="grid grid-cols-2 items-center">
<label for="publishDirectory" class="pt-2">Publish Directory</label> <div class="flex-col">
<div class="col-span-2"> <label for="publishDirectory" class="pt-2 text-base font-bold text-stone-100"
<input >Publish Directory</label
readonly={!$session.isAdmin} >
name="publishDirectory"
id="publishDirectory"
bind:value={application.publishDirectory}
placeholder=" default: /"
/>
<Explainer <Explainer
text="Directory containing all the assets for deployment. <br> For example: <span class='text-green-600 font-bold'>dist</span>,<span class='text-green-600 font-bold'>_site</span> or <span class='text-green-600 font-bold'>public</span>." text="Directory containing all the assets for deployment. <br> For example: <span class='text-green-500 font-bold'>dist</span>,<span class='text-green-500 font-bold'>_site</span> or <span class='text-green-500 font-bold'>public</span>."
/> />
</div> </div>
<input
readonly={!$session.isAdmin}
name="publishDirectory"
id="publishDirectory"
bind:value={application.publishDirectory}
placeholder=" default: /"
/>
</div> </div>
{/if} {/if}
</div> </div>
@@ -400,7 +396,7 @@
bind:setting={debug} bind:setting={debug}
on:click={() => changeSettings('debug')} on:click={() => changeSettings('debug')}
title="Debug Logs" title="Debug Logs"
description="Enable debug logs during build phase. <br>(<span class='text-red-500'>sensitive information</span> could be visible in logs)" description="Enable debug logs during build phase.<br><span class='text-red-500 font-bold'>Sensitive information</span> could be visible and saved in logs."
/> />
</div> </div>
</div> </div>

View File

@@ -11,6 +11,9 @@ export const get: RequestHandler = async (event) => {
const { id } = event.params; const { id } = event.params;
try { 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 destinationDocker = await db.getDestinationByApplicationId({ id, teamId });
const docker = dockerInstance({ destinationDocker }); const docker = dockerInstance({ destinationDocker });
const listContainers = await docker.engine.listContainers({ const listContainers = await docker.engine.listContainers({
@@ -35,7 +38,13 @@ export const get: RequestHandler = async (event) => {
}); });
return { return {
body: { 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) { } catch (error) {

View File

@@ -22,8 +22,19 @@
<script lang="ts"> <script lang="ts">
export let containers; export let containers;
export let application; export let application;
export let PRMRSecrets;
export let applicationSecrets;
import { getDomain } from '$lib/components/common'; 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> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
@@ -32,7 +43,57 @@
</div> </div>
</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"> <div class="flex flex-wrap justify-center space-x-2">
{#if containers.length > 0} {#if containers.length > 0}
{#each containers as container} {#each containers as container}

View File

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

View File

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

View File

@@ -41,36 +41,22 @@
</div> </div>
</div> </div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4"> <div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<table class="mx-auto"> <table class="mx-auto border-separate text-left">
<thead class=" rounded-xl border-b border-coolgray-500"> <thead>
<tr> <tr class="h-12">
<th <th scope="col">Name</th>
scope="col" <th scope="col">Value</th>
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider text-white">Name</th <th scope="col" class="w-64 text-center">Need during buildtime?</th>
> <th scope="col" class="w-96 text-center">Action</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> </tr>
</thead> </thead>
<tbody class=""> <tbody>
{#each secrets as secret} {#each secrets as secret}
{#key secret.id} {#key secret.id}
<tr class="hover:bg-coolgray-200"> <tr>
<Secret <Secret
name={secret.name} name={secret.name}
value={secret.value ? secret.value : 'ENCRYPTED'} value={secret.value}
isBuildSecret={secret.isBuildSecret} isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets} on:refresh={refreshSecrets}
/> />

View File

@@ -6,9 +6,9 @@
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">CouchDB</div> <div class="title">CouchDB</div>
</div> </div>
<div class="px-10"> <div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase">Default Database</label> <label for="defaultDatabase" class="text-base font-bold text-stone-100">Default Database</label>
<CopyPasswordField <CopyPasswordField
required required
readonly={database.defaultDatabase} readonly={database.defaultDatabase}
@@ -20,7 +20,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser">User</label> <label for="dbUser" class="text-base font-bold text-stone-100">User</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@@ -31,7 +31,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword">Password</label> <label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@@ -43,7 +43,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser">Root User</label> <label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@@ -54,7 +54,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword">Root's Password</label> <label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled

View File

@@ -36,7 +36,7 @@
function generateUrl() { function generateUrl() {
return browser return browser
? `${database.type}://${database.type === 'redis' && ':'}${ ? `${database.type}://${
databaseDbUser ? databaseDbUser + ':' : '' databaseDbUser ? databaseDbUser + ':' : ''
}${databaseDbUserPassword}@${ }${databaseDbUserPassword}@${
isPublic isPublic
@@ -56,9 +56,11 @@
appendOnly = !appendOnly; appendOnly = !appendOnly;
} }
try { try {
await post(`/databases/${id}/settings.json`, { isPublic, appendOnly }); const { publicPort } = await post(`/databases/${id}/settings.json`, { isPublic, appendOnly });
if (isPublic) {
database.publicPort = publicPort;
}
databaseUrl = generateUrl(); databaseUrl = generateUrl();
return;
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} }
@@ -89,7 +91,7 @@
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="name">Name</label> <label for="name" class="text-base font-bold text-stone-100">Name</label>
<input <input
readonly={!$session.isAdmin} readonly={!$session.isAdmin}
name="name" name="name"
@@ -99,7 +101,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="destination">Destination</label> <label for="destination" class="text-base font-bold text-stone-100">Destination</label>
{#if database.destinationDockerId} {#if database.destinationDockerId}
<div class="no-underline"> <div class="no-underline">
<input <input
@@ -114,14 +116,14 @@
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="version">Version</label> <label for="version" class="text-base font-bold text-stone-100">Version</label>
<input value={database.version} readonly disabled class="bg-transparent " /> <input value={database.version} readonly disabled class="bg-transparent " />
</div> </div>
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10 pt-2">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="host">Host</label> <label for="host" class="text-base font-bold text-stone-100">Host</label>
<CopyPasswordField <CopyPasswordField
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
isPasswordField={false} isPasswordField={false}
@@ -133,9 +135,9 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="publicPort">Port</label> <label for="publicPort" class="text-base font-bold text-stone-100">Port</label>
<CopyPasswordField <CopyPasswordField
placeholder="Generated automatically after start" placeholder="Generated automatically after set to public"
id="publicPort" id="publicPort"
readonly readonly
disabled disabled
@@ -157,7 +159,7 @@
<CouchDb bind:database /> <CouchDb bind:database />
{/if} {/if}
<div class="grid grid-cols-2 items-center px-10 pb-8"> <div class="grid grid-cols-2 items-center px-10 pb-8">
<label for="url">Connection String</label> <label for="url" class="text-base font-bold text-stone-100">Connection String</label>
<CopyPasswordField <CopyPasswordField
textarea={true} textarea={true}
placeholder="Generated automatically after start" placeholder="Generated automatically after start"

View File

@@ -6,9 +6,9 @@
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MongoDB</div> <div class="title">MongoDB</div>
</div> </div>
<div class="px-10"> <div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser">Root User</label> <label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
<CopyPasswordField <CopyPasswordField
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
id="rootUser" id="rootUser"
@@ -19,7 +19,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword">Root's Password</label> <label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<CopyPasswordField <CopyPasswordField
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
isPasswordField={true} isPasswordField={true}

View File

@@ -6,9 +6,9 @@
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MySQL</div> <div class="title">MySQL</div>
</div> </div>
<div class=" px-10"> <div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase">Default Database</label> <label for="defaultDatabase" class="text-base font-bold text-stone-100">Default Database</label>
<CopyPasswordField <CopyPasswordField
required required
readonly={database.defaultDatabase} readonly={database.defaultDatabase}
@@ -20,7 +20,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser">User</label> <label for="dbUser" class="text-base font-bold text-stone-100">User</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@@ -31,7 +31,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword">Password</label> <label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@@ -43,7 +43,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser">Root User</label> <label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@@ -54,7 +54,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword">Root's Password</label> <label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled

View File

@@ -6,9 +6,9 @@
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div> <div class="title">PostgreSQL</div>
</div> </div>
<div class="px-10"> <div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase">Default Database</label> <label for="defaultDatabase" class="text-base font-bold text-stone-100">Default Database</label>
<CopyPasswordField <CopyPasswordField
required required
readonly={database.defaultDatabase} readonly={database.defaultDatabase}
@@ -20,7 +20,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser">User</label> <label for="dbUser" class="text-base font-bold text-stone-100">User</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@@ -31,7 +31,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword">Password</label> <label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled

View File

@@ -6,9 +6,9 @@
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">Redis</div> <div class="title">Redis</div>
</div> </div>
<div class="px-10"> <div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword">Password</label> <label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<CopyPasswordField <CopyPasswordField
disabled disabled
readonly readonly

View File

@@ -3,30 +3,39 @@ import * as db from '$lib/database';
import { generateDatabaseConfiguration, ErrorHandler } from '$lib/database'; import { generateDatabaseConfiguration, ErrorHandler } from '$lib/database';
import { startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy'; import { startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import getPort, { portNumbers } from 'get-port';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { status, body, teamId } = await getUserDetails(event); const { status, body, teamId } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
const { isPublic, appendOnly = true } = await event.request.json(); const { isPublic, appendOnly = true } = await event.request.json();
const publicPort = await getPort({ port: portNumbers(minPort, maxPort) });
try { try {
await db.setDatabase({ id, isPublic, appendOnly }); await db.setDatabase({ id, isPublic, appendOnly });
const database = await db.getDatabase({ id, teamId }); const database = await db.getDatabase({ id, teamId });
const { destinationDockerId, destinationDocker, publicPort } = database; const { destinationDockerId, destinationDocker, publicPort: oldPublicPort } = database;
const { privatePort } = generateDatabaseConfiguration(database); const { privatePort } = generateDatabaseConfiguration(database);
if (destinationDockerId) { if (destinationDockerId) {
if (isPublic) { if (isPublic) {
await db.prisma.database.update({ where: { id }, data: { publicPort } });
await startTcpProxy(destinationDocker, id, publicPort, privatePort); await startTcpProxy(destinationDocker, id, publicPort, privatePort);
} else { } else {
await stopTcpHttpProxy(destinationDocker, publicPort); await db.prisma.database.update({ where: { id }, data: { publicPort: null } });
await stopTcpHttpProxy(destinationDocker, oldPublicPort);
} }
} }
return { return {
status: 201 status: 201,
body: {
publicPort
}
}; };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@@ -15,6 +15,7 @@ export const post: RequestHandler = async (event) => {
const everStarted = await stopDatabase(database); const everStarted = await stopDatabase(database);
if (everStarted) await stopTcpHttpProxy(database.destinationDocker, database.publicPort); if (everStarted) await stopTcpHttpProxy(database.destinationDocker, database.publicPort);
await db.setDatabase({ id, isPublic: false }); await db.setDatabase({ id, isPublic: false });
await db.prisma.database.update({ where: { id }, data: { publicPort: null } });
return { return {
status: 200 status: 200

View File

@@ -61,12 +61,12 @@
<div class="w-full text-center font-bold">Loading...</div> <div class="w-full text-center font-bold">Loading...</div>
{:else if app.foundByDomain} {:else if app.foundByDomain}
<div class="w-full bg-coolgray-200 text-xs"> <div class="w-full bg-coolgray-200 text-xs">
<span class="text-red-500">Domain</span> already configured for <span class="text-red-500">Domain</span> already used for
<span class="text-red-500">{app.foundName}</span> <span class="text-red-500">{app.foundName}</span>
</div> </div>
{:else if app.foundByRepository} {:else if app.foundByRepository}
<div class="w-full bg-coolgray-200 text-xs"> <div class="w-full bg-coolgray-200 text-xs">
<span class="text-red-500">Repository</span> already configured for <span class="text-red-500">Repository</span> already used for
<span class="text-red-500">{app.foundName}</span> <span class="text-red-500">{app.foundName}</span>
</div> </div>
{:else} {:else}

View File

@@ -122,80 +122,72 @@
} }
</script> </script>
<div class="flex justify-center px-6 pb-8"> <form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4"> <div class="flex space-x-1 pb-5">
<div class="flex h-8 items-center space-x-2"> <div class="title font-bold">Configuration</div>
<div class="text-xl font-bold text-white">Configuration</div> <button
<button type="submit"
type="submit" class="bg-sky-600 hover:bg-sky-500"
class="bg-sky-600 hover:bg-sky-500" class:bg-sky-600={!loading}
class:bg-sky-600={!loading} class:hover:bg-sky-500={!loading}
class:hover:bg-sky-500={!loading} disabled={loading}
disabled={loading} >{loading ? 'Saving...' : 'Save'}
>{loading ? 'Saving...' : 'Save'} </button>
</button> <button
<button class={restarting ? '' : 'bg-red-600 hover:bg-red-500'}
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'} disabled={restarting}
disabled={restarting} on:click|preventDefault={forceRestartProxy}
on:click|preventDefault={forceRestartProxy} >{restarting ? 'Restarting... please wait...' : 'Force restart proxy'}</button
>{restarting ? 'Restarting... please wait...' : 'Force restart proxy'}</button >
> <!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
<!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
>Scan for applications</button >Scan for applications</button
> --> > -->
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10 ">
<label for="name">Name</label> <label for="name" class="text-base font-bold text-stone-100">Name</label>
<div class="col-span-2"> <input name="name" placeholder="name" bind:value={destination.name} />
<input name="name" placeholder="name" bind:value={destination.name} /> </div>
</div>
</div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="engine">Engine</label> <label for="engine" class="text-base font-bold text-stone-100">Engine</label>
<div class="col-span-2"> <CopyPasswordField
<CopyPasswordField id="engine"
id="engine" readonly
readonly disabled
disabled name="engine"
name="engine" placeholder="eg: /var/run/docker.sock"
placeholder="eg: /var/run/docker.sock" value={destination.engine}
value={destination.engine} />
/> </div>
</div> <!-- <div class="flex items-center">
</div>
<!-- <div class="flex items-center">
<label for="remoteEngine">Remote Engine?</label> <label for="remoteEngine">Remote Engine?</label>
<input name="remoteEngine" type="checkbox" bind:checked={payload.remoteEngine} /> <input name="remoteEngine" type="checkbox" bind:checked={payload.remoteEngine} />
</div> --> </div> -->
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="network">Network</label> <label for="network" class="text-base font-bold text-stone-100">Network</label>
<div class="col-span-2"> <CopyPasswordField
<CopyPasswordField id="network"
id="network" readonly
readonly disabled
disabled name="network"
name="network" placeholder="default: coolify"
placeholder="default: coolify" value={destination.network}
value={destination.network} />
/> </div>
</div> <div class="grid grid-cols-2 items-center">
</div> <Setting
<div class="grid grid-cols-2 items-center"> disabled={cannotDisable}
<Setting bind:setting={destination.isCoolifyProxyUsed}
disabled={cannotDisable} on:click={changeProxySetting}
bind:setting={destination.isCoolifyProxyUsed} title="Use Coolify Proxy?"
on:click={changeProxySetting} 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>${
title="Use Coolify Proxy?" cannotDisable
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>${ ? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
cannotDisable : ''
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>' }`}
: '' />
}`} </div>
/> </form>
</div>
</form>
</div>
<!-- <div class="flex justify-center"> <!-- <div class="flex justify-center">
{#if payload.isCoolifyProxyUsed} {#if payload.isCoolifyProxyUsed}
{#if state} {#if state}

View File

@@ -61,8 +61,8 @@
class:hover:text-red-500={$session.isAdmin} class:hover:text-red-500={$session.isAdmin}
class="icons tooltip-bottom bg-transparent text-sm" class="icons tooltip-bottom bg-transparent text-sm"
data-tooltip={$session.isAdmin data-tooltip={$session.isAdmin
? 'Delete Git Source' ? 'Delete Destination'
: 'You do not have permission to delete a Git Source'}><DeleteIcon /></button : 'You do not have permission to delete this destination'}><DeleteIcon /></button
> >
</nav> </nav>
<slot /> <slot />

View File

@@ -42,5 +42,6 @@
<span class="arrow-right-applications px-1">></span> <span class="arrow-right-applications px-1">></span>
<span class="pr-2">{destination.name}</span> <span class="pr-2">{destination.name}</span>
</div> </div>
<div class="mx-auto max-w-4xl px-6">
<LocalDocker bind:destination {settings} {state} /> <LocalDocker bind:destination {settings} {state} />
</div>

View File

@@ -18,7 +18,7 @@
async function handleSubmit() { async function handleSubmit() {
loading = true; loading = true;
try { try {
const { teamId } = await post(`/login.json`, { email, password }); const { teamId } = await post(`/login.json`, { email: email.toLowerCase(), password });
if (teamId === '0') { if (teamId === '0') {
window.location.replace('/settings'); window.location.replace('/settings');
} else { } else {
@@ -58,7 +58,7 @@
required required
/> />
<div class="flex space-x-2 h-8 items-center justify-center pt-14"> <div class="flex space-x-2 h-8 items-center justify-center pt-8">
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
@@ -71,5 +71,15 @@
</div> </div>
</form> </form>
</div> </div>
{#if browser && window.location.host === 'demo.coolify.io'}
<div class="pt-5 font-bold">
Registration is <span class="text-pink-500">open</span>, just fill in an email (does not
need to be live email address for the demo instance) and a password.
</div>
<div class="pt-5 font-bold">
All users gets an <span class="text-pink-500">own namespace</span>, so you won't be able to
access other users data.
</div>
{/if}
{/if} {/if}
</div> </div>

View File

@@ -5,7 +5,7 @@
import { post } from '$lib/api'; import { post } from '$lib/api';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
import { enhance, errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
let loading = false; let loading = false;
@@ -24,8 +24,8 @@
<div class="flex justify-center px-6 pb-8"> <div class="flex justify-center px-6 pb-8">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4"> <form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex h-8 items-center space-x-2"> <div class="flex items-center space-x-2 pb-5">
<div class="text-xl font-bold text-white">Configuration</div> <div class="title font-bold">Configuration</div>
<button <button
type="submit" type="submit"
class:bg-sky-600={!loading} class:bg-sky-600={!loading}
@@ -38,24 +38,20 @@
: 'Save'}</button : 'Save'}</button
> >
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="mt-2 grid grid-cols-2 items-center px-10">
<label for="name">Name</label> <label for="name" class="text-base font-bold text-stone-100">Name</label>
<div class="col-span-2"> <input required name="name" placeholder="name" bind:value={payload.name} />
<input required name="name" placeholder="name" bind:value={payload.name} />
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="engine">Engine</label> <label for="engine" class="text-base font-bold text-stone-100">Engine</label>
<div class="col-span-2"> <input
<input required
required name="engine"
name="engine" placeholder="eg: /var/run/docker.sock"
placeholder="eg: /var/run/docker.sock" bind:value={payload.engine}
bind:value={payload.engine} />
/> <!-- <Explainer text="You can use remote Docker Engine with over SSH." /> -->
<!-- <Explainer text="You can use remote Docker Engine with over SSH." /> -->
</div>
</div> </div>
<!-- <div class="flex items-center"> <!-- <div class="flex items-center">
<label for="remoteEngine">Remote Docker Engine?</label> <label for="remoteEngine">Remote Docker Engine?</label>
@@ -75,27 +71,17 @@
</div> </div>
</div> </div>
{/if} --> {/if} -->
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="network">Network</label> <label for="network" class="text-base font-bold text-stone-100">Network</label>
<div class="col-span-2"> <input required name="network" placeholder="default: coolify" bind:value={payload.network} />
<input
required
name="network"
placeholder="default: coolify"
bind:value={payload.network}
/>
</div>
</div> </div>
<div class="flex justify-start"> <div class="grid grid-cols-2 items-center">
<ul class="mt-2 divide-y divide-stone-800"> <Setting
<Setting bind:setting={payload.isCoolifyProxyUsed}
bind:setting={payload.isCoolifyProxyUsed} on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)} title="Use Coolify Proxy?"
isPadding={false} description="This will install a proxy on the destination to allow you to access your applications and services without any manual configuration (recommended for Docker).<br><br>Databases will have their own proxy."
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 (recommended for Docker). Databases will have their own proxy."
/>
</ul>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -1,6 +1,6 @@
<script> <script>
import Docker from './_Docker.svelte'; import Docker from './_Docker.svelte';
import cuid from 'cuid';
let payload = {}; let payload = {};
let selected = 'docker'; let selected = 'docker';
@@ -15,7 +15,7 @@
user: 'root', user: 'root',
port: 22, port: 22,
privateKey: null, privateKey: null,
network: 'coolify', network: cuid(),
isCoolifyProxyUsed: true isCoolifyProxyUsed: true
}; };
break; break;

View File

@@ -24,26 +24,24 @@
} }
</script> </script>
<div class="flex justify-center pb-8"> <div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4"> <div class="flex justify-center pb-8">
<div class="flex h-8 items-center space-x-2"> <form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="text-xl font-bold text-white">Configuration</div> <div class="flex h-8 items-center space-x-2">
<button type="submit" class="bg-orange-600 hover:bg-orange-500">Save</button> <div class="text-xl font-bold text-white">Configuration</div>
</div> <button type="submit" class="bg-orange-600 hover:bg-orange-500">Save</button>
<div class="grid grid-cols-3 items-center"> </div>
<label for="type">Type</label> <div class="grid grid-cols-2 items-center px-10">
<label for="type" class="text-base font-bold text-stone-100">Type</label>
<div class="col-span-2">
<select name="type" id="type" class="w-96" bind:value={gitSource.type}> <select name="type" id="type" class="w-96" bind:value={gitSource.type}>
<option value="github">GitHub</option> <option value="github">GitHub</option>
<option value="gitlab">GitLab</option> <option value="gitlab">GitLab</option>
<option value="bitbucket">BitBucket</option> <option value="bitbucket">BitBucket</option>
</select> </select>
</div> </div>
</div> <div class="grid grid-cols-2 items-center px-10">
<div class="grid grid-cols-3 items-center"> <label for="name" class="text-base font-bold text-stone-100">Name</label>
<label for="name">Name</label>
<div class="col-span-2">
<input <input
name="name" name="name"
id="name" id="name"
@@ -53,11 +51,9 @@
bind:value={gitSource.name} bind:value={gitSource.name}
/> />
</div> </div>
</div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="htmlUrl">HTML URL</label> <label for="htmlUrl" class="text-base font-bold text-stone-100">HTML URL</label>
<div class="col-span-2">
<input <input
type="url" type="url"
name="htmlUrl" name="htmlUrl"
@@ -67,10 +63,8 @@
bind:value={gitSource.htmlUrl} bind:value={gitSource.htmlUrl}
/> />
</div> </div>
</div> <div class="grid grid-cols-2 items-center px-10">
<div class="grid grid-cols-3 items-center"> <label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<label for="apiUrl">API URL</label>
<div class="col-span-2">
<input <input
name="apiUrl" name="apiUrl"
type="url" type="url"
@@ -80,10 +74,15 @@
bind:value={gitSource.apiUrl} bind:value={gitSource.apiUrl}
/> />
</div> </div>
</div> <div class="grid grid-cols-2 px-10">
<div class="grid grid-cols-3"> <div class="flex flex-col">
<label for="organization" class="pt-2">Organization</label> <label for="organization" class="pt-2 text-base font-bold text-stone-100"
<div class="col-span-2"> >Organization</label
>
<Explainer
text="Fill it if you would like to use an organization's as your Git Source. Otherwise your user will be used."
/>
</div>
<input <input
name="organization" name="organization"
id="organization" id="organization"
@@ -91,11 +90,7 @@
bind:value={gitSource.organization} bind:value={gitSource.organization}
bind:this={organizationEl} bind:this={organizationEl}
/> />
<Explainer
text="Fill it if you would like to use an organization's as your Git Source. Otherwise your
user will be used."
/>
</div> </div>
</div> </form>
</form> </div>
</div> </div>

View File

@@ -27,56 +27,47 @@
<div class="text-xl font-bold text-white">Configuration</div> <div class="text-xl font-bold text-white">Configuration</div>
<button type="submit" class="bg-orange-600 hover:bg-orange-500">Save</button> <button type="submit" class="bg-orange-600 hover:bg-orange-500">Save</button>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="type">Type</label> <label for="type" class="text-base font-bold text-stone-100">Type</label>
<select name="type" id="type" class="w-96" bind:value={gitSource.type}>
<div class="col-span-2"> <option value="github">GitHub</option>
<select name="type" id="type" class="w-96" bind:value={gitSource.type}> <option value="gitlab">GitLab</option>
<option value="github">GitHub</option> <option value="bitbucket">BitBucket</option>
<option value="gitlab">GitLab</option> </select>
<option value="bitbucket">BitBucket</option>
</select>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="name">Name</label> <label for="name" class="text-base font-bold text-stone-100">Name</label>
<div class="col-span-2"> <input
<input name="name"
name="name" id="name"
id="name" placeholder="GitHub.com"
placeholder="GitHub.com" required
required bind:this={nameEl}
bind:this={nameEl} bind:value={gitSource.name}
bind:value={gitSource.name} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="htmlUrl">HTML URL</label> <label for="htmlUrl" class="text-base font-bold text-stone-100">HTML URL</label>
<div class="col-span-2"> <input
<input type="url"
type="url" name="htmlUrl"
name="htmlUrl" id="htmlUrl"
id="htmlUrl" placeholder="eg: https://github.com"
placeholder="eg: https://github.com" required
required bind:value={gitSource.htmlUrl}
bind:value={gitSource.htmlUrl} />
/>
</div>
</div> </div>
<div class="grid grid-cols-3 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="apiUrl">API URL</label> <label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<div class="col-span-2"> <input
<input name="apiUrl"
name="apiUrl" type="url"
type="url" id="apiUrl"
id="apiUrl" placeholder="eg: https://api.github.com"
placeholder="eg: https://api.github.com" required
required bind:value={gitSource.apiUrl}
bind:value={gitSource.apiUrl} />
/>
</div>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -7,7 +7,7 @@
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MinIO Server</div> <div class="title">MinIO Server</div>
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="rootUser">Root User</label> <label for="rootUser">Root User</label>
<input <input
name="rootUser" name="rootUser"
@@ -18,7 +18,7 @@
readonly readonly
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="rootUserPassword">Root's Password</label> <label for="rootUserPassword">Root's Password</label>
<CopyPasswordField <CopyPasswordField
id="rootUserPassword" id="rootUserPassword"
@@ -29,7 +29,7 @@
value={service.minio.rootUserPassword} value={service.minio.rootUserPassword}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center px-10">
<label for="publicPort">API Port</label> <label for="publicPort">API Port</label>
<input <input
name="publicPort" name="publicPort"

View File

@@ -57,7 +57,7 @@
} }
</script> </script>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-4xl px-6 pb-12">
<form on:submit|preventDefault={handleSubmit} class="py-4"> <form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold"> <div class="flex space-x-1 pb-5 font-bold">
<div class="title">General</div> <div class="title">General</div>
@@ -70,11 +70,7 @@
> >
{/if} {/if}
{#if service.type === 'plausibleanalytics' && isRunning} {#if service.type === 'plausibleanalytics' && isRunning}
<button <button on:click|preventDefault={setEmailsToVerified} disabled={loadingVerification}
on:click|preventDefault={setEmailsToVerified}
class:bg-pink-600={!loadingVerification}
class:hover:bg-pink-500={!loadingVerification}
disabled={loadingVerification}
>{loadingVerification ? 'Verifying' : 'Verify emails without SMTP'}</button >{loadingVerification ? 'Verifying' : 'Verify emails without SMTP'}</button
> >
{/if} {/if}
@@ -82,7 +78,7 @@
<div class="grid grid-flow-row gap-2"> <div class="grid grid-flow-row gap-2">
<div class="mt-2 grid grid-cols-2 items-center px-10"> <div class="mt-2 grid grid-cols-2 items-center px-10">
<label for="name">Name</label> <label for="name" class="text-base font-bold text-stone-100">Name</label>
<div> <div>
<input <input
readonly={!$session.isAdmin} readonly={!$session.isAdmin}
@@ -95,7 +91,7 @@
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="destination">Destination</label> <label for="destination" class="text-base font-bold text-stone-100">Destination</label>
<div> <div>
{#if service.destinationDockerId} {#if service.destinationDockerId}
<div class="no-underline"> <div class="no-underline">
@@ -110,22 +106,23 @@
</div> </div>
</div> </div>
<div class="grid grid-cols-2 px-10"> <div class="grid grid-cols-2 px-10">
<label for="fqdn" class="pt-2">Domain (FQDN)</label> <div class="flex-col ">
<div> <label for="fqdn" class="pt-2 text-base font-bold text-stone-100">Domain (FQDN)</label>
<CopyPasswordField
placeholder="eg: https://analytics.coollabs.io"
readonly={!$session.isAdmin && !isRunning}
disabled={!$session.isAdmin || isRunning}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
bind:value={service.fqdn}
required
/>
<Explainer <Explainer
text="If you specify <span class='text-pink-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-pink-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-pink-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-pink-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."
/> />
</div> </div>
<CopyPasswordField
placeholder="eg: https://analytics.coollabs.io"
readonly={!$session.isAdmin && !isRunning}
disabled={!$session.isAdmin || isRunning}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
bind:value={service.fqdn}
required
/>
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting

View File

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

View File

@@ -9,10 +9,9 @@ import {
configureSimpleServiceProxyOn, configureSimpleServiceProxyOn,
reloadHaproxy, reloadHaproxy,
setWwwRedirection, setWwwRedirection,
startHttpProxy, startHttpProxy
startTcpProxy
} from '$lib/haproxy'; } from '$lib/haproxy';
import getPort from 'get-port'; import getPort, { portNumbers } from 'get-port';
import { getDomain } from '$lib/components/common'; import { getDomain } from '$lib/components/common';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
@@ -35,14 +34,20 @@ export const post: RequestHandler = async (event) => {
minio: { rootUser, rootUserPassword } minio: { rootUser, rootUserPassword }
} = service; } = service;
const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data;
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine); const host = getEngine(destinationDocker.engine);
const publicPort = await getPort();
const publicPort = await getPort({ port: portNumbers(minPort, maxPort) });
const consolePort = 9001; const consolePort = 9001;
const apiPort = 9000; const apiPort = 9000;
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
const config = { const config = {

View File

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

View File

@@ -1,3 +1,4 @@
import { dev } from '$app/env';
import { getDomain, getUserDetails } from '$lib/common'; import { getDomain, getUserDetails } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { listSettings, ErrorHandler } from '$lib/database'; import { listSettings, ErrorHandler } from '$lib/database';
@@ -43,7 +44,12 @@ export const del: RequestHandler = async (event) => {
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { fqdn } = await event.request.json(); const { fqdn } = await event.request.json();
const ip = await dns.resolve(event.url.hostname); let ip;
try {
ip = await dns.resolve(fqdn);
} catch (error) {
// Do not care.
}
try { try {
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
await db.prisma.setting.update({ where: { fqdn }, data: { fqdn: null } }); await db.prisma.setting.update({ where: { fqdn }, data: { fqdn: null } });
@@ -53,7 +59,7 @@ export const del: RequestHandler = async (event) => {
status: 200, status: 200,
body: { body: {
message: 'Domain removed', message: 'Domain removed',
redirect: `http://${ip[0]}:3000/settings` redirect: ip ? `http://${ip[0]}:3000/settings` : undefined
} }
}; };
} catch (error) { } catch (error) {
@@ -71,7 +77,7 @@ export const post: RequestHandler = async (event) => {
}; };
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { fqdn, isRegistrationEnabled, dualCerts } = await event.request.json(); const { fqdn, isRegistrationEnabled, dualCerts, minPort, maxPort } = await event.request.json();
try { try {
const { const {
id, id,
@@ -112,6 +118,9 @@ export const post: RequestHandler = async (event) => {
data: { isCoolifyProxyUsed: true } data: { isCoolifyProxyUsed: true }
}); });
} }
if (minPort && maxPort) {
await db.prisma.setting.update({ where: { id }, data: { minPort, maxPort } });
}
return { return {
status: 201 status: 201

View File

@@ -35,6 +35,9 @@
let isRegistrationEnabled = settings.isRegistrationEnabled; let isRegistrationEnabled = settings.isRegistrationEnabled;
let dualCerts = settings.dualCerts; let dualCerts = settings.dualCerts;
let minPort = settings.minPort;
let maxPort = settings.maxPort;
let fqdn = settings.fqdn; let fqdn = settings.fqdn;
let isFqdnSet = !!settings.fqdn; let isFqdnSet = !!settings.fqdn;
let loading = { let loading = {
@@ -72,10 +75,14 @@
async function handleSubmit() { async function handleSubmit() {
try { try {
loading.save = true; loading.save = true;
if (fqdn) { if (fqdn !== settings.fqdn) {
await post(`/settings/check.json`, { fqdn }); await post(`/settings/check.json`, { fqdn });
await post(`/settings.json`, { fqdn }); await post(`/settings.json`, { fqdn });
return window.location.reload(); }
if (minPort !== settings.minPort || maxPort !== settings.maxPort) {
await post(`/settings.json`, { minPort, maxPort });
settings.minPort = minPort;
settings.maxPort = maxPort;
} }
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
@@ -90,9 +97,9 @@
</div> </div>
{#if $session.teamId === '0'} {#if $session.teamId === '0'}
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmit}> <form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 py-6 font-bold"> <div class="flex space-x-1 py-6">
<div class="title">Global Settings</div> <div class="title font-bold">Global Settings</div>
<button <button
type="submit" type="submit"
disabled={loading.save} disabled={loading.save}
@@ -112,7 +119,12 @@
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-2 items-start"> <div class="grid grid-cols-2 items-start">
<div class="pt-2 text-base font-bold text-stone-100">Domain (FQDN)</div> <div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100">Domain (FQDN)</div>
<Explainer
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 class="justify-start text-left"> <div class="justify-start text-left">
<input <input
bind:value={fqdn} bind:value={fqdn}
@@ -122,10 +134,31 @@
id="fqdn" id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$" pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coolify.io" placeholder="eg: https://coolify.io"
required
/> />
</div>
</div>
<div class="grid grid-cols-2 items-start py-6">
<div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100">Public Port Range</div>
<Explainer <Explainer
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." text="Ports used to expose databases/services/internal services.<br> Add them to your firewall (if applicable).<br><br>You can specify a range of ports, eg: <span class='text-yellow-500 font-bold'>9000-9100</span>"
/>
</div>
<div class="mx-auto flex-row items-center justify-center space-y-2">
<input
class="h-8 w-20 px-2"
type="number"
bind:value={minPort}
min="1024"
max={maxPort}
/>
-
<input
class="h-8 w-20 px-2"
type="number"
bind:value={maxPort}
min={minPort}
max="65543"
/> />
</div> </div>
</div> </div>
@@ -135,7 +168,7 @@
disabled={isFqdnSet} disabled={isFqdnSet}
bind:setting={dualCerts} bind:setting={dualCerts}
title="Generate SSL for www and non-www?" 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." description="It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-yellow-500'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both."
on:click={() => !isFqdnSet && changeSettings('dualCerts')} on:click={() => !isFqdnSet && changeSettings('dualCerts')}
/> />
</div> </div>
@@ -159,7 +192,7 @@
: browser && 'http://' + window.location.hostname + ':8404' : browser && 'http://' + window.location.hostname + ':8404'
} target="_blank">stats</a> page.`} } target="_blank">stats</a> page.`}
/> />
<div class="px-10 py-5"> <div class="space-y-2 px-10 py-5">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="proxyUser">User</label> <label for="proxyUser">User</label>
<CopyPasswordField <CopyPasswordField

View File

@@ -62,32 +62,28 @@
{#if !source.githubAppId} {#if !source.githubAppId}
<button on:click={newGithubApp}>Create new GitHub App</button> <button on:click={newGithubApp}>Create new GitHub App</button>
{:else if source.githubApp?.installationId} {:else if source.githubApp?.installationId}
<div class="mx-auto max-w-4xl px-6"> <form on:submit|preventDefault={handleSubmit} class="py-4">
<form on:submit|preventDefault={handleSubmit} class="py-4"> <div class="flex space-x-1 pb-5 font-bold">
<div class="flex space-x-1 pb-5 font-bold"> <div class="title">General</div>
<div class="title">General</div> {#if $session.isAdmin}
{#if $session.isAdmin} <button
<button type="submit"
type="submit" class:bg-orange-600={!loading}
class:bg-orange-600={!loading} class:hover:bg-orange-500={!loading}
class:hover:bg-orange-500={!loading} disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button >
> <button on:click|preventDefault={() => installRepositories(source)}
<button on:click|preventDefault={() => installRepositories(source)} >Change GitHub App Settings</button
>Change GitHub App Settings</button >
> {/if}
{/if} </div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-2 items-center mt-2">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<input name="name" id="name" required bind:value={source.name} />
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> </div>
<div class="mt-2 grid grid-cols-3 items-center"> </form>
<label for="name">Name</label>
<div class="col-span-2 ">
<input name="name" id="name" required bind:value={source.name} />
</div>
</div>
</div>
</form>
</div>
{:else} {:else}
<button on:click={() => installRepositories(source)}>Install Repositories</button> <button on:click={() => installRepositories(source)}>Install Repositories</button>
{/if} {/if}

View File

@@ -5,6 +5,7 @@
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { post } from '$lib/api'; import { post } from '$lib/api';
import { browser } from '$app/env';
const { id } = $page.params; const { id } = $page.params;
let loading = false; let loading = false;
@@ -89,125 +90,115 @@
} }
</script> </script>
<div class="flex flex-col justify-center"> {#if !source.gitlabApp?.appId}
{#if !source.gitlabApp?.appId} <form class="grid grid-flow-row gap-2 py-4" on:submit|preventDefault={newApp}>
<form class="grid grid-flow-row gap-2 py-4" on:submit|preventDefault={newApp}> <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-3 items-center"> <label for="type">GitLab Application Type</label>
<label for="type">GitLab Application Type</label> <select name="type" id="type" class="w-96" bind:value={payload.applicationType}>
<div class="col-span-2"> <option value="user">User owned application</option>
<select name="type" id="type" class="w-96" bind:value={payload.applicationType}> <option value="group">Group owned application</option>
<option value="user">User owned application</option> {#if source.htmlUrl !== 'https://gitlab.com'}
<option value="group">Group owned application</option> <option value="instance">Instance-wide application (self-hosted)</option>
{#if source.htmlUrl !== 'https://gitlab.com'} {/if}
<option value="instance">Instance-wide application (self-hosted)</option> </select>
{/if}
</select>
</div>
</div>
{#if payload.applicationType === 'group'}
<div class="grid grid-cols-3 items-center">
<label for="groupName">Group Name</label>
<div class="col-span-2">
<input name="groupName" id="groupName" required bind:value={payload.groupName} />
</div>
</div>
{/if}
<div class="w-full pt-10 text-center">
<button class="w-96 bg-orange-600 hover:bg-orange-500" type="submit"
>Register new OAuth application on GitLab</button
>
</div>
<Explainer
customClass="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)
<br>
<br>For extra security, you can add Expire access tokens!"
/>
</form>
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4 pt-10">
<div class="flex h-8 items-center space-x-2">
<div class="text-xl font-bold text-white">Configuration</div>
<button
type="submit"
class:bg-orange-600={!loading}
class:hover:bg-orange-500={!loading}
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
>
</div>
<div class="grid grid-cols-3 items-start">
<label for="oauthId" class="pt-2">OAuth ID</label>
<div class="col-span-2">
<input
on:change={checkOauthId}
bind:this={oauthIdEl}
name="oauthId"
id="oauthId"
type="number"
required
bind:value={payload.oauthId}
/>
<Explainer
text="The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class='font-bold text-orange-600' >in the URL</span> of your GitLab OAuth Application."
/>
</div>
</div>
{#if payload.applicationType === 'group'}
<div class="grid grid-cols-3 items-center">
<label for="groupName">Group Name</label>
<div class="col-span-2">
<input name="groupName" id="groupName" required bind:value={payload.groupName} />
</div>
</div>
{/if}
<div class="grid grid-cols-3 items-center">
<label for="appId">Application ID</label>
<div class="col-span-2">
<input name="appId" id="appId" required bind:value={payload.appId} />
</div>
</div>
<div class="grid grid-cols-3 items-center">
<label for="appSecret">Secret</label>
<div class="col-span-2">
<input
name="appSecret"
id="appSecret"
type="password"
required
bind:value={payload.appSecret}
/>
</div>
</div>
</form>
{:else}
<div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmitSave} class="py-4">
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">General</div>
{#if $session.isAdmin}
<button
type="submit"
class:bg-orange-600={!loading}
class:hover:bg-orange-500={!loading}
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
>
<button on:click|preventDefault={changeSettings}>Change GitLab App Settings</button>
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="mt-2 grid grid-cols-3 items-center">
<label for="name">Name</label>
<div class="col-span-2 ">
<input name="name" id="name" required bind:value={source.name} />
</div>
</div>
</div>
</form>
</div> </div>
{/if} {#if payload.applicationType === 'group'}
</div> <div class="grid grid-cols-2 items-center">
<label for="groupName">Group Name</label>
<input name="groupName" id="groupName" required bind:value={payload.groupName} />
</div>
{/if}
<div class="w-full pt-10 text-center">
<button class="w-96 bg-orange-600 hover:bg-orange-500" type="submit"
>Register new OAuth application on GitLab</button
>
</div>
<Explainer
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 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">
<div class="flex h-8 items-center space-x-2">
<div class="text-xl font-bold text-white">Configuration</div>
<button
type="submit"
class:bg-orange-600={!loading}
class:hover:bg-orange-500={!loading}
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
>
</div>
<div class="grid grid-cols-2 items-start">
<div class="flex-col">
<label for="oauthId" class="pt-2">OAuth ID</label>
<Explainer
text="The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class='font-bold text-orange-600' >in the URL</span> of your GitLab OAuth Application."
/>
</div>
<input
on:change={checkOauthId}
bind:this={oauthIdEl}
name="oauthId"
id="oauthId"
type="number"
required
bind:value={payload.oauthId}
/>
</div>
{#if payload.applicationType === 'group'}
<div class="grid grid-cols-2 items-center">
<label for="groupName">Group Name</label>
<input name="groupName" id="groupName" required bind:value={payload.groupName} />
</div>
{/if}
<div class="grid grid-cols-2 items-center">
<label for="appId">Application ID</label>
<input name="appId" id="appId" required bind:value={payload.appId} />
</div>
<div class="grid grid-cols-2 items-center">
<label for="appSecret">Secret</label>
<input
name="appSecret"
id="appSecret"
type="password"
required
bind:value={payload.appSecret}
/>
</div>
</form>
{:else}
<div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmitSave} class="py-4">
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">General</div>
{#if $session.isAdmin}
<button
type="submit"
class:bg-orange-600={!loading}
class:hover:bg-orange-500={!loading}
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
>
<button on:click|preventDefault={changeSettings}>Change GitLab App Settings</button>
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="mt-2 grid grid-cols-2 items-center">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<input name="name" id="name" required bind:value={source.name} />
</div>
</div>
</form>
</div>
{/if}

View File

@@ -40,7 +40,7 @@
<span class="pr-2">{source.name}</span> <span class="pr-2">{source.name}</span>
</div> </div>
<div class="flex justify-center space-x-2 px-6 py-3"> <div class="flex justify-center px-6 pb-8">
{#if source.type === 'github'} {#if source.type === 'github'}
<Github bind:source /> <Github bind:source />
{:else if source.type === 'gitlab'} {:else if source.type === 'gitlab'}

View File

@@ -47,7 +47,7 @@
await post(`/teams/${id}/invitation/invite.json`, { await post(`/teams/${id}/invitation/invite.json`, {
teamId: team.id, teamId: team.id,
teamName: invitation.teamName, teamName: invitation.teamName,
email: invitation.email, email: invitation.email.toLowerCase(),
permission: invitation.permission permission: invitation.permission
}); });
return window.location.reload(); return window.location.reload();
@@ -98,39 +98,40 @@
<span class="arrow-right-applications px-1 text-cyan-500">></span> <span class="arrow-right-applications px-1 text-cyan-500">></span>
<span class="pr-2">{team.name}</span> <span class="pr-2">{team.name}</span>
</div> </div>
<div class="mx-auto max-w-4xl"> <div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmit}> <form on:submit|preventDefault={handleSubmit} class=" py-4">
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 pb-5">
<div class="title">Settings</div> <div class="title font-bold">Settings</div>
<div class="text-center"> <button class="bg-cyan-600 hover:bg-cyan-500" type="submit">Save</button>
<button class="bg-cyan-600 hover:bg-cyan-500" type="submit">Save</button> </div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="mt-2 grid grid-cols-2">
<div class="flex-col">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
{#if team.id === '0'}
<Explainer
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)."
/>
{/if}
</div>
<input id="name" name="name" placeholder="name" bind:value={team.name} />
</div> </div>
</div> </div>
<div class="mx-2 flex items-center space-x-2 px-4 sm:px-6">
<label for="name">Name</label>
<input id="name" name="name" placeholder="name" bind:value={team.name} />
</div>
{#if team.id === '0'}
<div class="px-8 pt-4 text-left">
<Explainer
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}
</form> </form>
<div class="flex space-x-1 py-5 px-6 pt-10 font-bold"> <div class="flex space-x-1 py-5 pt-10 font-bold">
<div class="title">Members</div> <div class="title">Members</div>
</div> </div>
<div class="px-4 sm:px-6"> <div class="px-4 sm:px-6">
<table class="mx-2 w-full table-auto text-left"> <table class="w-full border-separate text-left">
<tr class="h-8 border-b border-coolgray-400"> <thead>
<th scope="col">Email</th> <tr class="h-8 border-b border-coolgray-400">
<th scope="col">Permission</th> <th scope="col">Email</th>
<th scope="col" class="text-center">Actions</th> <th scope="col">Permission</th>
</tr> <th scope="col" class="text-center">Actions</th>
</tr>
</thead>
{#each permissions as permission} {#each permissions as permission}
<tr class="text-xs"> <tr class="text-xs">
<td class="py-4" <td class="py-4"
@@ -176,25 +177,18 @@
{/each} {/each}
</table> </table>
</div> </div>
</div> {#if $session.isAdmin}
{#if $session.isAdmin} <form on:submit|preventDefault={sendInvitation} class="py-5 pt-10">
<div class="mx-auto max-w-4xl pt-8"> <div class="flex space-x-1">
<form on:submit|preventDefault={sendInvitation}> <div class="flex space-x-1">
<div class="flex space-x-1 p-6">
<div>
<div class="title font-bold">Invite new member</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> <button class="bg-cyan-600 hover:bg-cyan-500" type="submit">Send invitation</button>
</div> </div>
</div> </div>
<div class="flex-col space-y-2 px-4 sm:px-6"> <Explainer
text="You can only invite registered users at the moment - will be extended soon."
/>
<div class="flex-col space-y-2 px-4 pt-5 sm:px-6">
<div class="flex space-x-0"> <div class="flex space-x-0">
<input <input
bind:value={invitation.email} bind:value={invitation.email}
@@ -205,18 +199,20 @@
<div class="flex-1" /> <div class="flex-1" />
<button <button
on:click={() => (invitation.permission = 'read')} on:click={() => (invitation.permission = 'read')}
class="rounded-none rounded-l" class="rounded-none rounded-l border border-dashed border-transparent"
type="button" type="button"
class:border-coolgray-300={invitation.permission !== 'read'}
class:bg-pink-500={invitation.permission === 'read'}>Read</button class:bg-pink-500={invitation.permission === 'read'}>Read</button
> >
<button <button
on:click={() => (invitation.permission = 'admin')} on:click={() => (invitation.permission = 'admin')}
class="rounded-none rounded-r" class="rounded-none rounded-r border border-dashed border-transparent"
type="button" type="button"
class:border-coolgray-300={invitation.permission !== 'admin'}
class:bg-red-500={invitation.permission === 'admin'}>Admin</button class:bg-red-500={invitation.permission === 'admin'}>Admin</button
> >
</div> </div>
</div> </div>
</form> </form>
</div> {/if}
{/if} </div>

View File

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

View File

@@ -35,14 +35,14 @@ main,
} }
input { 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 pr-20 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 { 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 min-w-[24rem] rounded border border-transparent bg-transparent bg-coolgray-200 p-2 pr-20 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 { select {
@apply rounded bg-coolgray-200 p-2 text-xs font-bold tracking-tight text-white outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:text-stone-600 md:text-sm; @apply h-12 w-96 rounded bg-coolgray-200 p-2 text-xs font-bold tracking-tight text-white outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:text-stone-600 md:text-sm;
} }
label { label {