Merge branch 'main' into arm

This commit is contained in:
Andras Bacsai 2022-04-11 20:29:29 +02:00
commit abaa13fda8
143 changed files with 3665 additions and 1496 deletions

View File

@ -2,4 +2,5 @@ COOLIFY_APP_ID=
COOLIFY_SECRET_KEY=12341234123412341234123412341234
COOLIFY_DATABASE_URL=file:../db/dev.db
COOLIFY_SENTRY_DSN=
COOLIFY_IS_ON="docker"
COOLIFY_IS_ON="docker"
COOLIFY_WHITE_LABELED="false"

View File

@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint-staged
pnpm lint-staged

View File

@ -16,7 +16,7 @@ First of all, thank you for considering to contribute to my project! It means a
- Push to your fork repo
- Create a pull request: https://github.com/coollabsio/compare
- Write a proper description
- Click "Change to draft"
- Open the pull request to review
# How to start after you set up your local fork?
@ -29,7 +29,8 @@ You need to have [Docker Engine](https://docs.docker.com/engine/install/) instal
- Copy `.env.template` to `.env` and set the `COOLIFY_APP_ID` environment variable to something cool.
- Install dependencies with `pnpm install`.
- Need to create a local SQlite database with `pnpm db:push`.
- This will apply all migrations and seed the database at `db/dev.db`.
- This will apply all migrations at `db/dev.db`.
- Seed the database with base entities with `pnpm db:seed`
- You can start coding after starting `pnpm dev`.
## Database migrations

View File

@ -20,7 +20,7 @@ COPY --from=coollabsio/prisma-engine:latest /prisma-engines/query-engine /prisma
COPY --from=install /app/node_modules ./node_modules
COPY . .
RUN apk add --no-cache git openssh-client curl jq sqlite
RUN apk add --no-cache git git-lfs openssh-client curl jq cmake sqlite openssl
RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@6
RUN pnpm add -g pnpm

View File

@ -6,7 +6,7 @@ An open-source & self-hostable Heroku / Netlify alternative.
https://demo.coolify.io/
(If it is unresponsible, that means someone overloaded the server. 🙃)
(If it is unresponsive, that means someone overloaded the server. 🙃)
## How to install

View File

@ -1,10 +1,10 @@
{
"name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.2.7",
"version": "2.4.2",
"license": "AGPL-3.0",
"scripts": {
"dev": "docker-compose -f docker-compose-dev.yaml up -d && NODE_ENV=development svelte-kit dev",
"dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev",
"dev:stop": "docker-compose -f docker-compose-dev.yaml down",
"dev:logs": "docker-compose -f docker-compose-dev.yaml logs -f --tail 10",
"studio": "npx prisma studio",
@ -29,13 +29,14 @@
"@sveltejs/adapter-node": "1.0.0-next.73",
"@sveltejs/kit": "1.0.0-next.303",
"@types/js-cookie": "3.0.1",
"@types/js-yaml": "^4.0.5",
"@types/js-yaml": "4.0.5",
"@types/node": "17.0.23",
"@types/node-forge": "1.0.1",
"@typescript-eslint/eslint-plugin": "4.31.1",
"@typescript-eslint/parser": "4.31.1",
"@zerodevx/svelte-toast": "0.7.1",
"autoprefixer": "10.4.4",
"cross-env": "7.0.3",
"cross-var": "1.1.0",
"eslint": "7.32.0",
"eslint-config-prettier": "8.5.0",
@ -50,7 +51,7 @@
"svelte": "3.46.4",
"svelte-check": "2.4.6",
"svelte-preprocess": "4.10.4",
"svelte-select": "^4.4.7",
"svelte-select": "4.4.7",
"tailwindcss": "3.0.23",
"ts-node": "10.7.0",
"tslib": "2.3.1",
@ -65,7 +66,7 @@
"bullmq": "1.78.1",
"compare-versions": "4.1.3",
"cookie": "0.4.2",
"cooltipz-css": "^2.1.0",
"cooltipz-css": "2.1.0",
"cuid": "2.1.8",
"dayjs": "1.11.0",
"dockerode": "3.3.1",
@ -76,10 +77,11 @@
"js-cookie": "3.0.1",
"js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1",
"mustache": "^4.2.0",
"mustache": "4.2.0",
"node-forge": "1.3.0",
"p-limit": "4.0.0",
"svelte-kit-cookie-session": "2.1.2",
"tailwindcss-scrollbar": "^0.1.0",
"tailwindcss-scrollbar": "0.1.0",
"unique-names-generator": "4.7.1"
},
"prisma": {

43
pnpm-lock.yaml generated
View File

@ -7,7 +7,7 @@ specifiers:
'@sveltejs/adapter-node': 1.0.0-next.73
'@sveltejs/kit': 1.0.0-next.303
'@types/js-cookie': 3.0.1
'@types/js-yaml': ^4.0.5
'@types/js-yaml': 4.0.5
'@types/node': 17.0.23
'@types/node-forge': 1.0.1
'@typescript-eslint/eslint-plugin': 4.31.1
@ -18,7 +18,8 @@ specifiers:
bullmq: 1.78.1
compare-versions: 4.1.3
cookie: 0.4.2
cooltipz-css: ^2.1.0
cooltipz-css: 2.1.0
cross-env: 7.0.3
cross-var: 1.1.0
cuid: 2.1.8
dayjs: 1.11.0
@ -35,8 +36,9 @@ specifiers:
js-yaml: 4.1.0
jsonwebtoken: 8.5.1
lint-staged: 12.3.7
mustache: ^4.2.0
mustache: 4.2.0
node-forge: 1.3.0
p-limit: 4.0.0
postcss: 8.4.12
prettier: 2.6.1
prettier-plugin-svelte: 2.6.0
@ -46,9 +48,9 @@ specifiers:
svelte-check: 2.4.6
svelte-kit-cookie-session: 2.1.2
svelte-preprocess: 4.10.4
svelte-select: ^4.4.7
svelte-select: 4.4.7
tailwindcss: 3.0.23
tailwindcss-scrollbar: ^0.1.0
tailwindcss-scrollbar: 0.1.0
ts-node: 10.7.0
tslib: 2.3.1
typescript: 4.6.3
@ -75,6 +77,7 @@ dependencies:
jsonwebtoken: 8.5.1
mustache: 4.2.0
node-forge: 1.3.0
p-limit: 4.0.0
svelte-kit-cookie-session: 2.1.2
tailwindcss-scrollbar: 0.1.0_tailwindcss@3.0.23
unique-names-generator: 4.7.1
@ -90,6 +93,7 @@ devDependencies:
'@typescript-eslint/parser': 4.31.1_eslint@7.32.0+typescript@4.6.3
'@zerodevx/svelte-toast': 0.7.1
autoprefixer: 10.4.4_postcss@8.4.12
cross-env: 7.0.3
cross-var: 1.1.0
eslint: 7.32.0
eslint-config-prettier: 8.5.0_eslint@7.32.0
@ -1991,6 +1995,17 @@ packages:
luxon: 1.28.0
dev: false
/cross-env/7.0.3:
resolution:
{
integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==
}
engines: { node: '>=10.14', npm: '>=6', yarn: '>=1' }
hasBin: true
dependencies:
cross-spawn: 7.0.3
dev: true
/cross-spawn/5.1.0:
resolution: { integrity: sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= }
dependencies:
@ -4016,6 +4031,16 @@ packages:
engines: { node: '>=12.20' }
dev: false
/p-limit/4.0.0:
resolution:
{
integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==
}
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
dependencies:
yocto-queue: 1.0.0
dev: false
/p-map/2.1.0:
resolution:
{
@ -5392,3 +5417,11 @@ packages:
}
engines: { node: '>=6' }
dev: true
/yocto-queue/1.0.0:
resolution:
{
integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
}
engines: { node: '>=12.20' }
dev: false

View File

@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "pythonModule" TEXT;
ALTER TABLE "Application" ADD COLUMN "pythonVariable" TEXT;
ALTER TABLE "Application" ADD COLUMN "pythonWSGI" TEXT;

View File

@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE "MeiliSearch" (
"id" TEXT NOT NULL PRIMARY KEY,
"masterKey" TEXT NOT NULL,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "MeiliSearch_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "MeiliSearch_serviceId_key" ON "MeiliSearch"("serviceId");

View File

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

View File

@ -0,0 +1,5 @@
-- DropIndex
DROP INDEX "ApplicationPersistentStorage_path_key";
-- DropIndex
DROP INDEX "ApplicationPersistentStorage_applicationId_key";

View File

@ -88,6 +88,9 @@ model Application {
baseDirectory String?
publishDirectory String?
phpModules String?
pythonWSGI String?
pythonModule String?
pythonVariable String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
settings ApplicationSettings?
@ -115,8 +118,8 @@ model ApplicationSettings {
model ApplicationPersistentStorage {
id String @id @default(cuid())
application Application @relation(fields: [applicationId], references: [id])
applicationId String @unique
path String @unique
applicationId String
path String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@ -281,6 +284,7 @@ model Service {
wordpress Wordpress?
ghost Ghost?
serviceSecret ServiceSecret[]
meiliSearch MeiliSearch?
}
model PlausibleAnalytics {
@ -329,6 +333,12 @@ model Wordpress {
mysqlRootUserPassword String
mysqlDatabase String?
mysqlPublicPort Int?
ftpEnabled Boolean @default(false)
ftpUser String?
ftpPassword String?
ftpPublicPort Int?
ftpHostKey String?
ftpHostKeyPrivate String?
serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
@ -350,3 +360,12 @@ model Ghost {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model MeiliSearch {
id String @id @default(cuid())
masterKey String
serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

6
src/app.d.ts vendored
View File

@ -15,18 +15,20 @@ declare namespace App {
readOnly: boolean;
source: string;
settings: string;
database: Record<string, any>;
versions: string;
privatePort: string;
}
}
interface SessionData {
whiteLabeled: boolean;
version?: string;
userId?: string | null;
teamId?: string | null;
permission?: string;
isAdmin?: boolean;
expires?: string | null;
gitlabToken?: string | null;
ghToken?: string | null;
}
type DateTimeFormatOptions = {

View File

@ -2,7 +2,6 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Coolify</title>
%svelte.head%

View File

@ -7,6 +7,8 @@ import { version } from '$lib/common';
import cookie from 'cookie';
import { dev } from '$app/env';
const whiteLabeled = process.env['COOLIFY_WHITE_LABELED'] === 'true';
export const handle = handleSession(
{
secret: process.env['COOLIFY_SECRET_KEY'],
@ -71,6 +73,7 @@ export const handle = handleSession(
export const getSession: GetSession = function ({ locals }) {
return {
version,
whiteLabeled,
...locals.session.data
};
};

View File

@ -100,6 +100,7 @@ export const setDefaultConfiguration = async (data) => {
if (buildPack === 'static') port = 80;
else if (buildPack === 'node') port = 3000;
else if (buildPack === 'php') port = 80;
else if (buildPack === 'python') port = 8000;
}
if (!installCommand) installCommand = template?.installCommand || 'yarn install';
if (!startCommand) startCommand = template?.startCommand || 'yarn start';
@ -123,20 +124,13 @@ export const setDefaultConfiguration = async (data) => {
export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId) {
try {
// TODO: Write full .dockerignore for all deployments!!
if (buildPack === 'php') {
await fs.writeFile(
`${workdir}/.htaccess`,
`
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)$ index.php [QSA,L]
`
);
await fs.writeFile(`${workdir}/entrypoint.sh`, `chown -R 1000 /app`);
saveBuildLog({ line: 'Copied default configuration file for PHP.', buildId, applicationId });
await saveBuildLog({
line: 'Copied default configuration file for PHP.',
buildId,
applicationId
});
} else if (staticDeployments.includes(buildPack)) {
await fs.writeFile(
`${workdir}/nginx.conf`,
@ -190,7 +184,7 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
}
`
);
saveBuildLog({ line: 'Copied default configuration file.', buildId, applicationId });
await saveBuildLog({ line: 'Copied default configuration file.', buildId, applicationId });
}
} catch (error) {
console.log(error);

View File

@ -28,11 +28,11 @@ export default async function ({
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}

View File

@ -12,6 +12,7 @@ import php from './php';
import rust from './rust';
import astro from './static';
import eleventy from './static';
import python from './python';
export {
node,
@ -27,5 +28,6 @@ export {
php,
rust,
astro,
eleventy
eleventy,
python
};

View File

@ -23,11 +23,11 @@ const createDockerfile = async (data, image): Promise<void> => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}

View File

@ -24,11 +24,11 @@ const createDockerfile = async (data, image): Promise<void> => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}

View File

@ -23,11 +23,11 @@ const createDockerfile = async (data, image): Promise<void> => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}

View File

@ -1,23 +1,45 @@
import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => {
const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
const { workdir, baseDirectory } = data;
const Dockerfile: Array<string> = [];
let composerFound = false;
try {
await fs.readFile(`${workdir}${baseDirectory || ''}/composer.json`);
composerFound = true;
} catch (error) {}
Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`COPY .${baseDirectory || ''} /app`);
Dockerfile.push(`COPY /.htaccess .`);
if (htaccessFound) {
Dockerfile.push(`COPY .${baseDirectory || ''}/.htaccess ./`);
}
if (composerFound) {
Dockerfile.push(`RUN composer install`);
}
Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`);
Dockerfile.push(`EXPOSE 80`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
const { workdir, baseDirectory } = data;
try {
const image = 'webdevops/php-nginx';
await createDockerfile(data, image);
let htaccessFound = false;
try {
await fs.readFile(`${workdir}${baseDirectory || ''}/.htaccess`);
htaccessFound = true;
} catch (e) {
//
}
const image = htaccessFound
? 'webdevops/php-apache:8.0-alpine'
: 'webdevops/php-nginx:8.0-alpine';
await createDockerfile(data, image, htaccessFound);
await buildImage(data);
} catch (error) {
throw error;

View File

@ -0,0 +1,71 @@
import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => {
const {
workdir,
port,
baseDirectory,
secrets,
pullmergeRequestId,
pythonWSGI,
pythonModule,
pythonVariable
} = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
});
}
if (pythonWSGI?.toLowerCase() === 'gunicorn') {
Dockerfile.push(`RUN pip install gunicorn`);
} else if (pythonWSGI?.toLowerCase() === 'uwsgi') {
Dockerfile.push(`RUN apk add --no-cache uwsgi-python3`);
// Dockerfile.push(`RUN pip install --no-cache-dir uwsgi`)
}
try {
await fs.stat(`${workdir}${baseDirectory || ''}/requirements.txt`);
Dockerfile.push(`COPY .${baseDirectory || ''}/requirements.txt ./`);
Dockerfile.push(`RUN pip install --no-cache-dir -r .${baseDirectory || ''}/requirements.txt`);
} catch (e) {
//
}
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`EXPOSE ${port}`);
if (pythonWSGI?.toLowerCase() === 'gunicorn') {
Dockerfile.push(`CMD gunicorn -w=4 -b=0.0.0.0:8000 ${pythonModule}:${pythonVariable}`);
} else if (pythonWSGI?.toLowerCase() === 'uwsgi') {
Dockerfile.push(
`CMD uwsgi --master -p 4 --http-socket 0.0.0.0:8000 --uid uwsgi --plugins python3 --protocol uwsgi --wsgi ${pythonModule}:${pythonVariable}`
);
} else {
Dockerfile.push(`CMD python ${pythonModule}`);
}
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const image = 'python:3-alpine';
await createDockerfile(data, image);
await buildImage(data);
} catch (error) {
throw error;
}
}

View File

@ -22,11 +22,11 @@ const createDockerfile = async (data, image): Promise<void> => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}

View File

@ -46,13 +46,20 @@ const customConfig: Config = {
export const version = currentVersion;
export const asyncExecShell = util.promisify(child.exec);
export const asyncSleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay));
export const sentry = Sentry;
export const uniqueName = () => uniqueNamesGenerator(customConfig);
export const saveBuildLog = async ({ line, buildId, applicationId }) => {
const addTimestamp = `${generateTimestamp()} ${line}`;
return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId });
if (line) {
if (line.includes('ghs_')) {
const regex = /ghs_.*@/g;
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
}
const addTimestamp = `${generateTimestamp()} ${line}`;
return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId });
}
};
export const isTeamIdTokenAvailable = (request) => {
@ -80,7 +87,7 @@ export const getTeam = (event) => {
export const getUserDetails = async (event, isAdminRequired = true) => {
const teamId = getTeam(event);
const userId = event.locals.session.data.userId || null;
const userId = event?.locals?.session?.data?.userId || null;
const { permission = 'read' } = await db.prisma.permission.findFirst({
where: { teamId, userId },
select: { permission: true },
@ -95,6 +102,7 @@ export const getUserDetails = async (event, isAdminRequired = true) => {
message: 'OK'
}
};
if (isAdminRequired && permission !== 'admin' && permission !== 'owner') {
payload.status = 401;
payload.body.message =

View File

@ -0,0 +1,25 @@
<script>
export let database;
import Clickhouse from './svg/databases/Clickhouse.svelte';
import CouchDb from './svg/databases/CouchDB.svelte';
import MongoDb from './svg/databases/MongoDB.svelte';
import MySql from './svg/databases/MySQL.svelte';
import PostgreSql from './svg/databases/PostgreSQL.svelte';
import Redis from './svg/databases/Redis.svelte';
</script>
<span class="relative">
{#if database.type === 'clickhouse'}
<Clickhouse />
{:else if database.type === 'couchdb'}
<CouchDb />
{:else if database.type === 'mongodb'}
<MongoDb />
{:else if database.type === 'mysql'}
<MySql />
{:else if database.type === 'postgresql'}
<PostgreSql />
{:else if database.type === 'redis'}
<Redis />
{/if}
</span>

View File

@ -0,0 +1,55 @@
<script>
export let service;
import Ghost from './svg/services/Ghost.svelte';
import LanguageTool from './svg/services/LanguageTool.svelte';
import MinIo from './svg/services/MinIO.svelte';
import N8n from './svg/services/N8n.svelte';
import NocoDb from './svg/services/NocoDB.svelte';
import PlausibleAnalytics from './svg/services/PlausibleAnalytics.svelte';
import UptimeKuma from './svg/services/UptimeKuma.svelte';
import VaultWarden from './svg/services/VaultWarden.svelte';
import VsCodeServer from './svg/services/VSCodeServer.svelte';
import Wordpress from './svg/services/Wordpress.svelte';
</script>
{#if service.type === 'plausibleanalytics'}
<a href="https://plausible.io" target="_blank">
<PlausibleAnalytics />
</a>
{:else if service.type === 'nocodb'}
<a href="https://nocodb.com" target="_blank">
<NocoDb />
</a>
{:else if service.type === 'minio'}
<a href="https://min.io" target="_blank">
<MinIo />
</a>
{:else if service.type === 'vscodeserver'}
<a href="https://coder.com" target="_blank">
<VsCodeServer />
</a>
{:else if service.type === 'wordpress'}
<a href="https://wordpress.org" target="_blank">
<Wordpress />
</a>
{:else if service.type === 'vaultwarden'}
<a href="https://github.com/dani-garcia/vaultwarden" target="_blank">
<VaultWarden />
</a>
{:else if service.type === 'languagetool'}
<a href="https://languagetool.org/dev" target="_blank">
<LanguageTool />
</a>
{:else if service.type === 'n8n'}
<a href="https://n8n.io" target="_blank">
<N8n />
</a>
{:else if service.type === 'uptimekuma'}
<a href="https://github.com/louislam/uptime-kuma" target="_blank">
<UptimeKuma />
</a>
{:else if service.type === 'ghost'}
<a href="https://ghost.org" target="_blank">
<Ghost />
</a>
{/if}

View File

@ -7,6 +7,7 @@
export let isCenter = true;
export let disabled = false;
export let dataTooltip = null;
export let loading = false;
</script>
<div class="flex items-center py-4 pr-8">
@ -26,9 +27,10 @@
on:click
aria-pressed="false"
class="relative mx-20 inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out"
class:opacity-50={disabled}
class:bg-green-600={setting}
class:bg-stone-700={!setting}
class:opacity-50={disabled || loading}
class:bg-green-600={!loading && setting}
class:bg-stone-700={!loading && !setting}
class:bg-yellow-500={loading}
>
<span class="sr-only">Use setting</span>
<span
@ -40,6 +42,7 @@
class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
class:opacity-0={setting}
class:opacity-100={!setting}
class:animate-spin={loading}
aria-hidden="true"
>
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
@ -57,6 +60,7 @@
aria-hidden="true"
class:opacity-100={setting}
class:opacity-0={!setting}
class:animate-spin={loading}
>
<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
<path

View File

@ -19,7 +19,7 @@ export const staticDeployments = [
'astro',
'eleventy'
];
export const notNodeDeployments = ['php', 'docker', 'rust'];
export const notNodeDeployments = ['php', 'docker', 'rust', 'python'];
export function getDomain(domain) {
return domain?.replace('https://', '').replace('http://', '');
@ -37,3 +37,148 @@ export function dashify(str: string, options?: any): string {
.replace(/-{2,}/g, (m) => (options && options.condense ? '-' : m))
.toLowerCase();
}
export function changeQueryParams(buildId) {
const queryParams = new URLSearchParams(window.location.search);
queryParams.set('buildId', buildId);
return history.pushState(null, null, '?' + queryParams.toString());
}
export const supportedDatabaseTypesAndVersions = [
{
name: 'mongodb',
fancyName: 'MongoDB',
baseImage: 'bitnami/mongodb',
versions: ['5.0', '4.4', '4.2']
},
{ name: 'mysql', fancyName: 'MySQL', baseImage: 'bitnami/mysql', versions: ['8.0', '5.7'] },
{
name: 'postgresql',
fancyName: 'PostgreSQL',
baseImage: 'bitnami/postgresql',
versions: ['14.2.0', '13.6.0', '12.10.0 ', '11.15.0', '10.20.0']
},
{
name: 'redis',
fancyName: 'Redis',
baseImage: 'bitnami/redis',
versions: ['6.2', '6.0', '5.0']
},
{ name: 'couchdb', fancyName: 'CouchDB', baseImage: 'bitnami/couchdb', versions: ['3.2.1'] }
];
export const supportedServiceTypesAndVersions = [
{
name: 'plausibleanalytics',
fancyName: 'Plausible Analytics',
baseImage: 'plausible/analytics',
images: ['bitnami/postgresql:13.2.0', 'yandex/clickhouse-server:21.3.2.5'],
versions: ['latest', 'stable'],
recommendedVersion: 'stable',
ports: {
main: 8000
}
},
{
name: 'nocodb',
fancyName: 'NocoDB',
baseImage: 'nocodb/nocodb',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
{
name: 'minio',
fancyName: 'MinIO',
baseImage: 'minio/minio',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 9001
}
},
{
name: 'vscodeserver',
fancyName: 'VSCode Server',
baseImage: 'codercom/code-server',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
{
name: 'wordpress',
fancyName: 'Wordpress',
baseImage: 'wordpress',
images: ['bitnami/mysql:5.7'],
versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'],
recommendedVersion: 'latest',
ports: {
main: 80
}
},
{
name: 'vaultwarden',
fancyName: 'Vaultwarden',
baseImage: 'vaultwarden/server',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 80
}
},
{
name: 'languagetool',
fancyName: 'LanguageTool',
baseImage: 'silviof/docker-languagetool',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8010
}
},
{
name: 'n8n',
fancyName: 'n8n',
baseImage: 'n8nio/n8n',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 5678
}
},
{
name: 'uptimekuma',
fancyName: 'Uptime Kuma',
baseImage: 'louislam/uptime-kuma',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 3001
}
},
{
name: 'ghost',
fancyName: 'Ghost',
baseImage: 'bitnami/ghost',
images: ['bitnami/mariadb'],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 2368
}
},
{
name: 'meilisearch',
fancyName: 'Meilisearch',
baseImage: 'getmeili/meilisearch',
images: [],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 7700
}
}
];

View File

@ -0,0 +1,45 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
viewBox="0 0 127 74"
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'}
xmlns="http://www.w3.org/2000/svg"
><path
d="M.825 73.993l23.244-59.47A21.85 21.85 0 0144.42.625h14.014L35.19 60.096a21.85 21.85 0 01-20.352 13.897H.825z"
fill="url(#meilisearch_logo_svg__paint0_linear_0_6)"
/><path
d="M34.925 73.993l23.243-59.47A21.85 21.85 0 0178.52.626h14.013L69.29 60.096a21.85 21.85 0 01-20.351 13.897H34.925z"
fill="url(#meilisearch_logo_svg__paint1_linear_0_6)"
/><path
d="M69.026 73.993l23.244-59.47A21.85 21.85 0 01112.621.626h14.014l-23.244 59.47a21.851 21.851 0 01-20.352 13.897H69.026z"
fill="url(#meilisearch_logo_svg__paint2_linear_0_6)"
/><defs
><linearGradient
id="meilisearch_logo_svg__paint0_linear_0_6"
x1="126.635"
y1="-4.978"
x2="0.825"
y2="66.098"
gradientUnits="userSpaceOnUse"
><stop stop-color="#FF5CAA" /><stop offset="1" stop-color="#FF4E62" /></linearGradient
><linearGradient
id="meilisearch_logo_svg__paint1_linear_0_6"
x1="126.635"
y1="-4.978"
x2="0.825"
y2="66.098"
gradientUnits="userSpaceOnUse"
><stop stop-color="#FF5CAA" /><stop offset="1" stop-color="#FF4E62" /></linearGradient
><linearGradient
id="meilisearch_logo_svg__paint2_linear_0_6"
x1="126.635"
y1="-4.978"
x2="0.825"
y2="66.098"
gradientUnits="userSpaceOnUse"
><stop stop-color="#FF5CAA" /><stop offset="1" stop-color="#FF4E62" /></linearGradient
></defs
></svg
>

View File

@ -146,6 +146,13 @@ export function findBuildPack(pack, packageManager = 'npm') {
port: 80
};
}
if (pack === 'python') {
return {
...metaData,
startCommand: null,
port: 8000
};
}
return {
name: 'node',
fancyName: 'Node.js',
@ -249,9 +256,18 @@ export const buildPacks = [
fancyName: 'Rust',
hoverColor: 'hover:bg-pink-700',
color: 'bg-pink-700'
},
{
name: 'python',
fancyName: 'Python',
hoverColor: 'hover:bg-green-700',
color: 'bg-green-700'
}
];
export const scanningTemplates = {
'@sveltejs/kit': {
buildPack: 'nodejs'
},
astro: {
buildPack: 'astro'
},

View File

@ -5,7 +5,13 @@ import { getDomain, removeDestinationDocker } from '$lib/common';
import { prisma } from './common';
export async function listApplications(teamId) {
return await prisma.application.findMany({ where: { teams: { some: { id: teamId } } } });
if (teamId === '0') {
return await prisma.application.findMany({ include: { teams: true } });
}
return await prisma.application.findMany({
where: { teams: { some: { id: teamId } } },
include: { teams: true }
});
}
export async function newApplication({ name, teamId }) {
@ -67,7 +73,11 @@ export async function removeApplication({ id, teamId }) {
await prisma.build.deleteMany({ where: { applicationId: id } });
await prisma.secret.deleteMany({ where: { applicationId: id } });
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } });
await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } });
if (teamId === '0') {
await prisma.application.deleteMany({ where: { id } });
} else {
await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } });
}
}
export async function getApplicationWebhook({ projectId, branch }) {
@ -130,16 +140,30 @@ export async function getApplicationById({ id }) {
return { ...body };
}
export async function getApplication({ id, teamId }) {
let body = await prisma.application.findFirst({
where: { id, teams: { some: { id: teamId } } },
include: {
destinationDocker: true,
settings: true,
gitSource: { include: { githubApp: true, gitlabApp: true } },
secrets: true,
persistentStorage: true
}
});
let body = {};
if (teamId === '0') {
body = await prisma.application.findFirst({
where: { id },
include: {
destinationDocker: true,
settings: true,
gitSource: { include: { githubApp: true, gitlabApp: true } },
secrets: true,
persistentStorage: true
}
});
} else {
body = await prisma.application.findFirst({
where: { id, teams: { some: { id: teamId } } },
include: {
destinationDocker: true,
settings: true,
gitSource: { include: { githubApp: true, gitlabApp: true } },
secrets: true,
persistentStorage: true
}
});
}
if (body?.gitSource?.githubApp?.clientSecret) {
body.gitSource.githubApp.clientSecret = decrypt(body.gitSource.githubApp.clientSecret);
@ -214,11 +238,15 @@ export async function configureApplication({
buildCommand,
startCommand,
baseDirectory,
publishDirectory
publishDirectory,
pythonWSGI,
pythonModule,
pythonVariable
}) {
return await prisma.application.update({
where: { id },
data: {
name,
buildPack,
fqdn,
port,
@ -227,7 +255,9 @@ export async function configureApplication({
startCommand,
baseDirectory,
publishDirectory,
name
pythonWSGI,
pythonModule,
pythonVariable
}
});
}

View File

@ -1,5 +1,9 @@
import { dev } from '$app/env';
import { sentry } from '$lib/common';
import {
supportedDatabaseTypesAndVersions,
supportedServiceTypesAndVersions
} from '$lib/components/common';
import * as Prisma from '@prisma/client';
import { default as ProdPrisma } from '@prisma/client';
import type { PrismaClientOptions } from '@prisma/client/runtime';
@ -46,7 +50,9 @@ export function ErrorHandler(e) {
if (e.message?.includes('git clone')) {
truncatedError.message = 'git clone failed';
}
sentry.captureException(truncatedError);
if (!e.message?.includes('Coolify Proxy is not running')) {
sentry.captureException(truncatedError);
}
const payload = {
status: truncatedError.status || 500,
body: {
@ -80,124 +86,6 @@ export async function generateSshKeyPair(): Promise<{ publicKey: string; private
});
}
export const supportedDatabaseTypesAndVersions = [
{
name: 'mongodb',
fancyName: 'MongoDB',
baseImage: 'bitnami/mongodb',
versions: ['5.0.5', '4.4.11', '4.2.18', '4.0.27']
},
{ name: 'mysql', fancyName: 'MySQL', baseImage: 'bitnami/mysql', versions: ['8.0.27', '5.7.36'] },
{
name: 'postgresql',
fancyName: 'PostgreSQL',
baseImage: 'bitnami/postgresql',
versions: ['14.1.0', '13.5.0', '12.9.0', '11.14.0', '10.19.0', '9.6.24']
},
{
name: 'redis',
fancyName: 'Redis',
baseImage: 'bitnami/redis',
versions: ['6.2.6', '6.0.16', '5.0.14']
},
{ name: 'couchdb', fancyName: 'CouchDB', baseImage: 'bitnami/couchdb', versions: ['3.2.1'] }
];
export const supportedServiceTypesAndVersions = [
{
name: 'plausibleanalytics',
fancyName: 'Plausible Analytics',
baseImage: 'plausible/analytics',
images: ['bitnami/postgresql:13.2.0', 'yandex/clickhouse-server:21.3.2.5'],
versions: ['latest'],
ports: {
main: 8000
}
},
{
name: 'nocodb',
fancyName: 'NocoDB',
baseImage: 'nocodb/nocodb',
versions: ['latest'],
ports: {
main: 8080
}
},
{
name: 'minio',
fancyName: 'MinIO',
baseImage: 'minio/minio',
versions: ['latest'],
ports: {
main: 9001
}
},
{
name: 'vscodeserver',
fancyName: 'VSCode Server',
baseImage: 'codercom/code-server',
versions: ['latest'],
ports: {
main: 8080
}
},
{
name: 'wordpress',
fancyName: 'Wordpress',
baseImage: 'wordpress',
images: ['bitnami/mysql:5.7'],
versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'],
ports: {
main: 80
}
},
{
name: 'vaultwarden',
fancyName: 'Vaultwarden',
baseImage: 'vaultwarden/server',
versions: ['latest'],
ports: {
main: 80
}
},
{
name: 'languagetool',
fancyName: 'LanguageTool',
baseImage: 'silviof/docker-languagetool',
versions: ['latest'],
ports: {
main: 8010
}
},
{
name: 'n8n',
fancyName: 'n8n',
baseImage: 'n8nio/n8n',
versions: ['latest'],
ports: {
main: 5678
}
},
{
name: 'uptimekuma',
fancyName: 'Uptime Kuma',
baseImage: 'louislam/uptime-kuma',
versions: ['latest'],
ports: {
main: 3001
}
},
{
name: 'ghost',
fancyName: 'Ghost',
baseImage: 'bitnami/ghost',
images: ['bitnami/mariadb'],
versions: ['latest'],
ports: {
main: 2368
}
}
];
export function getVersions(type) {
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) {
@ -271,6 +159,7 @@ export function generateDatabaseConfiguration(database) {
// url: `psql://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 5432}/${defaultDatabase}`,
privatePort: 5432,
environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword,
POSTGRESQL_PASSWORD: dbUserPassword,
POSTGRESQL_USERNAME: dbUser,
POSTGRESQL_DATABASE: defaultDatabase

View File

@ -7,7 +7,14 @@ import getPort, { portNumbers } from 'get-port';
import { asyncExecShell, getEngine, removeContainer } from '$lib/common';
export async function listDatabases(teamId) {
return await prisma.database.findMany({ where: { teams: { some: { id: teamId } } } });
if (teamId === '0') {
return await prisma.database.findMany({ include: { teams: true } });
} else {
return await prisma.database.findMany({
where: { teams: { some: { id: teamId } } },
include: { teams: true }
});
}
}
export async function newDatabase({ name, teamId }) {
const dbUser = cuid();
@ -31,10 +38,18 @@ export async function newDatabase({ name, teamId }) {
}
export async function getDatabase({ id, teamId }) {
const body = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId } } },
include: { destinationDocker: true, settings: true }
});
let body = {};
if (teamId === '0') {
body = await prisma.database.findFirst({
where: { id },
include: { destinationDocker: true, settings: true }
});
} else {
body = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId } } },
include: { destinationDocker: true, settings: true }
});
}
if (body.dbUserPassword) body.dbUserPassword = decrypt(body.dbUserPassword);
if (body.rootUserPassword) body.rootUserPassword = decrypt(body.rootUserPassword);
@ -122,3 +137,43 @@ export async function stopDatabase(database) {
}
return everStarted;
}
export async function updatePasswordInDb(database, user, newPassword, isRoot) {
const {
id,
type,
rootUser,
rootUserPassword,
dbUser,
dbUserPassword,
defaultDatabase,
destinationDockerId,
destinationDocker: { engine }
} = database;
if (destinationDockerId) {
const host = getEngine(engine);
if (type === 'mysql') {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"ALTER USER '${user}'@'%' IDENTIFIED WITH caching_sha2_password BY '${newPassword}';\"`
);
} else if (type === 'postgresql') {
if (isRoot) {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec ${id} psql postgresql://postgres:${rootUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role postgres WITH PASSWORD '${newPassword}'"`
);
} else {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec ${id} psql postgresql://${dbUser}:${dbUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role ${user} WITH PASSWORD '${newPassword}'"`
);
}
} else if (type === 'mongodb') {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec ${id} mongo 'mongodb://${rootUser}:${rootUserPassword}@${id}:27017/admin?readPreference=primary&ssl=false' --eval "db.changeUserPassword('${user}','${newPassword}')"`
);
} else if (type === 'redis') {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec ${id} redis-cli -u redis://${dbUserPassword}@${id}:6379 --raw CONFIG SET requirepass ${newPassword}`
);
}
}
}

