Compare commits

..

2 Commits

Author SHA1 Message Date
Mauricio Siu
80d5313dd8 Merge branch 'canary' into 1365-create-preview-deployment-using-api 2025-07-13 20:49:12 -06:00
Mauricio Siu
da0e726326 feat(preview-deployment): enhance external deployment support
- Add support for external preview deployments with optional GitHub comment handling
- Modify deployment services to conditionally update GitHub issue comments
- Update queue types and deployment worker to handle external deployment flag
- Refactor preview deployment creation to support external deployments
- Improve preview deployment router with more flexible deployment creation logic
2025-03-08 17:07:07 -06:00
133 changed files with 1358 additions and 14448 deletions

View File

@@ -2,8 +2,7 @@ name: Build Docker images
on: on:
push: push:
branches: [main, canary] branches: ["canary", "main", "feat/monitoring"]
workflow_dispatch:
jobs: jobs:
build-and-push-cloud-image: build-and-push-cloud-image:

View File

@@ -2,8 +2,7 @@ name: Dokploy Docker Build
on: on:
push: push:
branches: [main, canary] branches: [main, canary, "1061-custom-docker-service-hostname"]
workflow_dispatch:
env: env:
IMAGE_NAME: dokploy/dokploy IMAGE_NAME: dokploy/dokploy

View File

@@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Setup biomeJs - name: Setup biomeJs
uses: biomejs/setup-biome@v2 uses: biomejs/setup-biome@v2
- name: Run Biome formatter - name: Run Biome formatter
run: biome format --write run: biome format . --write
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 # v1.3.2 - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef

View File

@@ -1,3 +0,0 @@
{
"recommendations": ["biomejs.biome"]
}

View File

@@ -1,8 +0,0 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
}
}

View File

@@ -58,7 +58,7 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& pnpm install -g tsx && pnpm install -g tsx
# Install Railpack # Install Railpack
ARG RAILPACK_VERSION=0.2.2 ARG RAILPACK_VERSION=0.0.64
RUN curl -sSL https://railpack.com/install.sh | bash RUN curl -sSL https://railpack.com/install.sh | bash
# Install buildpacks # Install buildpacks

View File

@@ -1,6 +1,6 @@
<div align="center"> <div align="center">
<a href="https://dokploy.com"> <a href="https://dokploy.com">
<img src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." width="100%" /> <img src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." align="center" width="100%" />
</a> </a>
</br> </br>
</br> </br>
@@ -13,7 +13,7 @@
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases. Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
## Features ### Features
Dokploy includes multiple features to make your life easier. Dokploy includes multiple features to make your life easier.
@@ -43,7 +43,7 @@ curl -sSL https://dokploy.com/install.sh | sh
For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
## ♥️ Sponsors ## Sponsors
🙏 We're deeply grateful to all our sponsors who make Dokploy possible! Your support helps cover the costs of hosting, testing, and developing new features. 🙏 We're deeply grateful to all our sponsors who make Dokploy possible! Your support helps cover the costs of hosting, testing, and developing new features.
@@ -95,6 +95,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Community Backers 🤝 ### Community Backers 🤝
#### Organizations: #### Organizations:
[Sponsors on Open Collective](https://opencollective.com/dokploy) [Sponsors on Open Collective](https://opencollective.com/dokploy)
@@ -106,15 +107,15 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Contributors 🤝 ### Contributors 🤝
<a href="https://github.com/dokploy/dokploy/graphs/contributors"> <a href="https://github.com/dokploy/dokploy/graphs/contributors">
<img src="https://contrib.rocks/image?repo=dokploy/dokploy" alt="Contributors" /> <img src="https://contrib.rocks/image?repo=dokploy/dokploy" />
</a> </a>
## 📺 Video Tutorial ## Video Tutorial
<a href="https://youtu.be/mznYKPvhcfw"> <a href="https://youtu.be/mznYKPvhcfw">
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400"/> <img src="https://dokploy.com/banner.png" alt="Watch the video" width="400"/>
</a> </a>
## 🤝 Contributing ## Contributing
Check out the [Contributing Guide](CONTRIBUTING.md) for more information. Check out the [Contributing Guide](CONTRIBUTING.md) for more information.

View File

@@ -29,9 +29,5 @@
"tsx": "^4.16.2", "tsx": "^4.16.2",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"packageManager": "pnpm@9.12.0", "packageManager": "pnpm@9.5.0"
"engines": {
"node": "^20.16.0",
"pnpm": ">=9.12.0"
}
} }

View File

@@ -1,5 +1,5 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllProperties } from "@dokploy/server"; import { addSuffixToAllProperties } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,6 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToConfigsRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,8 +1,6 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToConfigsInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToConfigsInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,6 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToAllConfigs } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllConfigs, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -108,136 +108,4 @@ describe("createDomainLabels", () => {
"traefik.http.services.test-app-1-web.loadbalancer.server.port=3000", "traefik.http.services.test-app-1-web.loadbalancer.server.port=3000",
); );
}); });
it("should add stripPath middleware when stripPath is enabled", async () => {
const stripPathDomain = {
...baseDomain,
path: "/api",
stripPath: true,
};
const labels = await createDomainLabels(appName, stripPathDomain, "web");
expect(labels).toContain(
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
);
expect(labels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=stripprefix-test-app-1",
);
});
it("should add internalPath middleware when internalPath is set", async () => {
const internalPathDomain = {
...baseDomain,
internalPath: "/hello",
};
const webLabels = await createDomainLabels(
appName,
internalPathDomain,
"web",
);
const websecureLabels = await createDomainLabels(
appName,
internalPathDomain,
"websecure",
);
// Middleware definition should only appear in web entrypoint
expect(webLabels).toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
// Both routers should reference the middleware
expect(webLabels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=addprefix-test-app-1",
);
expect(websecureLabels).toContain(
"traefik.http.routers.test-app-1-websecure.middlewares=addprefix-test-app-1",
);
});
it("should combine HTTPS redirect with internalPath middleware in correct order", async () => {
const combinedDomain = {
...baseDomain,
https: true,
internalPath: "/hello",
};
const webLabels = await createDomainLabels(appName, combinedDomain, "web");
const websecureLabels = await createDomainLabels(
appName,
combinedDomain,
"websecure",
);
// Web entrypoint should have both middlewares with redirect first
expect(webLabels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file,addprefix-test-app-1",
);
// Websecure should only have the addprefix middleware
expect(websecureLabels).toContain(
"traefik.http.routers.test-app-1-websecure.middlewares=addprefix-test-app-1",
);
// Middleware definition should only appear once (in web)
expect(webLabels).toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
});
it("should combine all middlewares in correct order", async () => {
const fullDomain = {
...baseDomain,
https: true,
path: "/api",
stripPath: true,
internalPath: "/hello",
};
const webLabels = await createDomainLabels(appName, fullDomain, "web");
// Should have all middleware definitions (only in web)
expect(webLabels).toContain(
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
);
expect(webLabels).toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
// Should have middlewares in correct order: redirect, stripprefix, addprefix
expect(webLabels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file,stripprefix-test-app-1,addprefix-test-app-1",
);
});
it("should not add middleware definitions for websecure entrypoint", async () => {
const internalPathDomain = {
...baseDomain,
path: "/api",
stripPath: true,
internalPath: "/hello",
};
const websecureLabels = await createDomainLabels(
appName,
internalPathDomain,
"websecure",
);
// Should not contain any middleware definitions
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
);
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
// But should reference the middlewares
expect(websecureLabels).toContain(
"traefik.http.routers.test-app-1-websecure.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
);
});
}); });

View File

@@ -1,5 +1,6 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToNetworksRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,8 +1,6 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNetworks } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToServiceNetworks,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,10 +1,10 @@
import type { ComposeSpecification } from "@dokploy/server"; import { generateRandomHash } from "@dokploy/server";
import { import {
addSuffixToAllNetworks, addSuffixToAllNetworks,
addSuffixToNetworksRoot,
addSuffixToServiceNetworks, addSuffixToServiceNetworks,
generateRandomHash,
} from "@dokploy/server"; } from "@dokploy/server";
import { addSuffixToNetworksRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,6 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToSecretsRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,8 +1,6 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToSecretsInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToSecretsInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllSecrets } from "@dokploy/server"; import { addSuffixToAllSecrets } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,6 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,6 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,6 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,6 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,6 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,8 +1,8 @@
import type { ComposeSpecification } from "@dokploy/server";
import { import {
addSuffixToAllServiceNames, addSuffixToAllServiceNames,
addSuffixToServiceNames, addSuffixToServiceNames,
} from "@dokploy/server"; } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,6 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,9 +1,6 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToAllVolumes,
addSuffixToVolumesRoot,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,6 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToVolumesRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,8 +1,6 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToVolumesInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToVolumesInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllVolumes } from "@dokploy/server"; import { addSuffixToAllVolumes } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]"; import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
import { describe, expect, it } from "vitest";
describe("GitHub Webhook Skip CI", () => { describe("GitHub Webhook Skip CI", () => {
const mockGithubHeaders = { const mockGithubHeaders = {

View File

@@ -1,12 +1,12 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { paths } from "@dokploy/server/constants";
const { APPLICATIONS_PATH } = paths();
import type { ApplicationNested } from "@dokploy/server"; import type { ApplicationNested } from "@dokploy/server";
import { unzipDrop } from "@dokploy/server"; import { unzipDrop } from "@dokploy/server";
import { paths } from "@dokploy/server/constants";
import AdmZip from "adm-zip"; import AdmZip from "adm-zip";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
const { APPLICATIONS_PATH } = paths();
vi.mock("@dokploy/server/constants", async (importOriginal) => { vi.mock("@dokploy/server/constants", async (importOriginal) => {
const actual = await importOriginal(); const actual = await importOriginal();
return { return {
@@ -25,12 +25,10 @@ if (typeof window === "undefined") {
} }
const baseApp: ApplicationNested = { const baseApp: ApplicationNested = {
railpackVersion: "0.2.2",
applicationId: "", applicationId: "",
herokuVersion: "", herokuVersion: "",
giteaBranch: "", giteaBranch: "",
giteaBuildPath: "", giteaBuildPath: "",
previewRequireCollaboratorPermissions: false,
giteaId: "", giteaId: "",
giteaOwner: "", giteaOwner: "",
giteaRepository: "", giteaRepository: "",
@@ -143,7 +141,7 @@ describe("unzipDrop using real zip files", () => {
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/single-file.zip"); const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
console.log(`Output Path: ${outputPath}`); console.log(`Output Path: ${outputPath}`);
const zipBuffer = zip.toBuffer() as Buffer<ArrayBuffer>; const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip"); const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp); await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true }); const files = await fs.readdir(outputPath, { withFileTypes: true });

View File

@@ -1,6 +1,5 @@
import { parseRawConfig, processLogs } from "@dokploy/server"; import { parseRawConfig, processLogs } from "@dokploy/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`; const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`;
describe("processLogs", () => { describe("processLogs", () => {

View File

@@ -1,9 +1,10 @@
import type { ApplicationNested, Domain, Redirect } from "@dokploy/server"; import type { Domain } from "@dokploy/server";
import type { Redirect } from "@dokploy/server";
import type { ApplicationNested } from "@dokploy/server";
import { createRouterConfig } from "@dokploy/server"; import { createRouterConfig } from "@dokploy/server";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
const baseApp: ApplicationNested = { const baseApp: ApplicationNested = {
railpackVersion: "0.2.2",
rollbackActive: false, rollbackActive: false,
applicationId: "", applicationId: "",
herokuVersion: "", herokuVersion: "",
@@ -17,7 +18,6 @@ const baseApp: ApplicationNested = {
appName: "", appName: "",
autoDeploy: true, autoDeploy: true,
enableSubmodules: false, enableSubmodules: false,
previewRequireCollaboratorPermissions: false,
serverId: "", serverId: "",
branch: null, branch: null,
dockerBuildStage: "", dockerBuildStage: "",

View File

@@ -151,7 +151,7 @@ export const HandleSecurity = ({
<FormItem> <FormItem>
<FormLabel>Password</FormLabel> <FormLabel>Password</FormLabel>
<FormControl> <FormControl>
<Input placeholder="test" type="password" {...field} /> <Input placeholder="test" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -7,9 +7,6 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { LockKeyhole, Trash2 } from "lucide-react"; import { LockKeyhole, Trash2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -61,18 +58,19 @@ export const ShowSecurity = ({ applicationId }: Props) => {
<div className="flex flex-col gap-6 "> <div className="flex flex-col gap-6 ">
{data?.security.map((security) => ( {data?.security.map((security) => (
<div key={security.securityId}> <div key={security.securityId}>
<div className="flex w-full flex-col md:flex-row justify-between md:items-center gap-4 md:gap-10 border rounded-lg p-4"> <div className="flex w-full flex-col sm:flex-row justify-between sm:items-center gap-4 sm:gap-10 border rounded-lg p-4">
<div className="grid grid-cols-1 md:grid-cols-2 flex-col gap-4 md:gap-8"> <div className="grid grid-cols-1 sm:grid-cols-2 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-1">
<Label>Username</Label> <span className="font-medium">Username</span>
<Input disabled value={security.username} /> <span className="text-sm text-muted-foreground">
{security.username}
</span>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-1">
<Label>Password</Label> <span className="font-medium">Password</span>
<ToggleVisibilityInput <span className="text-sm text-muted-foreground">
value={security.password} {security.password}
disabled </span>
/>
</div> </div>
</div> </div>
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">

View File

@@ -1,5 +1,3 @@
import { Package, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -11,10 +9,11 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Package, Trash2 } from "lucide-react";
import { toast } from "sonner";
import type { ServiceType } from "../show-resources"; import type { ServiceType } from "../show-resources";
import { AddVolumes } from "./add-volumes"; import { AddVolumes } from "./add-volumes";
import { UpdateVolume } from "./update-volume"; import { UpdateVolume } from "./update-volume";
interface Props { interface Props {
id: string; id: string;
type: ServiceType | "compose"; type: ServiceType | "compose";
@@ -81,7 +80,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4" className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
> >
{/* <Package className="size-8 self-center text-muted-foreground" /> */} {/* <Package className="size-8 self-center text-muted-foreground" /> */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 flex-col gap-4 sm:gap-8"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="font-medium">Mount Type</span> <span className="font-medium">Mount Type</span>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
@@ -113,21 +112,21 @@ export const ShowVolumes = ({ id, type }: Props) => {
</span> </span>
</div> </div>
)} )}
{mount.type === "file" && ( {mount.type === "file" ? (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="font-medium">File Path</span> <span className="font-medium">File Path</span>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{mount.filePath} {mount.filePath}
</span> </span>
</div> </div>
) : (
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
)} )}
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
</div> </div>
<div className="flex flex-row gap-1"> <div className="flex flex-row gap-1">
<UpdateVolume <UpdateVolume

View File

@@ -1,9 +1,3 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Cog } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -21,6 +15,12 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Cog } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
export enum BuildType { export enum BuildType {
dockerfile = "dockerfile", dockerfile = "dockerfile",
@@ -65,7 +65,6 @@ const mySchema = z.discriminatedUnion("buildType", [
}), }),
z.object({ z.object({
buildType: z.literal(BuildType.railpack), buildType: z.literal(BuildType.railpack),
railpackVersion: z.string().nullable().default("0.2.2"),
}), }),
z.object({ z.object({
buildType: z.literal(BuildType.static), buildType: z.literal(BuildType.static),
@@ -87,7 +86,6 @@ interface ApplicationData {
herokuVersion?: string | null; herokuVersion?: string | null;
publishDirectory?: string | null; publishDirectory?: string | null;
isStaticSpa?: boolean | null; isStaticSpa?: boolean | null;
railpackVersion?: string | null | undefined;
} }
function isValidBuildType(value: string): value is BuildType { function isValidBuildType(value: string): value is BuildType {
@@ -125,7 +123,6 @@ const resetData = (data: ApplicationData): AddTemplate => {
case BuildType.railpack: case BuildType.railpack:
return { return {
buildType: BuildType.railpack, buildType: BuildType.railpack,
railpackVersion: data.railpackVersion || null,
}; };
default: { default: {
const buildType = data.buildType as BuildType; const buildType = data.buildType as BuildType;
@@ -184,10 +181,6 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
: null, : null,
isStaticSpa: isStaticSpa:
data.buildType === BuildType.static ? data.isStaticSpa : null, data.buildType === BuildType.static ? data.isStaticSpa : null,
railpackVersion:
data.buildType === BuildType.railpack
? data.railpackVersion || "0.2.2"
: null,
}) })
.then(async () => { .then(async () => {
toast.success("Build type saved"); toast.success("Build type saved");
@@ -402,25 +395,6 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
)} )}
/> />
)} )}
{buildType === BuildType.railpack && (
<FormField
control={form.control}
name="railpackVersion"
render={({ field }) => (
<FormItem>
<FormLabel>Railpack Version</FormLabel>
<FormControl>
<Input
placeholder="Railpack Version"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit"> <Button isLoading={isLoading} type="submit">
Save Save

View File

@@ -1,5 +1,4 @@
import { DateTooltip } from "@/components/shared/date-tooltip"; import { DateTooltip } from "@/components/shared/date-tooltip";
import { DialogAction } from "@/components/shared/dialog-action";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -11,13 +10,14 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { type RouterOutputs, api } from "@/utils/api"; import { type RouterOutputs, api } from "@/utils/api";
import { Clock, Loader2, RefreshCcw, RocketIcon, Settings } from "lucide-react"; import { Clock, Loader2, RocketIcon, Settings, RefreshCcw } from "lucide-react";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { toast } from "sonner";
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
import { CancelQueues } from "./cancel-queues"; import { CancelQueues } from "./cancel-queues";
import { RefreshToken } from "./refresh-token"; import { RefreshToken } from "./refresh-token";
import { ShowDeployment } from "./show-deployment"; import { ShowDeployment } from "./show-deployment";
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
import { DialogAction } from "@/components/shared/dialog-action";
import { toast } from "sonner";
interface Props { interface Props {
id: string; id: string;

View File

@@ -1,10 +1,3 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import z from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -41,6 +34,14 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { zodResolver } from "@hookform/resolvers/zod";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import Link from "next/link";
import z from "zod";
export type CacheType = "fetch" | "cache"; export type CacheType = "fetch" | "cache";
@@ -122,7 +123,6 @@ interface Props {
export const AddDomain = ({ id, type, domainId = "", children }: Props) => { export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState<CacheType>("cache"); const [cacheType, setCacheType] = useState<CacheType>("cache");
const [isManualInput, setIsManualInput] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { data, refetch } = api.domain.one.useQuery( const { data, refetch } = api.domain.one.useQuery(
@@ -325,126 +325,46 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<FormItem className="w-full"> <FormItem className="w-full">
<FormLabel>Service Name</FormLabel> <FormLabel>Service Name</FormLabel>
<div className="flex gap-2"> <div className="flex gap-2">
{isManualInput ? ( <Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl> <FormControl>
<Input <SelectTrigger>
placeholder="Enter service name manually" <SelectValue placeholder="Select a service name" />
{...field} </SelectTrigger>
className="w-full"
/>
</FormControl> </FormControl>
) : (
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a service name" />
</SelectTrigger>
</FormControl>
<SelectContent> <SelectContent>
{services?.map((service, index) => ( {services?.map((service, index) => (
<SelectItem <SelectItem
value={service} value={service}
key={`${service}-${index}`} key={`${service}-${index}`}
> >
{service} {service}
</SelectItem>
))}
<SelectItem value="none" disabled>
Empty
</SelectItem> </SelectItem>
</SelectContent> ))}
</Select> <SelectItem value="none" disabled>
)} Empty
{!isManualInput && ( </SelectItem>
<> </SelectContent>
<TooltipProvider delayDuration={0}> </Select>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "fetch") {
refetchServices();
} else {
setCacheType("fetch");
}
}}
>
<RefreshCw className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Fetch: Will clone the repository and
load the services
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "cache") {
refetchServices();
} else {
setCacheType("cache");
}
}}
>
<DatabaseZap className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Cache: If you previously deployed this
compose, it will read the services
from the last deployment/fetch from
the repository
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
variant="secondary" variant="secondary"
type="button" type="button"
isLoading={isLoadingServices}
onClick={() => { onClick={() => {
setIsManualInput(!isManualInput); if (cacheType === "fetch") {
if (!isManualInput) { refetchServices();
field.onChange(""); } else {
setCacheType("fetch");
} }
}} }}
> >
{isManualInput ? ( <RefreshCw className="size-4 text-muted-foreground" />
<RefreshCw className="size-4 text-muted-foreground" />
) : (
<span className="text-xs text-muted-foreground">
Manual
</span>
)}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent <TooltipContent
@@ -453,9 +373,40 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
className="max-w-[10rem]" className="max-w-[10rem]"
> >
<p> <p>
{isManualInput Fetch: Will clone the repository and load
? "Switch to service selection" the services
: "Enter service name manually"} </p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingServices}
onClick={() => {
if (cacheType === "cache") {
refetchServices();
} else {
setCacheType("cache");
}
}}
>
<DatabaseZap className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>
Cache: If you previously deployed this
compose, it will read the services from
the last deployment/fetch from the
repository
</p> </p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>

View File

@@ -46,7 +46,6 @@ const schema = z
previewPath: z.string(), previewPath: z.string(),
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]), previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
previewCustomCertResolver: z.string().optional(), previewCustomCertResolver: z.string().optional(),
previewRequireCollaboratorPermissions: z.boolean(),
}) })
.superRefine((input, ctx) => { .superRefine((input, ctx) => {
if ( if (
@@ -84,7 +83,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewHttps: false, previewHttps: false,
previewPath: "/", previewPath: "/",
previewCertificateType: "none", previewCertificateType: "none",
previewRequireCollaboratorPermissions: true,
}, },
resolver: zodResolver(schema), resolver: zodResolver(schema),
}); });
@@ -107,8 +105,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewPath: data.previewPath || "/", previewPath: data.previewPath || "/",
previewCertificateType: data.previewCertificateType || "none", previewCertificateType: data.previewCertificateType || "none",
previewCustomCertResolver: data.previewCustomCertResolver || "", previewCustomCertResolver: data.previewCustomCertResolver || "",
previewRequireCollaboratorPermissions:
data.previewRequireCollaboratorPermissions || true,
}); });
} }
}, [data]); }, [data]);
@@ -125,8 +121,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewPath: formData.previewPath, previewPath: formData.previewPath,
previewCertificateType: formData.previewCertificateType, previewCertificateType: formData.previewCertificateType,
previewCustomCertResolver: formData.previewCustomCertResolver, previewCustomCertResolver: formData.previewCustomCertResolver,
previewRequireCollaboratorPermissions:
formData.previewRequireCollaboratorPermissions,
}) })
.then(() => { .then(() => {
toast.success("Preview Deployments settings updated"); toast.success("Preview Deployments settings updated");
@@ -318,37 +312,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
</div> </div>
</div> </div>
<div className="grid gap-4 lg:grid-cols-2">
<FormField
control={form.control}
name="previewRequireCollaboratorPermissions"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm col-span-2">
<div className="space-y-0.5">
<FormLabel>
Require Collaborator Permissions
</FormLabel>
<FormDescription>
Require collaborator permissions to preview
deployments, valid roles are:
<ul>
<li>Admin</li>
<li>Maintain</li>
<li>Write</li>
</ul>
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<FormField <FormField
control={form.control} control={form.control}
name="env" name="env"

View File

@@ -1,4 +1,3 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { DrawerLogs } from "@/components/shared/drawer-logs"; import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -43,8 +42,9 @@ import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { formatBytes } from "../../database/backups/restore-backup";
import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { formatBytes } from "../../database/backups/restore-backup";
import { AlertBlock } from "@/components/shared/alert-block";
interface Props { interface Props {
id: string; id: string;

View File

@@ -23,8 +23,8 @@ import {
Trash2, Trash2,
} from "lucide-react"; } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { HandleVolumeBackups } from "./handle-volume-backups"; import { HandleVolumeBackups } from "./handle-volume-backups";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { RestoreVolumeBackups } from "./restore-volume-backups"; import { RestoreVolumeBackups } from "./restore-volume-backups";
interface Props { interface Props {

View File

@@ -1,4 +1,3 @@
import { UnauthorizedGitProvider } from "@/components/dashboard/application/general/generic/unauthorized-git-provider";
import { import {
BitbucketIcon, BitbucketIcon,
GitIcon, GitIcon,
@@ -12,7 +11,6 @@ import { api } from "@/utils/api";
import { CodeIcon, GitBranch, Loader2 } from "lucide-react"; import { CodeIcon, GitBranch, Loader2 } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner";
import { ComposeFileEditor } from "../compose-file-editor"; import { ComposeFileEditor } from "../compose-file-editor";
import { ShowConvertedCompose } from "../show-converted-compose"; import { ShowConvertedCompose } from "../show-converted-compose";
import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose"; import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose";
@@ -20,6 +18,8 @@ import { SaveGitProviderCompose } from "./save-git-provider-compose";
import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose"; import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose";
import { SaveGithubProviderCompose } from "./save-github-provider-compose"; import { SaveGithubProviderCompose } from "./save-github-provider-compose";
import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose"; import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose";
import { UnauthorizedGitProvider } from "@/components/dashboard/application/general/generic/unauthorized-git-provider";
import { toast } from "sonner";
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea"; type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea";
interface Props { interface Props {

View File

@@ -1,9 +1,3 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Folder, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -43,6 +37,12 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Folder, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddTemplateSchema = z.object({ const AddTemplateSchema = z.object({
name: z.string().min(1, { name: z.string().min(1, {
@@ -75,8 +75,6 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
const slug = slugify(projectName); const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery(); const { data: servers } = api.server.withSSHKey.useQuery();
const hasServers = servers && servers.length > 0;
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
api.application.create.useMutation(); api.application.create.useMutation();
@@ -157,84 +155,68 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
{hasServers && ( <FormField
<FormField control={form.control}
control={form.control} name="serverId"
name="serverId" render={({ field }) => (
render={({ field }) => ( <FormItem>
<FormItem> <TooltipProvider delayDuration={0}>
<TooltipProvider delayDuration={0}> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center"> Select a Server {!isCloud ? "(Optional)" : ""}
Select a Server {!isCloud ? "(Optional)" : ""} <HelpCircle className="size-4 text-muted-foreground" />
<HelpCircle className="size-4 text-muted-foreground" /> </FormLabel>
</FormLabel> </TooltipTrigger>
</TooltipTrigger> <TooltipContent
<TooltipContent className="z-[999] w-[300px]"
className="z-[999] w-[300px]" align="start"
align="start" side="top"
side="top" >
> <span>
<span> If no server is selected, the application will be
If no server is selected, the application will be deployed on the server where the user is logged in.
deployed on the server where the user is logged in. </span>
</span> </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip> </TooltipProvider>
</TooltipProvider>
<Select <Select
onValueChange={field.onChange} onValueChange={field.onChange}
defaultValue={field.value} defaultValue={field.value}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a Server" /> <SelectValue placeholder="Select a Server" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
{servers?.map((server) => ( {servers?.map((server) => (
<SelectItem <SelectItem
key={server.serverId} key={server.serverId}
value={server.serverId} value={server.serverId}
> >
<span className="flex items-center gap-2 justify-between w-full"> <span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span> <span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center"> <span className="text-muted-foreground text-xs self-center">
{server.ipAddress} {server.ipAddress}
</span>
</span> </span>
</SelectItem> </span>
))} </SelectItem>
<SelectLabel>Servers ({servers?.length})</SelectLabel> ))}
</SelectGroup> <SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectContent> </SelectGroup>
</Select> </SelectContent>
<FormMessage /> </Select>
</FormItem> <FormMessage />
)} </FormItem>
/> )}
)} />
<FormField <FormField
control={form.control} control={form.control}
name="appName" name="appName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="flex items-center gap-2"> <FormLabel>App Name</FormLabel>
App Name
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent side="right">
<p>
This will be the name of the Docker Swarm service
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<FormControl> <FormControl>
<Input placeholder="my-app" {...field} /> <Input placeholder="my-app" {...field} />
</FormControl> </FormControl>

View File

@@ -1,9 +1,3 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, HelpCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -43,6 +37,12 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, HelpCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddComposeSchema = z.object({ const AddComposeSchema = z.object({
composeType: z.enum(["docker-compose", "stack"]).optional(), composeType: z.enum(["docker-compose", "stack"]).optional(),
@@ -78,8 +78,6 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
api.compose.create.useMutation(); api.compose.create.useMutation();
const hasServers = servers && servers.length > 0;
const form = useForm<AddCompose>({ const form = useForm<AddCompose>({
defaultValues: { defaultValues: {
name: "", name: "",
@@ -165,64 +163,62 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
)} )}
/> />
</div> </div>
{hasServers && ( <FormField
<FormField control={form.control}
control={form.control} name="serverId"
name="serverId" render={({ field }) => (
render={({ field }) => ( <FormItem>
<FormItem> <TooltipProvider delayDuration={0}>
<TooltipProvider delayDuration={0}> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center"> Select a Server {!isCloud ? "(Optional)" : ""}
Select a Server {!isCloud ? "(Optional)" : ""} <HelpCircle className="size-4 text-muted-foreground" />
<HelpCircle className="size-4 text-muted-foreground" /> </FormLabel>
</FormLabel> </TooltipTrigger>
</TooltipTrigger> <TooltipContent
<TooltipContent className="z-[999] w-[300px]"
className="z-[999] w-[300px]" align="start"
align="start" side="top"
side="top" >
> <span>
<span> If no server is selected, the application will be
If no server is selected, the application will be deployed on the server where the user is logged in.
deployed on the server where the user is logged in. </span>
</span> </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip> </TooltipProvider>
</TooltipProvider>
<Select <Select
onValueChange={field.onChange} onValueChange={field.onChange}
defaultValue={field.value} defaultValue={field.value}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a Server" /> <SelectValue placeholder="Select a Server" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
{servers?.map((server) => ( {servers?.map((server) => (
<SelectItem <SelectItem
key={server.serverId} key={server.serverId}
value={server.serverId} value={server.serverId}
> >
<span className="flex items-center gap-2 justify-between w-full"> <span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span> <span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center"> <span className="text-muted-foreground text-xs self-center">
{server.ipAddress} {server.ipAddress}
</span>
</span> </span>
</SelectItem> </span>
))} </SelectItem>
<SelectLabel>Servers ({servers?.length})</SelectLabel> ))}
</SelectGroup> <SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectContent> </SelectGroup>
</Select> </SelectContent>
<FormMessage /> </Select>
</FormItem> <FormMessage />
)} </FormItem>
/> )}
)} />
<FormField <FormField
control={form.control} control={form.control}
name="appName" name="appName"

View File

@@ -1,9 +1,3 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Database, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { import {
MariadbIcon, MariadbIcon,
MongodbIcon, MongodbIcon,
@@ -43,14 +37,14 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Database } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
type DbType = typeof mySchema._type.type; type DbType = typeof mySchema._type.type;
@@ -169,8 +163,6 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
const mariadbMutation = api.mariadb.create.useMutation(); const mariadbMutation = api.mariadb.create.useMutation();
const mysqlMutation = api.mysql.create.useMutation(); const mysqlMutation = api.mysql.create.useMutation();
const hasServers = servers && servers.length > 0;
const form = useForm<AddDatabase>({ const form = useForm<AddDatabase>({
defaultValues: { defaultValues: {
type: "postgres", type: "postgres",
@@ -382,62 +374,45 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
{hasServers && ( <FormField
<FormField control={form.control}
control={form.control} name="serverId"
name="serverId" render={({ field }) => (
render={({ field }) => ( <FormItem>
<FormItem> <FormLabel>Select a Server</FormLabel>
<FormLabel>Select a Server</FormLabel> <Select
<Select onValueChange={field.onChange}
onValueChange={field.onChange} defaultValue={field.value || ""}
defaultValue={field.value || ""} >
> <SelectTrigger>
<SelectTrigger> <SelectValue placeholder="Select a Server" />
<SelectValue placeholder="Select a Server" /> </SelectTrigger>
</SelectTrigger> <SelectContent>
<SelectContent> <SelectGroup>
<SelectGroup> {servers?.map((server) => (
{servers?.map((server) => ( <SelectItem
<SelectItem key={server.serverId}
key={server.serverId} value={server.serverId}
value={server.serverId} >
> {server.name}
{server.name} </SelectItem>
</SelectItem> ))}
))} <SelectLabel>
<SelectLabel> Servers ({servers?.length})
Servers ({servers?.length}) </SelectLabel>
</SelectLabel> </SelectGroup>
</SelectGroup> </SelectContent>
</SelectContent> </Select>
</Select> <FormMessage />
<FormMessage /> </FormItem>
</FormItem> )}
)} />
/>
)}
<FormField <FormField
control={form.control} control={form.control}
name="appName" name="appName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="flex items-center gap-2"> <FormLabel>App Name</FormLabel>
App Name
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent side="right">
<p>
This will be the name of the Docker Swarm
service
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<FormControl> <FormControl>
<Input placeholder="my-app" {...field} /> <Input placeholder="my-app" {...field} />
</FormControl> </FormControl>

View File

@@ -1,18 +1,3 @@
import {
BookText,
CheckIcon,
ChevronsUpDown,
Globe,
HelpCircle,
LayoutGrid,
List,
Loader2,
PuzzleIcon,
SearchIcon,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { GithubIcon } from "@/components/icons/data-tools-icons"; import { GithubIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { import {
@@ -69,6 +54,21 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import {
BookText,
CheckIcon,
ChevronsUpDown,
Globe,
HelpCircle,
LayoutGrid,
List,
Loader2,
PuzzleIcon,
SearchIcon,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { toast } from "sonner";
const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url"; const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url";
@@ -137,8 +137,6 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
return matchesTags && matchesQuery; return matchesTags && matchesQuery;
}) || []; }) || [];
const hasServers = servers && servers.length > 0;
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger className="w-full"> <DialogTrigger className="w-full">
@@ -427,62 +425,60 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
project. project.
</AlertDialogDescription> </AlertDialogDescription>
{hasServers && ( <div>
<div> <TooltipProvider delayDuration={0}>
<TooltipProvider delayDuration={0}> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5">
<Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5"> Select a Server{" "}
Select a Server{" "} {!isCloud ? "(Optional)" : ""}
{!isCloud ? "(Optional)" : ""} <HelpCircle className="size-4 text-muted-foreground" />
<HelpCircle className="size-4 text-muted-foreground" /> </Label>
</Label> </TooltipTrigger>
</TooltipTrigger> <TooltipContent
<TooltipContent className="z-[999] w-[300px]"
className="z-[999] w-[300px]" align="start"
align="start" side="top"
side="top" >
> <span>
<span> If no server is selected, the application
If no server is selected, the will be deployed on the server where the
application will be deployed on the user is logged in.
server where the user is logged in. </span>
</span> </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip> </TooltipProvider>
</TooltipProvider>
<Select <Select
onValueChange={(e) => { onValueChange={(e) => {
setServerId(e); setServerId(e);
}} }}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a Server" /> <SelectValue placeholder="Select a Server" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
{servers?.map((server) => ( {servers?.map((server) => (
<SelectItem <SelectItem
key={server.serverId} key={server.serverId}
value={server.serverId} value={server.serverId}
> >
<span className="flex items-center gap-2 justify-between w-full"> <span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span> <span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center"> <span className="text-muted-foreground text-xs self-center">
{server.ipAddress} {server.ipAddress}
</span>
</span> </span>
</SelectItem> </span>
))} </SelectItem>
<SelectLabel> ))}
Servers ({servers?.length}) <SelectLabel>
</SelectLabel> Servers ({servers?.length})
</SelectGroup> </SelectLabel>
</SelectContent> </SelectGroup>
</Select> </SelectContent>
</div> </Select>
)} </div>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>

View File

@@ -25,7 +25,6 @@ const examples = [
export const StepOne = ({ setTemplateInfo, templateInfo }: any) => { export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
// Get servers from the API // Get servers from the API
const { data: servers } = api.server.withSSHKey.useQuery(); const { data: servers } = api.server.withSSHKey.useQuery();
const hasServers = servers && servers.length > 0;
const handleExampleClick = (example: string) => { const handleExampleClick = (example: string) => {
setTemplateInfo({ ...templateInfo, userInput: example }); setTemplateInfo({ ...templateInfo, userInput: example });
@@ -48,39 +47,37 @@ export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
/> />
</div> </div>
{hasServers && ( <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor="server-deploy">
<Label htmlFor="server-deploy"> Select the server where you want to deploy (optional)
Select the server where you want to deploy (optional) </Label>
</Label> <Select
<Select value={templateInfo.server?.serverId}
value={templateInfo.server?.serverId} onValueChange={(value) => {
onValueChange={(value) => { const server = servers?.find((s) => s.serverId === value);
const server = servers?.find((s) => s.serverId === value); if (server) {
if (server) { setTemplateInfo({
setTemplateInfo({ ...templateInfo,
...templateInfo, server: server,
server: server, });
}); }
} }}
}} >
> <SelectTrigger className="w-full">
<SelectTrigger className="w-full"> <SelectValue placeholder="Select a server" />
<SelectValue placeholder="Select a server" /> </SelectTrigger>
</SelectTrigger> <SelectContent>
<SelectContent> <SelectGroup>
<SelectGroup> {servers?.map((server) => (
{servers?.map((server) => ( <SelectItem key={server.serverId} value={server.serverId}>
<SelectItem key={server.serverId} value={server.serverId}> {server.name}
{server.name} </SelectItem>
</SelectItem> ))}
))} <SelectLabel>Servers ({servers?.length})</SelectLabel>
<SelectLabel>Servers ({servers?.length})</SelectLabel> </SelectGroup>
</SelectGroup> </SelectContent>
</SelectContent> </Select>
</Select> </div>
</div>
)}
<div className="space-y-2"> <div className="space-y-2">
<Label>Examples:</Label> <Label>Examples:</Label>

View File

@@ -199,7 +199,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Generating template suggestions based on your input... Generating template suggestions based on your input...
</p> </p>
<pre className="whitespace-normal">{templateInfo.userInput}</pre> <pre>{templateInfo.userInput}</pre>
</div> </div>
); );
} }

View File

@@ -70,7 +70,6 @@ export const HandleDestinations = ({ destinationId }: Props) => {
}, },
{ {
enabled: !!destinationId, enabled: !!destinationId,
refetchOnWindowFocus: false,
}, },
); );
const { const {

View File

@@ -24,14 +24,12 @@ export const AddGithubProvider = () => {
const [isOrganization, setIsOrganization] = useState(false); const [isOrganization, setIsOrganization] = useState(false);
const [organizationName, setOrganization] = useState(""); const [organizationName, setOrganization] = useState("");
const randomString = () => Math.random().toString(36).slice(2, 8);
useEffect(() => { useEffect(() => {
const url = document.location.origin; const url = document.location.origin;
const manifest = JSON.stringify( const manifest = JSON.stringify(
{ {
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}&userId=${session?.user?.id}`, redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}&userId=${session?.user?.id}`,
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}-${randomString()}`, name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
url: origin, url: origin,
hook_attributes: { hook_attributes: {
url: `${url}/api/deploy/github`, url: `${url}/api/deploy/github`,

View File

@@ -33,7 +33,6 @@ import { AddGithubProvider } from "./github/add-github-provider";
import { EditGithubProvider } from "./github/edit-github-provider"; import { EditGithubProvider } from "./github/edit-github-provider";
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider"; import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
import { EditGitlabProvider } from "./gitlab/edit-gitlab-provider"; import { EditGitlabProvider } from "./gitlab/edit-gitlab-provider";
import { Badge } from "@/components/ui/badge";
export const ShowGitProviders = () => { export const ShowGitProviders = () => {
const { data, isLoading, refetch } = api.gitProvider.getAll.useQuery(); const { data, isLoading, refetch } = api.gitProvider.getAll.useQuery();
@@ -159,13 +158,7 @@ export const ShowGitProviders = () => {
<div className="flex flex-row gap-1"> <div className="flex flex-row gap-1">
{!haveGithubRequirements && isGithub && ( {!haveGithubRequirements && isGithub && (
<div className="flex flex-row gap-1 items-center"> <div className="flex flex-col gap-1">
<Badge
variant="outline"
className="text-xs"
>
Action Required
</Badge>
<Link <Link
href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`} href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`}
className={buttonVariants({ className={buttonVariants({
@@ -192,13 +185,7 @@ export const ShowGitProviders = () => {
</div> </div>
)} )}
{!haveGitlabRequirements && isGitlab && ( {!haveGitlabRequirements && isGitlab && (
<div className="flex flex-row gap-1 items-center"> <div className="flex flex-col gap-1">
<Badge
variant="outline"
className="text-xs"
>
Action Required
</Badge>
<Link <Link
href={getGitlabUrl( href={getGitlabUrl(
gitProvider.gitlab?.applicationId || "", gitProvider.gitlab?.applicationId || "",

View File

@@ -1,4 +1,3 @@
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -20,7 +19,7 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { generateSHA256Hash, getFallbackAvatarInitials } from "@/lib/utils"; import { generateSHA256Hash } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2, User } from "lucide-react"; import { Loader2, User } from "lucide-react";
@@ -37,7 +36,6 @@ const profileSchema = z.object({
password: z.string().nullable(), password: z.string().nullable(),
currentPassword: z.string().nullable(), currentPassword: z.string().nullable(),
image: z.string().optional(), image: z.string().optional(),
name: z.string().optional(),
allowImpersonation: z.boolean().optional().default(false), allowImpersonation: z.boolean().optional().default(false),
}); });
@@ -86,7 +84,6 @@ export const ProfileForm = () => {
image: data?.user?.image || "", image: data?.user?.image || "",
currentPassword: "", currentPassword: "",
allowImpersonation: data?.user?.allowImpersonation || false, allowImpersonation: data?.user?.allowImpersonation || false,
name: data?.user?.name || "",
}, },
resolver: zodResolver(profileSchema), resolver: zodResolver(profileSchema),
}); });
@@ -100,7 +97,6 @@ export const ProfileForm = () => {
image: data?.user?.image || "", image: data?.user?.image || "",
currentPassword: form.getValues("currentPassword") || "", currentPassword: form.getValues("currentPassword") || "",
allowImpersonation: data?.user?.allowImpersonation, allowImpersonation: data?.user?.allowImpersonation,
name: data?.user?.name || "",
}, },
{ {
keepValues: true, keepValues: true,
@@ -123,7 +119,6 @@ export const ProfileForm = () => {
image: values.image, image: values.image,
currentPassword: values.currentPassword || undefined, currentPassword: values.currentPassword || undefined,
allowImpersonation: values.allowImpersonation, allowImpersonation: values.allowImpersonation,
name: values.name || undefined,
}) })
.then(async () => { .then(async () => {
await refetch(); await refetch();
@@ -133,7 +128,6 @@ export const ProfileForm = () => {
password: "", password: "",
image: values.image, image: values.image,
currentPassword: "", currentPassword: "",
name: values.name || "",
}); });
}) })
.catch(() => { .catch(() => {
@@ -173,19 +167,6 @@ export const ProfileForm = () => {
className="grid gap-4" className="grid gap-4"
> >
<div className="space-y-4"> <div className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="email" name="email"
@@ -258,24 +239,6 @@ export const ProfileForm = () => {
value={field.value} value={field.value}
className="flex flex-row flex-wrap gap-2 max-xl:justify-center" className="flex flex-row flex-wrap gap-2 max-xl:justify-center"
> >
<FormItem key="no-avatar">
<FormLabel className="[&:has([data-state=checked])>.default-avatar]:border-primary [&:has([data-state=checked])>.default-avatar]:border-1 [&:has([data-state=checked])>.default-avatar]:p-px cursor-pointer">
<FormControl>
<RadioGroupItem
value=""
className="sr-only"
/>
</FormControl>
<Avatar className="default-avatar h-12 w-12 rounded-full border hover:p-px hover:border-primary transition-transform">
<AvatarFallback className="rounded-lg">
{getFallbackAvatarInitials(
data?.user?.name,
)}
</AvatarFallback>
</Avatar>
</FormLabel>
</FormItem>
{availableAvatars.map((image) => ( {availableAvatars.map((image) => (
<FormItem key={image}> <FormItem key={image}>
<FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer"> <FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer">

View File

@@ -1,6 +1,5 @@
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -11,6 +10,8 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
import { EditTraefikEnv } from "../../web-server/edit-traefik-env"; import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports"; import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports";
import { ShowModalLogs } from "../../web-server/show-modal-logs"; import { ShowModalLogs } from "../../web-server/show-modal-logs";

View File

@@ -20,9 +20,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
}, },
); );
const enabled = serverId const enabled = data?.user.enableDockerCleanup || server?.enableDockerCleanup;
? server?.enableDockerCleanup
: data?.user.enableDockerCleanup;
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation(); const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();

View File

@@ -1,11 +1,3 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -38,6 +30,14 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import { useTranslation } from "next-i18next";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({ const Schema = z.object({
name: z.string().min(1, { name: z.string().min(1, {
@@ -218,7 +218,7 @@ export const HandleServers = ({ serverId }: Props) => {
</AlertBlock> </AlertBlock>
</div> </div>
{!canCreateMoreServers && ( {!canCreateMoreServers && (
<AlertBlock type="warning" className="mt-4"> <AlertBlock type="warning">
You cannot create more servers,{" "} You cannot create more servers,{" "}
<Link href="/dashboard/settings/billing" className="text-primary"> <Link href="/dashboard/settings/billing" className="text-primary">
Please upgrade your plan Please upgrade your plan

View File

@@ -1,9 +1,3 @@
import { format } from "date-fns";
import { KeyIcon, Loader2, MoreHorizontal, ServerIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -33,6 +27,12 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { format } from "date-fns";
import { KeyIcon, Loader2, MoreHorizontal, ServerIcon } from "lucide-react";
import { useTranslation } from "next-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { toast } from "sonner";
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal"; import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
import { TerminalModal } from "../web-server/terminal-modal"; import { TerminalModal } from "../web-server/terminal-modal";
import { ShowServerActions } from "./actions/show-server-actions"; import { ShowServerActions } from "./actions/show-server-actions";
@@ -115,6 +115,24 @@ export const ShowServers = () => {
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-4 min-h-[25vh]"> <div className="flex flex-col gap-4 min-h-[25vh]">
{!canCreateMoreServers && (
<AlertBlock type="warning">
<div className="flex flex-row items-center gap-3 justify-center">
<span>
<div>
You cannot create more servers,{" "}
<Link
href="/dashboard/settings/billing"
className="text-primary"
>
Please upgrade your plan
</Link>
</div>
</span>
</div>
</AlertBlock>
)}
<Table> <Table>
<TableCaption> <TableCaption>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">

View File

@@ -1,9 +1,3 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
@@ -28,6 +22,12 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({ const Schema = z.object({
name: z.string().min(1, { name: z.string().min(1, {
@@ -108,7 +108,7 @@ export const CreateServer = ({ stepper }: Props) => {
<Card className="bg-background flex flex-col gap-4"> <Card className="bg-background flex flex-col gap-4">
<div className="flex flex-col gap-2 pt-5 px-4"> <div className="flex flex-col gap-2 pt-5 px-4">
{!canCreateMoreServers && ( {!canCreateMoreServers && (
<AlertBlock type="warning" className="mt-2"> <AlertBlock type="warning">
You cannot create more servers,{" "} You cannot create more servers,{" "}
<Link href="/dashboard/settings/billing" className="text-primary"> <Link href="/dashboard/settings/billing" className="text-primary">
Please upgrade your plan Please upgrade your plan

View File

@@ -1,22 +1,18 @@
import copy from "copy-to-clipboard";
import { CopyIcon, ExternalLinkIcon, Loader2 } from "lucide-react";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { CodeEditor } from "@/components/shared/code-editor"; import { CodeEditor } from "@/components/shared/code-editor";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { ExternalLinkIcon, Loader2 } from "lucide-react";
import { CopyIcon } from "lucide-react";
import Link from "next/link";
import { useEffect, useRef } from "react";
import { toast } from "sonner";
export const CreateSSHKey = () => { export const CreateSSHKey = () => {
const { data, refetch } = api.sshKey.all.useQuery(); const { data, refetch } = api.sshKey.all.useQuery();
const generateMutation = api.sshKey.generate.useMutation(); const generateMutation = api.sshKey.generate.useMutation();
const { mutateAsync, isLoading } = api.sshKey.create.useMutation(); const { mutateAsync, isLoading } = api.sshKey.create.useMutation();
const hasCreatedKey = useRef(false); const hasCreatedKey = useRef(false);
const [selectedOption, setSelectedOption] = useState<"manual" | "provider">(
"manual",
);
const cloudSSHKey = data?.find( const cloudSSHKey = data?.find(
(sshKey) => sshKey.name === "dokploy-cloud-ssh-key", (sshKey) => sshKey.name === "dokploy-cloud-ssh-key",
@@ -64,122 +60,89 @@ export const CreateSSHKey = () => {
</div> </div>
) : ( ) : (
<> <>
<div className="flex flex-col gap-4 text-sm text-muted-foreground"> <div className="flex flex-col gap-2 text-sm text-muted-foreground">
<p className="text-primary text-base font-semibold"> <p className="text-primary text-base font-semibold">
Choose how to add SSH Keys to your server: You have two options to add SSH Keys to your server:
</p> </p>
{/* Radio button options */} <ul>
<div className="grid gap-2"> <li>1. Add The SSH Key to Server Manually</li>
<RadioGroup
value={selectedOption}
onValueChange={(value) => {
setSelectedOption(value as "manual" | "provider");
}}
className="grid gap-3"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="manual" id="manual" />
<Label
htmlFor="manual"
className="text-primary font-medium cursor-pointer"
>
Add SSH Key to Server Manually
</Label>
</div>
<div className="flex items-center space-x-2"> <li>
<RadioGroupItem value="provider" id="provider" /> 2. Add the public SSH Key when you create a server in your
<Label preffered provider (Hostinger, Digital Ocean, Hetzner, etc){" "}
htmlFor="provider" </li>
className="text-primary font-medium cursor-pointer" </ul>
>
Add SSH Key when creating server in your provider <div className="flex flex-col gap-2 w-full border rounded-lg p-4">
</Label> <span className="text-base font-semibold text-primary">
</div> Option 1
</RadioGroup> </span>
<ul>
<li className="items-center flex gap-1">
1. Login to your server{" "}
</li>
<li>
2. When you are logged in run the following command
<div className="flex relative flex-col gap-4 w-full mt-2">
<CodeEditor
lineWrapping
language="properties"
value={`echo "${cloudSSHKey?.publicKey}" >> ~/.ssh/authorized_keys`}
readOnly
className="font-mono opacity-60"
/>
<button
type="button"
className="absolute right-2 top-2"
onClick={() => {
copy(
`echo "${cloudSSHKey?.publicKey}" >> ~/.ssh/authorized_keys`,
);
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="size-4" />
</button>
</div>
</li>
<li className="mt-1">
3. You're done, follow the next step to insert the details
of your server.
</li>
</ul>
</div> </div>
<div className="flex flex-col gap-2 w-full mt-2 border rounded-lg p-4">
{/* Content based on selected option */} <span className="text-base font-semibold text-primary">
{selectedOption === "manual" && ( Option 2
<div className="flex flex-col gap-2 w-full border rounded-lg p-4"> </span>
<span className="text-base font-semibold text-primary"> <div className="flex flex-col gap-4 w-full overflow-auto">
Manual Setup Instructions <div className="flex relative flex-col gap-2 overflow-y-auto">
</span> <div className="text-sm text-primary flex flex-row gap-2 items-center">
<ul className="space-y-2"> Copy Public Key
<li className="items-center flex gap-1"> <button
1. Login to your server type="button"
</li> className="right-2 top-8"
<li> onClick={() => {
2. When you are logged in run the following command copy(
<div className="flex relative flex-col gap-4 w-full mt-2"> cloudSSHKey?.publicKey || "Generate a SSH Key",
<CodeEditor );
lineWrapping toast.success("SSH Copied to clipboard");
language="properties" }}
value={`echo "${cloudSSHKey?.publicKey}" >> ~/.ssh/authorized_keys`} >
readOnly <CopyIcon className="size-4 text-muted-foreground" />
className="font-mono opacity-60" </button>
/>
<button
type="button"
className="absolute right-2 top-2"
onClick={() => {
copy(
`echo "${cloudSSHKey?.publicKey}" >> ~/.ssh/authorized_keys`,
);
toast.success("Copied to clipboard");
}}
>
<CopyIcon className="size-4" />
</button>
</div>
</li>
<li className="mt-1">
3. You're done, follow the next step to insert the
details of your server.
</li>
</ul>
</div>
)}
{selectedOption === "provider" && (
<div className="flex flex-col gap-2 w-full border rounded-lg p-4">
<span className="text-base font-semibold text-primary">
Provider Setup Instructions
</span>
<div className="flex flex-col gap-4 w-full overflow-auto">
<div className="flex relative flex-col gap-2 overflow-y-auto">
<div className="text-sm text-primary flex flex-row gap-2 items-center">
Copy Public Key
<button
type="button"
className="right-2 top-8"
onClick={() => {
copy(
cloudSSHKey?.publicKey || "Generate a SSH Key",
);
toast.success("SSH Copied to clipboard");
}}
>
<CopyIcon className="size-4 text-muted-foreground" />
</button>
</div>
</div> </div>
</div> </div>
<p className="text-sm mt-2">
Use this public key when creating a server in your
preferred provider (Hostinger, Digital Ocean, Hetzner,
etc.)
</p>
<Link
href="https://docs.dokploy.com/docs/core/multi-server/instructions#requirements"
target="_blank"
className="text-primary flex flex-row gap-2 mt-2"
>
View Tutorial <ExternalLinkIcon className="size-4" />
</Link>
</div> </div>
)} <Link
href="https://docs.dokploy.com/docs/core/multi-server/instructions#requirements"
target="_blank"
className="text-primary flex flex-row gap-2"
>
View Tutorial <ExternalLinkIcon className="size-4" />
</Link>
</div>
</div> </div>
</> </>
)} )}

View File

@@ -1,11 +1,3 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowRightLeft, Plus, Trash2 } from "lucide-react";
import { useTranslation } from "next-i18next";
import type React from "react";
import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
@@ -27,15 +19,15 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowRightLeft, Plus, Trash2 } from "lucide-react";
import { useTranslation } from "next-i18next";
import type React from "react";
import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
@@ -45,7 +37,6 @@ interface Props {
const PortSchema = z.object({ const PortSchema = z.object({
targetPort: z.number().min(1, "Target port is required"), targetPort: z.number().min(1, "Target port is required"),
publishedPort: z.number().min(1, "Published port is required"), publishedPort: z.number().min(1, "Published port is required"),
protocol: z.enum(["tcp", "udp", "sctp"]),
}); });
const TraefikPortsSchema = z.object({ const TraefikPortsSchema = z.object({
@@ -84,17 +75,12 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
useEffect(() => { useEffect(() => {
if (currentPorts) { if (currentPorts) {
form.reset({ form.reset({ ports: currentPorts });
ports: currentPorts.map((port) => ({
...port,
protocol: port.protocol as "tcp" | "udp" | "sctp",
})),
});
} }
}, [currentPorts, form]); }, [currentPorts, form]);
const handleAddPort = () => { const handleAddPort = () => {
append({ targetPort: 0, publishedPort: 0, protocol: "tcp" }); append({ targetPort: 0, publishedPort: 0 });
}; };
const onSubmit = async (data: TraefikPortsForm) => { const onSubmit = async (data: TraefikPortsForm) => {
@@ -110,9 +96,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
return ( return (
<> <>
<button type="button" onClick={() => setOpen(true)}> <div onClick={() => setOpen(true)}>{children}</div>
{children}
</button>
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-3xl"> <DialogContent className="sm:max-w-3xl">
<DialogHeader> <DialogHeader>
@@ -159,8 +143,8 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
<ScrollArea className="h-[400px] pr-4"> <ScrollArea className="h-[400px] pr-4">
<div className="grid gap-4"> <div className="grid gap-4">
{fields.map((field, index) => ( {fields.map((field, index) => (
<Card key={field.id} className="bg-transparent"> <Card key={field.id}>
<CardContent className="grid grid-cols-[1fr_1fr_1fr_auto] gap-4 p-4 transparent"> <CardContent className="grid grid-cols-[1fr_1fr_auto] gap-4 p-4 transparent">
<FormField <FormField
control={form.control} control={form.control}
name={`ports.${index}.targetPort`} name={`ports.${index}.targetPort`}
@@ -184,6 +168,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
); );
}} }}
value={field.value || ""} value={field.value || ""}
className="w-full dark:bg-black"
placeholder="e.g. 8080" placeholder="e.g. 8080"
/> />
</FormControl> </FormControl>
@@ -215,6 +200,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
); );
}} }}
value={field.value || ""} value={field.value || ""}
className="w-full dark:bg-black"
placeholder="e.g. 80" placeholder="e.g. 80"
/> />
</FormControl> </FormControl>
@@ -222,42 +208,6 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name={`ports.${index}.protocol`}
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-muted-foreground">
Protocol
</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a protocol" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{["tcp", "udp", "sctp"].map(
(protocol) => (
<SelectItem
key={protocol}
value={protocol}
>
{protocol}
</SelectItem>
),
)}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-end"> <div className="flex items-end">
<Button <Button

View File

@@ -126,7 +126,7 @@ export const UpdateServer = ({
</TooltipProvider> </TooltipProvider>
)} )}
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-lg"> <DialogContent className="max-w-lg p-6">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
<DialogTitle className="text-2xl font-semibold"> <DialogTitle className="text-2xl font-semibold">
Web Server Update Web Server Update
@@ -253,7 +253,7 @@ export const UpdateServer = ({
<ToggleAutoCheckUpdates disabled={isLoading} /> <ToggleAutoCheckUpdates disabled={isLoading} />
</div> </div>
<div className="space-y-4 flex items-center justify-end mt-4 "> <div className="space-y-4 flex items-center justify-end">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="outline" onClick={() => onOpenChange?.(false)}> <Button variant="outline" onClick={() => onOpenChange?.(false)}>
Cancel Cancel

View File

@@ -1,4 +1,3 @@
import { Layers, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -9,6 +8,7 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Layers, Loader2 } from "lucide-react";
import { type ApplicationList, columns } from "./columns"; import { type ApplicationList, columns } from "./columns";
import { DataTable } from "./data-table"; import { DataTable } from "./data-table";
@@ -20,10 +20,10 @@ export const ShowNodeApplications = ({ serverId }: Props) => {
const { data: NodeApps, isLoading: NodeAppsLoading } = const { data: NodeApps, isLoading: NodeAppsLoading } =
api.swarm.getNodeApps.useQuery({ serverId }); api.swarm.getNodeApps.useQuery({ serverId });
let applicationList: string[] = []; let applicationList = "";
if (NodeApps && NodeApps.length > 0) { if (NodeApps && NodeApps.length > 0) {
applicationList = NodeApps.map((app) => app.Name); applicationList = NodeApps.map((app) => app.Name).join(" ");
} }
const { data: NodeAppDetails, isLoading: NodeAppDetailsLoading } = const { data: NodeAppDetails, isLoading: NodeAppDetailsLoading } =

View File

@@ -1,6 +1,6 @@
import { cn } from "@/lib/utils";
import Link from "next/link"; import Link from "next/link";
import type React from "react"; import type React from "react";
import { cn } from "@/lib/utils";
import { GithubIcon } from "../icons/data-tools-icons"; import { GithubIcon } from "../icons/data-tools-icons";
import { Logo } from "../shared/logo"; import { Logo } from "../shared/logo";
import { Button } from "../ui/button"; import { Button } from "../ui/button";

View File

@@ -1,5 +1,4 @@
"use client"; "use client";
import type { inferRouterOutputs } from "@trpc/server";
import { import {
Activity, Activity,
BarChartHorizontalBigIcon, BarChartHorizontalBigIcon,
@@ -30,10 +29,10 @@ import {
User, User,
Users, Users,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import type * as React from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner";
import { import {
Breadcrumb, Breadcrumb,
BreadcrumbItem, BreadcrumbItem,
@@ -78,6 +77,10 @@ import { authClient } from "@/lib/auth-client";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { AppRouter } from "@/server/api/root"; import type { AppRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { inferRouterOutputs } from "@trpc/server";
import Link from "next/link";
import { useRouter } from "next/router";
import { toast } from "sonner";
import { AddOrganization } from "../dashboard/organization/handle-organization"; import { AddOrganization } from "../dashboard/organization/handle-organization";
import { DialogAction } from "../shared/dialog-action"; import { DialogAction } from "../shared/dialog-action";
import { Logo } from "../shared/logo"; import { Logo } from "../shared/logo";
@@ -93,7 +96,10 @@ type SingleNavItem = {
title: string; title: string;
url: string; url: string;
icon?: LucideIcon; icon?: LucideIcon;
isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean; isEnabled?: (opts: {
auth?: AuthQueryOutput;
isCloud: boolean;
}) => boolean;
}; };
// NavItem type // NavItem type
@@ -119,7 +125,10 @@ type ExternalLink = {
name: string; name: string;
url: string; url: string;
icon: React.ComponentType<{ className?: string }>; icon: React.ComponentType<{ className?: string }>;
isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean; isEnabled?: (opts: {
auth?: AuthQueryOutput;
isCloud: boolean;
}) => boolean;
}; };
// Menu type // Menu type
@@ -767,7 +776,9 @@ export default function Page({ children }: Props) {
setIsLoaded(true); setIsLoaded(true);
}, []); }, []);
const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const _currentPath = router.pathname;
const { data: auth } = api.user.get.useQuery(); const { data: auth } = api.user.get.useQuery();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery(); const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();

View File

@@ -1,7 +1,8 @@
import { api } from "@/utils/api";
import type { IUpdateData } from "@dokploy/server/index"; import type { IUpdateData } from "@dokploy/server/index";
import { Download } from "lucide-react"; import { Download } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { api } from "@/utils/api";
import UpdateServer from "../dashboard/settings/web-server/update-server"; import UpdateServer from "../dashboard/settings/web-server/update-server";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { import {
@@ -10,7 +11,6 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "../ui/tooltip"; } from "../ui/tooltip";
const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7; const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
export const UpdateServerButton = () => { export const UpdateServerButton = () => {
@@ -18,6 +18,7 @@ export const UpdateServerButton = () => {
latestVersion: null, latestVersion: null,
updateAvailable: false, updateAvailable: false,
}); });
const _router = useRouter();
const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync: getUpdateData } = const { mutateAsync: getUpdateData } =
api.settings.getUpdateData.useMutation(); api.settings.getUpdateData.useMutation();
@@ -25,6 +26,9 @@ export const UpdateServerButton = () => {
const checkUpdatesIntervalRef = useRef<null | NodeJS.Timeout>(null); const checkUpdatesIntervalRef = useRef<null | NodeJS.Timeout>(null);
if (isCloud) {
return null;
}
useEffect(() => { useEffect(() => {
// Handling of automatic check for server updates // Handling of automatic check for server updates
if (isCloud) { if (isCloud) {
@@ -73,7 +77,7 @@ export const UpdateServerButton = () => {
}; };
}, []); }, []);
return !isCloud && updateData.updateAvailable ? ( return updateData.updateAvailable ? (
<div className="border-t pt-4"> <div className="border-t pt-4">
<UpdateServer <UpdateServer
updateData={updateData} updateData={updateData}

View File

@@ -1,5 +1,3 @@
import { ChevronsUpDown } from "lucide-react";
import { useRouter } from "next/router";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { import {
DropdownMenu, DropdownMenu,
@@ -19,9 +17,10 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { Languages } from "@/lib/languages"; import { Languages } from "@/lib/languages";
import { getFallbackAvatarInitials } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import useLocale from "@/utils/hooks/use-locale"; import useLocale from "@/utils/hooks/use-locale";
import { ChevronsUpDown } from "lucide-react";
import { useRouter } from "next/router";
import { ModeToggle } from "../ui/modeToggle"; import { ModeToggle } from "../ui/modeToggle";
import { SidebarMenuButton } from "../ui/sidebar"; import { SidebarMenuButton } from "../ui/sidebar";
@@ -47,9 +46,7 @@ export const UserNav = () => {
src={data?.user?.image || ""} src={data?.user?.image || ""}
alt={data?.user?.image || ""} alt={data?.user?.image || ""}
/> />
<AvatarFallback className="rounded-lg"> <AvatarFallback className="rounded-lg">CN</AvatarFallback>
{getFallbackAvatarInitials(data?.user?.name)}
</AvatarFallback>
</Avatar> </Avatar>
<div className="grid flex-1 text-left text-sm leading-tight"> <div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">Account</span> <span className="truncate font-semibold">Account</span>
@@ -125,16 +122,18 @@ export const UserNav = () => {
)} )}
</> </>
) : ( ) : (
data?.role === "owner" && ( <>
<DropdownMenuItem {data?.role === "owner" && (
className="cursor-pointer" <DropdownMenuItem
onClick={() => { className="cursor-pointer"
router.push("/dashboard/settings/servers"); onClick={() => {
}} router.push("/dashboard/settings/servers");
> }}
Servers >
</DropdownMenuItem> Servers
) </DropdownMenuItem>
)}
</>
)} )}
</DropdownMenuGroup> </DropdownMenuGroup>
{isCloud && data?.role === "owner" && ( {isCloud && data?.role === "owner" && (

View File

@@ -1 +0,0 @@
ALTER TABLE "application" ADD COLUMN "previewRequireCollaboratorPermissions" boolean DEFAULT true;

View File

@@ -1 +0,0 @@
ALTER TABLE "application" ADD COLUMN "railpackVersion" text DEFAULT '0.2.2';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -722,20 +722,6 @@
"when": 1751848685503, "when": 1751848685503,
"tag": "0102_opposite_grandmaster", "tag": "0102_opposite_grandmaster",
"breakpoints": true "breakpoints": true
},
{
"idx": 103,
"version": "7",
"when": 1752465764072,
"tag": "0103_cultured_pestilence",
"breakpoints": true
},
{
"idx": 104,
"version": "7",
"when": 1754207407121,
"tag": "0104_omniscient_randall",
"breakpoints": true
} }
] ]
} }

View File

@@ -27,14 +27,3 @@ export function formatTimestamp(timestamp: string | number) {
return "Fecha inválida"; return "Fecha inválida";
} }
} }
export function getFallbackAvatarInitials(
fullName: string | undefined,
): string {
if (typeof fullName === "undefined" || fullName === "") return "CN";
const [name = "", surname = ""] = fullName.split(" ");
if (surname === "") {
return name.substring(0, 2).toUpperCase();
}
return (name.charAt(0) + surname.charAt(0)).toUpperCase();
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.24.6", "version": "v0.24.2",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
@@ -187,10 +187,10 @@
"ct3aMetadata": { "ct3aMetadata": {
"initVersion": "7.25.2" "initVersion": "7.25.2"
}, },
"packageManager": "pnpm@9.12.0", "packageManager": "pnpm@9.5.0",
"engines": { "engines": {
"node": "^20.16.0", "node": "^20.16.0",
"pnpm": ">=9.12.0" "pnpm": ">=9.5.0"
}, },
"lint-staged": { "lint-staged": {
"*": [ "*": [
@@ -198,8 +198,6 @@
] ]
}, },
"commitlint": { "commitlint": {
"extends": [ "extends": ["@commitlint/config-conventional"]
"@commitlint/config-conventional"
]
} }
} }

View File

@@ -5,10 +5,7 @@ import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy"; import { deploy } from "@/server/utils/deploy";
import { import {
IS_CLOUD, IS_CLOUD,
checkUserRepositoryPermissions,
createPreviewDeployment, createPreviewDeployment,
createSecurityBlockedComment,
findGithubById,
findPreviewDeploymentByApplicationId, findPreviewDeploymentByApplicationId,
findPreviewDeploymentsByPullRequestId, findPreviewDeploymentsByPullRequestId,
removePreviewDeployment, removePreviewDeployment,
@@ -349,18 +346,6 @@ export default async function handler(
const deploymentHash = githubBody?.pull_request?.head?.sha; const deploymentHash = githubBody?.pull_request?.head?.sha;
const branch = githubBody?.pull_request?.base?.ref; const branch = githubBody?.pull_request?.base?.ref;
const owner = githubBody?.repository?.owner?.login; const owner = githubBody?.repository?.owner?.login;
const prAuthor = githubBody?.pull_request?.user?.login;
// Validate PR author information is present
if (!prAuthor) {
console.warn(
"⚠️ SECURITY: PR author information missing in webhook payload",
);
res.status(400).json({
message: "PR author information missing",
});
return;
}
const apps = await db.query.applications.findMany({ const apps = await db.query.applications.findMany({
where: and( where: and(
@@ -376,72 +361,13 @@ export default async function handler(
}, },
}); });
// SECURITY: Check collaborator permissions per application setting
const secureApps: typeof apps = [];
const blockedApps: string[] = [];
let userPermission: string | null = null;
for (const app of apps) {
// If the app requires collaborator permissions, verify them
if (app.previewRequireCollaboratorPermissions !== false) {
try {
const githubProvider = await findGithubById(githubResult.githubId);
const { hasWriteAccess, permission } =
await checkUserRepositoryPermissions(
githubProvider,
owner,
repository,
prAuthor,
);
userPermission = permission; // Store permission for comment
if (!hasWriteAccess) {
console.warn(
`🚨 SECURITY: Blocked preview deployment for ${app.name} from unauthorized user ${prAuthor} on ${owner}/${repository}. Permission: ${permission || "none"}`,
);
blockedApps.push(app.name);
continue;
}
console.log(
`✅ SECURITY: Preview deployment authorized for ${app.name} from user ${prAuthor} on ${owner}/${repository}. Permission: ${permission}`,
);
} catch (error) {
console.error(
`Error validating PR author permissions for ${app.name}:`,
error,
);
blockedApps.push(app.name);
continue; // Skip this app on error
}
} else {
console.warn(
`⚠️ SECURITY: Preview deployment for ${app.name} allows deployment from any PR author (security check disabled)`,
);
}
secureApps.push(app);
}
const prBranch = githubBody?.pull_request?.head?.ref; const prBranch = githubBody?.pull_request?.head?.ref;
const prNumber = githubBody?.pull_request?.number; const prNumber = githubBody?.pull_request?.number;
const prTitle = githubBody?.pull_request?.title; const prTitle = githubBody?.pull_request?.title;
const prURL = githubBody?.pull_request?.html_url; const prURL = githubBody?.pull_request?.html_url;
// Create security notification comment if any apps were blocked for (const app of apps) {
if (blockedApps.length > 0) {
await createSecurityBlockedComment({
owner,
repository,
prNumber: Number.parseInt(prNumber),
prAuthor,
permission: userPermission,
githubId: githubResult.githubId,
});
}
for (const app of secureApps) {
const previewLimit = app?.previewLimit || 0; const previewLimit = app?.previewLimit || 0;
if (app?.previewDeployments?.length > previewLimit) { if (app?.previewDeployments?.length > previewLimit) {
continue; continue;

View File

@@ -37,6 +37,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server/lib/auth"; import { validateRequest } from "@dokploy/server/lib/auth";

View File

@@ -33,6 +33,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server/lib/auth"; import { validateRequest } from "@dokploy/server/lib/auth";

View File

@@ -28,8 +28,6 @@ import { projectRouter } from "./routers/project";
import { redirectsRouter } from "./routers/redirects"; import { redirectsRouter } from "./routers/redirects";
import { redisRouter } from "./routers/redis"; import { redisRouter } from "./routers/redis";
import { registryRouter } from "./routers/registry"; import { registryRouter } from "./routers/registry";
import { rollbackRouter } from "./routers/rollbacks";
import { scheduleRouter } from "./routers/schedule";
import { securityRouter } from "./routers/security"; import { securityRouter } from "./routers/security";
import { serverRouter } from "./routers/server"; import { serverRouter } from "./routers/server";
import { settingsRouter } from "./routers/settings"; import { settingsRouter } from "./routers/settings";
@@ -37,6 +35,8 @@ import { sshRouter } from "./routers/ssh-key";
import { stripeRouter } from "./routers/stripe"; import { stripeRouter } from "./routers/stripe";
import { swarmRouter } from "./routers/swarm"; import { swarmRouter } from "./routers/swarm";
import { userRouter } from "./routers/user"; import { userRouter } from "./routers/user";
import { scheduleRouter } from "./routers/schedule";
import { rollbackRouter } from "./routers/rollbacks";
import { volumeBackupsRouter } from "./routers/volume-backups"; import { volumeBackupsRouter } from "./routers/volume-backups";
/** /**
* This is the primary router for your server. * This is the primary router for your server.

View File

@@ -1,4 +1,31 @@
import { import {
createTRPCRouter,
protectedProcedure,
uploadProcedure,
} from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiCreateApplication,
apiFindMonitoringStats,
apiFindOneApplication,
apiReloadApplication,
apiSaveBitbucketProvider,
apiSaveBuildType,
apiSaveDockerProvider,
apiSaveEnvironmentVariables,
apiSaveGitProvider,
apiSaveGiteaProvider,
apiSaveGithubProvider,
apiSaveGitlabProvider,
apiUpdateApplication,
applications,
} from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { uploadFileSchema } from "@/utils/schema";
import {
IS_CLOUD,
addNewService, addNewService,
checkServiceAccess, checkServiceAccess,
createApplication, createApplication,
@@ -7,7 +34,6 @@ import {
findGitProviderById, findGitProviderById,
findProjectById, findProjectById,
getApplicationStats, getApplicationStats,
IS_CLOUD,
mechanizeDockerContainer, mechanizeDockerContainer,
readConfig, readConfig,
readRemoteConfig, readRemoteConfig,
@@ -31,32 +57,6 @@ import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import {
createTRPCRouter,
protectedProcedure,
uploadProcedure,
} from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiCreateApplication,
apiFindMonitoringStats,
apiFindOneApplication,
apiReloadApplication,
apiSaveBitbucketProvider,
apiSaveBuildType,
apiSaveDockerProvider,
apiSaveEnvironmentVariables,
apiSaveGiteaProvider,
apiSaveGithubProvider,
apiSaveGitlabProvider,
apiSaveGitProvider,
apiUpdateApplication,
applications,
} from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { uploadFileSchema } from "@/utils/schema";
export const applicationRouter = createTRPCRouter({ export const applicationRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure
@@ -364,7 +364,6 @@ export const applicationRouter = createTRPCRouter({
dockerBuildStage: input.dockerBuildStage, dockerBuildStage: input.dockerBuildStage,
herokuVersion: input.herokuVersion, herokuVersion: input.herokuVersion,
isStaticSpa: input.isStaticSpa, isStaticSpa: input.isStaticSpa,
railpackVersion: input.railpackVersion,
}); });
return true; return true;

View File

@@ -20,8 +20,8 @@ import {
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { z } from "zod";
export const deploymentRouter = createTRPCRouter({ export const deploymentRouter = createTRPCRouter({
all: protectedProcedure all: protectedProcedure

View File

@@ -8,9 +8,9 @@ import {
getServiceContainersByAppName, getServiceContainersByAppName,
getStackContainersByAppName, getStackContainersByAppName,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { TRPCError } from "@trpc/server";
export const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/; export const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/;

View File

@@ -12,8 +12,8 @@ import {
getServiceContainer, getServiceContainer,
updateMount, updateMount,
} from "@dokploy/server"; } from "@dokploy/server";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { z } from "zod";
export const mountRouter = createTRPCRouter({ export const mountRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure

View File

@@ -1,13 +1,23 @@
import { apiFindAllByApplication } from "@/server/db/schema"; import { db } from "@/server/db";
import { apiFindAllByApplication, applications } from "@/server/db/schema";
import { import {
createPreviewDeployment,
findApplicationById, findApplicationById,
findPreviewDeploymentByApplicationId,
findPreviewDeploymentById, findPreviewDeploymentById,
findPreviewDeploymentsByApplicationId, findPreviewDeploymentsByApplicationId,
findPreviewDeploymentsByPullRequestId,
IS_CLOUD,
removePreviewDeployment, removePreviewDeployment,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { eq } from "drizzle-orm";
import { and } from "drizzle-orm";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import type { DeploymentJob } from "@/server/queues/queue-types";
export const previewDeploymentRouter = createTRPCRouter({ export const previewDeploymentRouter = createTRPCRouter({
all: protectedProcedure all: protectedProcedure
@@ -59,4 +69,142 @@ export const previewDeploymentRouter = createTRPCRouter({
} }
return previewDeployment; return previewDeployment;
}), }),
create: protectedProcedure
.input(
z.object({
action: z.enum(["opened", "synchronize", "reopened", "closed"]),
pullRequestId: z.string(),
repository: z.string(),
owner: z.string(),
branch: z.string(),
deploymentHash: z.string(),
prBranch: z.string(),
prNumber: z.any(),
prTitle: z.string(),
prURL: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const organizationId = ctx.session.activeOrganizationId;
const action = input.action;
const prId = input.pullRequestId;
if (action === "closed") {
const previewDeploymentResult =
await findPreviewDeploymentsByPullRequestId(prId);
const filteredPreviewDeploymentResult = previewDeploymentResult.filter(
(previewDeployment) =>
previewDeployment.application.project.organizationId ===
organizationId,
);
if (filteredPreviewDeploymentResult.length > 0) {
for (const previewDeployment of filteredPreviewDeploymentResult) {
try {
await removePreviewDeployment(
previewDeployment.previewDeploymentId,
);
} catch (error) {
console.log(error);
}
}
}
return {
message: "Preview Deployments Closed",
};
}
if (
action === "opened" ||
action === "synchronize" ||
action === "reopened"
) {
const deploymentHash = input.deploymentHash;
const prBranch = input.prBranch;
const prNumber = input.prNumber;
const prTitle = input.prTitle;
const prURL = input.prURL;
const apps = await db.query.applications.findMany({
where: and(
eq(applications.sourceType, "github"),
eq(applications.repository, input.repository),
eq(applications.branch, input.branch),
eq(applications.isPreviewDeploymentsActive, true),
eq(applications.owner, input.owner),
),
with: {
previewDeployments: true,
project: true,
},
});
const filteredApps = apps.filter(
(app) => app.project.organizationId === organizationId,
);
console.log(filteredApps);
for (const app of filteredApps) {
const previewLimit = app?.previewLimit || 0;
if (app?.previewDeployments?.length > previewLimit) {
continue;
}
const previewDeploymentResult =
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
let previewDeploymentId =
previewDeploymentResult?.previewDeploymentId || "";
if (!previewDeploymentResult) {
try {
const previewDeployment = await createPreviewDeployment({
applicationId: app.applicationId as string,
branch: prBranch,
pullRequestId: prId,
pullRequestNumber: prNumber,
pullRequestTitle: prTitle,
pullRequestURL: prURL,
});
console.log(previewDeployment);
previewDeploymentId = previewDeployment.previewDeploymentId;
} catch (error) {
console.log(error);
}
}
const jobData: DeploymentJob = {
applicationId: app.applicationId as string,
titleLog: "Preview Deployment",
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application-preview",
server: !!app.serverId,
previewDeploymentId,
isExternal: true,
};
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
continue;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
}
return {
message: "Preview Deployments Created",
};
}),
}); });

View File

@@ -361,7 +361,6 @@ export const projectRouter = createTRPCRouter({
previewDeployments, previewDeployments,
mounts, mounts,
appName, appName,
refreshToken,
...application ...application
} = await findApplicationById(id); } = await findApplicationById(id);
const newAppName = appName.substring( const newAppName = appName.substring(
@@ -604,14 +603,8 @@ export const projectRouter = createTRPCRouter({
break; break;
} }
case "compose": { case "compose": {
const { const { composeId, mounts, domains, appName, ...compose } =
composeId, await findComposeById(id);
mounts,
domains,
appName,
refreshToken,
...compose
} = await findComposeById(id);
const newAppName = appName.substring( const newAppName = appName.substring(
0, 0,

View File

@@ -1,54 +1,3 @@
import {
canAccessToTraefikFiles,
checkGPUStatus,
cleanStoppedContainers,
cleanUpDockerBuilder,
cleanUpSystemPrune,
cleanUpUnusedImages,
cleanUpUnusedVolumes,
DEFAULT_UPDATE_DATA,
execAsync,
findServerById,
findUserById,
getDokployImage,
getDokployImageTag,
getLogCleanupStatus,
getUpdateData,
IS_CLOUD,
parseRawConfig,
paths,
prepareEnvironmentVariables,
processLogs,
pullLatestRelease,
readConfig,
readConfigInPath,
readDirectory,
readEnvironmentVariables,
readMainConfig,
readMonitoringConfig,
readPorts,
recreateDirectory,
reloadDockerResource,
sendDockerCleanupNotifications,
setupGPUSupport,
spawnAsync,
startLogCleanup,
stopLogCleanup,
updateLetsEncryptEmail,
updateServerById,
updateServerTraefik,
updateUser,
writeConfig,
writeMainConfig,
writeTraefikConfigInPath,
writeTraefikSetup,
} from "@dokploy/server";
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
import { TRPCError } from "@trpc/server";
import { sql } from "drizzle-orm";
import { dump, load } from "js-yaml";
import { scheduledJobs, scheduleJob } from "node-schedule";
import { z } from "zod";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { import {
apiAssignDomain, apiAssignDomain,
@@ -62,6 +11,54 @@ import {
apiUpdateDockerCleanup, apiUpdateDockerCleanup,
} from "@/server/db/schema"; } from "@/server/db/schema";
import { removeJob, schedule } from "@/server/utils/backup"; import { removeJob, schedule } from "@/server/utils/backup";
import {
DEFAULT_UPDATE_DATA,
IS_CLOUD,
canAccessToTraefikFiles,
cleanStoppedContainers,
cleanUpDockerBuilder,
cleanUpSystemPrune,
cleanUpUnusedImages,
cleanUpUnusedVolumes,
execAsync,
execAsyncRemote,
findServerById,
findUserById,
getDokployImage,
getDokployImageTag,
getLogCleanupStatus,
getUpdateData,
initializeTraefik,
parseRawConfig,
paths,
prepareEnvironmentVariables,
processLogs,
pullLatestRelease,
readConfig,
readConfigInPath,
readDirectory,
readMainConfig,
readMonitoringConfig,
recreateDirectory,
sendDockerCleanupNotifications,
spawnAsync,
startLogCleanup,
stopLogCleanup,
updateLetsEncryptEmail,
updateServerById,
updateServerTraefik,
updateUser,
writeConfig,
writeMainConfig,
writeTraefikConfigInPath,
} from "@dokploy/server";
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
import { TRPCError } from "@trpc/server";
import { sql } from "drizzle-orm";
import { dump, load } from "js-yaml";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { z } from "zod";
import packageInfo from "../../../package.json"; import packageInfo from "../../../package.json";
import { appRouter } from "../root"; import { appRouter } from "../root";
import { import {
@@ -76,7 +73,10 @@ export const settingsRouter = createTRPCRouter({
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return true;
} }
await reloadDockerResource("dokploy"); const { stdout } = await execAsync(
"docker service inspect dokploy --format '{{.ID}}'",
);
await execAsync(`docker service update --force ${stdout.trim()}`);
return true; return true;
}), }),
cleanRedis: adminProcedure.mutation(async () => { cleanRedis: adminProcedure.mutation(async () => {
@@ -101,15 +101,20 @@ export const settingsRouter = createTRPCRouter({
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return true;
} }
await reloadDockerResource("dokploy-redis");
await execAsync("docker service scale dokploy-redis=0");
await execAsync("docker service scale dokploy-redis=1");
return true; return true;
}), }),
reloadTraefik: adminProcedure reloadTraefik: adminProcedure
.input(apiServerSchema) .input(apiServerSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
await reloadDockerResource("dokploy-traefik", input?.serverId); if (input?.serverId) {
await execAsync("docker restart dokploy-traefik");
} else if (!IS_CLOUD) {
await execAsync("docker restart dokploy-traefik");
}
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
@@ -119,28 +124,17 @@ export const settingsRouter = createTRPCRouter({
toggleDashboard: adminProcedure toggleDashboard: adminProcedure
.input(apiEnableDashboard) .input(apiEnableDashboard)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const ports = await readPorts("dokploy-traefik", input.serverId); const ports = (await getTraefikPorts(input.serverId)).filter(
const env = await readEnvironmentVariables( (port) =>
"dokploy-traefik", port.targetPort !== 80 &&
input.serverId, port.targetPort !== 443 &&
port.targetPort !== 8080,
); );
const preparedEnv = prepareEnvironmentVariables(env); await initializeTraefik({
let newPorts = ports; additionalPorts: ports,
// If receive true, add 8080 to ports enableDashboard: input.enableDashboard,
if (input.enableDashboard) {
newPorts.push({
targetPort: 8080,
publishedPort: 8080,
protocol: "tcp",
});
} else {
newPorts = ports.filter((port) => port.targetPort !== 8080);
}
await writeTraefikSetup({
env: preparedEnv,
additionalPorts: newPorts,
serverId: input.serverId, serverId: input.serverId,
force: true,
}); });
return true; return true;
}), }),
@@ -557,23 +551,29 @@ export const settingsRouter = createTRPCRouter({
readTraefikEnv: adminProcedure readTraefikEnv: adminProcedure
.input(apiServerSchema) .input(apiServerSchema)
.query(async ({ input }) => { .query(async ({ input }) => {
const envVars = await readEnvironmentVariables( const command =
"dokploy-traefik", "docker container inspect dokploy-traefik --format '{{json .Config.Env}}'";
input?.serverId,
); let result = "";
return envVars; if (input?.serverId) {
const execResult = await execAsyncRemote(input.serverId, command);
result = execResult.stdout;
} else {
const execResult = await execAsync(command);
result = execResult.stdout;
}
const envVars = JSON.parse(result.trim());
return envVars.join("\n");
}), }),
writeTraefikEnv: adminProcedure writeTraefikEnv: adminProcedure
.input(z.object({ env: z.string(), serverId: z.string().optional() })) .input(z.object({ env: z.string(), serverId: z.string().optional() }))
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const envs = prepareEnvironmentVariables(input.env); const envs = prepareEnvironmentVariables(input.env);
const ports = await readPorts("dokploy-traefik", input?.serverId); await initializeTraefik({
await writeTraefikSetup({
env: envs, env: envs,
additionalPorts: ports,
serverId: input.serverId, serverId: input.serverId,
force: true,
}); });
return true; return true;
@@ -581,8 +581,22 @@ export const settingsRouter = createTRPCRouter({
haveTraefikDashboardPortEnabled: adminProcedure haveTraefikDashboardPortEnabled: adminProcedure
.input(apiServerSchema) .input(apiServerSchema)
.query(async ({ input }) => { .query(async ({ input }) => {
const ports = await readPorts("dokploy-traefik", input?.serverId); const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`;
return ports.some((port) => port.targetPort === 8080);
let stdout = "";
if (input?.serverId) {
const result = await execAsyncRemote(input.serverId, command);
stdout = result.stdout;
} else if (!IS_CLOUD) {
const result = await execAsync(command);
stdout = result.stdout;
}
const ports = JSON.parse(stdout.trim());
return Object.entries(ports).some(([containerPort, bindings]) => {
const [port] = containerPort.split("/");
return port === "8080" && bindings && (bindings as any[]).length > 0;
});
}), }),
readStatsLogs: adminProcedure readStatsLogs: adminProcedure
@@ -627,16 +641,10 @@ export const settingsRouter = createTRPCRouter({
}, },
}) })
.input( .input(
z z.object({
.object({ start: z.string().optional(),
dateRange: z end: z.string().optional(),
.object({ }),
start: z.string().optional(),
end: z.string().optional(),
})
.optional(),
})
.optional(),
) )
.query(async ({ input }) => { .query(async ({ input }) => {
if (IS_CLOUD) { if (IS_CLOUD) {
@@ -779,7 +787,6 @@ export const settingsRouter = createTRPCRouter({
z.object({ z.object({
targetPort: z.number(), targetPort: z.number(),
publishedPort: z.number(), publishedPort: z.number(),
protocol: z.enum(["tcp", "udp", "sctp"]),
}), }),
), ),
}), }),
@@ -792,16 +799,10 @@ export const settingsRouter = createTRPCRouter({
message: "Please set a serverId to update Traefik ports", message: "Please set a serverId to update Traefik ports",
}); });
} }
const env = await readEnvironmentVariables( await initializeTraefik({
"dokploy-traefik",
input?.serverId,
);
const preparedEnv = prepareEnvironmentVariables(env);
await writeTraefikSetup({
env: preparedEnv,
additionalPorts: input.additionalPorts,
serverId: input.serverId, serverId: input.serverId,
additionalPorts: input.additionalPorts,
force: true,
}); });
return true; return true;
} catch (error) { } catch (error) {
@@ -818,8 +819,7 @@ export const settingsRouter = createTRPCRouter({
getTraefikPorts: adminProcedure getTraefikPorts: adminProcedure
.input(apiServerSchema) .input(apiServerSchema)
.query(async ({ input }) => { .query(async ({ input }) => {
const ports = await readPorts("dokploy-traefik", input?.serverId); return await getTraefikPorts(input?.serverId);
return ports;
}), }),
updateLogCleanup: adminProcedure updateLogCleanup: adminProcedure
.input( .input(
@@ -849,3 +849,56 @@ export const settingsRouter = createTRPCRouter({
return ips; return ips;
}), }),
}); });
export const getTraefikPorts = async (serverId?: string) => {
const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`;
try {
let stdout = "";
if (serverId) {
const result = await execAsyncRemote(serverId, command);
stdout = result.stdout;
} else if (!IS_CLOUD) {
const result = await execAsync(command);
stdout = result.stdout;
}
const portsMap = JSON.parse(stdout.trim());
const additionalPorts: Array<{
targetPort: number;
publishedPort: number;
}> = [];
// Convert the Docker container port format to our expected format
for (const [containerPort, bindings] of Object.entries(portsMap)) {
if (!bindings) continue;
const [port = ""] = containerPort.split("/");
if (!port) continue;
const targetPortNum = Number.parseInt(port, 10);
if (Number.isNaN(targetPortNum)) continue;
// Skip default ports
if ([80, 443].includes(targetPortNum)) continue;
for (const binding of bindings as Array<{ HostPort: string }>) {
if (!binding.HostPort) continue;
const publishedPort = Number.parseInt(binding.HostPort, 10);
if (Number.isNaN(publishedPort)) continue;
additionalPorts.push({
targetPort: targetPortNum,
publishedPort,
});
}
}
return additionalPorts;
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to get Traefik ports",
cause: error,
});
}
};

View File

@@ -1,13 +1,13 @@
import { import {
findServerById,
getApplicationInfo, getApplicationInfo,
getNodeApplications, getNodeApplications,
getNodeInfo, getNodeInfo,
getSwarmNodes, getSwarmNodes,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { TRPCError } from "@trpc/server";
import { findServerById } from "@dokploy/server";
import { containerIdRegex } from "./docker"; import { containerIdRegex } from "./docker";
export const swarmRouter = createTRPCRouter({ export const swarmRouter = createTRPCRouter({
@@ -55,12 +55,7 @@ export const swarmRouter = createTRPCRouter({
getAppInfos: protectedProcedure getAppInfos: protectedProcedure
.input( .input(
z.object({ z.object({
appName: z appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
.string()
.min(1)
.regex(containerIdRegex, "Invalid app name.")
.array()
.min(1),
serverId: z.string().optional(), serverId: z.string().optional(),
}), }),
) )

View File

@@ -1,30 +1,30 @@
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
import { import {
IS_CLOUD, IS_CLOUD,
createVolumeBackup,
findVolumeBackupById,
removeVolumeBackup,
removeVolumeBackupJob,
restoreVolume,
runVolumeBackup,
scheduleVolumeBackup,
updateVolumeBackup, updateVolumeBackup,
removeVolumeBackup,
createVolumeBackup,
runVolumeBackup,
findVolumeBackupById,
restoreVolume,
scheduleVolumeBackup,
removeVolumeBackupJob,
} from "@dokploy/server"; } from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { import {
createVolumeBackupSchema, createVolumeBackupSchema,
updateVolumeBackupSchema, updateVolumeBackupSchema,
volumeBackups, volumeBackups,
} from "@dokploy/server/db/schema"; } from "@dokploy/server/db/schema";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { db } from "@dokploy/server/db";
import { eq } from "drizzle-orm";
import { observable } from "@trpc/server/observable";
import { import {
execAsyncRemote, execAsyncRemote,
execAsyncStream, execAsyncStream,
} from "@dokploy/server/utils/process/execAsync"; } from "@dokploy/server/utils/process/execAsync";
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const volumeBackupsRouter = createTRPCRouter({ export const volumeBackupsRouter = createTRPCRouter({
list: protectedProcedure list: protectedProcedure

View File

@@ -98,6 +98,7 @@ export const deploymentWorker = new Worker(
titleLog: job.data.titleLog, titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog, descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId, previewDeploymentId: job.data.previewDeploymentId,
isExternal: job.data.isExternal,
}); });
} }
} else { } else {
@@ -107,6 +108,7 @@ export const deploymentWorker = new Worker(
titleLog: job.data.titleLog, titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog, descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId, previewDeploymentId: job.data.previewDeploymentId,
isExternal: job.data.isExternal,
}); });
} }
} }

View File

@@ -26,6 +26,7 @@ type DeployJob =
applicationType: "application-preview"; applicationType: "application-preview";
previewDeploymentId: string; previewDeploymentId: string;
serverId?: string; serverId?: string;
isExternal?: boolean;
}; };
export type DeploymentJob = DeployJob; export type DeploymentJob = DeployJob;

View File

@@ -55,7 +55,7 @@ export const setupDockerContainerTerminalWebSocketServer = (
conn conn
.once("ready", () => { .once("ready", () => {
conn.exec( conn.exec(
`docker exec -it -w / ${containerId} ${activeWay}`, `docker exec -it ${containerId} ${activeWay}`,
{ pty: true }, { pty: true },
(err, stream) => { (err, stream) => {
if (err) throw err; if (err) throw err;
@@ -107,7 +107,7 @@ export const setupDockerContainerTerminalWebSocketServer = (
const shell = getShell(); const shell = getShell();
const ptyProcess = spawn( const ptyProcess = spawn(
shell, shell,
["-c", `docker exec -it -w / ${containerId} ${activeWay}`], ["-c", `docker exec -it ${containerId} ${activeWay}`],
{}, {},
); );

View File

@@ -1,4 +1,10 @@
import { execAsync } from "@dokploy/server"; import {
createDefaultMiddlewares,
createDefaultServerTraefikConfig,
createDefaultTraefikConfig,
initializeTraefik,
} from "@dokploy/server/setup/traefik-setup";
import { setupDirectories } from "@dokploy/server/setup/config-paths"; import { setupDirectories } from "@dokploy/server/setup/config-paths";
import { initializePostgres } from "@dokploy/server/setup/postgres-setup"; import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
import { initializeRedis } from "@dokploy/server/setup/redis-setup"; import { initializeRedis } from "@dokploy/server/setup/redis-setup";
@@ -6,13 +12,7 @@ import {
initializeNetwork, initializeNetwork,
initializeSwarm, initializeSwarm,
} from "@dokploy/server/setup/setup"; } from "@dokploy/server/setup/setup";
import { import { execAsync } from "@dokploy/server";
createDefaultMiddlewares,
createDefaultServerTraefikConfig,
createDefaultTraefikConfig,
initializeStandaloneTraefik,
} from "@dokploy/server/setup/traefik-setup";
(async () => { (async () => {
try { try {
setupDirectories(); setupDirectories();
@@ -22,7 +22,7 @@ import {
createDefaultTraefikConfig(); createDefaultTraefikConfig();
createDefaultServerTraefikConfig(); createDefaultServerTraefikConfig();
await execAsync("docker pull traefik:v3.1.2"); await execAsync("docker pull traefik:v3.1.2");
await initializeStandaloneTraefik(); await initializeTraefik();
await initializeRedis(); await initializeRedis();
await initializePostgres(); await initializePostgres();
} catch (e) { } catch (e) {

View File

@@ -13,7 +13,10 @@ declare global {
baseDomain?: string; baseDomain?: string;
}; };
chatwootSDK?: { chatwootSDK?: {
run: (config: { websiteToken: string; baseUrl: string }) => void; run: (config: {
websiteToken: string;
baseUrl: string;
}) => void;
}; };
$chatwoot?: { $chatwoot?: {
setUser: ( setUser: (

View File

@@ -29,9 +29,5 @@
"tsx": "^4.16.2", "tsx": "^4.16.2",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"packageManager": "pnpm@9.12.0", "packageManager": "pnpm@9.5.0"
"engines": {
"node": "^20.16.0",
"pnpm": ">=9.12.0"
}
} }

View File

@@ -1,17 +1,18 @@
{ {
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"files": { "files": {
"includes": [ "ignore": [
"**", "node_modules/**",
"!**/.docker", ".next/**",
"!**/.next/**", "drizzle/**",
"!**/dist", ".docker",
"!**/drizzle/**", "dist",
"!node_modules/**", "packages/server/package.json"
"!packages/server/package.json"
] ]
}, },
"assist": { "actions": { "source": { "organizeImports": "on" } } }, "organizeImports": {
"enabled": true
},
"linter": { "linter": {
"rules": { "rules": {
"security": { "security": {
@@ -19,29 +20,17 @@
}, },
"complexity": { "complexity": {
"noUselessCatch": "off", "noUselessCatch": "off",
"noBannedTypes": "off", "noBannedTypes": "off"
"noUselessFragments": "off"
}, },
"correctness": { "correctness": {
"useExhaustiveDependencies": "off", "useExhaustiveDependencies": "off",
"noUnsafeOptionalChaining": "off", "noUnsafeOptionalChaining": "off",
"noUnusedImports": "error", "noUnusedImports": "error",
"noUnusedFunctionParameters": "error", "noUnusedFunctionParameters": "error",
"noUnusedVariables": "error", "noUnusedVariables": "error"
"useHookAtTopLevel": "off"
}, },
"style": { "style": {
"noNonNullAssertion": "off", "noNonNullAssertion": "off"
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"useDefaultParameterLast": "error",
"useEnumInitializers": "error",
"useSelfClosingElements": "error",
"useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error",
"noInferrableTypes": "error",
"noUselessElse": "error"
}, },
"suspicious": { "suspicious": {
"noArrayIndexKey": "off", "noArrayIndexKey": "off",

View File

@@ -1,10 +1,7 @@
{ {
"name": "dokploy", "name": "dokploy",
"private": true, "private": true,
"workspaces": [ "workspaces": ["apps/*", "packages/*"],
"apps/*",
"packages/*"
],
"scripts": { "scripts": {
"dokploy:setup": "pnpm --filter=dokploy run setup", "dokploy:setup": "pnpm --filter=dokploy run setup",
"dokploy:dev": "pnpm --filter=dokploy run dev", "dokploy:dev": "pnpm --filter=dokploy run dev",
@@ -23,7 +20,7 @@
"format-and-lint:fix": "biome check . --write" "format-and-lint:fix": "biome check . --write"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.1.1", "@biomejs/biome": "1.9.4",
"@commitlint/cli": "^19.8.1", "@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1", "@commitlint/config-conventional": "^19.8.1",
"@types/node": "^18.19.104", "@types/node": "^18.19.104",
@@ -33,10 +30,10 @@
"lint-staged": "^15.5.2", "lint-staged": "^15.5.2",
"tsx": "4.16.2" "tsx": "4.16.2"
}, },
"packageManager": "pnpm@9.12.0", "packageManager": "pnpm@9.5.0",
"engines": { "engines": {
"node": "^20.16.0", "node": "^20.16.0",
"pnpm": ">=9.12.0" "pnpm": ">=9.5.0"
}, },
"lint-staged": { "lint-staged": {
"*": [ "*": [
@@ -44,9 +41,7 @@
] ]
}, },
"commitlint": { "commitlint": {
"extends": [ "extends": ["@commitlint/config-conventional"]
"@commitlint/config-conventional"
]
}, },
"resolutions": { "resolutions": {
"@types/react": "18.3.5", "@types/react": "18.3.5",

View File

@@ -105,10 +105,5 @@
"tsc-alias": "1.8.10", "tsc-alias": "1.8.10",
"tsx": "^4.16.2", "tsx": "^4.16.2",
"typescript": "^5.8.3" "typescript": "^5.8.3"
},
"packageManager": "pnpm@9.12.0",
"engines": {
"node": "^20.16.0",
"pnpm": ">=9.12.0"
} }
} }

View File

@@ -131,10 +131,6 @@ export const applications = pgTable("application", {
isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default( isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default(
false, false,
), ),
// Security: Require collaborator permissions for preview deployments
previewRequireCollaboratorPermissions: boolean(
"previewRequireCollaboratorPermissions",
).default(true),
rollbackActive: boolean("rollbackActive").default(false), rollbackActive: boolean("rollbackActive").default(false),
buildArgs: text("buildArgs"), buildArgs: text("buildArgs"),
memoryReservation: text("memoryReservation"), memoryReservation: text("memoryReservation"),
@@ -208,7 +204,6 @@ export const applications = pgTable("application", {
.notNull() .notNull()
.default("idle"), .default("idle"),
buildType: buildType("buildType").notNull().default("nixpacks"), buildType: buildType("buildType").notNull().default("nixpacks"),
railpackVersion: text("railpackVersion").default("0.2.2"),
herokuVersion: text("herokuVersion").default("24"), herokuVersion: text("herokuVersion").default("24"),
publishDirectory: text("publishDirectory"), publishDirectory: text("publishDirectory"),
isStaticSpa: boolean("isStaticSpa"), isStaticSpa: boolean("isStaticSpa"),
@@ -413,7 +408,6 @@ const createSchema = createInsertSchema(applications, {
"static", "static",
"railpack", "railpack",
]), ]),
railpackVersion: z.string().optional(),
herokuVersion: z.string().optional(), herokuVersion: z.string().optional(),
publishDirectory: z.string().optional(), publishDirectory: z.string().optional(),
isStaticSpa: z.boolean().optional(), isStaticSpa: z.boolean().optional(),
@@ -434,7 +428,6 @@ const createSchema = createInsertSchema(applications, {
previewHttps: z.boolean().optional(), previewHttps: z.boolean().optional(),
previewPath: z.string().optional(), previewPath: z.string().optional(),
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(), previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
previewRequireCollaboratorPermissions: z.boolean().optional(),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
cleanCache: z.boolean().optional(), cleanCache: z.boolean().optional(),
}); });
@@ -468,7 +461,6 @@ export const apiSaveBuildType = createSchema
dockerContextPath: true, dockerContextPath: true,
dockerBuildStage: true, dockerBuildStage: true,
herokuVersion: true, herokuVersion: true,
railpackVersion: true,
}) })
.required() .required()
.merge(createSchema.pick({ publishDirectory: true, isStaticSpa: true })); .merge(createSchema.pick({ publishDirectory: true, isStaticSpa: true }));

View File

@@ -13,9 +13,9 @@ import { applications } from "./application";
import { backups } from "./backups"; import { backups } from "./backups";
import { compose } from "./compose"; import { compose } from "./compose";
import { previewDeployments } from "./preview-deployments"; import { previewDeployments } from "./preview-deployments";
import { rollbacks } from "./rollbacks";
import { schedules } from "./schedule"; import { schedules } from "./schedule";
import { server } from "./server"; import { server } from "./server";
import { rollbacks } from "./rollbacks";
import { volumeBackups } from "./volume-backups"; import { volumeBackups } from "./volume-backups";
export const deploymentStatus = pgEnum("deploymentStatus", [ export const deploymentStatus = pgEnum("deploymentStatus", [
"running", "running",

View File

@@ -1,14 +1,14 @@
import type { Application } from "@dokploy/server/services/application";
import type { Mount } from "@dokploy/server/services/mount";
import type { Port } from "@dokploy/server/services/port";
import type { Project } from "@dokploy/server/services/project";
import type { Registry } from "@dokploy/server/services/registry";
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { jsonb, pgTable, serial, text } from "drizzle-orm/pg-core"; import { jsonb, pgTable, serial, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { deployments } from "./deployment"; import { deployments } from "./deployment";
import type { Application } from "@dokploy/server/services/application";
import type { Project } from "@dokploy/server/services/project";
import type { Mount } from "@dokploy/server/services/mount";
import type { Port } from "@dokploy/server/services/port";
import type { Registry } from "@dokploy/server/services/registry";
export const rollbacks = pgTable("rollback", { export const rollbacks = pgTable("rollback", {
rollbackId: text("rollbackId") rollbackId: text("rollbackId")

View File

@@ -1,4 +1,3 @@
import { paths } from "@dokploy/server/constants";
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { import {
boolean, boolean,
@@ -16,6 +15,7 @@ import { backups } from "./backups";
import { projects } from "./project"; import { projects } from "./project";
import { schedules } from "./schedule"; import { schedules } from "./schedule";
import { certificateType } from "./shared"; import { certificateType } from "./shared";
import { paths } from "@dokploy/server/constants";
/** /**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects. * database instance for multiple projects.
@@ -323,7 +323,6 @@ export const apiUpdateWebServerMonitoring = z.object({
export const apiUpdateUser = createSchema.partial().extend({ export const apiUpdateUser = createSchema.partial().extend({
password: z.string().optional(), password: z.string().optional(),
currentPassword: z.string().optional(), currentPassword: z.string().optional(),
name: z.string().optional(),
metricsConfig: z metricsConfig: z
.object({ .object({
server: z.object({ server: z.object({

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