Merge pull request #834 from coollabsio/next

v3.12.9
This commit is contained in:
Andras Bacsai 2023-01-11 11:00:56 +01:00 committed by GitHub
commit 58af09114b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 2659 additions and 264 deletions

View File

@ -27,7 +27,7 @@ jobs:
context: others/pocketbase/ context: others/pocketbase/
platforms: linux/arm64 platforms: linux/arm64
push: true push: true
tags: coollabsio/pocketbase:0.10.2-arm64 tags: coollabsio/pocketbase:0.11.0-arm64
amd64: amd64:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -48,7 +48,7 @@ jobs:
context: others/pocketbase/ context: others/pocketbase/
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
tags: coollabsio/pocketbase:0.10.2-amd64 tags: coollabsio/pocketbase:0.11.0-amd64
aarch64: aarch64:
runs-on: [self-hosted, arm64] runs-on: [self-hosted, arm64]
steps: steps:
@ -69,7 +69,7 @@ jobs:
context: others/pocketbase/ context: others/pocketbase/
platforms: linux/aarch64 platforms: linux/aarch64
push: true push: true
tags: coollabsio/pocketbase:0.10.2-aarch64 tags: coollabsio/pocketbase:0.11.0-aarch64
merge-manifest: merge-manifest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [amd64, arm64, aarch64] needs: [amd64, arm64, aarch64]
@ -87,5 +87,5 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create & publish manifest - name: Create & publish manifest
run: | run: |
docker manifest create coollabsio/pocketbase:0.10.2 --amend coollabsio/pocketbase:0.10.2-amd64 --amend coollabsio/pocketbase:0.10.2-arm64 --amend coollabsio/pocketbase:0.10.2-aarch64 docker manifest create coollabsio/pocketbase:0.11.0 --amend coollabsio/pocketbase:0.11.0-amd64 --amend coollabsio/pocketbase:0.11.0-arm64 --amend coollabsio/pocketbase:0.11.0-aarch64
docker manifest push coollabsio/pocketbase:0.10.2 docker manifest push coollabsio/pocketbase:0.11.0

1
.gitignore vendored
View File

