Compare commits

...

16 Commits

Author SHA1 Message Date
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
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
42 changed files with 529 additions and 237 deletions

View File

@@ -1,7 +1,7 @@
{ {
"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.17",
"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",

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

@@ -109,13 +109,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,7 +14,7 @@
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 actionsShow = false;
let isHttps = browser && window.location.protocol === 'https:'; let isHttps = browser && window.location.protocol === 'https:';
@@ -29,11 +29,7 @@
} }
</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
@@ -77,69 +73,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

@@ -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

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

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,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

@@ -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'));

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

@@ -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,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

@@ -34,6 +34,7 @@
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;
@@ -59,7 +60,7 @@
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'
@@ -78,7 +79,7 @@
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) || {};
@@ -94,7 +95,7 @@
} }
} 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(
@@ -111,7 +112,7 @@
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) || {};

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

@@ -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

@@ -197,7 +197,7 @@
value={application.gitSource.name} value={application.gitSource.name}
id="gitSource" id="gitSource"
disabled disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500" class="cursor-pointer hover:bg-coolgray-500"
/></a /></a
> >
</div> </div>
@@ -214,7 +214,7 @@
value="{application.repository}/{application.branch}" value="{application.repository}/{application.branch}"
id="repository" id="repository"
disabled disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500" class="cursor-pointer hover:bg-coolgray-500"
/></a /></a
> >
</div> </div>
@@ -232,7 +232,7 @@
value={application.buildPack} value={application.buildPack}
id="buildPack" id="buildPack"
disabled disabled
class="cursor-pointer bg-coolgray-200 hover:bg-coolgray-500" class="cursor-pointer hover:bg-coolgray-500"
/></a /></a
> >
</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);
} }
@@ -56,8 +61,7 @@
<td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white"> <td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white">
<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"
@@ -68,16 +72,13 @@
/> />
</td> </td>
<td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white"> <td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white">
<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="whitespace-nowrap px-6 py-2 text-center text-sm font-medium text-white">
@@ -132,11 +133,20 @@
<td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white"> <td class="whitespace-nowrap px-6 py-2 text-sm font-medium text-white">
{#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-col space-y-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="w-24 bg-green-600 hover:bg-green-500" on:click={() => saveSecret(false)}
>Set</button
>
</div>
{#if !isPRMRSecret}
<div class="flex justify-center items-end">
<button class="w-24 bg-red-600 hover:bg-red-500" on:click={removeSecret}>Remove</button>
</div>
{/if}
</div> </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

@@ -67,10 +67,10 @@
<tbody class=""> <tbody class="">
{#each secrets as secret} {#each secrets as secret}
{#key secret.id} {#key secret.id}
<tr class="hover:bg-coolgray-200"> <tr class="h-20 transition duration-100 hover:bg-coolgray-400">
<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,7 +6,7 @@
<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">Default Database</label>
<CopyPasswordField <CopyPasswordField

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

View File

@@ -6,7 +6,7 @@
<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">Root User</label>
<CopyPasswordField <CopyPasswordField

View File

@@ -6,7 +6,7 @@
<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">Default Database</label>
<CopyPasswordField <CopyPasswordField

View File

@@ -6,7 +6,7 @@
<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">Default Database</label>
<CopyPasswordField <CopyPasswordField

View File

@@ -6,7 +6,7 @@
<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">Password</label>
<CopyPasswordField <CopyPasswordField

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

@@ -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,13 @@ 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;
console.log(fqdn);
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 +60,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) {

View File

@@ -159,7 +159,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

@@ -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;
@@ -121,12 +122,16 @@
<Explainer <Explainer
customClass="w-full" customClass="w-full"
text="<span class='font-bold text-base'>Scopes required:</span> text="<span class='font-bold text-base text-white'>Scopes required:</span>
<br>- api (Access the authenticated user's API) <br>- <span class='text-orange-500 font-bold'>api</span> (Access the authenticated user's API)
<br>- read_repository (Allows read-only access to the repository) <br>- <span class='text-orange-500 font-bold'>read_repository</span> (Allows read-only access to the repository)
<br>- email (Allows read-only access to the user's primary email address using OpenID Connect) <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>
<br>For extra security, you can add Expire access tokens!" <br>For extra security, you can set Expire access tokens!
<br><br>Webhook URL: <span class='text-orange-500 font-bold'>{browser
? window.location.origin
: ''}/webhooks/gitlab</span>
<br>But if you will set a custom domain name for Coolify, use that instead."
/> />
</form> </form>
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4 pt-10"> <form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4 pt-10">

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,10 +35,10 @@ 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 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 w-96 rounded border border-transparent bg-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:border disabled:border-dashed disabled:border-coolgray-300 disabled:bg-transparent md:text-sm;
} }
select { select {