Merge branch 'main' into fix-coloured-tooltips
This commit is contained in:
commit
97a6f04aaa
@ -2,4 +2,5 @@ COOLIFY_APP_ID=
|
||||
COOLIFY_SECRET_KEY=12341234123412341234123412341234
|
||||
COOLIFY_DATABASE_URL=file:../db/dev.db
|
||||
COOLIFY_SENTRY_DSN=
|
||||
COOLIFY_IS_ON="docker"
|
||||
COOLIFY_IS_ON="docker"
|
||||
COOLIFY_WHITE_LABELED="false"
|
@ -16,7 +16,7 @@ # Recommended Pull Request Guideline
|
||||
- Push to your fork repo
|
||||
- Create a pull request: https://github.com/coollabsio/compare
|
||||
- Write a proper description
|
||||
- Click "Change to draft"
|
||||
- Open the pull request to review
|
||||
|
||||
# How to start after you set up your local fork?
|
||||
|
||||
|
@ -11,7 +11,7 @@ WORKDIR /app
|
||||
|
||||
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 pnpm add -g pnpm
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "coolify",
|
||||
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
|
||||
"version": "2.3.3",
|
||||
"version": "2.4.2",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev",
|
||||
|
@ -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;
|
@ -0,0 +1,5 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "ApplicationPersistentStorage_path_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "ApplicationPersistentStorage_applicationId_key";
|
@ -117,8 +117,8 @@ model ApplicationSettings {
|
||||
model ApplicationPersistentStorage {
|
||||
id String @id @default(cuid())
|
||||
application Application @relation(fields: [applicationId], references: [id])
|
||||
applicationId String @unique
|
||||
path String @unique
|
||||
applicationId String
|
||||
path String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@ -332,6 +332,12 @@ model Wordpress {
|
||||
mysqlRootUserPassword String
|
||||
mysqlDatabase String?
|
||||
mysqlPublicPort Int?
|
||||
ftpEnabled Boolean @default(false)
|
||||
ftpUser String?
|
||||
ftpPassword String?
|
||||
ftpPublicPort Int?
|
||||
ftpHostKey String?
|
||||
ftpHostKeyPrivate String?
|
||||
serviceId String @unique
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
|
6
src/app.d.ts
vendored
6
src/app.d.ts
vendored
@ -15,18 +15,20 @@ declare namespace App {
|
||||
readOnly: boolean;
|
||||
source: string;
|
||||
settings: string;
|
||||
database: Record<string, any>;
|
||||
versions: string;
|
||||
privatePort: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface SessionData {
|
||||
whiteLabeled: boolean;
|
||||
version?: string;
|
||||
userId?: string | null;
|
||||
teamId?: string | null;
|
||||
permission?: string;
|
||||
isAdmin?: boolean;
|
||||
expires?: string | null;
|
||||
gitlabToken?: string | null;
|
||||
ghToken?: string | null;
|
||||
}
|
||||
|
||||
type DateTimeFormatOptions = {
|
||||
|
@ -2,7 +2,6 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Coolify</title>
|
||||
%svelte.head%
|
||||
|
@ -7,6 +7,8 @@ import { version } from '$lib/common';
|
||||
import cookie from 'cookie';
|
||||
import { dev } from '$app/env';
|
||||
|
||||
const whiteLabeled = process.env['COOLIFY_WHITE_LABELED'] === 'true';
|
||||
|
||||
export const handle = handleSession(
|
||||
{
|
||||
secret: process.env['COOLIFY_SECRET_KEY'],
|
||||
@ -71,6 +73,7 @@ export const handle = handleSession(
|
||||
export const getSession: GetSession = function ({ locals }) {
|
||||
return {
|
||||
version,
|
||||
whiteLabeled,
|
||||
...locals.session.data
|
||||
};
|
||||
};
|
||||
|
@ -4,6 +4,12 @@ import { promises as fs } from 'fs';
|
||||
const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
|
||||
const { workdir, baseDirectory } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
let composerFound = false;
|
||||
try {
|
||||
await fs.readFile(`${workdir}${baseDirectory || ''}/composer.json`);
|
||||
composerFound = true;
|
||||
} catch (error) {}
|
||||
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push(`LABEL coolify.image=true`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
@ -11,6 +17,10 @@ const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
|
||||
if (htaccessFound) {
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''}/.htaccess ./`);
|
||||
}
|
||||
if (composerFound) {
|
||||
Dockerfile.push(`RUN composer install`);
|
||||
}
|
||||
|
||||
Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`);
|
||||
Dockerfile.push(`EXPOSE 80`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
|
||||
@ -21,12 +31,14 @@ export default async function (data) {
|
||||
try {
|
||||
let htaccessFound = false;
|
||||
try {
|
||||
const d = await fs.readFile(`${workdir}${baseDirectory || ''}/.htaccess`);
|
||||
await fs.readFile(`${workdir}${baseDirectory || ''}/.htaccess`);
|
||||
htaccessFound = true;
|
||||
} 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 buildImage(data);
|
||||
} catch (error) {
|
||||
|
@ -52,12 +52,14 @@ export const sentry = Sentry;
|
||||
export const uniqueName = () => uniqueNamesGenerator(customConfig);
|
||||
|
||||
export const saveBuildLog = async ({ line, buildId, applicationId }) => {
|
||||
if (line.includes('ghs_')) {
|
||||
const regex = /ghs_.*@/g;
|
||||
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
|
||||
if (line) {
|
||||
if (line.includes('ghs_')) {
|
||||
const regex = /ghs_.*@/g;
|
||||
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
|
||||
}
|
||||
const addTimestamp = `${generateTimestamp()} ${line}`;
|
||||
return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId });
|
||||
}
|
||||
const addTimestamp = `${generateTimestamp()} ${line}`;
|
||||
return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId });
|
||||
};
|
||||
|
||||
export const isTeamIdTokenAvailable = (request) => {
|
||||
@ -100,6 +102,7 @@ export const getUserDetails = async (event, isAdminRequired = true) => {
|
||||
message: 'OK'
|
||||
}
|
||||
};
|
||||
|
||||
if (isAdminRequired && permission !== 'admin' && permission !== 'owner') {
|
||||
payload.status = 401;
|
||||
payload.body.message =
|
||||
|
@ -7,6 +7,7 @@
|
||||
export let isCenter = true;
|
||||
export let disabled = false;
|
||||
export let dataTooltip = null;
|
||||
export let loading = false;
|
||||
</script>
|
||||
|
||||
<div class="flex items-center py-4 pr-8">
|
||||
@ -26,9 +27,10 @@
|
||||
on:click
|
||||
aria-pressed="false"
|
||||
class="relative mx-20 inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out"
|
||||
class:opacity-50={disabled}
|
||||
class:bg-green-600={setting}
|
||||
class:bg-stone-700={!setting}
|
||||
class:opacity-50={disabled || loading}
|
||||
class:bg-green-600={!loading && setting}
|
||||
class:bg-stone-700={!loading && !setting}
|
||||
class:bg-yellow-500={loading}
|
||||
>
|
||||
<span class="sr-only">Use setting</span>
|
||||
<span
|
||||
@ -40,6 +42,7 @@
|
||||
class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
|
||||
class:opacity-0={setting}
|
||||
class:opacity-100={!setting}
|
||||
class:animate-spin={loading}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
|
||||
@ -57,6 +60,7 @@
|
||||
aria-hidden="true"
|
||||
class:opacity-100={setting}
|
||||
class:opacity-0={!setting}
|
||||
class:animate-spin={loading}
|
||||
>
|
||||
<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
|
||||
<path
|
||||
|
@ -43,3 +43,142 @@ export function changeQueryParams(buildId) {
|
||||
queryParams.set('buildId', buildId);
|
||||
return history.pushState(null, null, '?' + queryParams.toString());
|
||||
}
|
||||
|
||||
export const supportedDatabaseTypesAndVersions = [
|
||||
{
|
||||
name: 'mongodb',
|
||||
fancyName: 'MongoDB',
|
||||
baseImage: 'bitnami/mongodb',
|
||||
versions: ['5.0', '4.4', '4.2']
|
||||
},
|
||||
{ name: 'mysql', fancyName: 'MySQL', baseImage: 'bitnami/mysql', versions: ['8.0', '5.7'] },
|
||||
{
|
||||
name: 'postgresql',
|
||||
fancyName: 'PostgreSQL',
|
||||
baseImage: 'bitnami/postgresql',
|
||||
versions: ['14.2.0', '13.6.0', '12.10.0 ', '11.15.0', '10.20.0']
|
||||
},
|
||||
{
|
||||
name: 'redis',
|
||||
fancyName: 'Redis',
|
||||
baseImage: 'bitnami/redis',
|
||||
versions: ['6.2', '6.0', '5.0']
|
||||
},
|
||||
{ name: 'couchdb', fancyName: 'CouchDB', baseImage: 'bitnami/couchdb', versions: ['3.2.1'] }
|
||||
];
|
||||
export const supportedServiceTypesAndVersions = [
|
||||
{
|
||||
name: 'plausibleanalytics',
|
||||
fancyName: 'Plausible Analytics',
|
||||
baseImage: 'plausible/analytics',
|
||||
images: ['bitnami/postgresql:13.2.0', 'yandex/clickhouse-server:21.3.2.5'],
|
||||
versions: ['latest', 'stable'],
|
||||
recommendedVersion: 'stable',
|
||||
ports: {
|
||||
main: 8000
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'nocodb',
|
||||
fancyName: 'NocoDB',
|
||||
baseImage: 'nocodb/nocodb',
|
||||
versions: ['latest'],
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 8080
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'minio',
|
||||
fancyName: 'MinIO',
|
||||
baseImage: 'minio/minio',
|
||||
versions: ['latest'],
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 9001
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'vscodeserver',
|
||||
fancyName: 'VSCode Server',
|
||||
baseImage: 'codercom/code-server',
|
||||
versions: ['latest'],
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 8080
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'wordpress',
|
||||
fancyName: 'Wordpress',
|
||||
baseImage: 'wordpress',
|
||||
images: ['bitnami/mysql:5.7'],
|
||||
versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'],
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 80
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'vaultwarden',
|
||||
fancyName: 'Vaultwarden',
|
||||
baseImage: 'vaultwarden/server',
|
||||
versions: ['latest'],
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 80
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'languagetool',
|
||||
fancyName: 'LanguageTool',
|
||||
baseImage: 'silviof/docker-languagetool',
|
||||
versions: ['latest'],
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 8010
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'n8n',
|
||||
fancyName: 'n8n',
|
||||
baseImage: 'n8nio/n8n',
|
||||
versions: ['latest'],
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 5678
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'uptimekuma',
|
||||
fancyName: 'Uptime Kuma',
|
||||
baseImage: 'louislam/uptime-kuma',
|
||||
versions: ['latest'],
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 3001
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'ghost',
|
||||
fancyName: 'Ghost',
|
||||
baseImage: 'bitnami/ghost',
|
||||
images: ['bitnami/mariadb'],
|
||||
versions: ['latest'],
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 2368
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'meilisearch',
|
||||
fancyName: 'Meilisearch',
|
||||
baseImage: 'getmeili/meilisearch',
|
||||
images: [],
|
||||
versions: ['latest'],
|
||||
recommendedVersion: 'latest',
|
||||
ports: {
|
||||
main: 7700
|
||||
}
|
||||
}
|
||||
];
|
||||
|
@ -5,7 +5,13 @@ import { getDomain, removeDestinationDocker } from '$lib/common';
|
||||
import { prisma } from './common';
|
||||
|
||||
export async function listApplications(teamId) {
|
||||
return await prisma.application.findMany({ where: { teams: { some: { id: teamId } } } });
|
||||
if (teamId === '0') {
|
||||
return await prisma.application.findMany({ include: { teams: true } });
|
||||
}
|
||||
return await prisma.application.findMany({
|
||||
where: { teams: { some: { id: teamId } } },
|
||||
include: { teams: true }
|
||||
});
|
||||
}
|
||||
|
||||
export async function newApplication({ name, teamId }) {
|
||||
@ -67,7 +73,11 @@ export async function removeApplication({ id, teamId }) {
|
||||
await prisma.build.deleteMany({ where: { applicationId: id } });
|
||||
await prisma.secret.deleteMany({ where: { applicationId: id } });
|
||||
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } });
|
||||
await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } });
|
||||
if (teamId === '0') {
|
||||
await prisma.application.deleteMany({ where: { id } });
|
||||
} else {
|
||||
await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getApplicationWebhook({ projectId, branch }) {
|
||||
@ -130,16 +140,30 @@ export async function getApplicationById({ id }) {
|
||||
return { ...body };
|
||||
}
|
||||
export async function getApplication({ id, teamId }) {
|
||||
let body = await prisma.application.findFirst({
|
||||
where: { id, teams: { some: { id: teamId } } },
|
||||
include: {
|
||||
destinationDocker: true,
|
||||
settings: true,
|
||||
gitSource: { include: { githubApp: true, gitlabApp: true } },
|
||||
secrets: true,
|
||||
persistentStorage: true
|
||||
}
|
||||
});
|
||||
let body = {};
|
||||
if (teamId === '0') {
|
||||
body = await prisma.application.findFirst({
|
||||
where: { id },
|
||||
include: {
|
||||
destinationDocker: true,
|
||||
settings: true,
|
||||
gitSource: { include: { githubApp: true, gitlabApp: true } },
|
||||
secrets: true,
|
||||
persistentStorage: true
|
||||
}
|
||||
});
|
||||
} else {
|
||||
body = await prisma.application.findFirst({
|
||||
where: { id, teams: { some: { id: teamId } } },
|
||||
include: {
|
||||
destinationDocker: true,
|
||||
settings: true,
|
||||
gitSource: { include: { githubApp: true, gitlabApp: true } },
|
||||
secrets: true,
|
||||
persistentStorage: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (body?.gitSource?.githubApp?.clientSecret) {
|
||||
body.gitSource.githubApp.clientSecret = decrypt(body.gitSource.githubApp.clientSecret);
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { dev } from '$app/env';
|
||||
import { sentry } from '$lib/common';
|
||||
import {
|
||||
supportedDatabaseTypesAndVersions,
|
||||
supportedServiceTypesAndVersions
|
||||
} from '$lib/components/common';
|
||||
import * as Prisma from '@prisma/client';
|
||||
import { default as ProdPrisma } from '@prisma/client';
|
||||
import type { PrismaClientOptions } from '@prisma/client/runtime';
|
||||
@ -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) {
|
||||
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
|
||||
if (found) {
|
||||
@ -283,6 +159,7 @@ export function generateDatabaseConfiguration(database) {
|
||||
// url: `psql://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 5432}/${defaultDatabase}`,
|
||||
privatePort: 5432,
|
||||
environmentVariables: {
|
||||
POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword,
|
||||
POSTGRESQL_PASSWORD: dbUserPassword,
|
||||
POSTGRESQL_USERNAME: dbUser,
|
||||
POSTGRESQL_DATABASE: defaultDatabase
|
||||
|
@ -7,7 +7,14 @@ import getPort, { portNumbers } from 'get-port';
|
||||
import { asyncExecShell, getEngine, removeContainer } from '$lib/common';
|
||||
|
||||
export async function listDatabases(teamId) {
|
||||
return await prisma.database.findMany({ where: { teams: { some: { id: teamId } } } });
|
||||
if (teamId === '0') {
|
||||
return await prisma.database.findMany({ include: { teams: true } });
|
||||
} else {
|
||||
return await prisma.database.findMany({
|
||||
where: { teams: { some: { id: teamId } } },
|
||||
include: { teams: true }
|
||||
});
|
||||
}
|
||||
}
|
||||
export async function newDatabase({ name, teamId }) {
|
||||
const dbUser = cuid();
|
||||
@ -31,10 +38,18 @@ export async function newDatabase({ name, teamId }) {
|
||||
}
|
||||
|
||||
export async function getDatabase({ id, teamId }) {
|
||||
const body = await prisma.database.findFirst({
|
||||
where: { id, teams: { some: { id: teamId } } },
|
||||
include: { destinationDocker: true, settings: true }
|
||||
});
|
||||
let body = {};
|
||||
if (teamId === '0') {
|
||||
body = await prisma.database.findFirst({
|
||||
where: { id },
|
||||
include: { destinationDocker: true, settings: true }
|
||||
});
|
||||
} else {
|
||||
body = await prisma.database.findFirst({
|
||||
where: { id, teams: { some: { id: teamId } } },
|
||||
include: { destinationDocker: true, settings: true }
|
||||
});
|
||||
}
|
||||
|
||||
if (body.dbUserPassword) body.dbUserPassword = decrypt(body.dbUserPassword);
|
||||
if (body.rootUserPassword) body.rootUserPassword = decrypt(body.rootUserPassword);
|
||||
@ -122,3 +137,43 @@ export async function stopDatabase(database) {
|
||||
}
|
||||
return everStarted;
|
||||
}
|
||||
|
||||
export async function updatePasswordInDb(database, user, newPassword, isRoot) {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
rootUser,
|
||||
rootUserPassword,
|
||||
dbUser,
|
||||
dbUserPassword,
|
||||
defaultDatabase,
|
||||
destinationDockerId,
|
||||
destinationDocker: { engine }
|
||||
} = database;
|
||||
if (destinationDockerId) {
|
||||
const host = getEngine(engine);
|
||||
if (type === 'mysql') {
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"ALTER USER '${user}'@'%' IDENTIFIED WITH caching_sha2_password BY '${newPassword}';\"`
|
||||
);
|
||||
} else if (type === 'postgresql') {
|
||||
if (isRoot) {
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker exec ${id} psql postgresql://postgres:${rootUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role postgres WITH PASSWORD '${newPassword}'"`
|
||||
);
|
||||
} else {
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker exec ${id} psql postgresql://${dbUser}:${dbUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role ${user} WITH PASSWORD '${newPassword}'"`
|
||||
);
|
||||
}
|
||||
} else if (type === 'mongodb') {
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker exec ${id} mongo 'mongodb://${rootUser}:${rootUserPassword}@${id}:27017/admin?readPreference=primary&ssl=false' --eval "db.changeUserPassword('${user}','${newPassword}')"`
|
||||
);
|
||||
} else if (type === 'redis') {
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker exec ${id} redis-cli -u redis://${dbUserPassword}@${id}:6379 --raw CONFIG SET requirepass ${newPassword}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,13 @@ import { getDatabaseImage } from '.';
|
||||
import { prisma } from './common';
|
||||
|
||||
export async function listDestinations(teamId) {
|
||||
return await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId } } } });
|
||||
if (teamId === '0') {
|
||||
return await prisma.destinationDocker.findMany({ include: { teams: true } });
|
||||
}
|
||||
return await prisma.destinationDocker.findMany({
|
||||
where: { teams: { some: { id: teamId } } },
|
||||
include: { teams: true }
|
||||
});
|
||||
}
|
||||
|
||||
export async function configureDestinationForService({ id, destinationId }) {
|
||||
@ -38,9 +44,7 @@ export async function configureDestinationForDatabase({ id, destinationId }) {
|
||||
const host = getEngine(engine);
|
||||
if (type && version) {
|
||||
const baseImage = getDatabaseImage(type);
|
||||
asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker pull ${baseImage}:${version} && echo "FROM ${baseImage}:${version}" | docker build --label coolify.image="true" -t "${baseImage}:${version}" -`
|
||||
);
|
||||
asyncExecShell(`DOCKER_HOST=${host} docker pull ${baseImage}:${version}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -124,12 +128,17 @@ export async function removeDestination({ id }) {
|
||||
}
|
||||
|
||||
export async function getDestination({ id, teamId }) {
|
||||
let destination = await prisma.destinationDocker.findFirst({
|
||||
where: { id, teams: { some: { id: teamId } } }
|
||||
});
|
||||
if (destination.remoteEngine) {
|
||||
destination.sshPrivateKey = decrypt(destination.sshPrivateKey);
|
||||
let destination = {};
|
||||
if (teamId === '0') {
|
||||
destination = await prisma.destinationDocker.findFirst({
|
||||
where: { id }
|
||||
});
|
||||
} else {
|
||||
destination = await prisma.destinationDocker.findFirst({
|
||||
where: { id, teams: { some: { id: teamId } } }
|
||||
});
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
export async function getDestinationByApplicationId({ id, teamId }) {
|
||||
|
@ -2,26 +2,26 @@ import { decrypt, encrypt } from '$lib/crypto';
|
||||
import { prisma } from './common';
|
||||
|
||||
export async function listSources(teamId) {
|
||||
if (teamId === '0') {
|
||||
return await prisma.gitSource.findMany({
|
||||
include: { githubApp: true, gitlabApp: true, teams: true }
|
||||
});
|
||||
}
|
||||
return await prisma.gitSource.findMany({
|
||||
where: { teams: { some: { id: teamId } } },
|
||||
include: { githubApp: true, gitlabApp: true }
|
||||
include: { githubApp: true, gitlabApp: true, teams: true }
|
||||
});
|
||||
}
|
||||
|
||||
export async function newSource({ name, teamId, type, htmlUrl, apiUrl, organization }) {
|
||||
export async function newSource({ teamId, name }) {
|
||||
return await prisma.gitSource.create({
|
||||
data: {
|
||||
teams: { connect: { id: teamId } },
|
||||
name,
|
||||
type,
|
||||
htmlUrl,
|
||||
apiUrl,
|
||||
organization
|
||||
teams: { connect: { id: teamId } }
|
||||
}
|
||||
});
|
||||
}
|
||||
export async function removeSource({ id }) {
|
||||
// TODO: Disconnect application with this sourceId! Maybe not needed?
|
||||
const source = await prisma.gitSource.delete({
|
||||
where: { id },
|
||||
include: { githubApp: true, gitlabApp: true }
|
||||
@ -31,10 +31,18 @@ export async function removeSource({ id }) {
|
||||
}
|
||||
|
||||
export async function getSource({ id, teamId }) {
|
||||
let body = await prisma.gitSource.findFirst({
|
||||
where: { id, teams: { some: { id: teamId } } },
|
||||
include: { githubApp: true, gitlabApp: true }
|
||||
});
|
||||
let body = {};
|
||||
if (teamId === '0') {
|
||||
body = await prisma.gitSource.findFirst({
|
||||
where: { id },
|
||||
include: { githubApp: true, gitlabApp: true }
|
||||
});
|
||||
} else {
|
||||
body = await prisma.gitSource.findFirst({
|
||||
where: { id, teams: { some: { id: teamId } } },
|
||||
include: { githubApp: true, gitlabApp: true }
|
||||
});
|
||||
}
|
||||
if (body?.githubApp?.clientSecret)
|
||||
body.githubApp.clientSecret = decrypt(body.githubApp.clientSecret);
|
||||
if (body?.githubApp?.webhookSecret)
|
||||
@ -43,8 +51,29 @@ export async function getSource({ id, teamId }) {
|
||||
if (body?.gitlabApp?.appSecret) body.gitlabApp.appSecret = decrypt(body.gitlabApp.appSecret);
|
||||
return body;
|
||||
}
|
||||
export async function addSource({ id, appId, teamId, oauthId, groupName, appSecret }) {
|
||||
export async function addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl }) {
|
||||
await prisma.gitSource.update({ where: { id }, data: { type, name, htmlUrl, apiUrl } });
|
||||
return await prisma.githubApp.create({
|
||||
data: {
|
||||
teams: { connect: { id: teamId } },
|
||||
gitSource: { connect: { id } }
|
||||
}
|
||||
});
|
||||
}
|
||||
export async function addGitLabSource({
|
||||
id,
|
||||
teamId,
|
||||
type,
|
||||
name,
|
||||
htmlUrl,
|
||||
apiUrl,
|
||||
oauthId,
|
||||
appId,
|
||||
appSecret,
|
||||
groupName
|
||||
}) {
|
||||
const encrptedAppSecret = encrypt(appSecret);
|
||||
await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name } });
|
||||
return await prisma.gitlabApp.create({
|
||||
data: {
|
||||
teams: { connect: { id: teamId } },
|
||||
@ -63,9 +92,9 @@ export async function configureGitsource({ id, gitSourceId }) {
|
||||
data: { gitSource: { connect: { id: gitSourceId } } }
|
||||
});
|
||||
}
|
||||
export async function updateGitsource({ id, name }) {
|
||||
export async function updateGitsource({ id, name, htmlUrl, apiUrl }) {
|
||||
return await prisma.gitSource.update({
|
||||
where: { id },
|
||||
data: { name }
|
||||
data: { name, htmlUrl, apiUrl }
|
||||
});
|
||||
}
|
||||
|
@ -5,7 +5,14 @@ import { generatePassword } from '.';
|
||||
import { prisma } from './common';
|
||||
|
||||
export async function listServices(teamId) {
|
||||
return await prisma.service.findMany({ where: { teams: { some: { id: teamId } } } });
|
||||
if (teamId === '0') {
|
||||
return await prisma.service.findMany({ include: { teams: true } });
|
||||
} else {
|
||||
return await prisma.service.findMany({
|
||||
where: { teams: { some: { id: teamId } } },
|
||||
include: { teams: true }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function newService({ name, teamId }) {
|
||||
@ -13,19 +20,28 @@ export async function newService({ name, teamId }) {
|
||||
}
|
||||
|
||||
export async function getService({ id, teamId }) {
|
||||
const body = await prisma.service.findFirst({
|
||||
where: { id, teams: { some: { id: teamId } } },
|
||||
include: {
|
||||
destinationDocker: true,
|
||||
plausibleAnalytics: true,
|
||||
minio: true,
|
||||
vscodeserver: true,
|
||||
wordpress: true,
|
||||
ghost: true,
|
||||
serviceSecret: true,
|
||||
meiliSearch: true
|
||||
}
|
||||
});
|
||||
let body = {};
|
||||
const include = {
|
||||
destinationDocker: true,
|
||||
plausibleAnalytics: true,
|
||||
minio: true,
|
||||
vscodeserver: true,
|
||||
wordpress: true,
|
||||
ghost: true,
|
||||
serviceSecret: true,
|
||||
meiliSearch: true
|
||||
};
|
||||
if (teamId === '0') {
|
||||
body = await prisma.service.findFirst({
|
||||
where: { id },
|
||||
include
|
||||
});
|
||||
} else {
|
||||
body = await prisma.service.findFirst({
|
||||
where: { id, teams: { some: { id: teamId } } },
|
||||
include
|
||||
});
|
||||
}
|
||||
|
||||
if (body.plausibleAnalytics?.postgresqlPassword)
|
||||
body.plausibleAnalytics.postgresqlPassword = decrypt(
|
||||
@ -59,8 +75,12 @@ export async function getService({ id, teamId }) {
|
||||
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 }) {
|
||||
@ -145,7 +165,7 @@ export async function configureServiceType({ id, type }) {
|
||||
}
|
||||
});
|
||||
} else if (type === 'ghost') {
|
||||
const defaultEmail = `${cuid()}@coolify.io`;
|
||||
const defaultEmail = `${cuid()}@example.com`;
|
||||
const defaultPassword = encrypt(generatePassword());
|
||||
const mariadbUser = cuid();
|
||||
const mariadbPassword = encrypt(generatePassword());
|
||||
@ -200,18 +220,6 @@ export async function updatePlausibleAnalyticsService({ id, fqdn, email, usernam
|
||||
export async function updateService({ id, fqdn, name }) {
|
||||
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
|
||||
}
|
||||
export async function updateLanguageToolService({ id, fqdn, name }) {
|
||||
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
|
||||
}
|
||||
export async function 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 }) {
|
||||
return await prisma.service.update({
|
||||
where: { id },
|
||||
|
@ -32,26 +32,42 @@ export async function login({ email, password, isLogin }) {
|
||||
if (users === 0) {
|
||||
await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } });
|
||||
// Create default network & start Coolify Proxy
|
||||
asyncExecShell(`docker network create --attachable coolify`)
|
||||
.then(() => {
|
||||
console.log('Network created');
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('Network already exists.');
|
||||
});
|
||||
|
||||
startCoolifyProxy('/var/run/docker.sock')
|
||||
.then(() => {
|
||||
console.log('Coolify Proxy started.');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
await asyncExecShell(`docker network create --attachable coolify`);
|
||||
await startCoolifyProxy('/var/run/docker.sock');
|
||||
uid = '0';
|
||||
}
|
||||
|
||||
if (userFound) {
|
||||
if (userFound.type === 'email') {
|
||||
if (userFound.password === 'RESETME') {
|
||||
const hashedPassword = await hashPassword(password);
|
||||
if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) {
|
||||
await prisma.user.update({
|
||||
where: { email: userFound.email },
|
||||
data: { password: 'RESETTIMEOUT' }
|
||||
});
|
||||
throw {
|
||||
error: 'Password reset link has expired. Please request a new one.'
|
||||
};
|
||||
} else {
|
||||
await prisma.user.update({
|
||||
where: { email: userFound.email },
|
||||
data: { password: hashedPassword }
|
||||
});
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Set-Cookie': `teamId=${uid}; HttpOnly; Path=/; Max-Age=15778800;`
|
||||
},
|
||||
body: {
|
||||
userId: userFound.id,
|
||||
teamId: userFound.id,
|
||||
permission: userFound.permission,
|
||||
isAdmin: true
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
const passwordMatch = await bcrypt.compare(password, userFound.password);
|
||||
if (!passwordMatch) {
|
||||
throw {
|
||||
|
@ -6,6 +6,7 @@ import crypto from 'crypto';
|
||||
import * as db from '$lib/database';
|
||||
import { checkContainer, checkHAProxy } from '.';
|
||||
import { asyncExecShell, getDomain, getEngine } from '$lib/common';
|
||||
import { supportedServiceTypesAndVersions } from '$lib/components/common';
|
||||
|
||||
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
|
||||
|
||||
@ -223,7 +224,7 @@ export async function configureHAProxy() {
|
||||
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;
|
||||
if (destinationDockerId) {
|
||||
const { engine } = destinationDocker;
|
||||
const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type);
|
||||
const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
|
||||
if (found) {
|
||||
const port = found.ports.main;
|
||||
const publicPort = service[type]?.publicPort;
|
||||
|
@ -108,7 +108,7 @@ export async function stopTcpHttpProxy(destinationDocker, publicPort) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
export async function startTcpProxy(destinationDocker, id, publicPort, privatePort) {
|
||||
export async function startTcpProxy(destinationDocker, id, publicPort, privatePort, volume = null) {
|
||||
const { network, engine } = destinationDocker;
|
||||
const host = getEngine(engine);
|
||||
|
||||
@ -123,7 +123,9 @@ export async function startTcpProxy(destinationDocker, id, publicPort, privatePo
|
||||
);
|
||||
const ip = JSON.parse(Config)[0].Gateway;
|
||||
return await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} -d coollabsio/${defaultProxyImageTcp}`
|
||||
`DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} ${
|
||||
volume ? `-v ${volume}` : ''
|
||||
} -d coollabsio/${defaultProxyImageTcp}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -10,7 +10,7 @@ export default async function ({
|
||||
buildId,
|
||||
privateSshKey
|
||||
}): 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 asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`);
|
||||
await asyncExecShell(`chmod 600 ${repodir}/id.rsa`);
|
||||
|
@ -3,7 +3,9 @@ import { checkContainer, reloadHaproxy } from '$lib/haproxy';
|
||||
import * as db from '$lib/database';
|
||||
import { dev } from '$app/env';
|
||||
import cuid from 'cuid';
|
||||
import fs from 'fs/promises';
|
||||
import getPort, { portNumbers } from 'get-port';
|
||||
import { supportedServiceTypesAndVersions } from '$lib/components/common';
|
||||
|
||||
export async function letsEncrypt(domain, id = null, isCoolify = false) {
|
||||
try {
|
||||
@ -160,7 +162,7 @@ export async function generateSSLCerts() {
|
||||
type,
|
||||
destinationDocker: { engine }
|
||||
} = service;
|
||||
const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type);
|
||||
const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
|
||||
if (found) {
|
||||
const domain = getDomain(fqdn);
|
||||
const isHttps = fqdn.startsWith('https://');
|
||||
@ -181,12 +183,41 @@ export async function generateSSLCerts() {
|
||||
if (isHttps) ssls.push({ domain, id: 'coolify', isCoolify: true });
|
||||
}
|
||||
if (ssls.length > 0) {
|
||||
const sslDir = dev ? '/tmp/ssl' : '/app/ssl';
|
||||
if (dev) {
|
||||
try {
|
||||
await asyncExecShell(`mkdir -p ${sslDir}`);
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
const files = await fs.readdir(sslDir);
|
||||
let certificates = [];
|
||||
if (files.length > 0) {
|
||||
for (const file of files) {
|
||||
file.endsWith('.pem') && certificates.push(file.replace(/\.pem$/, ''));
|
||||
}
|
||||
}
|
||||
for (const ssl of ssls) {
|
||||
if (!dev) {
|
||||
console.log('Checking SSL for', ssl.domain);
|
||||
await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
|
||||
if (
|
||||
certificates.includes(ssl.domain) ||
|
||||
certificates.includes(ssl.domain.replace('www.', ''))
|
||||
) {
|
||||
console.log(`Certificate for ${ssl.domain} already exists`);
|
||||
} else {
|
||||
console.log('Generating SSL for', ssl.domain);
|
||||
await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
|
||||
}
|
||||
} else {
|
||||
console.log('Checking SSL for', ssl.domain);
|
||||
if (
|
||||
certificates.includes(ssl.domain) ||
|
||||
certificates.includes(ssl.domain.replace('www.', ''))
|
||||
) {
|
||||
console.log(`Certificate for ${ssl.domain} already exists`);
|
||||
} else {
|
||||
console.log('Generating SSL for', ssl.domain);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,12 +20,9 @@ import {
|
||||
setDefaultConfiguration
|
||||
} from '$lib/buildPacks/common';
|
||||
import yaml from 'js-yaml';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
|
||||
export default async function (job) {
|
||||
/*
|
||||
Edge cases:
|
||||
1 - Change build pack and redeploy, what should happen?
|
||||
*/
|
||||
let {
|
||||
id: applicationId,
|
||||
repository,
|
||||
@ -274,7 +271,7 @@ export default async function (job) {
|
||||
}
|
||||
};
|
||||
});
|
||||
const compose = {
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[imageId]: {
|
||||
@ -283,7 +280,7 @@ export default async function (job) {
|
||||
volumes,
|
||||
env_file: envFound ? [`${workdir}/.env`] : [],
|
||||
networks: [docker.network],
|
||||
labels: labels,
|
||||
labels,
|
||||
depends_on: [],
|
||||
restart: 'always'
|
||||
}
|
||||
@ -295,7 +292,7 @@ export default async function (job) {
|
||||
},
|
||||
volumes: Object.assign({}, ...composeVolumes)
|
||||
};
|
||||
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(compose));
|
||||
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker compose --project-directory ${workdir} up -d`
|
||||
);
|
||||
|
53
src/lib/types/composeFile.ts
Normal file
53
src/lib/types/composeFile.ts
Normal 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;
|
||||
};
|
@ -12,7 +12,7 @@
|
||||
if (!session.userId) {
|
||||
return {};
|
||||
}
|
||||
const endpoint = `/teams.json`;
|
||||
const endpoint = `/dashboard.json`;
|
||||
const res = await fetch(endpoint);
|
||||
|
||||
if (res.ok) {
|
||||
@ -134,13 +134,18 @@
|
||||
|
||||
<svelte:head>
|
||||
<title>Coolify</title>
|
||||
{#if !$session.whiteLabeled}
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
<SvelteToast options={{ intro: { y: -64 }, duration: 3000, pausable: true }} />
|
||||
{#if $session.userId}
|
||||
<nav class="nav-main">
|
||||
<div class="flex h-screen w-full flex-col items-center transition-all duration-100">
|
||||
<div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div>
|
||||
<div class="flex flex-col space-y-4 py-2">
|
||||
{#if !$session.whiteLabeled}
|
||||
<div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div>
|
||||
{/if}
|
||||
<div class="flex flex-col space-y-4 py-2" class:mt-2={$session.whiteLabeled}>
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/"
|
||||
@ -312,7 +317,6 @@
|
||||
<path d="M7 18a4.6 4.4 0 0 1 0 -9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-12" />
|
||||
</svg>
|
||||
</a>
|
||||
<div class="border-t border-stone-700" />
|
||||
</div>
|
||||
<div class="flex-1" />
|
||||
|
||||
@ -430,13 +434,12 @@
|
||||
<div class="flex flex-col space-y-4 py-2">
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/teams"
|
||||
class="icons tooltip-cyan-500 tooltip-right bg-coolgray-200 hover:text-cyan-500"
|
||||
class:text-cyan-500={$page.url.pathname.startsWith('/teams')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/teams')}
|
||||
data-tooltip="Teams"
|
||||
>
|
||||
<svg
|
||||
href="/iam"
|
||||
class="icons tooltip-right bg-coolgray-200 hover:text-fuchsia-500"
|
||||
class:text-fuchsia-500={$page.url.pathname.startsWith('/iam')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
|
||||
data-tooltip="IAM"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
viewBox="0 0 24 24"
|
||||
@ -453,6 +456,7 @@
|
||||
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
{#if $session.teamId === '0'}
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
@ -480,6 +484,7 @@
|
||||
</svg>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="icons tooltip-red-500 tooltip-right bg-coolgray-200 hover:text-red-500"
|
||||
data-tooltip="Logout"
|
||||
@ -514,8 +519,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{#if $session.whiteLabeled}
|
||||
<span class="fixed bottom-0 left-[50px] z-50 m-2 px-4 text-xs text-stone-700"
|
||||
>Powered by <a href="https://coolify.io" target="_blank">Coolify</a></span
|
||||
>
|
||||
{/if}
|
||||
|
||||
<select
|
||||
class="fixed right-0 bottom-0 z-50 m-2 w-64 bg-opacity-30 p-2 px-4"
|
||||
class="fixed right-0 bottom-0 z-50 m-2 w-64 bg-opacity-30 p-2 px-4 hover:bg-opacity-100"
|
||||
bind:value={selectedTeamId}
|
||||
on:change={switchTeam}
|
||||
>
|
||||
|
@ -81,6 +81,9 @@
|
||||
);
|
||||
const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'blob');
|
||||
const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'blob');
|
||||
const composerPHP = files.find(
|
||||
(file) => file.name === 'composer.json' && file.type === 'blob'
|
||||
);
|
||||
|
||||
if (yarnLock) packageManager = 'yarn';
|
||||
if (pnpmLock) packageManager = 'pnpm';
|
||||
@ -103,7 +106,7 @@
|
||||
foundConfig = findBuildPack('python');
|
||||
} else if (indexHtml) {
|
||||
foundConfig = findBuildPack('static', packageManager);
|
||||
} else if (indexPHP) {
|
||||
} else if (indexPHP || composerPHP) {
|
||||
foundConfig = findBuildPack('php');
|
||||
} else {
|
||||
foundConfig = findBuildPack('node', packageManager);
|
||||
@ -127,6 +130,9 @@
|
||||
);
|
||||
const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'file');
|
||||
const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'file');
|
||||
const composerPHP = files.find(
|
||||
(file) => file.name === 'composer.json' && file.type === 'file'
|
||||
);
|
||||
|
||||
if (yarnLock) packageManager = 'yarn';
|
||||
if (pnpmLock) packageManager = 'pnpm';
|
||||
@ -146,7 +152,7 @@
|
||||
foundConfig = findBuildPack('python');
|
||||
} else if (indexHtml) {
|
||||
foundConfig = findBuildPack('static', packageManager);
|
||||
} else if (indexPHP) {
|
||||
} else if (indexPHP || composerPHP) {
|
||||
foundConfig = findBuildPack('php');
|
||||
} else {
|
||||
foundConfig = findBuildPack('node', packageManager);
|
||||
|
@ -29,7 +29,7 @@
|
||||
<script lang="ts">
|
||||
import type Prisma from '@prisma/client';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { page, session } from '$app/stores';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { goto } from '$app/navigation';
|
||||
import { post } from '$lib/api';
|
||||
@ -39,6 +39,16 @@
|
||||
|
||||
export let destinations: Prisma.DestinationDocker[];
|
||||
|
||||
const ownDestinations = destinations.filter((destination) => {
|
||||
if (destination.teams[0].id === $session.teamId) {
|
||||
return destination;
|
||||
}
|
||||
});
|
||||
const otherDestinations = destinations.filter((destination) => {
|
||||
if (destination.teams[0].id !== $session.teamId) {
|
||||
return destination;
|
||||
}
|
||||
});
|
||||
async function handleSubmit(destinationId) {
|
||||
try {
|
||||
await post(`/applications/${id}/configuration/destination.json`, { destinationId });
|
||||
@ -52,8 +62,8 @@
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Configure Destination</div>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
{#if !destinations || destinations.length === 0}
|
||||
<div class="flex flex-col justify-center">
|
||||
{#if !destinations || ownDestinations.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="pb-2">No configurable Destination found</div>
|
||||
<div class="flex justify-center">
|
||||
@ -75,8 +85,23 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each destinations as destination}
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each ownDestinations as destination}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(destination.id)}>
|
||||
<button type="submit" class="box-selection hover:bg-sky-700 font-bold">
|
||||
<div class="font-bold text-xl text-center truncate">{destination.name}</div>
|
||||
<div class="text-center truncate">{destination.network}</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if otherDestinations.length > 0 && $session.teamId === '0'}
|
||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Destinations</div>
|
||||
{/if}
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each otherDestinations as destination}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(destination.id)}>
|
||||
<button type="submit" class="box-selection hover:bg-sky-700 font-bold">
|
||||
|
@ -29,7 +29,7 @@
|
||||
<script lang="ts">
|
||||
import type Prisma from '@prisma/client';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { page, session } from '$app/stores';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { goto } from '$app/navigation';
|
||||
import { post } from '$lib/api';
|
||||
@ -46,6 +46,17 @@
|
||||
(source.type === 'github' && source.githubAppId && source.githubApp.installationId) ||
|
||||
(source.type === 'gitlab' && source.gitlabAppId)
|
||||
);
|
||||
const ownSources = filteredSources.filter((source) => {
|
||||
if (source.teams[0].id === $session.teamId) {
|
||||
return source;
|
||||
}
|
||||
});
|
||||
const otherSources = filteredSources.filter((source) => {
|
||||
if (source.teams[0].id !== $session.teamId) {
|
||||
return source;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit(gitSourceId) {
|
||||
try {
|
||||
await post(`/applications/${id}/configuration/source.json`, { gitSourceId });
|
||||
@ -54,17 +65,21 @@
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function newSource() {
|
||||
const { id } = await post('/sources/new', {});
|
||||
return await goto(`/sources/${id}`, { replaceState: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Select a Git Source</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center">
|
||||
{#if !filteredSources || filteredSources.length === 0}
|
||||
{#if !filteredSources || ownSources.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="pb-2">No configurable Git Source found</div>
|
||||
<div class="pb-2 text-center">No configurable Git Source found</div>
|
||||
<div class="flex justify-center">
|
||||
<a href="/new/source" sveltekit:prefetch class="add-icon bg-orange-600 hover:bg-orange-500">
|
||||
<button on:click={newSource} class="add-icon bg-orange-600 hover:bg-orange-500">
|
||||
<svg
|
||||
class="w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -78,12 +93,39 @@
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/></svg
|
||||
>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each filteredSources as source}
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each ownSources as source}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(source.id)}>
|
||||
<button
|
||||
disabled={source.gitlabApp && !source.gitlabAppId}
|
||||
type="submit"
|
||||
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group"
|
||||
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
|
||||
class:border-0={source.gitlabApp && !source.gitlabAppId}
|
||||
class:border-l-4={source.gitlabApp && !source.gitlabAppId}
|
||||
>
|
||||
<div class="font-bold text-xl text-center truncate">{source.name}</div>
|
||||
{#if source.gitlabApp && !source.gitlabAppId}
|
||||
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">
|
||||
Configuration missing
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if otherSources.length > 0 && $session.teamId === '0'}
|
||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Sources</div>
|
||||
{/if}
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each otherSources as source}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(source.id)}>
|
||||
<button
|
||||
|
@ -14,6 +14,7 @@ export const del: RequestHandler = async (event) => {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return ErrorHandler(error);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
@ -1,13 +1,40 @@
|
||||
<script lang="ts">
|
||||
export let applications: Array<Application>;
|
||||
import { session } from '$app/stores';
|
||||
import Application from './_Application.svelte';
|
||||
import { post } from '$lib/api';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import Rust from '$lib/components/svg/applications/Rust.svelte';
|
||||
import Nodejs from '$lib/components/svg/applications/Nodejs.svelte';
|
||||
import React from '$lib/components/svg/applications/React.svelte';
|
||||
import Svelte from '$lib/components/svg/applications/Svelte.svelte';
|
||||
import Vuejs from '$lib/components/svg/applications/Vuejs.svelte';
|
||||
import PHP from '$lib/components/svg/applications/PHP.svelte';
|
||||
import Python from '$lib/components/svg/applications/Python.svelte';
|
||||
import Static from '$lib/components/svg/applications/Static.svelte';
|
||||
import Nestjs from '$lib/components/svg/applications/Nestjs.svelte';
|
||||
import Nuxtjs from '$lib/components/svg/applications/Nuxtjs.svelte';
|
||||
import Nextjs from '$lib/components/svg/applications/Nextjs.svelte';
|
||||
import Gatsby from '$lib/components/svg/applications/Gatsby.svelte';
|
||||
import Docker from '$lib/components/svg/applications/Docker.svelte';
|
||||
import Astro from '$lib/components/svg/applications/Astro.svelte';
|
||||
import Eleventy from '$lib/components/svg/applications/Eleventy.svelte';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
|
||||
async function newApplication() {
|
||||
const { id } = await post('/applications/new', {});
|
||||
return await goto(`/applications/${id}`, { replaceState: true });
|
||||
}
|
||||
const ownApplications = applications.filter((application) => {
|
||||
if (application.teams[0].id === $session.teamId) {
|
||||
return application;
|
||||
}
|
||||
});
|
||||
const otherApplications = applications.filter((application) => {
|
||||
if (application.teams[0].id !== $session.teamId) {
|
||||
return application;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
@ -30,14 +57,125 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#if !applications || applications.length === 0}
|
||||
<div class="flex flex-col flex-wrap justify-center">
|
||||
{#if !applications || ownApplications.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="text-center text-xl font-bold">No applications found</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#each applications as application}
|
||||
<Application {application} />
|
||||
{/each}
|
||||
{/if}
|
||||
{#if ownApplications.length > 0 || otherApplications.length > 0}
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each ownApplications as application}
|
||||
<a href="/applications/{application.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection group relative hover:bg-green-600">
|
||||
{#if application.buildPack}
|
||||
{#if application.buildPack.toLowerCase() === 'rust'}
|
||||
<Rust />
|
||||
{:else if application.buildPack.toLowerCase() === 'node'}
|
||||
<Nodejs />
|
||||
{:else if application.buildPack.toLowerCase() === 'react'}
|
||||
<React />
|
||||
{:else if application.buildPack.toLowerCase() === 'svelte'}
|
||||
<Svelte />
|
||||
{:else if application.buildPack.toLowerCase() === 'vuejs'}
|
||||
<Vuejs />
|
||||
{:else if application.buildPack.toLowerCase() === 'php'}
|
||||
<PHP />
|
||||
{:else if application.buildPack.toLowerCase() === 'python'}
|
||||
<Python />
|
||||
{:else if application.buildPack.toLowerCase() === 'static'}
|
||||
<Static />
|
||||
{:else if application.buildPack.toLowerCase() === 'nestjs'}
|
||||
<Nestjs />
|
||||
{:else if application.buildPack.toLowerCase() === 'nuxtjs'}
|
||||
<Nuxtjs />
|
||||
{:else if application.buildPack.toLowerCase() === 'nextjs'}
|
||||
<Nextjs />
|
||||
{:else if application.buildPack.toLowerCase() === 'gatsby'}
|
||||
<Gatsby />
|
||||
{:else if application.buildPack.toLowerCase() === 'docker'}
|
||||
<Docker />
|
||||
{:else if application.buildPack.toLowerCase() === 'astro'}
|
||||
<Astro />
|
||||
{:else if application.buildPack.toLowerCase() === 'eleventy'}
|
||||
<Eleventy />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="truncate text-center text-xl font-bold">{application.name}</div>
|
||||
{#if $session.teamId === '0' && otherApplications.length > 0}
|
||||
<div class="truncate text-center">Team {application.teams[0].name}</div>
|
||||
{/if}
|
||||
{#if application.fqdn}
|
||||
<div class="truncate text-center">{getDomain(application.fqdn) || ''}</div>
|
||||
{/if}
|
||||
{#if !application.gitSourceId || !application.destinationDockerId || !application.fqdn}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
Configuration missing
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{#if otherApplications.length > 0 && $session.teamId === '0'}
|
||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Applications</div>
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each otherApplications as application}
|
||||
<a href="/applications/{application.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection group relative hover:bg-green-600">
|
||||
{#if application.buildPack}
|
||||
{#if application.buildPack.toLowerCase() === 'rust'}
|
||||
<Rust />
|
||||
{:else if application.buildPack.toLowerCase() === 'node'}
|
||||
<Nodejs />
|
||||
{:else if application.buildPack.toLowerCase() === 'react'}
|
||||
<React />
|
||||
{:else if application.buildPack.toLowerCase() === 'svelte'}
|
||||
<Svelte />
|
||||
{:else if application.buildPack.toLowerCase() === 'vuejs'}
|
||||
<Vuejs />
|
||||
{:else if application.buildPack.toLowerCase() === 'php'}
|
||||
<PHP />
|
||||
{:else if application.buildPack.toLowerCase() === 'python'}
|
||||
<Python />
|
||||
{:else if application.buildPack.toLowerCase() === 'static'}
|
||||
<Static />
|
||||
{:else if application.buildPack.toLowerCase() === 'nestjs'}
|
||||
<Nestjs />
|
||||
{:else if application.buildPack.toLowerCase() === 'nuxtjs'}
|
||||
<Nuxtjs />
|
||||
{:else if application.buildPack.toLowerCase() === 'nextjs'}
|
||||
<Nextjs />
|
||||
{:else if application.buildPack.toLowerCase() === 'gatsby'}
|
||||
<Gatsby />
|
||||
{:else if application.buildPack.toLowerCase() === 'docker'}
|
||||
<Docker />
|
||||
{:else if application.buildPack.toLowerCase() === 'astro'}
|
||||
<Astro />
|
||||
{:else if application.buildPack.toLowerCase() === 'eleventy'}
|
||||
<Eleventy />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="truncate text-center text-xl font-bold">{application.name}</div>
|
||||
{#if $session.teamId === '0'}
|
||||
<div class="truncate text-center">Team {application.teams[0].name}</div>
|
||||
{/if}
|
||||
{#if application.fqdn}
|
||||
<div class="truncate text-center">{getDomain(application.fqdn) || ''}</div>
|
||||
{/if}
|
||||
{#if !application.gitSourceId || !application.destinationDockerId || !application.fqdn}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
Configuration missing
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -9,23 +9,28 @@ export const get: RequestHandler = async (event) => {
|
||||
|
||||
try {
|
||||
const applicationsCount = await db.prisma.application.count({
|
||||
where: { teams: { some: { id: teamId } } }
|
||||
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
|
||||
});
|
||||
const sourcesCount = await db.prisma.gitSource.count({
|
||||
where: { teams: { some: { id: teamId } } }
|
||||
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
|
||||
});
|
||||
const destinationsCount = await db.prisma.destinationDocker.count({
|
||||
where: { teams: { some: { id: teamId } } }
|
||||
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
|
||||
});
|
||||
const teamsCount = await db.prisma.permission.count({ where: { userId } });
|
||||
const databasesCount = await db.prisma.database.count({
|
||||
where: { teams: { some: { id: teamId } } }
|
||||
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
|
||||
});
|
||||
const servicesCount = await db.prisma.service.count({
|
||||
where: { teams: { some: { id: teamId } } }
|
||||
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
|
||||
});
|
||||
const teams = await db.prisma.permission.findMany({
|
||||
where: { userId },
|
||||
include: { team: { include: { _count: { select: { users: true } } } } }
|
||||
});
|
||||
return {
|
||||
body: {
|
||||
teams,
|
||||
applicationsCount,
|
||||
sourcesCount,
|
||||
destinationsCount,
|
||||
|
@ -2,6 +2,8 @@
|
||||
export let database;
|
||||
export let privatePort;
|
||||
export let settings;
|
||||
export let isRunning;
|
||||
|
||||
import { page, session } from '$app/stores';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
@ -15,27 +17,39 @@
|
||||
import { browser } from '$app/env';
|
||||
import { post } from '$lib/api';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
let loading = false;
|
||||
let publicLoading = false;
|
||||
|
||||
let isPublic = database.settings.isPublic || false;
|
||||
let appendOnly = database.settings.appendOnly;
|
||||
|
||||
let databaseDefault = database.defaultDatabase;
|
||||
let databaseDbUser = database.dbUser;
|
||||
let databaseDbUserPassword = database.dbUserPassword;
|
||||
if (database.type === 'mongodb') {
|
||||
databaseDefault = '?readPreference=primary&ssl=false';
|
||||
databaseDbUser = database.rootUser;
|
||||
databaseDbUserPassword = database.rootUserPassword;
|
||||
} else if (database.type === 'redis') {
|
||||
databaseDefault = '';
|
||||
databaseDbUser = '';
|
||||
let databaseDefault;
|
||||
let databaseDbUser;
|
||||
let databaseDbUserPassword;
|
||||
|
||||
generateDbDetails();
|
||||
|
||||
function generateDbDetails() {
|
||||
databaseDefault = database.defaultDatabase;
|
||||
databaseDbUser = database.dbUser;
|
||||
databaseDbUserPassword = database.dbUserPassword;
|
||||
if (database.type === 'mongodb') {
|
||||
databaseDefault = '?readPreference=primary&ssl=false';
|
||||
databaseDbUser = database.rootUser;
|
||||
databaseDbUserPassword = database.rootUserPassword;
|
||||
} else if (database.type === 'redis') {
|
||||
databaseDefault = '';
|
||||
databaseDbUser = '';
|
||||
}
|
||||
}
|
||||
let databaseUrl = generateUrl();
|
||||
$: databaseUrl = generateUrl();
|
||||
|
||||
function generateUrl() {
|
||||
return browser
|
||||
return (databaseUrl = browser
|
||||
? `${database.type}://${
|
||||
databaseDbUser ? databaseDbUser + ':' : ''
|
||||
}${databaseDbUserPassword}@${
|
||||
@ -45,32 +59,50 @@
|
||||
: window.location.hostname
|
||||
: database.id
|
||||
}:${isPublic ? database.publicPort : privatePort}/${databaseDefault}`
|
||||
: 'Loading...';
|
||||
: 'Loading...');
|
||||
}
|
||||
|
||||
async function changeSettings(name) {
|
||||
if (publicLoading || !isRunning) return;
|
||||
publicLoading = true;
|
||||
let data = {
|
||||
isPublic,
|
||||
appendOnly
|
||||
};
|
||||
if (name === 'isPublic') {
|
||||
isPublic = !isPublic;
|
||||
data.isPublic = !isPublic;
|
||||
}
|
||||
if (name === 'appendOnly') {
|
||||
appendOnly = !appendOnly;
|
||||
data.appendOnly = !appendOnly;
|
||||
}
|
||||
try {
|
||||
const { publicPort } = await post(`/databases/${id}/settings.json`, { isPublic, appendOnly });
|
||||
const { publicPort } = await post(`/databases/${id}/settings.json`, {
|
||||
isPublic: data.isPublic,
|
||||
appendOnly: data.appendOnly
|
||||
});
|
||||
isPublic = data.isPublic;
|
||||
appendOnly = data.appendOnly;
|
||||
databaseUrl = generateUrl();
|
||||
if (isPublic) {
|
||||
database.publicPort = publicPort;
|
||||
}
|
||||
databaseUrl = generateUrl();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
publicLoading = false;
|
||||
}
|
||||
}
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await post(`/databases/${id}.json`, { ...database });
|
||||
return window.location.reload();
|
||||
loading = true;
|
||||
await post(`/databases/${id}.json`, { ...database, isRunning });
|
||||
generateDbDetails();
|
||||
databaseUrl = generateUrl();
|
||||
toast.push('Settings saved.');
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -142,21 +174,21 @@
|
||||
readonly
|
||||
disabled
|
||||
name="publicPort"
|
||||
value={isPublic ? database.publicPort : privatePort}
|
||||
value={publicLoading ? 'Loading...' : isPublic ? database.publicPort : privatePort}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2">
|
||||
{#if database.type === 'mysql'}
|
||||
<MySql bind:database />
|
||||
<MySql bind:database {isRunning} />
|
||||
{:else if database.type === 'postgresql'}
|
||||
<PostgreSql bind:database />
|
||||
<PostgreSql bind:database {isRunning} />
|
||||
{:else if database.type === 'mongodb'}
|
||||
<MongoDb {database} />
|
||||
<MongoDb bind:database {isRunning} />
|
||||
{:else if database.type === 'redis'}
|
||||
<Redis {database} />
|
||||
<Redis bind:database {isRunning} />
|
||||
{:else if database.type === 'couchdb'}
|
||||
<CouchDb bind:database />
|
||||
<CouchDb {database} />
|
||||
{/if}
|
||||
<div class="grid grid-cols-2 items-center px-10 pb-8">
|
||||
<label for="url" class="text-base font-bold text-stone-100">Connection String</label>
|
||||
@ -168,7 +200,7 @@
|
||||
name="url"
|
||||
readonly
|
||||
disabled
|
||||
value={databaseUrl}
|
||||
value={publicLoading || loading ? 'Loading...' : generateUrl()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -179,10 +211,12 @@
|
||||
<div class="px-10 pb-10">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
loading={publicLoading}
|
||||
bind:setting={isPublic}
|
||||
on:click={() => changeSettings('isPublic')}
|
||||
title="Set it public"
|
||||
description="Your database will be reachable over the internet. <br>Take security seriously in this case!"
|
||||
disabled={!isRunning}
|
||||
/>
|
||||
</div>
|
||||
{#if database.type === 'redis'}
|
||||
|
@ -1,6 +1,8 @@
|
||||
<script>
|
||||
export let database;
|
||||
export let isRunning;
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
@ -21,13 +23,14 @@
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
|
||||
<CopyPasswordField
|
||||
disabled={!isRunning}
|
||||
readonly={!isRunning}
|
||||
placeholder="Generated automatically after start"
|
||||
isPasswordField={true}
|
||||
readonly
|
||||
disabled
|
||||
id="rootUserPassword"
|
||||
name="rootUserPassword"
|
||||
value={database.rootUserPassword}
|
||||
bind:value={database.rootUserPassword}
|
||||
/>
|
||||
<Explainer text="Could be changed while the database is running." />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,8 @@
|
||||
<script>
|
||||
export let database;
|
||||
export let isRunning;
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
@ -33,14 +35,15 @@
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
disabled={!isRunning}
|
||||
readonly={!isRunning}
|
||||
placeholder="Generated automatically after start"
|
||||
isPasswordField
|
||||
id="dbUserPassword"
|
||||
name="dbUserPassword"
|
||||
value={database.dbUserPassword}
|
||||
bind:value={database.dbUserPassword}
|
||||
/>
|
||||
<Explainer text="Could be changed while the database is running." />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
|
||||
@ -56,13 +59,14 @@
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
disabled={!isRunning}
|
||||
readonly={!isRunning}
|
||||
placeholder="Generated automatically after start"
|
||||
isPasswordField
|
||||
id="rootUserPassword"
|
||||
name="rootUserPassword"
|
||||
value={database.rootUserPassword}
|
||||
bind:value={database.rootUserPassword}
|
||||
/>
|
||||
<Explainer text="Could be changed while the database is running." />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,8 @@
|
||||
<script>
|
||||
export let database;
|
||||
export let isRunning;
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
@ -19,6 +21,19 @@
|
||||
bind:value={database.defaultDatabase}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="rootUser" class="text-base font-bold text-stone-100"
|
||||
>Root (postgres) User Password</label
|
||||
>
|
||||
<CopyPasswordField
|
||||
disabled={!isRunning}
|
||||
readonly={!isRunning}
|
||||
placeholder="Generated automatically after start"
|
||||
id="rootUserPassword"
|
||||
name="rootUserPassword"
|
||||
bind:value={database.rootUserPassword}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="dbUser" class="text-base font-bold text-stone-100">User</label>
|
||||
<CopyPasswordField
|
||||
@ -33,13 +48,14 @@
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
disabled={!isRunning}
|
||||
readonly={!isRunning}
|
||||
placeholder="Generated automatically after start"
|
||||
isPasswordField
|
||||
id="dbUserPassword"
|
||||
name="dbUserPassword"
|
||||
value={database.dbUserPassword}
|
||||
bind:value={database.dbUserPassword}
|
||||
/>
|
||||
<Explainer text="Could be changed while the database is running." />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,8 @@
|
||||
<script>
|
||||
export let database;
|
||||
export let isRunning;
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
@ -10,40 +12,14 @@
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
|
||||
<CopyPasswordField
|
||||
disabled
|
||||
readonly
|
||||
disabled={!isRunning}
|
||||
readonly={!isRunning}
|
||||
placeholder="Generated automatically after start"
|
||||
isPasswordField
|
||||
id="dbUserPassword"
|
||||
name="dbUserPassword"
|
||||
value={database.dbUserPassword}
|
||||
bind:value={database.dbUserPassword}
|
||||
/>
|
||||
<Explainer text="Could be changed while the database is running." />
|
||||
</div>
|
||||
<!-- <div class="grid grid-cols-3 items-center">
|
||||
<label for="rootUser">Root User</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
disabled
|
||||
readonly
|
||||
placeholder="Generated automatically after start"
|
||||
id="rootUser"
|
||||
name="rootUser"
|
||||
value={database.rootUser}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 items-center">
|
||||
<label for="rootUserPassword">Root's Password</label>
|
||||
<div class="col-span-2 ">
|
||||
<CopyPasswordField
|
||||
disabled
|
||||
readonly
|
||||
placeholder="Generated automatically after start"
|
||||
isPasswordField
|
||||
id="rootUserPassword"
|
||||
name="rootUserPassword"
|
||||
value={database.rootUserPassword}
|
||||
/>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
@ -15,7 +15,7 @@
|
||||
const endpoint = `/databases/${params.id}.json`;
|
||||
const res = await fetch(endpoint);
|
||||
if (res.ok) {
|
||||
const { database, state, versions, privatePort, settings } = await res.json();
|
||||
const { database, isRunning, versions, privatePort, settings } = await res.json();
|
||||
if (!database || Object.entries(database).length === 0) {
|
||||
return {
|
||||
status: 302,
|
||||
@ -35,13 +35,13 @@
|
||||
return {
|
||||
props: {
|
||||
database,
|
||||
state,
|
||||
isRunning,
|
||||
versions,
|
||||
privatePort
|
||||
},
|
||||
stuff: {
|
||||
database,
|
||||
state,
|
||||
isRunning,
|
||||
versions,
|
||||
privatePort,
|
||||
settings
|
||||
@ -65,7 +65,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let database;
|
||||
export let state;
|
||||
export let isRunning;
|
||||
let loading = false;
|
||||
|
||||
async function deleteDatabase() {
|
||||
@ -91,8 +91,6 @@
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -103,8 +101,6 @@
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -114,7 +110,7 @@
|
||||
<Loading fullscreen cover />
|
||||
{:else}
|
||||
{#if database.type && database.destinationDockerId && database.version && database.defaultDatabase}
|
||||
{#if state === 'running'}
|
||||
{#if isRunning}
|
||||
<button
|
||||
on:click={stopDatabase}
|
||||
title="Stop database"
|
||||
@ -140,7 +136,7 @@
|
||||
<rect x="14" y="5" width="4" height="14" rx="1" />
|
||||
</svg>
|
||||
</button>
|
||||
{:else if state === 'not started'}
|
||||
{:else}
|
||||
<button
|
||||
on:click={startDatabase}
|
||||
title="Start database"
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import { supportedDatabaseTypesAndVersions } from '$lib/components/common';
|
||||
import * as db from '$lib/database';
|
||||
import { ErrorHandler, supportedDatabaseTypesAndVersions } from '$lib/database';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import { supportedDatabaseTypesAndVersions } from '$lib/components/common';
|
||||
import * as db from '$lib/database';
|
||||
import { ErrorHandler, supportedDatabaseTypesAndVersions } from '$lib/database';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { asyncExecShell, getEngine, getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { generateDatabaseConfiguration, getVersions, ErrorHandler } from '$lib/database';
|
||||
import {
|
||||
generateDatabaseConfiguration,
|
||||
getVersions,
|
||||
ErrorHandler,
|
||||
updatePasswordInDb
|
||||
} from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
@ -12,7 +17,7 @@ export const get: RequestHandler = async (event) => {
|
||||
const database = await db.getDatabase({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker } = database;
|
||||
|
||||
let state = 'not started';
|
||||
let isRunning = false;
|
||||
if (destinationDockerId) {
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
|
||||
@ -22,7 +27,7 @@ export const get: RequestHandler = async (event) => {
|
||||
);
|
||||
|
||||
if (JSON.parse(stdout).Running) {
|
||||
state = 'running';
|
||||
isRunning = true;
|
||||
}
|
||||
} catch (error) {
|
||||
//
|
||||
@ -34,7 +39,7 @@ export const get: RequestHandler = async (event) => {
|
||||
body: {
|
||||
privatePort: configuration?.privatePort,
|
||||
database,
|
||||
state,
|
||||
isRunning,
|
||||
versions: getVersions(database.type),
|
||||
settings
|
||||
}
|
||||
@ -48,10 +53,26 @@ export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
const { id } = event.params;
|
||||
const { name, defaultDatabase, dbUser, dbUserPassword, rootUser, rootUserPassword, version } =
|
||||
await event.request.json();
|
||||
const {
|
||||
name,
|
||||
defaultDatabase,
|
||||
dbUser,
|
||||
dbUserPassword,
|
||||
rootUser,
|
||||
rootUserPassword,
|
||||
version,
|
||||
isRunning
|
||||
} = await event.request.json();
|
||||
|
||||
try {
|
||||
const database = await db.getDatabase({ id, teamId });
|
||||
if (isRunning) {
|
||||
if (database.dbUserPassword !== dbUserPassword) {
|
||||
await updatePasswordInDb(database, dbUser, dbUserPassword, false);
|
||||
} else if (database.rootUserPassword !== rootUserPassword) {
|
||||
await updatePasswordInDb(database, rootUser, rootUserPassword, true);
|
||||
}
|
||||
}
|
||||
await db.updateDatabase({
|
||||
id,
|
||||
name,
|
||||
|
@ -8,7 +8,8 @@
|
||||
database: stuff.database,
|
||||
versions: stuff.versions,
|
||||
privatePort: stuff.privatePort,
|
||||
settings: stuff.settings
|
||||
settings: stuff.settings,
|
||||
isRunning: stuff.isRunning
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -35,6 +36,7 @@
|
||||
export let database;
|
||||
export let settings;
|
||||
export let privatePort;
|
||||
export let isRunning;
|
||||
</script>
|
||||
|
||||
<div class="flex items-center space-x-2 p-6 text-2xl font-bold">
|
||||
@ -47,4 +49,4 @@
|
||||
<DatabaseLinks {database} />
|
||||
</div>
|
||||
|
||||
<Databases bind:database {privatePort} {settings} />
|
||||
<Databases bind:database {privatePort} {settings} {isRunning} />
|
||||
|
@ -6,6 +6,7 @@ import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { makeLabelForStandaloneDatabase } from '$lib/buildPacks/common';
|
||||
import { startTcpProxy } from '$lib/haproxy';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
@ -33,7 +34,7 @@ export const post: RequestHandler = async (event) => {
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
|
||||
const composeFile = {
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
|
@ -8,11 +8,22 @@
|
||||
import Redis from '$lib/components/svg/databases/Redis.svelte';
|
||||
import { post } from '$lib/api';
|
||||
import { goto } from '$app/navigation';
|
||||
import { session } from '$app/stores';
|
||||
|
||||
async function newDatabase() {
|
||||
const { id } = await post('/databases/new', {});
|
||||
return await goto(`/databases/${id}`, { replaceState: true });
|
||||
}
|
||||
const ownDatabases = databases.filter((database) => {
|
||||
if (database.teams[0].id === $session.teamId) {
|
||||
return database;
|
||||
}
|
||||
});
|
||||
const otherDatabases = databases.filter((database) => {
|
||||
if (database.teams[0].id !== $session.teamId) {
|
||||
return database;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
@ -34,40 +45,83 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#if !databases || databases.length === 0}
|
||||
<div class="flex flex-col flex-wrap justify-center">
|
||||
{#if !databases || ownDatabases.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="text-center text-xl font-bold">No databases found</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#each databases as database}
|
||||
<a href="/databases/{database.id}" class="no-underline p-2 w-96">
|
||||
<div class="box-selection relative hover:bg-purple-600 group">
|
||||
{#if database.type === 'clickhouse'}
|
||||
<Clickhouse isAbsolute />
|
||||
{:else if database.type === 'couchdb'}
|
||||
<CouchDB isAbsolute />
|
||||
{:else if database.type === 'mongodb'}
|
||||
<MongoDB isAbsolute />
|
||||
{:else if database.type === 'mysql'}
|
||||
<MySQL isAbsolute />
|
||||
{:else if database.type === 'postgresql'}
|
||||
<PostgreSQL isAbsolute />
|
||||
{:else if database.type === 'redis'}
|
||||
<Redis isAbsolute />
|
||||
{/if}
|
||||
<div class="font-bold text-xl text-center truncate">
|
||||
{database.name}
|
||||
</div>
|
||||
{#if !database.type}
|
||||
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">
|
||||
Configuration missing
|
||||
{/if}
|
||||
{#if ownDatabases.length > 0 || otherDatabases.length > 0}
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each ownDatabases as database}
|
||||
<a href="/databases/{database.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection group relative hover:bg-purple-600">
|
||||
{#if database.type === 'clickhouse'}
|
||||
<Clickhouse isAbsolute />
|
||||
{:else if database.type === 'couchdb'}
|
||||
<CouchDB isAbsolute />
|
||||
{:else if database.type === 'mongodb'}
|
||||
<MongoDB isAbsolute />
|
||||
{:else if database.type === 'mysql'}
|
||||
<MySQL isAbsolute />
|
||||
{:else if database.type === 'postgresql'}
|
||||
<PostgreSQL isAbsolute />
|
||||
{:else if database.type === 'redis'}
|
||||
<Redis isAbsolute />
|
||||
{/if}
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{database.name}
|
||||
</div>
|
||||
{#if $session.teamId === '0' && otherDatabases.length > 0}
|
||||
<div class="truncate text-center">{database.teams[0].name}</div>
|
||||
{/if}
|
||||
{#if !database.type}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
Configuration missing
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center truncate">{database.type}</div>
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{#if otherDatabases.length > 0 && $session.teamId === '0'}
|
||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Databases</div>
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each otherDatabases as database}
|
||||
<a href="/databases/{database.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection group relative hover:bg-purple-600">
|
||||
{#if database.type === 'clickhouse'}
|
||||
<Clickhouse isAbsolute />
|
||||
{:else if database.type === 'couchdb'}
|
||||
<CouchDB isAbsolute />
|
||||
{:else if database.type === 'mongodb'}
|
||||
<MongoDB isAbsolute />
|
||||
{:else if database.type === 'mysql'}
|
||||
<MySQL isAbsolute />
|
||||
{:else if database.type === 'postgresql'}
|
||||
<PostgreSQL isAbsolute />
|
||||
{:else if database.type === 'redis'}
|
||||
<Redis isAbsolute />
|
||||
{/if}
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{database.name}
|
||||
</div>
|
||||
{#if $session.teamId === '0'}
|
||||
<div class="truncate text-center">{database.teams[0].name}</div>
|
||||
{/if}
|
||||
{#if !database.type}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
Configuration missing
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center truncate">{database.type}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -12,8 +12,8 @@
|
||||
import { onMount } from 'svelte';
|
||||
const { id } = $page.params;
|
||||
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
|
||||
// let scannedApps = [];
|
||||
let loading = false;
|
||||
let loadingProxy = false;
|
||||
let restarting = false;
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
@ -25,12 +25,6 @@
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
// async function scanApps() {
|
||||
// scannedApps = [];
|
||||
// const data = await fetch(`/destinations/${id}/scan.json`);
|
||||
// const { containers } = await data.json();
|
||||
// scannedApps = containers;
|
||||
// }
|
||||
onMount(async () => {
|
||||
if (state === false && destination.isCoolifyProxyUsed === true) {
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
@ -71,6 +65,7 @@
|
||||
}
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
try {
|
||||
loadingProxy = true;
|
||||
await post(`/destinations/${id}/settings.json`, {
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
engine: destination.engine
|
||||
@ -82,6 +77,8 @@
|
||||
}
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loadingProxy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -184,41 +181,20 @@
|
||||
value={destination.network}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
disabled={cannotDisable}
|
||||
bind:setting={destination.isCoolifyProxyUsed}
|
||||
on:click={changeProxySetting}
|
||||
title="Use Coolify Proxy?"
|
||||
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${
|
||||
cannotDisable
|
||||
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
|
||||
: ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<!-- <div class="flex justify-center">
|
||||
{#if payload.isCoolifyProxyUsed}
|
||||
{#if state}
|
||||
<button on:click={stopProxy}>Stop proxy</button>
|
||||
{:else}
|
||||
<button on:click={startProxy}>Start proxy</button>
|
||||
{/if}
|
||||
{#if $session.teamId === '0'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
loading={loadingProxy}
|
||||
disabled={cannotDisable}
|
||||
bind:setting={destination.isCoolifyProxyUsed}
|
||||
on:click={changeProxySetting}
|
||||
title="Use Coolify Proxy?"
|
||||
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${
|
||||
cannotDisable
|
||||
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'
|
||||
: ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div> -->
|
||||
|
||||
<!-- {#if scannedApps.length > 0}
|
||||
<div class="flex justify-center px-6 pb-10">
|
||||
<div class="flex space-x-2 h-8 items-center">
|
||||
<div class="font-bold text-xl text-white">Found applications</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-w-4xl mx-auto px-6">
|
||||
<div class="flex space-x-2 justify-center">
|
||||
{#each scannedApps as app}
|
||||
<FoundApp {app} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if} -->
|
||||
</form>
|
||||
|
@ -8,7 +8,7 @@ import type { RequestHandler } from '@sveltejs/kit';
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
console.log(teamId);
|
||||
const { id } = event.params;
|
||||
try {
|
||||
const destination = await db.getDestination({ id, teamId });
|
||||
|
@ -24,6 +24,16 @@
|
||||
|
||||
import { session } from '$app/stores';
|
||||
export let destinations: Prisma.DestinationDocker[];
|
||||
const ownDestinations = destinations.filter((destination) => {
|
||||
if (destination.teams[0].id === $session.teamId) {
|
||||
return destination;
|
||||
}
|
||||
});
|
||||
const otherDestinations = destinations.filter((destination) => {
|
||||
if (destination.teams[0].id !== $session.teamId) {
|
||||
return destination;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
@ -47,20 +57,43 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
{#if !destinations || destinations.length === 0}
|
||||
{#if !destinations || ownDestinations.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="text-center text-xl font-bold">No destination found</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each destinations as destination}
|
||||
<a href="/destinations/{destination.id}" class="no-underline p-2 w-96">
|
||||
<div class="box-selection hover:bg-sky-600">
|
||||
<div class="font-bold text-xl text-center truncate">{destination.name}</div>
|
||||
<div class="text-center truncate">{destination.network}</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if ownDestinations.length > 0 || otherDestinations.length > 0}
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each ownDestinations as destination}
|
||||
<a href="/destinations/{destination.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection hover:bg-sky-600">
|
||||
<div class="truncate text-center text-xl font-bold">{destination.name}</div>
|
||||
{#if $session.teamId === '0' && otherDestinations.length > 0}
|
||||
<div class="truncate text-center">{destination.teams[0].name}</div>
|
||||
{/if}
|
||||
<div class="truncate text-center">{destination.network}</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if otherDestinations.length > 0 && $session.teamId === '0'}
|
||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Destinations</div>
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each otherDestinations as destination}
|
||||
<a href="/destinations/{destination.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection hover:bg-sky-600">
|
||||
<div class="truncate text-center text-xl font-bold">{destination.name}</div>
|
||||
{#if $session.teamId === '0'}
|
||||
<div class="truncate text-center">{destination.teams[0].name}</div>
|
||||
{/if}
|
||||
<div class="truncate text-center">{destination.network}</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
130
src/routes/iam/index.json.ts
Normal file
130
src/routes/iam/index.json.ts
Normal 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
175
src/routes/iam/index.svelte
Normal 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>
|
22
src/routes/iam/password.json.ts
Normal file
22
src/routes/iam/password.json.ts
Normal 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
|
||||
};
|
||||
}
|
||||
};
|
@ -1,14 +1,14 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params }) => {
|
||||
const url = `/teams/${params.id}.json`;
|
||||
const url = `/iam/team/${params.id}.json`;
|
||||
const res = await fetch(url);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (!data.permissions || Object.entries(data.permissions).length === 0) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/teams'
|
||||
redirect: '/iam'
|
||||
};
|
||||
}
|
||||
return {
|
||||
@ -20,7 +20,7 @@
|
||||
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/teams'
|
||||
redirect: '/iam'
|
||||
};
|
||||
};
|
||||
</script>
|
@ -4,14 +4,14 @@ import { ErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { userId, status, body } = await getUserDetails(event, false);
|
||||
const { teamId, userId, status, body } = await getUserDetails(event, false);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const user = await db.prisma.user.findFirst({
|
||||
where: { id: userId, teams: { some: { id } } },
|
||||
where: { id: userId, teams: teamId === '0' ? undefined : { some: { id } } },
|
||||
include: { permission: true }
|
||||
});
|
||||
if (!user) {
|
@ -1,7 +1,7 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params }) => {
|
||||
const url = `/teams/${params.id}.json`;
|
||||
const url = `/iam/team/${params.id}.json`;
|
||||
const res = await fetch(url);
|
||||
|
||||
if (res.ok) {
|
||||
@ -44,7 +44,7 @@
|
||||
|
||||
async function sendInvitation() {
|
||||
try {
|
||||
await post(`/teams/${id}/invitation/invite.json`, {
|
||||
await post(`/iam/team/${id}/invitation/invite.json`, {
|
||||
teamId: team.id,
|
||||
teamName: invitation.teamName,
|
||||
email: invitation.email.toLowerCase(),
|
||||
@ -57,7 +57,7 @@
|
||||
}
|
||||
async function revokeInvitation(id: string) {
|
||||
try {
|
||||
await post(`/teams/${id}/invitation/revoke.json`, { id });
|
||||
await post(`/iam/team/${id}/invitation/revoke.json`, { id });
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
@ -65,7 +65,7 @@
|
||||
}
|
||||
async function removeFromTeam(uid: string) {
|
||||
try {
|
||||
await post(`/teams/${id}/remove/user.json`, { teamId: team.id, uid });
|
||||
await post(`/iam/team/${id}/remove/user.json`, { teamId: team.id, uid });
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
@ -77,7 +77,7 @@
|
||||
newPermission = 'admin';
|
||||
}
|
||||
try {
|
||||
await post(`/teams/${id}/permission/change.json`, { userId, newPermission, permissionId });
|
||||
await post(`/iam/team/${id}/permission/change.json`, { userId, newPermission, permissionId });
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
@ -85,7 +85,7 @@
|
||||
}
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await post(`/teams/${id}.json`, { ...team });
|
||||
await post(`/iam/team/${id}.json`, { ...team });
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
@ -92,7 +92,7 @@
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/teams"
|
||||
href="/iam"
|
||||
sveltekit:prefetch
|
||||
class="flex cursor-pointer flex-col rounded p-6 text-center text-cyan-500 no-underline transition duration-150 hover:bg-cyan-500 hover:text-white"
|
||||
>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { session } from '$app/stores';
|
||||
|
||||
export let payload;
|
||||
|
||||
@ -56,13 +57,15 @@
|
||||
<label for="network" class="text-base font-bold text-stone-100">Network</label>
|
||||
<input required name="network" placeholder="default: coolify" bind:value={payload.network} />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={payload.isCoolifyProxyUsed}
|
||||
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
|
||||
title="Use Coolify Proxy?"
|
||||
description="This will install a proxy on the destination to allow you to access your applications and services without any manual configuration (recommended for Docker).<br><br>Databases will have their own proxy."
|
||||
/>
|
||||
</div>
|
||||
{#if $session.teamId === '0'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={payload.isCoolifyProxyUsed}
|
||||
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
|
||||
title="Use Coolify Proxy?"
|
||||
description="This will install a proxy on the destination to allow you to access your applications and services without any manual configuration (recommended for Docker).<br><br>Databases will have their own proxy."
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -2,6 +2,7 @@
|
||||
export let service;
|
||||
export let isRunning;
|
||||
export let readOnly;
|
||||
export let settings;
|
||||
|
||||
import { page, session } from '$app/stores';
|
||||
import { post } from '$lib/api';
|
||||
@ -91,7 +92,22 @@
|
||||
/>
|
||||
</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">
|
||||
<label for="destination" class="text-base font-bold text-stone-100">Destination</label>
|
||||
<div>
|
||||
@ -143,7 +159,7 @@
|
||||
{:else if service.type === 'vscodeserver'}
|
||||
<VsCodeServer {service} />
|
||||
{:else if service.type === 'wordpress'}
|
||||
<Wordpress bind:service {isRunning} {readOnly} />
|
||||
<Wordpress bind:service {isRunning} {readOnly} {settings} />
|
||||
{:else if service.type === 'ghost'}
|
||||
<Ghost bind:service {readOnly} />
|
||||
{:else if service.type === 'meilisearch'}
|
||||
@ -151,17 +167,4 @@
|
||||
{/if}
|
||||
</div>
|
||||
</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>
|
||||
|
@ -1,9 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { post } from '$lib/api';
|
||||
import { page } from '$app/stores';
|
||||
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 isRunning;
|
||||
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>
|
||||
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
@ -28,6 +77,30 @@ define('SUBDOMAIN_INSTALL', false);`
|
||||
: 'N/A'}>{service.wordpress.extraConfig}</textarea
|
||||
>
|
||||
</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="title">MySQL</div>
|
||||
</div>
|
||||
|
@ -16,7 +16,7 @@
|
||||
const endpoint = `/services/${params.id}.json`;
|
||||
const res = await fetch(endpoint);
|
||||
if (res.ok) {
|
||||
const { service, isRunning } = await res.json();
|
||||
const { service, isRunning, settings } = await res.json();
|
||||
if (!service || Object.entries(service).length === 0) {
|
||||
return {
|
||||
status: 302,
|
||||
@ -45,7 +45,8 @@
|
||||
stuff: {
|
||||
service,
|
||||
isRunning,
|
||||
readOnly
|
||||
readOnly,
|
||||
settings
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import { supportedServiceTypesAndVersions } from '$lib/components/common';
|
||||
import * as db from '$lib/database';
|
||||
import { ErrorHandler, supportedServiceTypesAndVersions } from '$lib/database';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import { supportedServiceTypesAndVersions } from '$lib/components/common';
|
||||
import * as db from '$lib/database';
|
||||
import { ErrorHandler, supportedServiceTypesAndVersions } from '$lib/database';
|
||||
import { ErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
@ -14,6 +15,7 @@ export const get: RequestHandler = async (event) => {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
type,
|
||||
versions: supportedServiceTypesAndVersions.find((name) => name.name === type).versions
|
||||
}
|
||||
};
|
||||
|
@ -31,11 +31,16 @@
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { goto } from '$app/navigation';
|
||||
import { post } from '$lib/api';
|
||||
import { supportedServiceTypesAndVersions } from '$lib/components/common';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
export let versions;
|
||||
export let type;
|
||||
let recommendedVersion = supportedServiceTypesAndVersions.find(
|
||||
({ name }) => name === type
|
||||
)?.recommendedVersion;
|
||||
async function handleSubmit(version) {
|
||||
try {
|
||||
await post(`/services/${id}/configuration/version.json`, { version });
|
||||
@ -49,13 +54,26 @@
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Select a Service version</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">
|
||||
{#each versions as version}
|
||||
<div class="p-2">
|
||||
<form on:submit|preventDefault={() => handleSubmit(version)}>
|
||||
<button type="submit" class="box-selection text-xl font-bold hover:bg-pink-600"
|
||||
>{version}</button
|
||||
<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>
|
||||
</div>
|
||||
|
@ -11,6 +11,7 @@ import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { ErrorHandler, getServiceImage } from '$lib/database';
|
||||
import { makeLabelForServices } from '$lib/buildPacks/common';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
@ -43,12 +44,15 @@ export const post: RequestHandler = async (event) => {
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
const image = getServiceImage(type);
|
||||
const domain = getDomain(fqdn);
|
||||
const isHttps = fqdn.startsWith('https://');
|
||||
const config = {
|
||||
ghost: {
|
||||
image: `${image}:${version}`,
|
||||
volume: `${id}-ghost:/bitnami/ghost`,
|
||||
environmentVariables: {
|
||||
url: fqdn,
|
||||
GHOST_HOST: domain,
|
||||
GHOST_ENABLE_HTTPS: isHttps ? 'yes' : 'no',
|
||||
GHOST_EMAIL: defaultEmail,
|
||||
GHOST_PASSWORD: defaultPassword,
|
||||
GHOST_DATABASE_HOST: `${id}-mariadb`,
|
||||
@ -75,7 +79,7 @@ export const post: RequestHandler = async (event) => {
|
||||
config.ghost.environmentVariables[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
const composeFile = {
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
|
@ -17,7 +17,7 @@ export const get: RequestHandler = async (event) => {
|
||||
const { id } = event.params;
|
||||
try {
|
||||
const service = await db.getService({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, type, version } = service;
|
||||
const { destinationDockerId, destinationDocker, type, version, settings } = service;
|
||||
|
||||
let isRunning = false;
|
||||
if (destinationDockerId) {
|
||||
@ -46,7 +46,8 @@ export const get: RequestHandler = async (event) => {
|
||||
return {
|
||||
body: {
|
||||
isRunning,
|
||||
service
|
||||
service,
|
||||
settings
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
|
@ -6,7 +6,8 @@
|
||||
props: {
|
||||
service: stuff.service,
|
||||
isRunning: stuff.isRunning,
|
||||
readOnly: stuff.readOnly
|
||||
readOnly: stuff.readOnly,
|
||||
settings: stuff.settings
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -37,6 +38,7 @@
|
||||
export let service;
|
||||
export let isRunning;
|
||||
export let readOnly;
|
||||
export let settings;
|
||||
|
||||
if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) {
|
||||
service.fqdn = `http://${cuid()}.demo.coolify.io`;
|
||||
@ -76,4 +78,4 @@
|
||||
<ServiceLinks {service} />
|
||||
</div>
|
||||
|
||||
<Services bind:service {isRunning} {readOnly} />
|
||||
<Services bind:service {isRunning} {readOnly} {settings} />
|
||||
|
@ -13,7 +13,7 @@ export const post: RequestHandler = async (event) => {
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
|
||||
try {
|
||||
await db.updateMeiliSearchService({ id, fqdn, name });
|
||||
await db.updateService({ id, fqdn, name });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
|
@ -5,6 +5,7 @@ import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { ErrorHandler, getServiceImage } from '$lib/database';
|
||||
import { makeLabelForServices } from '$lib/buildPacks/common';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
@ -32,7 +33,7 @@ export const post: RequestHandler = async (event) => {
|
||||
config.environmentVariables[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
const composeFile = {
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
|
@ -13,7 +13,7 @@ export const post: RequestHandler = async (event) => {
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
|
||||
try {
|
||||
await db.updateLanguageToolService({ id, fqdn, name });
|
||||
await db.updateService({ id, fqdn, name });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
|
@ -5,6 +5,7 @@ import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { ErrorHandler, getServiceImage } from '$lib/database';
|
||||
import { makeLabelForServices } from '$lib/buildPacks/common';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
@ -37,7 +38,7 @@ export const post: RequestHandler = async (event) => {
|
||||
config.environmentVariables[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
const composeFile = {
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
|
@ -8,6 +8,7 @@ import getPort, { portNumbers } from 'get-port';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
import { ErrorHandler, getServiceImage } from '$lib/database';
|
||||
import { makeLabelForServices } from '$lib/buildPacks/common';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
@ -55,7 +56,7 @@ export const post: RequestHandler = async (event) => {
|
||||
config.environmentVariables[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
const composeFile = {
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
|
@ -5,6 +5,7 @@ import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { ErrorHandler, getServiceImage } from '$lib/database';
|
||||
import { makeLabelForServices } from '$lib/buildPacks/common';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
@ -33,7 +34,7 @@ export const post: RequestHandler = async (event) => {
|
||||
config.environmentVariables[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
const composeFile = {
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
|
@ -5,6 +5,7 @@ import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { ErrorHandler, getServiceImage } from '$lib/database';
|
||||
import { makeLabelForServices } from '$lib/buildPacks/common';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
@ -30,7 +31,7 @@ export const post: RequestHandler = async (event) => {
|
||||
config.environmentVariables[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
const composeFile = {
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
|
@ -5,6 +5,7 @@ import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { ErrorHandler, getServiceImage } from '$lib/database';
|
||||
import { makeLabelForServices } from '$lib/buildPacks/common';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
|
||||
export const post: RequestHandler = async (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`;
|
||||
|
||||
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile);
|
||||
const composeFile = {
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
|
@ -5,6 +5,7 @@ import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { ErrorHandler, getServiceImage } from '$lib/database';
|
||||
import { makeLabelForServices } from '$lib/buildPacks/common';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
@ -31,7 +32,7 @@ export const post: RequestHandler = async (event) => {
|
||||
config.environmentVariables[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
const composeFile = {
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
|
@ -12,7 +12,7 @@ export const post: RequestHandler = async (event) => {
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
|
||||
try {
|
||||
await db.updateVaultWardenService({ id, fqdn, name });
|
||||
await db.updateService({ id, fqdn, name });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
|
@ -5,6 +5,7 @@ import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { getServiceImage, ErrorHandler } from '$lib/database';
|
||||
import { makeLabelForServices } from '$lib/buildPacks/common';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
@ -32,7 +33,7 @@ export const post: RequestHandler = async (event) => {
|
||||
config.environmentVariables[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
const composeFile = {
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
|
@ -13,7 +13,7 @@ export const post: RequestHandler = async (event) => {
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
|
||||
try {
|
||||
await db.updateVsCodeServer({ id, fqdn, name });
|
||||
await db.updateService({ id, fqdn, name });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
|
@ -5,6 +5,7 @@ import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { ErrorHandler, getServiceImage } from '$lib/database';
|
||||
import { makeLabelForServices } from '$lib/buildPacks/common';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { teamId, status, body } = await getUserDetails(event);
|
||||
@ -41,7 +42,7 @@ export const post: RequestHandler = async (event) => {
|
||||
config.environmentVariables[secret.name] = secret.value;
|
||||
});
|
||||
}
|
||||
const composeFile = {
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
|
187
src/routes/services/[id]/wordpress/settings.json.ts
Normal file
187
src/routes/services/[id]/wordpress/settings.json.ts
Normal 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`
|
||||
);
|
||||
}
|
||||
};
|
@ -5,6 +5,7 @@ import yaml from 'js-yaml';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { ErrorHandler, getServiceImage } from '$lib/database';
|
||||
import { makeLabelForServices } from '$lib/buildPacks/common';
|
||||
import type { ComposeFile } from '$lib/types/composeFile';
|
||||
|
||||
export const post: RequestHandler = async (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;
|
||||
});
|
||||
}
|
||||
const composeFile = {
|
||||
const composeFile: ComposeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[id]: {
|
||||
|
@ -12,12 +12,24 @@
|
||||
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
|
||||
import Ghost from '$lib/components/svg/services/Ghost.svelte';
|
||||
import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte';
|
||||
import { session } from '$app/stores';
|
||||
import { getDomain } from '$lib/components/common';
|
||||
|
||||
export let services;
|
||||
async function newService() {
|
||||
const { id } = await post('/services/new', {});
|
||||
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>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
@ -39,50 +51,109 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#if !services || services.length === 0}
|
||||
<div class="flex flex-col flex-wrap justify-center">
|
||||
{#if !services || ownServices.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="text-center text-xl font-bold">No services found</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#each services as service}
|
||||
<a href="/services/{service.id}" class="no-underline p-2 w-96">
|
||||
<div class="box-selection relative hover:bg-pink-600 group">
|
||||
{#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="font-bold text-xl text-center truncate">
|
||||
{service.name}
|
||||
</div>
|
||||
{#if !service.type || !service.fqdn}
|
||||
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">
|
||||
Configuration missing
|
||||
{/if}
|
||||
{#if ownServices.length > 0 || otherServices.length > 0}
|
||||
<div class="flex flex-col">
|
||||
<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'}
|
||||
<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' && 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}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
Configuration missing
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center truncate">{service.type}</div>
|
||||
{/if}
|
||||
</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
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center truncate">{service.type}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -5,9 +5,9 @@ import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { promises as dns } from 'dns';
|
||||
|
||||
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 (teamId !== '0') return { status: 401, body: { message: 'You are not an admin.' } };
|
||||
try {
|
||||
const settings = await listSettings();
|
||||
return {
|
||||
|
@ -11,7 +11,12 @@
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (res.status === 401) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/databases'
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
|
@ -3,13 +3,19 @@
|
||||
import { page, session } from '$app/stores';
|
||||
import { post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
const { id } = $page.params;
|
||||
|
||||
let loading = false;
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
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 }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
@ -38,7 +44,18 @@
|
||||
}, 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 top = screen.height / 2 - 618 / 2;
|
||||
const newWindow = open(
|
||||
@ -59,31 +76,72 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !source.githubAppId}
|
||||
<button on:click={newGithubApp}>Create new GitHub App</button>
|
||||
{:else if source.githubApp?.installationId}
|
||||
<form on:submit|preventDefault={handleSubmit} class="py-4">
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="title">General</div>
|
||||
{#if $session.isAdmin}
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-orange-600={!loading}
|
||||
class:hover:bg-orange-500={!loading}
|
||||
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
|
||||
>
|
||||
<button on:click|preventDefault={() => installRepositories(source)}
|
||||
>Change GitHub App Settings</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
<div class="grid grid-cols-2 items-center mt-2">
|
||||
<label for="name" class="text-base font-bold text-stone-100">Name</label>
|
||||
<input name="name" id="name" required bind:value={source.name} />
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
{#if !source.githubAppId}
|
||||
<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.githubApp?.installationId}
|
||||
<form on:submit|preventDefault={handleSubmit} class="py-4">
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="title">General</div>
|
||||
{#if $session.isAdmin}
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-orange-600={!loading}
|
||||
class:hover:bg-orange-500={!loading}
|
||||
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
|
||||
>
|
||||
<button on:click|preventDefault={() => installRepositories(source)}
|
||||
>Change GitHub App Settings</button
|
||||
>
|
||||
{/if}
|
||||
</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>
|
||||
</form>
|
||||
{:else}
|
||||
<div class="text-center">
|
||||
<button class=" bg-orange-600 mt-8" on:click={() => installRepositories(source)}
|
||||
>Install Repositories</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
{:else}
|
||||
<button on:click={() => installRepositories(source)}>Install Repositories</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -1,35 +1,73 @@
|
||||
<script lang="ts">
|
||||
export let source;
|
||||
export let settings;
|
||||
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 { onMount } from 'svelte';
|
||||
import { post } from '$lib/api';
|
||||
import { browser } from '$app/env';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
|
||||
const { id } = $page.params;
|
||||
let url = browser ? (settings.fqdn ? settings.fqdn : window.location.origin) : '';
|
||||
|
||||
let loading = false;
|
||||
|
||||
let oauthIdEl;
|
||||
let payload = {
|
||||
oauthId: undefined,
|
||||
groupName: undefined,
|
||||
appId: undefined,
|
||||
appSecret: undefined,
|
||||
applicationType: 'user'
|
||||
};
|
||||
let applicationType;
|
||||
if (!source.gitlabAppId) {
|
||||
source.gitlabApp = {
|
||||
oauthId: null,
|
||||
groupName: null,
|
||||
appId: null,
|
||||
appSecret: null
|
||||
};
|
||||
}
|
||||
onMount(() => {
|
||||
oauthIdEl && oauthIdEl.focus();
|
||||
});
|
||||
async function handleSubmitSave() {
|
||||
|
||||
async function handleSubmit() {
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
try {
|
||||
return await post(`/sources/${id}.json`, { name: source.name });
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
if (!source.gitlabAppId) {
|
||||
// New GitLab App
|
||||
try {
|
||||
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 }) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
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() {
|
||||
const {
|
||||
htmlUrl,
|
||||
@ -53,23 +91,27 @@
|
||||
}, 100);
|
||||
}
|
||||
async function checkOauthId() {
|
||||
if (payload.oauthId) {
|
||||
if (source.gitlabApp?.oauthId) {
|
||||
try {
|
||||
await post(`/sources/${id}/check.json`, { oauthId: payload.oauthId });
|
||||
await post(`/sources/${id}/check.json`, {
|
||||
oauthId: source.gitlabApp?.oauthId
|
||||
});
|
||||
} catch ({ error }) {
|
||||
payload.oauthId = null;
|
||||
source.gitlabApp.oauthId = null;
|
||||
oauthIdEl.focus();
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
function newApp() {
|
||||
switch (payload.applicationType) {
|
||||
switch (applicationType) {
|
||||
case 'user':
|
||||
window.open(`${source.htmlUrl}/-/profile/applications`);
|
||||
break;
|
||||
case 'group':
|
||||
window.open(`${source.htmlUrl}/groups/${payload.groupName}/-/settings/applications`);
|
||||
window.open(
|
||||
`${source.htmlUrl}/groups/${source.gitlabApp.groupName}/-/settings/applications`
|
||||
);
|
||||
break;
|
||||
case 'instance':
|
||||
break;
|
||||
@ -77,128 +119,136 @@
|
||||
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>
|
||||
|
||||
{#if !source.gitlabApp?.appId}
|
||||
<form class="grid grid-flow-row gap-2 py-4" on:submit|preventDefault={newApp}>
|
||||
<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="title">General</div>
|
||||
{#if $session.isAdmin}
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-orange-600={!loading}
|
||||
class:hover:bg-orange-500={!loading}
|
||||
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
|
||||
>
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<form on:submit|preventDefault={handleSubmit} class="py-4">
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="title">General</div>
|
||||
{#if $session.isAdmin}
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-orange-600={!loading}
|
||||
class:hover:bg-orange-500={!loading}
|
||||
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
|
||||
>
|
||||
{#if source.gitlabAppId}
|
||||
<button on:click|preventDefault={changeSettings}>Change GitLab App Settings</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
{/if}
|
||||
</div>
|
||||
<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">
|
||||
<label for="name" class="text-base font-bold text-stone-100">Name</label>
|
||||
<input name="name" id="name" required bind:value={source.name} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
{#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>
|
||||
{/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>
|
||||
|
18
src/routes/sources/[id]/github.json.ts
Normal file
18
src/routes/sources/[id]/github.json.ts
Normal 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);
|
||||
}
|
||||
};
|
@ -9,11 +9,23 @@ export const post: RequestHandler = async (event) => {
|
||||
const { id } = event.params;
|
||||
|
||||
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);
|
||||
|
||||
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 };
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
|
@ -43,10 +43,10 @@ export const post: RequestHandler = async (event) => {
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
const { name } = await event.request.json();
|
||||
const { name, htmlUrl, apiUrl } = await event.request.json();
|
||||
|
||||
try {
|
||||
await db.updateGitsource({ id, name });
|
||||
await db.updateGitsource({ id, name, htmlUrl, apiUrl });
|
||||
return { status: 201 };
|
||||
} catch (error) {
|
||||
return ErrorHandler(error);
|
||||
|
@ -29,9 +29,41 @@
|
||||
|
||||
<script lang="ts">
|
||||
export let source: Prisma.GitSource;
|
||||
export let settings;
|
||||
import type Prisma from '@prisma/client';
|
||||
import Github from './_Github.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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center px-6 pb-8">
|
||||
{#if source.type === 'github'}
|
||||
<Github bind:source />
|
||||
{:else if source.type === 'gitlab'}
|
||||
<Gitlab bind:source />
|
||||
<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'}
|
||||
<Github bind:source />
|
||||
{:else if source.type === 'gitlab'}
|
||||
<Gitlab bind:source {settings} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -36,6 +36,7 @@
|
||||
export let settings;
|
||||
onMount(() => {
|
||||
const { organization, id, htmlUrl } = source;
|
||||
console.log(source);
|
||||
const { fqdn } = settings;
|
||||
const host = dev
|
||||
? 'http://localhost:3000'
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user