diff --git a/.env.template b/.env.template index ce02dc5bd..0fa7427f3 100644 --- a/.env.template +++ b/.env.template @@ -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" \ No newline at end of file +COOLIFY_IS_ON="docker" +COOLIFY_WHITE_LABELED="false" \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f839dfce6..8a21ec10a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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? diff --git a/Dockerfile b/Dockerfile index 9494ff702..df4614a13 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/package.json b/package.json index 2aa11b114..ac2396dab 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coolify", "description": "An open-source & self-hostable Heroku / Netlify alternative.", - "version": "2.3.3", + "version": "2.4.0", "license": "AGPL-3.0", "scripts": { "dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev", diff --git a/prisma/migrations/20220405151428_wordpress_sftp/migration.sql b/prisma/migrations/20220405151428_wordpress_sftp/migration.sql new file mode 100644 index 000000000..6c3c4b907 --- /dev/null +++ b/prisma/migrations/20220405151428_wordpress_sftp/migration.sql @@ -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; diff --git a/prisma/migrations/20220407220809_unique_storage_fix/migration.sql b/prisma/migrations/20220407220809_unique_storage_fix/migration.sql new file mode 100644 index 000000000..9b645cc3b --- /dev/null +++ b/prisma/migrations/20220407220809_unique_storage_fix/migration.sql @@ -0,0 +1,5 @@ +-- DropIndex +DROP INDEX "ApplicationPersistentStorage_path_key"; + +-- DropIndex +DROP INDEX "ApplicationPersistentStorage_applicationId_key"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 986a773bf..2c97a9e8b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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()) diff --git a/src/app.d.ts b/src/app.d.ts index 0557299b9..d432aef4a 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -15,18 +15,20 @@ declare namespace App { readOnly: boolean; source: string; settings: string; + database: Record; + 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 = { diff --git a/src/app.html b/src/app.html index 2dc97f37d..a0336e87d 100644 --- a/src/app.html +++ b/src/app.html @@ -2,7 +2,6 @@ - Coolify %svelte.head% diff --git a/src/hooks.ts b/src/hooks.ts index d6c6eca6d..c72c7fd6c 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -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 }; }; diff --git a/src/lib/buildPacks/php.ts b/src/lib/buildPacks/php.ts index 14ee5d0cb..cfb39d20a 100644 --- a/src/lib/buildPacks/php.ts +++ b/src/lib/buildPacks/php.ts @@ -4,6 +4,12 @@ import { promises as fs } from 'fs'; const createDockerfile = async (data, image, htaccessFound): Promise => { const { workdir, baseDirectory } = data; const Dockerfile: Array = []; + 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 => { 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) { diff --git a/src/lib/common.ts b/src/lib/common.ts index 680e35208..57cdabba2 100644 --- a/src/lib/common.ts +++ b/src/lib/common.ts @@ -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, '@'); + if (line) { + if (line.includes('ghs_')) { + const regex = /ghs_.*@/g; + line = line.replace(regex, '@'); + } + 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 = diff --git a/src/lib/components/Setting.svelte b/src/lib/components/Setting.svelte index c8764bed7..d7b028861 100644 --- a/src/lib/components/Setting.svelte +++ b/src/lib/components/Setting.svelte @@ -7,6 +7,7 @@ export let isCenter = true; export let disabled = false; export let dataTooltip = null; + export let loading = false;
@@ -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} > Use setting
-
- {#if !services || services.length === 0} +
+ {#if !services || ownServices.length === 0}
No services found
- {:else} - {#each services as service} - -
- {#if service.type === 'plausibleanalytics'} - - {:else if service.type === 'nocodb'} - - {:else if service.type === 'minio'} - - {:else if service.type === 'vscodeserver'} - - {:else if service.type === 'wordpress'} - - {:else if service.type === 'vaultwarden'} - - {:else if service.type === 'languagetool'} - - {:else if service.type === 'n8n'} - - {:else if service.type === 'uptimekuma'} - - {:else if service.type === 'ghost'} - - {:else if service.type === 'meilisearch'} - - {/if} -
- {service.name} -
- {#if !service.type || !service.fqdn} -
- Configuration missing + {/if} + {#if ownServices.length > 0 || otherServices.length > 0} +
+ + {#if otherServices.length > 0 && $session.teamId === '0'} +
Other Services
+ - - {/each} + {/if} +
{/if}
diff --git a/src/routes/settings/index.json.ts b/src/routes/settings/index.json.ts index db3dc0710..d126b2313 100644 --- a/src/routes/settings/index.json.ts +++ b/src/routes/settings/index.json.ts @@ -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 { diff --git a/src/routes/settings/index.svelte b/src/routes/settings/index.svelte index e6148a26f..0af8d534c 100644 --- a/src/routes/settings/index.svelte +++ b/src/routes/settings/index.svelte @@ -11,7 +11,12 @@ } }; } - + if (res.status === 401) { + return { + status: 302, + redirect: '/databases' + }; + } return { status: res.status, error: new Error(`Could not load ${url}`) diff --git a/src/routes/sources/[id]/_Github.svelte b/src/routes/sources/[id]/_Github.svelte index 102971353..6b8aa0c89 100644 --- a/src/routes/sources/[id]/_Github.svelte +++ b/src/routes/sources/[id]/_Github.svelte @@ -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 @@ } -{#if !source.githubAppId} - -{:else if source.githubApp?.installationId} -
-
-
General
- {#if $session.isAdmin} - - - {/if} -
-
-
- - +
+ {#if !source.githubAppId} + +
+
General
+
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ {#if source.apiUrl && source.htmlUrl && source.name} +
+ +
+ {/if} + + {:else if source.githubAppId} +
+
+
General
+ {#if $session.isAdmin} + + + {/if} +
+
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ {:else} +
+
- -{:else} - -{/if} + {/if} +
diff --git a/src/routes/sources/[id]/_Gitlab.svelte b/src/routes/sources/[id]/_Gitlab.svelte index 56bb0288d..b528b1850 100644 --- a/src/routes/sources/[id]/_Gitlab.svelte +++ b/src/routes/sources/[id]/_Gitlab.svelte @@ -1,35 +1,73 @@ -{#if !source.gitlabApp?.appId} -
-
- - -
- {#if payload.applicationType === 'group'} -
- - -
- {/if} - -
- -
- - - -
-
-
Configuration
- -
- -
-
- - -
- -
- {#if payload.applicationType === 'group'} -
- - -
- {/if} -
- - -
-
- - -
-
-{:else} -
-
-
-
General
- {#if $session.isAdmin} - +
+ +
+
General
+ {#if $session.isAdmin} + + {#if source.gitlabAppId} {/if} -
-
+ {/if} +
+
+ {#if !source.gitlabAppId} +
+ + +
+ + {#if applicationType === 'group'} +
+ + +
+ {/if} + {/if} + +
- -
-{/if} + {#if source.gitlabApp.groupName} +
+ + +
+ {/if} +
+ + +
+
+ + +
+
+
+ + {#if !source.gitlabAppId} + + {/if} +
+ +
+ +
+ + +
+
+ + +
+
+ + {#if !source.gitlabAppId} + + {/if} +
diff --git a/src/routes/sources/[id]/github.json.ts b/src/routes/sources/[id]/github.json.ts new file mode 100644 index 000000000..e0f1d3b61 --- /dev/null +++ b/src/routes/sources/[id]/github.json.ts @@ -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); + } +}; diff --git a/src/routes/sources/[id]/gitlab.json.ts b/src/routes/sources/[id]/gitlab.json.ts index 8420669e6..b1ed314bf 100644 --- a/src/routes/sources/[id]/gitlab.json.ts +++ b/src/routes/sources/[id]/gitlab.json.ts @@ -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); diff --git a/src/routes/sources/[id]/index.json.ts b/src/routes/sources/[id]/index.json.ts index 58fb7008b..cd19e6785 100644 --- a/src/routes/sources/[id]/index.json.ts +++ b/src/routes/sources/[id]/index.json.ts @@ -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); diff --git a/src/routes/sources/[id]/index.svelte b/src/routes/sources/[id]/index.svelte index 83f171412..015c98a1f 100644 --- a/src/routes/sources/[id]/index.svelte +++ b/src/routes/sources/[id]/index.svelte @@ -29,9 +29,41 @@
@@ -40,10 +72,21 @@ {source.name}
-
- {#if source.type === 'github'} - - {:else if source.type === 'gitlab'} - +
+ {#if !source.gitlabAppId && !source.githubAppId} +
+
Select a provider
+
+ + +
+
{/if} +
+ {#if source.type === 'github'} + + {:else if source.type === 'gitlab'} + + {/if} +
diff --git a/src/routes/sources/[id]/newGithubApp.svelte b/src/routes/sources/[id]/newGithubApp.svelte index c8cb4f6f3..b6a74c3ba 100644 --- a/src/routes/sources/[id]/newGithubApp.svelte +++ b/src/routes/sources/[id]/newGithubApp.svelte @@ -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' diff --git a/src/routes/sources/index.svelte b/src/routes/sources/index.svelte index bd9039514..1e2d4d510 100644 --- a/src/routes/sources/index.svelte +++ b/src/routes/sources/index.svelte @@ -22,12 +22,29 @@
Git Sources
{#if $session.isAdmin} - + {/if}
-
- {#if !sources || sources.length === 0} +
+ {#if !sources || ownSources.length === 0}
No git sources found
- {:else} -
- {#each sources as source} - - diff --git a/src/routes/new/source/index.json.ts b/src/routes/sources/new.ts similarity index 64% rename from src/routes/new/source/index.json.ts rename to src/routes/sources/new.ts index 803b597e9..25943f8bc 100644 --- a/src/routes/new/source/index.json.ts +++ b/src/routes/sources/new.ts @@ -1,4 +1,4 @@ -import { getUserDetails } from '$lib/common'; +import { getUserDetails, uniqueName } from '$lib/common'; import * as db from '$lib/database'; import { ErrorHandler } from '$lib/database'; import type { RequestHandler } from '@sveltejs/kit'; @@ -7,9 +7,9 @@ export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); if (status === 401) return { status, body }; - const { name, type, htmlUrl, apiUrl, organization } = await event.request.json(); + const name = uniqueName(); try { - const { id } = await db.newSource({ name, teamId, type, htmlUrl, apiUrl, organization }); + const { id } = await db.newSource({ teamId, name }); return { status: 201, body: { id } }; } catch (e) { return ErrorHandler(e); diff --git a/src/routes/teams/index.json.ts b/src/routes/teams/index.json.ts deleted file mode 100644 index cf8ff2583..000000000 --- a/src/routes/teams/index.json.ts +++ /dev/null @@ -1,26 +0,0 @@ -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 { userId, status, body } = await getUserDetails(event, false); - if (status === 401) return { status, body }; - - try { - const teams = await db.prisma.permission.findMany({ - where: { userId }, - include: { team: { include: { _count: { select: { users: true } } } } } - }); - const invitations = await db.prisma.teamInvitation.findMany({ where: { uid: userId } }); - return { - status: 200, - body: { - teams, - invitations - } - }; - } catch (error) { - return ErrorHandler(error); - } -}; diff --git a/src/routes/teams/index.svelte b/src/routes/teams/index.svelte deleted file mode 100644 index 05c020cdf..000000000 --- a/src/routes/teams/index.svelte +++ /dev/null @@ -1,111 +0,0 @@ - - - - -
-
Teams
- {#if $session.isAdmin} - - - - {/if} -
- -{#if invitations.length > 0} -
-
-
Pending invitations
-
-
- {#each invitations as invitation} -
-
- Invited to {invitation.teamName} with - {invitation.permission} permission. -
- - -
- {/each} -
-
-{/if} - diff --git a/src/routes/webhooks/gitlab/events.ts b/src/routes/webhooks/gitlab/events.ts index 4a1eeafcc..f8bb54383 100644 --- a/src/routes/webhooks/gitlab/events.ts +++ b/src/routes/webhooks/gitlab/events.ts @@ -22,6 +22,15 @@ export const post: RequestHandler = async (event) => { const allowedActions = ['opened', 'reopen', 'close', 'open', 'update']; const body = await event.request.json(); const buildId = cuid(); + const webhookToken = event.request.headers.get('x-gitlab-token'); + if (!webhookToken) { + return { + status: 500, + body: { + message: 'Ooops, something is not okay, are you okay?' + } + }; + } try { const { object_kind: objectKind } = body; if (objectKind === 'push') { @@ -77,16 +86,6 @@ export const post: RequestHandler = async (event) => { }; } } else if (objectKind === 'merge_request') { - const webhookToken = event.request.headers.get('x-gitlab-token'); - if (!webhookToken) { - return { - status: 500, - body: { - message: 'Ooops, something is not okay, are you okay?' - } - }; - } - const isDraft = body.object_attributes.work_in_progress; const action = body.object_attributes.action; const projectId = Number(body.project.id);