Merge branch 'main' into main

This commit is contained in:
esdete 2022-04-08 20:07:43 +02:00 committed by GitHub
commit 157e5fd7aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
105 changed files with 2338 additions and 1208 deletions

View File

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

View File

@ -16,7 +16,7 @@ # Recommended Pull Request Guideline
- Push to your fork repo - Push to your fork repo
- Create a pull request: https://github.com/coollabsio/compare - Create a pull request: https://github.com/coollabsio/compare
- Write a proper description - Write a proper description
- Click "Change to draft" - Open the pull request to review
# How to start after you set up your local fork? # How to start after you set up your local fork?

View File

@ -11,7 +11,7 @@ WORKDIR /app
LABEL coolify.managed true LABEL coolify.managed true
RUN apk add --no-cache git git-lfs openssh-client curl jq cmake 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 curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@6
RUN pnpm add -g pnpm RUN pnpm add -g pnpm

View File

@ -1,7 +1,7 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.3.3", "version": "2.4.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev", "dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev",

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

@ -117,8 +117,8 @@ model ApplicationSettings {
model ApplicationPersistentStorage { model ApplicationPersistentStorage {
id String @id @default(cuid()) id String @id @default(cuid())
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])
applicationId String @unique applicationId String
path String @unique path String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@ -332,6 +332,12 @@ model Wordpress {
mysqlRootUserPassword String mysqlRootUserPassword String
mysqlDatabase String? mysqlDatabase String?
mysqlPublicPort Int? mysqlPublicPort Int?
ftpEnabled Boolean @default(false)
ftpUser String?
ftpPassword String?
ftpPublicPort Int?
ftpHostKey String?
ftpHostKeyPrivate String?
serviceId String @unique serviceId String @unique
service Service @relation(fields: [serviceId], references: [id]) service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())

6
src/app.d.ts vendored
View File

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

View File

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

View File

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

View File

@ -4,6 +4,12 @@ import { promises as fs } from 'fs';
const createDockerfile = async (data, image, htaccessFound): Promise<void> => { const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
const { workdir, baseDirectory } = data; const { workdir, baseDirectory } = data;
const Dockerfile: Array<string> = []; 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(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
@ -11,6 +17,10 @@ const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
if (htaccessFound) { if (htaccessFound) {
Dockerfile.push(`COPY .${baseDirectory || ''}/.htaccess ./`); 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(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
@ -21,12 +31,14 @@ export default async function (data) {
try { try {
let htaccessFound = false; let htaccessFound = false;
try { try {
const d = await fs.readFile(`${workdir}${baseDirectory || ''}/.htaccess`); await fs.readFile(`${workdir}${baseDirectory || ''}/.htaccess`);
htaccessFound = true; htaccessFound = true;
} catch (e) { } catch (e) {
// //
} }
const image = htaccessFound ? 'webdevops/php-apache' : 'webdevops/php-nginx'; const image = htaccessFound
? 'webdevops/php-apache:8.0-alpine'
: 'webdevops/php-nginx:8.0-alpine';
await createDockerfile(data, image, htaccessFound); await createDockerfile(data, image, htaccessFound);
await buildImage(data); await buildImage(data);
} catch (error) { } catch (error) {

View File

@ -52,12 +52,14 @@ export const sentry = Sentry;
export const uniqueName = () => uniqueNamesGenerator(customConfig); export const uniqueName = () => uniqueNamesGenerator(customConfig);
export const saveBuildLog = async ({ line, buildId, applicationId }) => { export const saveBuildLog = async ({ line, buildId, applicationId }) => {
if (line) {
if (line.includes('ghs_')) { if (line.includes('ghs_')) {
const regex = /ghs_.*@/g; const regex = /ghs_.*@/g;
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@'); line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
} }
const addTimestamp = `${generateTimestamp()} ${line}`; const addTimestamp = `${generateTimestamp()} ${line}`;
return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId }); return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId });
}
}; };
export const isTeamIdTokenAvailable = (request) => { export const isTeamIdTokenAvailable = (request) => {
@ -100,6 +102,7 @@ export const getUserDetails = async (event, isAdminRequired = true) => {
message: 'OK' message: 'OK'
} }
}; };
if (isAdminRequired && permission !== 'admin' && permission !== 'owner') { if (isAdminRequired && permission !== 'admin' && permission !== 'owner') {
payload.status = 401; payload.status = 401;
payload.body.message = payload.body.message =

View File

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

View File

@ -43,3 +43,142 @@ export function changeQueryParams(buildId) {
queryParams.set('buildId', buildId); queryParams.set('buildId', buildId);
return history.pushState(null, null, '?' + queryParams.toString()); 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

@ -5,7 +5,13 @@ import { getDomain, removeDestinationDocker } from '$lib/common';
import { prisma } from './common'; import { prisma } from './common';
export async function listApplications(teamId) { 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 }) { 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.build.deleteMany({ where: { applicationId: id } });
await prisma.secret.deleteMany({ where: { applicationId: id } }); await prisma.secret.deleteMany({ where: { applicationId: id } });
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } }); await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } });
if (teamId === '0') {
await prisma.application.deleteMany({ where: { id } });
} else {
await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } }); await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } });
}
} }
export async function getApplicationWebhook({ projectId, branch }) { export async function getApplicationWebhook({ projectId, branch }) {
@ -130,7 +140,20 @@ export async function getApplicationById({ id }) {
return { ...body }; return { ...body };
} }
export async function getApplication({ id, teamId }) { export async function getApplication({ id, teamId }) {
let body = await prisma.application.findFirst({ 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 } } }, where: { id, teams: { some: { id: teamId } } },
include: { include: {
destinationDocker: true, destinationDocker: true,
@ -140,6 +163,7 @@ export async function getApplication({ id, teamId }) {
persistentStorage: true persistentStorage: true
} }
}); });
}
if (body?.gitSource?.githubApp?.clientSecret) { if (body?.gitSource?.githubApp?.clientSecret) {
body.gitSource.githubApp.clientSecret = decrypt(body.gitSource.githubApp.clientSecret); body.gitSource.githubApp.clientSecret = decrypt(body.gitSource.githubApp.clientSecret);

View File

@ -1,5 +1,9 @@
import { dev } from '$app/env'; import { dev } from '$app/env';
import { sentry } from '$lib/common'; import { sentry } from '$lib/common';
import {
supportedDatabaseTypesAndVersions,
supportedServiceTypesAndVersions
} from '$lib/components/common';
import * as Prisma from '@prisma/client'; import * as Prisma from '@prisma/client';
import { default as ProdPrisma } from '@prisma/client'; import { default as ProdPrisma } from '@prisma/client';
import type { PrismaClientOptions } from '@prisma/client/runtime'; import type { PrismaClientOptions } from '@prisma/client/runtime';
@ -82,134 +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
}
},
{
name: 'meilisearch',
fancyName: 'Meilisearch',
baseImage: 'getmeili/meilisearch',
images: [],
versions: ['latest'],
ports: {
main: 7700
}
}
];
export function getVersions(type) { export function getVersions(type) {
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type); const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) { if (found) {

View File

@ -7,7 +7,14 @@ import getPort, { portNumbers } from 'get-port';
import { asyncExecShell, getEngine, removeContainer } from '$lib/common'; import { asyncExecShell, getEngine, removeContainer } from '$lib/common';
export async function listDatabases(teamId) { export async function listDatabases(teamId) {
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 }) { export async function newDatabase({ name, teamId }) {
const dbUser = cuid(); const dbUser = cuid();
@ -31,10 +38,18 @@ export async function newDatabase({ name, teamId }) {
} }
export async function getDatabase({ id, teamId }) { export async function getDatabase({ id, teamId }) {
const body = await prisma.database.findFirst({ 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 } } }, where: { id, teams: { some: { id: teamId } } },
include: { destinationDocker: true, settings: true } include: { destinationDocker: true, settings: true }
}); });
}
if (body.dbUserPassword) body.dbUserPassword = decrypt(body.dbUserPassword); if (body.dbUserPassword) body.dbUserPassword = decrypt(body.dbUserPassword);
if (body.rootUserPassword) body.rootUserPassword = decrypt(body.rootUserPassword); if (body.rootUserPassword) body.rootUserPassword = decrypt(body.rootUserPassword);
@ -122,3 +137,37 @@ export async function stopDatabase(database) {
} }
return everStarted; return everStarted;
} }
export async function updatePasswordInDb(database, user, newPassword) {
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') {
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'; import { prisma } from './common';
export async function listDestinations(teamId) { 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 }) { export async function configureDestinationForService({ id, destinationId }) {
@ -38,9 +44,7 @@ export async function configureDestinationForDatabase({ id, destinationId }) {
const host = getEngine(engine); const host = getEngine(engine);
if (type && version) { if (type && version) {
const baseImage = getDatabaseImage(type); const baseImage = getDatabaseImage(type);
asyncExecShell( asyncExecShell(`DOCKER_HOST=${host} docker pull ${baseImage}:${version}`);
`DOCKER_HOST=${host} docker pull ${baseImage}:${version} && echo "FROM ${baseImage}:${version}" | docker build --label coolify.image="true" -t "${baseImage}:${version}" -`
);
} }
} }
} }
@ -124,12 +128,17 @@ export async function removeDestination({ id }) {
} }
export async function getDestination({ id, teamId }) { export async function getDestination({ id, teamId }) {
let destination = await prisma.destinationDocker.findFirst({ let destination = {};
if (teamId === '0') {
destination = await prisma.destinationDocker.findFirst({
where: { id }
});
} else {
destination = await prisma.destinationDocker.findFirst({
where: { id, teams: { some: { id: teamId } } } where: { id, teams: { some: { id: teamId } } }
}); });
if (destination.remoteEngine) {
destination.sshPrivateKey = decrypt(destination.sshPrivateKey);
} }
return destination; return destination;
} }
export async function getDestinationByApplicationId({ id, teamId }) { export async function getDestinationByApplicationId({ id, teamId }) {

View File

@ -2,26 +2,26 @@ import { decrypt, encrypt } from '$lib/crypto';
import { prisma } from './common'; import { prisma } from './common';
export async function listSources(teamId) { 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({ return await prisma.gitSource.findMany({
where: { teams: { some: { id: teamId } } }, 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({ return await prisma.gitSource.create({
data: { data: {
teams: { connect: { id: teamId } },
name, name,
type, teams: { connect: { id: teamId } }
htmlUrl,
apiUrl,
organization
} }
}); });
} }
export async function removeSource({ id }) { export async function removeSource({ id }) {
// TODO: Disconnect application with this sourceId! Maybe not needed?
const source = await prisma.gitSource.delete({ const source = await prisma.gitSource.delete({
where: { id }, where: { id },
include: { githubApp: true, gitlabApp: true } include: { githubApp: true, gitlabApp: true }
@ -31,10 +31,18 @@ export async function removeSource({ id }) {
} }
export async function getSource({ id, teamId }) { export async function getSource({ id, teamId }) {
let body = await prisma.gitSource.findFirst({ 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 } } }, where: { id, teams: { some: { id: teamId } } },
include: { githubApp: true, gitlabApp: true } include: { githubApp: true, gitlabApp: true }
}); });
}
if (body?.githubApp?.clientSecret) if (body?.githubApp?.clientSecret)
body.githubApp.clientSecret = decrypt(body.githubApp.clientSecret); body.githubApp.clientSecret = decrypt(body.githubApp.clientSecret);
if (body?.githubApp?.webhookSecret) 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); if (body?.gitlabApp?.appSecret) body.gitlabApp.appSecret = decrypt(body.gitlabApp.appSecret);
return body; 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); const encrptedAppSecret = encrypt(appSecret);
await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name } });
return await prisma.gitlabApp.create({ return await prisma.gitlabApp.create({
data: { data: {
teams: { connect: { id: teamId } }, teams: { connect: { id: teamId } },
@ -63,9 +92,9 @@ export async function configureGitsource({ id, gitSourceId }) {
data: { gitSource: { connect: { 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({ return await prisma.gitSource.update({
where: { id }, where: { id },
data: { name } data: { name, htmlUrl, apiUrl }
}); });
} }

View File

@ -5,7 +5,14 @@ import { generatePassword } from '.';
import { prisma } from './common'; import { prisma } from './common';
export async function listServices(teamId) { 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 }) { export async function newService({ name, teamId }) {
@ -13,9 +20,8 @@ export async function newService({ name, teamId }) {
} }
export async function getService({ id, teamId }) { export async function getService({ id, teamId }) {
const body = await prisma.service.findFirst({ let body = {};
where: { id, teams: { some: { id: teamId } } }, const include = {
include: {
destinationDocker: true, destinationDocker: true,
plausibleAnalytics: true, plausibleAnalytics: true,
minio: true, minio: true,
@ -24,8 +30,18 @@ export async function getService({ id, teamId }) {
ghost: true, ghost: true,
serviceSecret: true, serviceSecret: true,
meiliSearch: 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) if (body.plausibleAnalytics?.postgresqlPassword)
body.plausibleAnalytics.postgresqlPassword = decrypt( body.plausibleAnalytics.postgresqlPassword = decrypt(
@ -59,8 +75,12 @@ export async function getService({ id, teamId }) {
return s; 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 }) { export async function configureServiceType({ id, type }) {
@ -200,18 +220,6 @@ export async function updatePlausibleAnalyticsService({ id, fqdn, email, usernam
export async function updateService({ id, fqdn, name }) { export async function updateService({ id, fqdn, name }) {
return await prisma.service.update({ where: { id }, data: { 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 updateMeiliSearchService({ 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 }) { export async function updateWordpress({ id, fqdn, name, mysqlDatabase, extraConfig }) {
return await prisma.service.update({ return await prisma.service.update({
where: { id }, where: { id },

View File

@ -32,26 +32,42 @@ export async function login({ email, password, isLogin }) {
if (users === 0) { if (users === 0) {
await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } }); await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } });
// Create default network & start Coolify Proxy // Create default network & start Coolify Proxy
asyncExecShell(`docker network create --attachable coolify`) await asyncExecShell(`docker network create --attachable coolify`);
.then(() => { await startCoolifyProxy('/var/run/docker.sock');
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);
});
uid = '0'; uid = '0';
} }
if (userFound) { if (userFound) {
if (userFound.type === 'email') { 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); const passwordMatch = await bcrypt.compare(password, userFound.password);
if (!passwordMatch) { if (!passwordMatch) {
throw { throw {

View File

@ -6,6 +6,7 @@ import crypto from 'crypto';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { checkContainer, checkHAProxy } from '.'; import { checkContainer, checkHAProxy } from '.';
import { asyncExecShell, getDomain, getEngine } from '$lib/common'; import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555'; const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
@ -223,7 +224,7 @@ export async function configureHAProxy() {
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service; const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;
if (destinationDockerId) { if (destinationDockerId) {
const { engine } = destinationDocker; const { engine } = destinationDocker;
const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type); const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) { if (found) {
const port = found.ports.main; const port = found.ports.main;
const publicPort = service[type]?.publicPort; const publicPort = service[type]?.publicPort;

View File

@ -108,7 +108,7 @@ export async function stopTcpHttpProxy(destinationDocker, publicPort) {
return error; 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 { network, engine } = destinationDocker;
const host = getEngine(engine); const host = getEngine(engine);
@ -123,7 +123,9 @@ export async function startTcpProxy(destinationDocker, id, publicPort, privatePo
); );
const ip = JSON.parse(Config)[0].Gateway; const ip = JSON.parse(Config)[0].Gateway;
return await asyncExecShell( 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) { } catch (error) {

View File

@ -10,7 +10,7 @@ export default async function ({
buildId, buildId,
privateSshKey privateSshKey
}): Promise<any> { }): Promise<any> {
const url = htmlUrl.replace('https://', '').replace('http://', ''); const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId }); await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId });
await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`); await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`);
await asyncExecShell(`chmod 600 ${repodir}/id.rsa`); await asyncExecShell(`chmod 600 ${repodir}/id.rsa`);

View File

@ -3,7 +3,9 @@ import { checkContainer, reloadHaproxy } from '$lib/haproxy';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { dev } from '$app/env'; import { dev } from '$app/env';
import cuid from 'cuid'; import cuid from 'cuid';
import fs from 'fs/promises';
import getPort, { portNumbers } from 'get-port'; import getPort, { portNumbers } from 'get-port';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
export async function letsEncrypt(domain, id = null, isCoolify = false) { export async function letsEncrypt(domain, id = null, isCoolify = false) {
try { try {
@ -160,7 +162,7 @@ export async function generateSSLCerts() {
type, type,
destinationDocker: { engine } destinationDocker: { engine }
} = service; } = service;
const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type); const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) { if (found) {
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
@ -181,12 +183,41 @@ export async function generateSSLCerts() {
if (isHttps) ssls.push({ domain, id: 'coolify', isCoolify: true }); if (isHttps) ssls.push({ domain, id: 'coolify', isCoolify: true });
} }
if (ssls.length > 0) { 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) { for (const ssl of ssls) {
if (!dev) { if (!dev) {
console.log('Checking SSL for', ssl.domain); if (
await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify); certificates.includes(ssl.domain) ||
certificates.includes(ssl.domain.replace('www.', ''))
) {
console.log(`Certificate for ${ssl.domain} already exists`);
} else { } else {
console.log('Checking SSL for', ssl.domain); console.log('Generating SSL for', ssl.domain);
await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
}
} else {
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 setDefaultConfiguration
} from '$lib/buildPacks/common'; } from '$lib/buildPacks/common';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import type { ComposeFile } from '$lib/types/composeFile';
export default async function (job) { export default async function (job) {
/*
Edge cases:
1 - Change build pack and redeploy, what should happen?
*/
let { let {
id: applicationId, id: applicationId,
repository, repository,
@ -274,7 +271,7 @@ export default async function (job) {
} }
}; };
}); });
const compose = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[imageId]: { [imageId]: {
@ -283,7 +280,7 @@ export default async function (job) {
volumes, volumes,
env_file: envFound ? [`${workdir}/.env`] : [], env_file: envFound ? [`${workdir}/.env`] : [],
networks: [docker.network], networks: [docker.network],
labels: labels, labels,
depends_on: [], depends_on: [],
restart: 'always' restart: 'always'
} }
@ -295,7 +292,7 @@ export default async function (job) {
}, },
volumes: Object.assign({}, ...composeVolumes) 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( await asyncExecShell(
`DOCKER_HOST=${host} docker compose --project-directory ${workdir} up -d` `DOCKER_HOST=${host} docker compose --project-directory ${workdir} up -d`
); );

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@
<script lang="ts"> <script lang="ts">
import type Prisma from '@prisma/client'; import type Prisma from '@prisma/client';
import { page } from '$app/stores'; import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { post } from '$lib/api'; import { post } from '$lib/api';
@ -46,6 +46,17 @@
(source.type === 'github' && source.githubAppId && source.githubApp.installationId) || (source.type === 'github' && source.githubAppId && source.githubApp.installationId) ||
(source.type === 'gitlab' && source.gitlabAppId) (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) { async function handleSubmit(gitSourceId) {
try { try {
await post(`/applications/${id}/configuration/source.json`, { gitSourceId }); await post(`/applications/${id}/configuration/source.json`, { gitSourceId });
@ -54,17 +65,21 @@
return errorNotification(error); return errorNotification(error);
} }
} }
async function newSource() {
const { id } = await post('/sources/new', {});
return await goto(`/sources/${id}`, { replaceState: true });
}
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Git Source</div> <div class="mr-4 text-2xl tracking-tight">Select a Git Source</div>
</div> </div>
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
{#if !filteredSources || filteredSources.length === 0} {#if !filteredSources || ownSources.length === 0}
<div class="flex-col"> <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"> <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 <svg
class="w-6" class="w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -78,12 +93,39 @@
d="M12 6v6m0 0v6m0-6h6m-6 0H6" d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg /></svg
> >
</a> </button>
</div> </div>
</div> </div>
{:else} {:else}
<div class="flex flex-wrap justify-center"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each filteredSources as source} {#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"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(source.id)}> <form on:submit|preventDefault={() => handleSubmit(source.id)}>
<button <button

View File

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

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"> <script lang="ts">
export let applications: Array<Application>; export let applications: Array<Application>;
import { session } from '$app/stores'; import { session } from '$app/stores';
import Application from './_Application.svelte';
import { post } from '$lib/api'; import { post } from '$lib/api';
import { goto } from '$app/navigation'; 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() { async function newApplication() {
const { id } = await post('/applications/new', {}); const { id } = await post('/applications/new', {});
return await goto(`/applications/${id}`, { replaceState: true }); 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> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
@ -30,14 +57,125 @@
</div> </div>
{/if} {/if}
</div> </div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-col flex-wrap justify-center">
{#if !applications || applications.length === 0} {#if !applications || ownApplications.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">No applications found</div> <div class="text-center text-xl font-bold">No applications found</div>
</div> </div>
{:else} {/if}
{#each applications as application} {#if ownApplications.length > 0 || otherApplications.length > 0}
<Application {application} /> <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} {/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} {/if}
</div> </div>

View File

@ -9,23 +9,28 @@ export const get: RequestHandler = async (event) => {
try { try {
const applicationsCount = await db.prisma.application.count({ 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({ 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({ 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 teamsCount = await db.prisma.permission.count({ where: { userId } });
const databasesCount = await db.prisma.database.count({ 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({ 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 { return {
body: { body: {
teams,
applicationsCount, applicationsCount,
sourcesCount, sourcesCount,
destinationsCount, destinationsCount,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,8 @@
database: stuff.database, database: stuff.database,
versions: stuff.versions, versions: stuff.versions,
privatePort: stuff.privatePort, privatePort: stuff.privatePort,
settings: stuff.settings settings: stuff.settings,
isRunning: stuff.isRunning
} }
}; };
} }
@ -35,6 +36,7 @@
export let database; export let database;
export let settings; export let settings;
export let privatePort; export let privatePort;
export let isRunning;
</script> </script>
<div class="flex items-center space-x-2 p-6 text-2xl font-bold"> <div class="flex items-center space-x-2 p-6 text-2xl font-bold">
@ -47,4 +49,4 @@
<DatabaseLinks {database} /> <DatabaseLinks {database} />
</div> </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 type { RequestHandler } from '@sveltejs/kit';
import { makeLabelForStandaloneDatabase } from '$lib/buildPacks/common'; import { makeLabelForStandaloneDatabase } from '$lib/buildPacks/common';
import { startTcpProxy } from '$lib/haproxy'; import { startTcpProxy } from '$lib/haproxy';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(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 { workdir } = await createDirectories({ repository: type, buildId: id });
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -8,11 +8,22 @@
import Redis from '$lib/components/svg/databases/Redis.svelte'; import Redis from '$lib/components/svg/databases/Redis.svelte';
import { post } from '$lib/api'; import { post } from '$lib/api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { session } from '$app/stores';
async function newDatabase() { async function newDatabase() {
const { id } = await post('/databases/new', {}); const { id } = await post('/databases/new', {});
return await goto(`/databases/${id}`, { replaceState: true }); 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> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
@ -34,15 +45,18 @@
</div> </div>
</div> </div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-col flex-wrap justify-center">
{#if !databases || databases.length === 0} {#if !databases || ownDatabases.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">No databases found</div> <div class="text-center text-xl font-bold">No databases found</div>
</div> </div>
{:else} {/if}
{#each databases as database} {#if ownDatabases.length > 0 || otherDatabases.length > 0}
<a href="/databases/{database.id}" class="no-underline p-2 w-96"> <div class="flex flex-col">
<div class="box-selection relative hover:bg-purple-600 group"> <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'} {#if database.type === 'clickhouse'}
<Clickhouse isAbsolute /> <Clickhouse isAbsolute />
{:else if database.type === 'couchdb'} {:else if database.type === 'couchdb'}
@ -56,11 +70,48 @@
{:else if database.type === 'redis'} {:else if database.type === 'redis'}
<Redis isAbsolute /> <Redis isAbsolute />
{/if} {/if}
<div class="font-bold text-xl text-center truncate"> <div class="truncate text-center text-xl font-bold">
{database.name} {database.name}
</div> </div>
{#if $session.teamId === '0' && otherDatabases.length > 0}
<div class="truncate text-center">{database.teams[0].name}</div>
{/if}
{#if !database.type} {#if !database.type}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white"> <div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>
{/if}
</div>
</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 Configuration missing
</div> </div>
{:else} {:else}
@ -69,5 +120,8 @@
</div> </div>
</a> </a>
{/each} {/each}
</div>
{/if}
</div>
{/if} {/if}
</div> </div>

View File

@ -12,8 +12,8 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
const { id } = $page.params; const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock'; let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
// let scannedApps = [];
let loading = false; let loading = false;
let loadingProxy = false;
let restarting = false; let restarting = false;
async function handleSubmit() { async function handleSubmit() {
loading = true; loading = true;
@ -25,12 +25,6 @@
loading = false; loading = false;
} }
} }
// async function scanApps() {
// scannedApps = [];
// const data = await fetch(`/destinations/${id}/scan.json`);
// const { containers } = await data.json();
// scannedApps = containers;
// }
onMount(async () => { onMount(async () => {
if (state === false && destination.isCoolifyProxyUsed === true) { if (state === false && destination.isCoolifyProxyUsed === true) {
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed; destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
@ -71,6 +65,7 @@
} }
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed; destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
try { try {
loadingProxy = true;
await post(`/destinations/${id}/settings.json`, { await post(`/destinations/${id}/settings.json`, {
isCoolifyProxyUsed: destination.isCoolifyProxyUsed, isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
engine: destination.engine engine: destination.engine
@ -82,6 +77,8 @@
} }
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} finally {
loadingProxy = false;
} }
} }
} }
@ -184,8 +181,10 @@
value={destination.network} value={destination.network}
/> />
</div> </div>
{#if $session.teamId === '0'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
loading={loadingProxy}
disabled={cannotDisable} disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed} bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting} on:click={changeProxySetting}
@ -197,28 +196,5 @@
}`} }`}
/> />
</div> </div>
{/if}
</form> </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}
</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} -->

View File

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

View File

@ -24,6 +24,16 @@
import { session } from '$app/stores'; import { session } from '$app/stores';
export let destinations: Prisma.DestinationDocker[]; 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> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
@ -47,20 +57,43 @@
{/if} {/if}
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center">
{#if !destinations || destinations.length === 0} {#if !destinations || ownDestinations.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">No destination found</div> <div class="text-center text-xl font-bold">No destination found</div>
</div> </div>
{:else} {/if}
<div class="flex flex-wrap justify-center"> {#if ownDestinations.length > 0 || otherDestinations.length > 0}
{#each destinations as destination} <div class="flex flex-col">
<a href="/destinations/{destination.id}" class="no-underline p-2 w-96"> <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="box-selection hover:bg-sky-600">
<div class="font-bold text-xl text-center truncate">{destination.name}</div> <div class="truncate text-center text-xl font-bold">{destination.name}</div>
<div class="text-center truncate">{destination.network}</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> </div>
</a> </a>
{/each} {/each}
</div> </div>
{/if} {/if}
</div>
{/if}
</div> </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"> <script context="module" lang="ts">
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params }) => { 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); const res = await fetch(url);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
if (!data.permissions || Object.entries(data.permissions).length === 0) { if (!data.permissions || Object.entries(data.permissions).length === 0) {
return { return {
status: 302, status: 302,
redirect: '/teams' redirect: '/iam'
}; };
} }
return { return {
@ -20,7 +20,7 @@
return { return {
status: 302, status: 302,
redirect: '/teams' redirect: '/iam'
}; };
}; };
</script> </script>

View File

@ -4,14 +4,14 @@ import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { 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 }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
try { try {
const user = await db.prisma.user.findFirst({ 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 } include: { permission: true }
}); });
if (!user) { if (!user) {

View File

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

View File

@ -92,7 +92,7 @@
</a> </a>
<a <a
href="/teams" href="/iam"
sveltekit:prefetch 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" 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"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { session } from '$app/stores';
export let payload; export let payload;
@ -56,6 +57,7 @@
<label for="network" class="text-base font-bold text-stone-100">Network</label> <label for="network" class="text-base font-bold text-stone-100">Network</label>
<input required name="network" placeholder="default: coolify" bind:value={payload.network} /> <input required name="network" placeholder="default: coolify" bind:value={payload.network} />
</div> </div>
{#if $session.teamId === '0'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
bind:setting={payload.isCoolifyProxyUsed} bind:setting={payload.isCoolifyProxyUsed}
@ -64,5 +66,6 @@
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." 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> </div>
{/if}
</form> </form>
</div> </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

@ -2,6 +2,7 @@
export let service; export let service;
export let isRunning; export let isRunning;
export let readOnly; export let readOnly;
export let settings;
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { post } from '$lib/api'; import { post } from '$lib/api';
@ -91,7 +92,22 @@
/> />
</div> </div>
</div> </div>
<div class="grid grid-cols-2 items-center px-10">
<label for="version" class="text-base font-bold text-stone-100">Version / Tag</label>
<a
href={$session.isAdmin
? `/services/${id}/configuration/version?from=/services/${id}`
: ''}
class="no-underline"
>
<input
value={service.version}
id="service"
disabled
class="cursor-pointer hover:bg-coolgray-500"
/></a
>
</div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="destination" class="text-base font-bold text-stone-100">Destination</label> <label for="destination" class="text-base font-bold text-stone-100">Destination</label>
<div> <div>
@ -143,7 +159,7 @@
{:else if service.type === 'vscodeserver'} {:else if service.type === 'vscodeserver'}
<VsCodeServer {service} /> <VsCodeServer {service} />
{:else if service.type === 'wordpress'} {:else if service.type === 'wordpress'}
<Wordpress bind:service {isRunning} {readOnly} /> <Wordpress bind:service {isRunning} {readOnly} {settings} />
{:else if service.type === 'ghost'} {:else if service.type === 'ghost'}
<Ghost bind:service {readOnly} /> <Ghost bind:service {readOnly} />
{:else if service.type === 'meilisearch'} {:else if service.type === 'meilisearch'}
@ -151,17 +167,4 @@
{/if} {/if}
</div> </div>
</form> </form>
<!-- <div class="font-bold flex space-x-1 pb-5">
<div class="text-xl tracking-tight mr-4">Features</div>
</div>
<div class="px-4 sm:px-6 pb-10">
<ul class="mt-2 divide-y divide-stone-800">
<Setting
bind:setting={isPublic}
on:click={() => changeSettings('isPublic')}
title="Set it public"
description="Your database will be reachable over the internet. <br>Take security seriously in this case!"
/>
</ul>
</div> -->
</div> </div>

View File

@ -1,9 +1,58 @@
<script lang="ts"> <script lang="ts">
import { post } from '$lib/api';
import { page } from '$app/stores';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Setting from '$lib/components/Setting.svelte';
import { errorNotification } from '$lib/form';
import { browser } from '$app/env';
import { getDomain } from '$lib/components/common';
export let service; export let service;
export let isRunning; export let isRunning;
export let readOnly; export let readOnly;
export let settings;
const { id } = $page.params;
let ftpUrl = generateUrl(service.wordpress.ftpPublicPort);
let ftpUser = service.wordpress.ftpUser;
let ftpPassword = service.wordpress.ftpPassword;
let ftpLoading = false;
function generateUrl(publicPort) {
return browser
? `sftp://${
settings.fqdn ? getDomain(settings.fqdn) : window.location.hostname
}:${publicPort}`
: 'Loading...';
}
async function changeSettings(name) {
if (ftpLoading) return;
if (isRunning) {
ftpLoading = true;
let ftpEnabled = service.wordpress.ftpEnabled;
if (name === 'ftpEnabled') {
ftpEnabled = !ftpEnabled;
}
try {
const {
publicPort,
ftpUser: user,
ftpPassword: password
} = await post(`/services/${id}/wordpress/settings.json`, {
ftpEnabled
});
ftpUrl = generateUrl(publicPort);
ftpUser = user;
ftpPassword = password;
service.wordpress.ftpEnabled = ftpEnabled;
} catch ({ error }) {
return errorNotification(error);
} finally {
ftpLoading = false;
}
}
}
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
@ -28,6 +77,30 @@ define('SUBDOMAIN_INSTALL', false);`
: 'N/A'}>{service.wordpress.extraConfig}</textarea : 'N/A'}>{service.wordpress.extraConfig}</textarea
> >
</div> </div>
<div class="grid grid-cols-2 items-center px-10">
<Setting
bind:setting={service.wordpress.ftpEnabled}
loading={ftpLoading}
disabled={!isRunning}
on:click={() => changeSettings('ftpEnabled')}
title="Enable sFTP connection to WordPress data"
description="Enables an on-demand sFTP connection to the WordPress data directory. This is useful if you want to use sFTP to upload files."
/>
</div>
{#if service.wordpress.ftpEnabled}
<div class="grid grid-cols-2 items-center px-10">
<label for="ftpUrl">sFTP Connection URI</label>
<CopyPasswordField id="ftpUrl" readonly disabled name="ftpUrl" value={ftpUrl} />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="ftpUser">User</label>
<CopyPasswordField id="ftpUser" readonly disabled name="ftpUser" value={ftpUser} />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="ftpPassword">Password</label>
<CopyPasswordField id="ftpPassword" readonly disabled name="ftpPassword" value={ftpPassword} />
</div>
{/if}
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MySQL</div> <div class="title">MySQL</div>
</div> </div>

View File

@ -16,7 +16,7 @@
const endpoint = `/services/${params.id}.json`; const endpoint = `/services/${params.id}.json`;
const res = await fetch(endpoint); const res = await fetch(endpoint);
if (res.ok) { if (res.ok) {
const { service, isRunning } = await res.json(); const { service, isRunning, settings } = await res.json();
if (!service || Object.entries(service).length === 0) { if (!service || Object.entries(service).length === 0) {
return { return {
status: 302, status: 302,
@ -45,7 +45,8 @@
stuff: { stuff: {
service, service,
isRunning, isRunning,
readOnly readOnly,
settings
} }
}; };
} }

View File

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

View File

@ -1,6 +1,7 @@
import { getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler, supportedServiceTypesAndVersions } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
@ -14,6 +15,7 @@ export const get: RequestHandler = async (event) => {
return { return {
status: 200, status: 200,
body: { body: {
type,
versions: supportedServiceTypesAndVersions.find((name) => name.name === type).versions versions: supportedServiceTypesAndVersions.find((name) => name.name === type).versions
} }
}; };

View File

@ -31,11 +31,16 @@
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { post } from '$lib/api'; import { post } from '$lib/api';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
export let versions; export let versions;
export let type;
let recommendedVersion = supportedServiceTypesAndVersions.find(
({ name }) => name === type
)?.recommendedVersion;
async function handleSubmit(version) { async function handleSubmit(version) {
try { try {
await post(`/services/${id}/configuration/version.json`, { version }); await post(`/services/${id}/configuration/version.json`, { version });
@ -49,13 +54,26 @@
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Service version</div> <div class="mr-4 text-2xl tracking-tight">Select a Service version</div>
</div> </div>
{#if from}
<div class="pb-10 text-center">
Warning: you are about to change the version of this service.<br />This could cause problem
after you restart the service,
<span class="font-bold text-pink-600">like losing your data, incompatibility issues, etc</span
>.<br />Only do if you know what you are doing.
</div>
{/if}
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each versions as version} {#each versions as version}
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(version)}> <form on:submit|preventDefault={() => handleSubmit(version)}>
<button type="submit" class="box-selection text-xl font-bold hover:bg-pink-600" <button
>{version}</button type="submit"
class:bg-pink-500={recommendedVersion === version}
class="box-selection relative flex text-xl font-bold hover:bg-pink-600"
>{version}
{#if recommendedVersion === version}
<span class="absolute bottom-0 pb-2 text-xs">recommended</span>
{/if}</button
> >
</form> </form>
</div> </div>

View File

@ -11,6 +11,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -75,7 +76,7 @@ export const post: RequestHandler = async (event) => {
config.ghost.environmentVariables[secret.name] = secret.value; config.ghost.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -17,7 +17,7 @@ export const get: RequestHandler = async (event) => {
const { id } = event.params; const { id } = event.params;
try { try {
const service = await db.getService({ id, teamId }); const service = await db.getService({ id, teamId });
const { destinationDockerId, destinationDocker, type, version } = service; const { destinationDockerId, destinationDocker, type, version, settings } = service;
let isRunning = false; let isRunning = false;
if (destinationDockerId) { if (destinationDockerId) {
@ -46,7 +46,8 @@ export const get: RequestHandler = async (event) => {
return { return {
body: { body: {
isRunning, isRunning,
service service,
settings
} }
}; };
} catch (error) { } catch (error) {

View File

@ -6,7 +6,8 @@
props: { props: {
service: stuff.service, service: stuff.service,
isRunning: stuff.isRunning, isRunning: stuff.isRunning,
readOnly: stuff.readOnly readOnly: stuff.readOnly,
settings: stuff.settings
} }
}; };
} }
@ -37,6 +38,7 @@
export let service; export let service;
export let isRunning; export let isRunning;
export let readOnly; export let readOnly;
export let settings;
if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) { if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) {
service.fqdn = `http://${cuid()}.demo.coolify.io`; service.fqdn = `http://${cuid()}.demo.coolify.io`;
@ -76,4 +78,4 @@
<ServiceLinks {service} /> <ServiceLinks {service} />
</div> </div>
<Services bind:service {isRunning} {readOnly} /> <Services bind:service {isRunning} {readOnly} {settings} />

View File

@ -13,7 +13,7 @@ export const post: RequestHandler = async (event) => {
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
try { try {
await db.updateMeiliSearchService({ id, fqdn, name }); await db.updateService({ id, fqdn, name });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -32,7 +33,7 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; config.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -13,7 +13,7 @@ export const post: RequestHandler = async (event) => {
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
try { try {
await db.updateLanguageToolService({ id, fqdn, name }); await db.updateService({ id, fqdn, name });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -37,7 +38,7 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; config.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -8,6 +8,7 @@ import getPort, { portNumbers } from 'get-port';
import { getDomain } from '$lib/components/common'; import { getDomain } from '$lib/components/common';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -55,7 +56,7 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; config.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -33,7 +34,7 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; config.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -30,7 +31,7 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; config.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -120,7 +121,7 @@ COPY ./init.query /docker-entrypoint-initdb.d/init.query
COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`; COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile);
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -31,7 +32,7 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; config.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -12,7 +12,7 @@ export const post: RequestHandler = async (event) => {
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
try { try {
await db.updateVaultWardenService({ id, fqdn, name }); await db.updateService({ id, fqdn, name });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { getServiceImage, ErrorHandler } from '$lib/database'; import { getServiceImage, ErrorHandler } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -32,7 +33,7 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; config.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -13,7 +13,7 @@ export const post: RequestHandler = async (event) => {
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
try { try {
await db.updateVsCodeServer({ id, fqdn, name }); await db.updateService({ id, fqdn, name });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -41,7 +42,7 @@ export const post: RequestHandler = async (event) => {
config.environmentVariables[secret.name] = secret.value; config.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

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

View File

@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -65,7 +66,7 @@ export const post: RequestHandler = async (event) => {
config.wordpress.environmentVariables[secret.name] = secret.value; config.wordpress.environmentVariables[secret.name] = secret.value;
}); });
} }
const composeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[id]: { [id]: {

View File

@ -12,12 +12,24 @@
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte'; import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
import Ghost from '$lib/components/svg/services/Ghost.svelte'; import Ghost from '$lib/components/svg/services/Ghost.svelte';
import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte'; import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte';
import { session } from '$app/stores';
import { getDomain } from '$lib/components/common';
export let services; export let services;
async function newService() { async function newService() {
const { id } = await post('/services/new', {}); const { id } = await post('/services/new', {});
return await goto(`/services/${id}`, { replaceState: true }); return await goto(`/services/${id}`, { replaceState: true });
} }
const ownServices = services.filter((service) => {
if (service.teams[0].id === $session.teamId) {
return service;
}
});
const otherServices = services.filter((service) => {
if (service.teams[0].id !== $session.teamId) {
return service;
}
});
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
@ -39,15 +51,18 @@
</div> </div>
</div> </div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-col flex-wrap justify-center">
{#if !services || services.length === 0} {#if !services || ownServices.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">No services found</div> <div class="text-center text-xl font-bold">No services found</div>
</div> </div>
{:else} {/if}
{#each services as service} {#if ownServices.length > 0 || otherServices.length > 0}
<a href="/services/{service.id}" class="no-underline p-2 w-96"> <div class="flex flex-col">
<div class="box-selection relative hover:bg-pink-600 group"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownServices as service}
<a href="/services/{service.id}" class="w-96 p-2 no-underline">
<div class="box-selection group relative hover:bg-pink-600">
{#if service.type === 'plausibleanalytics'} {#if service.type === 'plausibleanalytics'}
<PlausibleAnalytics isAbsolute /> <PlausibleAnalytics isAbsolute />
{:else if service.type === 'nocodb'} {:else if service.type === 'nocodb'}
@ -71,11 +86,64 @@
{:else if service.type === 'meilisearch'} {:else if service.type === 'meilisearch'}
<MeiliSearch isAbsolute /> <MeiliSearch isAbsolute />
{/if} {/if}
<div class="font-bold text-xl text-center truncate"> <div class="truncate text-center text-xl font-bold">
{service.name} {service.name}
</div> </div>
{#if $session.teamId === '0' && otherServices.length > 0}
<div class="truncate text-center">{service.teams[0].name}</div>
{/if}
{#if service.fqdn}
<div class="truncate text-center">{getDomain(service.fqdn) || ''}</div>
{/if}
{#if !service.type || !service.fqdn} {#if !service.type || !service.fqdn}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white"> <div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>
{/if}
</div>
</a>
{/each}
</div>
{#if otherServices.length > 0 && $session.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Services</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherServices as service}
<a href="/services/{service.id}" class="w-96 p-2 no-underline">
<div class="box-selection group relative hover:bg-pink-600">
{#if service.type === 'plausibleanalytics'}
<PlausibleAnalytics isAbsolute />
{:else if service.type === 'nocodb'}
<NocoDb isAbsolute />
{:else if service.type === 'minio'}
<MinIo isAbsolute />
{:else if service.type === 'vscodeserver'}
<VsCodeServer isAbsolute />
{:else if service.type === 'wordpress'}
<Wordpress isAbsolute />
{:else if service.type === 'vaultwarden'}
<VaultWarden isAbsolute />
{:else if service.type === 'languagetool'}
<LanguageTool isAbsolute />
{:else if service.type === 'n8n'}
<N8n isAbsolute />
{:else if service.type === 'uptimekuma'}
<UptimeKuma isAbsolute />
{:else if service.type === 'ghost'}
<Ghost isAbsolute />
{:else if service.type === 'meilisearch'}
<MeiliSearch isAbsolute />
{/if}
<div class="truncate text-center text-xl font-bold">
{service.name}
</div>
{#if $session.teamId === '0'}
<div class="truncate text-center">{service.teams[0].name}</div>
{/if}
{#if service.fqdn}
<div class="truncate text-center">{getDomain(service.fqdn) || ''}</div>
{/if}
{#if !service.type || !service.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing Configuration missing
</div> </div>
{:else} {:else}
@ -84,5 +152,8 @@
</div> </div>
</a> </a>
{/each} {/each}
</div>
{/if}
</div>
{/if} {/if}
</div> </div>

View File

@ -5,9 +5,9 @@ import type { RequestHandler } from '@sveltejs/kit';
import { promises as dns } from 'dns'; import { promises as dns } from 'dns';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
if (teamId !== '0') return { status: 401, body: { message: 'You are not an admin.' } };
try { try {
const settings = await listSettings(); const settings = await listSettings();
return { return {

View File

@ -11,7 +11,12 @@
} }
}; };
} }
if (res.status === 401) {
return {
status: 302,
redirect: '/databases'
};
}
return { return {
status: res.status, status: res.status,
error: new Error(`Could not load ${url}`) error: new Error(`Could not load ${url}`)

View File

@ -3,13 +3,19 @@
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { post } from '$lib/api'; import { post } from '$lib/api';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
const { id } = $page.params; const { id } = $page.params;
let loading = false; let loading = false;
async function handleSubmit() { async function handleSubmit() {
loading = true; loading = true;
try { try {
return await post(`/sources/${id}.json`, { name: source.name }); await post(`/sources/${id}.json`, {
name: source.name,
htmlUrl: source.htmlUrl.replace(/\/$/, ''),
apiUrl: source.apiUrl.replace(/\/$/, '')
});
toast.push('Settings saved.');
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
@ -38,7 +44,18 @@
}, 100); }, 100);
} }
function newGithubApp() { async function newGithubApp() {
loading = true;
try {
await post(`/sources/${id}/github.json`, {
type: 'github',
name: source.name,
htmlUrl: source.htmlUrl.replace(/\/$/, ''),
apiUrl: source.apiUrl.replace(/\/$/, '')
});
} catch ({ error }) {
return errorNotification(error);
}
const left = screen.width / 2 - 1020 / 2; const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2; const top = screen.height / 2 - 618 / 2;
const newWindow = open( const newWindow = open(
@ -59,9 +76,35 @@
} }
</script> </script>
{#if !source.githubAppId} <div class="mx-auto max-w-4xl px-6">
<button on:click={newGithubApp}>Create new GitHub App</button> {#if !source.githubAppId}
{:else if source.githubApp?.installationId} <form on:submit|preventDefault={newGithubApp} class="py-4">
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">General</div>
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-flow-row gap-2">
<div class="mt-2 grid grid-cols-2 items-center">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<input name="name" id="name" required bind:value={source.name} />
</div>
</div>
<div class="grid grid-cols-2 items-center">
<label for="htmlUrl" class="text-base font-bold text-stone-100">HTML URL</label>
<input name="htmlUrl" id="htmlUrl" required bind:value={source.htmlUrl} />
</div>
<div class="grid grid-cols-2 items-center">
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<input name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} />
</div>
</div>
{#if source.apiUrl && source.htmlUrl && source.name}
<div class="text-center">
<button class=" mt-8 bg-orange-600" type="submit">Create new GitHub App</button>
</div>
{/if}
</form>
{:else if source.githubAppId}
<form on:submit|preventDefault={handleSubmit} class="py-4"> <form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold"> <div class="flex space-x-1 pb-5 font-bold">
<div class="title">General</div> <div class="title">General</div>
@ -78,12 +121,27 @@
{/if} {/if}
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-2 items-center mt-2"> <div class="grid grid-flow-row gap-2">
<div class="mt-2 grid grid-cols-2 items-center">
<label for="name" class="text-base font-bold text-stone-100">Name</label> <label for="name" class="text-base font-bold text-stone-100">Name</label>
<input name="name" id="name" required bind:value={source.name} /> <input name="name" id="name" required bind:value={source.name} />
</div> </div>
</div> </div>
<div class="grid grid-cols-2 items-center">
<label for="htmlUrl" class="text-base font-bold text-stone-100">HTML URL</label>
<input name="htmlUrl" id="htmlUrl" required bind:value={source.htmlUrl} />
</div>
<div class="grid grid-cols-2 items-center">
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<input name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} />
</div>
</div>
</form> </form>
{:else} {:else}
<button on:click={() => installRepositories(source)}>Install Repositories</button> <div class="text-center">
{/if} <button class=" bg-orange-600 mt-8" on:click={() => installRepositories(source)}
>Install Repositories</button
>
</div>
{/if}
</div>

View File

@ -1,35 +1,73 @@
<script lang="ts"> <script lang="ts">
export let source; export let source;
export let settings;
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import { enhance, errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { post } from '$lib/api'; import { post } from '$lib/api';
import { browser } from '$app/env'; import { browser } from '$app/env';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { toast } from '@zerodevx/svelte-toast';
const { id } = $page.params; const { id } = $page.params;
let url = browser ? (settings.fqdn ? settings.fqdn : window.location.origin) : '';
let loading = false; let loading = false;
let oauthIdEl; let oauthIdEl;
let payload = { let applicationType;
oauthId: undefined, if (!source.gitlabAppId) {
groupName: undefined, source.gitlabApp = {
appId: undefined, oauthId: null,
appSecret: undefined, groupName: null,
applicationType: 'user' appId: null,
appSecret: null
}; };
}
onMount(() => { onMount(() => {
oauthIdEl && oauthIdEl.focus(); oauthIdEl && oauthIdEl.focus();
}); });
async function handleSubmitSave() {
async function handleSubmit() {
if (loading) return;
loading = true; loading = true;
if (!source.gitlabAppId) {
// New GitLab App
try { try {
return await post(`/sources/${id}.json`, { name: source.name }); await post(`/sources/${id}/gitlab.json`, {
type: 'gitlab',
name: source.name,
htmlUrl: source.htmlUrl.replace(/\/$/, ''),
apiUrl: source.apiUrl.replace(/\/$/, ''),
oauthId: source.gitlabApp.oauthId,
appId: source.gitlabApp.appId,
appSecret: source.gitlabApp.appSecret,
groupName: source.gitlabApp.groupName
});
return window.location.reload();
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading = false; loading = false;
} }
} else {
// Update GitLab App
try {
await post(`/sources/${id}.json`, {
name: source.name,
htmlUrl: source.htmlUrl.replace(/\/$/, ''),
apiUrl: source.apiUrl.replace(/\/$/, '')
});
} catch ({ error }) {
return errorNotification(error);
} finally {
toast.push('Settings saved.');
loading = false;
} }
}
}
async function changeSettings() { async function changeSettings() {
const { const {
htmlUrl, htmlUrl,
@ -53,23 +91,27 @@
}, 100); }, 100);
} }
async function checkOauthId() { async function checkOauthId() {
if (payload.oauthId) { if (source.gitlabApp?.oauthId) {
try { try {
await post(`/sources/${id}/check.json`, { oauthId: payload.oauthId }); await post(`/sources/${id}/check.json`, {
oauthId: source.gitlabApp?.oauthId
});
} catch ({ error }) { } catch ({ error }) {
payload.oauthId = null; source.gitlabApp.oauthId = null;
oauthIdEl.focus(); oauthIdEl.focus();
return errorNotification(error); return errorNotification(error);
} }
} }
} }
function newApp() { function newApp() {
switch (payload.applicationType) { switch (applicationType) {
case 'user': case 'user':
window.open(`${source.htmlUrl}/-/profile/applications`); window.open(`${source.htmlUrl}/-/profile/applications`);
break; break;
case 'group': case 'group':
window.open(`${source.htmlUrl}/groups/${payload.groupName}/-/settings/applications`); window.open(
`${source.htmlUrl}/groups/${source.gitlabApp.groupName}/-/settings/applications`
);
break; break;
case 'instance': case 'instance':
break; break;
@ -77,110 +119,10 @@
break; break;
} }
} }
async function handleSubmit() {
loading = true;
try {
await post(`/sources/${id}/gitlab.json`, { ...payload });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
} finally {
loading = false;
}
}
</script> </script>
{#if !source.gitlabApp?.appId} <div class="mx-auto max-w-4xl px-6">
<form class="grid grid-flow-row gap-2 py-4" on:submit|preventDefault={newApp}> <form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="grid grid-cols-2 items-center">
<label for="type">GitLab Application Type</label>
<select name="type" id="type" class="w-96" bind:value={payload.applicationType}>
<option value="user">User owned application</option>
<option value="group">Group owned application</option>
{#if source.htmlUrl !== 'https://gitlab.com'}
<option value="instance">Instance-wide application (self-hosted)</option>
{/if}
</select>
</div>
{#if payload.applicationType === 'group'}
<div class="grid grid-cols-2 items-center">
<label for="groupName">Group Name</label>
<input name="groupName" id="groupName" required bind:value={payload.groupName} />
</div>
{/if}
<div class="w-full pt-10 text-center">
<button class="w-96 bg-orange-600 hover:bg-orange-500" type="submit"
>Register new OAuth application on GitLab</button
>
</div>
<Explainer
customClass="w-full"
text="<span class='font-bold text-base text-white'>Scopes required:</span>
<br>- <span class='text-orange-500 font-bold'>api</span> (Access the authenticated user's API)
<br>- <span class='text-orange-500 font-bold'>read_repository</span> (Allows read-only access to the repository)
<br>- <span class='text-orange-500 font-bold'>email</span> (Allows read-only access to the user's primary email address using OpenID Connect)
<br>
<br>For extra security, you can set Expire access tokens!
<br><br>Webhook URL: <span class='text-orange-500 font-bold'>{browser
? window.location.origin
: ''}/webhooks/gitlab</span>
<br>But if you will set a custom domain name for Coolify, use that instead."
/>
</form>
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4 pt-10">
<div class="flex h-8 items-center space-x-2">
<div class="text-xl font-bold text-white">Configuration</div>
<button
type="submit"
class:bg-orange-600={!loading}
class:hover:bg-orange-500={!loading}
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
>
</div>
<div class="grid grid-cols-2 items-start">
<div class="flex-col">
<label for="oauthId" class="pt-2">OAuth ID</label>
<Explainer
text="The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class='font-bold text-orange-600' >in the URL</span> of your GitLab OAuth Application."
/>
</div>
<input
on:change={checkOauthId}
bind:this={oauthIdEl}
name="oauthId"
id="oauthId"
type="number"
required
bind:value={payload.oauthId}
/>
</div>
{#if payload.applicationType === 'group'}
<div class="grid grid-cols-2 items-center">
<label for="groupName">Group Name</label>
<input name="groupName" id="groupName" required bind:value={payload.groupName} />
</div>
{/if}
<div class="grid grid-cols-2 items-center">
<label for="appId">Application ID</label>
<input name="appId" id="appId" required bind:value={payload.appId} />
</div>
<div class="grid grid-cols-2 items-center">
<label for="appSecret">Secret</label>
<input
name="appSecret"
id="appSecret"
type="password"
required
bind:value={payload.appSecret}
/>
</div>
</form>
{:else}
<div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmitSave} class="py-4">
<div class="flex space-x-1 pb-5 font-bold"> <div class="flex space-x-1 pb-5 font-bold">
<div class="title">General</div> <div class="title">General</div>
{#if $session.isAdmin} {#if $session.isAdmin}
@ -190,15 +132,123 @@
class:hover:bg-orange-500={!loading} class:hover:bg-orange-500={!loading}
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
> >
{#if source.gitlabAppId}
<button on:click|preventDefault={changeSettings}>Change GitLab App Settings</button> <button on:click|preventDefault={changeSettings}>Change GitLab App Settings</button>
{/if} {/if}
{/if}
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
{#if !source.gitlabAppId}
<div class="grid grid-cols-2 items-center">
<label for="type" class="text-base font-bold text-stone-100"
>GitLab Application Type</label
>
<select name="type" id="type" class="w-96" bind:value={applicationType}>
<option value="user">User owned application</option>
<option value="group">Group owned application</option>
{#if source.htmlUrl !== 'https://gitlab.com'}
<option value="instance">Instance-wide application (self-hosted)</option>
{/if}
</select>
</div>
{#if applicationType === 'group'}
<div class="grid grid-cols-2 items-center">
<label for="groupName" class="text-base font-bold text-stone-100">Group Name</label>
<input
name="groupName"
id="groupName"
required
bind:value={source.gitlabApp.groupName}
/>
</div>
{/if}
{/if}
<div class="grid grid-flow-row gap-2">
<div class="mt-2 grid grid-cols-2 items-center"> <div class="mt-2 grid grid-cols-2 items-center">
<label for="name" class="text-base font-bold text-stone-100">Name</label> <label for="name" class="text-base font-bold text-stone-100">Name</label>
<input name="name" id="name" required bind:value={source.name} /> <input name="name" id="name" required bind:value={source.name} />
</div> </div>
</div> </div>
</form> {#if source.gitlabApp.groupName}
<div class="grid grid-cols-2 items-center">
<label for="groupName" class="text-base font-bold text-stone-100">Group Name</label>
<input
name="groupName"
id="groupName"
disabled={source.gitlabAppId}
readonly={source.gitlabAppId}
required
bind:value={source.gitlabApp.groupName}
/>
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2 items-center">
<label for="htmlUrl" class="text-base font-bold text-stone-100">HTML URL</label>
<input name="htmlUrl" id="htmlUrl" required bind:value={source.htmlUrl} />
</div>
<div class="grid grid-cols-2 items-center">
<label for="apiUrl" class="text-base font-bold text-stone-100">API URL</label>
<input name="apiUrl" id="apiUrl" required bind:value={source.apiUrl} />
</div>
<div class="grid grid-cols-2 items-start">
<div class="flex-col">
<label for="oauthId" class="pt-2 text-base font-bold text-stone-100">OAuth ID</label>
{#if !source.gitlabAppId}
<Explainer
text="The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class='font-bold text-orange-600' >in the URL</span> of your GitLab OAuth Application."
/>
{/if}
</div>
<input
disabled={source.gitlabAppId}
readonly={source.gitlabAppId}
on:change={checkOauthId}
bind:this={oauthIdEl}
name="oauthId"
id="oauthId"
type="number"
required
bind:value={source.gitlabApp.oauthId}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="appId" class="text-base font-bold text-stone-100">Application ID</label>
<input
name="appId"
id="appId"
disabled={source.gitlabAppId}
readonly={source.gitlabAppId}
required
bind:value={source.gitlabApp.appId}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="appSecret" class="text-base font-bold text-stone-100">Secret</label>
<CopyPasswordField
disabled={source.gitlabAppId}
readonly={source.gitlabAppId}
isPasswordField={true}
name="appSecret"
id="appSecret"
required
bind:value={source.gitlabApp.appSecret}
/>
</div>
</div>
</form>
{#if !source.gitlabAppId}
<Explainer
customClass="w-full"
text="<span class='font-bold text-base text-white'>Scopes required:</span>
<br>- <span class='text-orange-500 font-bold'>api</span> (Access the authenticated user's API)
<br>- <span class='text-orange-500 font-bold'>read_repository</span> (Allows read-only access to the repository)
<br>- <span class='text-orange-500 font-bold'>email</span> (Allows read-only access to the user's primary email address using OpenID Connect)
<br>
<br>For extra security, you can set <span class='text-orange-500 font-bold'>Expire Access Tokens</span>
<br><br>Webhook URL: <span class='text-orange-500 font-bold'>{url}/webhooks/gitlab</span>"
/>
{/if}
</div>

View File

@ -0,0 +1,18 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
let { type, name, htmlUrl, apiUrl } = await event.request.json();
await db.addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -9,11 +9,23 @@ export const post: RequestHandler = async (event) => {
const { id } = event.params; const { id } = event.params;
try { try {
let { oauthId, groupName, appId, appSecret } = await event.request.json(); let { type, name, htmlUrl, apiUrl, oauthId, appId, appSecret, groupName } =
await event.request.json();
oauthId = Number(oauthId); oauthId = Number(oauthId);
await db.addSource({ id, teamId, oauthId, groupName, appId, appSecret }); await db.addGitLabSource({
id,
teamId,
type,
name,
htmlUrl,
apiUrl,
oauthId,
appId,
appSecret,
groupName
});
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@ -43,10 +43,10 @@ export const post: RequestHandler = async (event) => {
const { id } = event.params; const { id } = event.params;
const { name } = await event.request.json(); const { name, htmlUrl, apiUrl } = await event.request.json();
try { try {
await db.updateGitsource({ id, name }); await db.updateGitsource({ id, name, htmlUrl, apiUrl });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@ -29,9 +29,41 @@
<script lang="ts"> <script lang="ts">
export let source: Prisma.GitSource; export let source: Prisma.GitSource;
export let settings;
import type Prisma from '@prisma/client'; import type Prisma from '@prisma/client';
import Github from './_Github.svelte'; import Github from './_Github.svelte';
import Gitlab from './_Gitlab.svelte'; import Gitlab from './_Gitlab.svelte';
function setPredefined(type) {
switch (type) {
case 'github':
source.name = 'Github.com';
source.type = 'github';
source.htmlUrl = 'https://github.com';
source.apiUrl = 'https://api.github.com';
source.organization = undefined;
break;
case 'gitlab':
source.name = 'Gitlab.com';
source.type = 'gitlab';
source.htmlUrl = 'https://gitlab.com';
source.apiUrl = 'https://gitlab.com/api';
source.organization = undefined;
break;
case 'bitbucket':
source.name = 'Bitbucket.com';
source.type = 'bitbucket';
source.htmlUrl = 'https://bitbucket.com';
source.apiUrl = 'https://api.bitbucket.org';
source.organization = undefined;
break;
default:
break;
}
}
</script> </script>
<div class="flex space-x-1 p-6 px-6 text-2xl font-bold"> <div class="flex space-x-1 p-6 px-6 text-2xl font-bold">
@ -40,10 +72,21 @@
<span class="pr-2">{source.name}</span> <span class="pr-2">{source.name}</span>
</div> </div>
<div class="flex justify-center px-6 pb-8"> <div class="flex flex-col justify-center">
{#if !source.gitlabAppId && !source.githubAppId}
<div class="flex-col space-y-2 pb-10 text-center">
<div class="text-xl font-bold text-white">Select a provider</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>
</div>
</div>
{/if}
<div>
{#if source.type === 'github'} {#if source.type === 'github'}
<Github bind:source /> <Github bind:source />
{:else if source.type === 'gitlab'} {:else if source.type === 'gitlab'}
<Gitlab bind:source /> <Gitlab bind:source {settings} />
{/if} {/if}
</div>
</div> </div>

View File

@ -36,6 +36,7 @@
export let settings; export let settings;
onMount(() => { onMount(() => {
const { organization, id, htmlUrl } = source; const { organization, id, htmlUrl } = source;
console.log(source);
const { fqdn } = settings; const { fqdn } = settings;
const host = dev const host = dev
? 'http://localhost:3000' ? 'http://localhost:3000'

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