View File

@ -6,7 +6,13 @@ import { getDatabaseImage } from '.';
import { prisma } from './common';
export async function listDestinations(teamId) {
return await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId } } } });
if (teamId === '0') {
return await prisma.destinationDocker.findMany({ include: { teams: true } });
}
return await prisma.destinationDocker.findMany({
where: { teams: { some: { id: teamId } } },
include: { teams: true }
});
}
export async function configureDestinationForService({ id, destinationId }) {
@ -38,9 +44,7 @@ export async function configureDestinationForDatabase({ id, destinationId }) {
const host = getEngine(engine);
if (type && version) {
const baseImage = getDatabaseImage(type);
asyncExecShell(
`DOCKER_HOST=${host} docker pull ${baseImage}:${version} && echo "FROM ${baseImage}:${version}" | docker build --label coolify.image="true" -t "${baseImage}:${version}" -`
);
asyncExecShell(`DOCKER_HOST=${host} docker pull ${baseImage}:${version}`);
}
}
}
@ -124,12 +128,17 @@ export async function removeDestination({ id }) {
}
export async function getDestination({ id, teamId }) {
let destination = await prisma.destinationDocker.findFirst({
where: { id, teams: { some: { id: teamId } } }
});
if (destination.remoteEngine) {
destination.sshPrivateKey = decrypt(destination.sshPrivateKey);
let destination = {};
if (teamId === '0') {
destination = await prisma.destinationDocker.findFirst({
where: { id }
});
} else {
destination = await prisma.destinationDocker.findFirst({
where: { id, teams: { some: { id: teamId } } }
});
}
return destination;
}
export async function getDestinationByApplicationId({ id, teamId }) {

View File

@ -2,26 +2,26 @@ import { decrypt, encrypt } from '$lib/crypto';
import { prisma } from './common';
export async function listSources(teamId) {
if (teamId === '0') {
return await prisma.gitSource.findMany({
include: { githubApp: true, gitlabApp: true, teams: true }
});
}
return await prisma.gitSource.findMany({
where: { teams: { some: { id: teamId } } },
include: { githubApp: true, gitlabApp: true }
include: { githubApp: true, gitlabApp: true, teams: true }
});
}
export async function newSource({ name, teamId, type, htmlUrl, apiUrl, organization }) {
export async function newSource({ teamId, name }) {
return await prisma.gitSource.create({
data: {
teams: { connect: { id: teamId } },
name,
type,
htmlUrl,
apiUrl,
organization
teams: { connect: { id: teamId } }
}
});
}
export async function removeSource({ id }) {
// TODO: Disconnect application with this sourceId! Maybe not needed?
const source = await prisma.gitSource.delete({
where: { id },
include: { githubApp: true, gitlabApp: true }
@ -31,10 +31,18 @@ export async function removeSource({ id }) {
}
export async function getSource({ id, teamId }) {
let body = await prisma.gitSource.findFirst({
where: { id, teams: { some: { id: teamId } } },
include: { githubApp: true, gitlabApp: true }
});
let body = {};
if (teamId === '0') {
body = await prisma.gitSource.findFirst({
where: { id },
include: { githubApp: true, gitlabApp: true }
});
} else {
body = await prisma.gitSource.findFirst({
where: { id, teams: { some: { id: teamId } } },
include: { githubApp: true, gitlabApp: true }
});
}
if (body?.githubApp?.clientSecret)
body.githubApp.clientSecret = decrypt(body.githubApp.clientSecret);
if (body?.githubApp?.webhookSecret)
@ -43,8 +51,29 @@ export async function getSource({ id, teamId }) {
if (body?.gitlabApp?.appSecret) body.gitlabApp.appSecret = decrypt(body.gitlabApp.appSecret);
return body;
}
export async function addSource({ id, appId, teamId, oauthId, groupName, appSecret }) {
export async function addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl }) {
await prisma.gitSource.update({ where: { id }, data: { type, name, htmlUrl, apiUrl } });
return await prisma.githubApp.create({
data: {
teams: { connect: { id: teamId } },
gitSource: { connect: { id } }
}
});
}
export async function addGitLabSource({
id,
teamId,
type,
name,
htmlUrl,
apiUrl,
oauthId,
appId,
appSecret,
groupName
}) {
const encrptedAppSecret = encrypt(appSecret);
await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name } });
return await prisma.gitlabApp.create({
data: {
teams: { connect: { id: teamId } },
@ -63,9 +92,9 @@ export async function configureGitsource({ id, gitSourceId }) {
data: { gitSource: { connect: { id: gitSourceId } } }
});
}
export async function updateGitsource({ id, name }) {
export async function updateGitsource({ id, name, htmlUrl, apiUrl }) {
return await prisma.gitSource.update({
where: { id },
data: { name }
data: { name, htmlUrl, apiUrl }
});
}

View File

@ -5,7 +5,14 @@ import { generatePassword } from '.';
import { prisma } from './common';
export async function listServices(teamId) {
return await prisma.service.findMany({ where: { teams: { some: { id: teamId } } } });
if (teamId === '0') {
return await prisma.service.findMany({ include: { teams: true } });
} else {
return await prisma.service.findMany({
where: { teams: { some: { id: teamId } } },
include: { teams: true }
});
}
}
export async function newService({ name, teamId }) {
@ -13,18 +20,28 @@ export async function newService({ name, teamId }) {
}
export async function getService({ id, teamId }) {
const body = await prisma.service.findFirst({
where: { id, teams: { some: { id: teamId } } },
include: {
destinationDocker: true,
plausibleAnalytics: true,
minio: true,
vscodeserver: true,
wordpress: true,
ghost: true,
serviceSecret: true
}
});
let body = {};
const include = {
destinationDocker: true,
plausibleAnalytics: true,
minio: true,
vscodeserver: true,
wordpress: true,
ghost: true,
serviceSecret: true,
meiliSearch: true
};
if (teamId === '0') {
body = await prisma.service.findFirst({
where: { id },
include
});
} else {
body = await prisma.service.findFirst({
where: { id, teams: { some: { id: teamId } } },
include
});
}
if (body.plausibleAnalytics?.postgresqlPassword)
body.plausibleAnalytics.postgresqlPassword = decrypt(
@ -50,14 +67,20 @@ export async function getService({ id, teamId }) {
body.ghost.mariadbRootUserPassword = decrypt(body.ghost.mariadbRootUserPassword);
if (body.ghost?.defaultPassword) body.ghost.defaultPassword = decrypt(body.ghost.defaultPassword);
if (body.meiliSearch?.masterKey) body.meiliSearch.masterKey = decrypt(body.meiliSearch.masterKey);
if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value);
return s;
});
}
if (body.wordpress?.ftpPassword) {
body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
}
const settings = await prisma.setting.findFirst();
return { ...body };
return { ...body, settings };
}
export async function configureServiceType({ id, type }) {
@ -142,7 +165,7 @@ export async function configureServiceType({ id, type }) {
}
});
} else if (type === 'ghost') {
const defaultEmail = `${cuid()}@coolify.io`;
const defaultEmail = `${cuid()}@example.com`;
const defaultPassword = encrypt(generatePassword());
const mariadbUser = cuid();
const mariadbPassword = encrypt(generatePassword());
@ -165,6 +188,15 @@ export async function configureServiceType({ id, type }) {
}
}
});
} else if (type === 'meilisearch') {
const masterKey = encrypt(generatePassword(32));
await prisma.service.update({
where: { id },
data: {
type,
meiliSearch: { create: { masterKey } }
}
});
}
}
export async function setServiceVersion({ id, version }) {
@ -188,15 +220,6 @@ export async function updatePlausibleAnalyticsService({ id, fqdn, email, usernam
export async function updateService({ id, fqdn, name }) {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateLanguageToolService({ id, fqdn, name }) {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateVaultWardenService({ id, fqdn, name }) {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateVsCodeServer({ id, fqdn, name }) {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateWordpress({ id, fqdn, name, mysqlDatabase, extraConfig }) {
return await prisma.service.update({
where: { id },
@ -214,6 +237,7 @@ export async function updateGhostService({ id, fqdn, name, mariadbDatabase }) {
}
export async function removeService({ id }) {
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
await prisma.ghost.deleteMany({ where: { serviceId: id } });
await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } });
await prisma.minio.deleteMany({ where: { serviceId: id } });

View File

@ -32,26 +32,42 @@ export async function login({ email, password, isLogin }) {
if (users === 0) {
await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } });
// Create default network & start Coolify Proxy
asyncExecShell(`docker network create --attachable coolify`)
.then(() => {
console.log('Network created');
})
.catch(() => {
console.log('Network already exists.');
});
startCoolifyProxy('/var/run/docker.sock')
.then(() => {
console.log('Coolify Proxy started.');
})
.catch((err) => {
console.log(err);
});
await asyncExecShell(`docker network create --attachable coolify`);
await startCoolifyProxy('/var/run/docker.sock');
uid = '0';
}
if (userFound) {
if (userFound.type === 'email') {
if (userFound.password === 'RESETME') {
const hashedPassword = await hashPassword(password);
if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) {
await prisma.user.update({
where: { email: userFound.email },
data: { password: 'RESETTIMEOUT' }
});
throw {
error: 'Password reset link has expired. Please request a new one.'
};
} else {
await prisma.user.update({
where: { email: userFound.email },
data: { password: hashedPassword }
});
return {
status: 200,
headers: {
'Set-Cookie': `teamId=${uid}; HttpOnly; Path=/; Max-Age=15778800;`
},
body: {
userId: userFound.id,
teamId: userFound.id,
permission: userFound.permission,
isAdmin: true
}
};
}
}
const passwordMatch = await bcrypt.compare(password, userFound.password);
if (!passwordMatch) {
throw {

View File

@ -27,11 +27,11 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name} ${secret.value}`);
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
@ -88,12 +88,12 @@ export async function buildImage({
debug = false
}) {
if (isCache) {
saveBuildLog({ line: `Building cache image started.`, buildId, applicationId });
await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId });
} else {
saveBuildLog({ line: `Building image started.`, buildId, applicationId });
await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
}
if (!debug && isCache) {
saveBuildLog({
await saveBuildLog({
line: `Debug turned off. To see more details, allow it in the configuration.`,
buildId,
applicationId
@ -126,13 +126,17 @@ export async function streamEvents({ stream, docker, buildId, applicationId, deb
if (err) reject(err);
resolve(res);
}
function onProgress(event) {
async function onProgress(event) {
if (event.error) {
reject(event.error);
} else if (event.stream) {
if (event.stream !== '\n') {
if (debug)
saveBuildLog({ line: `${event.stream.replace('\n', '')}`, buildId, applicationId });
await saveBuildLog({
line: `${event.stream.replace('\n', '')}`,
buildId,
applicationId
});
}
}
}

View File

@ -6,6 +6,7 @@ import crypto from 'crypto';
import * as db from '$lib/database';
import { checkContainer, checkHAProxy } from '.';
import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
@ -175,7 +176,7 @@ export async function configureHAProxy() {
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? domain : 'www.' + domain,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain,
updatedAt: updatedAt.getTime()
});
}
@ -199,7 +200,7 @@ export async function configureHAProxy() {
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? previewDomain : 'www.' + previewDomain,
redirectTo: isWWW ? previewDomain.replace('www.', '') : 'www.' + previewDomain,
updatedAt: updatedAt.getTime()
});
}
@ -223,7 +224,7 @@ export async function configureHAProxy() {
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;
if (destinationDockerId) {
const { engine } = destinationDocker;
const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type);
const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) {
const port = found.ports.main;
const publicPort = service[type]?.publicPort;
@ -242,7 +243,7 @@ export async function configureHAProxy() {
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? domain : 'www.' + domain,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain,
updatedAt: updatedAt.getTime()
});
}
@ -262,7 +263,7 @@ export async function configureHAProxy() {
domain,
isHttps,
redirectValue,
redirectTo: isWWW ? domain : 'www.' + domain
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain
});
}
const output = mustache.render(template, data);

View File

@ -108,7 +108,7 @@ export async function stopTcpHttpProxy(destinationDocker, publicPort) {
return error;
}
}
export async function startTcpProxy(destinationDocker, id, publicPort, privatePort) {
export async function startTcpProxy(destinationDocker, id, publicPort, privatePort, volume = null) {
const { network, engine } = destinationDocker;
const host = getEngine(engine);
@ -123,7 +123,9 @@ export async function startTcpProxy(destinationDocker, id, publicPort, privatePo
);
const ip = JSON.parse(Config)[0].Gateway;
return await asyncExecShell(
`DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} -d coollabsio/${defaultProxyImageTcp}`
`DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} ${
volume ? `-v ${volume}` : ''
} -d coollabsio/${defaultProxyImageTcp}`
);
}
} catch (error) {

View File

@ -10,42 +10,40 @@ export default async function ({
workdir,
githubAppId,
repository,
apiUrl,
htmlUrl,
branch,
buildId
}): Promise<any> {
try {
saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId });
const { privateKey, appId, installationId } = await db.getUniqueGithubApp({ githubAppId });
const githubPrivateKey = privateKey.replace(/\\n/g, '\n').replace(/"/g, '');
const url = htmlUrl.replace('https://', '').replace('http://', '');
await saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId });
const { privateKey, appId, installationId } = await db.getUniqueGithubApp({ githubAppId });
const githubPrivateKey = privateKey.replace(/\\n/g, '\n').replace(/"/g, '');
const payload = {
iat: Math.round(new Date().getTime() / 1000),
exp: Math.round(new Date().getTime() / 1000 + 60),
iss: appId
};
const jwtToken = jsonwebtoken.sign(payload, githubPrivateKey, {
algorithm: 'RS256'
});
const { token } = await got
.post(`https://api.github.com/app/installations/${installationId}/access_tokens`, {
headers: {
Authorization: `Bearer ${jwtToken}`,
Accept: 'application/vnd.github.machine-man-preview+json'
}
})
.json();
saveBuildLog({
line: `Cloning ${repository}:${branch} branch.`,
buildId,
applicationId
});
await asyncExecShell(
`git clone -q -b ${branch} https://x-access-token:${token}@github.com/${repository}.git ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && cd ..`
);
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
return commit.replace('\n', '');
} catch (error) {
console.log({ error });
return ErrorHandler(error);
}
const payload = {
iat: Math.round(new Date().getTime() / 1000),
exp: Math.round(new Date().getTime() / 1000 + 60),
iss: appId
};
const jwtToken = jsonwebtoken.sign(payload, githubPrivateKey, {
algorithm: 'RS256'
});
const { token } = await got
.post(`${apiUrl}/app/installations/${installationId}/access_tokens`, {
headers: {
Authorization: `Bearer ${jwtToken}`,
Accept: 'application/vnd.github.machine-man-preview+json'
}
})
.json();
await saveBuildLog({
line: `Cloning ${repository}:${branch} branch.`,
buildId,
applicationId
});
await asyncExecShell(
`git clone -q -b ${branch} https://x-access-token:${token}@${url}/${repository}.git ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && git lfs pull && cd .. `
);
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
return commit.replace('\n', '');
}

View File

@ -1,28 +1,28 @@
import { asyncExecShell, saveBuildLog } from '$lib/common';
import { ErrorHandler } from '$lib/database';
export default async function ({
applicationId,
debug,
workdir,
repodir,
htmlUrl,
repository,
branch,
buildId,
privateSshKey
}): Promise<any> {
saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId });
const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId });
await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`);
await asyncExecShell(`chmod 600 ${repodir}/id.rsa`);
saveBuildLog({
await saveBuildLog({
line: `Cloning ${repository}:${branch} branch.`,
buildId,
applicationId
});
await asyncExecShell(
`git clone -q -b ${branch} git@gitlab.com:${repository}.git --config core.sshCommand="ssh -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && cd ..`
`git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. `
);
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
return commit.replace('\n', '');

View File

@ -3,7 +3,9 @@ import { checkContainer, reloadHaproxy } from '$lib/haproxy';
import * as db from '$lib/database';
import { dev } from '$app/env';
import cuid from 'cuid';
import fs from 'fs/promises';
import getPort, { portNumbers } from 'get-port';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
export async function letsEncrypt(domain, id = null, isCoolify = false) {
try {
@ -160,7 +162,7 @@ export async function generateSSLCerts() {
type,
destinationDocker: { engine }
} = service;
const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type);
const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
@ -181,12 +183,41 @@ export async function generateSSLCerts() {
if (isHttps) ssls.push({ domain, id: 'coolify', isCoolify: true });
}
if (ssls.length > 0) {
const sslDir = dev ? '/tmp/ssl' : '/app/ssl';
if (dev) {
try {
await asyncExecShell(`mkdir -p ${sslDir}`);
} catch (error) {
//
}
}
const files = await fs.readdir(sslDir);
let certificates = [];
if (files.length > 0) {
for (const file of files) {
file.endsWith('.pem') && certificates.push(file.replace(/\.pem$/, ''));
}
}
for (const ssl of ssls) {
if (!dev) {
console.log('Checking SSL for', ssl.domain);
await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
if (
certificates.includes(ssl.domain) ||
certificates.includes(ssl.domain.replace('www.', ''))
) {
console.log(`Certificate for ${ssl.domain} already exists`);
} else {
console.log('Generating SSL for', ssl.domain);
await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
}
} else {
console.log('Checking SSL for', ssl.domain);
if (
certificates.includes(ssl.domain) ||
certificates.includes(ssl.domain.replace('www.', ''))
) {
console.log(`Certificate for ${ssl.domain} already exists`);
} else {
console.log('Generating SSL for', ssl.domain);
}
}
}
}

View File

@ -20,12 +20,9 @@ import {
setDefaultConfiguration
} from '$lib/buildPacks/common';
import yaml from 'js-yaml';
import type { ComposeFile } from '$lib/types/composeFile';
export default async function (job) {
/*
Edge cases:
1 - Change build pack and redeploy, what should happen?
*/
let {
id: applicationId,
repository,
@ -51,14 +48,17 @@ export default async function (job) {
pullmergeRequestId = null,
sourceBranch = null,
settings,
persistentStorage
persistentStorage,
pythonWSGI,
pythonModule,
pythonVariable
} = job.data;
const { debug } = settings;
await asyncSleep(500);
await db.prisma.build.updateMany({
where: {
status: 'queued',
status: { in: ['queued', 'running'] },
id: { not: buildId },
applicationId,
createdAt: { lt: new Date(new Date().getTime() - 60 * 60 * 1000) }
@ -114,6 +114,7 @@ export default async function (job) {
branch,
buildId,
apiUrl: gitSource.apiUrl,
htmlUrl: gitSource.htmlUrl,
projectId,
deployKeyId: gitSource.gitlabApp?.deployKeyId || null,
privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null
@ -127,7 +128,7 @@ export default async function (job) {
}
try {
db.prisma.build.update({ where: { id: buildId }, data: { commit } });
await db.prisma.build.update({ where: { id: buildId }, data: { commit } });
} catch (err) {
console.log(err);
}
@ -157,7 +158,7 @@ export default async function (job) {
});
deployNeeded = true;
if (configHash) {
saveBuildLog({ line: 'Configuration changed.', buildId, applicationId });
await saveBuildLog({ line: 'Configuration changed.', buildId, applicationId });
}
} else {
deployNeeded = false;
@ -200,16 +201,19 @@ export default async function (job) {
startCommand,
baseDirectory,
secrets,
phpModules
phpModules,
pythonWSGI,
pythonModule,
pythonVariable
});
else {
saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
throw new Error(`Build pack ${buildPack} not found.`);
}
deployNeeded = true;
} else {
deployNeeded = false;
saveBuildLog({ line: 'Nothing changed.', buildId, applicationId });
await saveBuildLog({ line: 'Nothing changed.', buildId, applicationId });
}
// Deploy to Docker Engine
@ -259,15 +263,7 @@ export default async function (job) {
//
}
try {
saveBuildLog({ line: 'Deployment started.', buildId, applicationId });
// for await (const volume of volumes) {
// const id = volume.split(':')[0];
// try {
// await asyncExecShell(`DOCKER_HOST=${host} docker volume inspect ${id}`);
// } catch (error) {
// await asyncExecShell(`DOCKER_HOST=${host} docker volume create ${id}`);
// }
// }
await saveBuildLog({ line: 'Deployment started.', buildId, applicationId });
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
@ -275,7 +271,7 @@ export default async function (job) {
}
};
});
const compose = {
const composeFile: ComposeFile = {
version: '3.8',
services: {
[imageId]: {
@ -284,7 +280,7 @@ export default async function (job) {
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
networks: [docker.network],
labels: labels,
labels,
depends_on: [],
restart: 'always'
}
@ -296,23 +292,16 @@ export default async function (job) {
},
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(compose));
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await asyncExecShell(
`DOCKER_HOST=${host} docker compose --project-directory ${workdir} up -d`
);
// const { stderr } = await asyncExecShell(
// `DOCKER_HOST=${host} docker run ${envFound && `--env-file=${workdir}/.env`} ${labels.join(
// ' '
// )} --name ${imageId} --network ${docker.network} --restart always ${volumes.length > 0 ? volumes : ''
// } -d ${applicationId}:${tag}`
// );
saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} catch (error) {
saveBuildLog({ line: error, buildId, applicationId });
await saveBuildLog({ line: error, buildId, applicationId });
sentry.captureException(error);
throw new Error(error);
}
saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
}
}

View File

@ -118,10 +118,14 @@ buildWorker.on('completed', async (job: Bullmq.Job) => {
try {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } });
} catch (error) {
setTimeout(async () => {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } });
}, 1234);
console.log(error);
} finally {
const workdir = `/tmp/build-sources/${job.data.repository}/${job.data.build_id}`;
if (!dev) await asyncExecShell(`rm -fr ${workdir}`);
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } });
}
return;
});
@ -130,17 +134,21 @@ buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => {
try {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } });
} catch (error) {
setTimeout(async () => {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } });
}, 1234);
console.log(error);
} finally {
const workdir = `/tmp/build-sources/${job.data.repository}`;
if (!dev) await asyncExecShell(`rm -fr ${workdir}`);
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } });
}
saveBuildLog({
await saveBuildLog({
line: 'Failed to deploy!',
buildId: job.data.build_id,
applicationId: job.data.id
});
saveBuildLog({
await saveBuildLog({
line: `Reason: ${failedReason.toString()}`,
buildId: job.data.build_id,
applicationId: job.data.id

View File

@ -0,0 +1,53 @@
export type ComposeFile = {
version: ComposerFileVersion;
services: Record<string, ComposeFileService>;
networks: Record<string, ComposeFileNetwork>;
volumes?: Record<string, ComposeFileVolume>;
};
export type ComposeFileService = {
container_name: string;
image?: string;
networks: string[];
environment?: Record<string, unknown>;
volumes?: string[];
ulimits?: unknown;
labels?: string[];
env_file?: string[];
extra_hosts?: string[];
restart: ComposeFileRestartOption;
depends_on?: string[];
command?: string;
build?: {
context: string;
dockerfile: string;
args?: Record<string, unknown>;
};
};
export type ComposerFileVersion =
| '3.8'
| '3.7'
| '3.6'
| '3.5'
| '3.4'
| '3.3'
| '3.2'
| '3.1'
| '3.0'
| '2.4'
| '2.3'
| '2.2'
| '2.1'
| '2.0';
export type ComposeFileRestartOption = 'no' | 'always' | 'on-failure' | 'unless-stopped';
export type ComposeFileNetwork = {
external: boolean;
};
export type ComposeFileVolume = {
external?: boolean;
name?: string;
};

View File

@ -12,7 +12,7 @@
if (!session.userId) {
return {};
}
const endpoint = `/teams.json`;
const endpoint = `/dashboard.json`;
const res = await fetch(endpoint);
if (res.ok) {
@ -134,13 +134,18 @@
<svelte:head>
<title>Coolify</title>
{#if !$session.whiteLabeled}
<link rel="icon" href="/favicon.png" />
{/if}
</svelte:head>
<SvelteToast options={{ intro: { y: -64 }, duration: 3000, pausable: true }} />
{#if $session.userId}
<nav class="nav-main">
<div class="flex h-screen w-full flex-col items-center transition-all duration-100">
<div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div>
<div class="flex flex-col space-y-4 py-2">
{#if !$session.whiteLabeled}
<div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div>
{/if}
<div class="flex flex-col space-y-4 py-2" class:mt-2={$session.whiteLabeled}>
<a
sveltekit:prefetch
href="/"
@ -312,7 +317,6 @@
<path d="M7 18a4.6 4.4 0 0 1 0 -9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-12" />
</svg>
</a>
<div class="border-t border-stone-700" />
</div>
<div class="flex-1" />
@ -430,13 +434,12 @@
<div class="flex flex-col space-y-4 py-2">
<a
sveltekit:prefetch
href="/teams"
class="icons tooltip-right bg-coolgray-200 hover:text-cyan-500"
class:text-cyan-500={$page.url.pathname.startsWith('/teams')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/teams')}
data-tooltip="Teams"
>
<svg
href="/iam"
class="icons tooltip-right bg-coolgray-200 hover:text-fuchsia-500"
class:text-fuchsia-500={$page.url.pathname.startsWith('/iam')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
data-tooltip="IAM"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
viewBox="0 0 24 24"
@ -453,6 +456,7 @@
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
</svg>
</a>
{#if $session.teamId === '0'}
<a
sveltekit:prefetch
@ -480,6 +484,7 @@
</svg>
</a>
{/if}
<div
class="icons tooltip-right bg-coolgray-200 hover:text-red-500"
data-tooltip="Logout"
@ -514,8 +519,14 @@
</div>
</div>
</nav>
{#if $session.whiteLabeled}
<span class="fixed bottom-0 left-[50px] z-50 m-2 px-4 text-xs text-stone-700"
>Powered by <a href="https://coolify.io" target="_blank">Coolify</a></span
>
{/if}
<select
class="fixed right-0 bottom-0 z-50 m-2 w-64 bg-opacity-30 p-2 px-4"
class="fixed right-0 bottom-0 z-50 m-2 w-64 bg-opacity-30 p-2 px-4 hover:bg-opacity-100"
bind:value={selectedTeamId}
on:change={switchTeam}
>

View File

@ -88,7 +88,6 @@
try {
const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application });
toast.push('Deployment queued.');
console.log($page.url);
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
} else {
@ -255,9 +254,9 @@
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`}
>
<button
title="Secrets"
title="Secret"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Secrets"
data-tooltip="Secret"
>
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@ -81,6 +81,9 @@
);
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 composerPHP = files.find(
(file) => file.name === 'composer.json' && file.type === 'blob'
);
if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm';
@ -103,7 +106,7 @@
foundConfig = findBuildPack('python');
} else if (indexHtml) {
foundConfig = findBuildPack('static', packageManager);
} else if (indexPHP) {
} else if (indexPHP || composerPHP) {
foundConfig = findBuildPack('php');
} else {
foundConfig = findBuildPack('node', packageManager);
@ -127,6 +130,9 @@
);
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 composerPHP = files.find(
(file) => file.name === 'composer.json' && file.type === 'file'
);
if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm';
@ -146,7 +152,7 @@
foundConfig = findBuildPack('python');
} else if (indexHtml) {
foundConfig = findBuildPack('static', packageManager);
} else if (indexPHP) {
} else if (indexPHP || composerPHP) {
foundConfig = findBuildPack('php');
} else {
foundConfig = findBuildPack('node', packageManager);

View File

@ -29,7 +29,7 @@
<script lang="ts">
import type Prisma from '@prisma/client';
import { page } from '$app/stores';
import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
@ -39,6 +39,16 @@
export let destinations: Prisma.DestinationDocker[];
const ownDestinations = destinations.filter((destination) => {
if (destination.teams[0].id === $session.teamId) {
return destination;
}
});
const otherDestinations = destinations.filter((destination) => {
if (destination.teams[0].id !== $session.teamId) {
return destination;
}
});
async function handleSubmit(destinationId) {
try {
await post(`/applications/${id}/configuration/destination.json`, { destinationId });
@ -52,8 +62,8 @@
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Configure Destination</div>
</div>
<div class="flex justify-center">
{#if !destinations || destinations.length === 0}
<div class="flex flex-col justify-center">
{#if !destinations || ownDestinations.length === 0}
<div class="flex-col">
<div class="pb-2">No configurable Destination found</div>
<div class="flex justify-center">
@ -75,8 +85,23 @@
</div>
</div>
{:else}
<div class="flex flex-wrap justify-center">
{#each destinations as destination}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownDestinations as destination}
<div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(destination.id)}>
<button type="submit" class="box-selection hover:bg-sky-700 font-bold">
<div class="font-bold text-xl text-center truncate">{destination.name}</div>
<div class="text-center truncate">{destination.network}</div>
</button>
</form>
</div>
{/each}
</div>
{#if otherDestinations.length > 0 && $session.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Destinations</div>
{/if}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherDestinations as destination}
<div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(destination.id)}>
<button type="submit" class="box-selection hover:bg-sky-700 font-bold">

View File

@ -29,7 +29,7 @@
<script lang="ts">
import type Prisma from '@prisma/client';
import { page } from '$app/stores';
import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
@ -46,6 +46,17 @@
(source.type === 'github' && source.githubAppId && source.githubApp.installationId) ||
(source.type === 'gitlab' && source.gitlabAppId)
);
const ownSources = filteredSources.filter((source) => {
if (source.teams[0].id === $session.teamId) {
return source;
}
});
const otherSources = filteredSources.filter((source) => {
if (source.teams[0].id !== $session.teamId) {
return source;
}
});
async function handleSubmit(gitSourceId) {
try {
await post(`/applications/${id}/configuration/source.json`, { gitSourceId });
@ -54,17 +65,21 @@
return errorNotification(error);
}
}
async function newSource() {
const { id } = await post('/sources/new', {});
return await goto(`/sources/${id}`, { replaceState: true });
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Git Source</div>
</div>
<div class="flex flex-col justify-center">
{#if !filteredSources || filteredSources.length === 0}
{#if !filteredSources || ownSources.length === 0}
<div class="flex-col">
<div class="pb-2">No configurable Git Source found</div>
<div class="pb-2 text-center">No configurable Git Source found</div>
<div class="flex justify-center">
<a href="/new/source" sveltekit:prefetch class="add-icon bg-orange-600 hover:bg-orange-500">
<button on:click={newSource} class="add-icon bg-orange-600 hover:bg-orange-500">
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
@ -78,12 +93,39 @@
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</a>
</button>
</div>
</div>
{:else}
<div class="flex flex-wrap justify-center">
{#each filteredSources as source}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownSources as source}
<div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(source.id)}>
<button
disabled={source.gitlabApp && !source.gitlabAppId}
type="submit"
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group"
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
class:border-0={source.gitlabApp && !source.gitlabAppId}
class:border-l-4={source.gitlabApp && !source.gitlabAppId}
>
<div class="font-bold text-xl text-center truncate">{source.name}</div>
{#if source.gitlabApp && !source.gitlabAppId}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">
Configuration missing
</div>
{/if}
</button>
</form>
</div>
{/each}
</div>
{#if otherSources.length > 0 && $session.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Sources</div>
{/if}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherSources as source}
<div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(source.id)}>
<button

View File

@ -14,6 +14,7 @@ export const del: RequestHandler = async (event) => {
status: 200
};
} catch (error) {
console.log(error);
return ErrorHandler(error);
}
};

View File

@ -5,6 +5,7 @@ import { checkContainer } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
import jsonwebtoken from 'jsonwebtoken';
import { get as getRequest } from '$lib/api';
import { setDefaultConfiguration } from '$lib/buildPacks/common';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
@ -52,12 +53,23 @@ export const post: RequestHandler = async (event) => {
buildCommand,
startCommand,
baseDirectory,
publishDirectory
publishDirectory,
pythonWSGI,
pythonModule,
pythonVariable
} = await event.request.json();
if (port) port = Number(port);
try {
const defaultConfiguration = await setDefaultConfiguration({
buildPack,
port,
installCommand,
startCommand,
buildCommand,
publishDirectory,
baseDirectory
});
await db.configureApplication({
id,
buildPack,
@ -68,7 +80,11 @@ export const post: RequestHandler = async (event) => {
buildCommand,
startCommand,
baseDirectory,
publishDirectory
publishDirectory,
pythonWSGI,
pythonModule,
pythonVariable,
...defaultConfiguration
});
return { status: 201 };
} catch (error) {

View File

@ -38,6 +38,7 @@
import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form';
import { onMount } from 'svelte';
import Select from 'svelte-select';
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
@ -57,6 +58,23 @@
let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts;
let autodeploy = application.settings.autodeploy;
let wsgis = [
{
value: 'None',
label: 'None'
},
{
value: 'Gunicorn',
label: 'Gunicorn'
}
// },
// {
// value: 'uWSGI',
// label: 'uWSGI'
// }
];
if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
application.fqdn = `http://${cuid()}.demo.coolify.io`;
}
@ -111,7 +129,7 @@
await post(`/applications/${id}.json`, { ...application });
return window.location.reload();
} catch ({ error }) {
if (error.startsWith('DNS not set')) {
if (error?.startsWith('DNS not set')) {
forceSave = true;
}
return errorNotification(error);
@ -119,12 +137,19 @@
loading = false;
}
}
async function selectWSGI(event) {
application.pythonWSGI = event.detail.value;
}
</script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
{application.name}
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Configuration
</div>
<span class="text-xs">{application.name} </span>
</div>
{#if application.fqdn}
<a
href={application.fqdn}
@ -282,14 +307,14 @@
<div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-2">
<div class="flex-col">
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100">Domain (FQDN)</label>
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100">URL (FQDN)</label>
{#if browser && window.location.hostname === 'demo.coolify.io'}
<Explainer
text="<span class='text-white font-bold'>You can use the predefined random domain name or enter your own domain name.</span>"
text="<span class='text-white font-bold'>You can use the predefined random url name or enter your own domain name.</span>"
/>
{/if}
<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.<br><br><span class='text-white font-bold'>You must set your DNS to point to the server IP in advance.</span>"
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 url, 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>
<input
@ -315,6 +340,39 @@
on:click={() => !isRunning && changeSettings('dualCerts')}
/>
</div>
{#if application.buildPack === 'python'}
<div class="grid grid-cols-2 items-center">
<label for="pythonModule" class="text-base font-bold text-stone-100">WSGI</label>
<div class="custom-select-wrapper">
<Select id="wsgi" items={wsgis} on:select={selectWSGI} value={application.pythonWSGI} />
</div>
</div>
<div class="grid grid-cols-2 items-center">
<label for="pythonModule" class="text-base font-bold text-stone-100">Module</label>
<input
readonly={!$session.isAdmin}
name="pythonModule"
id="pythonModule"
required
bind:value={application.pythonModule}
placeholder={application.pythonWSGI?.toLowerCase() !== 'gunicorn' ? 'main.py' : 'main'}
/>
</div>
{#if application.pythonWSGI?.toLowerCase() === 'gunicorn'}
<div class="grid grid-cols-2 items-center">
<label for="pythonVariable" class="text-base font-bold text-stone-100">Variable</label>
<input
readonly={!$session.isAdmin}
name="pythonVariable"
id="pythonVariable"
required
bind:value={application.pythonVariable}
placeholder="default: app"
/>
</div>
{/if}
{/if}
{#if !staticDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center">
<label for="port" class="text-base font-bold text-stone-100">Port</label>
@ -323,7 +381,7 @@
name="port"
id="port"
bind:value={application.port}
placeholder="default: 3000"
placeholder={application.buildPack === 'python' ? '8000' : '3000'}
/>
</div>
{/if}

View File

@ -21,7 +21,7 @@
<script lang="ts">
import { page } from '$app/stores';
import { dateOptions, getDomain } from '$lib/components/common';
import { changeQueryParams, dateOptions, getDomain } from '$lib/components/common';
import BuildLog from './_BuildLog.svelte';
import { get } from '$lib/api';
@ -79,16 +79,81 @@
noMoreBuilds = true;
}
}
async function loadBuild(build) {
function loadBuild(build) {
buildId = build;
await goto(`/applications/${id}/logs/build?buildId=${buildId}`);
return changeQueryParams(buildId);
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">
Build logs of <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">Build Logs</div>
<span class="text-xs">{application.name} </span>
</div>
{#if application.fqdn}
<a
href={application.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
><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" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
</div>
<div class="block flex-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex">
<div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 ">
@ -130,9 +195,14 @@
</div>
{/each}
</div>
<div class="flex space-x-2">
<button disabled={noMoreBuilds} class="w-full" on:click={loadMoreBuilds}>Load More</button>
</div>
{#if !noMoreBuilds}
{#if buildCount > 5}
<div class="flex space-x-2">
<button disabled={noMoreBuilds} class="w-full" on:click={loadMoreBuilds}>Load More</button
>
</div>
{/if}
{/if}
</div>
<div class="flex-1 md:w-96">
{#if buildId}

View File

@ -68,16 +68,83 @@
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">
Application logs of <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Application Logs
</div>
<span class="text-xs">{application.name} </span>
</div>
{#if application.fqdn}
<a
href={application.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
><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" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
</div>
<div class="flex flex-row justify-center space-x-2 px-10 pt-6">
{#if logs.length === 0}
<div class="text-xl font-bold tracking-tighter">Waiting for the logs...</div>
{:else}
<div class="relative">
<div class="relative w-full">
<LoadingLogs />
<div class="flex justify-end sticky top-0 p-2">
<button
@ -105,7 +172,7 @@
</button>
</div>
<div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl}
>
<div class="px-2">

View File

@ -11,7 +11,6 @@
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${endpoint}`)
@ -50,14 +49,88 @@
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">
Previews for <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Preview Deployments
</div>
<span class="text-xs">{application.name} </span>
</div>
</div>
{#if applicationSecrets.length !== 0}
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
{#if application.fqdn}
<a
href={application.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
><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" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
</div>
<div class="mx-auto max-w-6xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={applicationSecrets.length === 0
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
/>
</div>
{#if applicationSecrets.length !== 0}
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
@ -84,16 +157,9 @@
{/each}
</tbody>
</table>
</div>
{/if}
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={applicationSecrets.length === 0
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>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."}
/>
{/if}
</div>
<div class="mx-auto max-w-4xl py-10">
<div class="flex flex-wrap justify-center space-x-2">
{#if containers.length > 0}

View File

@ -0,0 +1,48 @@
<script>
export let secrets;
export let refreshSecrets;
export let id;
import { saveSecret } from './utils';
import pLimit from 'p-limit';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
let batchSecrets = '';
function setBatchValue(event) {
batchSecrets = event.target?.value;
}
const limit = pLimit(1);
async function getValues(e) {
e.preventDefault();
const eachValuePair = batchSecrets.split('\n');
const batchSecretsPairs = eachValuePair
.filter((secret) => !secret.startsWith('#') && secret)
.map((secret) => {
const [name, value] = secret.split('=');
const cleanValue = value?.replaceAll('"', '') || '';
return {
name,
value: cleanValue,
isNew: !secrets.find((secret) => name === secret.name)
};
});
await Promise.all(
batchSecretsPairs.map(({ name, value, isNew }) =>
limit(() => saveSecret({ name, value, applicationId: id, isNew }))
)
);
batchSecrets = '';
refreshSecrets();
}
</script>
<h2 class="title my-6 font-bold">Paste .env file</h2>
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
<textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" />
<button
class="bg-green-600 hover:bg-green-500 disabled:text-white disabled:opacity-40"
type="submit">Batch add secrets</button
>
</form>

View File

@ -9,11 +9,12 @@
if (isPRMRSecret) value = PRMRSecret.value;
import { page } from '$app/stores';
import { del, post } from '$lib/api';
import { del } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
import { createEventDispatcher } from 'svelte';
import { saveSecret } from './utils';
const dispatch = createEventDispatcher();
const { id } = $page.params;
@ -30,31 +31,41 @@
return errorNotification(error);
}
}
async function saveSecret(isNew = false) {
if (!name) return errorNotification('Name is required.');
if (!value) return errorNotification('Value is required.');
try {
await post(`/applications/${id}/secrets.json`, {
name,
value,
isBuildSecret,
isPRMRSecret,
isNew
});
dispatch('refresh');
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
toast.push('Secret saved.');
} catch ({ error }) {
return errorNotification(error);
}
}
function setSecretValue() {
async function createSecret(isNew) {
await saveSecret({
isNew,
name,
value,
isBuildSecret,
isPRMRSecret,
isNewSecret,
applicationId: id
});
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
dispatch('refresh');
toast.push('Secret saved');
}
async function setSecretValue() {
if (!isPRMRSecret) {
isBuildSecret = !isBuildSecret;
if (!isNewSecret) {
await saveSecret({
isNew: isNewSecret,
name,
value,
isBuildSecret,
isPRMRSecret,
isNewSecret,
applicationId: id
});
toast.push('Secret saved');
}
}
}
</script>
@ -89,9 +100,9 @@
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out"
class:bg-green-600={isBuildSecret}
class:bg-stone-700={!isBuildSecret}
class:opacity-50={!isNewSecret}
class:cursor-not-allowed={!isNewSecret}
class:cursor-pointer={isNewSecret}
class:opacity-50={isPRMRSecret}
class:cursor-not-allowed={isPRMRSecret}
class:cursor-pointer={!isPRMRSecret}
>
<span class="sr-only">Use isBuildSecret</span>
<span
@ -133,12 +144,14 @@
<td>
{#if isNewSecret}
<div class="flex items-center justify-center">
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveSecret(true)}>Add</button>
<button class="bg-green-600 hover:bg-green-500" on:click={() => createSecret(true)}
>Add</button
>
</div>
{:else}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="" on:click={() => saveSecret(false)}>Set</button>
<button class="" on:click={() => createSecret(false)}>Set</button>
</div>
{#if !isPRMRSecret}
<div class="flex justify-center items-end">

View File

@ -1,10 +1,10 @@
import { getTeam, getUserDetails } from '$lib/common';
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
@ -24,7 +24,7 @@ export const get: RequestHandler = async (event) => {
};
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
@ -53,7 +53,7 @@ export const post: RequestHandler = async (event) => {
}
};
export const del: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;

View File

@ -22,25 +22,121 @@
<script lang="ts">
export let secrets;
export let application;
import pLimit from 'p-limit';
import Secret from './_Secret.svelte';
import { getDomain } from '$lib/components/common';
import { page } from '$app/stores';
import { get } from '$lib/api';
import { saveSecret } from './utils';
import { toast } from '@zerodevx/svelte-toast';
const limit = pLimit(1);
const { id } = $page.params;
let batchSecrets = '';
async function refreshSecrets() {
const data = await get(`/applications/${id}/secrets.json`);
secrets = [...data.secrets];
}
async function getValues(e) {
e.preventDefault();
const eachValuePair = batchSecrets.split('\n');
const batchSecretsPairs = eachValuePair
.filter((secret) => !secret.startsWith('#') && secret)
.map((secret) => {
const [name, value] = secret.split('=');
const cleanValue = value?.replaceAll('"', '') || '';
return {
name,
value: cleanValue,
isNew: !secrets.find((secret) => name === secret.name)
};
});
await Promise.all(
batchSecretsPairs.map(({ name, value, isNew }) =>
limit(() => saveSecret({ name, value, applicationId: id, isNew }))
)
);
batchSecrets = '';
await refreshSecrets();
toast.push('Secrets saved');
}
function asd() {
console.log(secrets);
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">
Secrets for <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">Secrets</div>
<span class="text-xs">{application.name} </span>
</div>
{#if application.fqdn}
<a
href={application.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
><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" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
</div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<div class="mx-auto max-w-6xl px-6 pt-4">
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
@ -68,4 +164,13 @@
</tr>
</tbody>
</table>
<button on:click={asd}>Save</button>
<h2 class="title my-6 font-bold">Paste .env file</h2>
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
<textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" />
<button
class="bg-green-600 hover:bg-green-500 disabled:text-white disabled:opacity-40"
type="submit">Batch add secrets</button
>
</form>
</div>

View File

@ -0,0 +1,42 @@
import { toast } from '@zerodevx/svelte-toast';
import { errorNotification } from '$lib/form';
import { post } from '$lib/api';
type Props = {
isNew: boolean;
name: string;
value: string;
isBuildSecret?: boolean;
isPRMRSecret?: boolean;
isNewSecret?: boolean;
applicationId: string;
};
export async function saveSecret({
isNew,
name,
value,
isBuildSecret,
isPRMRSecret,
isNewSecret,
applicationId
}: Props): Promise<void> {
if (!name) return errorNotification('Name is required.');
if (!value) return errorNotification('Value is required.');
try {
await post(`/applications/${applicationId}/secrets.json`, {
name,
value,
isBuildSecret,
isPRMRSecret,
isNew: isNew || false
});
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
} catch ({ error }) {
return errorNotification(error);
}
}

View File

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

View File

@ -1,66 +0,0 @@
<script lang="ts">
export let application;
import Rust from '$lib/components/svg/applications/Rust.svelte';
import Nodejs from '$lib/components/svg/applications/Nodejs.svelte';
import React from '$lib/components/svg/applications/React.svelte';
import Svelte from '$lib/components/svg/applications/Svelte.svelte';
import Vuejs from '$lib/components/svg/applications/Vuejs.svelte';
import PHP from '$lib/components/svg/applications/PHP.svelte';
import Python from '$lib/components/svg/applications/Python.svelte';
import Static from '$lib/components/svg/applications/Static.svelte';
import Nestjs from '$lib/components/svg/applications/Nestjs.svelte';
import Nuxtjs from '$lib/components/svg/applications/Nuxtjs.svelte';
import Nextjs from '$lib/components/svg/applications/Nextjs.svelte';
import Gatsby from '$lib/components/svg/applications/Gatsby.svelte';
import Docker from '$lib/components/svg/applications/Docker.svelte';
import Astro from '$lib/components/svg/applications/Astro.svelte';
import Eleventy from '$lib/components/svg/applications/Eleventy.svelte';
const buildPack = application?.buildPack?.toLowerCase();
</script>
<a href="/applications/{application.id}" class="w-96 p-2 no-underline">
<div class="box-selection group relative hover:bg-green-600">
{#if buildPack === 'rust'}
<Rust />
{:else if buildPack === 'node'}
<Nodejs />
{:else if buildPack === 'react'}
<React />
{:else if buildPack === 'svelte'}
<Svelte />
{:else if buildPack === 'vuejs'}
<Vuejs />
{:else if buildPack === 'php'}
<PHP />
{:else if buildPack === 'python'}
<Python />
{:else if buildPack === 'static'}
<Static />
{:else if buildPack === 'nestjs'}
<Nestjs />
{:else if buildPack === 'nuxtjs'}
<Nuxtjs />
{:else if buildPack === 'nextjs'}
<Nextjs />
{:else if buildPack === 'gatsby'}
<Gatsby />
{:else if buildPack === 'docker'}
<Docker />
{:else if buildPack === 'astro'}
<Astro />
{:else if buildPack === 'eleventy'}
<Eleventy />
{/if}
<div class="truncate text-center text-xl font-bold">{application.name}</div>
{#if application.fqdn}
<div class="truncate text-center">{application.fqdn}</div>
{/if}
{#if !application.gitSourceId || !application.destinationDockerId}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>
{/if}
</div>
</a>

View File

@ -1,13 +1,40 @@
<script lang="ts">
export let applications: Array<Application>;
import { session } from '$app/stores';
import Application from './_Application.svelte';
import { post } from '$lib/api';
import { goto } from '$app/navigation';
import Rust from '$lib/components/svg/applications/Rust.svelte';
import Nodejs from '$lib/components/svg/applications/Nodejs.svelte';
import React from '$lib/components/svg/applications/React.svelte';
import Svelte from '$lib/components/svg/applications/Svelte.svelte';
import Vuejs from '$lib/components/svg/applications/Vuejs.svelte';
import PHP from '$lib/components/svg/applications/PHP.svelte';
import Python from '$lib/components/svg/applications/Python.svelte';
import Static from '$lib/components/svg/applications/Static.svelte';
import Nestjs from '$lib/components/svg/applications/Nestjs.svelte';
import Nuxtjs from '$lib/components/svg/applications/Nuxtjs.svelte';
import Nextjs from '$lib/components/svg/applications/Nextjs.svelte';
import Gatsby from '$lib/components/svg/applications/Gatsby.svelte';
import Docker from '$lib/components/svg/applications/Docker.svelte';
import Astro from '$lib/components/svg/applications/Astro.svelte';
import Eleventy from '$lib/components/svg/applications/Eleventy.svelte';
import { getDomain } from '$lib/components/common';
async function newApplication() {
const { id } = await post('/applications/new', {});
return await goto(`/applications/${id}`, { replaceState: true });
}
const ownApplications = applications.filter((application) => {
if (application.teams[0].id === $session.teamId) {
return application;
}
});
const otherApplications = applications.filter((application) => {
if (application.teams[0].id !== $session.teamId) {
return application;
}
});
</script>
<div class="flex space-x-1 p-6 font-bold">
@ -30,14 +57,125 @@
</div>
{/if}
</div>
<div class="flex flex-wrap justify-center">
{#if !applications || applications.length === 0}
<div class="flex flex-col flex-wrap justify-center">
{#if !applications || ownApplications.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">No applications found</div>
</div>
{:else}
{#each applications as application}
<Application {application} />
{/each}
{/if}
{#if ownApplications.length > 0 || otherApplications.length > 0}
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownApplications as application}
<a href="/applications/{application.id}" class="w-96 p-2 no-underline">
<div class="box-selection group relative hover:bg-green-600">
{#if application.buildPack}
{#if application.buildPack.toLowerCase() === 'rust'}
<Rust />
{:else if application.buildPack.toLowerCase() === 'node'}
<Nodejs />
{:else if application.buildPack.toLowerCase() === 'react'}
<React />
{:else if application.buildPack.toLowerCase() === 'svelte'}
<Svelte />
{:else if application.buildPack.toLowerCase() === 'vuejs'}
<Vuejs />
{:else if application.buildPack.toLowerCase() === 'php'}
<PHP />
{:else if application.buildPack.toLowerCase() === 'python'}
<Python />
{:else if application.buildPack.toLowerCase() === 'static'}
<Static />
{:else if application.buildPack.toLowerCase() === 'nestjs'}
<Nestjs />
{:else if application.buildPack.toLowerCase() === 'nuxtjs'}
<Nuxtjs />
{:else if application.buildPack.toLowerCase() === 'nextjs'}
<Nextjs />
{:else if application.buildPack.toLowerCase() === 'gatsby'}
<Gatsby />
{:else if application.buildPack.toLowerCase() === 'docker'}
<Docker />
{:else if application.buildPack.toLowerCase() === 'astro'}
<Astro />
{:else if application.buildPack.toLowerCase() === 'eleventy'}
<Eleventy />
{/if}
{/if}
<div class="truncate text-center text-xl font-bold">{application.name}</div>
{#if $session.teamId === '0' && otherApplications.length > 0}
<div class="truncate text-center">Team {application.teams[0].name}</div>
{/if}
{#if application.fqdn}
<div class="truncate text-center">{getDomain(application.fqdn) || ''}</div>
{/if}
{#if !application.gitSourceId || !application.destinationDockerId || !application.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>
{/if}
</div>
</a>
{/each}
</div>
{#if otherApplications.length > 0 && $session.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Applications</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherApplications as application}
<a href="/applications/{application.id}" class="w-96 p-2 no-underline">
<div class="box-selection group relative hover:bg-green-600">
{#if application.buildPack}
{#if application.buildPack.toLowerCase() === 'rust'}
<Rust />
{:else if application.buildPack.toLowerCase() === 'node'}
<Nodejs />
{:else if application.buildPack.toLowerCase() === 'react'}
<React />
{:else if application.buildPack.toLowerCase() === 'svelte'}
<Svelte />
{:else if application.buildPack.toLowerCase() === 'vuejs'}
<Vuejs />
{:else if application.buildPack.toLowerCase() === 'php'}
<PHP />
{:else if application.buildPack.toLowerCase() === 'python'}
<Python />
{:else if application.buildPack.toLowerCase() === 'static'}
<Static />
{:else if application.buildPack.toLowerCase() === 'nestjs'}
<Nestjs />
{:else if application.buildPack.toLowerCase() === 'nuxtjs'}
<Nuxtjs />
{:else if application.buildPack.toLowerCase() === 'nextjs'}
<Nextjs />
{:else if application.buildPack.toLowerCase() === 'gatsby'}
<Gatsby />
{:else if application.buildPack.toLowerCase() === 'docker'}
<Docker />
{:else if application.buildPack.toLowerCase() === 'astro'}
<Astro />
{:else if application.buildPack.toLowerCase() === 'eleventy'}
<Eleventy />
{/if}
{/if}
<div class="truncate text-center text-xl font-bold">{application.name}</div>
{#if $session.teamId === '0'}
<div class="truncate text-center">Team {application.teams[0].name}</div>
{/if}
{#if application.fqdn}
<div class="truncate text-center">{getDomain(application.fqdn) || ''}</div>
{/if}
{#if !application.gitSourceId || !application.destinationDockerId || !application.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>
{/if}
</div>
</a>
{/each}
</div>
{/if}
</div>
{/if}
</div>

View File

@ -9,23 +9,28 @@ export const get: RequestHandler = async (event) => {
try {
const applicationsCount = await db.prisma.application.count({
where: { teams: { some: { id: teamId } } }
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
});
const sourcesCount = await db.prisma.gitSource.count({
where: { teams: { some: { id: teamId } } }
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
});
const destinationsCount = await db.prisma.destinationDocker.count({
where: { teams: { some: { id: teamId } } }
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
});
const teamsCount = await db.prisma.permission.count({ where: { userId } });
const databasesCount = await db.prisma.database.count({
where: { teams: { some: { id: teamId } } }
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
});
const servicesCount = await db.prisma.service.count({
where: { teams: { some: { id: teamId } } }
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
});
const teams = await db.prisma.permission.findMany({
where: { userId },
include: { team: { include: { _count: { select: { users: true } } } } }
});
return {
body: {
teams,
applicationsCount,
sourcesCount,
destinationsCount,

View File

@ -2,6 +2,8 @@
export let database;
export let privatePort;
export let settings;
export let isRunning;
import { page, session } from '$app/stores';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Setting from '$lib/components/Setting.svelte';
@ -15,27 +17,39 @@
import { browser } from '$app/env';
import { post } from '$lib/api';
import { getDomain } from '$lib/components/common';
import { toast } from '@zerodevx/svelte-toast';
const { id } = $page.params;
let loading = false;
let publicLoading = false;
let isPublic = database.settings.isPublic || false;
let appendOnly = database.settings.appendOnly;
let databaseDefault = database.defaultDatabase;
let databaseDbUser = database.dbUser;
let databaseDbUserPassword = database.dbUserPassword;
if (database.type === 'mongodb') {
databaseDefault = '?readPreference=primary&ssl=false';
databaseDbUser = database.rootUser;
databaseDbUserPassword = database.rootUserPassword;
} else if (database.type === 'redis') {
databaseDefault = '';
databaseDbUser = '';
let databaseDefault;
let databaseDbUser;
let databaseDbUserPassword;
generateDbDetails();
function generateDbDetails() {
databaseDefault = database.defaultDatabase;
databaseDbUser = database.dbUser;
databaseDbUserPassword = database.dbUserPassword;
if (database.type === 'mongodb') {
databaseDefault = '?readPreference=primary&ssl=false';
databaseDbUser = database.rootUser;
databaseDbUserPassword = database.rootUserPassword;
} else if (database.type === 'redis') {
databaseDefault = '';
databaseDbUser = '';
}
}
let databaseUrl = generateUrl();
$: databaseUrl = generateUrl();
function generateUrl() {
return browser
return (databaseUrl = browser
? `${database.type}://${
databaseDbUser ? databaseDbUser + ':' : ''
}${databaseDbUserPassword}@${
@ -45,32 +59,50 @@
: window.location.hostname
: database.id
}:${isPublic ? database.publicPort : privatePort}/${databaseDefault}`
: 'Loading...';
: 'Loading...');
}
async function changeSettings(name) {
if (publicLoading || !isRunning) return;
publicLoading = true;
let data = {
isPublic,
appendOnly
};
if (name === 'isPublic') {
isPublic = !isPublic;
data.isPublic = !isPublic;
}
if (name === 'appendOnly') {
appendOnly = !appendOnly;
data.appendOnly = !appendOnly;
}
try {
const { publicPort } = await post(`/databases/${id}/settings.json`, { isPublic, appendOnly });
const { publicPort } = await post(`/databases/${id}/settings.json`, {
isPublic: data.isPublic,
appendOnly: data.appendOnly
});
isPublic = data.isPublic;
appendOnly = data.appendOnly;
databaseUrl = generateUrl();
if (isPublic) {
database.publicPort = publicPort;
}
databaseUrl = generateUrl();
} catch ({ error }) {
return errorNotification(error);
} finally {
publicLoading = false;
}
}
async function handleSubmit() {
try {
await post(`/databases/${id}.json`, { ...database });
return window.location.reload();
loading = true;
await post(`/databases/${id}.json`, { ...database, isRunning });
generateDbDetails();
databaseUrl = generateUrl();
toast.push('Settings saved.');
} catch ({ error }) {
return errorNotification(error);
} finally {
loading = false;
}
}
</script>
@ -142,21 +174,21 @@
readonly
disabled
name="publicPort"
value={isPublic ? database.publicPort : privatePort}
value={publicLoading ? 'Loading...' : isPublic ? database.publicPort : privatePort}
/>
</div>
</div>
<div class="grid grid-flow-row gap-2">
{#if database.type === 'mysql'}
<MySql bind:database />
<MySql bind:database {isRunning} />
{:else if database.type === 'postgresql'}
<PostgreSql bind:database />
<PostgreSql bind:database {isRunning} />
{:else if database.type === 'mongodb'}
<MongoDb {database} />
<MongoDb bind:database {isRunning} />
{:else if database.type === 'redis'}
<Redis {database} />
<Redis bind:database {isRunning} />
{:else if database.type === 'couchdb'}
<CouchDb bind:database />
<CouchDb {database} />
{/if}
<div class="grid grid-cols-2 items-center px-10 pb-8">
<label for="url" class="text-base font-bold text-stone-100">Connection String</label>
@ -168,7 +200,7 @@
name="url"
readonly
disabled
value={databaseUrl}
value={publicLoading || loading ? 'Loading...' : generateUrl()}
/>
</div>
</div>
@ -179,10 +211,12 @@
<div class="px-10 pb-10">
<div class="grid grid-cols-2 items-center">
<Setting
loading={publicLoading}
bind:setting={isPublic}
on:click={() => changeSettings('isPublic')}
title="Set it public"
description="Your database will be reachable over the internet. <br>Take security seriously in this case!"
disabled={!isRunning}
/>
</div>
{#if database.type === 'redis'}

View File

@ -1,6 +1,8 @@
<script>
export let database;
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script>
<div class="flex space-x-1 py-5 font-bold">
@ -21,13 +23,14 @@
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
isPasswordField={true}
readonly
disabled
id="rootUserPassword"
name="rootUserPassword"
value={database.rootUserPassword}
bind:value={database.rootUserPassword}
/>
<Explainer text="Could be changed while the database is running." />
</div>
</div>

View File

@ -1,6 +1,8 @@
<script>
export let database;
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script>
<div class="flex space-x-1 py-5 font-bold">
@ -33,14 +35,15 @@
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<CopyPasswordField
readonly
disabled
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
value={database.dbUserPassword}
bind:value={database.dbUserPassword}
/>
<Explainer text="Could be changed while the database is running." />
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
@ -56,13 +59,14 @@
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<CopyPasswordField
readonly
disabled
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword"
name="rootUserPassword"
value={database.rootUserPassword}
bind:value={database.rootUserPassword}
/>
<Explainer text="Could be changed while the database is running." />
</div>
</div>

View File

@ -1,6 +1,8 @@
<script>
export let database;
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script>
<div class="flex space-x-1 py-5 font-bold">
@ -19,6 +21,19 @@
bind:value={database.defaultDatabase}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100"
>Root (postgres) User Password</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
id="rootUserPassword"
name="rootUserPassword"
bind:value={database.rootUserPassword}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">User</label>
<CopyPasswordField
@ -33,13 +48,14 @@
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<CopyPasswordField
readonly
disabled
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
value={database.dbUserPassword}
bind:value={database.dbUserPassword}
/>
<Explainer text="Could be changed while the database is running." />
</div>
</div>

View File

@ -1,6 +1,8 @@
<script>
export let database;
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script>
<div class="flex space-x-1 py-5 font-bold">
@ -10,40 +12,14 @@
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<CopyPasswordField
disabled
readonly
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
value={database.dbUserPassword}
bind:value={database.dbUserPassword}
/>
<Explainer text="Could be changed while the database is running." />
</div>
<!-- <div class="grid grid-cols-3 items-center">
<label for="rootUser">Root User</label>
<div class="col-span-2 ">
<CopyPasswordField
disabled
readonly
placeholder="Generated automatically after start"
id="rootUser"
name="rootUser"
value={database.rootUser}
/>
</div>
</div>
<div class="grid grid-cols-3 items-center">
<label for="rootUserPassword">Root's Password</label>
<div class="col-span-2 ">
<CopyPasswordField
disabled
readonly
placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword"
name="rootUserPassword"
value={database.rootUserPassword}
/>
</div>
</div> -->
</div>

View File

@ -15,7 +15,7 @@
const endpoint = `/databases/${params.id}.json`;
const res = await fetch(endpoint);
if (res.ok) {
const { database, state, versions, privatePort, settings } = await res.json();
const { database, isRunning, versions, privatePort, settings } = await res.json();
if (!database || Object.entries(database).length === 0) {
return {
status: 302,
@ -35,13 +35,13 @@
return {
props: {
database,
state,
isRunning,
versions,
privatePort
},
stuff: {
database,
state,
isRunning,
versions,
privatePort,
settings
@ -65,7 +65,7 @@
import { goto } from '$app/navigation';
export let database;
export let state;
export let isRunning;
let loading = false;
async function deleteDatabase() {
@ -91,8 +91,6 @@
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
} finally {
loading = false;
}
}
}
@ -103,8 +101,6 @@
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
} finally {
loading = false;
}
}
</script>
@ -114,7 +110,7 @@
<Loading fullscreen cover />
{:else}
{#if database.type && database.destinationDockerId && database.version && database.defaultDatabase}
{#if state === 'running'}
{#if isRunning}
<button
on:click={stopDatabase}
title="Stop database"
@ -140,7 +136,7 @@
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</button>
{:else if state === 'not started'}
{:else}
<button
on:click={startDatabase}
title="Start database"

View File

@ -1,6 +1,7 @@
import { getUserDetails } from '$lib/common';
import { supportedDatabaseTypesAndVersions } from '$lib/components/common';
import * as db from '$lib/database';
import { ErrorHandler, supportedDatabaseTypesAndVersions } from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {

View File

@ -1,6 +1,7 @@
import { getUserDetails } from '$lib/common';
import { supportedDatabaseTypesAndVersions } from '$lib/components/common';
import * as db from '$lib/database';
import { ErrorHandler, supportedDatabaseTypesAndVersions } from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {

View File

@ -1,6 +1,11 @@
import { asyncExecShell, getEngine, getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { generateDatabaseConfiguration, getVersions, ErrorHandler } from '$lib/database';
import {
generateDatabaseConfiguration,
getVersions,
ErrorHandler,
updatePasswordInDb
} from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
@ -12,7 +17,7 @@ export const get: RequestHandler = async (event) => {
const database = await db.getDatabase({ id, teamId });
const { destinationDockerId, destinationDocker } = database;
let state = 'not started';
let isRunning = false;
if (destinationDockerId) {
const host = getEngine(destinationDocker.engine);
@ -22,7 +27,7 @@ export const get: RequestHandler = async (event) => {
);
if (JSON.parse(stdout).Running) {
state = 'running';
isRunning = true;
}
} catch (error) {
//
@ -34,7 +39,7 @@ export const get: RequestHandler = async (event) => {
body: {
privatePort: configuration?.privatePort,
database,
state,
isRunning,
versions: getVersions(database.type),
settings
}
@ -48,10 +53,26 @@ export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const { name, defaultDatabase, dbUser, dbUserPassword, rootUser, rootUserPassword, version } =
await event.request.json();
const {
name,
defaultDatabase,
dbUser,
dbUserPassword,
rootUser,
rootUserPassword,
version,
isRunning
} = await event.request.json();
try {
const database = await db.getDatabase({ id, teamId });
if (isRunning) {
if (database.dbUserPassword !== dbUserPassword) {
await updatePasswordInDb(database, dbUser, dbUserPassword, false);
} else if (database.rootUserPassword !== rootUserPassword) {
await updatePasswordInDb(database, rootUser, rootUserPassword, true);
}
}
await db.updateDatabase({
id,
name,

View File

@ -8,7 +8,8 @@
database: stuff.database,
versions: stuff.versions,
privatePort: stuff.privatePort,
settings: stuff.settings
settings: stuff.settings,
isRunning: stuff.isRunning
}
};
}
@ -31,37 +32,21 @@
</script>
<script lang="ts">
import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte';
import CouchDb from '$lib/components/svg/databases/CouchDB.svelte';
import MongoDb from '$lib/components/svg/databases/MongoDB.svelte';
import MySql from '$lib/components/svg/databases/MySQL.svelte';
import PostgreSql from '$lib/components/svg/databases/PostgreSQL.svelte';
import Redis from '$lib/components/svg/databases/Redis.svelte';
import DatabaseLinks from '$lib/components/DatabaseLinks.svelte';
export let database;
export let settings;
export let privatePort;
export let isRunning;
</script>
<div class="flex items-center space-x-2 p-6 text-2xl font-bold">
<div class="md:max-w-64 truncate text-base tracking-tight md:block md:text-2xl">
{database.name}
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Configuration
</div>
<span class="text-xs">{database.name}</span>
</div>
<span class="relative">
{#if database.type === 'clickhouse'}
<Clickhouse />
{:else if database.type === 'couchdb'}
<CouchDb />
{:else if database.type === 'mongodb'}
<MongoDb />
{:else if database.type === 'mysql'}
<MySql />
{:else if database.type === 'postgresql'}
<PostgreSql />
{:else if database.type === 'redis'}
<Redis />
{/if}
</span>
<DatabaseLinks {database} />
</div>
<Databases bind:database {privatePort} {settings} />
<Databases bind:database {privatePort} {settings} {isRunning} />

View File

@ -6,6 +6,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit';
import { makeLabelForStandaloneDatabase } from '$lib/buildPacks/common';
import { startTcpProxy } from '$lib/haproxy';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
@ -33,7 +34,7 @@ export const post: RequestHandler = async (event) => {
const { workdir } = await createDirectories({ repository: type, buildId: id });
const composeFile = {
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {

View File

@ -8,11 +8,22 @@
import Redis from '$lib/components/svg/databases/Redis.svelte';
import { post } from '$lib/api';
import { goto } from '$app/navigation';
import { session } from '$app/stores';
async function newDatabase() {
const { id } = await post('/databases/new', {});
return await goto(`/databases/${id}`, { replaceState: true });
}
const ownDatabases = databases.filter((database) => {
if (database.teams[0].id === $session.teamId) {
return database;
}
});
const otherDatabases = databases.filter((database) => {
if (database.teams[0].id !== $session.teamId) {
return database;
}
});
</script>
<div class="flex space-x-1 p-6 font-bold">
@ -34,40 +45,83 @@
</div>
</div>
<div class="flex flex-wrap justify-center">
{#if !databases || databases.length === 0}
<div class="flex flex-col flex-wrap justify-center">
{#if !databases || ownDatabases.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">No databases found</div>
</div>
{:else}
{#each databases as database}
<a href="/databases/{database.id}" class="no-underline p-2 w-96">
<div class="box-selection relative hover:bg-purple-600 group">
{#if database.type === 'clickhouse'}
<Clickhouse isAbsolute />
{:else if database.type === 'couchdb'}
<CouchDB isAbsolute />
{:else if database.type === 'mongodb'}
<MongoDB isAbsolute />
{:else if database.type === 'mysql'}
<MySQL isAbsolute />
{:else if database.type === 'postgresql'}
<PostgreSQL isAbsolute />
{:else if database.type === 'redis'}
<Redis isAbsolute />
{/if}
<div class="font-bold text-xl text-center truncate">
{database.name}
</div>
{#if !database.type}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">
Configuration missing
{/if}
{#if ownDatabases.length > 0 || otherDatabases.length > 0}
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownDatabases as database}
<a href="/databases/{database.id}" class="w-96 p-2 no-underline">
<div class="box-selection group relative hover:bg-purple-600">
{#if database.type === 'clickhouse'}
<Clickhouse isAbsolute />
{:else if database.type === 'couchdb'}
<CouchDB isAbsolute />
{:else if database.type === 'mongodb'}
<MongoDB isAbsolute />
{:else if database.type === 'mysql'}
<MySQL isAbsolute />
{:else if database.type === 'postgresql'}
<PostgreSQL isAbsolute />
{:else if database.type === 'redis'}
<Redis isAbsolute />
{/if}
<div class="truncate text-center text-xl font-bold">
{database.name}
</div>
{#if $session.teamId === '0' && otherDatabases.length > 0}
<div class="truncate text-center">{database.teams[0].name}</div>
{/if}
{#if !database.type}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>
{/if}
</div>
{:else}
<div class="text-center truncate">{database.type}</div>
{/if}
</a>
{/each}
</div>
{#if otherDatabases.length > 0 && $session.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Databases</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherDatabases as database}
<a href="/databases/{database.id}" class="w-96 p-2 no-underline">
<div class="box-selection group relative hover:bg-purple-600">
{#if database.type === 'clickhouse'}
<Clickhouse isAbsolute />
{:else if database.type === 'couchdb'}
<CouchDB isAbsolute />
{:else if database.type === 'mongodb'}
<MongoDB isAbsolute />
{:else if database.type === 'mysql'}
<MySQL isAbsolute />
{:else if database.type === 'postgresql'}
<PostgreSQL isAbsolute />
{:else if database.type === 'redis'}
<Redis isAbsolute />
{/if}
<div class="truncate text-center text-xl font-bold">
{database.name}
</div>
{#if $session.teamId === '0'}
<div class="truncate text-center">{database.teams[0].name}</div>
{/if}
{#if !database.type}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>
{:else}
<div class="text-center truncate">{database.type}</div>
{/if}
</div>
</a>
{/each}
</div>
</a>
{/each}
{/if}
</div>
{/if}
</div>

View File

@ -12,8 +12,8 @@
import { onMount } from 'svelte';
const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
// let scannedApps = [];
let loading = false;
let loadingProxy = false;
let restarting = false;
async function handleSubmit() {
loading = true;
@ -25,12 +25,6 @@
loading = false;
}
}
// async function scanApps() {
// scannedApps = [];
// const data = await fetch(`/destinations/${id}/scan.json`);
// const { containers } = await data.json();
// scannedApps = containers;
// }
onMount(async () => {
if (state === false && destination.isCoolifyProxyUsed === true) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
@ -71,6 +65,7 @@
}
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try {
loadingProxy = true;
await post(`/destinations/${id}/settings.json`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine
@ -82,6 +77,8 @@
}
} catch ({ error }) {
return errorNotification(error);
} finally {
loadingProxy = false;
}
}
}
@ -184,41 +181,20 @@
value={destination.network}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
title="Use Coolify Proxy?"
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${
cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
: ''
}`}
/>
</div>
</form>
<!-- <div class="flex justify-center">
{#if payload.isCoolifyProxyUsed}
{#if state}
<button on:click={stopProxy}>Stop proxy</button>
{:else}
<button on:click={startProxy}>Start proxy</button>
{/if}
{#if $session.teamId === '0'}
<div class="grid grid-cols-2 items-center">
<Setting
loading={loadingProxy}
disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
title="Use Coolify Proxy?"
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${
cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
: ''
}`}
/>
</div>
{/if}
</div> -->
<!-- {#if scannedApps.length > 0}
<div class="flex justify-center px-6 pb-10">
<div class="flex space-x-2 h-8 items-center">
<div class="font-bold text-xl text-white">Found applications</div>
</div>
</div>
<div class="max-w-4xl mx-auto px-6">
<div class="flex space-x-2 justify-center">
{#each scannedApps as app}
<FoundApp {app} />
{/each}
</div>
</div>
{/if} -->
</form>

View File

@ -8,7 +8,7 @@ import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
console.log(teamId);
const { id } = event.params;
try {
const destination = await db.getDestination({ id, teamId });

View File

@ -24,6 +24,16 @@
import { session } from '$app/stores';
export let destinations: Prisma.DestinationDocker[];
const ownDestinations = destinations.filter((destination) => {
if (destination.teams[0].id === $session.teamId) {
return destination;
}
});
const otherDestinations = destinations.filter((destination) => {
if (destination.teams[0].id !== $session.teamId) {
return destination;
}
});
</script>
<div class="flex space-x-1 p-6 font-bold">
@ -47,20 +57,43 @@
{/if}
</div>
<div class="flex justify-center">
{#if !destinations || destinations.length === 0}
{#if !destinations || ownDestinations.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">No destination found</div>
</div>
{:else}
<div class="flex flex-wrap justify-center">
{#each destinations as destination}
<a href="/destinations/{destination.id}" class="no-underline p-2 w-96">
<div class="box-selection hover:bg-sky-600">
<div class="font-bold text-xl text-center truncate">{destination.name}</div>
<div class="text-center truncate">{destination.network}</div>
</div>
</a>
{/each}
{/if}
{#if ownDestinations.length > 0 || otherDestinations.length > 0}
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownDestinations as destination}
<a href="/destinations/{destination.id}" class="w-96 p-2 no-underline">
<div class="box-selection hover:bg-sky-600">
<div class="truncate text-center text-xl font-bold">{destination.name}</div>
{#if $session.teamId === '0' && otherDestinations.length > 0}
<div class="truncate text-center">{destination.teams[0].name}</div>
{/if}
<div class="truncate text-center">{destination.network}</div>
</div>
</a>
{/each}
</div>
{#if otherDestinations.length > 0 && $session.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Destinations</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherDestinations as destination}
<a href="/destinations/{destination.id}" class="w-96 p-2 no-underline">
<div class="box-selection hover:bg-sky-600">
<div class="truncate text-center text-xl font-bold">{destination.name}</div>
{#if $session.teamId === '0'}
<div class="truncate text-center">{destination.teams[0].name}</div>
{/if}
<div class="truncate text-center">{destination.network}</div>
</div>
</a>
{/each}
</div>
{/if}
</div>
{/if}
</div>

View File

@ -0,0 +1,130 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { teamId, userId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
try {
const account = await db.prisma.user.findUnique({
where: { id: userId },
select: { id: true, email: true, teams: true }
});
let accounts = [];
if (teamId === '0') {
accounts = await db.prisma.user.findMany({ select: { id: true, email: true, teams: true } });
}
const teams = await db.prisma.permission.findMany({
where: { userId: teamId === '0' ? undefined : userId },
include: { team: { include: { _count: { select: { users: true } } } } }
});
const invitations = await db.prisma.teamInvitation.findMany({ where: { uid: userId } });
return {
status: 200,
body: {
teams,
invitations,
account,
accounts
}
};
} catch (error) {
return ErrorHandler(error);
}
};
export const post: RequestHandler = async (event) => {
const { teamId, userId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
if (teamId !== '0')
return { status: 401, body: { message: 'You are not authorized to perform this action' } };
const { id } = await event.request.json();
try {
const aloneInTeams = await db.prisma.team.findMany({ where: { users: { every: { id } } } });
if (aloneInTeams.length > 0) {
for (const team of aloneInTeams) {
const applications = await db.prisma.application.findMany({
where: { teams: { every: { id: team.id } } }
});
if (applications.length > 0) {
for (const application of applications) {
await db.prisma.application.update({
where: { id: application.id },
data: { teams: { connect: { id: '0' } } }
});
}
}
const services = await db.prisma.service.findMany({
where: { teams: { every: { id: team.id } } }
});
if (services.length > 0) {
for (const service of services) {
await db.prisma.service.update({
where: { id: service.id },
data: { teams: { connect: { id: '0' } } }
});
}
}
const databases = await db.prisma.database.findMany({
where: { teams: { every: { id: team.id } } }
});
if (databases.length > 0) {
for (const database of databases) {
await db.prisma.database.update({
where: { id: database.id },
data: { teams: { connect: { id: '0' } } }
});
}
}
const sources = await db.prisma.gitSource.findMany({
where: { teams: { every: { id: team.id } } }
});
if (sources.length > 0) {
for (const source of sources) {
await db.prisma.gitSource.update({
where: { id: source.id },
data: { teams: { connect: { id: '0' } } }
});
}
}
const destinations = await db.prisma.destinationDocker.findMany({
where: { teams: { every: { id: team.id } } }
});
if (destinations.length > 0) {
for (const destination of destinations) {
await db.prisma.destinationDocker.update({
where: { id: destination.id },
data: { teams: { connect: { id: '0' } } }
});
}
}
await db.prisma.teamInvitation.deleteMany({ where: { teamId: team.id } });
await db.prisma.permission.deleteMany({ where: { teamId: team.id } });
await db.prisma.user.delete({ where: { id } });
await db.prisma.team.delete({ where: { id: team.id } });
}
}
const notAloneInTeams = await db.prisma.team.findMany({ where: { users: { some: { id } } } });
if (notAloneInTeams.length > 0) {
for (const team of notAloneInTeams) {
await db.prisma.team.update({
where: { id: team.id },
data: { users: { disconnect: { id } } }
});
}
}
return {
status: 201
};
} catch (error) {
return {
status: 500
};
}
};

175
src/routes/iam/index.svelte Normal file
View File

@ -0,0 +1,175 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch }) => {
const url = `/iam.json`;
const res = await fetch(url);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
if (res.status === 401) {
return {
status: 302,
redirect: '/'
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts">
import { session } from '$app/stores';
import { get, post } from '$lib/api';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
export let account;
export let accounts;
if (accounts.length === 0) {
accounts.push(account);
}
export let teams;
const ownTeams = teams.filter((team) => {
if (team.team.id === $session.teamId) {
return team;
}
});
const otherTeams = teams.filter((team) => {
if (team.team.id !== $session.teamId) {
return team;
}
});
async function resetPassword(id) {
const sure = window.confirm('Are you sure you want to reset the password?');
if (!sure) {
return;
}
try {
await post(`/iam/password.json`, { id });
toast.push('Password reset successfully. Please relogin to reset it.');
} catch ({ error }) {
return errorNotification(error);
}
}
async function deleteUser(id) {
const sure = window.confirm('Are you sure you want to delete this user?');
if (!sure) {
return;
}
try {
await post(`/iam.json`, { id });
toast.push('Account deleted.');
const data = await get('/iam.json');
accounts = data.accounts;
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Identity and Access Management</div>
</div>
<div class="mx-auto max-w-4xl px-6 py-4">
{#if $session.teamId === '0' && accounts.length > 0}
<div class="title font-bold">Accounts</div>
{:else}
<div class="title font-bold">Account</div>
{/if}
<div class="flex items-center justify-center pt-10">
<table class="mx-2 text-left">
<thead class="mb-2">
<tr>
{#if accounts.length > 1}
<th class="px-2">Email</th>
<th>Actions</th>
{/if}
</tr>
</thead>
<tbody>
{#each accounts as account}
<tr>
<td class="px-2">{account.email}</td>
<td class="flex space-x-2">
<form on:submit|preventDefault={() => resetPassword(account.id)}>
<button
class="mx-auto my-4 w-32 bg-coollabs hover:bg-coollabs-100 disabled:bg-coolgray-200"
>Reset Password</button
>
</form>
<form on:submit|preventDefault={() => deleteUser(account.id)}>
<button
disabled={account.id === $session.userId}
class="mx-auto my-4 w-32 bg-coollabs hover:bg-coollabs-100 disabled:bg-coolgray-200"
type="submit">Delete User</button
>
</form>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
<div class="mx-auto max-w-4xl px-6">
<div class="title font-bold">Teams</div>
<div class="flex items-center justify-center pt-10">
<div class="flex flex-col">
<div class="flex flex-col flex-wrap justify-center px-2 pb-10 md:flex-row">
{#each ownTeams as team}
<a href="/iam/team/{team.teamId}" class="w-96 p-2 no-underline">
<div
class="box-selection relative"
class:hover:bg-cyan-600={team.team?.id !== '0'}
class:hover:bg-red-500={team.team?.id === '0'}
>
<div class="truncate text-center text-xl font-bold">
{team.team.name}
</div>
<div class="truncate text-center font-bold">
{team.team?.id === '0' ? 'root team' : ''}
</div>
<div class="mt-1 text-center">{team.team._count.users} member(s)</div>
</div>
</a>
{/each}
</div>
{#if $session.teamId === '0' && otherTeams.length > 0}
<div class="pb-5 pt-10 text-xl font-bold">Other Teams</div>
{/if}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherTeams as team}
<a href="/iam/team/{team.teamId}" class="w-96 p-2 no-underline">
<div
class="box-selection relative"
class:hover:bg-cyan-600={team.team?.id !== '0'}
class:hover:bg-red-500={team.team?.id === '0'}
>
<div class="truncate text-center text-xl font-bold">
{team.team.name}
</div>
<div class="truncate text-center font-bold">
{team.team?.id === '0' ? 'root team' : ''}
</div>
<div class="mt-1 text-center">{team.team._count.users} member(s)</div>
</div>
</a>
{/each}
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,22 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { teamId, userId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = await event.request.json();
try {
await db.prisma.user.update({ where: { id }, data: { password: 'RESETME' } });
return {
status: 201
};
} catch (error) {
console.log(error);
return {
status: 500
};
}
};

View File

@ -1,14 +1,14 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params }) => {
const url = `/teams/${params.id}.json`;
const url = `/iam/team/${params.id}.json`;
const res = await fetch(url);
if (res.ok) {
const data = await res.json();
if (!data.permissions || Object.entries(data.permissions).length === 0) {
return {
status: 302,
redirect: '/teams'
redirect: '/iam'
};
}
return {
@ -20,7 +20,7 @@
return {
status: 302,
redirect: '/teams'
redirect: '/iam'
};
};
</script>

View File

@ -4,14 +4,14 @@ import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { userId, status, body } = await getUserDetails(event, false);
const { teamId, userId, status, body } = await getUserDetails(event, false);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const user = await db.prisma.user.findFirst({
where: { id: userId, teams: { some: { id } } },
where: { id: userId, teams: teamId === '0' ? undefined : { some: { id } } },
include: { permission: true }
});
if (!user) {

View File

@ -1,7 +1,7 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params }) => {
const url = `/teams/${params.id}.json`;
const url = `/iam/team/${params.id}.json`;
const res = await fetch(url);
if (res.ok) {
@ -44,7 +44,7 @@
async function sendInvitation() {
try {
await post(`/teams/${id}/invitation/invite.json`, {
await post(`/iam/team/${id}/invitation/invite.json`, {
teamId: team.id,
teamName: invitation.teamName,
email: invitation.email.toLowerCase(),
@ -57,7 +57,7 @@
}
async function revokeInvitation(id: string) {
try {
await post(`/teams/${id}/invitation/revoke.json`, { id });
await post(`/iam/team/${id}/invitation/revoke.json`, { id });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
@ -65,7 +65,7 @@
}
async function removeFromTeam(uid: string) {
try {
await post(`/teams/${id}/remove/user.json`, { teamId: team.id, uid });
await post(`/iam/team/${id}/remove/user.json`, { teamId: team.id, uid });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
@ -77,7 +77,7 @@
newPermission = 'admin';
}
try {
await post(`/teams/${id}/permission/change.json`, { userId, newPermission, permissionId });
await post(`/iam/team/${id}/permission/change.json`, { userId, newPermission, permissionId });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
@ -85,7 +85,7 @@
}
async function handleSubmit() {
try {
await post(`/teams/${id}.json`, { ...team });
await post(`/iam/team/${id}.json`, { ...team });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);

View File

@ -92,7 +92,7 @@
</a>
<a
href="/teams"
href="/iam"
sveltekit:prefetch
class="flex cursor-pointer flex-col rounded p-6 text-center text-cyan-500 no-underline transition duration-150 hover:bg-cyan-500 hover:text-white"
>

View File

@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { session } from '$app/stores';
export let payload;
@ -56,13 +57,15 @@
<label for="network" class="text-base font-bold text-stone-100">Network</label>
<input required name="network" placeholder="default: coolify" bind:value={payload.network} />
</div>
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={payload.isCoolifyProxyUsed}
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
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).<br><br>Databases will have their own proxy."
/>
</div>
{#if $session.teamId === '0'}
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={payload.isCoolifyProxyUsed}
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
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).<br><br>Databases will have their own proxy."
/>
</div>
{/if}
</form>
</div>

View File

@ -1,96 +0,0 @@
<script lang="ts">
export let gitSource;
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import Explainer from '$lib/components/Explainer.svelte';
import { errorNotification } from '$lib/form';
import { onMount } from 'svelte';
let nameEl;
let organizationEl;
onMount(() => {
nameEl.focus();
});
async function handleSubmit() {
try {
const { id } = await post(`/new/source.json`, { ...gitSource });
return await goto(`/sources/${id}/`);
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="mx-auto max-w-4xl px-6">
<div class="flex justify-center pb-8">
<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="text-xl font-bold text-white">Configuration</div>
<button type="submit" class="bg-orange-600 hover:bg-orange-500">Save</button>
</div>
<div class="grid grid-cols-2 items-center px-10">
<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}>
<option value="github">GitHub</option>
<option value="gitlab">GitLab</option>
<option value="bitbucket">BitBucket</option>
</select>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<input
name="name"
id="name"
placeholder="GitHub.com"
required
bind:this={nameEl}
bind:value={gitSource.name}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="htmlUrl" class="text-base font-bold text-stone-100">HTML URL</label>
<input
type="url"
name="htmlUrl"
id="htmlUrl"
placeholder="eg: https://github.com"
required
bind:value={gitSource.htmlUrl}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<input
name="apiUrl"
type="url"
id="apiUrl"
placeholder="eg: https://api.github.com"
required
bind:value={gitSource.apiUrl}
/>
</div>
<div class="grid grid-cols-2 px-10">
<div class="flex flex-col">
<label for="organization" class="pt-2 text-base font-bold text-stone-100"
>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
name="organization"
id="organization"
placeholder="eg: coollabsio"
bind:value={gitSource.organization}
bind:this={organizationEl}
/>
</div>
</form>
</div>
</div>

View File

@ -1,73 +0,0 @@
<script lang="ts">
export let gitSource;
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { errorNotification } from '$lib/form';
import { onMount } from 'svelte';
let nameEl;
onMount(() => {
nameEl.focus();
});
async function handleSubmit() {
try {
const { id } = await post(`/new/source.json`, { ...gitSource });
return await goto(`/sources/${id}/`);
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="flex justify-center pb-8">
<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="text-xl font-bold text-white">Configuration</div>
<button type="submit" class="bg-orange-600 hover:bg-orange-500">Save</button>
</div>
<div class="grid grid-cols-2 items-center px-10">
<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}>
<option value="github">GitHub</option>
<option value="gitlab">GitLab</option>
<option value="bitbucket">BitBucket</option>
</select>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<input
name="name"
id="name"
placeholder="GitHub.com"
required
bind:this={nameEl}
bind:value={gitSource.name}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="htmlUrl" class="text-base font-bold text-stone-100">HTML URL</label>
<input
type="url"
name="htmlUrl"
id="htmlUrl"
placeholder="eg: https://github.com"
required
bind:value={gitSource.htmlUrl}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<input
name="apiUrl"
type="url"
id="apiUrl"
placeholder="eg: https://api.github.com"
required
bind:value={gitSource.apiUrl}
/>
</div>
</form>
</div>

View File

@ -1,66 +0,0 @@
<script lang="ts">
import Github from './_Github.svelte';
import Gitlab from './_Gitlab.svelte';
let gitSource = {
name: undefined,
type: 'github',
htmlUrl: undefined,
apiUrl: undefined,
organization: undefined
};
function setPredefined(type) {
switch (type) {
case 'github':
gitSource = {
name: 'GitHub.com',
type,
htmlUrl: 'https://github.com',
apiUrl: 'https://api.github.com',
organization: undefined
};
break;
case 'gitlab':
gitSource = {
name: 'GitLab.com',
type,
htmlUrl: 'https://gitlab.com',
apiUrl: 'https://gitlab.com/api',
organization: undefined
};
break;
case 'bitbucket':
gitSource = {
name: 'BitBucket.com',
type,
htmlUrl: 'https://bitbucket.com',
apiUrl: 'https://bitbucket.com',
organization: undefined
};
break;
default:
break;
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Add New Git Source</div>
</div>
<div class="flex-col space-y-2 pb-10 text-center">
<div class="text-xl font-bold text-white">Official providers</div>
<div class="flex justify-center space-x-2">
<button class="w-32" on:click={() => setPredefined('github')}>GitHub.com</button>
<button class="w-32" on:click={() => setPredefined('gitlab')}>GitLab.com</button>
<button class="w-32" on:click={() => setPredefined('bitbucket')}>Bitbucket.com</button>
</div>
</div>
<div class="px-6">
{#if gitSource.type === 'github'}
<Github {gitSource} />
{:else if gitSource.type === 'gitlab'}
<Gitlab {gitSource} />
{:else if gitSource.type === 'bitbucket'}
<div class="text-center font-bold text-4xl py-10">Not implemented yet</div>
{/if}
</div>

View File

@ -19,6 +19,9 @@
emailEl.focus();
});
async function handleSubmit() {
// Prevent double submission
if (loading) return;
if (password !== passwordCheck) {
return errorNotification('Passwords do not match.');
}
@ -88,8 +91,13 @@
/>
<div class="flex space-x-2 h-8 items-center justify-center pt-8">
<button type="submit" class="hover:bg-coollabs-100 text-white bg-coollabs"
>Register</button
<button
type="submit"
class="hover:bg-coollabs-100 text-white"
disabled={loading}
class:bg-transparent={loading}
class:text-stone-600={loading}
class:bg-coollabs={!loading}>{loading ? 'Registering...' : 'Register'}</button
>
</div>
</form>

View File

@ -0,0 +1,19 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
export let service;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">MeiliSearch</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="masterKey">Admin API key</label>
<CopyPasswordField
id="masterKey"
isPasswordField
readonly
disabled
name="masterKey"
value={service.meiliSearch.masterKey}
/>
</div>

Some files were not shown because too many files have changed in this diff Show More