Merge branch 'next' into ui

This commit is contained in:
Andras Bacsai 2022-08-31 15:06:53 +02:00 committed by GitHub
commit ae4cf44728
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1722 additions and 896 deletions

View File

@ -125,7 +125,7 @@ ### Add supported versions
Supported versions are hardcoded into Coolify (for now). Supported versions are hardcoded into Coolify (for now).
You need to update `supportedServiceTypesAndVersions` function at [src/apps/api/src/lib/supportedVersions.ts](src/apps/api/src/lib/supportedVersions.ts). Example JSON: You need to update `supportedServiceTypesAndVersions` function at [apps/api/src/lib/services/supportedVersions.ts](apps/api/src/lib/services/supportedVersions.ts). Example JSON:
```js ```js
{ {
@ -209,21 +209,22 @@ ### Add required functions/properties
4. Add service deletion query to `removeService` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts) 4. Add service deletion query to `removeService` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts)
5. You need to add start process for the new service in [apps/api/src/routes/api/v1/services/handlers.ts](apps/api/src/routes/api/v1/services/handlers.ts) 5. Add start process for the new service in [apps/api/src/routes/api/v1/services/handlers.ts](apps/api/src/routes/api/v1/services/handlers.ts)
> See startUmamiService() function as example. > See startUmamiService() function as example.
6. Add the newly added start process to `startService` in [apps/api/src/routes/api/v1/services/handlers.ts](apps/api/src/routes/api/v1/services/handlers.ts)
6. You need to add a custom logo at [apps/ui/src/lib/components/svg/services](apps/ui/src/lib/components/svg/services) as a svelte component and export it in [apps/ui/src/lib/components/svg/services/index.ts](apps/ui/src/lib/components/svg/services/index.ts) 7. You need to add a custom logo at [apps/ui/src/lib/components/svg/services](apps/ui/src/lib/components/svg/services) as a svelte component and export it in [apps/ui/src/lib/components/svg/services/index.ts](apps/ui/src/lib/components/svg/services/index.ts)
SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning. SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning.
7. You need to include it the logo at: 8. You need to include it the logo at:
- [apps/ui/src/lib/components/svg/services/ServiceIcons.svelte](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) with `isAbsolute`. - [apps/ui/src/lib/components/svg/services/ServiceIcons.svelte](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) with `isAbsolute`.
- [apps/ui/src/routes/services/[id]/_ServiceLinks.svelte](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) with the link to the docs/main site of the service - [apps/ui/src/routes/services/[id]/_ServiceLinks.svelte](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) with the link to the docs/main site of the service
8. By default the URL and the name frontend forms are included in [apps/ui/src/routes/services/[id]/_Services/_Services.svelte](apps/ui/src/routes/services/[id]/_Services/_Services.svelte). 9. By default the URL and the name frontend forms are included in [apps/ui/src/routes/services/[id]/_Services/_Services.svelte](apps/ui/src/routes/services/[id]/_Services/_Services.svelte).
If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component to [apps/ui/src/routes/services/[id]/_Services](apps/ui/src/routes/services/[id]/_Services) with an underscore. If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component to [apps/ui/src/routes/services/[id]/_Services](apps/ui/src/routes/services/[id]/_Services) with an underscore.

View File

@ -0,0 +1,18 @@
-- CreateTable
CREATE TABLE "Weblate" (
"id" TEXT NOT NULL PRIMARY KEY,
"adminPassword" TEXT NOT NULL,
"postgresqlHost" TEXT NOT NULL,
"postgresqlPort" INTEGER NOT NULL,
"postgresqlUser" TEXT NOT NULL,
"postgresqlPassword" TEXT NOT NULL,
"postgresqlDatabase" TEXT NOT NULL,
"postgresqlPublicPort" INTEGER,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Weblate_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Weblate_serviceId_key" ON "Weblate"("serviceId");

View File

@ -348,6 +348,7 @@ model Service {
wordpress Wordpress? wordpress Wordpress?
appwrite Appwrite? appwrite Appwrite?
searxng Searxng? searxng Searxng?
weblate Weblate?
} }
model PlausibleAnalytics { model PlausibleAnalytics {
@ -559,3 +560,18 @@ model Searxng {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id]) service Service @relation(fields: [serviceId], references: [id])
} }
model Weblate {
id String @id @default(cuid())
adminPassword String
postgresqlHost String
postgresqlPort Int
postgresqlUser String
postgresqlPassword String
postgresqlDatabase String
postgresqlPublicPort Int?
serviceId String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}

View File

@ -21,7 +21,7 @@ import { scheduler } from './scheduler';
import { supportedServiceTypesAndVersions } from './services/supportedVersions'; import { supportedServiceTypesAndVersions } from './services/supportedVersions';
import { includeServices } from './services/common'; import { includeServices } from './services/common';
export const version = '3.8.9'; export const version = '3.9.0';
export const isDev = process.env.NODE_ENV === 'development'; export const isDev = process.env.NODE_ENV === 'development';
const algorithm = 'aes-256-ctr'; const algorithm = 'aes-256-ctr';
@ -45,7 +45,7 @@ export function getAPIUrl() {
if (process.env.CODESANDBOX_HOST) { if (process.env.CODESANDBOX_HOST) {
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}` return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`
} }
return isDev ? 'http://localhost:3001' : 'http://localhost:3000'; return isDev ? 'http://host.docker.internal:3001' : 'http://localhost:3000';
} }
export function getUIUrl() { export function getUIUrl() {
@ -1309,6 +1309,9 @@ export function saveUpdateableFields(type: string, data: any) {
temp = Boolean(temp) temp = Boolean(temp)
} }
} }
if (k.isNumber && temp === '') {
temp = null
}
update[k.name] = temp update[k.name] = temp
}); });
} }

View File