@ -12,6 +12,7 @@ dist
apps/api/db/*.db apps/api/db/*.db
apps/api/db/migration.db-journal apps/api/db/migration.db-journal
apps/api/core* apps/api/core*
apps/server/build
apps/backup/backups/* apps/backup/backups/*
!apps/backup/backups/.gitkeep !apps/backup/backups/.gitkeep
/logs /logs

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,227 @@
- templateVersion: 1.0.0
defaultVersion: "9.22"
documentation: https://docs.directus.io/getting-started/introduction.html
type: directus-postgresql
name: Directus
subname: (PostgreSQL)
description: >-
Directus is a free and open-source headless CMS framework for managing custom SQL-based databases.
labels:
- CMS
- headless
services:
$$id:
name: Directus
depends_on:
- $$id-postgresql
- $$id-redis
image: directus/directus:$$core_version
volumes:
- $$id-uploads:/directus/uploads
- $$id-database:/directus/database
- $$id-extensions:/directus/extensions
environment:
- KEY=$$secret_key
- SECRET=$$secret_secret
- DB_CLIENT=pg
- DB_CONNECTION_STRING=$$secret_db_connection_string
- CACHE_ENABLED=true
- CACHE_STORE=redis
- CACHE_REDIS=$$secret_cache_redis
- ADMIN_EMAIL=$$config_admin_email
- ADMIN_PASSWORD=$$secret_admin_password
- CACHE_AUTO_PURGE=true
- PUBLIC_URL=$$config_public_url
ports:
- "8055"
$$id-postgresql:
name: Directus PostgreSQL
depends_on: []
image: postgres:14-alpine
volumes:
- $$id-postgresql-data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=$$config_postgres_user
- POSTGRES_PASSWORD=$$secret_postgres_password
- POSTGRES_DB=$$config_postgres_db
ports: []
$$id-redis:
name: Directus Redis
depends_on: []
image: redis:7.0.4-alpine
command: "--maxmemory 512mb --maxmemory-policy allkeys-lru --maxmemory-samples 5"
volumes:
- "$$id-redis:/data"
environment: []
variables:
- id: $$config_public_url
name: PUBLIC_URL
label: Public URL
defaultValue: $$generate_fqdn
description: ""
- id: $$secret_db_connection_string
name: DB_CONNECTION_STRING
label: Directus Database Url
defaultValue: postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db
description: ""
- id: $$config_postgres_db
main: $$id-postgresql
name: POSTGRES_DB
label: Database
defaultValue: directus
description: ""
- id: $$config_postgres_user
main: $$id-postgresql
name: POSTGRES_USER
label: User
defaultValue: $$generate_username
description: ""
- id: $$secret_postgres_password
main: $$id-postgresql
name: POSTGRES_PASSWORD
label: Password
defaultValue: $$generate_password
description: ""
showOnConfiguration: true
- id: $$secret_cache_redis
name: CACHE_REDIS
label: Redis Url
defaultValue: redis://$$id-redis:6379
description: ""
- id: $$config_admin_email
name: ADMIN_EMAIL
label: Initial Admin Email
defaultValue: "admin@example.com"
description: "The email address of the first user that is automatically created. You can change it later in Directus."
- id: $$secret_admin_password
name: ADMIN_PASSWORD
label: Initial Admin Password
defaultValue: $$generate_password
description: "The password of the first user that is automatically created."
showOnConfiguration: true
- id: $$secret_key
name: KEY
label: Key
defaultValue: $$generate_password
description: "Unique identifier for the project."
showOnConfiguration: true
- id: $$secret_secret
name: SECRET
label: Secret
defaultValue: $$generate_password
description: "Secret string for the project."
showOnConfiguration: true
- templateVersion: 1.0.0
defaultVersion: v1.3.8
documentation: https://github.com/LibreTranslate/LibreTranslate
description: Free and Open Source Machine Translation API. 100% self-hosted, offline capable and easy to setup.
type: libretranslate
name: Libretranslate
labels:
- translator
- argos
- python
- libretranslate
services:
$$id:
name: Libretranslate
image: libretranslate/libretranslate:$$core_version
environment:
- LT_HOST=0.0.0.0
- LT_SUGGESTIONS=true
- LT_CHAR_LIMIT=$$config_lt_char_limit
- LT_REQ_LIMIT=$$config_lt_req_limit
- LT_BATCH_LIMIT=$$config_lt_batch_limit
- LT_GA_ID=$$config_lt_ga_id
- LT_DISABLE_WEB_UI=$$config_lt_web_ui
volumes:
- $$id-libretranslate:/libretranslate
ports:
- "5000"
variables:
- id: $$config_lt_char_limit
name: LT_CHAR_LIMIT
label: Char limit
defaultValue: "5000"
description: "Set character limit."
- id: $$config_lt_req_limit
name: LT_REQ_LIMIT
label: Request limit
defaultValue: "5000"
description: "Set maximum number of requests per minute per client."
- id: $$config_lt_batch_limit
name: LT_BATCH_LIMIT
label: Batch Limit
defaultValue: "5000"
description: "Set maximum number of texts to translate in a batch request."
- id: $$config_lt_ga_id
name: LT_GA_ID
label: Google Analytics ID
defaultValue: ""
description: "Enable Google Analytics on the API client page by providing an ID"
- id: $$config_lt_web_ui
name: LT_DISABLE_WEB_UI
label: Web UI
defaultValue: "false"
description: "Disable or enable web ui. True or false."
- templateVersion: 1.0.0
defaultVersion: 0.8.1
documentation: https://github.com/benbusby/whoogle-search
type: whoogle
name: Whoogle Search
description: A self-hosted, ad-free, privacy-respecting metasearch engine
labels:
- search
- google
services:
$$id:
name: Whoogle Search
documentation: https://github.com/benbusby/whoogle-search
depends_on: []
image: benbusby/whoogle-search:$$core_version
cap_drop:
- ALL
environment:
- WHOOGLE_USER=$$config_whoogle_username
- WHOOGLE_PASS=$$secret_whoogle_password
- WHOOGLE_CONFIG_PREFERENCES_KEY=$$secret_whoogle_preferences_key
ulimits:
nofile:
soft: 262144
hard: 262144
ports:
- "5000"
variables:
- id: $$config_whoogle_username
name: WHOOGLE_USER
label: Whoogle User
defaultValue: $$generate_username
description: "Username to log into Whoogle"
- id: $$secret_whoogle_password
name: WHOOGLE_PASSWORD
label: Whoogle Password
defaultValue: $$generate_password
description: "Password to log into Whoogle"
showOnConfiguration: true
- id: $$secret_whoogle_preferences_key
name: WHOOGLE_CONFIG_PREFERENCES_KEY
label: Whoogle preferences key
defaultValue: $$generate_password
description: "password to encrypt preferences"
- templateVersion: 1.0.0
defaultVersion: 1.1.3
documentation: https://docs.openblocks.dev/
type: openblocks
name: Openblocks
description: The Open Source Retool Alternative
services:
$$id:
image: openblocksdev/openblocks-ce:$$core_version
volumes:
- $$id-stacks-data:/openblocks-stacks
ports:
- "3000"
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: "0.10.2" defaultVersion: "0.10.2"
documentation: https://pocketbase.io/docs/ documentation: https://pocketbase.io/docs/
@ -124,12 +348,12 @@
description: "" description: ""
- id: $$config_disable_auth - id: $$config_disable_auth
name: DISABLE_AUTH name: DISABLE_AUTH
label: Disable Authentication label: Authentication
defaultValue: "false" defaultValue: "false"
description: "" description: ""
- id: $$config_disable_registration - id: $$config_disable_registration
name: DISABLE_REGISTRATION name: DISABLE_REGISTRATION
label: Disable Registration label: Registration
defaultValue: "true" defaultValue: "true"
description: "" description: ""
- id: $$config_postgres_user - id: $$config_postgres_user
@ -332,12 +556,12 @@
volumes: volumes:
- $$id-lavalink:/lavalink - $$id-lavalink:/lavalink
ports: ports:
- "2333" - $$config_port
files: files:
- location: /opt/Lavalink/application.yml - location: /opt/Lavalink/application.yml
content: >- content: >-
server: server:
port: $$config_port port: 2333
address: 0.0.0.0 address: 0.0.0.0
lavalink: lavalink:
server: server:
@ -364,11 +588,6 @@
max-file-size: 1GB max-file-size: 1GB
max-history: 30 max-history: 30
variables: variables:
- id: $$config_port
name: PORT
label: Port
defaultValue: "2333"
required: true
- id: $$secret_password - id: $$secret_password
name: PASSWORD name: PASSWORD
label: Password label: Password
@ -3250,12 +3469,12 @@
description: "" description: ""
- id: $$config_disable_auth - id: $$config_disable_auth
name: DISABLE_AUTH name: DISABLE_AUTH
label: Disable Authentication label: Authentication
defaultValue: "false" defaultValue: "false"
description: "" description: ""
- id: $$config_disable_registration - id: $$config_disable_registration
name: DISABLE_REGISTRATION name: DISABLE_REGISTRATION
label: Disable Registration label: Registration
defaultValue: "true" defaultValue: "true"
description: "" description: ""
- id: $$config_postgresql_username - id: $$config_postgresql_username

View File

@ -48,7 +48,7 @@
"is-ip": "5.0.0", "is-ip": "5.0.0",
"is-port-reachable": "4.0.0", "is-port-reachable": "4.0.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "9.0.0",
"minimist": "^1.2.7", "minimist": "^1.2.7",
"node-forge": "1.3.1", "node-forge": "1.3.1",
"node-os-utils": "1.3.7", "node-os-utils": "1.3.7",

View File

@ -0,0 +1,27 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_GitSource" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"forPublic" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT,
"apiUrl" TEXT,
"htmlUrl" TEXT,
"customPort" INTEGER NOT NULL DEFAULT 22,
"customUser" TEXT NOT NULL DEFAULT 'git',
"organization" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"githubAppId" TEXT,
"gitlabAppId" TEXT,
"isSystemWide" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "GitSource_gitlabAppId_fkey" FOREIGN KEY ("gitlabAppId") REFERENCES "GitlabApp" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "GitSource_githubAppId_fkey" FOREIGN KEY ("githubAppId") REFERENCES "GithubApp" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_GitSource" ("apiUrl", "createdAt", "customPort", "forPublic", "githubAppId", "gitlabAppId", "htmlUrl", "id", "isSystemWide", "name", "organization", "type", "updatedAt") SELECT "apiUrl", "createdAt", "customPort", "forPublic", "githubAppId", "gitlabAppId", "htmlUrl", "id", "isSystemWide", "name", "organization", "type", "updatedAt" FROM "GitSource";
DROP TABLE "GitSource";
ALTER TABLE "new_GitSource" RENAME TO "GitSource";
CREATE UNIQUE INDEX "GitSource_githubAppId_key" ON "GitSource"("githubAppId");
CREATE UNIQUE INDEX "GitSource_gitlabAppId_key" ON "GitSource"("gitlabAppId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@ -325,6 +325,7 @@ model GitSource {
apiUrl String? apiUrl String?
htmlUrl String? htmlUrl String?
customPort Int @default(22) customPort Int @default(22)
customUser String @default("git")
organization String? organization String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

View File

@ -156,7 +156,7 @@ const host = '0.0.0.0';
graceful.listen(); graceful.listen();
setInterval(async () => { setInterval(async () => {
if (!scheduler.workers.has('deployApplication')) { if (!scheduler.workers.has('deployApplication')) {
scheduler.run('deployApplication'); scheduler.run('deployApplication');
} }
}, 2000); }, 2000);

View File

@ -419,6 +419,7 @@ import * as buildpacks from '../lib/buildPacks';
githubAppId: gitSource.githubApp?.id, githubAppId: gitSource.githubApp?.id,
gitlabAppId: gitSource.gitlabApp?.id, gitlabAppId: gitSource.gitlabApp?.id,
customPort: gitSource.customPort, customPort: gitSource.customPort,
customUser: gitSource.customUser,
gitCommitHash, gitCommitHash,
configuration, configuration,
repository, repository,

View File

@ -1,4 +1,5 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildCacheImageForLaravel, buildImage } from './common'; import { buildCacheImageForLaravel, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
@ -7,6 +8,11 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
Dockerfile.push(env);
});
}
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`ENV WEB_DOCUMENT_ROOT /app/public`); Dockerfile.push(`ENV WEB_DOCUMENT_ROOT /app/public`);
Dockerfile.push(`COPY --chown=application:application composer.* ./`); Dockerfile.push(`COPY --chown=application:application composer.* ./`);

View File

@ -19,7 +19,7 @@ import { saveBuildLog, saveDockerRegistryCredentials } from './buildPacks/common
import { scheduler } from './scheduler'; import { scheduler } from './scheduler';
import type { ExecaChildProcess } from 'execa'; import type { ExecaChildProcess } from 'execa';
export const version = '3.12.8'; export const version = '3.12.9';
export const isDev = process.env.NODE_ENV === 'development'; export const isDev = process.env.NODE_ENV === 'development';
export const sentryDSN = export const sentryDSN =
'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216'; 'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216';
@ -1884,6 +1884,30 @@ export async function pushToRegistry(
}); });
} }
function parseSecret(secret, isBuild) {
if (secret.value.includes('$')) {
secret.value = secret.value.replaceAll('$', '$$$$');
}
if (secret.value.includes('\\n')) {
if (isBuild) {
return `ARG ${secret.name}=${secret.value}`;
} else {
return `${secret.name}=${secret.value}`;
}
} else if (secret.value.includes(' ')) {
if (isBuild) {
return `ARG ${secret.name}='${secret.value}'`;
} else {
return `${secret.name}='${secret.value}'`;
}
} else {
if (isBuild) {
return `ARG ${secret.name}=${secret.value}`;
} else {
return `${secret.name}=${secret.value}`;
}
}
}
export function generateSecrets( export function generateSecrets(
secrets: Array<any>, secrets: Array<any>,
pullmergeRequestId: string, pullmergeRequestId: string,
@ -1899,15 +1923,7 @@ export function generateSecrets(
return; return;
} }
const build = isBuild && secret.isBuildSecret; const build = isBuild && secret.isBuildSecret;
if (build) { envs.push(parseSecret(secret, build));
if (secret.value.includes(' ') || secret.value.includes('\\n')) {
envs.push(`ARG ${secret.name}='${secret.value}'`);
} else {
envs.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
envs.push(`${secret.name}=${secret.value}`);
}
}); });
} }
if (!pullmergeRequestId && normalSecrets.length > 0) { if (!pullmergeRequestId && normalSecrets.length > 0) {
@ -1916,15 +1932,7 @@ export function generateSecrets(
return; return;
} }
const build = isBuild && secret.isBuildSecret; const build = isBuild && secret.isBuildSecret;
if (build) { envs.push(parseSecret(secret, build));
if (secret.value.includes(' ') || secret.value.includes('\\n')) {
envs.push(`ARG ${secret.name}='${secret.value}'`);
} else {
envs.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
envs.push(`${secret.name}=${secret.value}`);
}
}); });
} }
const portFound = envs.filter((env) => env.startsWith('PORT')); const portFound = envs.filter((env) => env.startsWith('PORT'));

View File

@ -12,7 +12,8 @@ export default async function ({
buildId, buildId,
privateSshKey, privateSshKey,
customPort, customPort,
forPublic forPublic,
customUser,
}: { }: {
applicationId: string; applicationId: string;
workdir: string; workdir: string;
@ -25,6 +26,7 @@ export default async function ({
privateSshKey: string; privateSshKey: string;
customPort: number; customPort: number;
forPublic: boolean; forPublic: boolean;
customUser: string;
}): Promise<string> { }): Promise<string> {
const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, ''); const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
if (!forPublic) { if (!forPublic) {
@ -53,7 +55,7 @@ export default async function ({
} else { } else {
await executeCommand({ await executeCommand({
command: command:
`git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, shell: true `git clone -q -b ${branch} ${customUser}@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, shell: true
} }
); );
} }

View File

@ -764,7 +764,9 @@ export async function checkDomain(request: FastifyRequest<CheckDomain>) {
fqdn, fqdn,
settings: { dualCerts } settings: { dualCerts }
} = await prisma.application.findUnique({ where: { id }, include: { settings: true } }); } = await prisma.application.findUnique({ where: { id }, include: { settings: true } });
return await checkDomainsIsValidInDNS({ hostname: domain, fqdn, dualCerts }); // TODO: Disabled this because it is having problems with remote docker engines.
// return await checkDomainsIsValidInDNS({ hostname: domain, fqdn, dualCerts });
return {};
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }); return errorHandler({ status, message });
} }
@ -805,11 +807,12 @@ export async function checkDNS(request: FastifyRequest<CheckDNS>) {
remoteEngine, remoteEngine,
remoteIpAddress remoteIpAddress
}); });
if (isDNSCheckEnabled && !isDev && !forceSave) { // TODO: Disabled this because it is having problems with remote docker engines.
let hostname = request.hostname.split(':')[0]; // if (isDNSCheckEnabled && !isDev && !forceSave) {
if (remoteEngine) hostname = remoteIpAddress; // let hostname = request.hostname.split(':')[0];
return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }); // if (remoteEngine) hostname = remoteIpAddress;
} // return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
// }
return {}; return {};
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }); return errorHandler({ status, message });
@ -842,15 +845,16 @@ export async function getDockerImages(request) {
try { try {
const { stdout } = await executeCommand({ const { stdout } = await executeCommand({
dockerId: application.destinationDocker.id, dockerId: application.destinationDocker.id,
command: `docker images --format '{{.Repository}}#{{.Tag}}#{{.CreatedAt}}' | grep -i ${id} | grep -v cache`, command: `docker images --format '{{.Repository}}#{{.Tag}}#{{.CreatedAt}}'`
shell: true
}); });
const { stdout: runningImage } = await executeCommand({ const { stdout: runningImage } = await executeCommand({
dockerId: application.destinationDocker.id, dockerId: application.destinationDocker.id,
command: `docker ps -a --filter 'label=com.docker.compose.service=${id}' --format {{.Image}}` command: `docker ps -a --filter 'label=com.docker.compose.service=${id}' --format {{.Image}}`
}); });
const images = stdout.trim().split('\n'); const images = stdout
.trim()
.split('\n')
.filter((image) => image.includes(id) && !image.includes('-cache'));
for (const image of images) { for (const image of images) {
const [repository, tag, createdAt] = image.split('#'); const [repository, tag, createdAt] = image.split('#');
if (tag.includes('-')) { if (tag.includes('-')) {
@ -871,6 +875,7 @@ export async function getDockerImages(request) {
runningImage runningImage
}; };
} catch (error) { } catch (error) {
console.log(error);
return { return {
imagesAvailables imagesAvailables
}; };

View File

@ -263,14 +263,14 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
for (const secret of service.serviceSecret) { for (const secret of service.serviceSecret) {
let { name, value } = secret let { name, value } = secret
name = name.toLowerCase() name = name.toLowerCase()
const regexHashed = new RegExp(`\\$\\$hashed\\$\\$secret_${name}\"`, 'gi') const regexHashed = new RegExp(`\\$\\$hashed\\$\\$secret_${name}`, 'gi')
const regex = new RegExp(`\\$\\$secret_${name}\"`, 'gi') const regex = new RegExp(`\\$\\$secret_${name}`, 'gi')
if (value) { if (value) {
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value.replaceAll("\"", "\\\""), 10) + '"') strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value.replaceAll("\"", "\\\""), 10))
strParsedTemplate = strParsedTemplate.replaceAll(regex, value.replaceAll("\"", "\\\"") + '"') strParsedTemplate = strParsedTemplate.replaceAll(regex, value.replaceAll("\"", "\\\""))
} else { } else {
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, '' + '"') strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, '')
strParsedTemplate = strParsedTemplate.replaceAll(regex, '' + '"') strParsedTemplate = strParsedTemplate.replaceAll(regex, '')
} }
} }
} }
@ -504,7 +504,9 @@ export async function checkServiceDomain(request: FastifyRequest<CheckServiceDom
const { id } = request.params const { id } = request.params
const { domain } = request.query const { domain } = request.query
const { fqdn, dualCerts } = await prisma.service.findUnique({ where: { id } }) const { fqdn, dualCerts } = await prisma.service.findUnique({ where: { id } })
return await checkDomainsIsValidInDNS({ hostname: domain, fqdn, dualCerts }); // TODO: Disabled this because it is having problems with remote docker engines.
// return await checkDomainsIsValidInDNS({ hostname: domain, fqdn, dualCerts });
return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
@ -530,11 +532,12 @@ export async function checkService(request: FastifyRequest<CheckService>) {
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` } throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
} }
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }) if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
if (isDNSCheckEnabled && !isDev && !forceSave) { // TODO: Disabled this because it is having problems with remote docker engines.
let hostname = request.hostname.split(':')[0]; // if (isDNSCheckEnabled && !isDev && !forceSave) {
if (remoteEngine) hostname = remoteIpAddress; // let hostname = request.hostname.split(':')[0];
return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }); // if (remoteEngine) hostname = remoteIpAddress;
} // return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
// }
return {} return {}
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })

View File

@ -22,11 +22,11 @@ export async function listSources(request: FastifyRequest) {
export async function saveSource(request, reply) { export async function saveSource(request, reply) {
try { try {
const { id } = request.params const { id } = request.params
let { name, htmlUrl, apiUrl, customPort, isSystemWide } = request.body let { name, htmlUrl, apiUrl, customPort, customUser, isSystemWide } = request.body
if (customPort) customPort = Number(customPort) if (customPort) customPort = Number(customPort)
await prisma.gitSource.update({ await prisma.gitSource.update({
where: { id }, where: { id },
data: { name, htmlUrl, apiUrl, customPort, isSystemWide } data: { name, htmlUrl, apiUrl, customPort, customUser, isSystemWide }
}); });
return reply.code(201).send() return reply.code(201).send()
} catch ({ status, message }) { } catch ({ status, message }) {
@ -48,6 +48,7 @@ export async function getSource(request: FastifyRequest<OnlyId>) {
apiUrl: null, apiUrl: null,
organization: null, organization: null,
customPort: 22, customPort: 22,
customUser: 'git',
}, },
settings settings
} }
@ -133,7 +134,7 @@ export async function saveGitLabSource(request: FastifyRequest<SaveGitLabSource>
try { try {
const { id } = request.params const { id } = request.params
const { teamId } = request.user const { teamId } = request.user
let { type, name, htmlUrl, apiUrl, oauthId, appId, appSecret, groupName, customPort } = let { type, name, htmlUrl, apiUrl, oauthId, appId, appSecret, groupName, customPort, customUser } =
request.body request.body
if (oauthId) oauthId = Number(oauthId); if (oauthId) oauthId = Number(oauthId);
@ -142,7 +143,7 @@ export async function saveGitLabSource(request: FastifyRequest<SaveGitLabSource>
if (id === 'new') { if (id === 'new') {
const newId = cuid() const newId = cuid()
await prisma.gitSource.create({ data: { id: newId, type, apiUrl, htmlUrl, name, customPort, teams: { connect: { id: teamId } } } }); await prisma.gitSource.create({ data: { id: newId, type, apiUrl, htmlUrl, name, customPort, customUser, teams: { connect: { id: teamId } } } });
await prisma.gitlabApp.create({ await prisma.gitlabApp.create({
data: { data: {
teams: { connect: { id: teamId } }, teams: { connect: { id: teamId } },
@ -158,7 +159,7 @@ export async function saveGitLabSource(request: FastifyRequest<SaveGitLabSource>
id: newId id: newId
} }
} else { } else {
await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name, customPort } }); await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name, customPort, customUser } });
await prisma.gitlabApp.update({ await prisma.gitlabApp.update({
where: { id }, where: { id },
data: { data: {

View File

@ -21,6 +21,7 @@ export interface SaveGitLabSource extends OnlyId {
appSecret: string, appSecret: string,
groupName: string, groupName: string,
customPort: number, customPort: number,
customUser: string,
} }
} }
export interface CheckGitLabOAuthId extends OnlyId { export interface CheckGitLabOAuthId extends OnlyId {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -352,11 +352,12 @@
async function reloadCompose() { async function reloadCompose() {
if (loading.reloadCompose) return; if (loading.reloadCompose) return;
loading.reloadCompose = true; loading.reloadCompose = true;
const composeLocation = application.dockerComposeFileLocation.startsWith('/')
? application.dockerComposeFileLocation
: `/${application.dockerComposeFileLocation}`;
try { try {
if (application.gitSource.type === 'github') { if (application.gitSource.type === 'github') {
const composeLocation = application.dockerComposeFileLocation.startsWith('/')
? application.dockerComposeFileLocation
: `/${application.dockerComposeFileLocation}`;
const headers = isPublicRepository const headers = isPublicRepository
? {} ? {}
: { : {
@ -383,6 +384,17 @@
if (!$appSession.tokens.gitlab) { if (!$appSession.tokens.gitlab) {
await getGitlabToken(); await getGitlabToken();
} }
const composeLocation = application.dockerComposeFileLocation.startsWith('/')
? application.dockerComposeFileLocation.substring(1) // Remove the '/' from the start
: application.dockerComposeFileLocation;
// If the file is in a subdirectory, lastIndex will be > 0
// Otherwise it will be -1 and path will be an empty string
const lastIndex = composeLocation.lastIndexOf('/') + 1
const path = composeLocation.substring(0, lastIndex)
const fileName = composeLocation.substring(lastIndex)
const headers = isPublicRepository const headers = isPublicRepository
? {} ? {}
: { : {
@ -390,13 +402,13 @@
}; };
const url = isPublicRepository const url = isPublicRepository
? `` ? ``
: `/v4/projects/${application.projectId}/repository/tree`; : `/v4/projects/${application.projectId}/repository/tree?path=${path}`; // Use path param to find file in a subdirectory
const files = await get(`${apiUrl}${url}`, { const files = await get(`${apiUrl}${url}`, {
...headers ...headers
}); });
const dockerComposeFileYml = files.find( const dockerComposeFileYml = files.find(
(file: { name: string; type: string }) => (file: { name: string; type: string }) =>
file.name === composeLocation && file.type === 'blob' file.name === fileName && file.type === 'blob'
); );
const id = dockerComposeFileYml.id; const id = dockerComposeFileYml.id;

7
apps/server/nodemon.json Normal file
View File

@ -0,0 +1,7 @@
{
"watch": ["src"],
"ignore": ["src/**/*.test.ts"],
"ext": "ts,mjs,json,graphql",
"exec": "rimraf build && esbuild `find src \\( -name '*.ts' -o -name '*.js' \\)` --platform=node --outdir=build --format=cjs && node build",
"legacyWatch": true
}

