Compare commits

...

52 Commits

Author SHA1 Message Date
Andras Bacsai
5ccea1cfcc Merge pull request #370 from coollabsio/next
v2.5.2
2022-04-25 17:49:07 +02:00
Andras Bacsai
8ccb1bd34c show autoupdate in localhost 2022-04-25 17:48:25 +02:00
Andras Bacsai
c1a48dcf1e feat: Autoupdater 2022-04-25 15:51:43 +02:00
Andras Bacsai
11d74c0c1f feat: Coolify auto-updater 2022-04-25 09:54:28 +02:00
Andras Bacsai
8290ee856f migration for umami 2022-04-25 09:11:49 +02:00
Andras Bacsai
08332c8321 fix: Contribution guide 2022-04-25 08:55:04 +02:00
Andras Bacsai
046f738b7d feat: Umami service 2022-04-25 08:54:53 +02:00
Andras Bacsai
07708155ac WIP: Umami service 2022-04-25 00:00:06 +02:00
Andras Bacsai
df5e23c7c2 fix: Contribution guide 2022-04-24 00:27:27 +02:00
Andras Bacsai
41adc02801 fix: Contribution 2022-04-24 00:25:38 +02:00
Andras Bacsai
72b650b086 fix: Simplify list services 2022-04-24 00:24:08 +02:00
Andras Bacsai
06fe3f33c0 fix: Contribution guide 2022-04-24 00:23:35 +02:00
Andras Bacsai
cbabf7fc51 chore: version++ 2022-04-23 18:46:00 +02:00
Andras Bacsai
6aeafda604 fix: Reactivate posgtres password 2022-04-23 16:12:16 +02:00
Andras Bacsai
30d656698e Merge pull request #369 from coollabsio/next
v2.5.1
2022-04-23 13:16:35 +02:00
Andras Bacsai
94d1af01df Merge pull request #365 from coollabsio/restray-restray_i18n
v2.5.1
2022-04-23 13:15:13 +02:00
Andras Bacsai
af97d399b6 fix: Code cleanups 2022-04-22 13:57:28 +02:00
Andras Bacsai
2f90fd1fe6 fix: No logs found 2022-04-22 11:44:04 +02:00
Andras Bacsai
c05a140b0b fix: GitHub token cleanup on team switch 2022-04-22 11:43:55 +02:00
Andras Bacsai
cbfb9a3844 chore: version++ 2022-04-22 11:20:15 +02:00
Andras Bacsai
5a227f70c6 fix: Do not activate i18n for now 2022-04-22 11:19:56 +02:00
Andras Bacsai
44a102443d fix: Application logs is not reversed and queried better 2022-04-21 23:07:54 +02:00
Andras Bacsai
cf7fdf198d fix: locales 2022-04-21 14:57:52 +02:00
Andras Bacsai
68f2f4f978 fix: Vscode permission fix 2022-04-21 11:25:51 +02:00
Andras Bacsai
029b623f08 fix: i18n 2022-04-21 10:05:27 +02:00
Andras Bacsai
fe3702847a Merge branch 'restray_i18n' of https://github.com/restray/coolify into restray-restray_i18n 2022-04-21 09:51:29 +02:00
Restray
c39cb42601 feat (i18n) : go back i18n loading json files 2022-04-04 17:56:32 +02:00
Restray
0ead17ab70 Patch translation module not loaded 2022-04-04 17:06:03 +02:00
Restray
4a6062522e Merge branch 'main' into restray_i18n 2022-04-04 16:54:51 +02:00
Restray
bd15d85732 Add last translations for 2.3.0 2022-04-04 12:37:24 +02:00
Restray
b4bbd22781 Merge branch 'restray_i18n' of github.com:restray/coolify into restray_i18n 2022-04-04 12:31:48 +02:00
Restray
d4c972584a Add english translation for register page 2022-04-04 12:30:22 +02:00
Restray
edef4bd4a0 Merge branch 'main' into restray_i18n 2022-04-04 12:29:20 +02:00
Restray
448611039c Patch langs problems 2022-04-04 11:50:54 +02:00
Restray
e4f701b148 Add auto detect of locales files and contrib guide 2022-04-03 21:47:58 +02:00
Restray
8cd561b8cc Update french translations 2022-04-03 19:58:10 +02:00
Restray
a284928352 Patch flags and add french translation 2022-04-03 14:34:48 +02:00
Restray
fe787538e3 Finish routes translations 2022-04-03 14:14:59 +02:00
Restray
360fb5ea37 Add services i18n 2022-04-03 00:18:48 +02:00
Restray
13891110ce Add reset i18n 2022-04-02 23:57:37 +02:00
Restray
c1c25d59c8 Add "new" i18n 2022-04-02 23:53:10 +02:00
Restray
a53bda1436 Add destinations i18n 2022-04-02 23:34:30 +02:00
Restray
7a0d151467 Add database translation 2022-04-02 23:17:59 +02:00
Restray
a788b7bc13 Add translation for applications components 2022-04-02 23:00:03 +02:00
Restray
8f58b14629 Add application i18n 2022-04-02 22:04:50 +02:00
Restray
269250ef3d Begin translation and finish i18n system 2022-04-02 21:08:55 +02:00
Restray
a3241516cb Change the way to load i18n (go throw cookie) 2022-04-02 20:25:24 +02:00
Restray
943300509b Revert "Add Locale URL"
This reverts commit d910b21185.
2022-04-02 19:29:22 +02:00
Restray
92d1f5aa55 Revert "Patch bugs on locale redirections"
This reverts commit 614eb923d8.
2022-04-02 19:28:20 +02:00
Restray
614eb923d8 Patch bugs on locale redirections 2022-04-02 17:33:50 +02:00
Restray
d910b21185 Add Locale URL 2022-04-02 16:15:00 +02:00
Restray
741db1778b feat: install svelte-18n and init setup 2022-04-01 22:50:55 +02:00
108 changed files with 2392 additions and 663 deletions

View File

@@ -2,5 +2,7 @@ COOLIFY_APP_ID=
COOLIFY_SECRET_KEY=12341234123412341234123412341234
COOLIFY_DATABASE_URL=file:../db/dev.db
COOLIFY_SENTRY_DSN=
COOLIFY_IS_ON="docker"
COOLIFY_WHITE_LABELED="false"
COOLIFY_IS_ON=docker
COOLIFY_WHITE_LABELED=false
COOLIFY_WHITE_LABELED_ICON=
COOLIFY_AUTO_UPDATE=false

11
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"i18n-ally.localesPaths": ["src/lib/locales"],
"i18n-ally.keystyle": "nested",
"i18n-ally.extract.ignoredByFiles": {
"src\\routes\\__layout.svelte": ["Coolify", "coolLabs logo"]
},
"i18n-ally.sourceLanguage": "en",
"i18n-ally.enabledFrameworks": ["svelte"],
"i18n-ally.enabledParsers": ["js", "ts", "json"],
"i18n-ally.extract.autoDetect": true
}

View File

@@ -1,14 +1,23 @@
# Welcome
# 👋 Welcome
First of all, thank you for considering contributing to my project! It means a lot 💜.
# Technical skills required
## 🙋 Want to help?
- Node.js / Javascript
- Svelte / SvelteKit
- Prisma.io / SQL
If you begin in GitHub contribution, you can find the [first contribution](https://github.com/firstcontributions/first-contributions) and follow this guide.
# Recommended Pull Request Guideline
Follow the [introduction](#introduction) to get started then start contributing!
This is a little list of what you can do to help the project:
- [🧑‍💻 Develop your own ideas](#developer-contribution)
- [🌐 Translate the project](#translation)
## 👋 Introduction
🔴 At the moment, Coolify **doesn't support Windows**. You must use Linux or MacOS.
#### Recommended Pull Request Guideline
- Fork the project
- Clone your fork repo to local
@@ -26,7 +35,7 @@ Due to the lock file, this repository is best with [pnpm](https://pnpm.io). I re
You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally.
## Setup development environment
#### Setup a local development environment
- Copy `.env.template` to `.env` and set the `COOLIFY_APP_ID` environment variable to something cool.
- Install dependencies with `pnpm install`.
@@ -35,13 +44,28 @@ You need to have [Docker Engine](https://docs.docker.com/engine/install/) instal
- Seed the database with base entities with `pnpm db:seed`
- You can start coding after starting `pnpm dev`.
## Database migrations
#### How to start after you set up your local fork?
This repository works better with [pnpm](https://pnpm.io) due to the lock file. I recommend you to give it a try and use `pnpm` as well because it is cool and efficient!
You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally.
## 🧑‍💻 Developer contribution
### Technical skills required
- **Languages**: Node.js / Javascript / Typescript
- **Framework JS/TS**: Svelte / SvelteKit
- **Database ORM**: Prisma.io
- **Docker Engine**
### Database migrations
During development, if you change the database layout, you need to run `pnpm db:push` to migrate the database and create types for Prisma. You also need to restart the development process.
If the schema is finalized, you need to create a migration file with `pnpm db:migrate <nameOfMigration>` where `nameOfMigration` is given by you. Make it sense. :)
## Tricky parts
### Tricky parts
- BullMQ, the queue system Coolify uses, cannot be hot reloaded. So if you change anything in the files related to it, you need to restart the development process. I'm actively looking for a different queue/scheduler library. I'm open to discussion!
@@ -57,50 +81,159 @@ You can add any open-source and self-hostable software (service/application) to
## Backend
I use MinIO as an example.
There are 5 steps you should make on the backend side.
You need to add a new folder to [src/routes/services/[id]](src/routes/services/[id]) with the low-capital name of the service. It should have three files with the following properties:
1. Create Prisma / database schema for the new service.
2. Add supported versions of the service.
3. Update global functions.
4. Create API endpoints.
5. Define automatically generated variables.
1. `index.json.ts`: A POST endpoint that updates Coolify's database about the service.
> I will use [Umami](https://umami.is/) as an example service.
Basic services only require updating the URL(fqdn) and the name of the service.
### Create Prisma / database schema for the new service.
2. `start.json.ts`: A start endpoint that setups the docker-compose file (for Local Docker Engines) and starts the service.
You only need to do this if you store passwords or any persistent configuration. Mostly it is required by all services, but there are some exceptions, like NocoDB.
- To start a service, you need to know Coolify supported images and tags of the service. For that you need to update `supportedServiceTypesAndVersions` function at [src/lib/components/common.ts](src/lib/components/common.ts).
Update Prisma schema in [prisma/schema.prisma](prisma/schema.prisma).
Example JSON:
- Add new model with the new service name.
- Make a relationshup with `Service` model.
- In the `Service` model, the name of the new field should be with low-capital.
- If the service needs a database, define a `publicPort` field to be able to make it's database public, example field name in case of PostgreSQL: `postgresqlPublicPort`. It should be a optional field.
```js
If you are finished with the Prisma schema, you should update the database schema with `pnpm db:push` command.
> You must restart the running development environment to be able to use the new model
> If you use VSCode, you probably need to restart the `Typescript Language Server` to get the new types loaded in the running VSCode.
### Add supported versions
Supported versions are hardcoded into Coolify (for now).
You need to update `supportedServiceTypesAndVersions` function at [src/lib/components/common.ts](src/lib/components/common.ts). Example JSON:
```js
{
// Name used to identify the service in Coolify
name: 'minio',
// Name used to identify the service internally
name: 'umami',
// Fancier name to show to the user
fancyName: 'MinIO',
fancyName: 'Umami',
// Docker base image for the service
baseImage: 'minio/minio',
baseImage: 'ghcr.io/mikecao/umami',
// Optional: If there is any dependent image, you should list it here
images: [],
// Usable tags
versions: ['latest'],
versions: ['postgresql-latest'],
// Which tag is the recommended
recommendedVersion: 'latest',
// Application's default port, MinIO listens on 9001 (and 9000, more details later on)
recommendedVersion: 'postgresql-latest',
// Application's default port, Umami listens on 3000
ports: {
main: 9001
main: 3000
}
},
```
}
```
- You need to define a compose file as `const composeFile: ComposeFile` found in [src/routes/services/[id]/minio/start.json.ts](src/routes/services/[id]/minio/start.json.ts)
### Update global functions
**IMPORTANT:** It should contain `all the default environment variables` that are required for the service to function correctly and `all the volumes to persist data` in restarts.
1. Add the new service to the `include` variable in [src/lib/database/services.ts](src/lib/database/services.ts), so it will be included in all places in the database queries where it is required.
- You could also define an `HTTP` or `TCP` proxy for every other port that should be proxied to your server. (See `startHttpProxy` and `startTcpProxy` functions in [src/lib/haproxy/index.ts](src/lib/haproxy/index.ts))
```js
const include: Prisma.ServiceInclude = {
destinationDocker: true,
persistentStorage: true,
serviceSecret: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true,
meiliSearch: true,
umami: true // This line!
};
```
3. `stop.json.ts` A stop endpoint that stops the service.
2. Update the database update query with the new service type to `configureServiceType` function in [src/lib/database/services.ts](src/lib/database/services.ts). This function defines the automatically generated variables (passwords, users, etc.) and it's encryption process (if applicable).
It needs to stop all the services by their container name and proxies (if applicable).
```js
[...]
else if (type === 'umami') {
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword());
const postgresqlDatabase = 'umami';
const hashSalt = encrypt(generatePassword(64));
await prisma.service.update({
where: { id },
data: {
type,
umami: {
create: {
postgresqlDatabase,
postgresqlPassword,
postgresqlUser,
hashSalt,
}
}
}
});
}
```
4. You need to add the automatically generated variables (passwords, users, etc.) for the new service at [src/lib/database/services.ts](src/lib/database/services.ts), `configureServiceType` function.
3. Add decryption process for configurations and passwords to `getService` function in [src/lib/database/services.ts](src/lib/database/services.ts)
```js
if (body.umami?.postgresqlPassword)
body.umami.postgresqlPassword = decrypt(body.umami.postgresqlPassword);
if (body.umami?.hashSalt) body.umami.hashSalt = decrypt(body.umami.hashSalt);
```
4. Add service deletion query to `removeService` function in [src/lib/database/services.ts](src/lib/database/services.ts)
### Create API endpoints.
You need to add a new folder under [src/routes/services/[id]](src/routes/services/[id]) with the low-capital name of the service. You need 3 default files in that folder.
#### `index.json.ts`:
It has a POST endpoint that updates the service details in Coolify's database, such as name, url, other configurations, like passwords. It should look something like this:
```js
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let { name, fqdn } = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase();
try {
await db.updateService({ id, fqdn, name });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);
}
};
```
If it's necessary, you can create your own database update function, specifically for the new service.
#### `start.json.ts`
It has a POST endpoint that sets all the required secrets, persistent volumes, `docker-compose.yaml` file and sends a request to the specified docker engine.
You could also define an `HTTP` or `TCP` proxy for every other port that should be proxied to your server. (See `startHttpProxy` and `startTcpProxy` functions in [src/lib/haproxy/index.ts](src/lib/haproxy/index.ts))
#### `stop.json.ts`
It has a POST endpoint that stops the service and all dependent (TCP/HTTP proxies) containers. If publicPort is specified it also needs to cleanup it from the database.
## Frontend
@@ -108,10 +241,37 @@ You need to add a new folder to [src/routes/services/[id]](src/routes/services/[
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.
2. You need to include it the logo at [src/routes/services/index.svelte](src/routes/services/index.svelte) with `isAbsolute` and [src/lib/components/ServiceLinks.svelte](src/lib/components/ServiceLinks.svelte) with a link to the docs/main site of the service.
2. You need to include it the logo at
- [src/routes/services/index.svelte](src/routes/services/index.svelte) with `isAbsolute` in two places,
- [src/lib/components/ServiceLinks.svelte](src/lib/components/ServiceLinks.svelte) with `isAbsolute` and a link to the docs/main site of the service
- [src/routes/services/[id]/configuration/type.svelte](src/routes/services/[id]/configuration/type.svelte) with `isAbsolute`.
3. By default the URL and the name frontend forms are included in [src/routes/services/[id]/\_Services/\_Services.svelte](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 [src/routes/services/[id]/\_Services](src/routes/services/[id]/_Services) with an underscore. For example, see other files in that folder.
You also need to add the new inputs to the `index.json.ts` file of the specific service, like for MinIO here: [src/routes/services/[id]/minio/index.json.ts](src/routes/services/[id]/minio/index.json.ts)
## 🌐 Translate the project
The project use [sveltekit-i18n](https://github.com/sveltekit-i18n/lib) to translate the project.
It follows the [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to name languages.
### Installation
You must have gone throw all the [intro](#introduction) steps before you can start translating.
It's only an advice, but I recommend you to use:
- Visual Studio Code
- [i18n Ally for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally): ideal to see the progress of the translation.
- [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode): to get the syntax color for the project
### Adding a language
If your language doesn't appear in the [locales folder list](src/lib/locales/), follow the step below:
1. In `src/lib/locales/`, Copy paste `en.json` and rename it with your language (eg: `cz.json`).
2. In the [lang.json](src/lib/lang.json) file, add a line after the first bracket (`{`) with `"ISO of your language": "Language",` (eg: `"cz": "Czech",`).
3. Have fun translating!

View File

@@ -1,7 +1,7 @@
{
"name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.5.0",
"version": "2.5.2",
"license": "AGPL-3.0",
"scripts": {
"dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev --host 0.0.0.0",
@@ -56,6 +56,7 @@
"svelte-preprocess": "4.10.6",
"svelte-select": "4.4.7",
"tailwindcss": "3.0.24",
"sveltekit-i18n": "2.1.2",
"ts-node": "10.7.0",
"tslib": "2.3.1",
"typescript": "4.6.3"

35
pnpm-lock.yaml generated
View File

@@ -49,6 +49,7 @@ specifiers:
svelte-preprocess: 4.10.6
svelte-select: 4.4.7
tailwindcss: 3.0.24
sveltekit-i18n: 2.1.2
tailwindcss-scrollbar: 0.1.0
ts-node: 10.7.0
tslib: 2.3.1
@@ -109,6 +110,7 @@ devDependencies:
svelte-select: 4.4.7
tailwindcss: 3.0.24_ts-node@10.7.0
ts-node: 10.7.0_de7c86b0cde507c63a0402da5b982bd3
sveltekit-i18n: 2.1.2_svelte@3.46.4
tslib: 2.3.1
typescript: 4.6.3
@@ -422,6 +424,26 @@ packages:
- supports-color
dev: true
/@sveltekit-i18n/base/1.1.1_svelte@3.46.4:
resolution:
{
integrity: sha512-J/sMU0OwS3dCLOuilHMBqu8vZHuuXiNV9vFJx8Nb4/b5BlR/KCZ4bCXI8wZR02GHeCOYKZxWus07CM1scxa/jw==
}
peerDependencies:
svelte: ^3.x
dependencies:
svelte: 3.46.4
optionalDependencies:
'@sveltekit-i18n/parser-default': 1.0.3
dev: true
/@sveltekit-i18n/parser-default/1.0.3:
resolution:
{
integrity: sha512-HheveklTjp3hxpYQhoHfyA6B4bQaUeSV5MQf2usIv/58UF2jY/YqhCAWj9bDBjufbuZc5pSz4BXvdX3WVT+viA==
}
dev: true
/@szmarczak/http-timer/5.0.1:
resolution:
{
@@ -4955,6 +4977,19 @@ packages:
engines: { node: '>= 8' }
dev: true
/sveltekit-i18n/2.1.2_svelte@3.46.4:
resolution:
{
integrity: sha512-s5YxcbNd2EWNZaZR1A4Drt8s53E4fpUkN4XIWd3VRpw1pihZVWssqmBW1qkjQ6AB0kiu1Qwule+vt1HkbQOjrg==
}
peerDependencies:
svelte: ^3.x
dependencies:
'@sveltekit-i18n/base': 1.1.1_svelte@3.46.4
'@sveltekit-i18n/parser-default': 1.0.3
svelte: 3.46.4
dev: true
/table/6.7.2:
resolution:
{

View File

@@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "Umami" (
"id" TEXT NOT NULL PRIMARY KEY,
"serviceId" TEXT NOT NULL,
"postgresqlUser" TEXT NOT NULL,
"postgresqlPassword" TEXT NOT NULL,
"postgresqlDatabase" TEXT NOT NULL,
"postgresqlPublicPort" INTEGER,
"umamiAdminPassword" TEXT NOT NULL,
"hashSalt" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Umami_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Umami_serviceId_key" ON "Umami"("serviceId");

View File

@@ -0,0 +1,22 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Setting" (
"id" TEXT NOT NULL PRIMARY KEY,
"fqdn" TEXT,
"isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"minPort" INTEGER NOT NULL DEFAULT 9000,
"maxPort" INTEGER NOT NULL DEFAULT 9100,
"proxyPassword" TEXT NOT NULL,
"proxyUser" TEXT NOT NULL,
"proxyHash" TEXT,
"isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Setting" ("createdAt", "dualCerts", "fqdn", "id", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt") SELECT "createdAt", "dualCerts", "fqdn", "id", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt" FROM "Setting";
DROP TABLE "Setting";
ALTER TABLE "new_Setting" RENAME TO "Setting";
CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -18,6 +18,7 @@ model Setting {
proxyPassword String
proxyUser String
proxyHash String?
isAutoUpdateEnabled Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
@@ -91,9 +92,9 @@ model Application {
pythonWSGI String?
pythonModule String?
pythonVariable String?
dockerFileLocation String?
dockerFileLocation String?
denoMainFile String?
denoOptions String?
denoOptions String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
settings ApplicationSettings?
@@ -301,6 +302,7 @@ model Service {
serviceSecret ServiceSecret[]
meiliSearch MeiliSearch?
persistentStorage ServicePersistentStorage[]
umami Umami?
}
model PlausibleAnalytics {
@@ -385,3 +387,17 @@ model MeiliSearch {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Umami {
id String @id @default(cuid())
serviceId String @unique
postgresqlUser String
postgresqlPassword String
postgresqlDatabase String
postgresqlPublicPort Int?
umamiAdminPassword String
hashSalt String
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@@ -50,6 +50,20 @@ async function main() {
}
});
}
// Set auto-update based on env variable
const isAutoUpdateEnabled = process.env['COOLIFY_AUTO_UPDATE'] === 'true';
const settings = await prisma.setting.findFirst({});
if (settings) {
await prisma.setting.update({
where: {
id: settings.id
},
data: {
isAutoUpdateEnabled
}
});
}
}
main()
.catch((e) => {

1
src/app.d.ts vendored
View File

@@ -31,6 +31,7 @@ interface SessionData {
userId?: string | null;
teamId?: string | null;
permission?: string;
lang?: string;
isAdmin?: boolean;
expires?: string | null;
}

View File

@@ -6,6 +6,7 @@ import { getUserDetails, sentry } from '$lib/common';
import { version } from '$lib/common';
import cookie from 'cookie';
import { dev } from '$app/env';
import { locales } from '$lib/translations';
const whiteLabeled = process.env['COOLIFY_WHITE_LABELED'] === 'true';
const whiteLabelDetails = {
@@ -20,6 +21,24 @@ export const handle = handleSession(
},
async function ({ event, resolve }) {
let response;
const { url, request } = event;
// Get defined locales
const supportedLocales = locales.get();
let locale;
if (event.locals.cookies['lang']) {
locale = event.locals.cookies['lang'];
} else if (!locale) {
locale = `${`${request.headers.get('accept-language')}`.match(
/[a-zA-Z]+?(?=-|_|,|;)/
)}`.toLowerCase();
}
// Set default locale if user preferred locale does not match
if (!supportedLocales.includes(locale)) locale = 'en';
try {
if (event.locals.cookies) {
if (event.locals.cookies['kit.session']) {
@@ -39,12 +58,14 @@ export const handle = handleSession(
}
response = await resolve(event, {
ssr: !event.url.pathname.startsWith('/webhooks/success')
ssr: !event.url.pathname.startsWith('/webhooks/success'),
transformPage: ({ html }) => html.replace(/<html.*>/, `<html lang="${locale}">`)
});
} catch (error) {
console.log(error);
response = await resolve(event, {
ssr: !event.url.pathname.startsWith('/webhooks/success')
ssr: !event.url.pathname.startsWith('/webhooks/success'),
transformPage: ({ html }) => html.replace(/<html.*>/, `<html lang="${locale}">`)
});
response.headers.append(
'Set-Cookie',
@@ -67,14 +88,24 @@ export const handle = handleSession(
expires: new Date('Thu, 01 Jan 1970 00:00:01 GMT')
})
);
} finally {
return response;
}
response.headers.append(
'Set-Cookie',
cookie.serialize('lang', locale, {
path: '/',
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60
})
);
return response;
}
);
export const getSession: GetSession = function ({ locals }) {
return {
lang: locals.cookies.lang,
version,
whiteLabeled,
whiteLabelDetails,

View File

@@ -26,7 +26,7 @@ try {
initialScope: {
tags: {
appId: process.env['COOLIFY_APP_ID'],
'os.arch': os.arch(),
'os.arch': getOsArch(),
'os.platform': os.platform(),
'os.release': os.release()
}
@@ -175,3 +175,7 @@ export function generateTimestamp(): string {
export function getDomain(domain: string): string {
return domain?.replace('https://', '').replace('http://', '');
}
export function getOsArch() {
return os.arch();
}

View File

@@ -6,6 +6,7 @@
import N8n from './svg/services/N8n.svelte';
import NocoDb from './svg/services/NocoDB.svelte';
import PlausibleAnalytics from './svg/services/PlausibleAnalytics.svelte';
import Umami from './svg/services/Umami.svelte';
import UptimeKuma from './svg/services/UptimeKuma.svelte';
import VaultWarden from './svg/services/VaultWarden.svelte';
import VsCodeServer from './svg/services/VSCodeServer.svelte';
@@ -52,4 +53,8 @@
<a href="https://ghost.org" target="_blank">
<Ghost />
</a>
{:else if service.type === 'umami'}
<a href="https://umami.is" target="_blank">
<Umami />
</a>
{/if}

View File

@@ -180,5 +180,16 @@ export const supportedServiceTypesAndVersions = [
ports: {
main: 7700
}
},
{
name: 'umami',
fancyName: 'Umami',
baseImage: 'ghcr.io/mikecao/umami',
images: ['postgres:12-alpine'],
versions: ['postgresql-latest'],
recommendedVersion: 'postgresql-latest',
ports: {
main: 3000
}
}
];

View File

@@ -0,0 +1,85 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width="856.000000pt"
height="856.000000pt"
viewBox="0 0 856.000000 856.000000"
preserveAspectRatio="xMidYMid meet"
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'}
>
<metadata> Created by potrace 1.11, written by Peter Selinger 2001-2013 </metadata>
<g
transform="translate(0.000000,856.000000) scale(0.100000,-0.100000)"
fill="currentColor"
stroke="none"
>
<path
d="M4027 8163 c-2 -2 -28 -5 -58 -7 -50 -4 -94 -9 -179 -22 -19 -2 -48
-6 -65 -9 -47 -6 -236 -44 -280 -55 -22 -6 -49 -12 -60 -15 -34 -6 -58 -13
-130 -36 -38 -13 -72 -23 -75 -24 -29 -6 -194 -66 -264 -96 -49 -22 -95 -39
-102 -39 -7 0 -19 -7 -28 -15 -8 -9 -18 -15 -21 -14 -7 1 -197 -92 -205 -101
-3 -3 -21 -13 -40 -24 -79 -42 -123 -69 -226 -137 -94 -62 -246 -173 -280
-204 -6 -5 -29 -25 -52 -43 -136 -111 -329 -305 -457 -462 -21 -25 -41 -47
-44 -50 -4 -3 -22 -26 -39 -52 -18 -25 -38 -52 -45 -60 -34 -35 -207 -308
-259 -408 -13 -25 -25 -47 -28 -50 -11 -11 -121 -250 -159 -346 -42 -105 -114
-321 -126 -374 l-7 -30 -263 0 c-245 0 -268 -2 -321 -21 -94 -35 -171 -122
-191 -216 -9 -39 -8 -852 0 -938 9 -87 16 -150 23 -195 3 -19 6 -48 8 -65 3
-29 14 -97 22 -140 3 -11 7 -36 10 -55 3 -19 9 -51 14 -70 5 -19 11 -46 14
-60 29 -138 104 -401 145 -505 5 -11 23 -58 42 -105 18 -47 42 -105 52 -130
11 -25 21 -49 22 -55 3 -10 109 -224 164 -330 18 -33 50 -89 71 -124 22 -34
40 -64 40 -66 0 -8 104 -161 114 -167 6 -4 7 -8 3 -8 -4 0 4 -12 18 -27 14
-15 25 -32 25 -36 0 -5 6 -14 13 -21 6 -7 21 -25 32 -41 11 -15 34 -44 50 -64
17 -21 41 -52 55 -70 13 -18 33 -43 45 -56 11 -13 42 -49 70 -81 100 -118 359
-369 483 -469 34 -27 62 -53 62 -57 0 -5 6 -8 13 -8 7 0 19 -9 27 -20 8 -11
19 -20 26 -20 6 0 19 -9 29 -20 10 -11 22 -20 27 -20 5 0 23 -13 41 -30 18
-16 37 -30 44 -30 6 0 13 -4 15 -8 3 -8 186 -132 194 -132 2 0 27 -15 56 -34
132 -83 377 -207 558 -280 36 -15 74 -31 85 -36 62 -26 220 -81 320 -109 79
-23 191 -53 214 -57 14 -3 28 -7 31 -9 4 -2 20 -7 36 -9 16 -3 40 -8 54 -11
14 -3 36 -8 50 -11 14 -2 36 -7 50 -10 13 -3 40 -8 60 -10 19 -2 46 -7 60 -10
54 -10 171 -25 320 -40 90 -9 613 -12 636 -4 11 5 28 4 37 -1 9 -6 17 -6 17
-1 0 4 10 8 23 9 29 0 154 12 192 18 17 3 46 7 65 9 70 10 131 20 183 32 16 3
38 7 50 9 45 7 165 36 252 60 50 14 100 28 112 30 12 3 34 10 48 15 14 5 25 7
25 4 0 -4 6 -2 13 3 6 6 30 16 52 22 22 7 47 15 55 18 8 4 17 7 20 7 10 2 179
68 240 94 96 40 342 159 395 191 17 10 53 30 80 46 28 15 81 47 118 71 37 24
72 44 76 44 5 0 11 3 13 8 2 4 30 25 63 47 33 22 62 42 65 45 3 3 50 38 105
79 55 40 105 79 110 85 6 6 24 22 40 34 85 65 465 430 465 447 0 3 8 13 18 23
9 10 35 40 57 66 22 27 47 56 55 65 8 9 42 52 74 96 32 44 71 96 85 115 140
183 358 576 461 830 12 30 28 69 36 85 24 56 123 355 117 355 -3 0 -1 6 5 13
6 6 14 30 18 52 10 48 9 46 17 65 5 13 37 155 52 230 9 42 35 195 40 231 34
235 40 357 40 804 l0 420 -24 44 c-46 87 -143 157 -231 166 -19 2 -144 4 -276
4 l-242 1 -36 118 c-21 64 -46 139 -56 166 -11 27 -20 52 -20 57 0 5 -11 33
-25 63 -14 30 -25 58 -25 61 0 18 -152 329 -162 333 -5 2 -8 10 -8 18 0 8 -4
14 -10 14 -5 0 -9 3 -8 8 3 9 -40 82 -128 217 -63 97 -98 145 -187 259 -133
171 -380 420 -559 564 -71 56 -132 102 -138 102 -5 0 -10 3 -10 8 0 4 -25 23
-55 42 -30 19 -55 38 -55 43 0 4 -6 7 -13 7 -7 0 -22 8 -33 18 -11 9 -37 26
-59 37 -21 11 -44 25 -50 30 -41 37 -413 220 -540 266 -27 9 -61 22 -75 27
-14 5 -28 10 -32 11 -4 1 -28 10 -53 21 -25 11 -46 19 -48 18 -2 -1 -109 29
-137 40 -13 4 -32 9 -65 16 -5 1 -16 5 -22 9 -7 5 -13 6 -13 3 0 -2 -15 0 -32
5 -18 5 -44 11 -58 14 -14 3 -36 7 -50 10 -14 3 -50 9 -80 15 -30 6 -64 12
-75 14 -11 2 -45 6 -75 10 -30 4 -71 9 -90 12 -19 3 -53 6 -75 7 -22 1 -44 5
-50 8 -11 7 -542 9 -548 2z m57 -404 c7 10 436 8 511 -3 22 -3 60 -8 85 -11
25 -2 56 -6 70 -9 14 -2 43 -7 65 -10 38 -5 58 -9 115 -21 14 -3 34 -7 45 -9
11 -2 58 -14 105 -26 47 -12 92 -23 100 -25 35 -7 279 -94 308 -109 17 -9 34
-16 37 -16 3 1 20 -6 38 -14 17 -8 68 -31 112 -51 44 -20 82 -35 84 -35 2 1 7
-3 10 -8 3 -5 43 -28 88 -51 45 -23 87 -48 93 -56 7 -8 17 -15 22 -15 12 0
192 -121 196 -132 2 -4 8 -8 13 -8 10 0 119 -86 220 -172 102 -87 256 -244
349 -357 25 -30 53 -63 63 -73 9 -10 17 -22 17 -28 0 -5 3 -10 8 -10 4 0 25
-27 46 -60 22 -33 43 -60 48 -60 4 0 8 -5 8 -11 0 -6 11 -25 25 -43 14 -18 25
-38 25 -44 0 -7 4 -12 8 -12 5 0 16 -15 25 -32 9 -18 30 -55 47 -83 46 -77
161 -305 154 -305 -4 0 -2 -6 4 -12 6 -7 23 -47 40 -88 16 -41 33 -84 37 -95
5 -11 9 -22 10 -25 0 -3 11 -36 24 -73 13 -38 21 -70 19 -73 -3 -2 -1386 -3
-3075 -2 l-3071 3 38 110 c47 137 117 301 182 425 62 118 167 295 191 320 9
11 17 22 17 25 0 7 39 63 58 83 6 7 26 35 44 60 18 26 37 52 43 57 6 6 34 37
61 70 48 59 271 286 329 335 17 14 53 43 80 65 28 22 52 42 55 45 3 3 21 17
40 30 19 14 40 28 45 32 40 32 105 78 109 78 3 0 28 16 55 35 26 19 53 35 58
35 5 0 18 8 29 18 17 15 53 35 216 119 118 60 412 176 422 166 3 -4 6 -2 6 4
0 6 12 13 28 16 15 3 52 12 82 21 30 9 63 19 73 21 10 2 27 7 37 10 10 3 29 8
42 10 13 3 48 10 78 16 30 7 61 12 68 12 6 0 12 4 12 9 0 5 5 6 10 3 6 -4 34
-2 63 4 51 11 71 13 197 26 36 4 67 9 69 11 2 2 10 -1 17 -7 8 -6 14 -7 18 0z"
/>
</g>
</svg>

View File

@@ -154,6 +154,7 @@ export function generateDatabaseConfiguration(database: Database & { settings: D
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: string;
POSTGRESQL_USERNAME: string;
POSTGRESQL_PASSWORD: string;
POSTGRESQL_DATABASE: string;
@@ -220,6 +221,7 @@ export function generateDatabaseConfiguration(database: Database & { settings: D
return {
privatePort: 5432,
environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword,
POSTGRESQL_PASSWORD: dbUserPassword,
POSTGRESQL_USERNAME: dbUser,
POSTGRESQL_DATABASE: defaultDatabase

View File

@@ -1,9 +1,27 @@
import { decrypt, encrypt } from '$lib/crypto';
import type { Minio, Service } from '@prisma/client';
import type { Minio, Prisma, Service } from '@prisma/client';
import cuid from 'cuid';
import { generatePassword } from '.';
import { prisma } from './common';
const include: Prisma.ServiceInclude = {
destinationDocker: true,
persistentStorage: true,
serviceSecret: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true,
meiliSearch: true,
umami: true
};
export async function listServicesWithIncludes() {
return await prisma.service.findMany({
include,
orderBy: { createdAt: 'desc' }
});
}
export async function listServices(teamId: string): Promise<Service[]> {
if (teamId === '0') {
return await prisma.service.findMany({ include: { teams: true } });
@@ -30,35 +48,21 @@ export async function getService({ id, teamId }: { id: string; teamId: string })
if (teamId === '0') {
body = await prisma.service.findFirst({
where: { id },
include: {
destinationDocker: true,
plausibleAnalytics: true,
minio: true,
vscodeserver: true,
wordpress: true,
ghost: true,
serviceSecret: true,
meiliSearch: true,
persistentStorage: true
}
include
});
} else {
body = await prisma.service.findFirst({
where: { id, teams: { some: { id: teamId } } },
include: {
destinationDocker: true,
plausibleAnalytics: true,
minio: true,
vscodeserver: true,
wordpress: true,
ghost: true,
serviceSecret: true,
meiliSearch: true,
persistentStorage: true
}
include
});
}
if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value);
return s;
});
}
if (body.plausibleAnalytics?.postgresqlPassword)
body.plausibleAnalytics.postgresqlPassword = decrypt(
body.plausibleAnalytics.postgresqlPassword
@@ -85,15 +89,14 @@ export async function getService({ id, teamId }: { id: string; teamId: string })
if (body.meiliSearch?.masterKey) body.meiliSearch.masterKey = decrypt(body.meiliSearch.masterKey);
if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value);
return s;
});
}
if (body.wordpress?.ftpPassword) {
body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
}
if (body.wordpress?.ftpPassword) body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
if (body.umami?.postgresqlPassword)
body.umami.postgresqlPassword = decrypt(body.umami.postgresqlPassword);
if (body.umami?.umamiAdminPassword)
body.umami.umamiAdminPassword = decrypt(body.umami.umamiAdminPassword);
if (body.umami?.hashSalt) body.umami.hashSalt = decrypt(body.umami.hashSalt);
const settings = await prisma.setting.findFirst();
return { ...body, settings };
@@ -219,6 +222,27 @@ export async function configureServiceType({
meiliSearch: { create: { masterKey } }
}
});
} else if (type === 'umami') {
const umamiAdminPassword = encrypt(generatePassword());
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword());
const postgresqlDatabase = 'umami';
const hashSalt = encrypt(generatePassword(64));
await prisma.service.update({
where: { id },
data: {
type,
umami: {
create: {
umamiAdminPassword,
postgresqlDatabase,
postgresqlPassword,
postgresqlUser,
hashSalt
}
}
}
});
}
}
@@ -375,6 +399,7 @@ export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } });
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
await prisma.ghost.deleteMany({ where: { serviceId: id } });
await prisma.umami.deleteMany({ where: { serviceId: id } });
await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } });
await prisma.minio.deleteMany({ where: { serviceId: id } });
await prisma.vscodeserver.deleteMany({ where: { serviceId: id } });

View File

@@ -6,6 +6,7 @@ import crypto from 'crypto';
import { checkContainer, checkHAProxy } from '.';
import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
import { listServicesWithIncludes } from '$lib/database';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
@@ -208,17 +209,7 @@ export async function configureHAProxy(): Promise<void> {
}
}
}
const services = await db.prisma.service.findMany({
include: {
destinationDocker: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true,
meiliSearch: true
}
});
const services = await listServicesWithIncludes();
for (const service of services) {
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;

4
src/lib/lang.json Normal file
View File

@@ -0,0 +1,4 @@
{
"fr": "Français",
"en": "English"
}

View File

@@ -7,6 +7,7 @@ import fs from 'fs/promises';
import getPort, { portNumbers } from 'get-port';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
import { promises as dns } from 'dns';
import { listServicesWithIncludes } from '$lib/database';
export async function letsEncrypt(domain: string, id?: string, isCoolify = false): Promise<void> {
try {
@@ -145,18 +146,7 @@ export async function generateSSLCerts(): Promise<void> {
console.log(`Error during generateSSLCerts with ${application.fqdn}: ${error}`);
}
}
const services = await db.prisma.service.findMany({
include: {
destinationDocker: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true,
meiliSearch: true
},
orderBy: { createdAt: 'desc' }
});
const services = await listServicesWithIncludes();
for (const service of services) {
try {

329
src/lib/locales/en.json Normal file
View File

@@ -0,0 +1,329 @@
{
"layout": {
"update_done": "Update completed.",
"wait_new_version_startup": "Waiting for the new version to start...",
"new_version": "New version reachable. Reloading...",
"switch_to_a_different_team": "Switch to a different team...",
"update_available": "Update available"
},
"error": {
"you_can_find_your_way_back": "You can find your way back",
"here": "here",
"you_are_lost": "Ooops you are lost! But don't be afraid!"
},
"index": {
"dashboard": "Dashboard",
"applications": "Applications",
"destinations": "Destinations",
"git_sources": "Git Sources",
"databases": "Databases",
"services": "Services",
"teams": "Teams",
"not_implemented_yet": "Not implemented yet",
"database": "Database",
"settings": "Settings",
"global_settings": "Global Settings",
"secret": "Secret",
"team": "Team",
"logout": "Logout"
},
"login": {
"already_logged_in": "Already logged in...",
"authenticating": "Authenticating...",
"login": "Login"
},
"forms": {
"password": "Password",
"email": "Email address",
"passwords_not_match": "Passwords do not match.",
"password_again": "Password again",
"save": "Save",
"saving": "Saving...",
"name": "Name",
"value": "Value",
"action": "Action",
"is_required": "is required.",
"add": "Add",
"set": "Set",
"remove": "Remove",
"path": "Path",
"confirm_continue": "Are you sure to continue?",
"must_be_stopped_to_modify": "Must be stopped to modify.",
"port": "Port",
"default": "default",
"base_directory": "Base Directory",
"publish_directory": "Publish Directory",
"generated_automatically_after_start": "Generated automatically after start",
"roots_password": "Root's Password",
"root_user": "Root User",
"eg": "eg",
"user": "User",
"loading": "Loading...",
"version": "Version",
"host": "Host",
"already_used_for": "<span class=\"text-red-500\">{{type}}</span> already used for",
"configuration": "Configuration",
"engine": "Engine",
"network": "Network",
"ip_address": "IP Address",
"ssh_private_key": "SSH Private Key",
"type": "Type",
"html_url": "HTML URL",
"api_url": "API URL",
"organization": "Organization",
"new_password": "New password",
"super_secure_new_password": "Super secure new password",
"submit": "Submit",
"default_email_address": "Default Email Address",
"default_password": "Default Password",
"username": "Username",
"root_db_user": "Root DB User",
"root_db_password": "Root DB Password",
"api_port": "API Port",
"verifying": "Verifying",
"verify_emails_without_smtp": "Verify emails without SMTP",
"extra_config": "Extra Config",
"select_a_service": "Select a Service",
"select_a_service_version": "Select a Service version",
"removing": "Removing...",
"remove_domain": "Remove domain",
"public_port_range": "Public Port Range",
"public_port_range_explainer": "Ports used to expose databases/services/internal services.<br> Add them to your firewall (if applicable).<br><br>You can specify a range of ports, eg: <span class='text-yellow-500 font-bold'>9000-9100</span>",
"no_actions_available": "No actions available",
"admin_api_key": "Admin API key"
},
"register": {
"register": "Register",
"registering": "Registering...",
"first_user": "You are registering the first user. It will be the administrator of your Coolify instance."
},
"reset": {
"reset_password": "Reset",
"invalid_secret_key": "Invalid secret key.",
"secret_key": "Secret Key",
"find_path_secret_key": "You can find it in ~/coolify/.env (COOLIFY_SECRET_KEY)"
},
"application": {
"configuration": {
"buildpack": {
"choose_this_one": "Choose this one..."
},
"branch_already_in_use": "This branch is already used by another application. Webhooks won't work in this case for both applications. Are you sure you want to use it?",
"no_repositories_configured": "No repositories configured for your Git Application.",
"configure_it_now": "Configure it now",
"loading_repositories": "Loading repositories ...",
"select_a_repository": "Please select a repository",
"loading_branches": "Loading branches ...",
"select_a_repository_first": "Please select a repository first",
"select_a_branch": "Please select a branch",
"loading_groups": "Loading groups...",
"select_a_group": "Please select a group",
"loading_projects": "Loading projects...",
"select_a_project": "Please select a project",
"no_projects_found": "No projects found",
"no_branches_found": "No branches found",
"configure_build_pack": "Configure Build Pack",
"scanning_repository_suggest_build_pack": "Scanning repository to suggest a build pack for you...",
"found_lock_file": "Found lock file for <span class=\"font-bold text-orange-500 pl-1\">{{packageManager}}</span>. Using it for predefined commands commands.",
"configure_destination": "Configure Destination",
"no_configurable_destination": "No configurable Destination found",
"select_a_repository_project": "Select a Repository / Project",
"select_a_git_source": "Select a Git Source",
"no_configurable_git": "No configurable Git Source found",
"configuration_missing": "Configuration missing"
},
"build": {
"queued_waiting_exec": "Queued and waiting for execution.",
"build_logs_of": "Build logs of",
"running": "Running",
"queued": "Queued",
"finished_in": "Finished in",
"load_more": "Load More",
"no_logs": "No logs found",
"waiting_logs": "Waiting for the logs..."
},
"preview": {
"need_during_buildtime": "Need during buildtime?",
"setup_secret_app_first": "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments.",
"values_overwriting_app_secrets": "These values overwrite application secrets in PR/MR deployments. Useful for creating <span class='text-green-500 font-bold'>staging</span> environments.",
"redeploy": "Redeploy",
"no_previews_available": "No previews available"
},
"secrets": {
"secret_saved": "Secret saved.",
"use_isbuildsecret": "Use isBuildSecret",
"secrets_for": "Secrets for"
},
"storage": {
"path_is_required": "Path is required.",
"storage_saved": "Storage saved.",
"storage_updated": "Storage updated.",
"storage_deleted": "Storage deleted.",
"persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments. <br>This is useful for storing data such as a database (SQLite) or a cache."
},
"deployment_queued": "Deployment queued.",
"confirm_to_delete": "Are you sure you would like to delete '{{name}}'?",
"stop_application": "Stop application",
"permission_denied_stop_application": "You do not have permission to stop the application.",
"rebuild_application": "Rebuild application",
"permission_denied_rebuild_application": "You do not have permission to rebuild application.",
"build_and_start_application": "Build and start application",
"permission_denied_build_and_start_application": "You do not have permission to Build and start application.",
"configurations": "Configurations",
"secret": "Secrets",
"persistent_storage": "Persistent Storage",
"previews": "Previews",
"logs": "Application Logs",
"build_logs": "Build Logs",
"delete_application": "Delete application",
"permission_denied_delete_application": "You do not have permission to delete this application",
"domain_already_in_use": "Domain {{domain}} is already used.",
"dns_not_set_error": "DNS not set or propogated for {{domain}}.<br><br>Please check your DNS settings.",
"settings_saved": "Settings saved.",
"dns_not_set_partial_error": "DNS not set",
"git_source": "Git Source",
"git_repository": "Git Repository",
"build_pack": "Build Pack",
"destination": "Destination",
"application": "Application",
"url_fqdn": "URL (FQDN)",
"domain_fqdn": "Domain (FQDN)",
"https_explainer": "If you specify <span class='text-green-500 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-green-500 font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-white font-bold'>You must set your DNS to point to the server IP in advance.</span>",
"ssl_www_and_non_www": "Generate SSL for www and non-www?",
"ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-green-500'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both.",
"install_command": "Install Command",
"build_command": "Build Command",
"start_command": "Start Command",
"directory_to_use_explainer": "Directory to use as the base for all commands.<br>Could be useful with <span class='text-green-500 font-bold'>monorepos</span>.",
"publish_directory_explainer": "Directory containing all the assets for deployment. <br> For example: <span class='text-green-500 font-bold'>dist</span>,<span class='text-green-500 font-bold'>_site</span> or <span class='text-green-500 font-bold'>public</span>.",
"features": "Features",
"enable_automatic_deployment": "Enable Automatic Deployment",
"enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.",
"enable_mr_pr_previews": "Enable MR/PR Previews",
"enable_preview_deploy_mr_pr_requests": "Enable preview deployments from pull or merge requests.",
"debug_logs": "Debug Logs",
"enable_debug_log_during_build": "Enable debug logs during build phase.<br><span class='text-red-500 font-bold'>Sensitive information</span> could be visible and saved in logs.",
"cant_activate_auto_deploy_without_repo": "Cannot activate automatic deployments until only one application is defined for this repository / branch.",
"no_applications_found": "No applications found",
"secret__batch_dot_env": "Paste .env file",
"batch_secrets": "Batch add secrets"
},
"general": "General",
"database": {
"default_database": "Default Database",
"generated_automatically_after_set_to_public": "Generated automatically after set to public",
"connection_string": "Connection String",
"set_public": "Set it public",
"warning_database_public": "Your database will be reachable over the internet. <br>Take security seriously in this case!",
"change_append_only_mode": "Change append only mode",
"warning_append_only": "Useful if you would like to restore redis data from a backup.<br><span class='font-bold text-white'>Database restart is required.</span>",
"select_database_type": "Select a Database type",
"select_database_version": "Select a Database version",
"confirm_stop": "Are you sure you would like to stop {{name}}?",
"stop_database": "Stop database",
"permission_denied_stop_database": "You do not have permission to stop the database.",
"start_database": "Start database",
"permission_denied_start_database": "You do not have permission to start the database.",
"delete_database": "Delete Database",
"permission_denied_delete_database": "You do not have permission to delete a Database",
"no_databases_found": "No databases found"
},
"destination": {
"delete_destination": "Delete Destination",
"permission_denied_delete_destination": "You do not have permission to delete this destination",
"add_to_coolify": "Add to Coolify",
"coolify_proxy_stopped": "Coolify Proxy stopped!",
"coolify_proxy_started": "Coolify Proxy started!",
"confirm_restart_proxy": "Are you sure you want to restart the proxy? Everything will be reconfigured in ~10 secs.",
"coolify_proxy_restarting": "Coolify Proxy restarting...",
"restarting_please_wait": "Restarting... please wait...",
"force_restart_proxy": "Force restart proxy",
"use_coolify_proxy": "Use Coolify Proxy?",
"no_destination_found": "No destination found",
"new_error_network_already_exists": "Network {{network}} already configured for another team!",
"new": {
"saving_and_configuring_proxy": "Saving and configuring proxy...",
"install_proxy": "This will install a proxy on the destination to allow you to access your applications and services without any manual configuration (recommended for Docker).<br><br>Databases will have their own proxy.",
"add_new_destination": "Add New Destination",
"predefined_destinations": "Predefined destinations"
}
},
"sources": {
"local_docker": "Local Docker",
"remote_docker": "Remote Docker",
"organization_explainer": "Fill it if you would like to use an organization's as your Git Source. Otherwise your user will be used."
},
"source": {
"new": {
"git_source": "Add New Git Source",
"official_providers": "Official providers"
},
"no_git_sources_found": "No git sources found",
"delete_git_source": "Delete Git Source",
"permission_denied": "You do not have permission to delete a Git Source",
"create_new_app": "Create new {{name}} App",
"change_app_settings": "Change {{name}} App Settings",
"install_repositories": "Install Repositories",
"application_id": "Application ID",
"group_name": "Group Name",
"oauth_id": "OAuth ID",
"oauth_id_explainer": "The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class='font-bold text-orange-600' >in the URL</span> of your GitLab OAuth Application.",
"register_oauth_gitlab": "Register new OAuth application on GitLab",
"gitlab": {
"self_hosted": "Instance-wide application (self-hosted)",
"user_owned": "User owned application",
"group_owned": "Group owned application",
"gitlab_application_type": "GitLab Application Type",
"already_configured": "GitLab App is already configured."
},
"github": {
"redirecting": "Redirecting to Github..."
}
},
"services": {
"all_email_verified": "All email verified. You can login now.",
"generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-pink-600'>both DNS entries</span> set in advance.<br><br>Service needs to be restarted."
},
"service": {
"stop_service": "Stop Service",
"permission_denied_stop_service": "You do not have permission to stop the service.",
"start_service": "Start Service",
"permission_denied_start_service": "You do not have permission to start the service.",
"delete_service": "Delete Service",
"permission_denied_delete_service": "You do not have permission to delete a service.",
"no_service": "No services found"
},
"setting": {
"change_language": "Change Language",
"permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.",
"domain_removed": "Domain removed",
"ssl_explainer": "If you specify <span class='text-yellow-500 font-bold'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-yellow-500 font-bold'>www</span>, Coolify will be redirected (302) from non-www and vice versa.",
"must_remove_domain_before_changing": "Must remove the domain before you can change this setting.",
"registration_allowed": "Registration allowed?",
"registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.",
"coolify_proxy_settings": "Coolify Proxy Settings",
"credential_stat_explainer": "Credentials for <a class=\"text-white font-bold\" href=\"{{link}}\" target=\"_blank\">stats</a> page.",
"auto_update_enabled": "Auto update enabled?",
"auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running."
},
"team": {
"pending_invitations": "Pending invitations",
"accept": "Accept",
"delete": "Delete",
"member": "member(s)",
"root": "(root)",
"invited_with_permissions": "Invited to <span class=\"font-bold text-pink-600\">{{teamName}}</span> with <span class=\"font-bold text-rose-600\">{{permission}}</span> permission.",
"members": "Members",
"root_team_explainer": "This is the <span class='text-red-500 font-bold'>root</span> team. That means members of this group can manage instance wide settings and have all the priviliges in Coolify (imagine like root user on Linux).",
"permission": "Permission",
"you": "(You)",
"promote_to": "Promote to {{grade}}",
"revoke_invitation": "Revoke invitation",
"pending_invitation": "Pending invitation",
"invite_new_member": "Invite new member",
"send_invitation": "Send invitation",
"invite_only_register_explainer": "You can only invite registered users at the moment - will be extended soon.",
"admin": "Admin",
"read": "Read"
}
}

322
src/lib/locales/fr.json Normal file
View File

@@ -0,0 +1,322 @@
{
"application": {
"application": "Application",
"build": {
"build_logs_of": "Créer des journaux de",
"finished_in": "Fini en",
"load_more": "Charger plus",
"no_logs": "Aucun journal trouvé",
"queued": "En file d'attente",
"queued_waiting_exec": "En file d'attente et en attente d'exécution.",
"running": "Fonctionnement",
"waiting_logs": "En attente des logs..."
},
"build_and_start_application": "Build et démarrer l'application",
"build_command": "Commande Build",
"build_logs": "Créer des journaux",
"build_pack": "Pack de Build",
"cant_activate_auto_deploy_without_repo": "Impossible d'activer les déploiements automatiques tant qu'une seule application n'est pas définie pour ce dépôt/branche.",
"configuration": {
"branch_already_in_use": "Cette branche est déjà utilisée par une autre application. \nLes webhooks ne fonctionneront pas dans ce cas pour les deux applications. \nÊtes-vous sûr de vouloir l'utiliser ?",
"buildpack": {
"choose_this_one": "Choisir celui-ci..."
},
"configuration_missing": "Configuration manquante",
"configure_build_pack": "Configurer le pack de build",
"configure_destination": "Configurer la destination",
"configure_it_now": "Configurez-le maintenant",
"found_lock_file": "Fichier .lock trouvé pour <span class=\"font-bold text-orange-500 pl-1\">{{packageManager}}</span>. \nL'utiliser pour les commandes prédéfinies.",
"loading_branches": "Chargement des branches...",
"loading_groups": "Chargement des groupes...",
"loading_projects": "Chargement des projets...",
"loading_repositories": "Chargement des dépôts Git...",
"no_branches_found": "Aucune branche trouvée",
"no_configurable_destination": "Aucune destination configurable trouvée",
"no_configurable_git": "Aucune source Git configurable trouvée",
"no_projects_found": "Aucun projet trouvé",
"no_repositories_configured": "Aucun dépôt Git configuré pour votre application.",
"scanning_repository_suggest_build_pack": "Analyse du dépôt pour vous suggérer un pack de Build...",
"select_a_branch": "Veuillez sélectionner une branche",
"select_a_git_source": "Sélectionnez une source Git",
"select_a_group": "Veuillez sélectionner un groupe",
"select_a_project": "Veuillez sélectionner un projet",
"select_a_repository": "Veuillez sélectionner un dépôt",
"select_a_repository_first": "Veuillez d'abord sélectionner un dépôt",
"select_a_repository_project": "Sélectionnez un dépôt / projet"
},
"configurations": "Configurations",
"confirm_to_delete": "Voulez-vous vraiment supprimer '{{name}}'?",
"debug_logs": "Journaux de débogage",
"delete_application": "Supprimer l'application",
"deployment_queued": "Déploiement en file d'attente.",
"destination": "Destination",
"directory_to_use_explainer": "Répertoire à utiliser comme base pour toutes les commandes.<br>Pourrait être utile avec <span class='text-green-500 font-bold'>monorepos</span>.",
"dns_not_set_error": "DNS non défini ou propagé pour {{domain}}.<br><br>Veuillez vérifier vos paramètres DNS.",
"dns_not_set_partial_error": "DNS non défini",
"domain_already_in_use": "Le domaine {{domain}} est déjà utilisé.",
"domain_fqdn": "Domaine (FQDN)",
"url_fqdn": "URL (FQDN)",
"enable_auto_deploy_webhooks": "Activez le déploiement automatique via des webhooks.",
"enable_automatic_deployment": "Activer le déploiement automatique",
"enable_debug_log_during_build": "Activez les journaux de débogage pendant la phase de build.<br><span class='text-red-500 font-bold'>Les informations sensibles</span> peuvent être visibles et enregistrées dans les journaux.",
"enable_mr_pr_previews": "Activer les aperçus MR/PR",
"enable_preview_deploy_mr_pr_requests": "Activez les déploiements de prévisualisation à partir de demandes d'extraction ou de fusion.",
"features": "Caractéristiques",
"git_repository": "Dépôt Git",
"git_source": "Source Git",
"https_explainer": "Si vous spécifiez <span class='text-green-500 font-bold'>https</span>, l'application sera accessible uniquement via https. \nUn certificat SSL sera généré pour vous.<br>Si vous spécifiez <span class='text-green-500 font-bold'>www</span>, l'application sera redirigée (302) à partir de non-www et vice versa \n.<br><br>Pour modifier le domaine, vous devez d'abord arrêter l'application.<br><br><span class='text-white font-bold'>Vous devez configurer, en avance, votre DNS pour pointer vers l'IP du serveur.</span>",
"install_command": "Commande d'installation",
"logs": "Journaux des applications",
"no_applications_found": "Aucune application trouvée",
"permission_denied_build_and_start_application": "Vous n'êtes pas autorisé à créer et à démarrer l'application.",
"permission_denied_delete_application": "Vous n'êtes pas autorisé à supprimer cette application",
"permission_denied_rebuild_application": "Vous n'êtes pas autorisé à re-build l'application.",
"permission_denied_stop_application": "Vous n'êtes pas autorisé à arrêter l'application.",
"persistent_storage": "Stockage persistant",
"preview": {
"need_during_buildtime": "Besoin pendant la build ?",
"no_previews_available": "Aucun aperçu disponible",
"redeploy": "Redéployer",
"setup_secret_app_first": "Vous pouvez ajouter des secrets aux déploiements PR/MR. \nVeuillez d'abord ajouter des secrets à l'application. \n<br>Utile pour créer des environnements <span class='text-green-500 font-bold'>de mise en scène</span>.",
"values_overwriting_app_secrets": "Ces valeurs remplacent les secrets d'application dans les déploiements PR/MR. \nUtile pour créer des environnements <span class='text-green-500 font-bold'>de mise en scène</span>."
},
"previews": "Aperçus",
"publish_directory_explainer": "Répertoire contenant tous les actifs à déployer. \n<br> Par exemple : <span class='text-green-500 font-bold'>dist</span>,<span class='text-green-500 font-bold'>_site</span> ou <span \nclass='text-green-500 font-bold'>public</span>.",
"rebuild_application": "Re-build l'application",
"secret": "secrets",
"secrets": {
"secret_saved": "Secret enregistré.",
"secrets_for": "secrets pour",
"use_isbuildsecret": "Utiliser isBuildSecret"
},
"settings_saved": "Paramètres sauvegardés.",
"ssl_explainer": "Il générera des certificats pour www et non-www. \n<br>Vous devez avoir <span class='font-bold text-green-500'>les deux entrées DNS</span> définies à l'avance.<br><br>Utile si vous prévoyez d'avoir des visiteurs sur les deux.",
"ssl_www_and_non_www": "Générer SSL pour www et non-www ?",
"start_command": "Démarrer la commande",
"stop_application": "Arrêter l'application",
"storage": {
"path_is_required": "Le chemin est requis.",
"persistent_storage_explainer": "Vous pouvez spécifier n'importe quel dossier que vous souhaitez conserver dans les déploiements. \n<br>Ceci est utile pour stocker des données telles qu'une base de données (SQLite) ou un cache.",
"storage_deleted": "Stockage supprimé.",
"storage_saved": "Stockage enregistré.",
"storage_updated": "Stockage mis à jour."
}
},
"database": {
"change_append_only_mode": "Changer le mode d'ajout uniquement",
"confirm_stop": "Êtes-vous sûr de vouloir arrêter {{name}} ?",
"connection_string": "Connexion string",
"default_database": "Base de données par défaut",
"delete_database": "Supprimer la base de données",
"generated_automatically_after_set_to_public": "Généré automatiquement après avoir été défini sur public",
"no_databases_found": "Aucune base de données trouvée",
"permission_denied_delete_database": "Vous n'êtes pas autorisé à supprimer une base de données",
"permission_denied_start_database": "Vous n'êtes pas autorisé à démarrer la base de données.",
"permission_denied_stop_database": "Vous n'êtes pas autorisé à arrêter la base de données.",
"select_database_type": "Sélectionnez un type de base de données",
"select_database_version": "Sélectionnez une version de la base de données",
"set_public": "Rendre public",
"start_database": "Démarrer la base de données",
"stop_database": "Arrêter la base de données",
"warning_append_only": "Utile si vous souhaitez restaurer des données Redis à partir d'une sauvegarde.<br><span class='font-bold text-white'>Le redémarrage de la base de données est nécessaire.</span>",
"warning_database_public": "Votre base de données sera accessible depuis Internet. \n<br>Prenez la sécurité au sérieux dans ce cas!"
},
"destination": {
"add_to_coolify": "Ajouter à Coolify",
"confirm_restart_proxy": "Voulez-vous vraiment redémarrer le proxy? \nTout sera reconfiguré en ~10 secondes.",
"coolify_proxy_restarting": "Redémarrage du Proxy Coolify...",
"coolify_proxy_started": "Proxy Coolify démarré!",
"coolify_proxy_stopped": "Proxy Coolify arrêté!",
"delete_destination": "Supprimer le destinataire",
"force_restart_proxy": "Forcer le redémarrage du proxy",
"new": {
"add_new_destination": "Ajouter une nouvelle destination",
"install_proxy": "Cela installera un proxy sur la destination pour vous permettre d'accéder à vos applications et services sans aucune configuration manuelle (recommandé pour Docker).<br><br>Les bases de données auront leur propre proxy.",
"predefined_destinations": "Destinations prédéfinies",
"saving_and_configuring_proxy": "Enregistrement et configuration du proxy..."
},
"new_error_network_already_exists": "Réseau {{network}} déjà configuré pour une autre équipe !",
"no_destination_found": "Aucune destination trouvée",
"permission_denied_delete_destination": "Vous n'êtes pas autorisé à supprimer cette destination",
"restarting_please_wait": "Redémarrage... veuillez patienter...",
"use_coolify_proxy": "Utiliser le Proxy Coolify ?"
},
"error": {
"here": "ici",
"you_are_lost": "Oups vous êtes perdu ! \nMais n'ayez pas peur !",
"you_can_find_your_way_back": "Tu peux retrouver ton chemin"
},
"forms": {
"action": "action",
"add": "Ajouter",
"already_used_for": "<span class=\"text-red-500\">{{type}}</span> déjà utilisé pour",
"api_port": "Port API",
"api_url": "URL de l'API",
"base_directory": "Répertoire de base",
"configuration": "Configuration",
"confirm_continue": "Êtes-vous sûr de continuer ?",
"default": "défaut",
"default_email_address": "Adresse e-mail par défaut",
"default_password": "Mot de passe par défaut",
"eg": "ex",
"email": "Adresse e-mail",
"engine": "Moteur",
"extra_config": "Configuration supplémentaire",
"generated_automatically_after_start": "Généré automatiquement après le démarrage",
"host": "Hôte",
"html_url": "URL HTML",
"ip_address": "Adresse IP",
"is_required": "est requis.",
"loading": "Chargement...",
"must_be_stopped_to_modify": "Doit être arrêté pour être modifié.",
"name": "Nom",
"network": "Réseau",
"new_password": "Nouveau mot de passe",
"no_actions_available": "Aucune action disponible",
"organization": "Organisation",
"password": "Mot de passe",
"password_again": "Mot de passe à nouveau",
"passwords_not_match": "Les mots de passe ne correspondent pas.",
"path": "Chemin",
"port": "Port",
"public_port_range": "Gamme de ports publics",
"public_port_range_explainer": "Ports utilisés pour exposer les bases de données/services/services internes.<br> Ajoutez-les à votre pare-feu (le cas échéant).<br><br>Vous pouvez spécifier une plage de ports, par exemple : <span class='text-yellow-500 \nfont-bold'>9000-9100</span>",
"publish_directory": "Publier le répertoire",
"remove": "Retirer",
"remove_domain": "Supprimer le domaine",
"removing": "Suppression...",
"root_db_password": "Mot de passe root de la base de données",
"root_db_user": "Utilisateur root de la base de données",
"root_user": "Utilisateur root",
"roots_password": "Mot de passe de l'utilisateur root",
"save": "sauvegarder",
"saving": "Sauvegarde...",
"select_a_service": "Sélectionnez un service",
"select_a_service_version": "Sélectionnez une version de service",
"set": "Régler",
"ssh_private_key": "Clé privée SSH",
"submit": "Nous faire parvenir",
"super_secure_new_password": "Nouveau mot de passe super sécurisé",
"type": "Taper",
"user": "Utilisateur",
"username": "Nom d'utilisateur",
"value": "Valeur",
"verify_emails_without_smtp": "Vérifier les e-mails sans SMTP",
"verifying": "Vérification",
"version": "Version"
},
"general": "Général",
"index": {
"applications": "Applications",
"dashboard": "Tableau de bord",
"database": "Base de données",
"databases": "Bases de données",
"destinations": "Destinations",
"git_sources": "Sources Git",
"global_settings": "Paramètres globaux",
"logout": "Se déconnecter",
"not_implemented_yet": "Pas encore implémenté",
"secret": "Secret",
"services": "Services",
"settings": "Réglages",
"team": "Équipe",
"teams": "Équipes"
},
"layout": {
"new_version": "Nouvelle version accessible. \nRechargement...",
"switch_to_a_different_team": "Changer d'équipe...",
"update_available": "Mise à jour disponible",
"update_done": "Mise à jour terminée.",
"wait_new_version_startup": "En attendant le lancement de la nouvelle version..."
},
"login": {
"already_logged_in": "Déjà connecté...",
"authenticating": "Authentification...",
"login": "Connexion"
},
"register": {
"first_user": "Vous enregistrez le premier utilisateur. \nCe sera l'administrateur de votre instance Coolify.",
"register": "S'inscrire"
},
"reset": {
"find_path_secret_key": "Vous pouvez le trouver dans ~/coolify/.env (COOLIFY_SECRET_KEY)",
"invalid_secret_key": "Clé secrète invalide.",
"reset_password": "Réinitialiser",
"secret_key": "Clef secrète"
},
"service": {
"delete_service": "Supprimer le service",
"no_service": "Aucun service trouvé",
"permission_denied_delete_service": "Vous n'êtes pas autorisé à supprimer un service.",
"permission_denied_start_service": "Vous n'êtes pas autorisé à démarrer le service.",
"permission_denied_stop_service": "Vous n'êtes pas autorisé à arrêter le service.",
"start_service": "Démarrer le service",
"stop_service": "Stopper le service"
},
"services": {
"all_email_verified": "Tous les e-mails sont vérifiés. \nVous pouvez vous connecter maintenant.",
"generate_www_non_www_ssl": "Il générera des certificats pour www et non-www. \n<br>Vous devez avoir <span class='font-bold text-pink-600'>les deux entrées DNS</span> définies à l'avance.<br><br>Le service devra être redémarré."
},
"setting": {
"coolify_proxy_settings": "Paramètres du proxy Coolify",
"credential_stat_explainer": "Identifiants pour la page <a class=\"text-white font-bold\" href=\"{{link}}\" target=\"_blank\">statistiques</a>.",
"domain_removed": "Domaine supprimé",
"must_remove_domain_before_changing": "Vous devez supprimer le domaine avant de pouvoir modifier ce paramètre.",
"permission_denied": "Vous n'avez pas la permission de faire cela. \n\\nDemandez à un administrateur de modifier vos autorisations.",
"registration_allowed": "Inscription autorisée ?",
"registration_allowed_explainer": "Autoriser d'autres inscriptions à l'application. \n<br>Il est désactivé après la première inscription.",
"ssl_explainer": "Si vous spécifiez <span class='text-yellow-500 font-bold'>https</span>, Coolify sera accessible uniquement via https. \nUn certificat SSL sera généré pour vous.<br>Si vous spécifiez <span class='text-yellow-500 font-bold'>www</span>, Coolify sera redirigé (302) à partir de non-www et vice versa."
},
"source": {
"application_id": "ID d'application",
"change_app_settings": "Modifier les paramètres de l'application {{name}}",
"create_new_app": "Créer une nouvelle application {{name}}",
"delete_git_source": "Supprimer la source Git",
"github": {
"redirecting": "Redirection vers Github..."
},
"gitlab": {
"already_configured": "L'application GitLab est déjà configurée.",
"gitlab_application_type": "Type d'application GitLab",
"group_owned": "Application détenue par le groupe",
"self_hosted": "Application à l'échelle de l'instance (auto-hébergée)",
"user_owned": "Application appartenant à l'utilisateur"
},
"group_name": "Nom de groupe",
"install_repositories": "Installer les dépôts",
"new": {
"git_source": "Ajouter une nouvelle source Git",
"official_providers": "Fournisseurs officiels"
},
"no_git_sources_found": "Aucune source git trouvée",
"oauth_id": "ID OAuth",
"oauth_id_explainer": "L'identifiant OAuth est l'identifiant unique de l'application GitLab. \n<br>Vous pouvez le trouver <span class='font-bold text-orange-600' >dans l'URL</span> de votre application GitLab OAuth.",
"permission_denied": "Vous n'êtes pas autorisé à supprimer une source Git",
"register_oauth_gitlab": "Enregistrer une nouvelle application OAuth sur GitLab"
},
"sources": {
"local_docker": "Docker local",
"organization_explainer": "Remplissez-le si vous souhaitez utiliser une organisation comme source Git. \nSinon, votre utilisateur sera utilisé.",
"remote_docker": "Station d'accueil à distance"
},
"team": {
"accept": "J'accepte",
"admin": "Administrateur",
"delete": "Supprimer",
"invite_new_member": "Inviter un nouveau membre",
"invite_only_register_explainer": "Vous ne pouvez inviter que des utilisateurs enregistrés pour le moment - sera bientôt prolongé.",
"invited_with_permissions": "Invité à <span class=\"font-bold text-pink-600\">{{teamName}}</span> avec <span class=\"font-bold text-rose-600\">{{permission}}</span \n> autorisation.",
"member": "membre(s)",
"members": "Membres",
"pending_invitation": "Invitation en attente",
"pending_invitations": "Invitations en attente",
"permission": "Autorisation",
"promote_to": "Promouvoir à {{grade}}",
"read": "Lire",
"revoke_invitation": "Révoquer l'invitation",
"root": "(suprême)",
"root_team_explainer": "Il s'agit de l'équipe <span class='text-red-500 font-bold'>suprême</span>. \nCela signifie que les membres de ce groupe peuvent gérer les paramètres à l'échelle de l'instance et avoir tous les privilèges dans Coolify (imaginez comme un utilisateur root sous Linux).",
"send_invitation": "Envoyer une invitation",
"you": "(Toi)"
}
}

View File

@@ -0,0 +1,42 @@
import { prisma } from '$lib/database';
import { buildQueue } from '.';
import got from 'got';
import { asyncExecShell, version } from '$lib/common';
import compare from 'compare-versions';
import { dev } from '$app/env';
export default async function (): Promise<void> {
try {
const currentVersion = version;
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) {
const versions = await got
.get(
`https://get.coollabs.io/versions.json?appId=${process.env['COOLIFY_APP_ID']}&version=${currentVersion}`
)
.json();
const latestVersion = versions['coolify'].main.version;
const isUpdateAvailable = compare(latestVersion, currentVersion);
if (isUpdateAvailable === 1) {
const activeCount = await buildQueue.getActiveCount();
if (activeCount === 0) {
if (!dev) {
await buildQueue.pause();
console.log(`Updating Coolify to ${latestVersion}.`);
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-redis && docker rm coolify coolify-redis && docker compose up -d --force-recreate"`
);
} else {
await buildQueue.pause();
console.log('Updating (not really in dev mode).');
}
}
}
}
} catch (error) {
await buildQueue.resume();
console.log(error);
}
}

View File

@@ -10,6 +10,7 @@ import proxy from './proxy';
import proxyTcpHttp from './proxyTcpHttp';
import ssl from './ssl';
import sslrenewal from './sslrenewal';
import autoUpdater from './autoUpdater';
import { asyncExecShell, saveBuildLog } from '$lib/common';
@@ -34,19 +35,22 @@ const cron = async (): Promise<void> => {
new QueueScheduler('cleanup', connectionOptions);
new QueueScheduler('ssl', connectionOptions);
new QueueScheduler('sslRenew', connectionOptions);
new QueueScheduler('autoUpdater', connectionOptions);
const queue = {
proxy: new Queue('proxy', { ...connectionOptions }),
proxyTcpHttp: new Queue('proxyTcpHttp', { ...connectionOptions }),
cleanup: new Queue('cleanup', { ...connectionOptions }),
ssl: new Queue('ssl', { ...connectionOptions }),
sslRenew: new Queue('sslRenew', { ...connectionOptions })
sslRenew: new Queue('sslRenew', { ...connectionOptions }),
autoUpdater: new Queue('autoUpdater', { ...connectionOptions })
};
await queue.proxy.drain();
await queue.proxyTcpHttp.drain();
await queue.cleanup.drain();
await queue.ssl.drain();
await queue.sslRenew.drain();
await queue.autoUpdater.drain();
new Worker(
'proxy',
@@ -98,11 +102,22 @@ const cron = async (): Promise<void> => {
}
);
new Worker(
'autoUpdater',
async () => {
await autoUpdater();
},
{
...connectionOptions
}
);
await queue.proxy.add('proxy', {}, { repeat: { every: 10000 } });
await queue.proxyTcpHttp.add('proxyTcpHttp', {}, { repeat: { every: 10000 } });
await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } });
if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } });
await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } });
await queue.autoUpdater.add('autoUpdater', {}, { repeat: { every: 60000 } });
};
cron().catch((error) => {
console.log('cron failed to start');
@@ -115,6 +130,9 @@ const buildWorker = new Worker(buildQueueName, async (job) => await builder(job)
concurrency: 1,
...connectionOptions
});
buildQueue.resume().catch((err) => {
console.log('Build queue failed to resume!', err);
});
buildWorker.on('completed', async (job: Bullmq.Job) => {
try {
@@ -123,7 +141,6 @@ buildWorker.on('completed', async (job: Bullmq.Job) => {
setTimeout(async () => {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } });
}, 1234);
console.log(error);
} finally {
const workdir = `/tmp/build-sources/${job.data.repository}/${job.data.build_id}`;
if (!dev) await asyncExecShell(`rm -fr ${workdir}`);
@@ -139,7 +156,6 @@ buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => {
setTimeout(async () => {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } });
}, 1234);
console.log(error);
} finally {
const workdir = `/tmp/build-sources/${job.data.repository}`;
if (!dev) await asyncExecShell(`rm -fr ${workdir}`);

25
src/lib/translations.ts Normal file
View File

@@ -0,0 +1,25 @@
import i18n from 'sveltekit-i18n';
import lang from './lang.json';
/** @type {import('sveltekit-i18n').Config} */
export const config = {
fallbackLocale: 'en',
translations: {
en: { lang },
fr: { lang }
},
loaders: [
{
locale: 'en',
key: '',
loader: async () => (await import('./locales/en.json')).default
},
{
locale: 'fr',
key: '',
loader: async () => (await import('./locales/fr.json')).default
}
]
};
export const { t, locales, locale, loadTranslations } = new i18n(config);

View File

@@ -12,15 +12,18 @@
</script>
<script>
import { t } from '$lib/translations';
export let status;
export let error;
</script>
<div class="mx-auto flex h-screen flex-col items-center justify-center px-4">
<div class="pb-10 text-7xl font-bold">{status}</div>
<div class="text-3xl font-bold">Ooops you are lost! But don't be afraid!</div>
<div class="text-3xl font-bold">{$t('error.you_are_lost')}</div>
<div class="text-xl">
You can find your way back <a href="/" class="font-bold uppercase text-sky-400">here</a>
{$t('error.you_can_find_your_way_back')}
<a href="/" class="font-bold uppercase text-sky-400">{$t('error.here')}</a>
</div>
<div class="py-10 text-xs font-bold">
<pre

View File

@@ -1,8 +1,15 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
import { publicPaths } from '$lib/settings';
import { locale, loadTranslations } from '$lib/translations';
export const load: Load = async ({ fetch, url, session }) => {
const { pathname } = url;
const defaultLocale = 'en';
const sessionLocale = session.lang;
const initLocale = sessionLocale || locale.get() || defaultLocale;
await loadTranslations(initLocale, pathname);
if (!session.userId && !publicPaths.includes(url.pathname)) {
return {
status: 302,
@@ -39,7 +46,6 @@
import { asyncSleep } from '$lib/components/common';
import { del, get, post } from '$lib/api';
import { browser, dev } from '$app/env';
let isUpdateAvailable = false;
let updateStatus = {
@@ -456,7 +462,6 @@
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
</svg>
</a>
{#if $session.teamId === '0'}
<a
sveltekit:prefetch

View File

@@ -81,6 +81,7 @@
import { toast } from '@zerodevx/svelte-toast';
import { disabledButton } from '$lib/store';
import { onDestroy, onMount } from 'svelte';
import { t } from '$lib/translations';
if (githubToken) $gitTokens.githubToken = githubToken;
if (gitlabToken) $gitTokens.gitlabToken = gitlabToken;
@@ -99,7 +100,7 @@
async function handleDeploySubmit() {
try {
const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application });
toast.push('Deployment queued.');
toast.push($t('application.deployment_queued'));
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
} else {
@@ -113,7 +114,7 @@
}
async function deleteApplication(name) {
const sure = confirm(`Are you sure you would like to delete '${name}'?`);
const sure = confirm($t('application.confirm_to_delete', { name }));
if (sure) {
loading = true;
try {
@@ -186,8 +187,8 @@
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-red-500"
data-tooltip={$session.isAdmin
? 'Stop application'
: 'You do not have permission to stop the application.'}
? $t('application.stop_application')
: $t('application.permission_denied_stop_application')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -400,10 +401,10 @@
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`}
>
<button
title="Application Logs"
title={$t('application.logs')}
disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip="Application Logs"
data-tooltip={$t('application.logs')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -463,14 +464,14 @@
<button
on:click={() => deleteApplication(application.name)}
title="Delete application"
title={$t('application.delete_application')}
type="submit"
disabled={!$session.isAdmin}
class:hover:text-red-500={$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip={$session.isAdmin
? 'Delete application'
: 'You do not have permission to delete this application'}
? $t('application.delete_application')
: $t('application.permission_denied_delete_application')}
>
<DeleteIcon />
</button>

View File

@@ -4,6 +4,7 @@ import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
import { promises as dns } from 'dns';
import { t } from '$lib/translations';
export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
@@ -18,7 +19,9 @@ export const post: RequestHandler = async (event) => {
const found = await db.isDomainConfigured({ id, fqdn });
if (found) {
throw {
message: `Domain ${getDomain(fqdn).replace('www.', '')} is already used.`
message: t.get('application.domain_already_in_use', {
domain: getDomain(fqdn).replace('www.', '')
})
};
}
if (!dev && !forceSave) {
@@ -36,7 +39,7 @@ export const post: RequestHandler = async (event) => {
if (localIp?.length > 0) {
if (ip?.length === 0 || !ip.includes(localIp[0])) {
throw {
message: `DNS not set or propogated for ${domain}.<br><br>Please check your DNS settings.`
message: t.get('application.dns_not_set_error', { domain: domain })
};
}
}

View File

@@ -5,6 +5,7 @@
import { post } from '$lib/api';
import { findBuildPack } from '$lib/components/templates';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -43,7 +44,9 @@
buildPack.name && buildPack.color}"
><span>{buildPack.fancyName}</span>
{#if !scanning && foundConfig?.name === buildPack.name}
<span class="absolute bottom-0 pb-2 text-xs">Choose this one...</span>
<span class="absolute bottom-0 pb-2 text-xs"
>{$t('application.configuration.buildpack.choose_this_one')}</span
>
{/if}
</button>
</form>

View File

@@ -7,6 +7,7 @@
import { errorNotification } from '$lib/form';
import { onMount } from 'svelte';
import { gitTokens } from '$lib/store';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -95,9 +96,7 @@
`/applications/${id}/configuration/repository.json?repository=${selected.repository}&branch=${selected.branch}`
);
if (data.used) {
const sure = confirm(
`This branch is already used by another application. Webhooks won't work in this case for both applications. Are you sure you want to use it?`
);
const sure = confirm($t('application.configuration.branch_already_in_use'));
if (sure) {
selected.autodeploy = false;
showSave = true;
@@ -171,8 +170,10 @@
{#if repositories.length === 0 && loading.repositories === false}
<div class="flex-col text-center">
<div class="pb-4">No repositories configured for your Git Application.</div>
<a href={`/sources/${application.gitSource.id}`}><button>Configure it now</button></a>
<div class="pb-4">{$t('application.configuration.no_repositories_configured')}</div>
<a href={`/sources/${application.gitSource.id}`}
><button>{$t('application.configuration.configure_it_now')}</button></a
>
</div>
{:else}
<form on:submit|preventDefault={handleSubmit} class="flex flex-col justify-center text-center">
@@ -181,8 +182,8 @@
<div class="custom-select-wrapper">
<Select
placeholder={loading.repositories
? 'Loading repositories...'
: 'Please select a repository'}
? $t('application.configuration.loading_repositories')
: $t('application.configuration.select_a_repository')}
id="repository"
showIndicator={true}
isWaiting={loading.repositories}
@@ -196,10 +197,10 @@
<div class="custom-select-wrapper">
<Select
placeholder={loading.branches
? 'Loading branches...'
? $t('application.configuration.loading_branches')
: !selected.repository
? 'Please select a repository first'
: 'Please select a branch'}
? $t('application.configuration.select_a_repository_first')
: $t('application.configuration.select_a_branch')}
isWaiting={loading.branches}
showIndicator={selected.repository}
id="branches"
@@ -217,7 +218,7 @@
type="submit"
disabled={!showSave}
class:bg-orange-600={showSave}
class:hover:bg-orange-500={showSave}>Save</button
class:hover:bg-orange-500={showSave}>{$t('forms.save')}</button
>
</div>
</form>

View File

@@ -9,6 +9,7 @@
import { goto } from '$app/navigation';
import { del, get, post, put } from '$lib/api';
import { gitTokens } from '$lib/store';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -139,9 +140,7 @@
`/applications/${id}/configuration/repository.json?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}`
);
if (data.used) {
const sure = confirm(
`This branch is already used by another application. Webhooks won't work in this case for both applications. Are you sure you want to use it?`
);
const sure = confirm($t('application.configuration.branch_already_in_use'));
if (sure) {
autodeploy = false;
showSave = true;
@@ -270,11 +269,11 @@
<div class="flex flex-col space-y-2 px-4 xl:flex-row xl:space-y-0 xl:space-x-2 ">
{#if loading.base}
<select name="group" disabled class="w-96">
<option selected value="">Loading groups...</option>
<option selected value="">{$t('application.configuration.loading_groups')}</option>
</select>
{:else}
<select name="group" class="w-96" bind:value={selected.group} on:change={loadProjects}>
<option value="" disabled selected>Please select a group</option>
<option value="" disabled selected>{$t('application.configuration.select_a_group')}</option>
{#each groups as group}
<option value={group}>{group.full_name}</option>
{/each}
@@ -282,7 +281,7 @@
{/if}
{#if loading.projects}
<select name="project" disabled class="w-96">
<option selected value="">Loading projects...</option>
<option selected value="">{$t('application.configuration.loading_projects')}</option>
</select>
{:else if !loading.projects && projects.length > 0}
<select
@@ -292,20 +291,24 @@
on:change={loadBranches}
disabled={!selected.group}
>
<option value="" disabled selected>Please select a project</option>
<option value="" disabled selected
>{$t('application.configuration.select_a_project')}</option
>
{#each projects as project}
<option value={project}>{project.name}</option>
{/each}
</select>
{:else}
<select name="project" disabled class="w-96">
<option disabled selected value="">No projects found</option>
<option disabled selected value=""
>{$t('application.configuration.no_projects_found')}</option
>
</select>
{/if}
{#if loading.branches}
<select name="branch" disabled class="w-96">
<option selected value="">Loading branches...</option>
<option selected value="">{$t('application.configuration.loading_branches')}</option>
</select>
{:else if !loading.branches && branches.length > 0}
<select
@@ -315,14 +318,17 @@
on:change={isBranchAlreadyUsed}
disabled={!selected.project}
>
<option value="" disabled selected>Please select a branch</option>
<option value="" disabled selected>{$t('application.configuration.select_a_branch')}</option
>
{#each branches as branch}
<option value={branch}>{branch.name}</option>
{/each}
</select>
{:else}
<select name="project" disabled class="w-96">
<option disabled selected value="">No branches found</option>
<option disabled selected value=""
>{$t('application.configuration.no_branches_found')}</option
>
</select>
{/if}
</div>
@@ -334,7 +340,7 @@
disabled={!showSave || loading.save}
class:bg-orange-600={showSave && !loading.save}
class:hover:bg-orange-500={showSave && !loading.save}
>{loading.save ? 'Saving...' : 'Save'}</button
>{loading.save ? $t('forms.saving') : $t('forms.save')}</button
>
</div>
</form>

View File

@@ -36,6 +36,7 @@
import { errorNotification } from '$lib/form';
import { gitTokens } from '$lib/store';
import { browser } from '$app/env';
import { t } from '$lib/translations';
const { id } = $page.params;
@@ -204,18 +205,21 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Configure Build Pack</div>
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.configure_build_pack')}
</div>
</div>
{#if scanning}
<div class="flex justify-center space-x-1 p-6 font-bold">
<div class="text-xl tracking-tight">Scanning repository to suggest a build pack for you...</div>
<div class="text-xl tracking-tight">
{$t('application.configuration.scanning_repository_suggest_build_pack')}
</div>
</div>
{:else}
{#if packageManager === 'yarn' || packageManager === 'pnpm'}
<div class="flex justify-center p-6">
Found lock file for <span class="font-bold text-orange-500 pl-1">{packageManager}</span>.
Using it for predefined commands commands.
{@html $t('application.configuration.found_lock_file', { packageManager })}
</div>
{/if}
<div class="max-w-7xl mx-auto flex flex-wrap justify-center">

View File

@@ -33,6 +33,7 @@
import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -60,12 +61,14 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Configure Destination</div>
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.configure_destination')}
</div>
</div>
<div class="flex flex-col justify-center">
{#if !destinations || ownDestinations.length === 0}
<div class="flex-col">
<div class="pb-2">No configurable Destination found</div>
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</div>
<div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg

View File

@@ -18,6 +18,8 @@
</script>
<script lang="ts">
import { t } from '$lib/translations';
export let application;
export let appId;
@@ -26,7 +28,9 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Repository / Project</div>
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.select_a_repository_project')}
</div>
</div>
<div class="flex flex-wrap justify-center">
{#if application.gitSource.type === 'github'}

View File

@@ -33,6 +33,7 @@
import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -72,12 +73,14 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Git Source</div>
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.select_a_git_source')}
</div>
</div>
<div class="flex flex-col justify-center">
{#if !filteredSources || ownSources.length === 0}
<div class="flex-col">
<div class="pb-2 text-center">No configurable Git Source found</div>
<div class="pb-2 text-center">{$t('application.configuration.no_configurable_git')}</div>
<div class="flex justify-center">
<button on:click={newSource} class="add-icon bg-orange-600 hover:bg-orange-500">
<svg
@@ -139,7 +142,7 @@
<div class="font-bold text-xl text-center truncate">{source.name}</div>
{#if source.gitlabApp && !source.gitlabAppId}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">
Configuration missing
{$t('application.configuration.configuration_missing')}
</div>
{/if}
</button>

View File

@@ -49,6 +49,7 @@
import cuid from 'cuid';
import { browser } from '$app/env';
import { disabledButton } from '$lib/store';
import { t } from '$lib/translations';
const { id } = $page.params;
let domainEl: HTMLInputElement;
@@ -101,7 +102,7 @@
branch: application.branch,
projectId: application.projectId
});
return toast.push('Settings saved.');
return toast.push($t('application.settings_saved'));
} catch ({ error }) {
if (name === 'debug') {
debug = !debug;
@@ -126,7 +127,7 @@
$disabledButton = false;
return toast.push('Configurations saved.');
} catch ({ error }) {
if (error?.startsWith('DNS not set')) {
if (error?.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true;
}
return errorNotification(error);
@@ -216,7 +217,7 @@
<!-- svelte-ignore missing-declaration -->
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">General</div>
<div class="title">{$t('general')}</div>
{#if $session.isAdmin}
<button
type="submit"
@@ -225,13 +226,17 @@
class:hover:bg-green-500={!loading}
class:hover:bg-orange-400={forceSave}
disabled={loading}
>{loading ? 'Saving...' : forceSave ? 'Are you sure to continue?' : 'Save'}</button
>{loading
? $t('forms.saving')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="mt-2 grid grid-cols-2 items-center">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input
readonly={!$session.isAdmin}
name="name"
@@ -241,7 +246,9 @@
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="gitSource" class="text-base font-bold text-stone-100">Git Source</label>
<label for="gitSource" class="text-base font-bold text-stone-100"
>{$t('application.git_source')}</label
>
<a
href={$session.isAdmin
? `/applications/${id}/configuration/source?from=/applications/${id}`
@@ -256,7 +263,9 @@
>
</div>
<div class="grid grid-cols-2 items-center">
<label for="repository" class="text-base font-bold text-stone-100">Git Repository</label>
<label for="repository" class="text-base font-bold text-stone-100"
>{$t('application.git_repository')}</label
>
<a
href={$session.isAdmin
? `/applications/${id}/configuration/repository?from=/applications/${id}&to=/applications/${id}/configuration/buildpack`
@@ -271,7 +280,9 @@
>
</div>
<div class="grid grid-cols-2 items-center">
<label for="buildPack" class="text-base font-bold text-stone-100">Build Pack</label>
<label for="buildPack" class="text-base font-bold text-stone-100"
>{$t('application.build_pack')}</label
>
<a
href={$session.isAdmin
? `/applications/${id}/configuration/buildpack?from=/applications/${id}`
@@ -287,7 +298,9 @@
>
</div>
<div class="grid grid-cols-2 items-center pb-8">
<label for="destination" class="text-base font-bold text-stone-100">Destination</label>
<label for="destination" class="text-base font-bold text-stone-100"
>{$t('application.destination')}</label
>
<div class="no-underline">
<input
value={application.destinationDocker.name}
@@ -299,20 +312,20 @@
</div>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Application</div>
<div class="title">{$t('application.application')}</div>
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-2">
<div class="flex-col">
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100">URL (FQDN)</label>
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100"
>{$t('application.url_fqdn')}</label
>
{#if browser && window.location.hostname === 'demo.coolify.io'}
<Explainer
text="<span class='text-white font-bold'>You can use the predefined random url name or enter your own domain name.</span>"
/>
{/if}
<Explainer
text="If you specify <span class='text-green-500 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-green-500 font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the url, you must first stop the application.<br><br><span class='text-white font-bold'>You must set your DNS to point to the server IP in advance.</span>"
/>
<Explainer text={$t('application.https_explainer')} />
</div>
<input
readonly={!$session.isAdmin || isRunning}
@@ -328,12 +341,12 @@
</div>
<div class="grid grid-cols-2 items-center pb-8">
<Setting
dataTooltip="Must be stopped to modify."
dataTooltip={$t('forms.must_be_stopped_to_modify')}
disabled={isRunning}
isCenter={false}
bind:setting={dualCerts}
title="Generate SSL for www and non-www?"
description="It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-green-500'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both."
title={$t('application.ssl_www_and_non_www')}
description={$t('application.ssl_explainer')}
on:click={() => !isRunning && changeSettings('dualCerts')}
/>
</div>
@@ -372,13 +385,13 @@
{/if}
{#if !staticDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center">
<label for="port" class="text-base font-bold text-stone-100">Port</label>
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
<input
readonly={!$session.isAdmin}
name="port"
id="port"
bind:value={application.port}
placeholder={application.buildPack === 'python' ? '8000' : '3000'}
placeholder="{$t('forms.default')}: 'python' ? '8000' : '3000'"
/>
</div>
{/if}
@@ -386,34 +399,38 @@
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center">
<label for="installCommand" class="text-base font-bold text-stone-100"
>Install Command</label
>{$t('application.install_command')}</label
>
<input
readonly={!$session.isAdmin}
name="installCommand"
id="installCommand"
bind:value={application.installCommand}
placeholder="default: yarn install"
placeholder="{$t('forms.default')}: yarn install"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="buildCommand" class="text-base font-bold text-stone-100">Build Command</label>
<label for="buildCommand" class="text-base font-bold text-stone-100"
>{$t('application.build_command')}</label
>
<input
readonly={!$session.isAdmin}
name="buildCommand"
id="buildCommand"
bind:value={application.buildCommand}
placeholder="default: yarn build"
placeholder="{$t('forms.default')}: yarn build"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="startCommand" class="text-base font-bold text-stone-100">Start Command</label>
<label for="startCommand" class="text-base font-bold text-stone-100"
>{$t('application.start_command')}</label
>
<input
readonly={!$session.isAdmin}
name="startCommand"
id="startCommand"
bind:value={application.startCommand}
placeholder="default: yarn start"
placeholder="{$t('forms.default')}: yarn start"
/>
</div>
{/if}
@@ -462,29 +479,25 @@
<div class="grid grid-cols-2 items-center">
<div class="flex-col">
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"
>Base Directory</label
>{$t('forms.base_directory')}</label
>
<Explainer
text="Directory to use as the base for all commands.<br>Could be useful with <span class='text-green-500 font-bold'>monorepos</span>."
/>
<Explainer text={$t('application.directory_to_use_explainer')} />
</div>
<input
readonly={!$session.isAdmin}
name="baseDirectory"
id="baseDirectory"
bind:value={application.baseDirectory}
placeholder="default: /"
placeholder="{$t('forms.default')}: /"
/>
</div>
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center">
<div class="flex-col">
<label for="publishDirectory" class="pt-2 text-base font-bold text-stone-100"
>Publish Directory</label
>{$t('forms.publish_directory')}</label
>
<Explainer
text="Directory containing all the assets for deployment. <br> For example: <span class='text-green-500 font-bold'>dist</span>,<span class='text-green-500 font-bold'>_site</span> or <span class='text-green-500 font-bold'>public</span>."
/>
<Explainer text={$t('application.publish_directory_explainer')} />
</div>
<input
@@ -492,14 +505,14 @@
name="publishDirectory"
id="publishDirectory"
bind:value={application.publishDirectory}
placeholder=" default: /"
placeholder=" {$t('forms.default')}: /"
/>
</div>
{/if}
</div>
</form>
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">Features</div>
<div class="title">{$t('application.features')}</div>
</div>
<div class="px-10 pb-10">
<div class="grid grid-cols-2 items-center">
@@ -507,8 +520,8 @@
isCenter={false}
bind:setting={autodeploy}
on:click={() => changeSettings('autodeploy')}
title="Enable Automatic Deployment"
description="Enable automatic deployment through webhooks."
title={$t('application.enable_automatic_deployment')}
description={$t('application.enable_auto_deploy_webhooks')}
/>
</div>
<div class="grid grid-cols-2 items-center">
@@ -516,8 +529,8 @@
isCenter={false}
bind:setting={previews}
on:click={() => changeSettings('previews')}
title="Enable MR/PR Previews"
description="Enable preview deployments from pull or merge requests."
title={$t('application.enable_mr_pr_previews')}
description={$t('application.enable_preview_deploy_mr_pr_requests')}
/>
</div>
<div class="grid grid-cols-2 items-center">
@@ -525,8 +538,8 @@
isCenter={false}
bind:setting={debug}
on:click={() => changeSettings('debug')}
title="Debug Logs"
description="Enable debug logs during build phase.<br><span class='text-red-500 font-bold'>Sensitive information</span> could be visible and saved in logs."
title={$t('application.debug_logs')}
description={$t('application.enable_debug_log_during_build')}
/>
</div>
</div>

View File

@@ -10,6 +10,7 @@
import LoadingLogs from '../_Loading.svelte';
import { get } from '$lib/api';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
let logs = [];
let loading = true;
@@ -84,7 +85,7 @@
<LoadingLogs />
{/if}
{#if currentStatus === 'queued'}
<div class="text-center font-bold text-xl">Queued and waiting for execution.</div>
<div class="text-center font-bold text-xl">{$t('application.build.queued_waiting_exec')}</div>
{:else}
<div class="flex justify-end sticky top-0 p-2">
<button

View File

@@ -26,6 +26,7 @@
import BuildLog from './_BuildLog.svelte';
import { get } from '$lib/api';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
export let builds;
export let application;
@@ -86,7 +87,9 @@
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">Build Logs</div>
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
{$t('application.build_logs')}
</div>
<span class="text-xs">{application.name} </span>
</div>
@@ -183,12 +186,14 @@
<div class="w-48 text-center text-xs">
{#if build.status === 'running'}
<div class="font-bold">Running</div>
<div class="font-bold">{$t('application.build.running')}</div>
{:else if build.status === 'queued'}
<div class="font-bold">Queued</div>
<div class="font-bold">{$t('application.build.queued')}</div>
{:else}
<div>{build.since}</div>
<div>Finished in <span class="font-bold">{build.took}s</span></div>
<div>
{$t('application.build.finished_in')} <span class="font-bold">{build.took}s</span>
</div>
{/if}
</div>
</div>
@@ -197,7 +202,8 @@
{#if !noMoreBuilds}
{#if buildCount > 5}
<div class="flex space-x-2">
<button disabled={noMoreBuilds} class="w-full" on:click={loadMoreBuilds}>Load More</button
<button disabled={noMoreBuilds} class="w-full" on:click={loadMoreBuilds}
>{$t('application.build.load_more')}</button
>
</div>
{/if}
@@ -212,5 +218,5 @@
</div>
</div>
{#if buildCount === 0}
<div class="text-center text-xl font-bold">No logs found</div>
<div class="text-center text-xl font-bold">{$t('application.build.no_logs')}</div>
{/if}

View File

@@ -10,6 +10,10 @@ export const get: RequestHandler = async (event) => {
if (status === 401) return { status, body };
const { id } = event.params;
let since = event.url.searchParams.get('since') || 0;
if (since !== 0) {
since = dayjs(since).unix();
}
try {
const { destinationDockerId, destinationDocker } = await db.prisma.application.findUnique({
where: { id },
@@ -20,16 +24,22 @@ export const get: RequestHandler = async (event) => {
try {
const container = await docker.engine.getContainer(id);
if (container) {
const logs = (
await container.logs({
stdout: true,
stderr: true,
timestamps: true,
since,
tail: 5000
})
)
.toString()
.split('\n')
.map((l) => l.slice(8))
.filter((a) => a);
return {
body: {
logs: (
await container.logs({ stdout: true, stderr: true, timestamps: true, tail: 5000 })
)
.toString()
.split('\n')
.map((l) => l.slice(8))
.filter((a) => a)
.reverse()
logs
}
};
}

View File

@@ -26,17 +26,15 @@
import LoadingLogs from './_Loading.svelte';
import { get } from '$lib/api';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
let loadLogsInterval = null;
let allLogs = {
logs: []
};
let logs = [];
let currentPage = 1;
let endOfLogs = false;
let startOfLogs = true;
let lastLog = null;
let followingInterval;
let followingLogs;
let logsEl;
let position = 0;
const { id } = $page.params;
onMount(async () => {
@@ -52,52 +50,50 @@
async function loadAllLogs() {
try {
const data: any = await get(`/applications/${id}/logs.json`);
allLogs = data.logs;
logs = data.logs.slice(0, 100);
if (logs.length < 100) {
endOfLogs = true;
if (data?.logs) {
lastLog = data.logs[data.logs.length - 1];
logs = data.logs;
}
return;
} catch ({ error }) {
} catch (error) {
console.log(error);
return errorNotification(error);
}
}
async function loadLogs() {
try {
const newLogs = await get(`/applications/${id}/logs.json`);
logs = newLogs.logs.slice(0, 100);
return;
} catch ({ error }) {
const newLogs: any = await get(
`/applications/${id}/logs.json?since=${lastLog?.split(' ')[0] || 0}`
);
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(newLogs.logs);
lastLog = newLogs.logs[newLogs.logs.length - 1];
}
} catch (error) {
return errorNotification(error);
}
}
async function loadOlderLogs() {
clearInterval(loadLogsInterval);
loadLogsInterval = null;
logsEl.scrollTop = 0;
if (logs.length < 100) {
endOfLogs = true;
return;
}
startOfLogs = false;
endOfLogs = false;
currentPage += 1;
logs = allLogs.slice(currentPage * 100 - 100, currentPage * 100);
}
async function loadNewerLogs() {
currentPage -= 1;
logsEl.scrollTop = 0;
if (currentPage !== 1) {
clearInterval(loadLogsInterval);
endOfLogs = false;
loadLogsInterval = null;
logs = allLogs.slice(currentPage * 100 - 100, currentPage * 100);
function detect() {
if (position < logsEl.scrollTop) {
position = logsEl.scrollTop;
} else {
startOfLogs = true;
loadLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
if (followingLogs) {
clearInterval(followingInterval);
followingLogs = false;
}
position = logsEl.scrollTop;
}
}
function followBuild() {
followingLogs = !followingLogs;
if (followingLogs) {
followingInterval = setInterval(() => {
logsEl.scrollTop = logsEl.scrollHeight;
window.scrollTo(0, document.body.scrollHeight);
}, 1000);
} else {
clearInterval(followingInterval);
}
}
</script>
@@ -176,7 +172,7 @@
</div>
<div class="flex flex-row justify-center space-x-2 px-10 pt-6">
{#if logs.length === 0}
<div class="text-xl font-bold tracking-tighter">Waiting for the logs...</div>
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
{:else}
<div class="relative w-full">
<div class="text-right " />
@@ -185,12 +181,10 @@
{/if}
<div class="flex justify-end sticky top-0 p-2 mx-1">
<button
on:click={loadOlderLogs}
class:text-coolgray-100={endOfLogs}
class:hover:bg-coolgray-400={!endOfLogs}
class="bg-transparent tooltip-bottom"
data-tooltip="Older logs"
disabled={endOfLogs}
on:click={followBuild}
class="bg-transparent"
data-tooltip="Follow logs"
class:text-green-500={followingLogs}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -203,39 +197,17 @@
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M20 15h-8v3.586a1 1 0 0 1 -1.707 .707l-6.586 -6.586a1 1 0 0 1 0 -1.414l6.586 -6.586a1 1 0 0 1 1.707 .707v3.586h8a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1z"
/>
</svg>
</button>
<button
on:click={loadNewerLogs}
class:text-coolgray-100={startOfLogs}
class:hover:bg-coolgray-400={!startOfLogs}
class="bg-transparent tooltip-bottom"
data-tooltip="Newer logs"
disabled={startOfLogs}
>
<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="M4 9h8v-3.586a1 1 0 0 1 1.707 -.707l6.586 6.586a1 1 0 0 1 0 1.414l-6.586 6.586a1 1 0 0 1 -1.707 -.707v-3.586h-8a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1z"
/>
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
</button>
</div>
<div
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl}
on:scroll={detect}
>
<div class="px-2 pr-14">
{#each logs as log}

View File

@@ -30,6 +30,7 @@
import Explainer from '$lib/components/Explainer.svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
const { id } = $page.params;
async function refreshSecrets() {
@@ -134,10 +135,12 @@
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">Name</th>
<th scope="col">Value</th>
<th scope="col" class="w-64 text-center">Need during buildtime?</th>
<th scope="col" class="w-96 text-center">Action</th>
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-64 text-center"
>{$t('application.preview.need_during_buildtime')}</th
>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>
@@ -171,13 +174,15 @@
</a>
<div class="flex items-center justify-center">
<button class="bg-coollabs hover:bg-coollabs-100" on:click={() => redeploy(container)}
>Redeploy</button
>{$t('application.preview.redeploy')}</button
>
</div>
{/each}
{:else}
<div class="flex-col">
<div class="text-center font-bold text-xl">No previews available</div>
<div class="text-center font-bold text-xl">
{$t('application.preview.no_previews_available')}
</div>
</div>
{/if}
</div>

View File

@@ -6,6 +6,7 @@
import { saveSecret } from './utils';
import pLimit from 'p-limit';
import { createEventDispatcher } from 'svelte';
import { t } from '$lib/translations';
const dispatch = createEventDispatcher();
let batchSecrets = '';
@@ -38,11 +39,11 @@
}
</script>
<h2 class="title my-6 font-bold">Paste .env file</h2>
<h2 class="title my-6 font-bold">{$t('application.secret__batch_dot_env')}</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="bg-green-600 hover:bg-green-500 disabled:text-white disabled:opacity-40"
type="submit">Batch add secrets</button
type="submit">{$t('application.batch_secrets')}</button
>
</form>

View File

@@ -12,6 +12,7 @@
import { del } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
import { toast } from '@zerodevx/svelte-toast';
import { createEventDispatcher } from 'svelte';
import { saveSecret } from './utils';
@@ -104,7 +105,7 @@
class:cursor-not-allowed={isPRMRSecret}
class:cursor-pointer={!isPRMRSecret}
>
<span class="sr-only">Use isBuildSecret</span>
<span class="sr-only">{$t('application.secrets.use_isbuildsecret')}</span>
<span
class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
class:translate-x-5={isBuildSecret}
@@ -145,17 +146,19 @@
{#if isNewSecret}
<div class="flex items-center justify-center">
<button class="bg-green-600 hover:bg-green-500" on:click={() => createSecret(true)}
>Add</button
>{$t('forms.add')}</button
>
</div>
{:else}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="" on:click={() => createSecret(false)}>Set</button>
<button class="" on:click={() => createSecret(false)}>{$t('forms.set')}</button>
</div>
{#if !isPRMRSecret}
<div class="flex justify-center items-end">
<button class="bg-red-600 hover:bg-red-500" on:click={removeSecret}>Remove</button>
<button class="bg-red-600 hover:bg-red-500" on:click={removeSecret}
>{$t('forms.remove')}</button
>
</div>
{/if}
</div>

View File

@@ -25,6 +25,7 @@
import pLimit from 'p-limit';
import Secret from './_Secret.svelte';
import { page } from '$app/stores';
import { t } from '$lib/translations';
import { get } from '$lib/api';
import { saveSecret } from './utils';
import { toast } from '@zerodevx/svelte-toast';
@@ -65,7 +66,9 @@
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">Secrets</div>
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
{$t('application.secret')}
</div>
<span class="text-xs">{application.name} </span>
</div>
@@ -137,10 +140,12 @@
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">Name</th>
<th scope="col">Value</th>
<th scope="col" class="w-64 text-center">Need during buildtime?</th>
<th scope="col" class="w-96 text-center">Action</th>
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-64 text-center"
>{$t('application.preview.need_during_buildtime')}</th
>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>

View File

@@ -1,6 +1,7 @@
import { toast } from '@zerodevx/svelte-toast';
import { errorNotification } from '$lib/form';
import { post } from '$lib/api';
import { t } from '$lib/translations';
type Props = {
isNew: boolean;
@@ -21,8 +22,8 @@ export async function saveSecret({
isNewSecret,
applicationId
}: Props): Promise<void> {
if (!name) return errorNotification('Name is required.');
if (!value) return errorNotification('Value is required.');
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(`/applications/${applicationId}/secrets.json`, {
name,

View File

@@ -1,6 +1,7 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { t } from '$lib/translations';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
@@ -14,8 +15,7 @@ export const post: RequestHandler = async (event) => {
const isDouble = await db.checkDoubleBranch(branch, projectId);
if (isDouble && autodeploy) {
throw {
message:
'Cannot activate automatic deployments until only one application is defined for this repository / branch.'
message: t.get('application.cant_activate_auto_deploy_without_repo')
};
}
await db.setApplicationSettings({ id, debug, previews, dualCerts, autodeploy });

View File

@@ -10,12 +10,13 @@
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
const { id } = $page.params;
const dispatch = createEventDispatcher();
async function saveStorage(newStorage = false) {
try {
if (!storage.path) return errorNotification('Path is required.');
if (!storage.path) return errorNotification($t('application.storage.path_is_required'));
storage.path = storage.path.startsWith('/') ? storage.path : `/${storage.path}`;
storage.path = storage.path.endsWith('/') ? storage.path.slice(0, -1) : storage.path;
storage.path.replace(/\/\//g, '/');
@@ -29,8 +30,8 @@
storage.path = null;
storage.id = null;
}
if (newStorage) toast.push('Storage saved.');
else toast.push('Storage updated.');
if (newStorage) toast.push($t('application.storage.storage_saved'));
else toast.push($t('application.storage.storage_updated'));
} catch ({ error }) {
return errorNotification(error);
}
@@ -39,7 +40,7 @@
try {
await del(`/applications/${id}/storage.json`, { path: storage.path });
dispatch('refresh');
toast.push('Storage deleted.');
toast.push($t('application.storage.storage_deleted'));
} catch ({ error }) {
return errorNotification(error);
}
@@ -57,16 +58,19 @@
<td>
{#if isNew}
<div class="flex items-center justify-center">
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveStorage(true)}>Add</button
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveStorage(true)}
>{$t('forms.add')}</button
>
</div>
{:else}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="" on:click={() => saveStorage(false)}>Set</button>
<button class="" on:click={() => saveStorage(false)}>{$t('forms.set')}</button>
</div>
<div class="flex justify-center items-end">
<button class="bg-red-600 hover:bg-red-500" on:click={removeStorage}>Remove</button>
<button class="bg-red-600 hover:bg-red-500" on:click={removeStorage}
>{$t('forms.remove')}</button
>
</div>
</div>
{/if}

View File

@@ -28,6 +28,7 @@
import Storage from './_Storage.svelte';
import { get } from '$lib/api';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
const { id } = $page.params;
async function refreshStorage() {
@@ -111,15 +112,12 @@
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={'You can specify any folder that you want to be persistent across deployments. <br>This is useful for storing data such as a database (SQLite) or a cache.'}
/>
<Explainer customClass="w-full" text={$t('application.storage.persistent_storage_explainer')} />
</div>
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">Path</th>
<th scope="col">{$t('forms.path')}</th>
</tr>
</thead>
<tbody>

View File

@@ -3,6 +3,8 @@
import { session } from '$app/stores';
import { post } from '$lib/api';
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
import { getDomain } from '$lib/components/common';
import Rust from '$lib/components/svg/applications/Rust.svelte';
import Nodejs from '$lib/components/svg/applications/Nodejs.svelte';
@@ -20,7 +22,6 @@
import Astro from '$lib/components/svg/applications/Astro.svelte';
import Eleventy from '$lib/components/svg/applications/Eleventy.svelte';
import Deno from '$lib/components/svg/applications/Deno.svelte';
import { getDomain } from '$lib/components/common';
async function newApplication() {
const { id } = await post('/applications/new', {});
@@ -39,7 +40,7 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl ">Applications</div>
<div class="mr-4 text-2xl ">{$t('index.applications')}</div>
{#if $session.isAdmin}
<div on:click={newApplication} class="add-icon cursor-pointer bg-green-600 hover:bg-green-500">
<svg
@@ -61,7 +62,7 @@
<div class="flex flex-col flex-wrap justify-center">
{#if !applications || ownApplications.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">No applications found</div>
<div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div>
</div>
{/if}
{#if ownApplications.length > 0 || otherApplications.length > 0}

View File

@@ -57,7 +57,8 @@ export const post: RequestHandler = async (event) => {
headers: {
'set-cookie': [
`${cookie}=${value}; HttpOnly; Path=/; Max-Age=15778800;`,
'gitlabToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
'gitlabToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT',
'githubToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
],
Location: from
}

View File

@@ -1,6 +1,7 @@
<script>
export let database;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { t } from '$lib/translations';
</script>
<div class="flex space-x-1 py-5 font-bold">
@@ -8,34 +9,38 @@
</div>
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100">Default Database</label>
<label for="defaultDatabase" class="text-base font-bold text-stone-100"
>{$t('database.default_database')}</label
>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="eg: mydb"
placeholder="{$t('forms.eg')}: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">User</label>
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label
>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
@@ -43,22 +48,24 @@
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="rootUser"
name="rootUser"
value={database.rootUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<label for="rootUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.roots_password')}</label
>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="rootUserPassword"
name="rootUserPassword"

View File

@@ -18,6 +18,7 @@
import { post } from '$lib/api';
import { getDomain } from '$lib/components/common';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
const { id } = $page.params;
@@ -59,7 +60,7 @@
: window.location.hostname
: database.id
}:${isPublic ? database.publicPort : privatePort}/${databaseDefault}`
: 'Loading...');
: $t('forms.loading'));
}
async function changeSettings(name) {
@@ -110,20 +111,20 @@
<div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">General</div>
<div class="title">{$t('general')}</div>
{#if $session.isAdmin}
<button
type="submit"
class:bg-purple-600={!loading}
class:hover:bg-purple-500={!loading}
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
disabled={loading}>{loading ? $t('forms.saving') : $t('forms.save')}</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input
readonly={!$session.isAdmin}
name="name"
@@ -133,7 +134,9 @@
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="destination" class="text-base font-bold text-stone-100">Destination</label>
<label for="destination" class="text-base font-bold text-stone-100"
>{$t('application.destination')}</label
>
{#if database.destinationDockerId}
<div class="no-underline">
<input
@@ -148,16 +151,17 @@
</div>
<div class="grid grid-cols-2 items-center">
<label for="version" class="text-base font-bold text-stone-100">Version</label>
<label for="version" class="text-base font-bold text-stone-100">{$t('forms.version')}</label
>
<input value={database.version} readonly disabled class="bg-transparent " />
</div>
</div>
<div class="grid grid-flow-row gap-2 px-10 pt-2">
<div class="grid grid-cols-2 items-center">
<label for="host" class="text-base font-bold text-stone-100">Host</label>
<label for="host" class="text-base font-bold text-stone-100">{$t('forms.host')}</label>
<CopyPasswordField
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField={false}
readonly
disabled
@@ -167,9 +171,10 @@
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="publicPort" class="text-base font-bold text-stone-100">Port</label>
<label for="publicPort" class="text-base font-bold text-stone-100">{$t('forms.port')}</label
>
<CopyPasswordField
placeholder="Generated automatically after set to public"
placeholder={$t('database.generated_automatically_after_set_to_public')}
id="publicPort"
readonly
disabled
@@ -191,10 +196,12 @@
<CouchDb {database} />
{/if}
<div class="grid grid-cols-2 items-center px-10 pb-8">
<label for="url" class="text-base font-bold text-stone-100">Connection String</label>
<label for="url" class="text-base font-bold text-stone-100"
>{$t('database.connection_string')}</label
>
<CopyPasswordField
textarea={true}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField={false}
id="url"
name="url"
@@ -206,7 +213,7 @@
</div>
</form>
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">Features</div>
<div class="title">{$t('application.features')}</div>
</div>
<div class="px-10 pb-10">
<div class="grid grid-cols-2 items-center">
@@ -214,8 +221,8 @@
loading={publicLoading}
bind:setting={isPublic}
on:click={() => changeSettings('isPublic')}
title="Set it public"
description="Your database will be reachable over the internet. <br>Take security seriously in this case!"
title={$t('database.set_public')}
description={$t('database.warning_database_public')}
disabled={!isRunning}
/>
</div>
@@ -224,8 +231,8 @@
<Setting
bind:setting={appendOnly}
on:click={() => changeSettings('appendOnly')}
title="Change append only mode"
description="Useful if you would like to restore redis data from a backup.<br><span class='font-bold text-white'>Database restart is required.</span>"
title={$t('database.change_append_only_mode')}
description={$t('database.warning_append_only')}
/>
</div>
{/if}

View File

@@ -3,6 +3,7 @@
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
</script>
<div class="flex space-x-1 py-5 font-bold">
@@ -10,9 +11,9 @@
</div>
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label>
<CopyPasswordField
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="rootUser"
readonly
disabled
@@ -21,11 +22,13 @@
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<label for="rootUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.roots_password')}</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField={true}
id="rootUserPassword"
name="rootUserPassword"

View File

@@ -3,6 +3,7 @@
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
</script>
<div class="flex space-x-1 py-5 font-bold">
@@ -10,34 +11,38 @@
</div>
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100">Default Database</label>
<label for="defaultDatabase" class="text-base font-bold text-stone-100"
>{$t('database.default_database')}</label
>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="eg: mydb"
placeholder="{$t('forms.eg')}: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">User</label>
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
@@ -46,22 +51,24 @@
<Explainer text="Could be changed while the database is running." />
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="rootUser"
name="rootUser"
value={database.rootUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<label for="rootUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.roots_password')}</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="rootUserPassword"
name="rootUserPassword"

View File

@@ -3,6 +3,7 @@
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
</script>
<div class="flex space-x-1 py-5 font-bold">
@@ -10,12 +11,14 @@
</div>
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100">Default Database</label>
<label for="defaultDatabase" class="text-base font-bold text-stone-100"
>{$t('database.default_database')}</label
>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="eg: mydb"
placeholder="{$t('forms.eg')}: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
@@ -37,22 +40,24 @@
<Explainer text="Could be changed while the database is running." />
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">User</label>
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<CopyPasswordField
readonly
disabled
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="dbUserPassword"
name="dbUserPassword"

View File

@@ -3,6 +3,7 @@
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
</script>
<div class="flex space-x-1 py-5 font-bold">
@@ -10,11 +11,13 @@
</div>
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="dbUserPassword"
name="dbUserPassword"

View File

@@ -63,6 +63,7 @@
import Loading from '$lib/components/Loading.svelte';
import { del, post } from '$lib/api';
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
export let database;
export let isRunning;
@@ -83,7 +84,7 @@
}
}
async function stopDatabase() {
const sure = confirm(`Are you sure you would like to stop '${database.name}'?`);
const sure = confirm($t('database.confirm_stop', { name: database.name }));
if (sure) {
loading = true;
try {
@@ -113,13 +114,13 @@
{#if isRunning}
<button
on:click={stopDatabase}
title="Stop database"
title={$t('database.stop_database')}
type="submit"
disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-red-500"
data-tooltip={$session.isAdmin
? 'Stop database'
: 'You do not have permission to stop the database.'}
? $t('database.stop_database')
: $t('database.permission_denied_stop_database')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -139,13 +140,13 @@
{:else}
<button
on:click={startDatabase}
title="Start database"
title={$t('database.start_database')}
type="submit"
disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-green-500"
data-tooltip={$session.isAdmin
? 'Start database'
: 'You do not have permission to start the database.'}
? $t('database.start_database')
: $t('database.permission_denied_start_database')}
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
@@ -164,14 +165,14 @@
{/if}
<button
on:click={deleteDatabase}
title="Delete Database"
title={$t('database.delete_database')}
type="submit"
disabled={!$session.isAdmin}
class:hover:text-red-500={$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip={$session.isAdmin
? 'Delete Database'
: 'You do not have permission to delete a Database'}><DeleteIcon /></button
? $t('database.delete_database')
: $t('database.permission_denied_delete_database')}><DeleteIcon /></button
>
{/if}
</nav>

View File

@@ -34,6 +34,7 @@
import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -52,12 +53,14 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Configure Destination</div>
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.configure_destination')}
</div>
</div>
<div class="flex justify-center">
{#if !destinations || destinations.length === 0}
<div class="flex-col">
<div class="pb-2">No configurable Destination found</div>
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</div>
<div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg

View File

@@ -42,6 +42,7 @@
import Redis from '$lib/components/svg/databases/Redis.svelte';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { t } from '$lib/translations';
async function handleSubmit(type) {
try {
await post(`/databases/${id}/configuration/type.json`, { type });
@@ -53,7 +54,7 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Database type</div>
<div class="mr-4 text-2xl tracking-tight">{$t('database.select_database_type')}</div>
</div>
<div class="flex flex-wrap justify-center">

View File

@@ -31,6 +31,7 @@
import { enhance, errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -47,7 +48,7 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Database version</div>
<div class="mr-4 text-2xl tracking-tight">{$t('database.select_database_version')}</div>
</div>
<div class="flex flex-wrap justify-center">

View File

@@ -9,6 +9,7 @@
import { post } from '$lib/api';
import { goto } from '$app/navigation';
import { session } from '$app/stores';
import { t } from '$lib/translations';
async function newDatabase() {
const { id } = await post('/databases/new', {});
@@ -27,7 +28,7 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Databases</div>
<div class="mr-4 text-2xl tracking-tight">{$t('index.databases')}</div>
<div on:click={newDatabase} class="add-icon cursor-pointer bg-purple-600 hover:bg-purple-500">
<svg
class="w-6"
@@ -48,7 +49,7 @@
<div class="flex flex-col flex-wrap justify-center">
{#if !databases || ownDatabases.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">No databases found</div>
<div class="text-center text-xl font-bold">{$t('database.no_databases_found')}</div>
</div>
{/if}
{#if ownDatabases.length > 0 || otherDatabases.length > 0}
@@ -78,7 +79,7 @@
{/if}
{#if !database.type}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
{$t('application.configuration.configuration_missing')}
</div>
{/if}
</div>

View File

@@ -2,6 +2,7 @@
export let app;
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { t } from '$lib/translations';
const { id } = $page.params;
let loading = true;
async function checkApp() {
@@ -58,20 +59,20 @@
<div class="box-selection hover:border-transparent hover:bg-coolgray-200">
<div class="truncate pb-2 text-center text-xl font-bold">{app.domain}</div>
{#if loading}
<div class="w-full text-center font-bold">Loading...</div>
<div class="w-full text-center font-bold">{$t('forms.loading')}</div>
{:else if app.foundByDomain}
<div class="w-full bg-coolgray-200 text-xs">
<span class="text-red-500">Domain</span> already used for
{@html $t('forms.already_used_for', { type: 'Domains' })}
<span class="text-red-500">{app.foundName}</span>
</div>
{:else if app.foundByRepository}
<div class="w-full bg-coolgray-200 text-xs">
<span class="text-red-500">Repository</span> already used for
{@html $t('forms.already_used_for', { type: 'Repository' })}
<span class="text-red-500">{app.foundName}</span>
</div>
{:else}
<button class="bg-green-600 hover:bg-green-500 w-full" on:click={addToCoolify}
>Add to Coolify</button
>{$t('destination.add_to_coolify')}</button
>
{/if}
</div>

View File

@@ -10,6 +10,7 @@
import { post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { onMount } from 'svelte';
import { t } from '$lib/translations';
const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
let loading = false;
@@ -85,7 +86,7 @@
async function stopProxy() {
try {
await post(`/destinations/${id}/stop.json`, { engine: destination.engine });
return toast.push('Coolify Proxy stopped!');
return toast.push($t('destination.coolify_proxy_stopped'));
} catch ({ error }) {
return errorNotification(error);
}
@@ -93,19 +94,17 @@
async function startProxy() {
try {
await post(`/destinations/${id}/start.json`, { engine: destination.engine });
return toast.push('Coolify Proxy started!');
return toast.push($t('destination.coolify_proxy_started'));
} catch ({ error }) {
return errorNotification(error);
}
}
async function forceRestartProxy() {
const sure = confirm(
'Are you sure you want to restart the proxy? Everything will be reconfigured in ~10 secs.'
);
const sure = confirm($t('destination.confirm_restart_proxy'));
if (sure) {
try {
restarting = true;
toast.push('Coolify Proxy restarting...');
toast.push($t('destination.coolify_proxy_restarting'));
await post(`/destinations/${id}/restart.json`, {
engine: destination.engine,
fqdn: settings.fqdn
@@ -123,7 +122,7 @@
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-5">
<div class="title font-bold">Configuration</div>
<div class="title font-bold">{$t('forms.configuration')}</div>
{#if $session.isAdmin}
<button
type="submit"
@@ -131,13 +130,15 @@
class:bg-sky-600={!loading}
class:hover:bg-sky-500={!loading}
disabled={loading}
>{loading ? 'Saving...' : 'Save'}
>{loading ? $t('forms.saving') : $t('forms.save')}
</button>
<button
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'}
disabled={restarting}
on:click|preventDefault={forceRestartProxy}
>{restarting ? 'Restarting... please wait...' : 'Force restart proxy'}</button
>{restarting
? $t('destination.restarting_please_wait')
: $t('destination.force_restart_proxy')}</button
>
{/if}
<!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
@@ -145,10 +146,10 @@
> -->
</div>
<div class="grid grid-cols-2 items-center px-10 ">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input
name="name"
placeholder="name"
placeholder={$t('forms.name')}
disabled={!$session.isAdmin}
readonly={!$session.isAdmin}
bind:value={destination.name}
@@ -156,13 +157,13 @@
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="engine" class="text-base font-bold text-stone-100">Engine</label>
<label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label>
<CopyPasswordField
id="engine"
readonly
disabled
name="engine"
placeholder="eg: /var/run/docker.sock"
placeholder="{$t('forms.eg')}: /var/run/docker.sock"
value={destination.engine}
/>
</div>
@@ -171,13 +172,13 @@
<input name="remoteEngine" type="checkbox" bind:checked={payload.remoteEngine} />
</div> -->
<div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">Network</label>
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<CopyPasswordField
id="network"
readonly
disabled
name="network"
placeholder="default: coolify"
placeholder="{$t('forms.default')}: coolify"
value={destination.network}
/>
</div>
@@ -188,7 +189,7 @@
disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
title="Use Coolify Proxy?"
title={$t('destination.use_coolify_proxy')}
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${
cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'

View File

@@ -11,6 +11,7 @@
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { onMount } from 'svelte';
import { generateRemoteEngine } from '$lib/components/common';
import { t } from '$lib/translations';
const { id } = $page.params;
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
// let scannedApps = [];
@@ -90,7 +91,7 @@
try {
const engine = generateRemoteEngine(destination);
await post(`/destinations/${id}/stop.json`, { engine });
return toast.push('Coolify Proxy stopped!');
return toast.push($t('destination.coolify_proxy_stopped'));
} catch ({ error }) {
return errorNotification(error);
}
@@ -99,19 +100,17 @@
try {
const engine = generateRemoteEngine(destination);
await post(`/destinations/${id}/start.json`, { engine });
return toast.push('Coolify Proxy started!');
return toast.push($t('destination.coolify_proxy_started'));
} catch ({ error }) {
return errorNotification(error);
}
}
async function forceRestartProxy() {
const sure = confirm(
'Are you sure you want to restart the proxy? Everything will be reconfigured in ~10 secs.'
);
const sure = confirm($t('destination.confirm_restart_proxy'));
if (sure) {
try {
restarting = true;
toast.push('Coolify Proxy restarting...');
toast.push($t('destination.coolify_proxy_restarting'));
await post(`/destinations/${id}/restart.json`, {
engine: destination.engine,
fqdn: settings.fqdn
@@ -127,7 +126,7 @@
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-5">
<div class="title font-bold">Configuration</div>
<div class="title font-bold">{$t('forms.configuration')}</div>
{#if $session.isAdmin}
<button
type="submit"
@@ -135,13 +134,15 @@
class:bg-sky-600={!loading}
class:hover:bg-sky-500={!loading}
disabled={loading}
>{loading ? 'Saving...' : 'Save'}
>{loading ? $t('forms.saving') : $t('forms.save')}
</button>
<button
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'}
disabled={restarting}
on:click|preventDefault={forceRestartProxy}
>{restarting ? 'Restarting... please wait...' : 'Force restart proxy'}</button
>{restarting
? $t('destination.restarting_please_wait')
: $t('destination.force_restart_proxy')}</button
>
{/if}
<!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
@@ -149,10 +150,10 @@
> -->
</div>
<div class="grid grid-cols-2 items-center px-10 ">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input
name="name"
placeholder="name"
placeholder={$t('forms.name')}
disabled={!$session.isAdmin}
readonly={!$session.isAdmin}
bind:value={destination.name}
@@ -160,13 +161,13 @@
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="engine" class="text-base font-bold text-stone-100">Engine</label>
<label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label>
<CopyPasswordField
id="engine"
readonly
disabled
name="engine"
placeholder="eg: /var/run/docker.sock"
placeholder="{$t('forms.eg')}: /var/run/docker.sock"
value={destination.engine}
/>
</div>
@@ -175,13 +176,13 @@
<input name="remoteEngine" type="checkbox" bind:checked={payload.remoteEngine} />
</div> -->
<div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">Network</label>
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<CopyPasswordField
id="network"
readonly
disabled
name="network"
placeholder="default: coolify"
placeholder="{$t('forms.default')}: coolify"
value={destination.network}
/>
</div>
@@ -190,7 +191,7 @@
disabled={cannotDisable}
bind:setting={destination.isCoolifyProxyUsed}
on:click={changeProxySetting}
title="Use Coolify Proxy?"
title={$t('destination.use_coolify_proxy')}
description={`This will install a proxy on the destination to allow you to access your applications and services without any manual configuration. Databases will have their own proxy. <br><br>${
cannotDisable
? '<span class="font-bold text-white">You cannot disable this proxy as FQDN is configured for Coolify.</span>'

View File

@@ -37,10 +37,11 @@
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import { del } from '$lib/api';
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
export let destination;
async function deleteDestination(destination) {
const sure = confirm(`Are you sure you would like to delete '${destination.name}'?`);
const sure = confirm($t('application.confirm_to_delete', { name: destination.name }));
if (sure) {
try {
await del(`/destinations/${destination.id}.json`, { id: destination.id });
@@ -55,14 +56,14 @@
<nav class="nav-side">
<button
on:click={() => deleteDestination(destination)}
title="Delete Destination"
title={$t('destination.delete_destination')}
type="submit"
disabled={!$session.isAdmin}
class:hover:text-red-500={$session.isAdmin}
class="icons tooltip-bottom bg-transparent text-sm"
data-tooltip={$session.isAdmin
? 'Delete Destination'
: 'You do not have permission to delete this destination'}><DeleteIcon /></button
? $t('destination.delete_destination')
: $t('destination.permission_denied_delete_destination')}><DeleteIcon /></button
>
</nav>
<slot />

View File

@@ -36,10 +36,11 @@
import type Prisma from '@prisma/client';
import LocalDocker from './_LocalDocker.svelte';
import RemoteDocker from './_RemoteDocker.svelte';
import { t } from '$lib/translations';
</script>
<div class="flex space-x-1 p-6 text-2xl font-bold">
<div class="tracking-tight">Destination</div>
<div class="tracking-tight">{$t('application.destination')}</div>
<span class="arrow-right-applications px-1">></span>
<span class="pr-2">{destination.name}</span>
</div>

View File

@@ -23,6 +23,8 @@
import type Prisma from '@prisma/client';
import { session } from '$app/stores';
import { t } from '$lib/translations';
export let destinations: Prisma.DestinationDocker[];
const ownDestinations = destinations.filter((destination) => {
if (destination.teams[0].id === $session.teamId) {
@@ -37,7 +39,7 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Destinations</div>
<div class="mr-4 text-2xl tracking-tight">{$t('index.destinations')}</div>
{#if $session.isAdmin}
<a href="/new/destination" class="add-icon bg-sky-600 hover:bg-sky-500">
<svg
@@ -59,7 +61,7 @@
<div class="flex justify-center">
{#if !destinations || ownDestinations.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">No destination found</div>
<div class="text-center text-xl font-bold">{$t('destination.no_destination_found')}</div>
</div>
{/if}
{#if ownDestinations.length > 0 || otherDestinations.length > 0}

View File

@@ -85,6 +85,21 @@
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Identity and Access Management</div>
<a href="/new/team" class="add-icon cursor-pointer bg-fuchsia-600 hover:bg-fuchsia-500">
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</a>
</div>
{#if invitations.length > 0}
@@ -134,14 +149,14 @@
<td class="flex space-x-2">
<form on:submit|preventDefault={() => resetPassword(account.id)}>
<button
class="mx-auto my-4 w-32 bg-coollabs hover:bg-coollabs-100 disabled:bg-coolgray-200"
class="mx-auto my-4 w-32 bg-fuchsia-600 hover:bg-fuchsia-500 disabled:bg-coolgray-200"
>Reset Password</button
>
</form>
<form on:submit|preventDefault={() => deleteUser(account.id)}>
<button
disabled={account.id === $session.userId}
class="mx-auto my-4 w-32 bg-coollabs hover:bg-coollabs-100 disabled:bg-coolgray-200"
class="mx-auto my-4 w-32 bg-red-600 hover:bg-red-500 disabled:bg-coolgray-200"
type="submit">Delete User</button
>
</form>
@@ -162,7 +177,7 @@
<a href="/iam/team/{team.id}" class="w-96 p-2 no-underline">
<div
class="box-selection relative"
class:hover:bg-cyan-600={team.id !== '0'}
class:hover:bg-fuchsia-600={team.id !== '0'}
class:hover:bg-red-500={team.id === '0'}
>
<div class="truncate text-center text-xl font-bold">
@@ -186,7 +201,7 @@
<a href="/iam/team/{team.id}" class="w-96 p-2 no-underline">
<div
class="box-selection relative"
class:hover:bg-cyan-600={team.id !== '0'}
class:hover:bg-fuchsia-600={team.id !== '0'}
class:hover:bg-red-500={team.id === '0'}
>
<div class="truncate text-center text-xl font-bold">

View File

@@ -27,6 +27,7 @@
import Explainer from '$lib/components/Explainer.svelte';
import { errorNotification } from '$lib/form';
import { post } from '$lib/api';
import { t } from '$lib/translations';
const { id } = $page.params;
let invitation = {
teamName: team.name,
@@ -94,25 +95,22 @@
</script>
<div class="flex space-x-1 p-6 px-6 text-2xl font-bold">
<div class="tracking-tight">Team</div>
<span class="arrow-right-applications px-1 text-cyan-500">></span>
<div class="tracking-tight">{$t('index.team')}</div>
<span class="arrow-right-applications px-1 text-fuchsia-500">></span>
<span class="pr-2">{team.name}</span>
</div>
<div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmit} class=" py-4">
<div class="flex space-x-1 pb-5">
<div class="title font-bold">Settings</div>
<button class="bg-cyan-600 hover:bg-cyan-500" type="submit">Save</button>
<div class="title font-bold">{$t('index.settings')}</div>
<button class="bg-fuchsia-600 hover:bg-fuchsia-500" type="submit">{$t('forms.save')}</button>
</div>
<div class="grid grid-flow-row gap-2 px-10">
<div class="mt-2 grid grid-cols-2">
<div class="flex-col">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
{#if team.id === '0'}
<Explainer
customClass="w-full"
text="This is the <span class='text-red-500 font-bold'>root</span> team. That means members of this group can manage instance wide settings and have all the priviliges in Coolify (imagine like root user on Linux)."
/>
<Explainer customClass="w-full" text={$t('team.root_team_explainer')} />
{/if}
</div>
<input id="name" name="name" placeholder="name" bind:value={team.name} />
@@ -121,22 +119,23 @@
</form>
<div class="flex space-x-1 py-5 pt-10 font-bold">
<div class="title">Members</div>
<div class="title">{$t('team.members')}</div>
</div>
<div class="px-4 sm:px-6">
<table class="w-full border-separate text-left">
<thead>
<tr class="h-8 border-b border-coolgray-400">
<th scope="col">Email</th>
<th scope="col">Permission</th>
<th scope="col" class="text-center">Actions</th>
<th scope="col">{$t('forms.email')}</th>
<th scope="col">{$t('team.permission')}</th>
<th scope="col" class="text-center">{$t('forms.action')}</th>
</tr>
</thead>
{#each permissions as permission}
<tr class="text-xs">
<td class="py-4"
>{permission.user.email}
<span class="font-bold">{permission.user.id === $session.userId ? '(You)' : ''}</span
<span class="font-bold"
>{permission.user.id === $session.userId ? $t('team.you') : ''}</span
></td
>
<td class="py-4">{permission.permission}</td>
@@ -144,17 +143,21 @@
<td class="flex flex-col items-center justify-center space-y-2 py-4 text-center">
<button
class="w-52 bg-red-600 hover:bg-red-500"
on:click={() => removeFromTeam(permission.user.id)}>Remove</button
on:click={() => removeFromTeam(permission.user.id)}>{$t('forms.remove')}</button
>
<button
class="w-52"
on:click={() =>
changePermission(permission.user.id, permission.id, permission.permission)}
>Promote to {permission.permission === 'admin' ? 'read' : 'admin'}</button
>{$t('team.promote_to', {
grade: permission.permission === 'admin' ? 'read' : 'admin'
})}</button
>
</td>
{:else}
<td class="text-center py-4 flex-col space-y-2"> No actions available </td>
<td class="text-center py-4 flex-col space-y-2">
{$t('forms.no_actions_available')}
</td>
{/if}
</tr>
{/each}
@@ -167,11 +170,12 @@
<td class="flex-col space-y-2 py-4 text-center">
<button
class="w-52 bg-red-600 hover:bg-red-500"
on:click={() => revokeInvitation(invitation.id)}>Revoke invitation</button
on:click={() => revokeInvitation(invitation.id)}
>{$t('team.revoke_invitation')}</button
>
</td>
{:else}
<td class="text-center py-4 flex-col space-y-2">Pending invitation</td>
<td class="text-center py-4 flex-col space-y-2">{$t('team.pending_invitation')}</td>
{/if}
</tr>
{/each}
@@ -181,18 +185,18 @@
<form on:submit|preventDefault={sendInvitation} class="py-5 pt-10">
<div class="flex space-x-1">
<div class="flex space-x-1">
<div class="title font-bold">Invite new member</div>
<button class="bg-cyan-600 hover:bg-cyan-500" type="submit">Send invitation</button>
<div class="title font-bold">{$t('team.invite_new_member')}</div>
<button class="bg-fuchsia-600 hover:bg-fuchsia-500" type="submit"
>{$t('team.send_invitation')}</button
>
</div>
</div>
<Explainer
text="You can only invite registered users at the moment - will be extended soon."
/>
<Explainer text={$t('team.invite_only_register_explainer')} />
<div class="flex-col space-y-2 px-4 pt-5 sm:px-6">
<div class="flex space-x-0">
<input
bind:value={invitation.email}
placeholder="Email address"
placeholder={$t('forms.email')}
class="mr-2 w-full"
required
/>
@@ -202,14 +206,14 @@
class="rounded-none rounded-l border border-dashed border-transparent"
type="button"
class:border-coolgray-300={invitation.permission !== 'read'}
class:bg-pink-500={invitation.permission === 'read'}>Read</button
class:bg-fuchsia-500={invitation.permission === 'read'}>{$t('team.read')}</button
>
<button
on:click={() => (invitation.permission = 'admin')}
class="rounded-none rounded-r border border-dashed border-transparent"
type="button"
class:border-coolgray-300={invitation.permission !== 'admin'}
class:bg-red-500={invitation.permission === 'admin'}>Admin</button
class:bg-red-500={invitation.permission === 'admin'}>{$t('team.admin')}</button
>
</div>
</div>

View File

@@ -20,6 +20,8 @@
</script>
<script lang="ts">
import { t } from '$lib/translations';
export let applicationsCount: number;
export let sourcesCount: number;
export let destinationsCount: number;
@@ -29,7 +31,7 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Dashboard</div>
<div class="mr-4 text-2xl tracking-tight">{$t('index.dashboard')}</div>
</div>
<div class="mt-10 pb-12 tracking-tight sm:pb-16">
@@ -44,7 +46,7 @@
class="flex cursor-pointer flex-col rounded p-6 text-center text-green-500 no-underline transition duration-150 hover:bg-green-500 hover:text-white"
>
<dt class="order-2 mt-2 text-sm font-bold uppercase leading-6 text-white">
Applications
{$t('index.applications')}
</dt>
<dd class="order-1 text-5xl font-extrabold ">
{applicationsCount}
@@ -56,7 +58,7 @@
class="flex cursor-pointer flex-col rounded p-6 text-center text-sky-500 no-underline transition duration-150 hover:bg-sky-500 hover:text-white"
>
<dt class="order-2 mt-2 text-sm font-bold uppercase leading-6 text-white">
Destinations
{$t('index.destinations')}
</dt>
<dd class="order-1 text-5xl font-extrabold ">
{destinationsCount}
@@ -68,7 +70,7 @@
class="flex cursor-pointer flex-col rounded p-6 text-center text-orange-500 no-underline transition duration-150 hover:bg-orange-500 hover:text-white"
>
<dt class="order-2 mt-2 text-sm font-bold uppercase leading-6 text-white">
Git Sources
{$t('index.git_sources')}
</dt>
<dd class="order-1 text-5xl font-extrabold ">
{sourcesCount}
@@ -79,7 +81,9 @@
sveltekit:prefetch
class="flex cursor-pointer flex-col rounded p-6 text-center text-purple-500 no-underline transition duration-150 hover:bg-purple-500 hover:text-white"
>
<dt class="order-2 mt-2 text-sm font-bold uppercase leading-6 text-white">Databases</dt>
<dt class="order-2 mt-2 text-sm font-bold uppercase leading-6 text-white">
{$t('index.databases')}
</dt>
<dd class="order-1 text-5xl font-extrabold ">{databasesCount}</dd>
</a>
<a
@@ -87,7 +91,9 @@
sveltekit:prefetch
class="flex cursor-pointer flex-col rounded p-6 text-center text-pink-500 no-underline transition duration-150 hover:bg-pink-500 hover:text-white"
>
<dt class="order-2 mt-2 text-sm font-bold uppercase leading-6 text-white">Services</dt>
<dt class="order-2 mt-2 text-sm font-bold uppercase leading-6 text-white">
{$t('index.services')}
</dt>
<dd class="order-1 text-5xl font-extrabold ">{servicesCount}</dd>
</a>
@@ -96,7 +102,9 @@
sveltekit:prefetch
class="flex cursor-pointer flex-col rounded p-6 text-center text-cyan-500 no-underline transition duration-150 hover:bg-cyan-500 hover:text-white"
>
<dt class="order-2 mt-2 text-sm font-bold uppercase leading-6 text-white">Teams</dt>
<dt class="order-2 mt-2 text-sm font-bold uppercase leading-6 text-white">
{$t('index.teams')}
</dt>
<dd class="order-1 text-5xl font-extrabold ">
{teamsCount}
</dd>

View File

@@ -4,6 +4,7 @@
import { session } from '$app/stores';
import { post } from '$lib/api';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
import { onMount } from 'svelte';
let loading = false;
let emailEl;
@@ -37,9 +38,13 @@
}
</script>
<svelt:head>
<title>{$t('login.login')}</title>
</svelt:head>
<div class="flex h-screen flex-col items-center justify-center">
{#if $session.userId}
<div class="flex justify-center px-4 text-xl font-bold">Already logged in...</div>
<div class="flex justify-center px-4 text-xl font-bold">{$t('login.already_logged_in')}</div>
{:else}
<div class="flex justify-center px-4">
<form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-2">
@@ -55,7 +60,7 @@
<input
type="email"
name="email"
placeholder="Email"
placeholder={$t('forms.email')}
autocomplete="off"
required
bind:this={emailEl}
@@ -64,7 +69,7 @@
<input
type="password"
name="password"
placeholder="Password"
placeholder={$t('forms.password')}
bind:value={password}
required
/>
@@ -76,12 +81,14 @@
class="hover:opacity-90 text-white"
class:bg-transparent={loading}
class:text-stone-600={loading}
class:bg-coollabs={!loading}>{loading ? 'Authenticating...' : 'Login'}</button
class:bg-coollabs={!loading}
>{loading ? $t('login.authenticating') : $t('login.login')}</button
>
<button
on:click|preventDefault={() => goto('/register')}
class="bg-transparent hover:bg-coolgray-300 text-white ">Register</button
class="bg-transparent hover:bg-coolgray-300 text-white "
>{$t('register.register')}</button
>
</div>
</form>

View File

@@ -7,6 +7,7 @@
import { post } from '$lib/api';
import Setting from '$lib/components/Setting.svelte';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
let loading = false;
@@ -28,7 +29,7 @@
<div class="flex justify-center px-6 pb-8">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex items-center space-x-2 pb-5">
<div class="title font-bold">Configuration</div>
<div class="title font-bold">{$t('forms.configuration')}</div>
<button
type="submit"
class:bg-sky-600={!loading}
@@ -36,36 +37,41 @@
disabled={loading}
>{loading
? payload.isCoolifyProxyUsed
? 'Saving and configuring proxy...'
: 'Saving...'
: 'Save'}</button
? $t('destination.new.saving_and_configuring_proxy')
: $t('forms.saving')
: $t('forms.save')}</button
>
</div>
<div class="mt-2 grid grid-cols-2 items-center px-10">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<input required name="name" placeholder="name" bind:value={payload.name} />
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input required name="name" placeholder={$t('forms.name')} bind:value={payload.name} />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="engine" class="text-base font-bold text-stone-100">Engine</label>
<label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label>
<input
required
name="engine"
placeholder="eg: /var/run/docker.sock"
placeholder="{$t('forms.eg')}: /var/run/docker.sock"
bind:value={payload.engine}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">Network</label>
<input required name="network" placeholder="default: coolify" bind:value={payload.network} />
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<input
required
name="network"
placeholder="{$t('forms.default')}: coolify"
bind:value={payload.network}
/>
</div>
{#if $session.teamId === '0'}
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={payload.isCoolifyProxyUsed}
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
title="Use Coolify Proxy?"
description="This will install a proxy on the destination to allow you to access your applications and services without any manual configuration (recommended for Docker).<br><br>Databases will have their own proxy."
title={$t('destination.use_coolify_proxy')}
description={$t('destination.new.install_proxy')}
/>
</div>
{/if}

View File

@@ -7,6 +7,7 @@
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
let loading = false;
@@ -25,7 +26,7 @@
<div class="flex justify-center px-6 pb-8">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex items-center space-x-2 pb-5">
<div class="title font-bold">Configuration</div>
<div class="title font-bold">{$t('forms.configuration')}</div>
<button
type="submit"
class:bg-sky-600={!loading}
@@ -33,57 +34,66 @@
disabled={loading}
>{loading
? payload.isCoolifyProxyUsed
? 'Saving and configuring proxy...'
: 'Saving...'
: 'Save'}</button
? $t('destination.new.saving_and_configuring_proxy')
: $t('forms.saving')
: $t('forms.save')}</button
>
</div>
<div class="mt-2 grid grid-cols-2 items-center px-10">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<input required name="name" placeholder="name" bind:value={payload.name} />
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<input required name="name" placeholder={$t('forms.name')} bind:value={payload.name} />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="ipAddress" class="text-base font-bold text-stone-100">IP Address</label>
<label for="ipAddress" class="text-base font-bold text-stone-100"
>{$t('forms.ip_address')}</label
>
<input
required
name="ipAddress"
placeholder="eg: 192.168..."
placeholder="{$t('forms.eg')}: 192.168..."
bind:value={payload.ipAddress}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="user" class="text-base font-bold text-stone-100">User</label>
<input required name="user" placeholder="eg: root" bind:value={payload.user} />
<label for="user" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<input required name="user" placeholder="{$t('forms.eg')}: root" bind:value={payload.user} />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="port" class="text-base font-bold text-stone-100">Port</label>
<input required name="port" placeholder="eg: 22" bind:value={payload.port} />
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
<input required name="port" placeholder="{$t('forms.eg')}: 22" bind:value={payload.port} />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="sshPrivateKey" class="text-base font-bold text-stone-100">SSH Private Key</label>
<label for="sshPrivateKey" class="text-base font-bold text-stone-100"
>{$t('forms.ssh_private_key')}</label
>
<textarea
rows="10"
class="resize-none"
required
name="sshPrivateKey"
placeholder="eg: -----BEGIN...."
placeholder="{$t('forms.eg')}: -----BEGIN...."
bind:value={payload.sshPrivateKey}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="network" class="text-base font-bold text-stone-100">Network</label>
<input required name="network" placeholder="default: coolify" bind:value={payload.network} />
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
<input
required
name="network"
placeholder="{$t('forms.default')}: coolify"
bind:value={payload.network}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={payload.isCoolifyProxyUsed}
on:click={() => (payload.isCoolifyProxyUsed = !payload.isCoolifyProxyUsed)}
title="Use Coolify Proxy?"
description="This will install a proxy on the destination to allow you to access your applications and services without any manual configuration (recommended for Docker).<br><br>Databases will have their own proxy."
title={$t('destination.use_coolify_proxy')}
description={$t('destination.new.install_proxy')}
/>
</div>
</form>

View File

@@ -1,5 +1,6 @@
import { getUserDetails } from '$lib/common';
import { isDockerNetworkExists, ErrorHandler } from '$lib/database';
import { t } from '$lib/translations';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
@@ -9,9 +10,10 @@ export const post: RequestHandler = async (event) => {
const { network } = await event.request.json();
try {
const found = await isDockerNetworkExists({ network });
if (found) {
throw {
error: `Network ${network} already configured for another team!`
error: t.get('destination.new_error_network_already_exists', { network: network })
};
}
return {

View File

@@ -2,6 +2,7 @@
import LocalDocker from './_LocalDocker.svelte';
import cuid from 'cuid';
import RemoteDocker from './_RemoteDocker.svelte';
import { t } from '$lib/translations';
let payload = {};
let selected = 'localDocker';
@@ -10,7 +11,7 @@
switch (type) {
case 'localDocker':
payload = {
name: 'Local Docker',
name: t.get('sources.local_docker'),
engine: '/var/run/docker.sock',
remoteEngine: false,
network: cuid(),
@@ -19,7 +20,7 @@
break;
case 'remoteDocker':
payload = {
name: 'Remote Docker',
name: $t('sources.remote_docker'),
remoteEngine: true,
ipAddress: null,
user: 'root',
@@ -36,12 +37,14 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Add New Destination</div>
<div class="mr-4 text-2xl tracking-tight">{$t('destination.new.add_new_destination')}</div>
</div>
<div class="flex-col space-y-2 pb-10 text-center">
<div class="text-xl font-bold text-white">Predefined destinations</div>
<div class="text-xl font-bold text-white">{$t('destination.new.predefined_destinations')}</div>
<div class="flex justify-center space-x-2">
<button class="w-32" on:click={() => setPredefined('localDocker')}>Local Docker</button>
<button class="w-32" on:click={() => setPredefined('localDocker')}
>{$t('sources.local_docker')}</button
>
<!-- <button class="w-32" on:click={() => setPredefined('remoteDocker')}>Remote Docker</button> -->
<button class="w-32" on:click={() => setPredefined('kubernetes')}>Kubernetes</button>
</div>
@@ -51,5 +54,5 @@
{:else if selected === 'remoteDocker'}
<RemoteDocker {payload} />
{:else}
<div class="text-center font-bold text-4xl py-10">Not implemented yet</div>
<div class="text-center font-bold text-4xl py-10">{$t('index.not_implemented_yet')}</div>
{/if}

View File

@@ -34,7 +34,7 @@
if (name) {
try {
const { id } = await post('/new/team.json', { name });
return await goto(`/teams/${id}`);
return await goto(`/iam/team/${id}`);
} catch ({ error }) {
return errorNotification(error);
}
@@ -50,7 +50,7 @@
<form on:submit|preventDefault={handleSubmit}>
<div class="flex flex-col items-center space-y-4">
<input name="name" placeholder="Team name" required bind:this={autofocus} bind:value={name} />
<button type="submit" class="bg-green-600 hover:bg-green-500">Save</button>
<button type="submit" class="bg-fuchsia-600 hover:bg-fuchsia-500">Save</button>
</div>
</form>
</div>

View File

@@ -6,6 +6,7 @@
import { session } from '$app/stores';
import { post } from '$lib/api';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
import { onMount } from 'svelte';
let loading = false;
@@ -23,7 +24,7 @@
if (loading) return;
if (password !== passwordCheck) {
return errorNotification('Passwords do not match.');
return errorNotification($t('forms.passwords_not_match'));
}
loading = true;
try {
@@ -60,7 +61,7 @@
</div>
<div class="flex h-screen flex-col items-center justify-center">
{#if $session.userId}
<div class="flex justify-center px-4 text-xl font-bold">Already logged in...</div>
<div class="flex justify-center px-4 text-xl font-bold">{$t('login.already_logged_in')}</div>
{:else}
<div class="flex justify-center px-4">
<form on:submit|preventDefault={handleSubmit} class="flex flex-col py-4 space-y-2">
@@ -76,7 +77,7 @@
<input
type="email"
name="email"
placeholder="Email"
placeholder={$t('forms.email')}
autocomplete="off"
required
bind:this={emailEl}
@@ -85,14 +86,14 @@
<input
type="password"
name="password"
placeholder="Password"
placeholder={$t('forms.password')}
bind:value={password}
required
/>
<input
type="password"
name="passwordCheck"
placeholder="Password again"
placeholder={$t('forms.password_again')}
bind:value={passwordCheck}
required
/>
@@ -104,17 +105,15 @@
disabled={loading}
class:bg-transparent={loading}
class:text-stone-600={loading}
class:bg-coollabs={!loading}>{loading ? 'Registering...' : 'Register'}</button
class:bg-coollabs={!loading}
>{loading ? $t('register.registering') : $t('register.register')}</button
>
</div>
</form>
</div>
{#if userCount === 0}
<div class="pt-5">
You are registering the first user. It will be the administrator of your Coolify instance.
<br />
It will take a while, because Coolify will configure itself, the proxy and other docker related
stuff.
{$t('register.first_user')}
</div>
{/if}
{/if}

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { t } from '$lib/translations';
export let readOnly;
export let service;
</script>
@@ -8,19 +9,19 @@
<div class="title">Ghost</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="email">Default Email Address</label>
<label for="email">{$t('forms.default_email_address')}</label>
<input
name="email"
id="email"
disabled
readonly
placeholder="Email address"
placeholder={$t('forms.email')}
value={service.ghost.defaultEmail}
required
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="defaultPassword">Default Password</label>
<label for="defaultPassword">{$t('forms.default_password')}</label>
<CopyPasswordField
id="defaultPassword"
isPasswordField
@@ -34,7 +35,7 @@
<div class="title">MariaDB</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbUser">Username</label>
<label for="mariadbUser">{$t('forms.username')}</label>
<CopyPasswordField
name="mariadbUser"
id="mariadbUser"
@@ -44,7 +45,7 @@
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbPassword">Password</label>
<label for="mariadbPassword">{$t('forms.password')}</label>
<CopyPasswordField
id="mariadbPassword"
isPasswordField
@@ -55,7 +56,7 @@
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbDatabase">Database</label>
<label for="mariadbDatabase">{$t('index.database')}</label>
<input
name="mariadbDatabase"
id="mariadbDatabase"
@@ -63,11 +64,11 @@
readonly={readOnly}
disabled={readOnly}
bind:value={service.ghost.mariadbDatabase}
placeholder="eg: ghost_db"
placeholder="{$t('forms.eg')}: ghost_db"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbRootUser">Root DB User</label>
<label for="mariadbRootUser">{$t('forms.root_db_user')}</label>
<CopyPasswordField
id="mariadbRootUser"
isPasswordField
@@ -78,7 +79,7 @@
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbRootUserPassword">Root DB Password</label>
<label for="mariadbRootUserPassword">{$t('forms.root_db_password')}</label>
<CopyPasswordField
id="mariadbRootUserPassword"
isPasswordField

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { t } from '$lib/translations';
export let service;
</script>
@@ -7,7 +8,7 @@
<div class="title">MeiliSearch</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="masterKey">Admin API key</label>
<label for="masterKey">{$t('forms.admin_api_key')}</label>
<CopyPasswordField
id="masterKey"
isPasswordField

View File

@@ -1,25 +1,26 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { t } from '$lib/translations';
export let service;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">MinIO Server</div>
<div class="title">MinIO</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="rootUser">Root User</label>
<label for="rootUser">{$t('forms.root_user')}</label>
<input
name="rootUser"
id="rootUser"
placeholder="User to login"
placeholder={$t('forms.username')}
value={service.minio.rootUser}
disabled
readonly
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="rootUserPassword">Root's Password</label>
<label for="rootUserPassword">{$t('forms.roots_password')}</label>
<CopyPasswordField
id="rootUserPassword"
isPasswordField
@@ -30,13 +31,13 @@
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="publicPort">API Port</label>
<label for="publicPort">{$t('forms.api_port')}</label>
<input
name="publicPort"
id="publicPort"
value={service.minio.publicPort}
disabled
readonly
placeholder="Generated automatically after start"
placeholder={$t('forms.generated_automatically_after_start')}
/>
</div>

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { t } from '$lib/translations';
export let service;
export let readOnly;
</script>
@@ -8,31 +9,31 @@
<div class="title">Plausible Analytics</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="email">Email Address</label>
<label for="email">{$t('forms.email')}</label>
<input
name="email"
id="email"
disabled={readOnly}
readonly={readOnly}
placeholder="Email address"
placeholder={$t('forms.email')}
bind:value={service.plausibleAnalytics.email}
required
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="username">Username</label>
<label for="username">{$t('forms.username')}</label>
<CopyPasswordField
name="username"
id="username"
disabled={readOnly}
readonly={readOnly}
placeholder="User to login"
placeholder={$t('forms.username')}
bind:value={service.plausibleAnalytics.username}
required
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="password">Password</label>
<label for="password">{$t('forms.password')}</label>
<CopyPasswordField
id="password"
isPasswordField
@@ -46,7 +47,7 @@
<div class="title">PostgreSQL</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlUser">Username</label>
<label for="postgresqlUser">{$t('forms.username')}</label>
<CopyPasswordField
name="postgresqlUser"
id="postgresqlUser"
@@ -56,7 +57,7 @@
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlPassword">Password</label>
<label for="postgresqlPassword">{$t('forms.password')}</label>
<CopyPasswordField
id="postgresqlPassword"
isPasswordField
@@ -67,7 +68,7 @@
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlDatabase">Database</label>
<label for="postgresqlDatabase">{$t('index.database')}</label>
<CopyPasswordField
name="postgresqlDatabase"
id="postgresqlDatabase"
@@ -80,7 +81,7 @@
<label for="postgresqlPublicPort">Public Port</label>
<div class="col-span-2 ">
<CopyPasswordField
placeholder="Generated automatically after start"
placeholder="{ $t('forms.generated_automatically_after_start') }"
readonly
disabled
id="postgresqlPublicPort"

View File

@@ -10,11 +10,13 @@
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import { errorNotification } from '$lib/form';
import { t } from '$lib/translations';
import { toast } from '@zerodevx/svelte-toast';
import Ghost from './_Ghost.svelte';
import MeiliSearch from './_MeiliSearch.svelte';
import MinIo from './_MinIO.svelte';
import PlausibleAnalytics from './_PlausibleAnalytics.svelte';
import Umami from './_Umami.svelte';
import VsCodeServer from './_VSCodeServer.svelte';
import Wordpress from './_Wordpress.svelte';
@@ -40,7 +42,7 @@
loadingVerification = true;
try {
await post(`/services/${id}/${service.type}/activate.json`, { id: service.id });
toast.push('All email verified. You can login now.');
toast.push(t.get('services.all_email_verified'));
} catch ({ error }) {
return errorNotification(error);
} finally {
@@ -53,7 +55,7 @@
dualCerts = !dualCerts;
}
await post(`/services/${id}/settings.json`, { dualCerts });
return toast.push('Settings saved.');
return toast.push(t.get('application.settings_saved'));
} catch ({ error }) {
return errorNotification(error);
}
@@ -63,25 +65,27 @@
<div class="mx-auto max-w-4xl px-6 pb-12">
<form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">General</div>
<div class="title">{$t('general')}</div>
{#if $session.isAdmin}
<button
type="submit"
class:bg-pink-600={!loading}
class:hover:bg-pink-500={!loading}
disabled={loading}>{loading ? 'Saving...' : 'Save'}</button
disabled={loading}>{loading ? $t('forms.saving') : $t('forms.save')}</button
>
{/if}
{#if service.type === 'plausibleanalytics' && isRunning}
<button on:click|preventDefault={setEmailsToVerified} disabled={loadingVerification}
>{loadingVerification ? 'Verifying' : 'Verify emails without SMTP'}</button
>{loadingVerification
? $t('forms.verifying')
: $t('forms.verify_emails_without_smtp')}</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2">
<div class="mt-2 grid grid-cols-2 items-center px-10">
<label for="name" class="text-base font-bold text-stone-100">Name</label>
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<div>
<input
readonly={!$session.isAdmin}
@@ -109,7 +113,9 @@
>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="destination" class="text-base font-bold text-stone-100">Destination</label>
<label for="destination" class="text-base font-bold text-stone-100"
>{$t('application.destination')}</label
>
<div>
{#if service.destinationDockerId}
<div class="no-underline">
@@ -125,10 +131,10 @@
</div>
<div class="grid grid-cols-2 px-10">
<div class="flex-col ">
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100">URL (FQDN)</label>
<Explainer
text="If you specify <span class='text-pink-600 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-pink-600 font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the url, you must first stop the application."
/>
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100"
>{$t('application.url_fqdn')}</label
>
<Explainer text={$t('application.https_explainer')} />
</div>
<CopyPasswordField
@@ -145,10 +151,10 @@
<div class="grid grid-cols-2 items-center px-10">
<Setting
disabled={isRunning}
dataTooltip="Must be stopped to modify."
dataTooltip={$t('forms.must_be_stopped_to_modify')}
bind:setting={dualCerts}
title="Generate SSL for www and non-www?"
description="It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-pink-600'>both DNS entries</span> set in advance.<br><br>Service needs to be restarted."
title={$t('application.ssl_www_and_non_www')}
description={$t('services.generate_www_non_www_ssl')}
on:click={() => !isRunning && changeSettings('dualCerts')}
/>
</div>
@@ -164,6 +170,8 @@
<Ghost bind:service {readOnly} />
{:else if service.type === 'meilisearch'}
<MeiliSearch bind:service />
{:else if service.type === 'umami'}
<Umami bind:service />
{/if}
</div>
</form>

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
export let service;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Umami</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="adminUser">Admin User</label>
<input name="adminUser" id="adminUser" placeholder="admin" value="admin" disabled readonly />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="umamiAdminPassword">Initial Admin Password</label>
<CopyPasswordField
isPasswordField
name="umamiAdminPassword"
id="umamiAdminPassword"
placeholder="admin"
value={service.umami.umamiAdminPassword}
disabled
readonly
/>
<Explainer
text="It could be changed in Umami. <br>This is just the password set initially after the first start."
/>
</div>

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { t } from '$lib/translations';
export let service;
</script>
@@ -8,7 +9,7 @@
<div class="title">VSCode Server</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="password">Password</label>
<label for="password">{$t('forms.password')}</label>
<CopyPasswordField
id="password"
isPasswordField

View File

@@ -6,6 +6,7 @@
import { errorNotification } from '$lib/form';
import { browser } from '$app/env';
import { getDomain } from '$lib/components/common';
import { t } from '$lib/translations';
export let service;
export let isRunning;
@@ -60,7 +61,7 @@
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="extraConfig">Extra Config</label>
<label for="extraConfig">{$t('forms.extra_config')}</label>
<textarea
bind:value={service.wordpress.extraConfig}
disabled={isRunning}
@@ -70,7 +71,7 @@
name="extraConfig"
id="extraConfig"
placeholder={!isRunning
? `eg:
? `${$t('forms.eg')}:
define('WP_ALLOW_MULTISITE', true);
define('MULTISITE', true);
@@ -106,7 +107,7 @@ define('SUBDOMAIN_INSTALL', false);`
<div class="title">MySQL</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mysqlDatabase">Database</label>
<label for="mysqlDatabase">{$t('index.database')}</label>
<input
name="mysqlDatabase"
id="mysqlDatabase"
@@ -114,22 +115,22 @@ define('SUBDOMAIN_INSTALL', false);`
readonly={readOnly}
disabled={readOnly}
bind:value={service.wordpress.mysqlDatabase}
placeholder="eg: wordpress_db"
placeholder="{$t('forms.eg')}: wordpress_db"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mysqlRootUser">Root User</label>
<label for="mysqlRootUser">{$t('forms.root_user')}</label>
<input
name="mysqlRootUser"
id="mysqlRootUser"
placeholder="MySQL Root User"
placeholder="MySQL {$t('forms.root_user')}"
value={service.wordpress.mysqlRootUser}
disabled
readonly
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mysqlRootUserPassword">Root's Password</label>
<label for="mysqlRootUserPassword">{$t('forms.roots_password')}</label>
<CopyPasswordField
id="mysqlRootUserPassword"
isPasswordField
@@ -140,11 +141,11 @@ define('SUBDOMAIN_INSTALL', false);`
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mysqlUser">User</label>
<label for="mysqlUser">{$t('forms.user')}</label>
<input name="mysqlUser" id="mysqlUser" value={service.wordpress.mysqlUser} disabled readonly />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mysqlPassword">Password</label>
<label for="mysqlPassword">{$t('forms.password')}</label>
<CopyPasswordField
id="mysqlPassword"
isPasswordField

View File

@@ -65,6 +65,7 @@
import Loading from '$lib/components/Loading.svelte';
import { del, post } from '$lib/api';
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
const { id } = $page.params;
export let service;
@@ -73,7 +74,7 @@
let loading = false;
async function deleteService() {
const sure = confirm(`Are you sure you would like to delete '${service.name}'?`);
const sure = confirm($t('application.confirm_to_delete', { name: service.name }));
if (sure) {
loading = true;
try {
@@ -88,7 +89,7 @@
}
}
async function stopService() {
const sure = confirm(`Are you sure you would like to stop '${service.name}'?`);
const sure = confirm($t('database.confirm_stop', { name: service.name }));
if (sure) {
loading = true;
try {
@@ -122,13 +123,13 @@
{#if isRunning}
<button
on:click={stopService}
title="Stop Service"
title={$t('service.stop_service')}
type="submit"
disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-red-500"
data-tooltip={$session.isAdmin
? 'Stop Service'
: 'You do not have permission to stop the service.'}
? $t('service.stop_service')
: $t('service.permission_denied_stop_service')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -148,13 +149,13 @@
{:else}
<button
on:click={startService}
title="Start Service"
title={$t('service.start_service')}
type="submit"
disabled={!$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-green-500"
data-tooltip={$session.isAdmin
? 'Start Service'
: 'You do not have permission to start the service.'}
? $t('service.start_service')
: $t('service.permission_denied_start_service')}
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
@@ -181,9 +182,9 @@
class:bg-coolgray-500={$page.url.pathname === `/services/${id}`}
>
<button
title="Configurations"
title={$t('application.configurations')}
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Configurations"
data-tooltip={$t('application.configurations')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -216,9 +217,9 @@
class:bg-coolgray-500={$page.url.pathname === `/services/${id}/secrets`}
>
<button
title="Secrets"
title={$t('application.secret')}
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Secrets"
data-tooltip={$t('application.secret')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -272,14 +273,14 @@
{/if}
<button
on:click={deleteService}
title="Delete Service"
title={$t('service.delete_service')}
type="submit"
disabled={!$session.isAdmin}
class:hover:text-red-500={$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip={$session.isAdmin
? 'Delete Service'
: 'You do not have permission to delete a service.'}><DeleteIcon /></button
? $t('service.delete_service')
: $t('service.permission_denied_delete_service')}><DeleteIcon /></button
>
{/if}
</nav>

View File

@@ -34,6 +34,7 @@
import { enhance, errorNotification } from '$lib/form';
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -50,12 +51,14 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Configure Destination</div>
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.configure_destination')}
</div>
</div>
<div class="flex justify-center">
{#if !destinations || destinations.length === 0}
<div class="flex-col">
<div class="pb-2">No configurable Destination found</div>
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</div>
<div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg

View File

@@ -41,7 +41,9 @@
import N8n from '$lib/components/svg/services/N8n.svelte';
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
import Ghost from '$lib/components/svg/services/Ghost.svelte';
import { t } from '$lib/translations';
import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte';
import Umami from '$lib/components/svg/services/Umami.svelte';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -59,7 +61,7 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Service</div>
<div class="mr-4 text-2xl tracking-tight">{$t('forms.select_a_service')}</div>
</div>
<div class="flex flex-wrap justify-center">
@@ -89,6 +91,8 @@
<Ghost isAbsolute />
{:else if type.name === 'meilisearch'}
<MeiliSearch isAbsolute />
{:else if type.name === 'umami'}
<Umami isAbsolute />
{/if}{type.fancyName}
</button>
</form>

View File

@@ -32,6 +32,7 @@
import { goto } from '$app/navigation';
import { post } from '$lib/api';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
import { t } from '$lib/translations';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -52,7 +53,7 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Service version</div>
<div class="mr-4 text-2xl tracking-tight">{$t('forms.select_a_service_version')}</div>
</div>
{#if from}
<div class="pb-10 text-center">

View File

@@ -26,6 +26,7 @@
import { getDomain } from '$lib/components/common';
import { page } from '$app/stores';
import { get } from '$lib/api';
import { t } from '$lib/translations';
import ServiceLinks from '$lib/components/ServiceLinks.svelte';
const { id } = $page.params;
@@ -42,7 +43,9 @@
class:p-6={!service.fqdn}
>
<div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">Secrets</div>
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
{$t('application.secret')}
</div>
<span class="text-xs">{service.name}</span>
</div>
{#if service.fqdn}
@@ -74,9 +77,9 @@
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">Name</th>
<th scope="col">Value</th>
<th scope="col" class="w-96 text-center">Action</th>
<th scope="col">{$t('forms.name')}</th>
<th scope="col">{$t('forms.value')}</th>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>

View File

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

View File

@@ -0,0 +1,214 @@
import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { promises as fs } from 'fs';
import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
import type { Service, DestinationDocker, Prisma } from '@prisma/client';
import bcrypt from 'bcryptjs';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const service: Service & Prisma.ServiceInclude & { destinationDocker: DestinationDocker } =
await db.getService({ id, teamId });
const {
type,
version,
destinationDockerId,
destinationDocker,
serviceSecret,
umami: {
umamiAdminPassword,
postgresqlUser,
postgresqlPassword,
postgresqlDatabase,
hashSalt
}
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const config = {
umami: {
image: `${image}:${version}`,
environmentVariables: {
DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`,
DATABASE_TYPE: 'postgresql',
HASH_SALT: hashSalt
}
},
postgresql: {
image: 'postgres:12-alpine',
volume: `${id}-postgresql-data:/var/lib/postgresql/data`,
environmentVariables: {
POSTGRES_USER: postgresqlUser,
POSTGRES_PASSWORD: postgresqlPassword,
POSTGRES_DB: postgresqlDatabase
}
}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.umami.environmentVariables[secret.name] = secret.value;
});
}
const initDbSQL = `
drop table if exists event;
drop table if exists pageview;
drop table if exists session;
drop table if exists website;
drop table if exists account;
create table account (
user_id serial primary key,
username varchar(255) unique not null,
password varchar(60) not null,
is_admin bool not null default false,
created_at timestamp with time zone default current_timestamp,
updated_at timestamp with time zone default current_timestamp
);
create table website (
website_id serial primary key,
website_uuid uuid unique not null,
user_id int not null references account(user_id) on delete cascade,
name varchar(100) not null,
domain varchar(500),
share_id varchar(64) unique,
created_at timestamp with time zone default current_timestamp
);
create table session (
session_id serial primary key,
session_uuid uuid unique not null,
website_id int not null references website(website_id) on delete cascade,
created_at timestamp with time zone default current_timestamp,
hostname varchar(100),
browser varchar(20),
os varchar(20),
device varchar(20),
screen varchar(11),
language varchar(35),
country char(2)
);
create table pageview (
view_id serial primary key,
website_id int not null references website(website_id) on delete cascade,
session_id int not null references session(session_id) on delete cascade,
created_at timestamp with time zone default current_timestamp,
url varchar(500) not null,
referrer varchar(500)
);
create table event (
event_id serial primary key,
website_id int not null references website(website_id) on delete cascade,
session_id int not null references session(session_id) on delete cascade,
created_at timestamp with time zone default current_timestamp,
url varchar(500) not null,
event_type varchar(50) not null,
event_value varchar(50) not null
);
create index website_user_id_idx on website(user_id);
create index session_created_at_idx on session(created_at);
create index session_website_id_idx on session(website_id);
create index pageview_created_at_idx on pageview(created_at);
create index pageview_website_id_idx on pageview(website_id);
create index pageview_session_id_idx on pageview(session_id);
create index pageview_website_id_created_at_idx on pageview(website_id, created_at);
create index pageview_website_id_session_id_created_at_idx on pageview(website_id, session_id, created_at);
create index event_created_at_idx on event(created_at);
create index event_website_id_idx on event(website_id);
create index event_session_id_idx on event(session_id);
insert into account (username, password, is_admin) values ('admin', '${bcrypt.hashSync(
umamiAdminPassword,
10
)}', true);`;
await fs.writeFile(`${workdir}/schema.postgresql.sql`, initDbSQL);
const Dockerfile = `
FROM ${config.postgresql.image}
COPY ./schema.postgresql.sql /docker-entrypoint-initdb.d/schema.postgresql.sql`;
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile);
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.umami.image,
environment: config.umami.environmentVariables,
networks: [network],
volumes: [],
restart: 'always',
labels: makeLabelForServices('umami'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
},
depends_on: [`${id}-postgresql`]
},
[`${id}-postgresql`]: {
build: workdir,
container_name: `${id}-postgresql`,
environment: config.postgresql.environmentVariables,
networks: [network],
volumes: [config.postgresql.volume],
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[config.postgresql.volume.split(':')[0]]: {
name: config.postgresql.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return {
status: 200
};
} catch (error) {
console.log(error);
return ErrorHandler(error);
}
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -0,0 +1,42 @@
import { getUserDetails, removeDestinationDocker } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { checkContainer, stopTcpHttpProxy } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const service = await db.getService({ id, teamId });
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
if (found) {
await removeDestinationDocker({ id, engine });
}
} catch (error) {
console.error(error);
}
try {
const found = await checkContainer(engine, `${id}-postgresql`);
if (found) {
await removeDestinationDocker({ id: `${id}-postgresql`, engine });
}
} catch (error) {
console.error(error);
}
}
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@@ -100,12 +100,13 @@ export const post: RequestHandler = async (event) => {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
const changePermissionOn = persistentStorage.map((p) => p.path);
await asyncExecShell(
`DOCKER_HOST=${host} docker exec -u root ${id} chown -R 1000:1000 ${changePermissionOn.join(
' '
)}`
);
if (changePermissionOn.length > 0) {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec -u root ${id} chown -R 1000:1000 ${changePermissionOn.join(
' '
)}`
);
}
return {
status: 200
};

View File

@@ -11,9 +11,11 @@
import N8n from '$lib/components/svg/services/N8n.svelte';
import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
import Ghost from '$lib/components/svg/services/Ghost.svelte';
import { t } from '$lib/translations';
import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte';
import { session } from '$app/stores';
import { getDomain } from '$lib/components/common';
import Umami from '$lib/components/svg/services/Umami.svelte';
export let services;
async function newService() {
@@ -33,7 +35,7 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Services</div>
<div class="mr-4 text-2xl tracking-tight">{$t('index.services')}</div>
<div on:click={newService} class="add-icon cursor-pointer bg-pink-600 hover:bg-pink-500">
<svg
class="w-6"
@@ -54,7 +56,7 @@
<div class="flex flex-col flex-wrap justify-center">
{#if !services || ownServices.length === 0}
<div class="flex-col">
<div class="text-center text-xl font-bold">No services found</div>
<div class="text-center text-xl font-bold">{$t('service.no_service')}</div>
</div>
{/if}
{#if ownServices.length > 0 || otherServices.length > 0}
@@ -85,6 +87,8 @@
<Ghost isAbsolute />
{:else if service.type === 'meilisearch'}
<MeiliSearch isAbsolute />
{:else if service.type === 'umami'}
<Umami isAbsolute />
{/if}
<div class="truncate text-center text-xl font-bold">
{service.name}
@@ -97,7 +101,7 @@
{/if}
{#if !service.type || !service.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
{$t('application.configuration.configuration_missing')}
</div>
{/if}
</div>
@@ -132,6 +136,8 @@
<Ghost isAbsolute />
{:else if service.type === 'meilisearch'}
<MeiliSearch isAbsolute />
{:else if service.type === 'umami'}
<Umami isAbsolute />
{/if}
<div class="truncate text-center text-xl font-bold">
{service.name}

View File

@@ -0,0 +1,22 @@
<script>
import { t } from '$lib/translations';
import Cookies from 'js-cookie';
import langs from '$lib/lang.json';
function setLocale(locale) {
Cookies.set('lang', locale);
return window.location.reload();
}
</script>
<div class="grid grid-cols-2 items-start pb-4">
<div class="flex-col">
<div class="text-base font-bold text-stone-100">
{$t('setting.change_language')}
</div>
</div>
<div class="items-center justify-start space-x-2 text-left">
{#each Object.entries(langs) as [lang, name]}
<button on:click={() => setLocale(lang)}>Change to {name}</button>
{/each}
</div>
</div>

View File

@@ -1,6 +1,7 @@
import { asyncExecShell, getEngine, getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { t } from '$lib/translations';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
@@ -16,7 +17,8 @@ export const post: RequestHandler = async (event) => {
return {
status: found ? 500 : 200,
body: {
error: found && `Domain ${fqdn.replace('www.', '')} is already used.`
error:
found && t.get('application.domain_already_in_use', { domain: fqdn.replace('www.', '') })
}
};
} catch (error) {

View File

@@ -1,13 +1,14 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { listSettings, ErrorHandler } from '$lib/database';
import { t } from '$lib/translations';
import type { RequestHandler } from '@sveltejs/kit';
import { promises as dns } from 'dns';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
if (teamId !== '0') return { status: 401, body: { message: 'You are not an admin.' } };
if (teamId !== '0') return { status: 200, body: { settings: {} } };
try {
const settings = await listSettings();
return {
@@ -27,7 +28,7 @@ export const del: RequestHandler = async (event) => {
return {
status: 401,
body: {
message: 'You do not have permission to do this. \nAsk an admin to modify your permissions.'
message: t.get('setting.permission_denied')
}
};
if (status === 401) return { status, body };
@@ -44,7 +45,7 @@ export const del: RequestHandler = async (event) => {
return {
status: 200,
body: {
message: 'Domain removed',
message: t.get('setting.domain_removed'),
redirect: ip ? `http://${ip[0]}:3000/settings` : undefined
}
};
@@ -58,15 +59,19 @@ export const post: RequestHandler = async (event) => {
return {
status: 401,
body: {
message: 'You do not have permission to do this. \nAsk an admin to modify your permissions.'
message: t.get('setting.permission_denied')
}
};
if (status === 401) return { status, body };
const { fqdn, isRegistrationEnabled, dualCerts, minPort, maxPort } = await event.request.json();
const { fqdn, isRegistrationEnabled, dualCerts, minPort, maxPort, isAutoUpdateEnabled } =
await event.request.json();
try {
const { id } = await db.listSettings();
await db.prisma.setting.update({ where: { id }, data: { isRegistrationEnabled, dualCerts } });
await db.prisma.setting.update({
where: { id },
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled }
});
if (fqdn) {
await db.prisma.setting.update({ where: { id }, data: { fqdn } });
}

View File

@@ -28,6 +28,8 @@
import { session } from '$app/stores';
export let settings;
import Cookies from 'js-cookie';
import langs from '$lib/lang.json';
import Setting from '$lib/components/Setting.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { errorNotification } from '$lib/form';
@@ -36,9 +38,11 @@
import { browser } from '$app/env';
import { getDomain } from '$lib/components/common';
import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations';
let isRegistrationEnabled = settings.isRegistrationEnabled;
let dualCerts = settings.dualCerts;
let isAutoUpdateEnabled = settings.isAutoUpdateEnabled;
let minPort = settings.minPort;
let maxPort = settings.maxPort;
@@ -71,8 +75,11 @@
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
await post(`/settings.json`, { isRegistrationEnabled, dualCerts });
return toast.push('Settings saved.');
if (name === 'isAutoUpdateEnabled') {
isAutoUpdateEnabled = !isAutoUpdateEnabled;
}
await post(`/settings.json`, { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled });
return toast.push(t.get('application.settings_saved'));
} catch ({ error }) {
return errorNotification(error);
}
@@ -99,19 +106,19 @@
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Settings</div>
<div class="mr-4 text-2xl tracking-tight">{$t('index.settings')}</div>
</div>
{#if $session.teamId === '0'}
<div class="mx-auto max-w-4xl px-6">
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
<div class="flex space-x-1 pb-6">
<div class="title font-bold">Global Settings</div>
<div class="title font-bold">{$t('index.global_settings')}</div>
<button
type="submit"
disabled={loading.save}
class:bg-yellow-500={!loading.save}
class:hover:bg-yellow-400={!loading.save}
class="mx-2 ">{loading.save ? 'Saving...' : 'Save'}</button
class="mx-2 ">{loading.save ? $t('forms.saving') : $t('forms.save')}</button
>
{#if isFqdnSet}
<button
@@ -119,17 +126,18 @@
disabled={loading.remove}
class:bg-red-600={!loading.remove}
class:hover:bg-red-500={!loading.remove}
>{loading.remove ? 'Removing...' : 'Remove domain'}</button
>{loading.remove ? $t('forms.removing') : $t('forms.remove_domain')}</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2 px-10">
<!-- <Language /> -->
<div class="grid grid-cols-2 items-start">
<div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100">URL (FQDN)</div>
<Explainer
text="If you specify <span class='text-yellow-500 font-bold'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-yellow-500 font-bold'>www</span>, Coolify will be redirected (302) from non-www and vice versa."
/>
<div class="pt-2 text-base font-bold text-stone-100">
{$t('application.url_fqdn')}
</div>
<Explainer text={$t('setting.ssl_explainer')} />
</div>
<div class="justify-start text-left">
<input
@@ -139,16 +147,16 @@
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coolify.io"
placeholder="{$t('forms.eg')}: https://coolify.io"
/>
</div>
</div>
<div class="grid grid-cols-2 items-start py-6">
<div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100">Public Port Range</div>
<Explainer
text="Ports used to expose databases/services/internal services.<br> Add them to your firewall (if applicable).<br><br>You can specify a range of ports, eg: <span class='text-yellow-500 font-bold'>9000-9100</span>"
/>
<div class="pt-2 text-base font-bold text-stone-100">
{$t('forms.public_port_range')}
</div>
<Explainer text={$t('forms.public_port_range_explainer')} />
</div>
<div class="mx-auto flex-row items-center justify-center space-y-2">
<input
@@ -170,40 +178,50 @@
</div>
<div class="grid grid-cols-2 items-center">
<Setting
dataTooltip="Must remove the domain before you can change this setting."
dataTooltip={$t('setting.must_remove_domain_before_changing')}
disabled={isFqdnSet}
bind:setting={dualCerts}
title="Generate SSL for www and non-www?"
description="It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-yellow-500'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both."
title={$t('application.ssl_www_and_non_www')}
description={$t('services.generate_www_non_www_ssl')}
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isRegistrationEnabled}
title="Registration allowed?"
description="Allow further registrations to the application. <br>It's turned off after the first registration. "
title={$t('setting.registration_allowed')}
description={$t('setting.registration_allowed_explainer')}
on:click={() => changeSettings('isRegistrationEnabled')}
/>
</div>
{#if browser && (window.location.hostname === 'staging.coolify.io' || window.location.hostname === 'localhost')}
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isAutoUpdateEnabled}
title={$t('setting.auto_update_enabled')}
description={$t('setting.auto_update_enabled_explainer')}
on:click={() => changeSettings('isAutoUpdateEnabled')}
/>
</div>
{/if}
</div>
</form>
<div class="flex space-x-1 pt-6 font-bold">
<div class="title">Coolify Proxy Settings</div>
<div class="title">{$t('setting.coolify_proxy_settings')}</div>
</div>
<Explainer
text={`Credentials for <a class="text-white font-bold" href=${
fqdn
text={$t('setting.credential_stat_explainer', {
link: fqdn
? `http://${settings.proxyUser}:${settings.proxyPassword}@` + getDomain(fqdn) + ':8404'
: browser &&
`http://${settings.proxyUser}:${settings.proxyPassword}@` +
window.location.hostname +
':8404'
} target="_blank">stats</a> page.`}
})}
/>
<div class="space-y-2 px-10 py-5">
<div class="grid grid-cols-2 items-center">
<label for="proxyUser">User</label>
<label for="proxyUser">{$t('forms.user')}</label>
<CopyPasswordField
readonly
disabled
@@ -213,7 +231,7 @@
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="proxyPassword">Password</label>
<label for="proxyPassword">{$t('forms.password')}</label>
<CopyPasswordField
readonly
disabled
@@ -225,4 +243,8 @@
</div>
</div>
</div>
{:else}
<div class="mx-auto max-w-4xl px-6">
<!-- <Language /> -->
</div>
{/if}

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