@ -1,23 +1,7 @@
import { exec } from 'node:child_process'
import util from 'util';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import forge from 'node-forge';
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
import type { Config } from 'unique-names-generator';
import generator from 'generate-password';
import crypto from 'crypto';
import { promises as dns } from 'dns';
import { PrismaClient } from '@prisma/client';
import cuid from 'cuid'; import cuid from 'cuid';
import os from 'os';
import sshConfig from 'ssh-config'
import { encrypt, generatePassword, prisma } from '../common'; import { encrypt, generatePassword, prisma } from '../common';
export const version = '3.8.2';
export const isDev = process.env.NODE_ENV === 'development';
export const includeServices: any = { export const includeServices: any = {
destinationDocker: true, destinationDocker: true,
persistentStorage: true, persistentStorage: true,
@ -34,7 +18,8 @@ export const includeServices: any = {
moodle: true, moodle: true,
appwrite: true, appwrite: true,
glitchTip: true, glitchTip: true,
searxng: true searxng: true,
weblate: true
}; };
export async function configureServiceType({ export async function configureServiceType({
id, id,
@ -312,6 +297,27 @@ export async function configureServiceType({
} }
} }
}); });
}else if (type === 'weblate') {
const adminPassword = encrypt(generatePassword({}))
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword({}));
const postgresqlDatabase = 'weblate';
await prisma.service.update({
where: { id },
data: {
type,
weblate: {
create: {
adminPassword,
postgresqlHost: `${id}-postgresql`,
postgresqlPort: 5432,
postgresqlUser,
postgresqlPassword,
postgresqlDatabase,
}
}
}
});
} else { } else {
await prisma.service.update({ await prisma.service.update({
where: { id }, where: { id },
@ -338,7 +344,7 @@ export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.moodle.deleteMany({ where: { serviceId: id } }); await prisma.moodle.deleteMany({ where: { serviceId: id } });
await prisma.appwrite.deleteMany({ where: { serviceId: id } }); await prisma.appwrite.deleteMany({ where: { serviceId: id } });
await prisma.searxng.deleteMany({ where: { serviceId: id } }); await prisma.searxng.deleteMany({ where: { serviceId: id } });
await prisma.weblate.deleteMany({ where: { serviceId: id } });
await prisma.service.delete({ where: { id } }); await prisma.service.delete({ where: { id } });
} }

View File

@ -63,6 +63,9 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
if (type === 'searxng') { if (type === 'searxng') {
return await startSearXNGService(request) return await startSearXNGService(request)
} }
if (type === 'weblate') {
return await startWeblateService(request)
}
throw `Service type ${type} not supported.` throw `Service type ${type} not supported.`
} catch (error) { } catch (error) {
throw { status: 500, message: error?.message || error } throw { status: 500, message: error?.message || error }
@ -2224,3 +2227,106 @@ async function startSearXNGService(request: FastifyRequest<ServiceStartStop>) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
async function startWeblateService(request: FastifyRequest<ServiceStartStop>) {
try {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const {
weblate: { adminPassword, postgresqlHost, postgresqlPort, postgresqlUser, postgresqlPassword, postgresqlDatabase }
} = service;
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage, fqdn } =
service;
const network = destinationDockerId && destinationDocker.network;
const port = getServiceMainPort('weblate');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const config = {
weblate: {
image: `${image}:${version}`,
volume: `${id}-data:/app/data`,
environmentVariables: {
WEBLATE_SITE_DOMAIN: getDomain(fqdn),
WEBLATE_ADMIN_PASSWORD: adminPassword,
POSTGRES_PASSWORD: postgresqlPassword,
POSTGRES_USER: postgresqlUser,
POSTGRES_DATABASE: postgresqlDatabase,
POSTGRES_HOST: postgresqlHost,
POSTGRES_PORT: postgresqlPort,
REDIS_HOST: `${id}-redis`,
}
},
postgresql: {
image: `postgres:14-alpine`,
volume: `${id}-postgresql-data:/var/lib/postgresql/data`,
environmentVariables: {
POSTGRES_PASSWORD: postgresqlPassword,
POSTGRES_USER: postgresqlUser,
POSTGRES_DB: postgresqlDatabase,
POSTGRES_HOST: postgresqlHost,
POSTGRES_PORT: postgresqlPort,
}
},
redis: {
image: `redis:6-alpine`,
volume: `${id}-redis-data:/data`,
}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.weblate.environmentVariables[secret.name] = secret.value;
});
}
const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.weblate.image,
environment: config.weblate.environmentVariables,
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
volumes,
labels: makeLabelForServices('weblate'),
...defaultComposeConfiguration(network),
},
[`${id}-postgresql`]: {
container_name: `${id}-postgresql`,
image: config.postgresql.image,
environment: config.postgresql.environmentVariables,
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
volumes,
labels: makeLabelForServices('weblate'),
...defaultComposeConfiguration(network),
},
[`${id}-redis`]: {
container_name: `${id}-redis`,
image: config.redis.image,
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
volumes,
labels: makeLabelForServices('weblate'),
...defaultComposeConfiguration(network),
}
},
networks: {
[network]: {
external: true
}
},
volumes: volumeMounts
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await startServiceContainers(destinationDocker.id, composeFileDestination)
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@ -599,6 +599,54 @@ export const glitchTip = [{
isBoolean: false, isBoolean: false,
isEncrypted: true isEncrypted: true
}, },
{
name: 'emailSmtpHost',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'emailSmtpPassword',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'emailSmtpUseSsl',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: true,
isEncrypted: false
},
{
name: 'emailSmtpUseSsl',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: true,
isEncrypted: false
},
{
name: 'emailSmtpPort',
isEditable: true,
isLowerCase: false,
isNumber: true,
isBoolean: false,
isEncrypted: false
},
{
name: 'emailSmtpUser',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{ {
name: 'defaultEmail', name: 'defaultEmail',
isEditable: false, isEditable: false,
@ -624,7 +672,7 @@ export const glitchTip = [{
isEncrypted: true isEncrypted: true
}, },
{ {
name: 'defaultFromEmail', name: 'defaultEmailFrom',
isEditable: true, isEditable: true,
isLowerCase: false, isLowerCase: false,
isNumber: false, isNumber: false,
@ -688,3 +736,52 @@ export const searxng = [{
isBoolean: false, isBoolean: false,
isEncrypted: true isEncrypted: true
}] }]
export const weblate = [{
name: 'adminPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'postgresqlHost',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlPort',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'postgresqlDatabase',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
}]

View File

@ -190,4 +190,15 @@ export const supportedServiceTypesAndVersions = [
main: 8080 main: 8080
} }
}, },
{
name: 'weblate',
fancyName: 'Weblate',
baseImage: 'weblate/weblate',
images: ['postgres:14-alpine','redis:6-alpine'],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
]; ];

View File

@ -3,16 +3,23 @@ import crypto from 'node:crypto'
import jsonwebtoken from 'jsonwebtoken'; import jsonwebtoken from 'jsonwebtoken';
import axios from 'axios'; import axios from 'axios';
import { FastifyReply } from 'fastify'; import { FastifyReply } from 'fastify';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import { day } from '../../../../lib/dayjs'; import { day } from '../../../../lib/dayjs';
import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; import { makeLabelForStandaloneApplication, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, decrypt, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, getFreeExposedPort, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common'; import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common';
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker'; import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
import { scheduler } from '../../../../lib/scheduler';
import type { FastifyRequest } from 'fastify'; import type { FastifyRequest } from 'fastify';
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication } from './types'; import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication } from './types';
import { OnlyId } from '../../../../types'; import { OnlyId } from '../../../../types';
function filterObject(obj, callback) {
return Object.fromEntries(Object.entries(obj).
filter(([key, val]) => callback(val, key)));
}
export async function listApplications(request: FastifyRequest) { export async function listApplications(request: FastifyRequest) {
try { try {
const { teamId } = request.user const { teamId } = request.user
@ -312,6 +319,113 @@ export async function stopPreviewApplication(request: FastifyRequest<StopPreview
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function restartApplication(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try {
const { id } = request.params
const { teamId } = request.user
let application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
const buildId = cuid();
const { id: dockerId, network } = application.destinationDocker;
const { secrets, pullmergeRequestId, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application;
const envs = [
`PORT=${port}`
];
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
}
});
}
const { workdir } = await createDirectories({ repository, buildId });
const labels = []
let image = null
const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}' --format '{{json .}}'` })
const containersArray = container.trim().split('\n');
for (const container of containersArray) {
const containerObj = formatLabelsOnDocker(container);
image = containerObj[0].Image
Object.keys(containerObj[0].Labels).forEach(function (key) {
if (key.startsWith('coolify')) {
labels.push(`${key}=${containerObj[0].Labels[key]}`)
}
})
}
let imageFound = false;
try {
await executeDockerCmd({
dockerId,
command: `docker image inspect ${image}`
})
imageFound = true;
} catch (error) {
//
}
if (!imageFound) {
throw { status: 500, message: 'Image not found, cannot restart application.' }
}
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
let envFound = false;
try {
envFound = !!(await fs.stat(`${workdir}/.env`));
} catch (error) {
//
}
const volumes =
persistentStorage?.map((storage) => {
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : ''
}${storage.path}`;
}) || [];
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const composeFile = {
version: '3.8',
services: {
[applicationId]: {
image,
container_name: applicationId,
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
labels,
depends_on: [],
expose: [port],
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
...defaultComposeConfiguration(network),
}
},
networks: {
[network]: {
external: true
}
},
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
await executeDockerCmd({ dockerId, command: `docker compose --project-directory ${workdir} up -d` })
return reply.code(201).send();
}
throw { status: 500, message: 'Application cannot be restarted.' }
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function stopApplication(request: FastifyRequest<OnlyId>, reply: FastifyReply) { export async function stopApplication(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try { try {
const { id } = request.params const { id } = request.params

View File

@ -1,6 +1,6 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { OnlyId } from '../../../../types'; import { OnlyId } from '../../../../types';
import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers'; import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, restartApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers';
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types'; import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
@ -19,6 +19,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<OnlyId>('/:id/status', async (request) => await getApplicationStatus(request)); fastify.get<OnlyId>('/:id/status', async (request) => await getApplicationStatus(request));
fastify.post<OnlyId>('/:id/restart', async (request, reply) => await restartApplication(request, reply));
fastify.post<OnlyId>('/:id/stop', async (request, reply) => await stopApplication(request, reply)); fastify.post<OnlyId>('/:id/stop', async (request, reply) => await stopApplication(request, reply));
fastify.post<StopPreviewApplication>('/:id/stop/preview', async (request, reply) => await stopPreviewApplication(request, reply)); fastify.post<StopPreviewApplication>('/:id/stop/preview', async (request, reply) => await stopPreviewApplication(request, reply));

View File

@ -8,7 +8,7 @@ import { checkContainer, isContainerExited, removeContainer } from '../../../../
import cuid from 'cuid'; import cuid from 'cuid';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types';
import { defaultServiceConfigurations } from '../../../../lib/services'; import { defaultServiceConfigurations } from '../../../../lib/services';
import { supportedServiceTypesAndVersions } from '../../../../lib/services/supportedVersions'; import { supportedServiceTypesAndVersions } from '../../../../lib/services/supportedVersions';
import { configureServiceType, removeService } from '../../../../lib/services/common'; import { configureServiceType, removeService } from '../../../../lib/services/common';
@ -269,7 +269,6 @@ export async function saveService(request: FastifyRequest<SaveService>, reply: F
if (exposePort) exposePort = Number(exposePort); if (exposePort) exposePort = Number(exposePort);
type = fixType(type) type = fixType(type)
const update = saveUpdateableFields(type, request.body[type]) const update = saveUpdateableFields(type, request.body[type])
const data = { const data = {
fqdn, fqdn,
@ -400,17 +399,33 @@ export async function deleteServiceStorage(request: FastifyRequest<DeleteService
} }
} }
export async function setSettingsService(request: FastifyRequest<ServiceStartStop & SetWordpressSettings>, reply: FastifyReply) { export async function setSettingsService(request: FastifyRequest<ServiceStartStop & SetWordpressSettings & SetGlitchTipSettings>, reply: FastifyReply) {
try { try {
const { type } = request.params const { type } = request.params
if (type === 'wordpress') { if (type === 'wordpress') {
return await setWordpressSettings(request, reply) return await setWordpressSettings(request, reply)
} }
if (type === 'glitchtip') {
return await setGlitchTipSettings(request, reply)
}
throw `Service type ${type} not supported.` throw `Service type ${type} not supported.`
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
async function setGlitchTipSettings(request: FastifyRequest<SetGlitchTipSettings>, reply: FastifyReply) {
try {
const { id } = request.params
const { enableOpenUserRegistration, emailSmtpUseSsl, emailSmtpUseTls } = request.body
await prisma.glitchTip.update({
where: { serviceId: id },
data: { enableOpenUserRegistration, emailSmtpUseSsl, emailSmtpUseTls }
});
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
async function setWordpressSettings(request: FastifyRequest<ServiceStartStop & SetWordpressSettings>, reply: FastifyReply) { async function setWordpressSettings(request: FastifyRequest<ServiceStartStop & SetWordpressSettings>, reply: FastifyReply) {
try { try {
const { id } = request.params const { id } = request.params

View File

@ -29,7 +29,7 @@ import {
} from './handlers'; } from './handlers';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types';
import { startService, stopService } from '../../../../lib/services/handlers'; import { startService, stopService } from '../../../../lib/services/handlers';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
@ -71,7 +71,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<ServiceStartStop>('/:id/:type/start', async (request) => await startService(request)); fastify.post<ServiceStartStop>('/:id/:type/start', async (request) => await startService(request));
fastify.post<ServiceStartStop>('/:id/:type/stop', async (request) => await stopService(request)); fastify.post<ServiceStartStop>('/:id/:type/stop', async (request) => await stopService(request));
fastify.post<ServiceStartStop & SetWordpressSettings>('/:id/:type/settings', async (request, reply) => await setSettingsService(request, reply)); fastify.post<ServiceStartStop & SetWordpressSettings & SetGlitchTipSettings>('/:id/:type/settings', async (request, reply) => await setSettingsService(request, reply));
fastify.post<OnlyId>('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply)); fastify.post<OnlyId>('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply));
fastify.post<OnlyId>('/:id/plausibleanalytics/cleanup', async (request, reply) => await cleanupPlausibleLogs(request, reply)); fastify.post<OnlyId>('/:id/plausibleanalytics/cleanup', async (request, reply) => await cleanupPlausibleLogs(request, reply));

View File

@ -89,6 +89,12 @@ export interface ActivateWordpressFtp extends OnlyId {
} }
} }
export interface SetGlitchTipSettings extends OnlyId {
Body: {
enableOpenUserRegistration: boolean,
emailSmtpUseSsl: boolean,
emailSmtpUseTls: boolean
}
}

4
apps/i18n/.env.example Normal file
View File

@ -0,0 +1,4 @@
WEBLATE_INSTANCE_URL=http://localhost
WEBLATE_COMPONENT_NAME=coolify
WEBLATE_TOKEN=
TRANSLATION_DIR=

1
apps/i18n/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
locales/*

63
apps/i18n/index.mjs Normal file
View File

@ -0,0 +1,63 @@
import dotenv from 'dotenv';
dotenv.config()
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url';
import Gettext from 'node-gettext'
import { po } from 'gettext-parser'
import got from 'got';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const weblateInstanceURL = process.env.WEBLATE_INSTANCE_URL;
const weblateComponentName = process.env.WEBLATE_COMPONENT_NAME
const token = process.env.WEBLATE_TOKEN;
const translationsDir = process.env.TRANSLATION_DIR;
const translationsPODir = './locales';
const locales = []
const domain = 'locale'
const translations = await got(`${weblateInstanceURL}/api/components/${weblateComponentName}/glossary/translations/?format=json`, {
headers: {
"Authorization": `Token ${token}`
}
}).json()
for (const translation of translations.results) {
const code = translation.language_code
locales.push(code)
const fileUrl = translation.file_url.replace('=json', '=po')
const file = await got(fileUrl, {
headers: {
"Authorization": `Token ${token}`
}
}).text()
fs.writeFileSync(path.join(__dirname, translationsPODir, domain + '-' + code + '.po'), file)
}
const gt = new Gettext()
locales.forEach((locale) => {
let json = {}
const fileName = `${domain}-${locale}.po`
const translationsFilePath = path.join(translationsPODir, fileName)
const translationsContent = fs.readFileSync(translationsFilePath)
const parsedTranslations = po.parse(translationsContent)
const a = gt.gettext(parsedTranslations)
for (const [key, value] of Object.entries(a)) {
if (key === 'translations') {
for (const [key1, value1] of Object.entries(value)) {
if (key1 !== '') {
for (const [key2, value2] of Object.entries(value1)) {
json[value2.msgctxt] = value2.msgstr[0]
}
}
}
}
}
fs.writeFileSync(`${translationsDir}/${locale}.json`, JSON.stringify(json))
})

15
apps/i18n/package.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "i18n-converter",
"description": "Convert Weblate translations to sveltekit-i18n",
"license": "Apache-2.0",
"scripts": {
"translate": "node index.mjs"
},
"type": "module",
"dependencies": {
"node-gettext": "3.0.0",
"gettext-parser": "6.0.0",
"got": "12.3.1",
"dotenv": "16.0.2"
}
}

View File

@ -40,4 +40,6 @@
<Icons.GlitchTip {isAbsolute} /> <Icons.GlitchTip {isAbsolute} />
{:else if type === 'searxng'} {:else if type === 'searxng'}
<Icons.Searxng {isAbsolute} /> <Icons.Searxng {isAbsolute} />
{:else if type === 'weblate'}
<Icons.Weblate {isAbsolute} />
{/if} {/if}

View File

@ -0,0 +1,61 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'w-16 h-16 absolute top-0 left-0 -m-7' : 'w-12 h-12 mx-auto'}
version="1.1"
viewBox="0 0 300 300"
><linearGradient
id="a"
x1=".3965"
x2="98.808"
y1="55.253"
y2="55.253"
gradientTransform="scale(.98308 1.0172)"
gradientUnits="userSpaceOnUse"
><stop stop-color="#00d2e6" offset="0" /><stop
stop-color="#2eccaa"
offset="1"
/></linearGradient
><linearGradient
id="b"
x1="49.017"
x2="99.793"
y1="137.89"
y2="113.96"
gradientTransform="scale(1.1631 .8598)"
gradientUnits="userSpaceOnUse"
><stop stop-opacity="0" offset="0" /><stop offset=".51413" /><stop
stop-opacity="0"
offset="1"
/></linearGradient
><linearGradient
id="c"
x1="201.82"
x2="103.58"
y1="57.649"
y2="57.649"
gradientTransform="scale(.98308 1.0172)"
gradientUnits="userSpaceOnUse"
><stop stop-color="#1fa385" offset="0" /><stop
stop-color="#2eccaa"
offset="1"
/></linearGradient
><g transform="translate(50,76)" fill-rule="evenodd"
><path
d="m127.25 111.61c-2.8884-0.0145-5.7666-0.6024-8.4797-1.7847-6.1117-2.6626-11.493-7.6912-15.872-14.495 1.2486-2.2193 2.3738-4.5173 3.3784-6.8535 4.4051-10.243 6.5-21.46 6.6607-32.593-0.0233-0.22082-0.0416-0.44244-0.0552-0.66483l-0.0121-0.57132c-0.01-4.3654-0.67459-8.7898-2.1767-12.909-1.7304-4.7458-4.4887-9.4955-8.865-11.348-0.79519-0.33595-1.6316-0.47701-2.4642-0.45737-5.5049-10.289-5.6799-20.149 0-29.537 0.10115 0 0.20619 3.9293e-4 0.30734 0.001179 6.7012 0.07387 13.34 2.1418 19.021 5.7536 15.469 9.835 23.182 29.001 23.352 47.818 2e-3 0.22083-3.9e-4 0.44126-7e-3 0.66169h0.0868c-0.0226 19.887-4.8049 40.054-14.875 56.979zm-34.3 31.216c-14.448 5.9425-31.228 5.6236-45.549-1.025-16.476-7.6476-29.065-22.512-36.818-39.479-13.262-29.022-13.566-63.715-0.98815-93.182 9.4458 3.7788 17.845-2.2397 17.845-2.2397s-0.01945 9.2605 8.9478 13.905c-9.2007 21.556-8.979 47.167 0.2412 68.173 4.4389 10.107 11.22 19.519 20.619 24.842 3.3547 1.8996 7.041 3.126 10.833 3.5862 0.01404 0.0219 0.02808 0.0439 0.04214 0.0658 6.6965 10.449 15.132 19.157 24.828 25.354z"
fill="url(#a)"
fill-rule="nonzero"
/><path
d="m127.24 111.61c-2.8869-0.0151-5.7636-0.60296-8.4755-1.7846-6.1127-2.663-11.495-7.6928-15.874-14.498 1.2494-2.2205 2.3754-4.5198 3.3806-6.8572 1.3282-3.0884 2.4463-6.2648 3.3644-9.501 2.128-7.4978 30.382 2.0181 26.072 14.371-2.2239 6.373-5.0394 12.509-8.4675 18.27zm-34.302 31.212c-14.446 5.9396-31.224 5.6198-45.543-1.0278-16.476-7.6476 0.44739-33.303 9.8465-27.981 3.3533 1.8988 7.0378 3.125 10.828 3.5856 0.01567 0.0245 0.03135 0.049 0.04704 0.0735 6.695 10.447 15.128 19.153 24.821 25.349z"
fill="url(#b)"
opacity=".3"
/><path
d="m56.762 54.628c-0.0066-0.22043-0.0093-0.44086-7e-3 -0.66169 0.17001-18.817 7.8827-37.983 23.352-47.818 5.6811-3.6118 12.32-5.6798 19.021-5.7536 0.10115-7.8585e-4 0.20619-0.001179 0.30734-0.001179v29.537c-0.83254-0.01965-1.669 0.12141-2.4642 0.45737-4.3763 1.8523-7.1345 6.602-8.865 11.348-1.5021 4.1191-2.1669 8.5434-2.1767 12.909l-0.01206 0.57132c-0.01362 0.2224-0.0319 0.44401-0.05524 0.66483 0.16067 11.134 2.2556 22.35 6.6607 32.593 4.9334 11.472 12.775 22.025 23.847 26.849 8.3526 3.6397 17.612 2.7811 25.182-1.5057 9.3991-5.3226 16.18-14.734 20.619-24.842 9.2202-21.006 9.4419-46.617 0.24121-68.173 8.9673-4.6444 8.9478-13.905 8.9478-13.905s8.3993 6.0185 17.845 2.2397c12.578 29.466 12.274 64.16-0.98815 93.182-7.7535 16.967-20.343 31.831-36.818 39.479-14.667 6.809-31.913 6.9792-46.591 0.58389-13.19-5.7489-23.918-16.106-31.637-28.15-11.179-17.443-16.472-38.678-16.496-59.604z"
fill="url(#c)"
fill-rule="nonzero"
/></g
></svg
>

View File

@ -17,3 +17,4 @@ export { default as Appwrite } from './Appwrite.svelte';
export { default as Moodle } from './Moodle.svelte'; export { default as Moodle } from './Moodle.svelte';
export { default as GlitchTip } from './GlitchTip.svelte'; export { default as GlitchTip } from './GlitchTip.svelte';
export { default as Searxng } from './Searxng.svelte'; export { default as Searxng } from './Searxng.svelte';
export { default as Weblate } from './Weblate.svelte';

View File

@ -62,9 +62,7 @@
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { appSession, disabledButton, status, location, setLocation, addToast } from '$lib/store'; import { appSession, disabledButton, status, location, setLocation, addToast } from '$lib/store';
import { errorNotification, handlerNotFoundLoad } from '$lib/common'; import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import Loading from '$lib/components/Loading.svelte';
let loading = false;
let statusInterval: any; let statusInterval: any;
$disabledButton = $disabledButton =
!$appSession.isAdmin || !$appSession.isAdmin ||
@ -78,7 +76,10 @@
async function handleDeploySubmit(forceRebuild = false) { async function handleDeploySubmit(forceRebuild = false) {
try { try {
const { buildId } = await post(`/applications/${id}/deploy`, { ...application, forceRebuild }); const { buildId } = await post(`/applications/${id}/deploy`, {
...application,
forceRebuild
});
addToast({ addToast({
message: $t('application.deployment_queued'), message: $t('application.deployment_queued'),
type: 'success' type: 'success'
@ -98,22 +99,41 @@
async function deleteApplication(name: string) { async function deleteApplication(name: string) {
const sure = confirm($t('application.confirm_to_delete', { name })); const sure = confirm($t('application.confirm_to_delete', { name }));
if (sure) { if (sure) {
loading = true; $status.application.initialLoading = true;
try { try {
await del(`/applications/${id}`, { id }); await del(`/applications/${id}`, { id });
return await goto(`/applications`); return await goto(`/applications`);
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally {
$status.application.initialLoading = false;
} }
} }
} }
async function restartApplication() {
try {
$status.application.initialLoading = true;
$status.application.loading = true;
await post(`/applications/${id}/restart`, {});
} catch (error) {
return errorNotification(error);
} finally {
$status.application.initialLoading = false;
$status.application.loading = false;
await getStatus();
}
}
async function stopApplication() { async function stopApplication() {
try { try {
loading = true; $status.application.initialLoading = true;
$status.application.loading = true;
await post(`/applications/${id}/stop`, {}); await post(`/applications/${id}/stop`, {});
return window.location.reload();
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally {
$status.application.initialLoading = false;
$status.application.loading = false;
await getStatus();
} }
} }
async function getStatus() { async function getStatus() {
@ -152,9 +172,6 @@
</script> </script>
<nav class="nav-side"> <nav class="nav-side">
{#if loading}
<Loading fullscreen cover />
{:else}
{#if $location} {#if $location}
<a <a
href={$location} href={$location}
@ -177,7 +194,6 @@
</svg></a </svg></a
> >
<div class="border border-coolgray-500 h-8" /> <div class="border border-coolgray-500 h-8" />
{/if} {/if}
{#if $status.application.isExited} {#if $status.application.isExited}
@ -236,7 +252,7 @@
disabled={$disabledButton} disabled={$disabledButton}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2 text-error" class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2 text-error"
data-tip={$appSession.isAdmin data-tip={$appSession.isAdmin
? $t('application.stop_application') ? 'Stop'
: $t('application.permission_denied_stop_application')} : $t('application.permission_denied_stop_application')}
> >
<svg <svg
@ -254,13 +270,37 @@
<rect x="14" y="5" width="4" height="14" rx="1" /> <rect x="14" y="5" width="4" height="14" rx="1" />
</svg> </svg>
</button> </button>
<button
on:click={restartApplication}
type="submit"
disabled={$disabledButton}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2 "
data-tip={$appSession.isAdmin
? 'Restart (useful for changing secrets)'
: $t('application.permission_denied_stop_application')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
</svg>
</button>
<form on:submit|preventDefault={() => handleDeploySubmit(true)}> <form on:submit|preventDefault={() => handleDeploySubmit(true)}>
<button <button
type="submit" type="submit"
disabled={$disabledButton} disabled={$disabledButton}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2" class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2"
data-tip={$appSession.isAdmin data-tip={$appSession.isAdmin
? 'Force Rebuild Application' ? 'Force Rebuild without cache'
: 'You do not have permission to rebuild application.'} : 'You do not have permission to rebuild application.'}
> >
<svg <svg
@ -517,6 +557,5 @@
> >
<DeleteIcon /> <DeleteIcon />
</button> </button>
{/if}
</nav> </nav>
<slot /> <slot />

View File

@ -60,11 +60,9 @@
import { errorNotification, handlerNotFoundLoad } from '$lib/common'; import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import { appSession, status, disabledButton } from '$lib/store'; import { appSession, status, disabledButton } from '$lib/store';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import Loading from '$lib/components/Loading.svelte';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
const { id } = $page.params; const { id } = $page.params;
let loading = false;
let statusInterval: any = false; let statusInterval: any = false;
$disabledButton = !$appSession.isAdmin; $disabledButton = !$appSession.isAdmin;
@ -72,36 +70,41 @@
async function deleteDatabase() { async function deleteDatabase() {
const sure = confirm(`Are you sure you would like to delete '${database.name}'?`); const sure = confirm(`Are you sure you would like to delete '${database.name}'?`);
if (sure) { if (sure) {
loading = true; $status.database.initialLoading = true;
try { try {
await del(`/databases/${database.id}`, { id: database.id }); await del(`/databases/${database.id}`, { id: database.id });
return await goto('/databases'); return await goto('/databases');
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading = false; $status.database.initialLoading = false;
} }
} }
} }
async function stopDatabase() { async function stopDatabase() {
const sure = confirm($t('database.confirm_stop', { name: database.name })); const sure = confirm($t('database.confirm_stop', { name: database.name }));
if (sure) { if (sure) {
loading = true; $status.database.initialLoading = true;
try { try {
await post(`/databases/${database.id}/stop`, {}); await post(`/databases/${database.id}/stop`, {});
return window.location.reload();
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally {
$status.database.initialLoading = false;
} }
} }
} }
async function startDatabase() { async function startDatabase() {
loading = true; $status.database.initialLoading = true;
$status.database.loading = true;
try { try {
await post(`/databases/${database.id}/start`, {}); await post(`/databases/${database.id}/start`, {});
return window.location.reload();
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally {
$status.database.initialLoading = false;
$status.database.loading = false;
await getStatus();
} }
} }
async function getStatus() { async function getStatus() {
@ -137,9 +140,6 @@
{#if id !== 'new'} {#if id !== 'new'}
<nav class="nav-side"> <nav class="nav-side">
{#if loading}
<Loading fullscreen cover />
{:else}
{#if database.type && database.destinationDockerId && database.version && database.defaultDatabase} {#if database.type && database.destinationDockerId && database.version && database.defaultDatabase}
{#if $status.database.isExited} {#if $status.database.isExited}
<a <a
@ -317,7 +317,6 @@
? $t('database.delete_database') ? $t('database.delete_database')
: $t('database.permission_denied_delete_database')}><DeleteIcon /></button : $t('database.permission_denied_delete_database')}><DeleteIcon /></button
> >
{/if}
</nav> </nav>
{/if} {/if}
<slot /> <slot />

View File

@ -1,4 +1,4 @@
<script> <script lang="ts">
export let name = ''; export let name = '';
export let value = ''; export let value = '';
export let isNewSecret = false; export let isNewSecret = false;

View File

@ -71,4 +71,8 @@
<a href="https://searxng.org" target="_blank"> <a href="https://searxng.org" target="_blank">
<Icons.Searxng /> <Icons.Searxng />
</a> </a>
{:else if service.type === 'weblate'}
<a href="https://weblate.org" target="_blank">
<Icons.Weblate />
</a>
{/if} {/if}

View File

@ -1,17 +1,51 @@
<script lang="ts"> <script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte'; import { addToast, status } from '$lib/store';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { post } from '$lib/api';
import { page } from '$app/stores';
import { errorNotification } from '$lib/common';
export let service: any; export let service: any;
function toggleEmailSmtpUseTls() {
service.glitchTip.emailSmtpUseTls = !service.glitchTip.emailSmtpUseTls; const { id } = $page.params;
let loading = false;
async function changeSettings(name: any) {
if (loading || $status.service.isRunning) return;
let enableOpenUserRegistration = service.glitchTip.enableOpenUserRegistration;
let emailSmtpUseSsl = service.glitchTip.emailSmtpUseSsl;
let emailSmtpUseTls = service.glitchTip.emailSmtpUseTls;
loading = true;
if (name === 'enableOpenUserRegistration') {
enableOpenUserRegistration = !enableOpenUserRegistration;
} }
function toggleEmailSmtpUseSsl() { if (name === 'emailSmtpUseSsl') {
service.glitchTip.emailSmtpUseSsl = !service.glitchTip.emailSmtpUseSsl; emailSmtpUseSsl = !emailSmtpUseSsl;
}
if (name === 'emailSmtpUseTls') {
emailSmtpUseTls = !emailSmtpUseTls;
}
try {
await post(`/services/${id}/glitchtip/settings`, {
enableOpenUserRegistration,
emailSmtpUseSsl,
emailSmtpUseTls
});
service.glitchTip.emailSmtpUseTls = emailSmtpUseTls;
service.glitchTip.emailSmtpUseSsl = emailSmtpUseSsl;
service.glitchTip.enableOpenUserRegistration = enableOpenUserRegistration;
return addToast({
message: 'Settings updated.',
type: 'success',
});
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
} }
function toggleEnableOpenUserRegistration() {
service.glitchTip.enableOpenUserRegistration = !service.glitchTip.enableOpenUserRegistration;
} }
</script> </script>
@ -19,76 +53,33 @@
<div class="title">GlitchTip</div> <div class="title">GlitchTip</div>
</div> </div>
<div class="flex space-x-1 py-2 font-bold">
<div class="subtitle">Settings</div>
</div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting
bind:setting={service.glitchTip.enableOpenUserRegistration}
{loading}
disabled={$status.service.isRunning}
on:click={() => changeSettings('enableOpenUserRegistration')}
title="Enable Open User Registration"
description={''}
/>
<!-- <Setting
bind:setting={service.glitchTip.enableOpenUserRegistration} bind:setting={service.glitchTip.enableOpenUserRegistration}
on:click={toggleEnableOpenUserRegistration} on:click={toggleEnableOpenUserRegistration}
title={'Enable Open User Registration'} title={'Enable Open User Registration'}
description={''} description={''}
/> /> -->
</div> </div>
<div class="flex space-x-1 py-2 font-bold"> <div class="flex space-x-1 py-2 font-bold">
<div class="subtitle">Email settings</div> <div class="subtitle">Email settings</div>
</div> </div>
<div class="grid grid-cols-2 items-center px-10">
<label for="defaultEmailFrom" class="text-base font-bold text-stone-100">Default Email From</label
>
<CopyPasswordField
required
name="defaultEmailFrom"
id="defaultEmailFrom"
value={service.glitchTip.defaultEmailFrom}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpHost" class="text-base font-bold text-stone-100">SMTP Host</label>
<CopyPasswordField
name="emailSmtpHost"
id="emailSmtpHost"
value={service.glitchTip.emailSmtpHost}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpPort" class="text-base font-bold text-stone-100">SMTP Port</label>
<CopyPasswordField
name="emailSmtpPort"
id="emailSmtpPort"
value={service.glitchTip.emailSmtpPort}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpUser" class="text-base font-bold text-stone-100">SMTP User</label>
<CopyPasswordField
name="emailSmtpUser"
id="emailSmtpUser"
value={service.glitchTip.emailSmtpUser}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpPassword" class="text-base font-bold text-stone-100">SMTP Password</label>
<CopyPasswordField
name="emailSmtpPassword"
id="emailSmtpPassword"
value={service.glitchTip.emailSmtpPassword}
isPasswordField
/>
</div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting
bind:setting={service.glitchTip.emailSmtpUseTls} bind:setting={service.glitchTip.emailSmtpUseTls}
on:click={toggleEmailSmtpUseTls} {loading}
title={'SMTP Use TLS'} disabled={$status.service.isRunning}
on:click={() => changeSettings('emailSmtpUseTls')}
title="Use TLS for SMTP"
description={''} description={''}
/> />
</div> </div>
@ -96,32 +87,84 @@
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting
bind:setting={service.glitchTip.emailSmtpUseSsl} bind:setting={service.glitchTip.emailSmtpUseSsl}
on:click={toggleEmailSmtpUseSsl} {loading}
title={'SMTP Use SSL'} disabled={$status.service.isRunning}
on:click={() => changeSettings('emailSmtpUseSsl')}
title="Use SSL for SMTP"
description={''} description={''}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="emailBackend" class="text-base font-bold text-stone-100">Email Backend</label> <label for="defaultEmailFrom">Default Email From</label>
<CopyPasswordField name="emailBackend" id="emailBackend" value={service.glitchTip.emailBackend} />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mailgunApiKey" class="text-base font-bold text-stone-100">Mailgun API Key</label>
<CopyPasswordField <CopyPasswordField
name="mailgunApiKey" required
id="mailgunApiKey" name="defaultEmailFrom"
value={service.glitchTip.mailgunApiKey} id="defaultEmailFrom"
bind:value={service.glitchTip.defaultEmailFrom}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="sendgridApiKey" class="text-base font-bold text-stone-100">SendGrid API Key</label> <label for="emailSmtpHost">SMTP Host</label>
<CopyPasswordField
name="emailSmtpHost"
id="emailSmtpHost"
bind:value={service.glitchTip.emailSmtpHost}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpPort">SMTP Port</label>
<CopyPasswordField
name="emailSmtpPort"
id="emailSmtpPort"
bind:value={service.glitchTip.emailSmtpPort}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpUser">SMTP User</label>
<CopyPasswordField
name="emailSmtpUser"
id="emailSmtpUser"
bind:value={service.glitchTip.emailSmtpUser}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailSmtpPassword">SMTP Password</label>
<CopyPasswordField
name="emailSmtpPassword"
id="emailSmtpPassword"
bind:value={service.glitchTip.emailSmtpPassword}
isPasswordField
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="emailBackend">Email Backend</label>
<CopyPasswordField
name="emailBackend"
id="emailBackend"
bind:value={service.glitchTip.emailBackend}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mailgunApiKey">Mailgun API Key</label>
<CopyPasswordField
name="mailgunApiKey"
id="mailgunApiKey"
bind:value={service.glitchTip.mailgunApiKey}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="sendgridApiKey">SendGrid API Key</label>
<CopyPasswordField <CopyPasswordField
name="sendgridApiKey" name="sendgridApiKey"
id="sendgridApiKey" id="sendgridApiKey"
value={service.glitchTip.sendgridApiKey} bind:value={service.glitchTip.sendgridApiKey}
/> />
</div> </div>
@ -130,35 +173,31 @@
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="defaultEmail" class="text-base font-bold text-stone-100">{$t('forms.email')}</label> <label for="defaultEmail">{$t('forms.email')}</label>
<CopyPasswordField <CopyPasswordField
name="defaultEmail" name="defaultEmail"
id="defaultEmail" id="defaultEmail"
value={service.glitchTip.defaultEmail} bind:value={service.glitchTip.defaultEmail}
readonly readonly
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="defaultUsername" class="text-base font-bold text-stone-100" <label for="defaultUsername">{$t('forms.username')}</label>
>{$t('forms.username')}</label
>
<CopyPasswordField <CopyPasswordField
name="defaultUsername" name="defaultUsername"
id="defaultUsername" id="defaultUsername"
value={service.glitchTip.defaultUsername} bind:value={service.glitchTip.defaultUsername}
readonly readonly
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="defaultPassword" class="text-base font-bold text-stone-100" <label for="defaultPassword">{$t('forms.password')}</label>
>{$t('forms.password')}</label
>
<CopyPasswordField <CopyPasswordField
name="defaultPassword" name="defaultPassword"
id="defaultPassword" id="defaultPassword"
value={service.glitchTip.defaultPassword} bind:value={service.glitchTip.defaultPassword}
readonly readonly
disabled disabled
isPasswordField isPasswordField
@ -170,38 +209,32 @@
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlUser" class="text-base font-bold text-stone-100" <label for="postgresqlUser">{$t('forms.username')}</label>
>{$t('forms.username')}</label
>
<CopyPasswordField <CopyPasswordField
name="postgresqlUser" name="postgresqlUser"
id="postgresqlUser" id="postgresqlUser"
value={service.glitchTip.postgresqlUser} bind:value={service.glitchTip.postgresqlUser}
readonly readonly
disabled disabled
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlPassword" class="text-base font-bold text-stone-100" <label for="postgresqlPassword">{$t('forms.password')}</label>
>{$t('forms.password')}</label
>
<CopyPasswordField <CopyPasswordField
id="postgresqlPassword" id="postgresqlPassword"
isPasswordField isPasswordField
readonly readonly
disabled disabled
name="postgresqlPassword" name="postgresqlPassword"
value={service.glitchTip.postgresqlPassword} bind:value={service.glitchTip.postgresqlPassword}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlDatabase" class="text-base font-bold text-stone-100" <label for="postgresqlDatabase">{$t('index.database')}</label>
>{$t('index.database')}</label
>
<CopyPasswordField <CopyPasswordField
name="postgresqlDatabase" name="postgresqlDatabase"
id="postgresqlDatabase" id="postgresqlDatabase"
value={service.glitchTip.postgresqlDatabase} bind:value={service.glitchTip.postgresqlDatabase}
readonly readonly
disabled disabled
/> />

View File

@ -30,6 +30,7 @@
import Appwrite from './_Appwrite.svelte'; import Appwrite from './_Appwrite.svelte';
import Moodle from './_Moodle.svelte'; import Moodle from './_Moodle.svelte';
import Searxng from './_Searxng.svelte'; import Searxng from './_Searxng.svelte';
import Weblate from './_Weblate.svelte';
const { id } = $page.params; const { id } = $page.params;
$: isDisabled = $: isDisabled =
@ -405,6 +406,8 @@
<GlitchTip bind:service /> <GlitchTip bind:service />
{:else if service.type === 'searxng'} {:else if service.type === 'searxng'}
<Searxng bind:service /> <Searxng bind:service />
{:else if service.type === 'weblate'}
<Weblate bind:service />
{/if} {/if}
</div> </div>
</form> </form>

View File

@ -0,0 +1,66 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
export let service: any;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Weblate</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="adminPassword">Admin password</label>
<CopyPasswordField
name="adminPassword"
id="adminPassword"
isPasswordField
value={service.weblate.adminPassword}
readonly
disabled
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlHost">PostgreSQL Host</label>
<CopyPasswordField
name="postgresqlHost"
id="postgresqlHost"
value={service.weblate.postgresqlHost}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlPort">PostgreSQL Port</label>
<CopyPasswordField
name="postgresqlPort"
id="postgresqlPort"
value={service.weblate.postgresqlPort}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlUser">PostgreSQL User</label>
<CopyPasswordField
name="postgresqlUser"
id="postgresqlUser"
value={service.weblate.postgresqlUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlPassword">PostgreSQL Password</label>
<CopyPasswordField
name="postgresqlPassword"
id="postgresqlPassword"
isPasswordField
value={service.weblate.postgresqlPassword}
readonly
disabled
/>
</div>

View File

@ -56,7 +56,6 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import Loading from '$lib/components/Loading.svelte';
import { del, get, post } from '$lib/api'; import { del, get, post } from '$lib/api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
@ -74,13 +73,12 @@
!service.version || !service.version ||
!service.type; !service.type;
let loading = false;
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) {
loading = true; $status.service.initialLoading = true;
try { try {
if (service.type && $status.service.isRunning) if (service.type && $status.service.isRunning)
await post(`/services/${service.id}/${service.type}/stop`, {}); await post(`/services/${service.id}/${service.type}/stop`, {});
@ -89,31 +87,36 @@
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading = false; $status.service.initialLoading = false;
} }
} }
} }
async function stopService() { async function stopService() {
const sure = confirm($t('database.confirm_stop', { name: service.name })); const sure = confirm($t('database.confirm_stop', { name: service.name }));
if (sure) { if (sure) {
loading = true; $status.service.initialLoading = true;
$status.service.loading = true;
try { try {
await post(`/services/${service.id}/${service.type}/stop`, {}); await post(`/services/${service.id}/${service.type}/stop`, {});
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading = false; $status.service.initialLoading = false;
$status.service.loading = false;
} }
} }
} }
async function startService() { async function startService() {
loading = true; $status.service.initialLoading = true;
$status.service.loading = true
try { try {
await post(`/services/${service.id}/${service.type}/start`, {}); await post(`/services/${service.id}/${service.type}/start`, {});
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading = false; $status.service.initialLoading = false;
$status.service.loading = false;
await getStatus();
} }
} }
async function getStatus() { async function getStatus() {
@ -146,9 +149,6 @@
</script> </script>
<nav class="nav-side"> <nav class="nav-side">
{#if loading}
<Loading fullscreen cover />
{:else}
{#if service.type && service.destinationDockerId && service.version} {#if service.type && service.destinationDockerId && service.version}
{#if $location} {#if $location}
<a <a
@ -409,6 +409,5 @@
? $t('service.delete_service') ? $t('service.delete_service')
: $t('service.permission_denied_delete_service')}><DeleteIcon /></button : $t('service.permission_denied_delete_service')}><DeleteIcon /></button
> >
{/if}
</nav> </nav>
<slot /> <slot />

View File

@ -25,14 +25,47 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
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 ServiceLinks from './_ServiceLinks.svelte'; import ServiceLinks from './_ServiceLinks.svelte';
import { addToast } from '$lib/store';
import { saveSecret } from './utils';
const limit = pLimit(1);
const { id } = $page.params; const { id } = $page.params;
let batchSecrets = '';
async function refreshSecrets() { async function refreshSecrets() {
const data = await get(`/services/${id}/secrets`); const data = await get(`/services/${id}/secrets`);
secrets = [...data.secrets]; secrets = [...data.secrets];
} }
async function getValues(e: any) {
e.preventDefault();
const eachValuePair = batchSecrets.split('\n');
const batchSecretsPairs = eachValuePair
.filter((secret) => !secret.startsWith('#') && secret)
.map((secret) => {
const [name, ...rest] = secret.split('=');
const value = rest.join('=');
const cleanValue = value?.replaceAll('"', '') || '';
return {
name,
value: cleanValue,
isNew: !secrets.find((secret: any) => name === secret.name)
};
});
await Promise.all(
batchSecretsPairs.map(({ name, value, isNew }) =>
limit(() => saveSecret({ name, value, serviceId: id, isNew }))
)
);
batchSecrets = '';
await refreshSecrets();
addToast({
message: 'Secrets saved.',
type: 'success'
});
}
</script> </script>
<div <div
@ -93,4 +126,9 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<h2 class="title my-6 font-bold">Paste .env file</h2>
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
<textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" />
<button class="btn btn-sm bg-applications" type="submit">Batch add secrets</button>
</form>
</div> </div>

View File

@ -0,0 +1,42 @@
import { post } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
type Props = {
isNew: boolean;
name: string;
value: string;
isBuildSecret?: boolean;
isPRMRSecret?: boolean;
isNewSecret?: boolean;
serviceId: string;
};
export async function saveSecret({
isNew,
name,
value,
isBuildSecret,
isPRMRSecret,
isNewSecret,
serviceId
}: Props): Promise<void> {
if (!name) return errorNotification(`${t.get('forms.name')} ${t.get('forms.is_required')}`);
if (!value) return errorNotification(`${t.get('forms.value')} ${t.get('forms.is_required')}`);
try {
await post(`/services/${serviceId}/secrets`, {
name,
value,
isBuildSecret,
isPRMRSecret,
isNew: isNew || false
});
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
} catch (error) {
throw error
}
}

View File

@ -1,11 +1,12 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "3.8.9", "version": "3.9.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": "github:coollabsio/coolify", "repository": "github:coollabsio/coolify",
"scripts": { "scripts": {
"oc": "opencollective-setup", "oc": "opencollective-setup",
"translate": "pnpm run --filter i18n-converter translate",
"db:studio": "pnpm run --filter api db:studio", "db:studio": "pnpm run --filter api db:studio",
"db:push": "pnpm run --filter api db:push", "db:push": "pnpm run --filter api db:push",
"db:seed": "pnpm run --filter api db:seed", "db:seed": "pnpm run --filter api db:seed",

View File

@ -23,7 +23,7 @@ importers:
'@fastify/static': 6.5.0 '@fastify/static': 6.5.0
'@iarna/toml': 2.2.5 '@iarna/toml': 2.2.5
'@ladjs/graceful': 3.0.2 '@ladjs/graceful': 3.0.2
'@prisma/client': 4.2.1 '@prisma/client': 3.15.2
'@types/node': 18.7.13 '@types/node': 18.7.13
'@types/node-os-utils': 1.3.0 '@types/node-os-utils': 1.3.0
'@typescript-eslint/eslint-plugin': 5.35.1 '@typescript-eslint/eslint-plugin': 5.35.1
@ -56,7 +56,7 @@ importers:
p-all: 4.0.0 p-all: 4.0.0
p-throttle: 5.0.0 p-throttle: 5.0.0
prettier: 2.7.1 prettier: 2.7.1
prisma: 4.2.1 prisma: 3.15.2
public-ip: 6.0.1 public-ip: 6.0.1
rimraf: 3.0.2 rimraf: 3.0.2
ssh-config: 4.1.6 ssh-config: 4.1.6
@ -74,7 +74,7 @@ importers:
'@fastify/static': 6.5.0 '@fastify/static': 6.5.0
'@iarna/toml': 2.2.5 '@iarna/toml': 2.2.5
'@ladjs/graceful': 3.0.2 '@ladjs/graceful': 3.0.2
'@prisma/client': 4.2.1_prisma@4.2.1 '@prisma/client': 3.15.2_prisma@3.15.2
axios: 0.27.2 axios: 0.27.2
bcryptjs: 2.4.3 bcryptjs: 2.4.3
bree: 9.1.2 bree: 9.1.2
@ -112,11 +112,23 @@ importers:
eslint-plugin-prettier: 4.2.1_tgumt6uwl2md3n6uqnggd6wvce eslint-plugin-prettier: 4.2.1_tgumt6uwl2md3n6uqnggd6wvce
nodemon: 2.0.19 nodemon: 2.0.19
prettier: 2.7.1 prettier: 2.7.1
prisma: 4.2.1 prisma: 3.15.2
rimraf: 3.0.2 rimraf: 3.0.2
tsconfig-paths: 4.1.0 tsconfig-paths: 4.1.0
typescript: 4.7.4 typescript: 4.7.4
apps/i18n:
specifiers:
dotenv: 16.0.2
gettext-parser: 6.0.0
got: 12.3.1
node-gettext: 3.0.0
dependencies:
dotenv: 16.0.2
gettext-parser: 6.0.0
got: 12.3.1
node-gettext: 3.0.0
apps/ui: apps/ui:
specifiers: specifiers:
'@playwright/test': 1.25.1 '@playwright/test': 1.25.1
@ -446,9 +458,9 @@ packages:
playwright-core: 1.25.1 playwright-core: 1.25.1
dev: true dev: true
/@prisma/client/4.2.1_prisma@4.2.1: /@prisma/client/3.15.2_prisma@3.15.2:
resolution: {integrity: sha512-PZBkY60+k5oix+e6IUfl3ub8TbRLNsPLdfWrdy2eh80WcHTaT+/UfvXf/B7gXedH7FRtbPFHZXk1hZenJiJZFQ==} resolution: {integrity: sha512-ErqtwhX12ubPhU4d++30uFY/rPcyvjk+mdifaZO5SeM21zS3t4jQrscy8+6IyB0GIYshl5ldTq6JSBo1d63i8w==}
engines: {node: '>=14.17'} engines: {node: '>=12.6'}
requiresBuild: true requiresBuild: true
peerDependencies: peerDependencies:
prisma: '*' prisma: '*'
@ -456,16 +468,16 @@ packages:
prisma: prisma:
optional: true optional: true
dependencies: dependencies:
'@prisma/engines-version': 4.2.0-33.2920a97877e12e055c1333079b8d19cee7f33826 '@prisma/engines-version': 3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e
prisma: 4.2.1 prisma: 3.15.2
dev: false dev: false
/@prisma/engines-version/4.2.0-33.2920a97877e12e055c1333079b8d19cee7f33826: /@prisma/engines-version/3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e:
resolution: {integrity: sha512-tktkqdiwqE4QhmE088boPt+FwPj1Jub/zk+5F6sEfcRHzO5yz9jyMD5HFVtiwxZPLx/8Xg9ElnuTi8E5lWVQFQ==} resolution: {integrity: sha512-e3k2Vd606efd1ZYy2NQKkT4C/pn31nehyLhVug6To/q8JT8FpiMrDy7zmm3KLF0L98NOQQcutaVtAPhzKhzn9w==}
dev: false dev: false
/@prisma/engines/4.2.1: /@prisma/engines/3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e:
resolution: {integrity: sha512-0KqBwREUOjBiHwITsQzw2DWfLHjntvbqzGRawj4sBMnIiL5CXwyDUKeHOwXzKMtNr1rEjxEsypM14g0CzLRK3g==} resolution: {integrity: sha512-NHlojO1DFTsSi3FtEleL9QWXeSF/UjhCW0fgpi7bumnNZ4wj/eQ+BJJ5n2pgoOliTOGv9nX2qXvmHap7rJMNmg==}
requiresBuild: true requiresBuild: true
/@rollup/pluginutils/4.2.1: /@rollup/pluginutils/4.2.1:
@ -2101,6 +2113,11 @@ packages:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
dev: false dev: false
/content-type/1.0.4:
resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==}
engines: {node: '>= 0.6'}
dev: false
/convert-hrtime/3.0.0: /convert-hrtime/3.0.0:
resolution: {integrity: sha512-7V+KqSvMiHp8yWDuwfww06XleMWVVB9b9tURBx+G7UTADuo5hYPuowKloz4OzOqbPezxgo+fdQ1522WzPG4OeA==} resolution: {integrity: sha512-7V+KqSvMiHp8yWDuwfww06XleMWVVB9b9tURBx+G7UTADuo5hYPuowKloz4OzOqbPezxgo+fdQ1522WzPG4OeA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -2448,6 +2465,11 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dev: false dev: false
/dotenv/16.0.2:
resolution: {integrity: sha512-JvpYKUmzQhYoIFgK2MOnF3bciIZoItIIoryihy0rIA+H4Jy0FmgyKYAHCTN98P5ybGSJcIFbh6QKeJdtZd1qhA==}
engines: {node: '>=12'}
dev: false
/dotenv/8.6.0: /dotenv/8.6.0:
resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -2481,6 +2503,12 @@ packages:
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
dev: false dev: false
/encoding/0.1.13:
resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
dependencies:
iconv-lite: 0.6.3
dev: false
/end-of-stream/1.4.4: /end-of-stream/1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
dependencies: dependencies:
@ -3560,6 +3588,15 @@ packages:
get-intrinsic: 1.1.1 get-intrinsic: 1.1.1
dev: true dev: true
/gettext-parser/6.0.0:
resolution: {integrity: sha512-eWFsR78gc/eKnzDgc919Us3cbxQbzxK1L8vAIZrKMQqOUgULyeqmczNlBjTlVTk2FaB7nV9C1oobd/PGBOqNmg==}
dependencies:
content-type: 1.0.4
encoding: 0.1.13
readable-stream: 4.1.0
safe-buffer: 5.2.1
dev: false
/glob-parent/5.1.2: /glob-parent/5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -3759,6 +3796,13 @@ packages:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
dev: true dev: true
/iconv-lite/0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
dependencies:
safer-buffer: 2.1.2
dev: false
/ieee754/1.2.1: /ieee754/1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: false dev: false
@ -4264,6 +4308,10 @@ packages:
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
dev: false dev: false
/lodash.get/4.4.2:
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
dev: false
/lodash.includes/4.3.0: /lodash.includes/4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
dev: false dev: false
@ -4535,6 +4583,12 @@ packages:
engines: {node: '>= 6.13.0'} engines: {node: '>= 6.13.0'}
dev: false dev: false
/node-gettext/3.0.0:
resolution: {integrity: sha512-/VRYibXmVoN6tnSAY2JWhNRhWYJ8Cd844jrZU/DwLVoI4vBI6ceYbd8i42sYZ9uOgDH3S7vslIKOWV/ZrT2YBA==}
dependencies:
lodash.get: 4.4.2
dev: false
/node-os-utils/1.3.7: /node-os-utils/1.3.7:
resolution: {integrity: sha512-fvnX9tZbR7WfCG5BAy3yO/nCLyjVWD6MghEq0z5FDfN+ZXpLWNITBdbifxQkQ25ebr16G0N7eRWJisOcMEHG3Q==} resolution: {integrity: sha512-fvnX9tZbR7WfCG5BAy3yO/nCLyjVWD6MghEq0z5FDfN+ZXpLWNITBdbifxQkQ25ebr16G0N7eRWJisOcMEHG3Q==}
dev: false dev: false
@ -5071,13 +5125,13 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/prisma/4.2.1: /prisma/3.15.2:
resolution: {integrity: sha512-HuYqnTDgH8atjPGtYmY0Ql9XrrJnfW7daG1PtAJRW0E6gJxc50lY3vrIDn0yjMR3TvRlypjTcspQX8DT+xD4Sg==} resolution: {integrity: sha512-nMNSMZvtwrvoEQ/mui8L/aiCLZRCj5t6L3yujKpcDhIPk7garp8tL4nMx2+oYsN0FWBacevJhazfXAbV1kfBzA==}
engines: {node: '>=14.17'} engines: {node: '>=12.6'}
hasBin: true hasBin: true
requiresBuild: true requiresBuild: true
dependencies: dependencies:
'@prisma/engines': 4.2.1 '@prisma/engines': 3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e
/private/0.1.8: /private/0.1.8:
resolution: {integrity: sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==} resolution: {integrity: sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==}
@ -5210,6 +5264,13 @@ packages:
abort-controller: 3.0.0 abort-controller: 3.0.0
dev: false dev: false
/readable-stream/4.1.0:
resolution: {integrity: sha512-sVisi3+P2lJ2t0BPbpK629j8wRW06yKGJUcaLAGXPAUhyUxVJm7VsCTit1PFgT4JHUDMrGNR+ZjSKpzGaRF3zw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
abort-controller: 3.0.0
dev: false
/readdirp/3.6.0: /readdirp/3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'} engines: {node: '>=8.10.0'}