View File

@ -4,7 +4,8 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {
"build": "rimraf ../../build && tsc --outDir ../../build", "build": "rimraf ../../build && tsc --outDir ../../build",
"dev": "tsx watch --clear-screen=false src", "dev-old": "tsx watch --clear-screen=false src",
"dev": "nodemon",
"lint": "prettier --plugin-search-dir . --check . && eslint .", "lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write .", "format": "prettier --plugin-search-dir . --write .",
"test-dev": "start-server-and-test 'tsx src/server' http-get://localhost:2022 'tsx src/client'", "test-dev": "start-server-and-test 'tsx src/server' http-get://localhost:2022 'tsx src/client'",
@ -43,7 +44,7 @@
"is-ip": "5.0.0", "is-ip": "5.0.0",
"is-port-reachable": "4.0.0", "is-port-reachable": "4.0.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "9.0.0",
"node-fetch": "3.3.0", "node-fetch": "3.3.0",
"p-all": "4.0.0", "p-all": "4.0.0",
"p-throttle": "5.0.0", "p-throttle": "5.0.0",
@ -65,6 +66,8 @@
"@types/node-fetch": "2.6.2", "@types/node-fetch": "2.6.2",
"@types/shell-quote": "^1.7.1", "@types/shell-quote": "^1.7.1",
"@types/ws": "8.5.3", "@types/ws": "8.5.3",
"esbuild": "0.15.15",
"nodemon": "2.0.20",
"npm-run-all": "4.1.5", "npm-run-all": "4.1.5",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"start-server-and-test": "1.14.0", "start-server-and-test": "1.14.0",

View File

@ -325,6 +325,7 @@ model GitSource {
apiUrl String? apiUrl String?
htmlUrl String? htmlUrl String?
customPort Int @default(22) customPort Int @default(22)
customUser String @default("git")
organization String? organization String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

View File

@ -1,6 +1,6 @@
import Bree from 'bree'; import Bree from 'bree';
import path from 'path'; import path from 'path';
import Cabin from 'cabin'; // import Cabin from 'cabin';
import TSBree from '@breejs/ts-worker'; import TSBree from '@breejs/ts-worker';
export const isDev = process.env['NODE_ENV'] === 'development'; export const isDev = process.env['NODE_ENV'] === 'development';
@ -9,7 +9,7 @@ Bree.extend(TSBree);
const options: any = { const options: any = {
defaultExtension: 'js', defaultExtension: 'js',
logger: new Cabin(), logger: false,
// logger: false, // logger: false,
// workerMessageHandler: async ({ name, message }) => { // workerMessageHandler: async ({ name, message }) => {
// if (name === 'deployApplication' && message?.deploying) { // if (name === 'deployApplication' && message?.deploying) {
@ -18,9 +18,8 @@ const options: any = {
// } // }
// } // }
// }, // },
// jobs: [{ name: 'deployApplication' }] jobs: [{ name: 'deployApplication' }, { name: 'worker' }]
jobs: [{ name: 'worker' }]
}; };
if (isDev) options.root = path.join(__dirname, '../jobs'); if (isDev) options.root = path.join(__dirname, './jobs');
export const scheduler = new Bree(options); export const scheduler = new Bree(options);

View File

@ -5,6 +5,7 @@
const handleError = (ev: { target: { src: string } }) => (ev.target.src = fallback); const handleError = (ev: { target: { src: string } }) => (ev.target.src = fallback);
let extension = 'png'; let extension = 'png';
let svgs = [ let svgs = [
'directus',
'pocketbase', 'pocketbase',
'gitea', 'gitea',
'languagetool', 'languagetool',

View File

@ -58,11 +58,10 @@ export const appSession: Writable<AppSession> = writable({
}); });
export const disabledButton: Writable<boolean> = writable(false); export const disabledButton: Writable<boolean> = writable(false);
export const isDeploymentEnabled: Writable<boolean> = writable(false); export const isDeploymentEnabled: Writable<boolean> = writable(false);
export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) { export function checkIfDeploymentEnabledApplications(application: any) {
return !!( return !!(
isAdmin &&
(application.buildPack === 'compose') || (application.buildPack === 'compose') ||
(application.fqdn || application.settings.isBot) && (application.fqdn || application.settings?.isBot) &&
((application.gitSource && ((application.gitSource &&
application.repository && application.repository &&
application.buildPack) || application.simpleDockerfile) && application.buildPack) || application.simpleDockerfile) &&
@ -70,9 +69,8 @@ export function checkIfDeploymentEnabledApplications(isAdmin: boolean, applicati
); );
} }
export function checkIfDeploymentEnabledServices(isAdmin: boolean, service: any) { export function checkIfDeploymentEnabledServices( service: any) {
return ( return (
isAdmin &&
service.fqdn && service.fqdn &&
service.destinationDocker && service.destinationDocker &&
service.version && service.version &&

View File

@ -162,12 +162,12 @@
<input id="main-drawer" type="checkbox" class="drawer-toggle" bind:this={sidedrawerToggler} /> <input id="main-drawer" type="checkbox" class="drawer-toggle" bind:this={sidedrawerToggler} />
<div class="drawer-content"> <div class="drawer-content">
{#if $appSession.userId} {#if $appSession.userId}
<Tooltip triggeredBy="#dashboard" placement="right" color="bg-pink-500">Dashboard</Tooltip>
<Tooltip triggeredBy="#servers" placement="right" color="bg-sky-500">Servers</Tooltip>
<Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</Tooltip> <Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</Tooltip>
<Tooltip triggeredBy="#settings" placement="right" color="bg-settings text-black" <Tooltip triggeredBy="#settings" placement="right" color="bg-settings text-black"
>Settings</Tooltip >Settings</Tooltip
> >
<Tooltip triggeredBy="#documentation" placement="right" color="bg-info">Documentation</Tooltip
>
<Tooltip triggeredBy="#logout" placement="right" color="bg-red-600">Logout</Tooltip> <Tooltip triggeredBy="#logout" placement="right" color="bg-red-600">Logout</Tooltip>
<nav class="nav-main hidden lg:block z-20"> <nav class="nav-main hidden lg:block z-20">
<div class="flex h-screen w-full flex-col items-center transition-all duration-100"> <div class="flex h-screen w-full flex-col items-center transition-all duration-100">
@ -183,7 +183,6 @@
<div class="flex flex-col space-y-2 py-2" class:mt-2={$appSession.whiteLabeled}> <div class="flex flex-col space-y-2 py-2" class:mt-2={$appSession.whiteLabeled}>
<a <a
id="dashboard" id="dashboard"
sveltekit:prefetch
href="/" href="/"
class="icons hover:text-pink-500" class="icons hover:text-pink-500"
class:text-pink-500={$page.url.pathname === '/'} class:text-pink-500={$page.url.pathname === '/'}
@ -210,7 +209,6 @@
{#if $appSession.teamId === '0'} {#if $appSession.teamId === '0'}
<a <a
id="servers" id="servers"
sveltekit:prefetch
href="/servers" href="/servers"
class="icons hover:text-sky-500" class="icons hover:text-sky-500"
class:text-sky-500={$page.url.pathname === '/servers'} class:text-sky-500={$page.url.pathname === '/servers'}
@ -236,8 +234,6 @@
</a> </a>
{/if} {/if}
</div> </div>
<Tooltip triggeredBy="#dashboard" placement="right">Dashboard</Tooltip>
<Tooltip triggeredBy="#servers" placement="right">Servers</Tooltip>
<div class="flex-1" /> <div class="flex-1" />
<div class="lg:block hidden"> <div class="lg:block hidden">
<UpdateAvailable /> <UpdateAvailable />
@ -245,7 +241,6 @@
<div class="flex flex-col space-y-2 py-2"> <div class="flex flex-col space-y-2 py-2">
<a <a
id="iam" id="iam"
sveltekit:prefetch
href={$appSession.pendingInvitations.length > 0 ? '/iam/pending' : '/iam'} href={$appSession.pendingInvitations.length > 0 ? '/iam/pending' : '/iam'}
class="icons hover:text-iam indicator" class="icons hover:text-iam indicator"
class:text-iam={$page.url.pathname.startsWith('/iam')} class:text-iam={$page.url.pathname.startsWith('/iam')}
@ -274,7 +269,6 @@
</a> </a>
<a <a
id="settings" id="settings"
sveltekit:prefetch
href={$appSession.teamId === '0' ? '/settings/coolify' : '/settings/docker'} href={$appSession.teamId === '0' ? '/settings/coolify' : '/settings/docker'}
class="icons hover:text-settings" class="icons hover:text-settings"
class:text-settings={$page.url.pathname.startsWith('/settings')} class:text-settings={$page.url.pathname.startsWith('/settings')}
@ -299,10 +293,9 @@
</a> </a>
<a <a
id="documentation" id="documentation"
sveltekit:prefetch
href="https://docs.coollabs.io/coolify/" href="https://docs.coollabs.io/coolify/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noreferrer external"
class="icons hover:text-info" class="icons hover:text-info"
> >
<svg <svg
@ -395,7 +388,6 @@
<li> <li>
<a <a
class="no-underline icons hover:text-white hover:bg-pink-500" class="no-underline icons hover:text-white hover:bg-pink-500"
sveltekit:prefetch
href="/" href="/"
class:bg-pink-500={$page.url.pathname === '/'} class:bg-pink-500={$page.url.pathname === '/'}
on:click={closeDrawer} on:click={closeDrawer}
@ -424,7 +416,6 @@
<a <a
id="servers" id="servers"
class="no-underline icons hover:text-white hover:bg-sky-500" class="no-underline icons hover:text-white hover:bg-sky-500"
sveltekit:prefetch
href="/servers" href="/servers"
class:bg-sky-500={$page.url.pathname.startsWith('/servers')} class:bg-sky-500={$page.url.pathname.startsWith('/servers')}
on:click={closeDrawer} on:click={closeDrawer}
@ -504,6 +495,30 @@
Settings Settings
</a> </a>
</li> </li>
<li>
<a
class="no-underline icons hover:text-white hover:bg-info"
href="https://docs.coollabs.io/coolify/"
target="_blank"
rel="noreferrer external"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-8 h-8"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"
/>
</svg>
Documentation
</a>
</li>
<li class="flex-1 bg-transparent" /> <li class="flex-1 bg-transparent" />
<div class="block lg:hidden"> <div class="block lg:hidden">
<UpdateAvailable /> <UpdateAvailable />

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
export let application: any; export let application: any;
import { status } from '$lib/store'; import { appSession, status } from '$lib/store';
import { page } from '$app/stores'; import { page } from '$app/stores';
</script> </script>
@ -220,7 +220,7 @@
<li class="menu-title"> <li class="menu-title">
<span>Advanced</span> <span>Advanced</span>
</li> </li>
{#if application.gitSourceId} {#if application.gitSourceId && $appSession.isAdmin}
<li <li
class="rounded" class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/revert`} class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/revert`}
@ -295,6 +295,7 @@
> >
</li> </li>
{/if} {/if}
{#if $appSession.isAdmin}
<li <li
class="rounded" class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/danger`} class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/danger`}
@ -318,4 +319,5 @@
</svg>Danger Zone</a </svg>Danger Zone</a
> >
</li> </li>
{/if}
</ul> </ul>

View File

@ -9,7 +9,7 @@
import { del, post, put } from '$lib/api'; import { del, post, put } from '$lib/api';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { addToast } from '$lib/store'; import { addToast, appSession } from '$lib/store';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
@ -120,6 +120,7 @@
<label for="name" class="pb-5 uppercase lg:block hidden font-bold" /> <label for="name" class="pb-5 uppercase lg:block hidden font-bold" />
{/if} {/if}
{#if $appSession.isAdmin}
<div class="flex justify-center h-full items-center pt-3"> <div class="flex justify-center h-full items-center pt-3">
<div class="flex flex-row justify-center space-x-2"> <div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
@ -127,5 +128,6 @@
</div> </div>
</div> </div>
</div> </div>
{/if}
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@
import { del, post, put } from '$lib/api'; import { del, post, put } from '$lib/api';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { addToast } from '$lib/store'; import { addToast, appSession } from '$lib/store';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
@ -124,8 +124,9 @@
<div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0"> <div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
<button <button
on:click={() => updateSecret({ changeIsBuildSecret: true })} on:click={() => updateSecret({ changeIsBuildSecret: true })}
disabled={!$appSession.isAdmin}
aria-pressed="false" aria-pressed="false"
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out " class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out"
class:bg-green-600={isBuildSecret} class:bg-green-600={isBuildSecret}
class:bg-stone-700={!isBuildSecret} class:bg-stone-700={!isBuildSecret}
> >
@ -177,7 +178,7 @@
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={addNewSecret}>Add</button> <button class="btn btn-sm btn-primary" on:click={addNewSecret}>Add</button>
</div> </div>
{:else} {:else if $appSession.isAdmin}
<div class="flex flex-row justify-center space-x-2"> <div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={() => updateSecret()}>Set</button> <button class="btn btn-sm btn-primary" on:click={() => updateSecret()}>Set</button>

View File

@ -83,7 +83,7 @@
let forceDelete = false; let forceDelete = false;
let stopping = false; let stopping = false;
const { id } = $page.params; const { id } = $page.params;
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications(application);
async function deleteApplication(name: string, force: boolean) { async function deleteApplication(name: string, force: boolean) {
const sure = confirm($t('application.confirm_to_delete', { name })); const sure = confirm($t('application.confirm_to_delete', { name }));
@ -292,7 +292,6 @@
<a <a
href={$isDeploymentEnabled ? `/applications/${id}/logs` : null} href={$isDeploymentEnabled ? `/applications/${id}/logs` : null}
class="btn btn-sm text-sm gap-2" class="btn btn-sm text-sm gap-2"
sveltekit:prefetch
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -363,7 +362,7 @@
<button <button
on:click={restartApplication} on:click={restartApplication}
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
class="btn btn-sm gap-2" class="btn btn-sm gap-2"
> >
<svg <svg
@ -383,7 +382,7 @@
</button> </button>
{/if} {/if}
<button <button
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
class="btn btn-sm gap-2" class="btn btn-sm gap-2"
on:click={() => handleDeploySubmit(true)} on:click={() => handleDeploySubmit(true)}
> >
@ -409,7 +408,7 @@
<button <button
on:click={stopApplication} on:click={stopApplication}
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
class="btn btn-sm gap-2" class="btn btn-sm gap-2"
> >
<svg <svg
@ -432,7 +431,7 @@
<button <button
on:click={stopApplication} on:click={stopApplication}
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
class="btn btn-sm gap-2" class="btn btn-sm gap-2"
> >
<svg <svg
@ -453,7 +452,7 @@
{/if} {/if}
<button <button
class="btn btn-sm gap-2" class="btn btn-sm gap-2"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
on:click={() => handleDeploySubmit(true)} on:click={() => handleDeploySubmit(true)}
> >
{#if $status.application.overallStatus !== 'degraded'} {#if $status.application.overallStatus !== 'degraded'}

View File

@ -72,7 +72,6 @@
<div class="flex justify-center"> <div class="flex justify-center">
<a <a
href={`/destinations/new?from=/applications/${id}/configuration/destination`} href={`/destinations/new?from=/applications/${id}/configuration/destination`}
sveltekit:prefetch
class="add-icon bg-sky-600 hover:bg-sky-500" class="add-icon bg-sky-600 hover:bg-sky-500"
> >
<svg <svg

View File

@ -255,7 +255,7 @@
{/if} {/if}
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<div class="title py-4 pr-4">Public Repository from Git</div> <div class="title py-4 pr-4">Public Repository from Git</div>
<DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository" /> <DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository-from-git" />
</div> </div>
<PublicRepository /> <PublicRepository />
<div class="flex flex-row items-center pt-10"> <div class="flex flex-row items-center pt-10">

View File

@ -51,6 +51,7 @@
let isDBBranching = application.settings.isDBBranching; let isDBBranching = application.settings.isDBBranching;
async function changeSettings(name: any) { async function changeSettings(name: any) {
if (!$appSession.isAdmin) return
if (name === 'previews') { if (name === 'previews') {
previews = !previews; previews = !previews;
} }
@ -102,7 +103,7 @@
} }
return errorNotification(error); return errorNotification(error);
} finally { } finally {
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications(application);
} }
} }
</script> </script>
@ -119,6 +120,7 @@
id="autodeploy" id="autodeploy"
isCenter={false} isCenter={false}
bind:setting={autodeploy} bind:setting={autodeploy}
disabled={!$appSession.isAdmin}
on:click={() => changeSettings('autodeploy')} on:click={() => changeSettings('autodeploy')}
title={$t('application.enable_automatic_deployment')} title={$t('application.enable_automatic_deployment')}
description={$t('application.enable_auto_deploy_webhooks')} description={$t('application.enable_auto_deploy_webhooks')}
@ -130,6 +132,7 @@
id="previews" id="previews"
isCenter={false} isCenter={false}
bind:setting={previews} bind:setting={previews}
disabled={!$appSession.isAdmin}
on:click={() => changeSettings('previews')} on:click={() => changeSettings('previews')}
title={$t('application.enable_mr_pr_previews')} title={$t('application.enable_mr_pr_previews')}
description={$t('application.enable_preview_deploy_mr_pr_requests')} description={$t('application.enable_preview_deploy_mr_pr_requests')}

View File

@ -58,7 +58,7 @@
$status.application.overallStatus === 'degraded' || $status.application.overallStatus === 'degraded' ||
$status.application.overallStatus === 'healthy' || $status.application.overallStatus === 'healthy' ||
$status.application.initialLoading; $status.application.initialLoading;
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications(application);
let statues: any = {}; let statues: any = {};
let loading = { let loading = {
save: false, save: false,
@ -235,7 +235,7 @@
} }
return errorNotification(error); return errorNotification(error);
} finally { } finally {
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications(application);
} }
} }
async function handleSubmit(toast: boolean = true) { async function handleSubmit(toast: boolean = true) {
@ -269,7 +269,7 @@
} }
await saveForm(id, application, baseDatabaseBranch, dockerComposeConfiguration); await saveForm(id, application, baseDatabaseBranch, dockerComposeConfiguration);
setLocation(application, settings); setLocation(application, settings);
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications(application);
forceSave = false; forceSave = false;
if (toast) { if (toast) {
@ -366,11 +366,12 @@
async function reloadCompose() { async function reloadCompose() {
if (loading.reloadCompose) return; if (loading.reloadCompose) return;
loading.reloadCompose = true; loading.reloadCompose = true;
const composeLocation = application.dockerComposeFileLocation.startsWith('/')
? application.dockerComposeFileLocation
: `/${application.dockerComposeFileLocation}`;
try { try {
if (application.gitSource.type === 'github') { if (application.gitSource.type === 'github') {
const composeLocation = application.dockerComposeFileLocation.startsWith('/')
? application.dockerComposeFileLocation
: `/${application.dockerComposeFileLocation}`;
const headers = isPublicRepository const headers = isPublicRepository
? {} ? {}
: { : {
@ -397,6 +398,17 @@
if (!$appSession.tokens.gitlab) { if (!$appSession.tokens.gitlab) {
await getGitlabToken(); await getGitlabToken();
} }
const composeLocation = application.dockerComposeFileLocation.startsWith('/')
? application.dockerComposeFileLocation.substring(1) // Remove the '/' from the start
: application.dockerComposeFileLocation;
// If the file is in a subdirectory, lastIndex will be > 0
// Otherwise it will be -1 and path will be an empty string
const lastIndex = composeLocation.lastIndexOf('/') + 1
const path = composeLocation.substring(0, lastIndex)
const fileName = composeLocation.substring(lastIndex)
const headers = isPublicRepository const headers = isPublicRepository
? {} ? {}
: { : {
@ -404,13 +416,13 @@
}; };
const url = isPublicRepository const url = isPublicRepository
? `` ? ``
: `/v4/projects/${application.projectId}/repository/tree`; : `/v4/projects/${application.projectId}/repository/tree?path=${path}`;
const files = await get(`${apiUrl}${url}`, { const files = await get(`${apiUrl}${url}`, {
...headers ...headers
}); });
const dockerComposeFileYml = files.find( const dockerComposeFileYml = files.find(
(file: { name: string; type: string }) => (file: { name: string; type: string }) =>
file.name === composeLocation && file.type === 'blob' file.name === fileName && file.type === 'blob'
); );
const id = dockerComposeFileYml.id; const id = dockerComposeFileYml.id;
@ -490,7 +502,7 @@
<div class="grid grid-flow-row gap-2 px-4"> <div class="grid grid-flow-row gap-2 px-4">
<div class="mt-2 grid grid-cols-2 items-center"> <div class="mt-2 grid grid-cols-2 items-center">
<label for="name">{$t('forms.name')}</label> <label for="name">{$t('forms.name')}</label>
<input name="name" id="name" class="w-full" bind:value={application.name} required /> <input name="name" id="name" class="w-full" bind:value={application.name} disabled={!$appSession.isAdmin} required />
</div> </div>
{#if !isSimpleDockerfile} {#if !isSimpleDockerfile}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">

View File

@ -23,7 +23,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { asyncSleep, errorNotification, getRndInteger } from '$lib/common'; import { asyncSleep, errorNotification, getRndInteger } from '$lib/common';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { addToast } from '$lib/store'; import { addToast, appSession } from '$lib/store';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
@ -264,6 +264,7 @@
{:else} {:else}
<button <button
id="restart" id="restart"
disabled={!$appSession.isAdmin}
on:click={() => restartPreview(preview)} on:click={() => restartPreview(preview)}
type="submit" type="submit"
class="icons bg-transparent text-sm flex items-center space-x-2" class="icons bg-transparent text-sm flex items-center space-x-2"
@ -286,7 +287,12 @@
{/if} {/if}
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip> <Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
<button id="forceredeploypreview" class="icons" on:click={() => redeploy(preview)}> <button
id="forceredeploypreview"
class="icons"
disabled={!$appSession.isAdmin}
on:click={() => redeploy(preview)}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6"
@ -310,7 +316,7 @@
id="deletepreview" id="deletepreview"
class="icons" class="icons"
class:hover:text-error={!loading.removing} class:hover:text-error={!loading.removing}
disabled={loading.removing} disabled={loading.removing || !$appSession.isAdmin}
on:click={() => removeApplication(preview)} on:click={() => removeApplication(preview)}
><DeleteIcon /> ><DeleteIcon />
</button> </button>

View File

@ -25,7 +25,7 @@
import pLimit from 'p-limit'; import pLimit from 'p-limit';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { get, post, put } from '$lib/api'; import { get, post, put } from '$lib/api';
import { addToast } from '$lib/store'; import { addToast, appSession } from '$lib/store';
import Secret from './_Secret.svelte'; import Secret from './_Secret.svelte';
import PreviewSecret from './_PreviewSecret.svelte'; import PreviewSecret from './_PreviewSecret.svelte';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
@ -106,46 +106,50 @@
/> />
{/key} {/key}
{/each} {/each}
<div class="lg:pt-0 pt-10"> {#if $appSession.isAdmin}
<Secret on:refresh={refreshSecrets} length={secrets.length} isNewSecret /> <div class="lg:pt-0 pt-10">
</div> <Secret on:refresh={refreshSecrets} length={secrets.length} isNewSecret />
{#if !application.settings.isBot && !application.simpleDockerfile}
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3 pt-8">
Preview Secrets <Explainer
explanation="These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
/>
</div> </div>
</div>
{#if previewSecrets.length !== 0}
{#each previewSecrets as secret, index}
{#key index}
<PreviewSecret
{index}
length={secrets.length}
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
{/key}
{/each}
{:else}
Add secrets first to see Preview Secrets.
{/if} {/if}
{#if !application.settings.isBot && !application.simpleDockerfile}
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3 pt-8">
Preview Secrets <Explainer
explanation="These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
/>
</div>
</div>
{#if previewSecrets.length !== 0}
{#each previewSecrets as secret, index}
{#key index}
<PreviewSecret
{index}
length={secrets.length}
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
{/key}
{/each}
{:else}
Add secrets first to see Preview Secrets.
{/if}
{/if} {/if}
</div> </div>
<form on:submit|preventDefault={getValues} class="mb-12 w-full"> {#if $appSession.isAdmin}
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 pt-10"> <form on:submit|preventDefault={getValues} class="mb-12 w-full">
<div class="flex flex-row space-x-2"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 pt-10">
<div class="title font-bold pb-3 ">Paste <code>.env</code> file</div> <div class="flex flex-row space-x-2">
<button type="submit" class="btn btn-sm bg-primary">Add Secrets in Batch</button> <div class="title font-bold pb-3 ">Paste <code>.env</code> file</div>
<button type="submit" class="btn btn-sm bg-primary">Add Secrets in Batch</button>
</div>
</div> </div>
</div>
<textarea <textarea
placeholder={`PORT=1337\nPASSWORD=supersecret`} placeholder={`PORT=1337\nPASSWORD=supersecret`}
bind:value={batchSecrets} bind:value={batchSecrets}
class="mb-2 min-h-[200px] w-full" class="mb-2 min-h-[200px] w-full"
/> />
</form> </form>
{/if}

View File

@ -27,6 +27,7 @@
import { get } from '$lib/api'; import { get } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import { appSession } from '$lib/store';
let composeJson = JSON.parse(application?.dockerComposeFile || '{}'); let composeJson = JSON.parse(application?.dockerComposeFile || '{}');
let predefinedVolumes: any[] = []; let predefinedVolumes: any[] = [];
@ -86,12 +87,15 @@
<Storage on:refresh={refreshStorage} {storage} /> <Storage on:refresh={refreshStorage} {storage} />
{/key} {/key}
{/each} {/each}
<div class="Preview Secrets" class:pt-10={predefinedVolumes.length > 0}> {#if $appSession.isAdmin}
<div class:pt-10={predefinedVolumes.length > 0}>
Add New Volume <Explainer Add New Volume <Explainer
position="dropdown-bottom" position="dropdown-bottom"
explanation={$t('application.storage.persistent_storage_explainer')} explanation={$t('application.storage.persistent_storage_explainer')}
/> />
</div> </div>
<Storage on:refresh={refreshStorage} isNew /> <Storage on:refresh={refreshStorage} isNew />
{/if}
</div> </div>
</div> </div>

View File

@ -176,7 +176,6 @@
id="exited" id="exited"
href={!$status.database.isRunning ? `/databases/${id}/logs` : null} href={!$status.database.isRunning ? `/databases/${id}/logs` : null}
class="icons bg-transparent text-red-500 tooltip-error" class="icons bg-transparent text-red-500 tooltip-error"
sveltekit:prefetch
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -271,7 +270,6 @@
<a <a
id="configuration" id="configuration"
href="/databases/{id}" href="/databases/{id}"
sveltekit:prefetch
class="hover:text-yellow-500 rounded" class="hover:text-yellow-500 rounded"
class:text-yellow-500={$page.url.pathname === `/databases/${id}`} class:text-yellow-500={$page.url.pathname === `/databases/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}`} class:bg-coolgray-500={$page.url.pathname === `/databases/${id}`}
@ -305,7 +303,6 @@
<a <a
id="databaselogs" id="databaselogs"
href={$status.database.isRunning ? `/databases/${id}/logs` : null} href={$status.database.isRunning ? `/databases/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded" class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/databases/${id}/logs`} class:text-pink-500={$page.url.pathname === `/databases/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/databases/${id}/logs`} class:bg-coolgray-500={$page.url.pathname === `/databases/${id}/logs`}

View File

@ -60,7 +60,7 @@
{$t('application.configuration.no_configurable_destination')} {$t('application.configuration.no_configurable_destination')}
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center">
<a href="/destinations/new" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500"> <a href="/destinations/new" class="add-icon bg-sky-600 hover:bg-sky-500">
<svg <svg
class="w-6" class="w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@ -57,11 +57,7 @@
<div class="flex-col"> <div class="flex-col">
<div class="pb-2 text-center font-bold">No SSH key found</div> <div class="pb-2 text-center font-bold">No SSH key found</div>
<div class="flex justify-center"> <div class="flex justify-center">
<a <a href="/settings/ssh" class="add-icon bg-sky-600 hover:bg-sky-500">
href="/settings/ssh"
sveltekit:prefetch
class="add-icon bg-sky-600 hover:bg-sky-500"
>
<svg <svg
class="w-6" class="w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@ -669,7 +669,7 @@
<button class="btn btn-sm btn-primary" on:click={refreshStatusApplications} <button class="btn btn-sm btn-primary" on:click={refreshStatusApplications}
>{noInitialStatus.applications ? 'Load Status' : 'Refresh Status'}</button >{noInitialStatus.applications ? 'Load Status' : 'Refresh Status'}</button
> >
{#if foundUnconfiguredApplication} {#if foundUnconfiguredApplication && $appSession.isAdmin}
<button <button
class="btn btn-sm" class="btn btn-sm"
class:loading={loading.applications} class:loading={loading.applications}
@ -783,11 +783,13 @@
</svg> </svg>
</a> </a>
{/if} {/if}
{#if $appSession.isAdmin}
<button <button
class="icons hover:bg-green-500" class="icons hover:bg-green-500"
on:click|stopPropagation|preventDefault={() => on:click|stopPropagation|preventDefault={() =>
deleteApplication(application.id)}><DeleteIcon /></button deleteApplication(application.id)}><DeleteIcon /></button
> >
{/if}
</div> </div>
</div> </div>
</div> </div>
@ -896,11 +898,13 @@
</svg> </svg>
</a> </a>
{/if} {/if}
{#if $appSession.isAdmin}
<button <button
class="icons hover:bg-green-500" class="icons hover:bg-green-500"
on:click|stopPropagation|preventDefault={() => on:click|stopPropagation|preventDefault={() =>
deleteApplication(application.id)}><DeleteIcon /></button deleteApplication(application.id)}><DeleteIcon /></button
> >
{/if}
</div> </div>
</div> </div>
</div> </div>
@ -991,11 +995,13 @@
</svg> </svg>
</a> </a>
{/if} {/if}
{#if $appSession.isAdmin}
<button <button
class="icons hover:bg-pink-500" class="icons hover:bg-pink-500"
on:click|stopPropagation|preventDefault={() => deleteService(service.id)} on:click|stopPropagation|preventDefault={() => deleteService(service.id)}
><DeleteIcon /></button ><DeleteIcon /></button
> >
{/if}
</div> </div>
</div> </div>
</div> </div>
@ -1077,11 +1083,13 @@
</svg> </svg>
</a> </a>
{/if} {/if}
{#if $appSession.isAdmin}
<button <button
class="icons hover:bg-pink-500" class="icons hover:bg-pink-500"
on:click|stopPropagation|preventDefault={() => deleteService(service.id)} on:click|stopPropagation|preventDefault={() => deleteService(service.id)}
><DeleteIcon /></button ><DeleteIcon /></button
> >
{/if}
</div> </div>
</div> </div>
</div> </div>
@ -1173,11 +1181,13 @@
</svg> </svg>
</div> </div>
{/if} {/if}
{#if $appSession.isAdmin}
<button <button
class="icons hover:bg-databases-100" class="icons hover:bg-databases-100"
on:click|stopPropagation|preventDefault={() => deleteDatabase(database.id)} on:click|stopPropagation|preventDefault={() => deleteDatabase(database.id)}
><DeleteIcon /></button ><DeleteIcon /></button
> >
{/if}
</div> </div>
</div> </div>
</div> </div>
@ -1259,11 +1269,13 @@
</svg> </svg>
</div> </div>
{/if} {/if}
{#if $appSession.isAdmin}
<button <button
class="icons hover:bg-databases" class="icons hover:bg-databases"
on:click|stopPropagation|preventDefault={() => deleteDatabase(database.id)} on:click|stopPropagation|preventDefault={() => deleteDatabase(database.id)}
><DeleteIcon /></button ><DeleteIcon /></button
> >
{/if}
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,6 +2,7 @@
export let service: any; export let service: any;
export let template: any; export let template: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import { appSession } from '$lib/store';
import ServiceLinks from './_ServiceLinks.svelte'; import ServiceLinks from './_ServiceLinks.svelte';
</script> </script>
@ -106,6 +107,7 @@
</svg>Service</a </svg>Service</a
> >
</li> </li>
{#if $appSession.isAdmin}
<li class="menu-title"> <li class="menu-title">
<span>Advanced</span> <span>Advanced</span>
</li> </li>
@ -132,4 +134,5 @@
</svg>Danger Zone</a </svg>Danger Zone</a
> >
</li> </li>
{/if}
</ul> </ul>

View File

@ -8,7 +8,7 @@
import { del, post } from '$lib/api'; import { del, post } from '$lib/api';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { addToast } from '$lib/store'; import { addToast, appSession } from '$lib/store';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -77,6 +77,7 @@
/> />
</td> </td>
{#if $appSession.isAdmin}
<td> <td>
{#if isNewSecret} {#if isNewSecret}
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
@ -93,3 +94,4 @@
</div> </div>
{/if} {/if}
</td> </td>
{/if}

View File

@ -76,11 +76,10 @@
import { dev } from '$app/env'; import { dev } from '$app/env';
const { id } = $page.params; const { id } = $page.params;
$isDeploymentEnabled = checkIfDeploymentEnabledServices($appSession.isAdmin, service); $isDeploymentEnabled = checkIfDeploymentEnabledServices(service);
let statusInterval: any; let statusInterval: any;
async function deleteService() { async function deleteService() {
const sure = confirm($t('application.confirm_to_delete', { name: service.name })); const sure = confirm($t('application.confirm_to_delete', { name: service.name }));
if (sure) { if (sure) {
@ -291,7 +290,7 @@
</button> </button>
{:else if $status.service.overallStatus === 'healthy'} {:else if $status.service.overallStatus === 'healthy'}
<button <button
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
class="btn btn-sm gap-2" class="btn btn-sm gap-2"
on:click={() => restartService()} on:click={() => restartService()}
> >
@ -317,7 +316,7 @@
<button <button
on:click={() => stopService(false)} on:click={() => stopService(false)}
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
class="btn btn-sm gap-2" class="btn btn-sm gap-2"
> >
<svg <svg
@ -338,10 +337,10 @@
</button> </button>
{:else if $status.service.overallStatus === 'degraded'} {:else if $status.service.overallStatus === 'degraded'}
<button <button
on:click={stopService} on:click={() => stopService()}
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
class="btn btn-sm gap-2" class="btn btn-sm gap-2"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -362,7 +361,7 @@
{#if $status.service.overallStatus === 'degraded'} {#if $status.service.overallStatus === 'degraded'}
<button <button
class="btn btn-sm gap-2" class="btn btn-sm gap-2"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
on:click={() => restartService()} on:click={() => restartService()}
> >
<svg <svg
@ -386,7 +385,7 @@
{:else if $status.service.overallStatus === 'stopped'} {:else if $status.service.overallStatus === 'stopped'}
<button <button
class="btn btn-sm gap-2" class="btn btn-sm gap-2"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
on:click={() => startService()} on:click={() => startService()}
> >
<svg <svg

View File

@ -59,7 +59,7 @@
{$t('application.configuration.no_configurable_destination')} {$t('application.configuration.no_configurable_destination')}
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500"> <a href="/new/destination" class="add-icon bg-sky-600 hover:bg-sky-500">
<svg <svg
class="w-6" class="w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@ -110,7 +110,7 @@
if (formData) service = await saveForm(formData, service); if (formData) service = await saveForm(formData, service);
setLocation(service); setLocation(service);
forceSave = false; forceSave = false;
$isDeploymentEnabled = checkIfDeploymentEnabledServices($appSession.isAdmin, service); $isDeploymentEnabled = checkIfDeploymentEnabledServices(service);
return addToast({ return addToast({
message: 'Configuration saved.', message: 'Configuration saved.',
type: 'success' type: 'success'
@ -165,6 +165,7 @@
} }
} }
async function changeSettings(name: any) { async function changeSettings(name: any) {
if (!$appSession.isAdmin) return;
try { try {
if (name === 'dualCerts') { if (name === 'dualCerts') {
dualCerts = !dualCerts; dualCerts = !dualCerts;
@ -277,7 +278,14 @@
<div class="grid grid-flow-row gap-2 px-4"> <div class="grid grid-flow-row gap-2 px-4">
<div class="mt-2 grid grid-cols-2 items-center"> <div class="mt-2 grid grid-cols-2 items-center">
<label for="name">{$t('forms.name')}</label> <label for="name">{$t('forms.name')}</label>
<input name="name" id="name" class="w-full" bind:value={service.name} required /> <input
name="name"
id="name"
class="w-full"
disabled={!$appSession.isAdmin}
bind:value={service.name}
required
/>
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="version">Version / Tag</label> <label for="version">Version / Tag</label>
@ -386,7 +394,7 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="dualCerts" id="dualCerts"
disabled={$status.service.isRunning} disabled={$status.service.isRunning || !$appSession.isAdmin}
dataTooltip={$t('forms.must_be_stopped_to_modify')} dataTooltip={$t('forms.must_be_stopped_to_modify')}
bind:setting={dualCerts} bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')} title={$t('application.ssl_www_and_non_www')}
@ -482,7 +490,7 @@
required={variable?.required} required={variable?.required}
/> />
{:else if variable.defaultValue === 'true' || variable.defaultValue === 'false'} {:else if variable.defaultValue === 'true' || variable.defaultValue === 'false'}
{#if variable.value === 'true' || variable.value === 'false'} {#if variable.value === 'true' || variable.value === 'false' || variable.value === 'invite_only'}
<select <select
class="w-full font-normal" class="w-full font-normal"
readonly={isDisabled} readonly={isDisabled}
@ -496,6 +504,9 @@
> >
<option value="true">enabled</option> <option value="true">enabled</option>
<option value="false">disabled</option> <option value="false">disabled</option>
{#if service.type.startsWith('plausibleanalytics') && variable.id == 'config_disable_registration'}
<option value="invite_only">invite_only</option>
{/if}
</select> </select>
{:else} {:else}
<select <select
@ -510,7 +521,7 @@
required={variable?.required} required={variable?.required}
> >
<option value="true">true</option> <option value="true">true</option>
<option value="false"> false</option> <option value="false">false</option>
</select> </select>
{/if} {/if}
{:else if variable.defaultValue === '$$generate_password'} {:else if variable.defaultValue === '$$generate_password'}

View File

@ -25,7 +25,7 @@
import { get } from '$lib/api'; import { get } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import pLimit from 'p-limit'; import pLimit from 'p-limit';
import { addToast } from '$lib/store'; import { addToast, appSession } from '$lib/store';
import { saveSecret } from './utils'; import { saveSecret } from './utils';
const limit = pLimit(1); const limit = pLimit(1);
@ -83,7 +83,12 @@
{#each secrets as secret} {#each secrets as secret}
{#key secret.id} {#key secret.id}
<tr> <tr>
<Secret name={secret.name} value={secret.value} readonly={secret.readOnly} on:refresh={refreshSecrets} /> <Secret
name={secret.name}
value={secret.value}
readonly={secret.readOnly}
on:refresh={refreshSecrets}
/>
</tr> </tr>
{/key} {/key}
{/each} {/each}
@ -93,18 +98,20 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<form on:submit|preventDefault={getValues} class="mb-12 w-full"> {#if $appSession.isAdmin}
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 pt-10"> <form on:submit|preventDefault={getValues} class="mb-12 w-full">
<div class="flex flex-row space-x-2"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 pt-10">
<div class="title font-bold pb-3 ">Paste <code>.env</code> file</div> <div class="flex flex-row space-x-2">
<button type="submit" class="btn btn-sm bg-primary">Add Secrets in Batch</button> <div class="title font-bold pb-3 ">Paste <code>.env</code> file</div>
<button type="submit" class="btn btn-sm bg-primary">Add Secrets in Batch</button>
</div>
</div> </div>
</div>
<textarea <textarea
placeholder={`PORT=1337\nPASSWORD=supersecret`} placeholder={`PORT=1337\nPASSWORD=supersecret`}
bind:value={batchSecrets} bind:value={batchSecrets}
class="mb-2 min-h-[200px] w-full" class="mb-2 min-h-[200px] w-full"
/> />
</form> </form>
{/if}
</div> </div>

View File

@ -26,6 +26,7 @@
import { get } from '$lib/api'; import { get } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import { appSession } from '$lib/store';
const { id } = $page.params; const { id } = $page.params;
async function refreshStorage() { async function refreshStorage() {
@ -81,10 +82,11 @@
{/key} {/key}
{/each} {/each}
{/if} {/if}
{#if $appSession.isAdmin}
<div class="title" class:pt-10={persistentStorages.filter((s) => s.predefined).length > 0}> <div class="title" class:pt-10={persistentStorages.filter((s) => s.predefined).length > 0}>
Add New Volume Add New Volume
</div> </div>
<Storage on:refresh={refreshStorage} isNew {services} /> <Storage on:refresh={refreshStorage} isNew {services} />
{/if}
</div> </div>
</div> </div>

View File

@ -190,7 +190,7 @@
id="name" id="name"
required required
bind:value={source.name} bind:value={source.name}
disabled={$appSession.teamId !== '0'} disabled={!$appSession.isAdmin}
/> />
<label for="htmlUrl">HTML URL</label> <label for="htmlUrl">HTML URL</label>
<input <input
@ -236,16 +236,18 @@
placeholder="eg: coollabsio" placeholder="eg: coollabsio"
bind:value={source.organization} bind:value={source.organization}
/> />
<Setting {#if $appSession.isAdmin}
customClass="pt-4" <Setting
id="autodeploy" customClass="pt-4"
isCenter={false} id="autodeploy"
disabled={$appSession.teamId !== '0'} isCenter={false}
bind:setting={source.isSystemWide} disabled={$appSession.teamId !== '0'}
on:click={() => changeSettings('isSystemWide', true)} bind:setting={source.isSystemWide}
title="System Wide Git Source" on:click={() => changeSettings('isSystemWide', true)}
description="System Wide Git Sources are available to all the users in your Coolify instance. <br><br> <span class='font-bold text-warning'>Use with caution, as it can be a security risk.</span>" title="System Wide Git Source"
/> description="System Wide Git Sources are available to all the users in your Coolify instance. <br><br> <span class='font-bold text-warning'>Use with caution, as it can be a security risk.</span>"
/>
{/if}
</div> </div>
</form> </form>
{:else} {:else}

View File

@ -51,7 +51,8 @@
appId: source.gitlabApp.appId, appId: source.gitlabApp.appId,
appSecret: source.gitlabApp.appSecret, appSecret: source.gitlabApp.appSecret,
groupName: source.gitlabApp.groupName, groupName: source.gitlabApp.groupName,
customPort: source.customPort customPort: source.customPort,
customUser: source.customUser,
}); });
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
if (from) { if (from) {
@ -70,7 +71,8 @@
name: source.name, name: source.name,
htmlUrl: source.htmlUrl.replace(/\/$/, ''), htmlUrl: source.htmlUrl.replace(/\/$/, ''),
apiUrl: source.apiUrl.replace(/\/$/, ''), apiUrl: source.apiUrl.replace(/\/$/, ''),
customPort: source.customPort customPort: source.customPort,
customUser: source.customUser
}); });
return addToast({ return addToast({
message: 'Configuration saved.', message: 'Configuration saved.',
@ -244,6 +246,22 @@
/> />
</div> </div>
{#if selfHosted} {#if selfHosted}
<div class="grid grid-cols-2 items-center">
<label for="customPort" class="text-base font-bold text-stone-100"
>Custom SSH User <Explainer
explanation={'If you use a self-hosted version of Git, you can provide a custom SSH user for all the Git related actions.'}
/></label
>
<input
class="w-full"
name="customUser"
id="customUser"
disabled={!selfHosted}
readonly={!selfHosted}
required
bind:value={source.customUser}
/>
</div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="customPort" class="text-base font-bold text-stone-100" <label for="customPort" class="text-base font-bold text-stone-100"
>Custom SSH Port <Explainer >Custom SSH Port <Explainer

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" fill="none">
<rect width="200" height="200" rx="30" fill="#4422dd" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M148.883 112.106C148.124 111.916 147.491 111.726 146.921 111.473C146.5 111.286 146.148 111.064 145.839 110.807C145.635 110.637 145.547 110.369 145.572 110.104C145.877 106.923 145.541 104.118 145.845 100.964C147.111 88.1771 155.15 92.2285 162.367 90.1395C166.511 88.9722 170.655 86.6747 172.169 82.1555C172.417 81.4157 172.199 80.6114 171.684 80.0255C166.956 74.6505 161.723 69.9012 156.037 65.831C136.973 52.2582 112.197 46.6221 89.4444 49.9189C88.5825 50.0438 88.1307 50.9987 88.6016 51.7314C91.4833 56.2156 95.2849 59.8853 99.6447 62.5864C100.437 63.0773 100.119 64.1376 99.2092 63.9337C97.0711 63.4543 94.3238 62.5194 91.7401 60.6961C91.4911 60.5204 91.1704 60.4762 90.8873 60.5891C89.7323 61.0497 88.0643 61.7139 86.6859 62.3089C85.8925 62.6514 85.731 63.6747 86.3841 64.2407C97.8122 74.1467 114.437 75.6526 127.455 67.7254C128.248 67.2425 129.518 68.2348 129.262 69.1275C128.853 70.5574 128.375 72.523 127.867 75.1999C124.638 91.5322 115.332 90.2661 103.811 86.1514C80.7898 77.8076 67.7294 84.9328 56.1177 70.8281C55.3109 69.8481 53.8925 69.5083 52.9328 70.3392C50.5358 72.4145 49.1172 75.4437 49.1172 78.6816C49.1172 82.5166 51.0968 85.8018 54.0311 87.7682C54.3981 88.0142 54.8858 87.9102 55.1598 87.5636C55.8748 86.6586 56.4597 86.0587 57.1881 85.6794C57.9852 85.2641 58.374 86.4045 57.701 87.0003C55.2349 89.1839 54.527 91.7851 52.9154 96.913C50.3832 104.952 51.4594 113.182 39.6217 115.334C33.3546 115.651 33.4812 119.892 31.2023 126.222C28.5575 133.863 25.0942 137.247 18.6844 143.924C17.8078 144.837 17.7326 146.297 18.696 147.118C21.2564 149.301 23.8969 149.421 26.5812 148.315C33.228 145.53 38.3556 136.921 43.1666 131.35C48.5474 125.146 61.4613 127.805 71.21 121.728C76.4677 118.504 79.6266 114.386 78.6164 108.217C78.4537 107.224 79.5906 106.626 80.0029 107.545C80.7856 109.289 81.2988 111.149 81.5121 113.071C81.5681 113.575 82.0184 113.947 82.5245 113.919C93.0718 113.326 106.711 124.959 119.458 128.107C120.233 128.299 120.784 127.403 120.346 126.736C119.539 125.507 118.854 124.233 118.308 122.931C117.744 121.578 117.317 120.265 117.015 118.998C116.778 118.007 118.226 117.741 118.721 118.632C121.99 124.524 128.523 130.057 137.615 130.717C140.717 130.97 144.136 130.59 147.681 129.514C151.922 128.248 155.847 126.602 160.531 127.488C164.013 128.122 167.241 129.894 169.267 132.869C172.11 137.015 178.11 138.113 181.308 133.781C181.743 133.191 181.78 132.403 181.492 131.729C174.45 115.223 156.569 114.089 148.883 112.106Z" fill="white" />
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,6 +1,6 @@
FROM alpine:3.17 FROM alpine:3.17
ARG BUILDARCH ARG BUILDARCH
ARG PB_VERSION=0.10.2 ARG PB_VERSION=0.11.0
RUN apk add --no-cache \ RUN apk add --no-cache \
unzip \ unzip \
ca-certificates ca-certificates

View File

@ -1,7 +1,7 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "3.12.8", "version": "3.12.9",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": "github:coollabsio/coolify", "repository": "github:coollabsio/coolify",
"scripts": { "scripts": {

View File

@ -55,7 +55,7 @@ importers:
is-ip: 5.0.0 is-ip: 5.0.0
is-port-reachable: 4.0.0 is-port-reachable: 4.0.0
js-yaml: 4.1.0 js-yaml: 4.1.0
jsonwebtoken: 8.5.1 jsonwebtoken: 9.0.0
minimist: ^1.2.7 minimist: ^1.2.7
node-forge: 1.3.1 node-forge: 1.3.1
node-os-utils: 1.3.7 node-os-utils: 1.3.7
@ -109,7 +109,7 @@ importers:
is-ip: 5.0.0 is-ip: 5.0.0
is-port-reachable: 4.0.0 is-port-reachable: 4.0.0
js-yaml: 4.1.0 js-yaml: 4.1.0
jsonwebtoken: 8.5.1 jsonwebtoken: 9.0.0
minimist: 1.2.7 minimist: 1.2.7
node-forge: 1.3.1 node-forge: 1.3.1
node-os-utils: 1.3.7 node-os-utils: 1.3.7
@ -265,6 +265,7 @@ importers:
cuid: 2.1.8 cuid: 2.1.8
dayjs: 1.11.6 dayjs: 1.11.6
dotenv: ^16.0.3 dotenv: ^16.0.3
esbuild: 0.15.15
execa: 6.1.0 execa: 6.1.0
fastify: 4.10.2 fastify: 4.10.2
fastify-plugin: 4.4.0 fastify-plugin: 4.4.0
@ -272,8 +273,9 @@ importers:
is-ip: 5.0.0 is-ip: 5.0.0
is-port-reachable: 4.0.0 is-port-reachable: 4.0.0
js-yaml: 4.1.0 js-yaml: 4.1.0
jsonwebtoken: 8.5.1 jsonwebtoken: 9.0.0
node-fetch: 3.3.0 node-fetch: 3.3.0
nodemon: 2.0.20
npm-run-all: 4.1.5 npm-run-all: 4.1.5
p-all: 4.0.0 p-all: 4.0.0
p-throttle: 5.0.0 p-throttle: 5.0.0
@ -319,7 +321,7 @@ importers:
is-ip: 5.0.0 is-ip: 5.0.0
is-port-reachable: 4.0.0 is-port-reachable: 4.0.0
js-yaml: 4.1.0 js-yaml: 4.1.0
jsonwebtoken: 8.5.1 jsonwebtoken: 9.0.0
node-fetch: 3.3.0 node-fetch: 3.3.0
p-all: 4.0.0 p-all: 4.0.0
p-throttle: 5.0.0 p-throttle: 5.0.0
@ -340,6 +342,8 @@ importers:
'@types/node-fetch': 2.6.2 '@types/node-fetch': 2.6.2
'@types/shell-quote': 1.7.1 '@types/shell-quote': 1.7.1
'@types/ws': 8.5.3 '@types/ws': 8.5.3
esbuild: 0.15.15
nodemon: 2.0.20
npm-run-all: 4.1.5 npm-run-all: 4.1.5
rimraf: 3.0.2 rimraf: 3.0.2
start-server-and-test: 1.14.0 start-server-and-test: 1.14.0
@ -1449,6 +1453,7 @@ packages:
engines: {node: '>= 12.11'} engines: {node: '>= 12.11'}
peerDependencies: peerDependencies:
bree: '>=9.0.0' bree: '>=9.0.0'
tsconfig-paths: '>= 4'
dependencies: dependencies:
bree: 9.1.2 bree: 9.1.2
ts-node: 10.8.2_wup25etrarvlqkprac7h35hj7u ts-node: 10.8.2_wup25etrarvlqkprac7h35hj7u
@ -5733,20 +5738,14 @@ packages:
graceful-fs: 4.2.10 graceful-fs: 4.2.10
dev: false dev: false
/jsonwebtoken/8.5.1: /jsonwebtoken/9.0.0:
resolution: {integrity: sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==} resolution: {integrity: sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==}
engines: {node: '>=4', npm: '>=1.4.28'} engines: {node: '>=12', npm: '>=6'}
dependencies: dependencies:
jws: 3.2.2 jws: 3.2.2
lodash.includes: 4.3.0 lodash: 4.17.21
lodash.isboolean: 3.0.3
lodash.isinteger: 4.0.4
lodash.isnumber: 3.0.3
lodash.isplainobject: 4.0.6
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3 ms: 2.1.3
semver: 5.7.1 semver: 7.3.8
dev: false dev: false
/jwa/1.4.1: /jwa/1.4.1:
@ -5902,37 +5901,13 @@ packages:
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
dev: false dev: false
/lodash.includes/4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
dev: false
/lodash.isboolean/3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
dev: false
/lodash.isinteger/4.0.4:
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
dev: false
/lodash.isnumber/3.0.3:
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
dev: false
/lodash.isplainobject/4.0.6: /lodash.isplainobject/4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
dev: false dev: false
/lodash.isstring/4.0.1:
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
dev: false
/lodash.merge/4.6.2: /lodash.merge/4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
/lodash.once/4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
dev: false
/lodash.uniqby/4.5.0: /lodash.uniqby/4.5.0:
resolution: {integrity: sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ==} resolution: {integrity: sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ==}
dependencies: dependencies: