mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-07 06:55:23 +02:00
Compare commits
154 Commits
2326-add-s
...
v0.26.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd4964f70f | ||
|
|
07bf520e9b | ||
|
|
c42e859215 | ||
|
|
e666cfb374 | ||
|
|
1d9b9ff9b6 | ||
|
|
6c61919202 | ||
|
|
a9a42d2066 | ||
|
|
0f6ac310b5 | ||
|
|
c267faef08 | ||
|
|
d2be0855c1 | ||
|
|
c9aaee149a | ||
|
|
d435553839 | ||
|
|
28f40066a2 | ||
|
|
22e6a06426 | ||
|
|
94faf78f16 | ||
|
|
c4351482fa | ||
|
|
5412c5a873 | ||
|
|
212006ba9e | ||
|
|
18d12d1a6f | ||
|
|
5d5af8f57f | ||
|
|
7f8f97c48f | ||
|
|
67865c5283 | ||
|
|
817264eae4 | ||
|
|
5360df7a53 | ||
|
|
fec4daa59b | ||
|
|
aae7906e77 | ||
|
|
86d14465cb | ||
|
|
f84c659121 | ||
|
|
89cb9c24c9 | ||
|
|
c7fcea7d6a | ||
|
|
d4555e6985 | ||
|
|
daa54cea8d | ||
|
|
77aff700fd | ||
|
|
cdb0de9a72 | ||
|
|
a5353e5457 | ||
|
|
a9b8beb50b | ||
|
|
6022f2f6a3 | ||
|
|
075e387bb6 | ||
|
|
568293ef3c | ||
|
|
a9ae39dc94 | ||
|
|
55a9640e31 | ||
|
|
32d5959733 | ||
|
|
bccd531457 | ||
|
|
f5de5130f3 | ||
|
|
bd751658be | ||
|
|
9b23aa9c8c | ||
|
|
dbc1396fa6 | ||
|
|
4210eefd37 | ||
|
|
91050ce3a5 | ||
|
|
9394d97163 | ||
|
|
f91a3aab25 | ||
|
|
9fbd0dce9a | ||
|
|
9e405c0728 | ||
|
|
44892404c1 | ||
|
|
1362fdd4b4 | ||
|
|
c7c3b1018b | ||
|
|
0d9b72e00a | ||
|
|
80ed041420 | ||
|
|
ba9c2ef369 | ||
|
|
8bd4f403c4 | ||
|
|
7ea7ee739f | ||
|
|
4873baa975 | ||
|
|
287dfb5402 | ||
|
|
439fba1f4b | ||
|
|
1ba24630a8 | ||
|
|
de6c1a7981 | ||
|
|
7948721f5a | ||
|
|
99cb80757c | ||
|
|
7467ada3a9 | ||
|
|
f0f2188652 | ||
|
|
2c1dfe9377 | ||
|
|
9e8efab909 | ||
|
|
35612b21a0 | ||
|
|
7873af1c39 | ||
|
|
ade727e2ed | ||
|
|
ac1fb6fb86 | ||
|
|
b3168f75d0 | ||
|
|
0e7b550642 | ||
|
|
3e1030edda | ||
|
|
3ea90de4e1 | ||
|
|
bccef0da4c | ||
|
|
dc28ddba2a | ||
|
|
ed312dc1c0 | ||
|
|
6cafb15dbb | ||
|
|
c34fdf7a46 | ||
|
|
e627c9af99 | ||
|
|
18e609313b | ||
|
|
fbf840bf6e | ||
|
|
76613de095 | ||
|
|
5a7f55ea63 | ||
|
|
be3403af0c | ||
|
|
f190cc548c | ||
|
|
4df9b935a8 | ||
|
|
b9becbafd8 | ||
|
|
60be376a4f | ||
|
|
ef9732d5d9 | ||
|
|
d35307ead6 | ||
|
|
c98db390dc | ||
|
|
0c7265c9c9 | ||
|
|
f1ef1d8489 | ||
|
|
fbd095334c | ||
|
|
e3832eff07 | ||
|
|
25b7069e31 | ||
|
|
0fbb063d06 | ||
|
|
56d4e61c1f | ||
|
|
7ce36a50e8 | ||
|
|
4f5b557a60 | ||
|
|
d09163a24e | ||
|
|
e1d8505757 | ||
|
|
b6de55c4d9 | ||
|
|
e22d503182 | ||
|
|
32631e957a | ||
|
|
79d3c1d7f3 | ||
|
|
9213061c26 | ||
|
|
085ef35b46 | ||
|
|
c9f356e314 | ||
|
|
4f691d27b2 | ||
|
|
3c70db9fc8 | ||
|
|
5890b321b2 | ||
|
|
d02913d69e | ||
|
|
c459997453 | ||
|
|
1c0673b327 | ||
|
|
334d9c91ef | ||
|
|
615d89ee0c | ||
|
|
0c24507872 | ||
|
|
d2cd01aff7 | ||
|
|
6349cabf27 | ||
|
|
94536ab05a | ||
|
|
8e5be8dbcb | ||
|
|
046606e496 | ||
|
|
e9cf1f4caa | ||
|
|
ee0a299343 | ||
|
|
1b77c8029b | ||
|
|
48e4fd3ddf | ||
|
|
276f870e74 | ||
|
|
6e86fafa5e | ||
|
|
008788a38a | ||
|
|
b4a14e6e76 | ||
|
|
e64ee98d99 | ||
|
|
95d0da25a0 | ||
|
|
0cc8c02359 | ||
|
|
7f3fe52b53 | ||
|
|
b5bc384664 | ||
|
|
39d0b9649f | ||
|
|
1ce880bd6d | ||
|
|
8a8ed58fef | ||
|
|
95714c1749 | ||
|
|
a181b7b8b8 | ||
|
|
0e2f1e2832 | ||
|
|
2ec495b2f2 | ||
|
|
178ccb3f45 | ||
|
|
a47a5f3b9e | ||
|
|
95bf60ac75 | ||
|
|
544408886e |
@@ -16,11 +16,11 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server
|
|||||||
|
|
||||||
|
|
||||||
# Deploy only the dokploy app
|
# Deploy only the dokploy app
|
||||||
ARG NEXT_PUBLIC_UMAMI_HOST
|
# ARG NEXT_PUBLIC_UMAMI_HOST
|
||||||
ENV NEXT_PUBLIC_UMAMI_HOST=$NEXT_PUBLIC_UMAMI_HOST
|
# ENV NEXT_PUBLIC_UMAMI_HOST=$NEXT_PUBLIC_UMAMI_HOST
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
# ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||||
ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
# ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||||
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||||
|
|||||||
215
apps/dokploy/__test__/compose/domain/host-rule-format.test.ts
Normal file
215
apps/dokploy/__test__/compose/domain/host-rule-format.test.ts
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import type { Domain } from "@dokploy/server";
|
||||||
|
import { createDomainLabels } from "@dokploy/server";
|
||||||
|
import { parse, stringify } from "yaml";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression tests for Traefik Host rule label format.
|
||||||
|
*
|
||||||
|
* These tests verify that the Host rule is generated with the correct format:
|
||||||
|
* - Host(`domain.com`) - with opening and closing parentheses
|
||||||
|
* - Host(`domain.com`) && PathPrefix(`/path`) - for path-based routing
|
||||||
|
*
|
||||||
|
* Issue: https://github.com/Dokploy/dokploy/issues/3161
|
||||||
|
* The bug caused Host rules to be malformed as Host`domain.com`)
|
||||||
|
* (missing opening parenthesis) which broke all domain routing.
|
||||||
|
*/
|
||||||
|
describe("Host rule format regression tests", () => {
|
||||||
|
const baseDomain: Domain = {
|
||||||
|
host: "example.com",
|
||||||
|
port: 8080,
|
||||||
|
https: false,
|
||||||
|
uniqueConfigKey: 1,
|
||||||
|
customCertResolver: null,
|
||||||
|
certificateType: "none",
|
||||||
|
applicationId: "",
|
||||||
|
composeId: "",
|
||||||
|
domainType: "compose",
|
||||||
|
serviceName: "test-app",
|
||||||
|
domainId: "",
|
||||||
|
path: "/",
|
||||||
|
createdAt: "",
|
||||||
|
previewDeploymentId: "",
|
||||||
|
internalPath: "/",
|
||||||
|
stripPath: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Host rule format validation", () => {
|
||||||
|
it("should generate Host rule with correct parentheses format", async () => {
|
||||||
|
const labels = await createDomainLabels("test-app", baseDomain, "web");
|
||||||
|
const ruleLabel = labels.find((l) => l.includes(".rule="));
|
||||||
|
|
||||||
|
expect(ruleLabel).toBeDefined();
|
||||||
|
// Verify exact format: Host(`domain`)
|
||||||
|
expect(ruleLabel).toMatch(/Host\(`[^`]+`\)/);
|
||||||
|
// Ensure opening parenthesis is present after Host
|
||||||
|
expect(ruleLabel).toContain("Host(`example.com`)");
|
||||||
|
// Ensure it does NOT have the malformed format
|
||||||
|
expect(ruleLabel).not.toMatch(/Host`[^`]+`\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate PathPrefix with correct parentheses format", async () => {
|
||||||
|
const labels = await createDomainLabels(
|
||||||
|
"test-app",
|
||||||
|
{ ...baseDomain, path: "/api" },
|
||||||
|
"web",
|
||||||
|
);
|
||||||
|
const ruleLabel = labels.find((l) => l.includes(".rule="));
|
||||||
|
|
||||||
|
expect(ruleLabel).toBeDefined();
|
||||||
|
// Verify PathPrefix format
|
||||||
|
expect(ruleLabel).toMatch(/PathPrefix\(`[^`]+`\)/);
|
||||||
|
expect(ruleLabel).toContain("PathPrefix(`/api`)");
|
||||||
|
// Ensure opening parenthesis is present
|
||||||
|
expect(ruleLabel).not.toMatch(/PathPrefix`[^`]+`\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate combined Host and PathPrefix with correct format", async () => {
|
||||||
|
const labels = await createDomainLabels(
|
||||||
|
"test-app",
|
||||||
|
{ ...baseDomain, path: "/api/v1" },
|
||||||
|
"websecure",
|
||||||
|
);
|
||||||
|
const ruleLabel = labels.find((l) => l.includes(".rule="));
|
||||||
|
|
||||||
|
expect(ruleLabel).toBeDefined();
|
||||||
|
expect(ruleLabel).toBe(
|
||||||
|
"traefik.http.routers.test-app-1-websecure.rule=Host(`example.com`) && PathPrefix(`/api/v1`)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("YAML serialization preserves Host rule format", () => {
|
||||||
|
it("should preserve Host rule format through YAML stringify/parse", async () => {
|
||||||
|
const labels = await createDomainLabels("test-app", baseDomain, "web");
|
||||||
|
const ruleLabel = labels.find((l) => l.includes(".rule="));
|
||||||
|
|
||||||
|
// Simulate compose file structure
|
||||||
|
const composeSpec = {
|
||||||
|
services: {
|
||||||
|
myapp: {
|
||||||
|
image: "nginx",
|
||||||
|
labels: labels,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stringify to YAML
|
||||||
|
const yamlOutput = stringify(composeSpec, { lineWidth: 1000 });
|
||||||
|
|
||||||
|
// Parse back
|
||||||
|
const parsed = parse(yamlOutput) as typeof composeSpec;
|
||||||
|
const parsedRuleLabel = parsed.services.myapp.labels.find((l: string) =>
|
||||||
|
l.includes(".rule="),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify format is preserved
|
||||||
|
expect(parsedRuleLabel).toBe(ruleLabel);
|
||||||
|
expect(parsedRuleLabel).toContain("Host(`example.com`)");
|
||||||
|
expect(parsedRuleLabel).not.toMatch(/Host`[^`]+`\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve complex rule format through YAML serialization", async () => {
|
||||||
|
const labels = await createDomainLabels(
|
||||||
|
"test-app",
|
||||||
|
{ ...baseDomain, path: "/api", https: true },
|
||||||
|
"websecure",
|
||||||
|
);
|
||||||
|
|
||||||
|
const composeSpec = {
|
||||||
|
services: {
|
||||||
|
myapp: {
|
||||||
|
labels: labels,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const yamlOutput = stringify(composeSpec, { lineWidth: 1000 });
|
||||||
|
const parsed = parse(yamlOutput) as typeof composeSpec;
|
||||||
|
const parsedRuleLabel = parsed.services.myapp.labels.find((l: string) =>
|
||||||
|
l.includes(".rule="),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parsedRuleLabel).toContain(
|
||||||
|
"Host(`example.com`) && PathPrefix(`/api`)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Edge cases for domain names", () => {
|
||||||
|
const domainCases = [
|
||||||
|
{ name: "simple domain", host: "example.com" },
|
||||||
|
{ name: "subdomain", host: "app.example.com" },
|
||||||
|
{ name: "deep subdomain", host: "api.v1.app.example.com" },
|
||||||
|
{ name: "numeric domain", host: "123.example.com" },
|
||||||
|
{ name: "hyphenated domain", host: "my-app.example-host.com" },
|
||||||
|
{ name: "localhost", host: "localhost" },
|
||||||
|
{ name: "IP address style", host: "192.168.1.100" },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { name, host } of domainCases) {
|
||||||
|
it(`should generate correct Host rule for ${name}: ${host}`, async () => {
|
||||||
|
const labels = await createDomainLabels(
|
||||||
|
"test-app",
|
||||||
|
{ ...baseDomain, host },
|
||||||
|
"web",
|
||||||
|
);
|
||||||
|
const ruleLabel = labels.find((l) => l.includes(".rule="));
|
||||||
|
|
||||||
|
expect(ruleLabel).toBeDefined();
|
||||||
|
expect(ruleLabel).toContain(`Host(\`${host}\`)`);
|
||||||
|
// Verify parenthesis is present
|
||||||
|
expect(ruleLabel).toMatch(
|
||||||
|
new RegExp(`Host\\(\\\`${host.replace(/\./g, "\\.")}\\\`\\)`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Multiple domains scenario", () => {
|
||||||
|
it("should generate correct format for both web and websecure entrypoints", async () => {
|
||||||
|
const webLabels = await createDomainLabels("test-app", baseDomain, "web");
|
||||||
|
const websecureLabels = await createDomainLabels(
|
||||||
|
"test-app",
|
||||||
|
baseDomain,
|
||||||
|
"websecure",
|
||||||
|
);
|
||||||
|
|
||||||
|
const webRule = webLabels.find((l) => l.includes(".rule="));
|
||||||
|
const websecureRule = websecureLabels.find((l) => l.includes(".rule="));
|
||||||
|
|
||||||
|
// Both should have correct format
|
||||||
|
expect(webRule).toContain("Host(`example.com`)");
|
||||||
|
expect(websecureRule).toContain("Host(`example.com`)");
|
||||||
|
|
||||||
|
// Neither should have malformed format
|
||||||
|
expect(webRule).not.toMatch(/Host`[^`]+`\)/);
|
||||||
|
expect(websecureRule).not.toMatch(/Host`[^`]+`\)/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Special characters in paths", () => {
|
||||||
|
const pathCases = [
|
||||||
|
{ name: "simple path", path: "/api" },
|
||||||
|
{ name: "nested path", path: "/api/v1/users" },
|
||||||
|
{ name: "path with hyphen", path: "/api-v1" },
|
||||||
|
{ name: "path with underscore", path: "/api_v1" },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { name, path } of pathCases) {
|
||||||
|
it(`should generate correct PathPrefix for ${name}: ${path}`, async () => {
|
||||||
|
const labels = await createDomainLabels(
|
||||||
|
"test-app",
|
||||||
|
{ ...baseDomain, path },
|
||||||
|
"web",
|
||||||
|
);
|
||||||
|
const ruleLabel = labels.find((l) => l.includes(".rule="));
|
||||||
|
|
||||||
|
expect(ruleLabel).toBeDefined();
|
||||||
|
expect(ruleLabel).toContain(`PathPrefix(\`${path}\`)`);
|
||||||
|
// Verify parenthesis is present
|
||||||
|
expect(ruleLabel).not.toMatch(/PathPrefix`[^`]+`\)/);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -189,7 +189,7 @@ describe("deployApplication - Command Generation Tests", () => {
|
|||||||
|
|
||||||
it("should verify nixpacks command is called with correct app", async () => {
|
it("should verify nixpacks command is called with correct app", async () => {
|
||||||
const mockNixpacksCommand = "nixpacks build /path/to/app --name test-app";
|
const mockNixpacksCommand = "nixpacks build /path/to/app --name test-app";
|
||||||
vi.mocked(builders.getBuildCommand).mockReturnValue(mockNixpacksCommand);
|
vi.mocked(builders.getBuildCommand).mockResolvedValue(mockNixpacksCommand);
|
||||||
|
|
||||||
await deployApplication({
|
await deployApplication({
|
||||||
applicationId: "test-app-id",
|
applicationId: "test-app-id",
|
||||||
@@ -220,7 +220,7 @@ describe("deployApplication - Command Generation Tests", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const mockRailpackCommand = "railpack prepare /path/to/app";
|
const mockRailpackCommand = "railpack prepare /path/to/app";
|
||||||
vi.mocked(builders.getBuildCommand).mockReturnValue(mockRailpackCommand);
|
vi.mocked(builders.getBuildCommand).mockResolvedValue(mockRailpackCommand);
|
||||||
|
|
||||||
await deployApplication({
|
await deployApplication({
|
||||||
applicationId: "test-app-id",
|
applicationId: "test-app-id",
|
||||||
@@ -241,7 +241,7 @@ describe("deployApplication - Command Generation Tests", () => {
|
|||||||
|
|
||||||
it("should execute commands in correct order", async () => {
|
it("should execute commands in correct order", async () => {
|
||||||
const mockNixpacksCommand = "nixpacks build";
|
const mockNixpacksCommand = "nixpacks build";
|
||||||
vi.mocked(builders.getBuildCommand).mockReturnValue(mockNixpacksCommand);
|
vi.mocked(builders.getBuildCommand).mockResolvedValue(mockNixpacksCommand);
|
||||||
|
|
||||||
await deployApplication({
|
await deployApplication({
|
||||||
applicationId: "test-app-id",
|
applicationId: "test-app-id",
|
||||||
@@ -260,7 +260,7 @@ describe("deployApplication - Command Generation Tests", () => {
|
|||||||
|
|
||||||
it("should include log redirection in command", async () => {
|
it("should include log redirection in command", async () => {
|
||||||
const mockCommand = "nixpacks build";
|
const mockCommand = "nixpacks build";
|
||||||
vi.mocked(builders.getBuildCommand).mockReturnValue(mockCommand);
|
vi.mocked(builders.getBuildCommand).mockResolvedValue(mockCommand);
|
||||||
|
|
||||||
await deployApplication({
|
await deployApplication({
|
||||||
applicationId: "test-app-id",
|
applicationId: "test-app-id",
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ const baseApp: ApplicationNested = {
|
|||||||
giteaRepository: "",
|
giteaRepository: "",
|
||||||
cleanCache: false,
|
cleanCache: false,
|
||||||
watchPaths: [],
|
watchPaths: [],
|
||||||
|
rollbackRegistryId: "",
|
||||||
|
rollbackRegistry: null,
|
||||||
|
deployments: [],
|
||||||
enableSubmodules: false,
|
enableSubmodules: false,
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
triggerType: "push",
|
triggerType: "push",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import type { ApplicationNested } from "@dokploy/server/utils/builders";
|
import type { ApplicationNested } from "@dokploy/server/utils/builders";
|
||||||
import { mechanizeDockerContainer } from "@dokploy/server/utils/builders";
|
import { mechanizeDockerContainer } from "@dokploy/server/utils/builders";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
type MockCreateServiceOptions = {
|
type MockCreateServiceOptions = {
|
||||||
TaskTemplate?: {
|
TaskTemplate?: {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ const baseAdmin: User = {
|
|||||||
enablePaidFeatures: false,
|
enablePaidFeatures: false,
|
||||||
allowImpersonation: false,
|
allowImpersonation: false,
|
||||||
role: "user",
|
role: "user",
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
metricsConfig: {
|
metricsConfig: {
|
||||||
containers: {
|
containers: {
|
||||||
refreshRate: 20,
|
refreshRate: 20,
|
||||||
@@ -61,7 +63,6 @@ const baseAdmin: User = {
|
|||||||
expirationDate: "",
|
expirationDate: "",
|
||||||
id: "",
|
id: "",
|
||||||
isRegistered: false,
|
isRegistered: false,
|
||||||
name: "",
|
|
||||||
createdAt2: new Date().toISOString(),
|
createdAt2: new Date().toISOString(),
|
||||||
emailVerified: false,
|
emailVerified: false,
|
||||||
image: "",
|
image: "",
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ const baseApp: ApplicationNested = {
|
|||||||
giteaBuildPath: "",
|
giteaBuildPath: "",
|
||||||
giteaId: "",
|
giteaId: "",
|
||||||
args: [],
|
args: [],
|
||||||
|
rollbackRegistryId: "",
|
||||||
|
rollbackRegistry: null,
|
||||||
|
deployments: [],
|
||||||
cleanCache: false,
|
cleanCache: false,
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
endpointSpecSwarm: null,
|
endpointSpecSwarm: null,
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export const ShowDeployments = ({
|
|||||||
See the last 10 deployments for this {type}
|
See the last 10 deployments for this {type}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center flex-wrap gap-2">
|
||||||
{(type === "application" || type === "compose") && (
|
{(type === "application" || type === "compose") && (
|
||||||
<KillBuild id={id} type={type} />
|
<KillBuild id={id} type={type} />
|
||||||
)}
|
)}
|
||||||
@@ -373,7 +373,19 @@ export const ShowDeployments = ({
|
|||||||
type === "application" && (
|
type === "application" && (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Rollback to this deployment"
|
title="Rollback to this deployment"
|
||||||
description="Are you sure you want to rollback to this deployment?"
|
description={
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
Are you sure you want to rollback to this
|
||||||
|
deployment?
|
||||||
|
</p>
|
||||||
|
<AlertBlock type="info" className="text-sm">
|
||||||
|
Please wait a few seconds while the image is
|
||||||
|
pulled from the registry. Your application
|
||||||
|
should be running shortly.
|
||||||
|
</AlertBlock>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await rollback({
|
await rollback({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useState } from "react";
|
import Link from "next/link";
|
||||||
|
import { useEffect, 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";
|
||||||
@@ -20,13 +21,37 @@ import {
|
|||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z
|
||||||
rollbackActive: z.boolean(),
|
.object({
|
||||||
});
|
rollbackActive: z.boolean(),
|
||||||
|
rollbackRegistryId: z.string().optional(),
|
||||||
|
})
|
||||||
|
.superRefine((values, ctx) => {
|
||||||
|
if (
|
||||||
|
values.rollbackActive &&
|
||||||
|
(!values.rollbackRegistryId || values.rollbackRegistryId === "none")
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["rollbackRegistryId"],
|
||||||
|
message: "Registry is required when rollbacks are enabled",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
type FormValues = z.infer<typeof formSchema>;
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
@@ -49,17 +74,33 @@ export const ShowRollbackSettings = ({ applicationId, children }: Props) => {
|
|||||||
const { mutateAsync: updateApplication, isLoading } =
|
const { mutateAsync: updateApplication, isLoading } =
|
||||||
api.application.update.useMutation();
|
api.application.update.useMutation();
|
||||||
|
|
||||||
|
const { data: registries } = api.registry.all.useQuery();
|
||||||
|
|
||||||
const form = useForm<FormValues>({
|
const form = useForm<FormValues>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
rollbackActive: application?.rollbackActive ?? false,
|
rollbackActive: application?.rollbackActive ?? false,
|
||||||
|
rollbackRegistryId: application?.rollbackRegistryId || "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (application) {
|
||||||
|
form.reset({
|
||||||
|
rollbackActive: application.rollbackActive ?? false,
|
||||||
|
rollbackRegistryId: application.rollbackRegistryId || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [application, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: FormValues) => {
|
const onSubmit = async (data: FormValues) => {
|
||||||
await updateApplication({
|
await updateApplication({
|
||||||
applicationId,
|
applicationId,
|
||||||
rollbackActive: data.rollbackActive,
|
rollbackActive: data.rollbackActive,
|
||||||
|
rollbackRegistryId:
|
||||||
|
data.rollbackRegistryId === "none" || !data.rollbackRegistryId
|
||||||
|
? null
|
||||||
|
: data.rollbackRegistryId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Rollback settings updated");
|
toast.success("Rollback settings updated");
|
||||||
@@ -112,6 +153,65 @@ export const ShowRollbackSettings = ({ applicationId, children }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{form.watch("rollbackActive") && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="rollbackRegistryId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Rollback Registry</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
value={field.value || "none"}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a registry" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value="none">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span>None</span>
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
{registries?.map((registry) => (
|
||||||
|
<SelectItem
|
||||||
|
key={registry.registryId}
|
||||||
|
value={registry.registryId}
|
||||||
|
>
|
||||||
|
{registry.registryName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectLabel>
|
||||||
|
Registries ({registries?.length || 0})
|
||||||
|
</SelectLabel>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{!registries || registries.length === 0 ? (
|
||||||
|
<FormDescription className="text-amber-600 dark:text-amber-500">
|
||||||
|
No registries available. Please{" "}
|
||||||
|
<Link
|
||||||
|
href="/dashboard/settings/registry"
|
||||||
|
className="underline font-medium hover:text-amber-700 dark:hover:text-amber-400"
|
||||||
|
>
|
||||||
|
configure a registry
|
||||||
|
</Link>{" "}
|
||||||
|
first to enable rollbacks.
|
||||||
|
</FormDescription>
|
||||||
|
) : (
|
||||||
|
<FormDescription>
|
||||||
|
Select a registry where rollback images will be stored.
|
||||||
|
</FormDescription>
|
||||||
|
)}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button type="submit" className="w-full" isLoading={isLoading}>
|
<Button type="submit" className="w-full" isLoading={isLoading}>
|
||||||
Save Settings
|
Save Settings
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -60,6 +60,30 @@ export const commonCronExpressions = [
|
|||||||
{ label: "Custom", value: "custom" },
|
{ label: "Custom", value: "custom" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const commonTimezones = [
|
||||||
|
{ label: "UTC (Coordinated Universal Time)", value: "UTC" },
|
||||||
|
{ label: "America/New_York (Eastern Time)", value: "America/New_York" },
|
||||||
|
{ label: "America/Chicago (Central Time)", value: "America/Chicago" },
|
||||||
|
{ label: "America/Denver (Mountain Time)", value: "America/Denver" },
|
||||||
|
{ label: "America/Los_Angeles (Pacific Time)", value: "America/Los_Angeles" },
|
||||||
|
{
|
||||||
|
label: "America/Mexico_City (Central Mexico)",
|
||||||
|
value: "America/Mexico_City",
|
||||||
|
},
|
||||||
|
{ label: "America/Sao_Paulo (Brasilia Time)", value: "America/Sao_Paulo" },
|
||||||
|
{ label: "Europe/London (Greenwich Mean Time)", value: "Europe/London" },
|
||||||
|
{ label: "Europe/Paris (Central European Time)", value: "Europe/Paris" },
|
||||||
|
{ label: "Europe/Berlin (Central European Time)", value: "Europe/Berlin" },
|
||||||
|
{ label: "Asia/Tokyo (Japan Standard Time)", value: "Asia/Tokyo" },
|
||||||
|
{ label: "Asia/Shanghai (China Standard Time)", value: "Asia/Shanghai" },
|
||||||
|
{ label: "Asia/Dubai (Gulf Standard Time)", value: "Asia/Dubai" },
|
||||||
|
{ label: "Asia/Kolkata (India Standard Time)", value: "Asia/Kolkata" },
|
||||||
|
{
|
||||||
|
label: "Australia/Sydney (Australian Eastern Time)",
|
||||||
|
value: "Australia/Sydney",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const formSchema = z
|
const formSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
@@ -75,6 +99,7 @@ const formSchema = z
|
|||||||
"dokploy-server",
|
"dokploy-server",
|
||||||
]),
|
]),
|
||||||
script: z.string(),
|
script: z.string(),
|
||||||
|
timezone: z.string().optional(),
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
if (data.scheduleType === "compose" && !data.serviceName) {
|
if (data.scheduleType === "compose" && !data.serviceName) {
|
||||||
@@ -213,6 +238,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
|||||||
serviceName: "",
|
serviceName: "",
|
||||||
scheduleType: scheduleType || "application",
|
scheduleType: scheduleType || "application",
|
||||||
script: "",
|
script: "",
|
||||||
|
timezone: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -251,6 +277,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
|||||||
serviceName: schedule.serviceName || "",
|
serviceName: schedule.serviceName || "",
|
||||||
scheduleType: schedule.scheduleType,
|
scheduleType: schedule.scheduleType,
|
||||||
script: schedule.script || "",
|
script: schedule.script || "",
|
||||||
|
timezone: schedule.timezone || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, schedule, scheduleId]);
|
}, [form, schedule, scheduleId]);
|
||||||
@@ -464,6 +491,54 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
|||||||
formControl={form.control}
|
formControl={form.control}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="timezone"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center gap-2">
|
||||||
|
Timezone
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Select a timezone for the schedule. If not
|
||||||
|
specified, UTC will be used.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="UTC (default)" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{commonTimezones.map((tz) => (
|
||||||
|
<SelectItem key={tz.value} value={tz.value}>
|
||||||
|
{tz.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
Optional: Choose a timezone for the schedule execution time
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
{(scheduleTypeForm === "application" ||
|
{(scheduleTypeForm === "application" ||
|
||||||
scheduleTypeForm === "compose") && (
|
scheduleTypeForm === "compose") && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function parseLogs(logString: string): LogLine[] {
|
|||||||
// { timestamp: new Date("2024-12-10T10:00:00.000Z"),
|
// { timestamp: new Date("2024-12-10T10:00:00.000Z"),
|
||||||
// message: "The server is running on port 8080" }
|
// message: "The server is running on port 8080" }
|
||||||
const logRegex =
|
const logRegex =
|
||||||
/^(?:(\d+)\s+)?(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC)?\s*(.*)$/;
|
/^(?:(?<lineNumber>\d+)\s+)?(?<timestamp>(?:\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC))?\s*(?<message>[\s\S]*)$/;
|
||||||
|
|
||||||
return logString
|
return logString
|
||||||
.split("\n")
|
.split("\n")
|
||||||
@@ -59,7 +59,7 @@ export function parseLogs(logString: string): LogLine[] {
|
|||||||
const match = line.match(logRegex);
|
const match = line.match(logRegex);
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
|
|
||||||
const [, , timestamp, message] = match;
|
const { timestamp, message } = match.groups ?? {};
|
||||||
|
|
||||||
if (!message?.trim()) return null;
|
if (!message?.trim()) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export const ImpersonationBar = () => {
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
||||||
toast.success("Successfully impersonating user", {
|
toast.success("Successfully impersonating user", {
|
||||||
description: `You are now viewing as ${selectedUser.name || selectedUser.email}`,
|
description: `You are now viewing as ${`${selectedUser.name} ${selectedUser.lastName}`.trim() || selectedUser.email}`,
|
||||||
});
|
});
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -195,7 +195,8 @@ export const ImpersonationBar = () => {
|
|||||||
<UserIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
<UserIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
||||||
<span className="truncate flex flex-col items-start">
|
<span className="truncate flex flex-col items-start">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{selectedUser.name || ""}
|
{`${selectedUser.name} ${selectedUser.lastName}`.trim() ||
|
||||||
|
""}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{selectedUser.email}
|
{selectedUser.email}
|
||||||
@@ -242,7 +243,8 @@ export const ImpersonationBar = () => {
|
|||||||
<UserIcon className="h-4 w-4 flex-shrink-0" />
|
<UserIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
<span className="flex flex-col items-start">
|
<span className="flex flex-col items-start">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{user.name || ""}
|
{`${user.name} ${user.lastName}`.trim() ||
|
||||||
|
""}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{user.email} • {user.role}
|
{user.email} • {user.role}
|
||||||
@@ -283,10 +285,14 @@ export const ImpersonationBar = () => {
|
|||||||
<AvatarImage
|
<AvatarImage
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
src={data?.user?.image || ""}
|
src={data?.user?.image || ""}
|
||||||
alt={data?.user?.name || ""}
|
alt={
|
||||||
|
`${data?.user?.firstName} ${data?.user?.lastName}`.trim() ||
|
||||||
|
""
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
{data?.user?.name?.slice(0, 2).toUpperCase() || "U"}
|
{`${data?.user?.firstName?.[0] || ""}${data?.user?.lastName?.[0] || ""}`.toUpperCase() ||
|
||||||
|
"U"}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -299,7 +305,8 @@ export const ImpersonationBar = () => {
|
|||||||
Impersonating
|
Impersonating
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{data?.user?.name || ""}
|
{`${data?.user?.firstName} ${data?.user?.lastName}`.trim() ||
|
||||||
|
""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 text-sm text-muted-foreground flex-wrap">
|
<div className="flex items-center gap-3 text-sm text-muted-foreground flex-wrap">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { DockerNetworkChart } from "./docker-network-chart";
|
|||||||
|
|
||||||
const defaultData = {
|
const defaultData = {
|
||||||
cpu: {
|
cpu: {
|
||||||
value: 0,
|
value: "0%",
|
||||||
time: "",
|
time: "",
|
||||||
},
|
},
|
||||||
memory: {
|
memory: {
|
||||||
@@ -46,7 +46,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
export interface DockerStats {
|
export interface DockerStats {
|
||||||
cpu: {
|
cpu: {
|
||||||
value: number;
|
value: string;
|
||||||
time: string;
|
time: string;
|
||||||
};
|
};
|
||||||
memory: {
|
memory: {
|
||||||
@@ -220,7 +220,13 @@ export const ContainerFreeMonitoring = ({
|
|||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Used: {currentData.cpu.value}
|
Used: {currentData.cpu.value}
|
||||||
</span>
|
</span>
|
||||||
<Progress value={currentData.cpu.value} className="w-[100%]" />
|
<Progress
|
||||||
|
value={Number.parseInt(
|
||||||
|
currentData.cpu.value.replace("%", ""),
|
||||||
|
10,
|
||||||
|
)}
|
||||||
|
className="w-[100%]"
|
||||||
|
/>
|
||||||
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
|
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import type { findEnvironmentsByProjectId } from "@dokploy/server";
|
import type { findEnvironmentsByProjectId } from "@dokploy/server";
|
||||||
import {
|
import { ChevronDownIcon, PencilIcon, PlusIcon, TrashIcon } from "lucide-react";
|
||||||
ChevronDownIcon,
|
|
||||||
PencilIcon,
|
|
||||||
PlusIcon,
|
|
||||||
Terminal,
|
|
||||||
TrashIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { EnvironmentVariables } from "@/components/dashboard/project/environment-variables";
|
|
||||||
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 {
|
||||||
@@ -246,20 +239,6 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
{/* Action buttons for non-production environments */}
|
|
||||||
{/* <EnvironmentVariables environmentId={environment.environmentId}>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Terminal className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</EnvironmentVariables> */}
|
|
||||||
{environment.name !== "production" && (
|
{environment.name !== "production" && (
|
||||||
<div className="flex items-center gap-1 px-2">
|
<div className="flex items-center gap-1 px-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
@@ -54,16 +55,23 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { TimeBadge } from "@/components/ui/time-badge";
|
import { TimeBadge } from "@/components/ui/time-badge";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useDebounce } from "@/utils/hooks/use-debounce";
|
||||||
import { HandleProject } from "./handle-project";
|
import { HandleProject } from "./handle-project";
|
||||||
import { ProjectEnvironment } from "./project-environment";
|
import { ProjectEnvironment } from "./project-environment";
|
||||||
|
|
||||||
export const ShowProjects = () => {
|
export const ShowProjects = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const router = useRouter();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data, isLoading } = api.project.all.useQuery();
|
const { data, isLoading } = api.project.all.useQuery();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { mutateAsync } = api.project.remove.useMutation();
|
const { mutateAsync } = api.project.remove.useMutation();
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
|
||||||
|
const [searchQuery, setSearchQuery] = useState(
|
||||||
|
router.isReady && typeof router.query.q === "string" ? router.query.q : "",
|
||||||
|
);
|
||||||
|
const debouncedSearchQuery = useDebounce(searchQuery, 500);
|
||||||
|
|
||||||
const [sortBy, setSortBy] = useState<string>(() => {
|
const [sortBy, setSortBy] = useState<string>(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
return localStorage.getItem("projectsSort") || "createdAt-desc";
|
return localStorage.getItem("projectsSort") || "createdAt-desc";
|
||||||
@@ -75,14 +83,41 @@ export const ShowProjects = () => {
|
|||||||
localStorage.setItem("projectsSort", sortBy);
|
localStorage.setItem("projectsSort", sortBy);
|
||||||
}, [sortBy]);
|
}, [sortBy]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!router.isReady) return;
|
||||||
|
const urlQuery = typeof router.query.q === "string" ? router.query.q : "";
|
||||||
|
if (urlQuery !== searchQuery) {
|
||||||
|
setSearchQuery(urlQuery);
|
||||||
|
}
|
||||||
|
}, [router.isReady, router.query.q]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!router.isReady) return;
|
||||||
|
const urlQuery = typeof router.query.q === "string" ? router.query.q : "";
|
||||||
|
if (debouncedSearchQuery === urlQuery) return;
|
||||||
|
|
||||||
|
const newQuery = { ...router.query };
|
||||||
|
if (debouncedSearchQuery) {
|
||||||
|
newQuery.q = debouncedSearchQuery;
|
||||||
|
} else {
|
||||||
|
delete newQuery.q;
|
||||||
|
}
|
||||||
|
router.replace({ pathname: router.pathname, query: newQuery }, undefined, {
|
||||||
|
shallow: true,
|
||||||
|
});
|
||||||
|
}, [debouncedSearchQuery]);
|
||||||
|
|
||||||
const filteredProjects = useMemo(() => {
|
const filteredProjects = useMemo(() => {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
|
|
||||||
// First filter by search query
|
|
||||||
const filtered = data.filter(
|
const filtered = data.filter(
|
||||||
(project) =>
|
(project) =>
|
||||||
project.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
project.name
|
||||||
project.description?.toLowerCase().includes(searchQuery.toLowerCase()),
|
.toLowerCase()
|
||||||
|
.includes(debouncedSearchQuery.toLowerCase()) ||
|
||||||
|
project.description
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes(debouncedSearchQuery.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Then sort the filtered results
|
// Then sort the filtered results
|
||||||
@@ -130,7 +165,7 @@ export const ShowProjects = () => {
|
|||||||
}
|
}
|
||||||
return direction === "asc" ? comparison : -comparison;
|
return direction === "asc" ? comparison : -comparison;
|
||||||
});
|
});
|
||||||
}, [data, searchQuery, sortBy]);
|
}, [data, debouncedSearchQuery, sortBy]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -65,6 +65,25 @@ export const ShowRequests = () => {
|
|||||||
to: Date | undefined;
|
to: Date | undefined;
|
||||||
}>(getDefaultDateRange());
|
}>(getDefaultDateRange());
|
||||||
|
|
||||||
|
// Check if logs exist to determine if traefik has been reloaded
|
||||||
|
// Only fetch when active to minimize network calls
|
||||||
|
const { data: statsLogsCheck } = api.settings.readStatsLogs.useQuery(
|
||||||
|
{
|
||||||
|
page: {
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!isActive,
|
||||||
|
refetchInterval: 5000, // Check every 5 seconds when active
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine if warning should be shown
|
||||||
|
// Show warning only if active but no logs exist yet
|
||||||
|
const shouldShowWarning = isActive && (statsLogsCheck?.totalCount ?? 0) === 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (logCleanupStatus) {
|
if (logCleanupStatus) {
|
||||||
setCronExpression(logCleanupStatus.cronExpression || "0 0 * * *");
|
setCronExpression(logCleanupStatus.cronExpression || "0 0 * * *");
|
||||||
@@ -85,16 +104,18 @@ export const ShowRequests = () => {
|
|||||||
See all the incoming requests that pass trough Traefik
|
See all the incoming requests that pass trough Traefik
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
|
||||||
<AlertBlock type="warning">
|
{shouldShowWarning && (
|
||||||
When you activate, you need to reload traefik to apply the
|
<AlertBlock type="warning">
|
||||||
changes, you can reload traefik in{" "}
|
When you activate, you need to reload traefik to apply the
|
||||||
<Link
|
changes, you can reload traefik in{" "}
|
||||||
href="/dashboard/settings/server"
|
<Link
|
||||||
className="text-primary"
|
href="/dashboard/settings/server"
|
||||||
>
|
className="text-primary"
|
||||||
Settings
|
>
|
||||||
</Link>
|
Settings
|
||||||
</AlertBlock>
|
</Link>
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 py-8 border-t">
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
<div className="flex w-full gap-4 justify-end items-center">
|
<div className="flex w-full gap-4 justify-end items-center">
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, Mail, PenBoxIcon, PlusIcon } from "lucide-react";
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
Mail,
|
||||||
|
PenBoxIcon,
|
||||||
|
PlusIcon,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useFieldArray, useForm } from "react-hook-form";
|
import { useFieldArray, useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -44,6 +50,7 @@ const notificationBaseSchema = z.object({
|
|||||||
appDeploy: z.boolean().default(false),
|
appDeploy: z.boolean().default(false),
|
||||||
appBuildError: z.boolean().default(false),
|
appBuildError: z.boolean().default(false),
|
||||||
databaseBackup: z.boolean().default(false),
|
databaseBackup: z.boolean().default(false),
|
||||||
|
volumeBackup: z.boolean().default(false),
|
||||||
dokployRestart: z.boolean().default(false),
|
dokployRestart: z.boolean().default(false),
|
||||||
dockerCleanup: z.boolean().default(false),
|
dockerCleanup: z.boolean().default(false),
|
||||||
serverThreshold: z.boolean().default(false),
|
serverThreshold: z.boolean().default(false),
|
||||||
@@ -107,6 +114,21 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
|||||||
priority: z.number().min(1).max(5).default(3),
|
priority: z.number().min(1).max(5).default(3),
|
||||||
})
|
})
|
||||||
.merge(notificationBaseSchema),
|
.merge(notificationBaseSchema),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal("custom"),
|
||||||
|
endpoint: z.string().min(1, { message: "Endpoint URL is required" }),
|
||||||
|
headers: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.default([]),
|
||||||
|
})
|
||||||
|
.merge(notificationBaseSchema),
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
type: z.literal("lark"),
|
type: z.literal("lark"),
|
||||||
@@ -144,6 +166,10 @@ export const notificationsMap = {
|
|||||||
icon: <NtfyIcon />,
|
icon: <NtfyIcon />,
|
||||||
label: "ntfy",
|
label: "ntfy",
|
||||||
},
|
},
|
||||||
|
custom: {
|
||||||
|
icon: <PenBoxIcon size={29} className="text-muted-foreground" />,
|
||||||
|
label: "Custom",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NotificationSchema = z.infer<typeof notificationSchema>;
|
export type NotificationSchema = z.infer<typeof notificationSchema>;
|
||||||
@@ -179,6 +205,13 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
api.notification.testNtfyConnection.useMutation();
|
api.notification.testNtfyConnection.useMutation();
|
||||||
const { mutateAsync: testLarkConnection, isLoading: isLoadingLark } =
|
const { mutateAsync: testLarkConnection, isLoading: isLoadingLark } =
|
||||||
api.notification.testLarkConnection.useMutation();
|
api.notification.testLarkConnection.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: testCustomConnection, isLoading: isLoadingCustom } =
|
||||||
|
api.notification.testCustomConnection.useMutation();
|
||||||
|
|
||||||
|
const customMutation = notificationId
|
||||||
|
? api.notification.updateCustom.useMutation()
|
||||||
|
: api.notification.createCustom.useMutation();
|
||||||
const slackMutation = notificationId
|
const slackMutation = notificationId
|
||||||
? api.notification.updateSlack.useMutation()
|
? api.notification.updateSlack.useMutation()
|
||||||
: api.notification.createSlack.useMutation();
|
: api.notification.createSlack.useMutation();
|
||||||
@@ -217,6 +250,15 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
name: "toAddresses" as never,
|
name: "toAddresses" as never,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
fields: headerFields,
|
||||||
|
append: appendHeader,
|
||||||
|
remove: removeHeader,
|
||||||
|
} = useFieldArray({
|
||||||
|
control: form.control,
|
||||||
|
name: "headers" as never,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (type === "email" && fields.length === 0) {
|
if (type === "email" && fields.length === 0) {
|
||||||
append("");
|
append("");
|
||||||
@@ -231,6 +273,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: notification.appDeploy,
|
appDeploy: notification.appDeploy,
|
||||||
dokployRestart: notification.dokployRestart,
|
dokployRestart: notification.dokployRestart,
|
||||||
databaseBackup: notification.databaseBackup,
|
databaseBackup: notification.databaseBackup,
|
||||||
|
volumeBackup: notification.volumeBackup,
|
||||||
dockerCleanup: notification.dockerCleanup,
|
dockerCleanup: notification.dockerCleanup,
|
||||||
webhookUrl: notification.slack?.webhookUrl,
|
webhookUrl: notification.slack?.webhookUrl,
|
||||||
channel: notification.slack?.channel || "",
|
channel: notification.slack?.channel || "",
|
||||||
@@ -244,6 +287,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: notification.appDeploy,
|
appDeploy: notification.appDeploy,
|
||||||
dokployRestart: notification.dokployRestart,
|
dokployRestart: notification.dokployRestart,
|
||||||
databaseBackup: notification.databaseBackup,
|
databaseBackup: notification.databaseBackup,
|
||||||
|
volumeBackup: notification.volumeBackup,
|
||||||
botToken: notification.telegram?.botToken,
|
botToken: notification.telegram?.botToken,
|
||||||
messageThreadId: notification.telegram?.messageThreadId || "",
|
messageThreadId: notification.telegram?.messageThreadId || "",
|
||||||
chatId: notification.telegram?.chatId,
|
chatId: notification.telegram?.chatId,
|
||||||
@@ -258,6 +302,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: notification.appDeploy,
|
appDeploy: notification.appDeploy,
|
||||||
dokployRestart: notification.dokployRestart,
|
dokployRestart: notification.dokployRestart,
|
||||||
databaseBackup: notification.databaseBackup,
|
databaseBackup: notification.databaseBackup,
|
||||||
|
volumeBackup: notification.volumeBackup,
|
||||||
type: notification.notificationType,
|
type: notification.notificationType,
|
||||||
webhookUrl: notification.discord?.webhookUrl,
|
webhookUrl: notification.discord?.webhookUrl,
|
||||||
decoration: notification.discord?.decoration || undefined,
|
decoration: notification.discord?.decoration || undefined,
|
||||||
@@ -271,6 +316,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: notification.appDeploy,
|
appDeploy: notification.appDeploy,
|
||||||
dokployRestart: notification.dokployRestart,
|
dokployRestart: notification.dokployRestart,
|
||||||
databaseBackup: notification.databaseBackup,
|
databaseBackup: notification.databaseBackup,
|
||||||
|
volumeBackup: notification.volumeBackup,
|
||||||
type: notification.notificationType,
|
type: notification.notificationType,
|
||||||
smtpServer: notification.email?.smtpServer,
|
smtpServer: notification.email?.smtpServer,
|
||||||
smtpPort: notification.email?.smtpPort,
|
smtpPort: notification.email?.smtpPort,
|
||||||
@@ -288,6 +334,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: notification.appDeploy,
|
appDeploy: notification.appDeploy,
|
||||||
dokployRestart: notification.dokployRestart,
|
dokployRestart: notification.dokployRestart,
|
||||||
databaseBackup: notification.databaseBackup,
|
databaseBackup: notification.databaseBackup,
|
||||||
|
volumeBackup: notification.volumeBackup,
|
||||||
type: notification.notificationType,
|
type: notification.notificationType,
|
||||||
appToken: notification.gotify?.appToken,
|
appToken: notification.gotify?.appToken,
|
||||||
decoration: notification.gotify?.decoration || undefined,
|
decoration: notification.gotify?.decoration || undefined,
|
||||||
@@ -302,6 +349,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: notification.appDeploy,
|
appDeploy: notification.appDeploy,
|
||||||
dokployRestart: notification.dokployRestart,
|
dokployRestart: notification.dokployRestart,
|
||||||
databaseBackup: notification.databaseBackup,
|
databaseBackup: notification.databaseBackup,
|
||||||
|
volumeBackup: notification.volumeBackup,
|
||||||
type: notification.notificationType,
|
type: notification.notificationType,
|
||||||
accessToken: notification.ntfy?.accessToken || "",
|
accessToken: notification.ntfy?.accessToken || "",
|
||||||
topic: notification.ntfy?.topic,
|
topic: notification.ntfy?.topic,
|
||||||
@@ -323,6 +371,26 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
dockerCleanup: notification.dockerCleanup,
|
dockerCleanup: notification.dockerCleanup,
|
||||||
serverThreshold: notification.serverThreshold,
|
serverThreshold: notification.serverThreshold,
|
||||||
});
|
});
|
||||||
|
} else if (notification.notificationType === "custom") {
|
||||||
|
form.reset({
|
||||||
|
appBuildError: notification.appBuildError,
|
||||||
|
appDeploy: notification.appDeploy,
|
||||||
|
dokployRestart: notification.dokployRestart,
|
||||||
|
databaseBackup: notification.databaseBackup,
|
||||||
|
type: notification.notificationType,
|
||||||
|
endpoint: notification.custom?.endpoint || "",
|
||||||
|
headers: notification.custom?.headers
|
||||||
|
? Object.entries(notification.custom.headers).map(
|
||||||
|
([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
name: notification.name,
|
||||||
|
dockerCleanup: notification.dockerCleanup,
|
||||||
|
serverThreshold: notification.serverThreshold,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
form.reset();
|
form.reset();
|
||||||
@@ -337,6 +405,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
gotify: gotifyMutation,
|
gotify: gotifyMutation,
|
||||||
ntfy: ntfyMutation,
|
ntfy: ntfyMutation,
|
||||||
lark: larkMutation,
|
lark: larkMutation,
|
||||||
|
custom: customMutation,
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: NotificationSchema) => {
|
const onSubmit = async (data: NotificationSchema) => {
|
||||||
@@ -345,6 +414,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy,
|
appDeploy,
|
||||||
dokployRestart,
|
dokployRestart,
|
||||||
databaseBackup,
|
databaseBackup,
|
||||||
|
volumeBackup,
|
||||||
dockerCleanup,
|
dockerCleanup,
|
||||||
serverThreshold,
|
serverThreshold,
|
||||||
} = data;
|
} = data;
|
||||||
@@ -355,6 +425,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: appDeploy,
|
appDeploy: appDeploy,
|
||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
|
volumeBackup: volumeBackup,
|
||||||
webhookUrl: data.webhookUrl,
|
webhookUrl: data.webhookUrl,
|
||||||
channel: data.channel,
|
channel: data.channel,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -369,6 +440,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: appDeploy,
|
appDeploy: appDeploy,
|
||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
|
volumeBackup: volumeBackup,
|
||||||
botToken: data.botToken,
|
botToken: data.botToken,
|
||||||
messageThreadId: data.messageThreadId || "",
|
messageThreadId: data.messageThreadId || "",
|
||||||
chatId: data.chatId,
|
chatId: data.chatId,
|
||||||
@@ -384,6 +456,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: appDeploy,
|
appDeploy: appDeploy,
|
||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
|
volumeBackup: volumeBackup,
|
||||||
webhookUrl: data.webhookUrl,
|
webhookUrl: data.webhookUrl,
|
||||||
decoration: data.decoration,
|
decoration: data.decoration,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -398,6 +471,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: appDeploy,
|
appDeploy: appDeploy,
|
||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
|
volumeBackup: volumeBackup,
|
||||||
smtpServer: data.smtpServer,
|
smtpServer: data.smtpServer,
|
||||||
smtpPort: data.smtpPort,
|
smtpPort: data.smtpPort,
|
||||||
username: data.username,
|
username: data.username,
|
||||||
@@ -416,6 +490,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: appDeploy,
|
appDeploy: appDeploy,
|
||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
|
volumeBackup: volumeBackup,
|
||||||
serverUrl: data.serverUrl,
|
serverUrl: data.serverUrl,
|
||||||
appToken: data.appToken,
|
appToken: data.appToken,
|
||||||
priority: data.priority,
|
priority: data.priority,
|
||||||
@@ -431,6 +506,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: appDeploy,
|
appDeploy: appDeploy,
|
||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
|
volumeBackup: volumeBackup,
|
||||||
serverUrl: data.serverUrl,
|
serverUrl: data.serverUrl,
|
||||||
accessToken: data.accessToken || "",
|
accessToken: data.accessToken || "",
|
||||||
topic: data.topic,
|
topic: data.topic,
|
||||||
@@ -453,6 +529,32 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
larkId: notification?.larkId || "",
|
larkId: notification?.larkId || "",
|
||||||
serverThreshold: serverThreshold,
|
serverThreshold: serverThreshold,
|
||||||
});
|
});
|
||||||
|
} else if (data.type === "custom") {
|
||||||
|
// Convert headers array to object
|
||||||
|
const headersRecord =
|
||||||
|
data.headers && data.headers.length > 0
|
||||||
|
? data.headers.reduce(
|
||||||
|
(acc, { key, value }) => {
|
||||||
|
if (key.trim()) acc[key] = value;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>,
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
promise = customMutation.mutateAsync({
|
||||||
|
appBuildError: appBuildError,
|
||||||
|
appDeploy: appDeploy,
|
||||||
|
dokployRestart: dokployRestart,
|
||||||
|
databaseBackup: databaseBackup,
|
||||||
|
endpoint: data.endpoint,
|
||||||
|
headers: headersRecord,
|
||||||
|
name: data.name,
|
||||||
|
dockerCleanup: dockerCleanup,
|
||||||
|
serverThreshold: serverThreshold,
|
||||||
|
notificationId: notificationId || "",
|
||||||
|
customId: notification?.customId || "",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (promise) {
|
if (promise) {
|
||||||
@@ -1043,7 +1145,92 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{type === "custom" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="endpoint"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://api.example.com/webhook"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
The URL where POST requests will be sent with
|
||||||
|
notification data.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<FormLabel>Headers</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Optional. Custom headers for your POST request (e.g.,
|
||||||
|
Authorization, Content-Type).
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{headerFields.map((field, index) => (
|
||||||
|
<div
|
||||||
|
key={field.id}
|
||||||
|
className="flex items-center gap-2 p-2 border rounded-md bg-muted/50"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`headers.${index}.key` as never}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Key" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`headers.${index}.value` as never}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-[2]">
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Value" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeHeader(index)}
|
||||||
|
className="text-red-500 hover:text-red-700 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => appendHeader({ key: "", value: "" })}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4 mr-2" />
|
||||||
|
Add header
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{type === "lark" && (
|
{type === "lark" && (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -1134,6 +1321,27 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="volumeBackup"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Volume Backup</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Trigger the action when a volume backup is created.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="dockerCleanup"
|
name="dockerCleanup"
|
||||||
@@ -1215,7 +1423,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
isLoadingEmail ||
|
isLoadingEmail ||
|
||||||
isLoadingGotify ||
|
isLoadingGotify ||
|
||||||
isLoadingNtfy ||
|
isLoadingNtfy ||
|
||||||
isLoadingLark
|
isLoadingLark ||
|
||||||
|
isLoadingCustom
|
||||||
}
|
}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1269,6 +1478,21 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
await testLarkConnection({
|
await testLarkConnection({
|
||||||
webhookUrl: data.webhookUrl,
|
webhookUrl: data.webhookUrl,
|
||||||
});
|
});
|
||||||
|
} else if (data.type === "custom") {
|
||||||
|
const headersRecord =
|
||||||
|
data.headers && data.headers.length > 0
|
||||||
|
? data.headers.reduce(
|
||||||
|
(acc, { key, value }) => {
|
||||||
|
if (key.trim()) acc[key] = value;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>,
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
await testCustomConnection({
|
||||||
|
endpoint: data.endpoint,
|
||||||
|
headers: headersRecord,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
toast.success("Connection Success");
|
toast.success("Connection Success");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Bell, Loader2, Mail, Trash2 } from "lucide-react";
|
import { Bell, Loader2, Mail, PenBoxIcon, Trash2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
DiscordIcon,
|
DiscordIcon,
|
||||||
@@ -96,6 +96,11 @@ export const ShowNotifications = () => {
|
|||||||
<NtfyIcon className="size-6" />
|
<NtfyIcon className="size-6" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{notification.notificationType === "custom" && (
|
||||||
|
<div className="flex items-center justify-center rounded-lg ">
|
||||||
|
<PenBoxIcon className="size-6 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{notification.notificationType === "lark" && (
|
{notification.notificationType === "lark" && (
|
||||||
<div className="flex items-center justify-center rounded-lg">
|
<div className="flex items-center justify-center rounded-lg">
|
||||||
<LarkIcon className="size-7 text-muted-foreground" />
|
<LarkIcon className="size-7 text-muted-foreground" />
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const profileSchema = z.object({
|
|||||||
currentPassword: z.string().nullable(),
|
currentPassword: z.string().nullable(),
|
||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
|
lastName: z.string().optional(),
|
||||||
allowImpersonation: z.boolean().optional().default(false),
|
allowImpersonation: z.boolean().optional().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,7 +89,8 @@ 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 || "",
|
name: data?.user?.firstName || "",
|
||||||
|
lastName: data?.user?.lastName || "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(profileSchema),
|
resolver: zodResolver(profileSchema),
|
||||||
});
|
});
|
||||||
@@ -102,7 +104,8 @@ 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 || "",
|
name: data?.user?.firstName || "",
|
||||||
|
lastName: data?.user?.lastName || "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keepValues: true,
|
keepValues: true,
|
||||||
@@ -127,6 +130,7 @@ export const ProfileForm = () => {
|
|||||||
currentPassword: values.currentPassword || undefined,
|
currentPassword: values.currentPassword || undefined,
|
||||||
allowImpersonation: values.allowImpersonation,
|
allowImpersonation: values.allowImpersonation,
|
||||||
name: values.name || undefined,
|
name: values.name || undefined,
|
||||||
|
lastName: values.lastName || undefined,
|
||||||
});
|
});
|
||||||
await refetch();
|
await refetch();
|
||||||
toast.success("Profile Updated");
|
toast.success("Profile Updated");
|
||||||
@@ -136,6 +140,7 @@ export const ProfileForm = () => {
|
|||||||
image: values.image,
|
image: values.image,
|
||||||
currentPassword: "",
|
currentPassword: "",
|
||||||
name: values.name || "",
|
name: values.name || "",
|
||||||
|
lastName: values.lastName || "",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Error updating the profile");
|
toast.error("Error updating the profile");
|
||||||
@@ -180,9 +185,22 @@ export const ProfileForm = () => {
|
|||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>First Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Name" {...field} />
|
<Input placeholder="John" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="lastName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Last Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Doe" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -280,7 +298,7 @@ export const ProfileForm = () => {
|
|||||||
<Avatar className="default-avatar h-12 w-12 rounded-full border hover:p-px hover:border-primary transition-transform">
|
<Avatar className="default-avatar h-12 w-12 rounded-full border hover:p-px hover:border-primary transition-transform">
|
||||||
<AvatarFallback className="rounded-lg">
|
<AvatarFallback className="rounded-lg">
|
||||||
{getFallbackAvatarInitials(
|
{getFallbackAvatarInitials(
|
||||||
data?.user?.name,
|
`${data?.user?.firstName} ${data?.user?.lastName}`.trim(),
|
||||||
)}
|
)}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -85,7 +87,26 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</EditTraefikEnv>
|
</EditTraefikEnv>
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DialogAction
|
||||||
|
title={
|
||||||
|
haveTraefikDashboardPortEnabled
|
||||||
|
? "Disable Traefik Dashboard"
|
||||||
|
: "Enable Traefik Dashboard"
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<div className="space-y-4">
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
The Traefik container will be recreated from scratch. This
|
||||||
|
means the container will be deleted and created again, which
|
||||||
|
may cause downtime in your applications.
|
||||||
|
</AlertBlock>
|
||||||
|
<p>
|
||||||
|
Are you sure you want to{" "}
|
||||||
|
{haveTraefikDashboardPortEnabled ? "disable" : "enable"} the
|
||||||
|
Traefik dashboard?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await toggleDashboard({
|
await toggleDashboard({
|
||||||
enableDashboard: !haveTraefikDashboardPortEnabled,
|
enableDashboard: !haveTraefikDashboardPortEnabled,
|
||||||
@@ -97,14 +118,26 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
|
|||||||
);
|
);
|
||||||
refetchDashboard();
|
refetchDashboard();
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch((error) => {
|
||||||
|
const errorMessage =
|
||||||
|
error?.message ||
|
||||||
|
"Failed to toggle dashboard. Please check if port 8080 is available.";
|
||||||
|
toast.error(errorMessage);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className="w-full cursor-pointer space-x-3"
|
disabled={toggleDashboardIsLoading}
|
||||||
|
type="default"
|
||||||
>
|
>
|
||||||
<span>
|
<DropdownMenuItem
|
||||||
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"} Dashboard
|
onSelect={(e) => e.preventDefault()}
|
||||||
</span>
|
className="w-full cursor-pointer space-x-3"
|
||||||
</DropdownMenuItem>
|
>
|
||||||
|
<span>
|
||||||
|
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"}{" "}
|
||||||
|
Dashboard
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogAction>
|
||||||
<ManageTraefikPorts serverId={serverId}>
|
<ManageTraefikPorts serverId={serverId}>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={(e) => e.preventDefault()}
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ export const AddInvitation = () => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="member">Member</SelectItem>
|
<SelectItem value="member">Member</SelectItem>
|
||||||
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
|
|||||||
159
apps/dokploy/components/dashboard/settings/users/change-role.tsx
Normal file
159
apps/dokploy/components/dashboard/settings/users/change-role.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
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 { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
const changeRoleSchema = z.object({
|
||||||
|
role: z.enum(["admin", "member"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
type ChangeRoleSchema = z.infer<typeof changeRoleSchema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
memberId: string;
|
||||||
|
currentRole: "admin" | "member";
|
||||||
|
userEmail: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
const { mutateAsync, isError, error, isLoading } =
|
||||||
|
api.organization.updateMemberRole.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<ChangeRoleSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
role: currentRole,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(changeRoleSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
form.reset({
|
||||||
|
role: currentRole,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, currentRole, isOpen]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: ChangeRoleSchema) => {
|
||||||
|
await mutateAsync({
|
||||||
|
memberId,
|
||||||
|
role: data.role,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Role updated successfully");
|
||||||
|
await utils.user.all.invalidate();
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(error?.message || "Error updating role");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger className="" asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Change Role
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-[85vh] sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Change User Role</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Change the role for <strong>{userEmail}</strong>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form-change-role"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="w-full space-y-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="role"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Role</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a role" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
|
<SelectItem value="member">Member</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
<strong>Admin:</strong> Can manage users and settings.
|
||||||
|
<br />
|
||||||
|
<strong>Member:</strong> Limited permissions, can be
|
||||||
|
customized.
|
||||||
|
<br />
|
||||||
|
<em className="text-muted-foreground text-xs">
|
||||||
|
Note: Owner role is intransferible.
|
||||||
|
</em>
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
form="hook-form-change-role"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Update Role
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -29,12 +29,15 @@ import {
|
|||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { AddUserPermissions } from "./add-permissions";
|
import { AddUserPermissions } from "./add-permissions";
|
||||||
|
import { ChangeRole } from "./change-role";
|
||||||
|
|
||||||
export const ShowUsers = () => {
|
export const ShowUsers = () => {
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data, isLoading, refetch } = api.user.all.useQuery();
|
const { data, isLoading, refetch } = api.user.all.useQuery();
|
||||||
const { mutateAsync } = api.user.remove.useMutation();
|
const { mutateAsync } = api.user.remove.useMutation();
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const { data: session } = authClient.useSession();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -81,6 +84,52 @@ export const ShowUsers = () => {
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data?.map((member) => {
|
{data?.map((member) => {
|
||||||
|
const currentUserRole = data?.find(
|
||||||
|
(m) => m.user.id === session?.user?.id,
|
||||||
|
)?.role;
|
||||||
|
|
||||||
|
// Owner never has "Edit Permissions" (they're absolute owner)
|
||||||
|
// Other users can edit permissions if target is not themselves and target is a member
|
||||||
|
const canEditPermissions =
|
||||||
|
member.role !== "owner" &&
|
||||||
|
member.role === "member" &&
|
||||||
|
member.user.id !== session?.user?.id;
|
||||||
|
|
||||||
|
// Can change role based on hierarchy:
|
||||||
|
// - Owner: Can change anyone's role (except themselves and other owners)
|
||||||
|
// - Admin: Can only change member roles (not other admins or owners)
|
||||||
|
// - Owner role is intransferible
|
||||||
|
const canChangeRole =
|
||||||
|
member.role !== "owner" &&
|
||||||
|
member.user.id !== session?.user?.id &&
|
||||||
|
(currentUserRole === "owner" ||
|
||||||
|
(currentUserRole === "admin" &&
|
||||||
|
member.role === "member"));
|
||||||
|
|
||||||
|
// Delete/Unlink follow same hierarchy as role changes
|
||||||
|
// - Owner: Can delete/unlink anyone (except themselves and owner can't be deleted)
|
||||||
|
// - Admin: Can only delete/unlink members (not other admins or owner)
|
||||||
|
const canDelete =
|
||||||
|
member.role !== "owner" &&
|
||||||
|
!isCloud &&
|
||||||
|
member.user.id !== session?.user?.id &&
|
||||||
|
(currentUserRole === "owner" ||
|
||||||
|
(currentUserRole === "admin" &&
|
||||||
|
member.role === "member"));
|
||||||
|
|
||||||
|
const canUnlink =
|
||||||
|
member.role !== "owner" &&
|
||||||
|
member.user.id !== session?.user?.id &&
|
||||||
|
(currentUserRole === "owner" ||
|
||||||
|
(currentUserRole === "admin" &&
|
||||||
|
member.role === "member"));
|
||||||
|
|
||||||
|
const hasAnyAction =
|
||||||
|
canEditPermissions ||
|
||||||
|
canChangeRole ||
|
||||||
|
canDelete ||
|
||||||
|
canUnlink;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={member.id}>
|
<TableRow key={member.id}>
|
||||||
<TableCell className="w-[100px]">
|
<TableCell className="w-[100px]">
|
||||||
@@ -109,7 +158,7 @@ export const ShowUsers = () => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="text-right flex justify-end">
|
<TableCell className="text-right flex justify-end">
|
||||||
{member.role !== "owner" && (
|
{hasAnyAction ? (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
@@ -127,11 +176,23 @@ export const ShowUsers = () => {
|
|||||||
Actions
|
Actions
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
|
|
||||||
<AddUserPermissions
|
{canChangeRole && (
|
||||||
userId={member.user.id}
|
<ChangeRole
|
||||||
/>
|
memberId={member.id}
|
||||||
|
currentRole={
|
||||||
|
member.role as "admin" | "member"
|
||||||
|
}
|
||||||
|
userEmail={member.user.email}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isCloud && (
|
{canEditPermissions && (
|
||||||
|
<AddUserPermissions
|
||||||
|
userId={member.user.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{canDelete && (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Delete User"
|
title="Delete User"
|
||||||
description="Are you sure you want to delete this user?"
|
description="Are you sure you want to delete this user?"
|
||||||
@@ -146,9 +207,10 @@ export const ShowUsers = () => {
|
|||||||
);
|
);
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((err) => {
|
||||||
toast.error(
|
toast.error(
|
||||||
"Error deleting destination",
|
err?.message ||
|
||||||
|
"Error deleting user",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -162,66 +224,79 @@ export const ShowUsers = () => {
|
|||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogAction
|
{canUnlink && (
|
||||||
title="Unlink User"
|
<DialogAction
|
||||||
description="Are you sure you want to unlink this user?"
|
title="Unlink User"
|
||||||
type="destructive"
|
description="Are you sure you want to unlink this user?"
|
||||||
onClick={async () => {
|
type="destructive"
|
||||||
if (!isCloud) {
|
onClick={async () => {
|
||||||
const orgCount =
|
if (!isCloud) {
|
||||||
await utils.user.checkUserOrganizations.fetch(
|
const orgCount =
|
||||||
{
|
await utils.user.checkUserOrganizations.fetch(
|
||||||
|
{
|
||||||
|
userId: member.user.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (orgCount === 1) {
|
||||||
|
await mutateAsync({
|
||||||
userId: member.user.id,
|
userId: member.user.id,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
"User deleted successfully",
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(
|
||||||
|
"Error deleting user",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } =
|
||||||
|
await authClient.organization.removeMember(
|
||||||
|
{
|
||||||
|
memberIdOrEmail: member.id,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(orgCount);
|
if (!error) {
|
||||||
|
toast.success(
|
||||||
if (orgCount === 1) {
|
"User unlinked successfully",
|
||||||
await mutateAsync({
|
);
|
||||||
userId: member.user.id,
|
refetch();
|
||||||
})
|
} else {
|
||||||
.then(() => {
|
toast.error(
|
||||||
toast.success(
|
"Error unlinking user",
|
||||||
"User deleted successfully",
|
);
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error(
|
|
||||||
"Error deleting user",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
|
|
||||||
const { error } =
|
|
||||||
await authClient.organization.removeMember(
|
|
||||||
{
|
|
||||||
memberIdOrEmail: member.id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!error) {
|
|
||||||
toast.success(
|
|
||||||
"User unlinked successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
} else {
|
|
||||||
toast.error("Error unlinking user");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
>
|
||||||
Unlink User
|
<DropdownMenuItem
|
||||||
</DropdownMenuItem>
|
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||||
</DialogAction>
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Unlink User
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<span className="sr-only">
|
||||||
|
No actions available
|
||||||
|
</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import { api } from "@/utils/api";
|
|||||||
|
|
||||||
const addServerDomain = z
|
const addServerDomain = z
|
||||||
.object({
|
.object({
|
||||||
domain: z.string(),
|
domain: z.string().trim().toLowerCase(),
|
||||||
letsEncryptEmail: z.string(),
|
letsEncryptEmail: z.string(),
|
||||||
https: z.boolean().optional(),
|
https: z.boolean().optional(),
|
||||||
certificateType: z.enum(["letsencrypt", "none", "custom"]),
|
certificateType: z.enum(["letsencrypt", "none", "custom"]),
|
||||||
@@ -49,7 +49,11 @@ const addServerDomain = z
|
|||||||
message: "Required",
|
message: "Required",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) {
|
if (
|
||||||
|
data.https &&
|
||||||
|
data.certificateType === "letsencrypt" &&
|
||||||
|
!data.letsEncryptEmail
|
||||||
|
) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message:
|
message:
|
||||||
|
|||||||
@@ -105,7 +105,9 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
toast.success(t("settings.server.webServer.traefik.portsUpdated"));
|
toast.success(t("settings.server.webServer.traefik.portsUpdated"));
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch {}
|
} catch (error) {
|
||||||
|
toast.error((error as Error).message || "Error updating Traefik ports");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -156,11 +158,11 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ScrollArea className="h-[400px] pr-4">
|
<ScrollArea className="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} className="bg-transparent">
|
||||||
<CardContent className="grid grid-cols-[1fr_1fr_1fr_auto] gap-4 p-4 transparent">
|
<CardContent className="grid grid-cols-4 gap-4 p-4 transparent">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`ports.${index}.targetPort`}
|
name={`ports.${index}.targetPort`}
|
||||||
@@ -303,6 +305,12 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
The Traefik container will be recreated from scratch. This
|
||||||
|
means the container will be deleted and created again, which
|
||||||
|
may cause downtime in your applications.
|
||||||
|
</AlertBlock>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -158,7 +158,8 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/schedules",
|
url: "/dashboard/schedules",
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
// Only enabled in non-cloud environments
|
// Only enabled in non-cloud environments
|
||||||
isEnabled: ({ isCloud, auth }) => !isCloud && auth?.role === "owner",
|
isEnabled: ({ isCloud, auth }) =>
|
||||||
|
!isCloud && (auth?.role === "owner" || auth?.role === "admin"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -168,7 +169,9 @@ const MENU: Menu = {
|
|||||||
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!(
|
!!(
|
||||||
(auth?.role === "owner" || auth?.canAccessToTraefikFiles) &&
|
(auth?.role === "owner" ||
|
||||||
|
auth?.role === "admin" ||
|
||||||
|
auth?.canAccessToTraefikFiles) &&
|
||||||
!isCloud
|
!isCloud
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -179,7 +182,12 @@ const MENU: Menu = {
|
|||||||
icon: BlocksIcon,
|
icon: BlocksIcon,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud),
|
!!(
|
||||||
|
(auth?.role === "owner" ||
|
||||||
|
auth?.role === "admin" ||
|
||||||
|
auth?.canAccessToDocker) &&
|
||||||
|
!isCloud
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -188,7 +196,12 @@ const MENU: Menu = {
|
|||||||
icon: PieChart,
|
icon: PieChart,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud),
|
!!(
|
||||||
|
(auth?.role === "owner" ||
|
||||||
|
auth?.role === "admin" ||
|
||||||
|
auth?.canAccessToDocker) &&
|
||||||
|
!isCloud
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -197,7 +210,12 @@ const MENU: Menu = {
|
|||||||
icon: Forward,
|
icon: Forward,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud),
|
!!(
|
||||||
|
(auth?.role === "owner" ||
|
||||||
|
auth?.role === "admin" ||
|
||||||
|
auth?.canAccessToDocker) &&
|
||||||
|
!isCloud
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Legacy unused menu, adjusted to the new structure
|
// Legacy unused menu, adjusted to the new structure
|
||||||
@@ -264,7 +282,8 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/server",
|
url: "/dashboard/settings/server",
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
// Only enabled for admins in non-cloud environments
|
// Only enabled for admins in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
|
!!((auth?.role === "owner" || auth?.role === "admin") && !isCloud),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -278,7 +297,8 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/servers",
|
url: "/dashboard/settings/servers",
|
||||||
icon: Server,
|
icon: Server,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
isEnabled: ({ auth }) =>
|
||||||
|
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -286,7 +306,8 @@ const MENU: Menu = {
|
|||||||
icon: Users,
|
icon: Users,
|
||||||
url: "/dashboard/settings/users",
|
url: "/dashboard/settings/users",
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
isEnabled: ({ auth }) =>
|
||||||
|
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -295,14 +316,19 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/ssh-keys",
|
url: "/dashboard/settings/ssh-keys",
|
||||||
// Only enabled for admins and users with access to SSH keys
|
// Only enabled for admins and users with access to SSH keys
|
||||||
isEnabled: ({ auth }) =>
|
isEnabled: ({ auth }) =>
|
||||||
!!(auth?.role === "owner" || auth?.canAccessToSSHKeys),
|
!!(
|
||||||
|
auth?.role === "owner" ||
|
||||||
|
auth?.canAccessToSSHKeys ||
|
||||||
|
auth?.role === "admin"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "AI",
|
title: "AI",
|
||||||
icon: BotIcon,
|
icon: BotIcon,
|
||||||
url: "/dashboard/settings/ai",
|
url: "/dashboard/settings/ai",
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
isEnabled: ({ auth }) =>
|
||||||
|
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -311,7 +337,11 @@ const MENU: Menu = {
|
|||||||
icon: GitBranch,
|
icon: GitBranch,
|
||||||
// Only enabled for admins and users with access to Git providers
|
// Only enabled for admins and users with access to Git providers
|
||||||
isEnabled: ({ auth }) =>
|
isEnabled: ({ auth }) =>
|
||||||
!!(auth?.role === "owner" || auth?.canAccessToGitProviders),
|
!!(
|
||||||
|
auth?.role === "owner" ||
|
||||||
|
auth?.canAccessToGitProviders ||
|
||||||
|
auth?.role === "admin"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -319,7 +349,8 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/registry",
|
url: "/dashboard/settings/registry",
|
||||||
icon: Package,
|
icon: Package,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
isEnabled: ({ auth }) =>
|
||||||
|
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -327,7 +358,8 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/destinations",
|
url: "/dashboard/settings/destinations",
|
||||||
icon: Database,
|
icon: Database,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
isEnabled: ({ auth }) =>
|
||||||
|
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -336,7 +368,8 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/certificates",
|
url: "/dashboard/settings/certificates",
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
isEnabled: ({ auth }) =>
|
||||||
|
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -344,7 +377,8 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/cluster",
|
url: "/dashboard/settings/cluster",
|
||||||
icon: Boxes,
|
icon: Boxes,
|
||||||
// Only enabled for admins in non-cloud environments
|
// Only enabled for admins in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
|
!!((auth?.role === "owner" || auth?.role === "admin") && !isCloud),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -352,7 +386,8 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/notifications",
|
url: "/dashboard/settings/notifications",
|
||||||
icon: Bell,
|
icon: Bell,
|
||||||
// Only enabled for admins
|
// Only enabled for admins
|
||||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
isEnabled: ({ auth }) =>
|
||||||
|
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -718,7 +753,9 @@ function SidebarLogo() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{(user?.role === "owner" || isCloud) && (
|
{(user?.role === "owner" ||
|
||||||
|
user?.role === "admin" ||
|
||||||
|
isCloud) && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<AddOrganization />
|
<AddOrganization />
|
||||||
@@ -1082,7 +1119,7 @@ export default function Page({ children }: Props) {
|
|||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<SidebarMenu className="flex flex-col gap-2">
|
<SidebarMenu className="flex flex-col gap-2">
|
||||||
{!isCloud && auth?.role === "owner" && (
|
{!isCloud && (auth?.role === "owner" || auth?.role === "admin") && (
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<UpdateServerButton />
|
<UpdateServerButton />
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ export const UserNav = () => {
|
|||||||
alt={data?.user?.image || ""}
|
alt={data?.user?.image || ""}
|
||||||
/>
|
/>
|
||||||
<AvatarFallback className="rounded-lg">
|
<AvatarFallback className="rounded-lg">
|
||||||
{getFallbackAvatarInitials(data?.user?.name)}
|
{getFallbackAvatarInitials(
|
||||||
|
`${data?.user?.firstName} ${data?.user?.lastName}`.trim(),
|
||||||
|
)}
|
||||||
</AvatarFallback>
|
</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">
|
||||||
@@ -102,7 +104,9 @@ export const UserNav = () => {
|
|||||||
>
|
>
|
||||||
Monitoring
|
Monitoring
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{(data?.role === "owner" || data?.canAccessToTraefikFiles) && (
|
{(data?.role === "owner" ||
|
||||||
|
data?.role === "admin" ||
|
||||||
|
data?.canAccessToTraefikFiles) && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -112,7 +116,9 @@ export const UserNav = () => {
|
|||||||
Traefik
|
Traefik
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{(data?.role === "owner" || data?.canAccessToDocker) && (
|
{(data?.role === "owner" ||
|
||||||
|
data?.role === "admin" ||
|
||||||
|
data?.canAccessToDocker) && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -126,7 +132,7 @@ export const UserNav = () => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
data?.role === "owner" && (
|
(data?.role === "owner" || data?.role === "admin") && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
2
apps/dokploy/drizzle/0125_neat_the_phantom.sql
Normal file
2
apps/dokploy/drizzle/0125_neat_the_phantom.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "application" ADD COLUMN "rollbackRegistryId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD CONSTRAINT "application_rollbackRegistryId_registry_registryId_fk" FOREIGN KEY ("rollbackRegistryId") REFERENCES "public"."registry"("registryId") ON DELETE set null ON UPDATE no action;
|
||||||
1
apps/dokploy/drizzle/0126_nifty_monster_badoon.sql
Normal file
1
apps/dokploy/drizzle/0126_nifty_monster_badoon.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user" ALTER COLUMN "enableDockerCleanup" SET DEFAULT true;
|
||||||
1
apps/dokploy/drizzle/0127_superb_alice.sql
Normal file
1
apps/dokploy/drizzle/0127_superb_alice.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "notification" ADD COLUMN "volumeBackup" boolean DEFAULT false NOT NULL;
|
||||||
2
apps/dokploy/drizzle/0128_hard_falcon.sql
Normal file
2
apps/dokploy/drizzle/0128_hard_falcon.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "user" RENAME COLUMN "name" TO "firstName";--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" ADD COLUMN "lastName" text DEFAULT '' NOT NULL;
|
||||||
9
apps/dokploy/drizzle/0129_pale_roughhouse.sql
Normal file
9
apps/dokploy/drizzle/0129_pale_roughhouse.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TYPE "public"."notificationType" ADD VALUE 'custom' BEFORE 'lark';--> statement-breakpoint
|
||||||
|
CREATE TABLE "custom" (
|
||||||
|
"customId" text PRIMARY KEY NOT NULL,
|
||||||
|
"endpoint" text NOT NULL,
|
||||||
|
"headers" jsonb
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "notification" ADD COLUMN "customId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "notification" ADD CONSTRAINT "notification_customId_custom_customId_fk" FOREIGN KEY ("customId") REFERENCES "public"."custom"("customId") ON DELETE cascade ON UPDATE no action;
|
||||||
1
apps/dokploy/drizzle/0130_perpetual_screwball.sql
Normal file
1
apps/dokploy/drizzle/0130_perpetual_screwball.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "schedule" ADD COLUMN "timezone" text;
|
||||||
6850
apps/dokploy/drizzle/meta/0125_snapshot.json
Normal file
6850
apps/dokploy/drizzle/meta/0125_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6850
apps/dokploy/drizzle/meta/0126_snapshot.json
Normal file
6850
apps/dokploy/drizzle/meta/0126_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6857
apps/dokploy/drizzle/meta/0127_snapshot.json
Normal file
6857
apps/dokploy/drizzle/meta/0127_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6864
apps/dokploy/drizzle/meta/0128_snapshot.json
Normal file
6864
apps/dokploy/drizzle/meta/0128_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6915
apps/dokploy/drizzle/meta/0129_snapshot.json
Normal file
6915
apps/dokploy/drizzle/meta/0129_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6921
apps/dokploy/drizzle/meta/0130_snapshot.json
Normal file
6921
apps/dokploy/drizzle/meta/0130_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -876,6 +876,48 @@
|
|||||||
"when": 1764571454170,
|
"when": 1764571454170,
|
||||||
"tag": "0124_certain_cloak",
|
"tag": "0124_certain_cloak",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 125,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1764573207555,
|
||||||
|
"tag": "0125_neat_the_phantom",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 126,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1765065295708,
|
||||||
|
"tag": "0126_nifty_monster_badoon",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 127,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1765095189368,
|
||||||
|
"tag": "0127_superb_alice",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 128,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1765101709413,
|
||||||
|
"tag": "0128_hard_falcon",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 129,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1765136384035,
|
||||||
|
"tag": "0129_pale_roughhouse",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 130,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1765167657813,
|
||||||
|
"tag": "0130_perpetual_screwball",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
adminClient,
|
adminClient,
|
||||||
apiKeyClient,
|
apiKeyClient,
|
||||||
|
inferAdditionalFields,
|
||||||
organizationClient,
|
organizationClient,
|
||||||
twoFactorClient,
|
twoFactorClient,
|
||||||
} from "better-auth/client/plugins";
|
} from "better-auth/client/plugins";
|
||||||
@@ -13,5 +14,12 @@ export const authClient = createAuthClient({
|
|||||||
twoFactorClient(),
|
twoFactorClient(),
|
||||||
apiKeyClient(),
|
apiKeyClient(),
|
||||||
adminClient(),
|
adminClient(),
|
||||||
|
inferAdditionalFields({
|
||||||
|
user: {
|
||||||
|
lastName: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,9 +6,6 @@
|
|||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
eslint: {
|
|
||||||
ignoreDuringBuilds: true,
|
|
||||||
},
|
|
||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.25.11",
|
"version": "v0.26.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"micromatch": "4.0.8",
|
"micromatch": "4.0.8",
|
||||||
"nanoid": "3.3.11",
|
"nanoid": "3.3.11",
|
||||||
"next": "^15.3.2",
|
"next": "^16.0.7",
|
||||||
"next-i18next": "^15.4.2",
|
"next-i18next": "^15.4.2",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"nextjs-toploader": "^3.9.17",
|
"nextjs-toploader": "^3.9.17",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { NextPage } from "next";
|
|||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Script from "next/script";
|
|
||||||
import { appWithTranslation } from "next-i18next";
|
import { appWithTranslation } from "next-i18next";
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
import NextTopLoader from "nextjs-toploader";
|
import NextTopLoader from "nextjs-toploader";
|
||||||
@@ -43,14 +42,6 @@ const MyApp = ({
|
|||||||
<Head>
|
<Head>
|
||||||
<title>Dokploy</title>
|
<title>Dokploy</title>
|
||||||
</Head>
|
</Head>
|
||||||
{process.env.NEXT_PUBLIC_UMAMI_HOST &&
|
|
||||||
process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && (
|
|
||||||
<Script
|
|
||||||
src={process.env.NEXT_PUBLIC_UMAMI_HOST}
|
|
||||||
data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="system"
|
defaultTheme="system"
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { user } = await validateRequest(ctx.req);
|
const { user } = await validateRequest(ctx.req);
|
||||||
if (!user || user.role !== "owner") {
|
if (!user || (user.role !== "owner" && user.role !== "admin")) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export async function getServerSideProps(
|
|||||||
}
|
}
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user || user.role === "member") {
|
if (!user || user.role !== "owner") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ const Page = () => {
|
|||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||||
<ProfileForm />
|
<ProfileForm />
|
||||||
{(data?.canAccessToAPI || data?.role === "owner") && <ShowApiKeys />}
|
{(data?.canAccessToAPI ||
|
||||||
|
data?.role === "owner" ||
|
||||||
|
data?.role === "admin") && <ShowApiKeys />}
|
||||||
|
|
||||||
{/* {isCloud && <RemoveSelfAccount />} */}
|
{/* {isCloud && <RemoveSelfAccount />} */}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ import { api } from "@/utils/api";
|
|||||||
const registerSchema = z
|
const registerSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1, {
|
name: z.string().min(1, {
|
||||||
message: "Name is required",
|
message: "First name is required",
|
||||||
|
}),
|
||||||
|
lastName: z.string().min(1, {
|
||||||
|
message: "Last name is required",
|
||||||
}),
|
}),
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
@@ -92,6 +95,7 @@ const Invitation = ({
|
|||||||
const form = useForm<Register>({
|
const form = useForm<Register>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
|
lastName: "",
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
@@ -115,6 +119,7 @@ const Invitation = ({
|
|||||||
email: values.email,
|
email: values.email,
|
||||||
password: values.password,
|
password: values.password,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
|
lastName: values.lastName,
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
headers: {
|
headers: {
|
||||||
"x-dokploy-token": token,
|
"x-dokploy-token": token,
|
||||||
@@ -197,12 +202,22 @@ const Invitation = ({
|
|||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>First Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input placeholder="John" {...field} />
|
||||||
placeholder="Enter your name"
|
</FormControl>
|
||||||
{...field}
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="lastName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Last Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Doe" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ import { authClient } from "@/lib/auth-client";
|
|||||||
const registerSchema = z
|
const registerSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1, {
|
name: z.string().min(1, {
|
||||||
message: "Name is required",
|
message: "First name is required",
|
||||||
|
}),
|
||||||
|
lastName: z.string().min(1, {
|
||||||
|
message: "Last name is required",
|
||||||
}),
|
}),
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
@@ -79,6 +82,7 @@ const Register = ({ isCloud }: Props) => {
|
|||||||
const form = useForm<Register>({
|
const form = useForm<Register>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
|
lastName: "",
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
@@ -95,6 +99,7 @@ const Register = ({ isCloud }: Props) => {
|
|||||||
email: values.email,
|
email: values.email,
|
||||||
password: values.password,
|
password: values.password,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
|
lastName: values.lastName,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -158,9 +163,22 @@ const Register = ({ isCloud }: Props) => {
|
|||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>First Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="name" {...field} />
|
<Input placeholder="John" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="lastName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Last Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Doe" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { findAdmin } from "@dokploy/server";
|
import { findOwner } from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { user } from "@dokploy/server/db/schema";
|
import { user } from "@dokploy/server/db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const result = await findAdmin();
|
const result = await findOwner();
|
||||||
|
|
||||||
const update = await db
|
const update = await db
|
||||||
.update(user)
|
.update(user)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { findAdmin, generateRandomPassword } from "@dokploy/server";
|
import { findOwner, generateRandomPassword } from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { account } from "@dokploy/server/db/schema";
|
import { account } from "@dokploy/server/db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
@@ -7,7 +7,7 @@ import { eq } from "drizzle-orm";
|
|||||||
try {
|
try {
|
||||||
const randomPassword = await generateRandomPassword();
|
const randomPassword = await generateRandomPassword();
|
||||||
|
|
||||||
const result = await findAdmin();
|
const result = await findOwner();
|
||||||
|
|
||||||
const update = await db
|
const update = await db
|
||||||
.update(account)
|
.update(account)
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import {
|
|||||||
createMount,
|
createMount,
|
||||||
deployMariadb,
|
deployMariadb,
|
||||||
findBackupsByDbId,
|
findBackupsByDbId,
|
||||||
findMariadbById,
|
|
||||||
findEnvironmentById,
|
findEnvironmentById,
|
||||||
|
findMariadbById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
rebuildDatabase,
|
rebuildDatabase,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import {
|
|||||||
createMount,
|
createMount,
|
||||||
deployMongo,
|
deployMongo,
|
||||||
findBackupsByDbId,
|
findBackupsByDbId,
|
||||||
findMongoById,
|
|
||||||
findEnvironmentById,
|
findEnvironmentById,
|
||||||
|
findMongoById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
rebuildDatabase,
|
rebuildDatabase,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
createCustomNotification,
|
||||||
createDiscordNotification,
|
createDiscordNotification,
|
||||||
createEmailNotification,
|
createEmailNotification,
|
||||||
createGotifyNotification,
|
createGotifyNotification,
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
findNotificationById,
|
findNotificationById,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
removeNotificationById,
|
removeNotificationById,
|
||||||
|
sendCustomNotification,
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
sendGotifyNotification,
|
sendGotifyNotification,
|
||||||
@@ -17,6 +19,7 @@ import {
|
|||||||
sendServerThresholdNotifications,
|
sendServerThresholdNotifications,
|
||||||
sendSlackNotification,
|
sendSlackNotification,
|
||||||
sendTelegramNotification,
|
sendTelegramNotification,
|
||||||
|
updateCustomNotification,
|
||||||
updateDiscordNotification,
|
updateDiscordNotification,
|
||||||
updateEmailNotification,
|
updateEmailNotification,
|
||||||
updateGotifyNotification,
|
updateGotifyNotification,
|
||||||
@@ -36,6 +39,7 @@ import {
|
|||||||
} from "@/server/api/trpc";
|
} from "@/server/api/trpc";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import {
|
import {
|
||||||
|
apiCreateCustom,
|
||||||
apiCreateDiscord,
|
apiCreateDiscord,
|
||||||
apiCreateEmail,
|
apiCreateEmail,
|
||||||
apiCreateGotify,
|
apiCreateGotify,
|
||||||
@@ -44,6 +48,7 @@ import {
|
|||||||
apiCreateSlack,
|
apiCreateSlack,
|
||||||
apiCreateTelegram,
|
apiCreateTelegram,
|
||||||
apiFindOneNotification,
|
apiFindOneNotification,
|
||||||
|
apiTestCustomConnection,
|
||||||
apiTestDiscordConnection,
|
apiTestDiscordConnection,
|
||||||
apiTestEmailConnection,
|
apiTestEmailConnection,
|
||||||
apiTestGotifyConnection,
|
apiTestGotifyConnection,
|
||||||
@@ -51,6 +56,7 @@ import {
|
|||||||
apiTestNtfyConnection,
|
apiTestNtfyConnection,
|
||||||
apiTestSlackConnection,
|
apiTestSlackConnection,
|
||||||
apiTestTelegramConnection,
|
apiTestTelegramConnection,
|
||||||
|
apiUpdateCustom,
|
||||||
apiUpdateDiscord,
|
apiUpdateDiscord,
|
||||||
apiUpdateEmail,
|
apiUpdateEmail,
|
||||||
apiUpdateGotify,
|
apiUpdateGotify,
|
||||||
@@ -334,6 +340,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
email: true,
|
email: true,
|
||||||
gotify: true,
|
gotify: true,
|
||||||
ntfy: true,
|
ntfy: true,
|
||||||
|
custom: true,
|
||||||
lark: true,
|
lark: true,
|
||||||
},
|
},
|
||||||
orderBy: desc(notifications.createdAt),
|
orderBy: desc(notifications.createdAt),
|
||||||
@@ -518,6 +525,59 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
createCustom: adminProcedure
|
||||||
|
.input(apiCreateCustom)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
return await createCustomNotification(
|
||||||
|
input,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error creating the notification",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
updateCustom: adminProcedure
|
||||||
|
.input(apiUpdateCustom)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const notification = await findNotificationById(input.notificationId);
|
||||||
|
if (notification.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to update this notification",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await updateCustomNotification({
|
||||||
|
...input,
|
||||||
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
testCustomConnection: adminProcedure
|
||||||
|
.input(apiTestCustomConnection)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
await sendCustomNotification(input, {
|
||||||
|
title: "Test Notification",
|
||||||
|
message: "Hi, From Dokploy 👋",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: `${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
createLark: adminProcedure
|
createLark: adminProcedure
|
||||||
.input(apiCreateLark)
|
.input(apiCreateLark)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
if (ctx.user.role !== "owner" && !IS_CLOUD) {
|
if (ctx.user.role !== "owner" && ctx.user.role !== "admin" && !IS_CLOUD) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "FORBIDDEN",
|
code: "FORBIDDEN",
|
||||||
message: "Only the organization owner can create an organization",
|
message: "Only the organization owner can create an organization",
|
||||||
@@ -32,8 +32,6 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
.returning()
|
.returning()
|
||||||
.then((res) => res[0]);
|
.then((res) => res[0]);
|
||||||
|
|
||||||
console.log("result", result);
|
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
@@ -82,7 +80,22 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
organizationId: z.string(),
|
organizationId: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
|
// Verify user is a member of this organization
|
||||||
|
const userMember = await db.query.member.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(member.organizationId, input.organizationId),
|
||||||
|
eq(member.userId, ctx.user.id),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userMember) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "You are not a member of this organization",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return await db.query.organization.findFirst({
|
return await db.query.organization.findFirst({
|
||||||
where: eq(organization.id, input.organizationId),
|
where: eq(organization.id, input.organizationId),
|
||||||
});
|
});
|
||||||
@@ -96,12 +109,45 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
if (ctx.user.role !== "owner" && !IS_CLOUD) {
|
// First, verify the organization exists
|
||||||
|
const org = await db.query.organization.findFirst({
|
||||||
|
where: eq(organization.id, input.organizationId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!org) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Organization not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify user is a member of this organization
|
||||||
|
const userMember = await db.query.member.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(member.organizationId, input.organizationId),
|
||||||
|
eq(member.userId, ctx.user.id),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userMember) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "You are not a member of this organization",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only owners can update the organization
|
||||||
|
// Verify the user is either the organization owner or has the owner role
|
||||||
|
const isOwner =
|
||||||
|
org.ownerId === ctx.user.id || userMember.role === "owner";
|
||||||
|
|
||||||
|
if (!isOwner) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "FORBIDDEN",
|
code: "FORBIDDEN",
|
||||||
message: "Only the organization owner can update it",
|
message: "Only the organization owner can update it",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await db
|
const result = await db
|
||||||
.update(organization)
|
.update(organization)
|
||||||
.set({
|
.set({
|
||||||
@@ -119,12 +165,7 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
if (ctx.user.role !== "owner" && !IS_CLOUD) {
|
// First, verify the organization exists
|
||||||
throw new TRPCError({
|
|
||||||
code: "FORBIDDEN",
|
|
||||||
message: "Only the organization owner can delete it",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const org = await db.query.organization.findFirst({
|
const org = await db.query.organization.findFirst({
|
||||||
where: eq(organization.id, input.organizationId),
|
where: eq(organization.id, input.organizationId),
|
||||||
});
|
});
|
||||||
@@ -136,7 +177,27 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (org.ownerId !== ctx.user.id) {
|
// Verify user is a member of this organization
|
||||||
|
const userMember = await db.query.member.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(member.organizationId, input.organizationId),
|
||||||
|
eq(member.userId, ctx.user.id),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userMember) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "You are not a member of this organization",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only owners can delete the organization
|
||||||
|
// Verify the user is either the organization owner or has the owner role
|
||||||
|
const isOwner =
|
||||||
|
org.ownerId === ctx.user.id || userMember.role === "owner";
|
||||||
|
|
||||||
|
if (!isOwner) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "FORBIDDEN",
|
code: "FORBIDDEN",
|
||||||
message: "Only the organization owner can delete it",
|
message: "Only the organization owner can delete it",
|
||||||
@@ -194,6 +255,65 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
.delete(invitation)
|
.delete(invitation)
|
||||||
.where(eq(invitation.id, input.invitationId));
|
.where(eq(invitation.id, input.invitationId));
|
||||||
}),
|
}),
|
||||||
|
updateMemberRole: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
memberId: z.string(),
|
||||||
|
role: z.enum(["admin", "member"]),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
// Fetch the target member
|
||||||
|
const target = await db.query.member.findFirst({
|
||||||
|
where: eq(member.id, input.memberId),
|
||||||
|
with: { user: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND", message: "Member not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "You are not allowed to update this member's role",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent users from changing their own role
|
||||||
|
if (target.userId === ctx.user.id) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "You cannot change your own role",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owner role is intransferible - cannot change to or from owner
|
||||||
|
if (target.role === "owner") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "The owner role is intransferible",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only owners can change admin roles
|
||||||
|
// Admins can only change member roles
|
||||||
|
if (ctx.user.role === "admin" && target.role === "admin") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message:
|
||||||
|
"Only the organization owner can change admin roles. Admins can only modify member roles.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the target member's role
|
||||||
|
await db
|
||||||
|
.update(member)
|
||||||
|
.set({ role: input.role })
|
||||||
|
.where(eq(member.id, input.memberId));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
setDefault: protectedProcedure
|
setDefault: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const scheduleRouter = createTRPCRouter({
|
|||||||
scheduleId: newSchedule.scheduleId,
|
scheduleId: newSchedule.scheduleId,
|
||||||
type: "schedule",
|
type: "schedule",
|
||||||
cronSchedule: newSchedule.cronExpression,
|
cronSchedule: newSchedule.cronExpression,
|
||||||
|
timezone: newSchedule.timezone,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
scheduleJob(newSchedule);
|
scheduleJob(newSchedule);
|
||||||
@@ -49,6 +50,7 @@ export const scheduleRouter = createTRPCRouter({
|
|||||||
scheduleId: updatedSchedule.scheduleId,
|
scheduleId: updatedSchedule.scheduleId,
|
||||||
type: "schedule",
|
type: "schedule",
|
||||||
cronSchedule: updatedSchedule.cronExpression,
|
cronSchedule: updatedSchedule.cronExpression,
|
||||||
|
timezone: updatedSchedule.timezone,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await removeJob({
|
await removeJob({
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
canAccessToTraefikFiles,
|
canAccessToTraefikFiles,
|
||||||
checkGPUStatus,
|
checkGPUStatus,
|
||||||
cleanStoppedContainers,
|
checkPortInUse,
|
||||||
cleanUpDockerBuilder,
|
cleanupAll,
|
||||||
cleanUpSystemPrune,
|
cleanupBuilders,
|
||||||
cleanUpUnusedImages,
|
cleanupContainers,
|
||||||
cleanUpUnusedVolumes,
|
cleanupImages,
|
||||||
|
cleanupSystem,
|
||||||
|
cleanupVolumes,
|
||||||
DEFAULT_UPDATE_DATA,
|
DEFAULT_UPDATE_DATA,
|
||||||
execAsync,
|
execAsync,
|
||||||
findServerById,
|
findServerById,
|
||||||
@@ -130,6 +132,17 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
let newPorts = ports;
|
let newPorts = ports;
|
||||||
// If receive true, add 8080 to ports
|
// If receive true, add 8080 to ports
|
||||||
if (input.enableDashboard) {
|
if (input.enableDashboard) {
|
||||||
|
// Check if port 8080 is already in use before enabling dashboard
|
||||||
|
const portCheck = await checkPortInUse(8080, input.serverId);
|
||||||
|
if (portCheck.isInUse) {
|
||||||
|
const conflictingContainer = portCheck.conflictingContainer
|
||||||
|
? ` by container "${portCheck.conflictingContainer}"`
|
||||||
|
: "";
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: `Port 8080 is already in use${conflictingContainer}. Please stop the conflicting service or use a different port for the Traefik dashboard.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
newPorts.push({
|
newPorts.push({
|
||||||
targetPort: 8080,
|
targetPort: 8080,
|
||||||
publishedPort: 8080,
|
publishedPort: 8080,
|
||||||
@@ -149,41 +162,38 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
cleanUnusedImages: adminProcedure
|
cleanUnusedImages: adminProcedure
|
||||||
.input(apiServerSchema)
|
.input(apiServerSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await cleanUpUnusedImages(input?.serverId);
|
await cleanupImages(input?.serverId);
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
cleanUnusedVolumes: adminProcedure
|
cleanUnusedVolumes: adminProcedure
|
||||||
.input(apiServerSchema)
|
.input(apiServerSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await cleanUpUnusedVolumes(input?.serverId);
|
await cleanupVolumes(input?.serverId);
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
cleanStoppedContainers: adminProcedure
|
cleanStoppedContainers: adminProcedure
|
||||||
.input(apiServerSchema)
|
.input(apiServerSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await cleanStoppedContainers(input?.serverId);
|
await cleanupContainers(input?.serverId);
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
cleanDockerBuilder: adminProcedure
|
cleanDockerBuilder: adminProcedure
|
||||||
.input(apiServerSchema)
|
.input(apiServerSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await cleanUpDockerBuilder(input?.serverId);
|
await cleanupBuilders(input?.serverId);
|
||||||
}),
|
}),
|
||||||
cleanDockerPrune: adminProcedure
|
cleanDockerPrune: adminProcedure
|
||||||
.input(apiServerSchema)
|
.input(apiServerSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await cleanUpSystemPrune(input?.serverId);
|
await cleanupSystem(input?.serverId);
|
||||||
await cleanUpDockerBuilder(input?.serverId);
|
await cleanupBuilders(input?.serverId);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
cleanAll: adminProcedure
|
cleanAll: adminProcedure
|
||||||
.input(apiServerSchema)
|
.input(apiServerSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await cleanUpUnusedImages(input?.serverId);
|
await cleanupAll(input?.serverId);
|
||||||
await cleanStoppedContainers(input?.serverId);
|
|
||||||
await cleanUpDockerBuilder(input?.serverId);
|
|
||||||
await cleanUpSystemPrune(input?.serverId);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
@@ -201,7 +211,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await updateUser(ctx.user.id, {
|
await updateUser(ctx.user.ownerId, {
|
||||||
sshPrivateKey: input.sshPrivateKey,
|
sshPrivateKey: input.sshPrivateKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -213,11 +223,9 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const user = await updateUser(ctx.user.id, {
|
const user = await updateUser(ctx.user.ownerId, {
|
||||||
host: input.host,
|
host: input.host,
|
||||||
...(input.letsEncryptEmail && {
|
letsEncryptEmail: input.letsEncryptEmail,
|
||||||
letsEncryptEmail: input.letsEncryptEmail,
|
|
||||||
}),
|
|
||||||
certificateType: input.certificateType,
|
certificateType: input.certificateType,
|
||||||
https: input.https,
|
https: input.https,
|
||||||
});
|
});
|
||||||
@@ -240,7 +248,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await updateUser(ctx.user.id, {
|
await updateUser(ctx.user.ownerId, {
|
||||||
sshPrivateKey: null,
|
sshPrivateKey: null,
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
@@ -281,9 +289,9 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
console.log(
|
console.log(
|
||||||
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||||
);
|
);
|
||||||
await cleanUpUnusedImages(server.serverId);
|
|
||||||
await cleanUpDockerBuilder(server.serverId);
|
await cleanupAll(server.serverId);
|
||||||
await cleanUpSystemPrune(server.serverId);
|
|
||||||
await sendDockerCleanupNotifications(server.organizationId);
|
await sendDockerCleanupNotifications(server.organizationId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -300,7 +308,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!IS_CLOUD) {
|
} else if (!IS_CLOUD) {
|
||||||
const userUpdated = await updateUser(ctx.user.id, {
|
const userUpdated = await updateUser(ctx.user.ownerId, {
|
||||||
enableDockerCleanup: input.enableDockerCleanup,
|
enableDockerCleanup: input.enableDockerCleanup,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -309,9 +317,9 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
console.log(
|
console.log(
|
||||||
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||||
);
|
);
|
||||||
await cleanUpUnusedImages();
|
|
||||||
await cleanUpDockerBuilder();
|
await cleanupAll();
|
||||||
await cleanUpSystemPrune();
|
|
||||||
await sendDockerCleanupNotifications(
|
await sendDockerCleanupNotifications(
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
@@ -810,6 +818,19 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
"dokploy-traefik",
|
"dokploy-traefik",
|
||||||
input?.serverId,
|
input?.serverId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
for (const port of input.additionalPorts) {
|
||||||
|
const portCheck = await checkPortInUse(
|
||||||
|
port.publishedPort,
|
||||||
|
input.serverId,
|
||||||
|
);
|
||||||
|
if (portCheck.isInUse) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
message: `Port ${port.targetPort} is already in use by ${portCheck.conflictingContainer}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
const preparedEnv = prepareEnvironmentVariables(env);
|
const preparedEnv = prepareEnvironmentVariables(env);
|
||||||
|
|
||||||
await writeTraefikSetup({
|
await writeTraefikSetup({
|
||||||
|
|||||||
@@ -56,15 +56,16 @@ export const stripeRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const items = getStripeItems(input.serverQuantity, input.isAnnual);
|
const items = getStripeItems(input.serverQuantity, input.isAnnual);
|
||||||
const user = await findUserById(ctx.user.id);
|
// Always operate on the organization owner's Stripe customer
|
||||||
|
const owner = await findUserById(ctx.user.ownerId);
|
||||||
|
|
||||||
let stripeCustomerId = user.stripeCustomerId;
|
let stripeCustomerId = owner.stripeCustomerId;
|
||||||
|
|
||||||
if (stripeCustomerId) {
|
if (stripeCustomerId) {
|
||||||
const customer = await stripe.customers.retrieve(stripeCustomerId);
|
const customer = await stripe.customers.retrieve(stripeCustomerId);
|
||||||
|
|
||||||
if (customer.deleted) {
|
if (customer.deleted) {
|
||||||
await updateUser(user.id, {
|
await updateUser(owner.id, {
|
||||||
stripeCustomerId: null,
|
stripeCustomerId: null,
|
||||||
});
|
});
|
||||||
stripeCustomerId = null;
|
stripeCustomerId = null;
|
||||||
@@ -78,7 +79,7 @@ export const stripeRouter = createTRPCRouter({
|
|||||||
customer: stripeCustomerId,
|
customer: stripeCustomerId,
|
||||||
}),
|
}),
|
||||||
metadata: {
|
metadata: {
|
||||||
adminId: user.id,
|
adminId: owner.id,
|
||||||
},
|
},
|
||||||
allow_promotion_codes: true,
|
allow_promotion_codes: true,
|
||||||
success_url: `${WEBSITE_URL}/dashboard/settings/servers?success=true`,
|
success_url: `${WEBSITE_URL}/dashboard/settings/servers?success=true`,
|
||||||
@@ -88,15 +89,16 @@ export const stripeRouter = createTRPCRouter({
|
|||||||
return { sessionId: session.id };
|
return { sessionId: session.id };
|
||||||
}),
|
}),
|
||||||
createCustomerPortalSession: adminProcedure.mutation(async ({ ctx }) => {
|
createCustomerPortalSession: adminProcedure.mutation(async ({ ctx }) => {
|
||||||
const user = await findUserById(ctx.user.id);
|
// Use the organization's owner account for billing portal
|
||||||
|
const owner = await findUserById(ctx.user.ownerId);
|
||||||
|
|
||||||
if (!user.stripeCustomerId) {
|
if (!owner.stripeCustomerId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Stripe Customer ID not found",
|
message: "Stripe Customer ID not found",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const stripeCustomerId = user.stripeCustomerId;
|
const stripeCustomerId = owner.stripeCustomerId;
|
||||||
|
|
||||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
||||||
apiVersion: "2024-09-30.acacia",
|
apiVersion: "2024-09-30.acacia",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
createApiKey,
|
createApiKey,
|
||||||
findAdmin,
|
|
||||||
findNotificationById,
|
findNotificationById,
|
||||||
findOrganizationById,
|
findOrganizationById,
|
||||||
findUserById,
|
findUserById,
|
||||||
@@ -87,7 +86,11 @@ export const userRouter = createTRPCRouter({
|
|||||||
// Allow access if:
|
// Allow access if:
|
||||||
// 1. User is requesting their own information
|
// 1. User is requesting their own information
|
||||||
// 2. User has owner role (admin permissions) AND user is in the same organization
|
// 2. User has owner role (admin permissions) AND user is in the same organization
|
||||||
if (memberResult.userId !== ctx.user.id && ctx.user.role !== "owner") {
|
if (
|
||||||
|
memberResult.userId !== ctx.user.id &&
|
||||||
|
ctx.user.role !== "owner" &&
|
||||||
|
ctx.user.role !== "admin"
|
||||||
|
) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You are not authorized to access this user",
|
message: "You are not authorized to access this user",
|
||||||
@@ -223,10 +226,61 @@ export const userRouter = createTRPCRouter({
|
|||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the acting user has admin privileges in the active organization
|
||||||
|
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Only owners or admins can delete users",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch target member within the active organization
|
||||||
|
const targetMember = await db.query.member.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(member.userId, input.userId),
|
||||||
|
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!targetMember) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Target user is not a member of this organization",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Never allow deleting the organization owner via this endpoint
|
||||||
|
if (targetMember.role === "owner") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "You cannot delete the organization owner",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin self-protection: an admin cannot delete themselves
|
||||||
|
if (targetMember.role === "admin" && input.userId === ctx.user.id) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message:
|
||||||
|
"Admins cannot delete themselves. Ask the owner or another admin.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only owners can delete admins
|
||||||
|
// Admins can only delete members
|
||||||
|
if (ctx.user.role === "admin" && targetMember.role === "admin") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message:
|
||||||
|
"Only the organization owner can delete admins. Admins can only delete members.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return await removeUserById(input.userId);
|
return await removeUserById(input.userId);
|
||||||
}),
|
}),
|
||||||
assignPermissions: adminProcedure
|
assignPermissions: adminProcedure
|
||||||
@@ -376,6 +430,23 @@ export const userRouter = createTRPCRouter({
|
|||||||
createApiKey: protectedProcedure
|
createApiKey: protectedProcedure
|
||||||
.input(apiCreateApiKey)
|
.input(apiCreateApiKey)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
// Verify user is a member of the organization specified in metadata
|
||||||
|
if (input.metadata?.organizationId) {
|
||||||
|
const userMember = await db.query.member.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(member.organizationId, input.metadata.organizationId),
|
||||||
|
eq(member.userId, ctx.user.id),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userMember) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "You are not a member of this organization",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const apiKey = await createApiKey(ctx.user.id, input);
|
const apiKey = await createApiKey(ctx.user.id, input);
|
||||||
return apiKey;
|
return apiKey;
|
||||||
}),
|
}),
|
||||||
@@ -386,7 +457,35 @@ export const userRouter = createTRPCRouter({
|
|||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
// Users can check their own organizations
|
||||||
|
// Admins and owners can check organizations of members in their active organization
|
||||||
|
if (input.userId !== ctx.user.id) {
|
||||||
|
// Verify the target user is a member of the active organization
|
||||||
|
const targetMember = await db.query.member.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(member.userId, input.userId),
|
||||||
|
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!targetMember) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "User is not a member of your active organization",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only admins and owners can check other users' organizations
|
||||||
|
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message:
|
||||||
|
"Only admins and owners can check other users' organizations",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const organizations = await db.query.member.findMany({
|
const organizations = await db.query.member.findMany({
|
||||||
where: eq(member.userId, input.userId),
|
where: eq(member.userId, input.userId),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -183,7 +183,11 @@ export const uploadProcedure = async (opts: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
||||||
if (!ctx.session || !ctx.user || ctx.user.role !== "owner") {
|
if (
|
||||||
|
!ctx.session ||
|
||||||
|
!ctx.user ||
|
||||||
|
(ctx.user.role !== "owner" && ctx.user.role !== "admin")
|
||||||
|
) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
}
|
}
|
||||||
return next({
|
return next({
|
||||||
@@ -197,7 +201,11 @@ export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const adminProcedure = t.procedure.use(({ ctx, next }) => {
|
export const adminProcedure = t.procedure.use(({ ctx, next }) => {
|
||||||
if (!ctx.session || !ctx.user || ctx.user.role !== "owner") {
|
if (
|
||||||
|
!ctx.session ||
|
||||||
|
!ctx.user ||
|
||||||
|
(ctx.user.role !== "owner" && ctx.user.role !== "admin")
|
||||||
|
) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
}
|
}
|
||||||
return next({
|
return next({
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import {
|
|||||||
createDefaultServerTraefikConfig,
|
createDefaultServerTraefikConfig,
|
||||||
createDefaultTraefikConfig,
|
createDefaultTraefikConfig,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
|
initCancelDeployments,
|
||||||
initCronJobs,
|
initCronJobs,
|
||||||
initializeNetwork,
|
initializeNetwork,
|
||||||
initSchedules,
|
initSchedules,
|
||||||
initVolumeBackupsCronJobs,
|
initVolumeBackupsCronJobs,
|
||||||
initCancelDeployments,
|
|
||||||
sendDokployRestartNotifications,
|
sendDokployRestartNotifications,
|
||||||
setupDirectories,
|
setupDirectories,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
@@ -26,6 +26,16 @@ config({ path: ".env" });
|
|||||||
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
||||||
const HOST = process.env.HOST || "0.0.0.0";
|
const HOST = process.env.HOST || "0.0.0.0";
|
||||||
const dev = process.env.NODE_ENV !== "production";
|
const dev = process.env.NODE_ENV !== "production";
|
||||||
|
|
||||||
|
// Initialize critical directories and Traefik config BEFORE Next.js starts
|
||||||
|
// This prevents race conditions with the install script
|
||||||
|
if (process.env.NODE_ENV === "production" && !IS_CLOUD) {
|
||||||
|
setupDirectories();
|
||||||
|
createDefaultTraefikConfig();
|
||||||
|
createDefaultServerTraefikConfig();
|
||||||
|
console.log("✅ Critical initialization complete");
|
||||||
|
}
|
||||||
|
|
||||||
const app = next({ dev, turbopack: process.env.TURBOPACK === "1" });
|
const app = next({ dev, turbopack: process.env.TURBOPACK === "1" });
|
||||||
const handle = app.getRequestHandler();
|
const handle = app.getRequestHandler();
|
||||||
void app.prepare().then(async () => {
|
void app.prepare().then(async () => {
|
||||||
@@ -45,11 +55,8 @@ void app.prepare().then(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production" && !IS_CLOUD) {
|
if (process.env.NODE_ENV === "production" && !IS_CLOUD) {
|
||||||
setupDirectories();
|
|
||||||
createDefaultMiddlewares();
|
createDefaultMiddlewares();
|
||||||
await initializeNetwork();
|
await initializeNetwork();
|
||||||
createDefaultTraefikConfig();
|
|
||||||
createDefaultServerTraefikConfig();
|
|
||||||
await migration();
|
await migration();
|
||||||
await initCronJobs();
|
await initCronJobs();
|
||||||
await initSchedules();
|
await initSchedules();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type QueueJob =
|
|||||||
type: "schedule";
|
type: "schedule";
|
||||||
cronSchedule: string;
|
cronSchedule: string;
|
||||||
scheduleId: string;
|
scheduleId: string;
|
||||||
|
timezone?: string | null;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "volume-backup";
|
type: "volume-backup";
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ export const setupDockerContainerLogsWebSocketServer = (
|
|||||||
const command = search
|
const command = search
|
||||||
? `${baseCommand} 2>&1 | grep --line-buffered -iF "${escapedSearch}"`
|
? `${baseCommand} 2>&1 | grep --line-buffered -iF "${escapedSearch}"`
|
||||||
: baseCommand;
|
: baseCommand;
|
||||||
client.exec(command, (err, stream) => {
|
// Use pty: true to ensure the remote process receives SIGHUP when SSH connection closes
|
||||||
|
// This is crucial for terminating docker logs processes when the connection is closed
|
||||||
|
client.exec(command, { pty: true }, (err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Execution error:", err);
|
console.error("Execution error:", err);
|
||||||
ws.close();
|
ws.close();
|
||||||
|
|||||||
@@ -58,7 +58,12 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
|||||||
`docker exec -it -w / ${containerId} ${activeWay}`,
|
`docker exec -it -w / ${containerId} ${activeWay}`,
|
||||||
{ pty: true },
|
{ pty: true },
|
||||||
(err, stream) => {
|
(err, stream) => {
|
||||||
if (err) throw err;
|
if (err) {
|
||||||
|
console.error("SSH exec error:", err);
|
||||||
|
ws.close();
|
||||||
|
conn.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
stream
|
stream
|
||||||
.on("close", (code: number, _signal: string) => {
|
.on("close", (code: number, _signal: string) => {
|
||||||
@@ -93,10 +98,20 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
|||||||
|
|
||||||
ws.on("close", () => {
|
ws.on("close", () => {
|
||||||
stream.end();
|
stream.end();
|
||||||
|
// Ensure SSH connection is closed when WebSocket closes
|
||||||
|
conn.end();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
.on("error", (err) => {
|
||||||
|
console.error("SSH connection error:", err);
|
||||||
|
if (ws.readyState === ws.OPEN) {
|
||||||
|
ws.send(`SSH error: ${err.message}`);
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
conn.end();
|
||||||
|
})
|
||||||
.connect({
|
.connect({
|
||||||
host: server.ipAddress,
|
host: server.ipAddress,
|
||||||
port: server.port,
|
port: server.port,
|
||||||
|
|||||||
@@ -31,8 +31,11 @@ export const setupDeploymentLogsWebSocketServer = (
|
|||||||
const serverId = url.searchParams.get("serverId");
|
const serverId = url.searchParams.get("serverId");
|
||||||
const { user, session } = await validateRequest(req);
|
const { user, session } = await validateRequest(req);
|
||||||
|
|
||||||
|
// Generate unique connection ID for tracking
|
||||||
|
const connectionId = `deployment-logs-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
||||||
|
|
||||||
if (!logPath) {
|
if (!logPath) {
|
||||||
console.log("logPath no provided");
|
console.log(`[${connectionId}] logPath no provided`);
|
||||||
ws.close(4000, "logPath no provided");
|
ws.close(4000, "logPath no provided");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -42,40 +45,55 @@ export const setupDeploymentLogsWebSocketServer = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let tailProcess: ReturnType<typeof spawn> | null = null;
|
||||||
|
let sshClient: Client | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
const server = await findServerById(serverId);
|
const server = await findServerById(serverId);
|
||||||
|
|
||||||
if (!server.sshKeyId) return;
|
if (!server.sshKeyId) {
|
||||||
const client = new Client();
|
ws.close();
|
||||||
client
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sshClient = new Client();
|
||||||
|
sshClient
|
||||||
.on("ready", () => {
|
.on("ready", () => {
|
||||||
const command = `
|
const command = `
|
||||||
tail -n +1 -f ${logPath};
|
tail -n +1 -f ${logPath};
|
||||||
`;
|
`;
|
||||||
client.exec(command, (err, stream) => {
|
sshClient!.exec(command, (err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Execution error:", err);
|
sshClient!.end();
|
||||||
ws.close();
|
ws.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stream
|
stream
|
||||||
.on("close", () => {
|
.on("close", () => {
|
||||||
client.end();
|
sshClient!.end();
|
||||||
ws.close();
|
ws.close();
|
||||||
})
|
})
|
||||||
.on("data", (data: string) => {
|
.on("data", (data: string) => {
|
||||||
ws.send(data.toString());
|
if (ws.readyState === ws.OPEN) {
|
||||||
|
ws.send(data.toString());
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.stderr.on("data", (data) => {
|
.stderr.on("data", (data) => {
|
||||||
ws.send(data.toString());
|
if (ws.readyState === ws.OPEN) {
|
||||||
|
ws.send(data.toString());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.on("error", (err) => {
|
.on("error", (err) => {
|
||||||
console.error("SSH connection error:", err);
|
if (ws.readyState === ws.OPEN) {
|
||||||
ws.send(`SSH error: ${err.message}`);
|
ws.send(`SSH error: ${err.message}`);
|
||||||
ws.close(); // Cierra el WebSocket si hay un error con SSH
|
ws.close();
|
||||||
|
}
|
||||||
|
if (sshClient) {
|
||||||
|
sshClient.end();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.connect({
|
.connect({
|
||||||
host: server.ipAddress,
|
host: server.ipAddress,
|
||||||
@@ -85,26 +103,75 @@ export const setupDeploymentLogsWebSocketServer = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
ws.on("close", () => {
|
ws.on("close", () => {
|
||||||
client.end();
|
if (sshClient) {
|
||||||
|
sshClient.end();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const tail = spawn("tail", ["-n", "+1", "-f", logPath]);
|
tailProcess = spawn("tail", ["-n", "+1", "-f", logPath]);
|
||||||
|
|
||||||
tail.stdout.on("data", (data) => {
|
const stdout = tailProcess.stdout;
|
||||||
ws.send(data.toString());
|
const stderr = tailProcess.stderr;
|
||||||
});
|
|
||||||
|
|
||||||
tail.stderr.on("data", (data) => {
|
if (stdout) {
|
||||||
ws.send(new Error(`tail error: ${data.toString()}`).message);
|
stdout.on("data", (data) => {
|
||||||
});
|
if (ws.readyState === ws.OPEN) {
|
||||||
tail.on("close", () => {
|
ws.send(data.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
stderr.on("data", (data) => {
|
||||||
|
if (ws.readyState === ws.OPEN) {
|
||||||
|
ws.send(new Error(`tail error: ${data.toString()}`).message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tailProcess.on("close", () => {
|
||||||
ws.close();
|
ws.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tailProcess.on("error", () => {
|
||||||
|
if (ws.readyState === ws.OPEN) {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("close", () => {
|
||||||
|
if (tailProcess && !tailProcess.killed) {
|
||||||
|
tailProcess.kill("SIGTERM");
|
||||||
|
// Force kill after a timeout if it doesn't terminate
|
||||||
|
setTimeout(() => {
|
||||||
|
if (tailProcess && !tailProcess.killed) {
|
||||||
|
tailProcess.kill("SIGKILL");
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Clean up resources on error
|
||||||
|
if (tailProcess && !tailProcess.killed) {
|
||||||
|
tailProcess.kill("SIGTERM");
|
||||||
|
setTimeout(() => {
|
||||||
|
if (tailProcess && !tailProcess.killed) {
|
||||||
|
tailProcess.kill("SIGKILL");
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
if (sshClient) {
|
||||||
|
sshClient.end();
|
||||||
|
}
|
||||||
|
if (ws.readyState === ws.OPEN) {
|
||||||
|
// @ts-ignore
|
||||||
|
const errorMessage = error?.message as unknown as string;
|
||||||
|
ws.send(errorMessage || "An error occurred");
|
||||||
|
ws.close();
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// @ts-ignore
|
|
||||||
// const errorMessage = error?.message as unknown as string;
|
|
||||||
// ws.send(errorMessage);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,21 +8,22 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
|
||||||
/* Strictness */
|
/* Strictness */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
|
|
||||||
/* Bundled projects */
|
/* Bundled projects */
|
||||||
"lib": ["dom", "dom.iterable", "ES2022"],
|
"lib": ["dom", "dom.iterable", "ES2022"],
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"jsx": "preserve",
|
"jsx": "react-jsx",
|
||||||
"plugins": [{ "name": "next" }],
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
|
|
||||||
/* Path Aliases */
|
/* Path Aliases */
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
@@ -30,7 +31,6 @@
|
|||||||
"@dokploy/server/*": ["../../packages/server/src/*"]
|
"@dokploy/server/*": ["../../packages/server/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"include": [
|
"include": [
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const scheduleJob = (job: QueueJob) => {
|
|||||||
jobQueue.add(job.scheduleId, job, {
|
jobQueue.add(job.scheduleId, job, {
|
||||||
repeat: {
|
repeat: {
|
||||||
pattern: job.cronSchedule,
|
pattern: job.cronSchedule,
|
||||||
|
tz: job.timezone || "UTC",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (job.type === "volume-backup") {
|
} else if (job.type === "volume-backup") {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const jobQueueSchema = z.discriminatedUnion("type", [
|
|||||||
cronSchedule: z.string(),
|
cronSchedule: z.string(),
|
||||||
type: z.literal("schedule"),
|
type: z.literal("schedule"),
|
||||||
scheduleId: z.string(),
|
scheduleId: z.string(),
|
||||||
|
timezone: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
cronSchedule: z.string(),
|
cronSchedule: z.string(),
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
cleanUpDockerBuilder,
|
cleanupAll,
|
||||||
cleanUpSystemPrune,
|
|
||||||
cleanUpUnusedImages,
|
|
||||||
findBackupById,
|
findBackupById,
|
||||||
findScheduleById,
|
findScheduleById,
|
||||||
findServerById,
|
findServerById,
|
||||||
@@ -91,9 +89,7 @@ export const runJobs = async (job: QueueJob) => {
|
|||||||
logger.info("Server is inactive");
|
logger.info("Server is inactive");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await cleanUpUnusedImages(serverId);
|
await cleanupAll(serverId);
|
||||||
await cleanUpDockerBuilder(serverId);
|
|
||||||
await cleanUpSystemPrune(serverId);
|
|
||||||
} else if (job.type === "schedule") {
|
} else if (job.type === "schedule") {
|
||||||
const { scheduleId } = job;
|
const { scheduleId } = job;
|
||||||
const schedule = await findScheduleById(scheduleId);
|
const schedule = await findScheduleById(scheduleId);
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
"switch:prod": "node scripts/switchToDist.js",
|
"switch:prod": "node scripts/switchToDist.js",
|
||||||
"dev": "rm -rf ./dist && pnpm esbuild && tsc --emitDeclarationOnly --outDir dist -p tsconfig.server.json",
|
"dev": "rm -rf ./dist && pnpm esbuild && tsc --emitDeclarationOnly --outDir dist -p tsconfig.server.json",
|
||||||
"esbuild": "tsx ./esbuild.config.ts && tsc --project tsconfig.server.json --emitDeclarationOnly ",
|
"esbuild": "tsx ./esbuild.config.ts && tsc --project tsconfig.server.json --emitDeclarationOnly ",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"dbml:generate": "npx tsx src/db/schema/dbml.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^2.0.5",
|
"@ai-sdk/anthropic": "^2.0.5",
|
||||||
|
|||||||
1200
packages/server/schema.dbml
Normal file
1200
packages/server/schema.dbml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -187,6 +187,12 @@ export const applications = pgTable("application", {
|
|||||||
registryId: text("registryId").references(() => registry.registryId, {
|
registryId: text("registryId").references(() => registry.registryId, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
|
rollbackRegistryId: text("rollbackRegistryId").references(
|
||||||
|
() => registry.registryId,
|
||||||
|
{
|
||||||
|
onDelete: "set null",
|
||||||
|
},
|
||||||
|
),
|
||||||
environmentId: text("environmentId")
|
environmentId: text("environmentId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
||||||
@@ -270,6 +276,11 @@ export const applicationsRelations = relations(
|
|||||||
relationName: "applicationBuildRegistry",
|
relationName: "applicationBuildRegistry",
|
||||||
}),
|
}),
|
||||||
previewDeployments: many(previewDeployments),
|
previewDeployments: many(previewDeployments),
|
||||||
|
rollbackRegistry: one(registry, {
|
||||||
|
fields: [applications.rollbackRegistryId],
|
||||||
|
references: [registry.registryId],
|
||||||
|
relationName: "applicationRollbackRegistry",
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
import {
|
||||||
|
boolean,
|
||||||
|
integer,
|
||||||
|
jsonb,
|
||||||
|
pgEnum,
|
||||||
|
pgTable,
|
||||||
|
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";
|
||||||
@@ -12,6 +19,7 @@ export const notificationType = pgEnum("notificationType", [
|
|||||||
"email",
|
"email",
|
||||||
"gotify",
|
"gotify",
|
||||||
"ntfy",
|
"ntfy",
|
||||||
|
"custom",
|
||||||
"lark",
|
"lark",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -24,6 +32,7 @@ export const notifications = pgTable("notification", {
|
|||||||
appDeploy: boolean("appDeploy").notNull().default(false),
|
appDeploy: boolean("appDeploy").notNull().default(false),
|
||||||
appBuildError: boolean("appBuildError").notNull().default(false),
|
appBuildError: boolean("appBuildError").notNull().default(false),
|
||||||
databaseBackup: boolean("databaseBackup").notNull().default(false),
|
databaseBackup: boolean("databaseBackup").notNull().default(false),
|
||||||
|
volumeBackup: boolean("volumeBackup").notNull().default(false),
|
||||||
dokployRestart: boolean("dokployRestart").notNull().default(false),
|
dokployRestart: boolean("dokployRestart").notNull().default(false),
|
||||||
dockerCleanup: boolean("dockerCleanup").notNull().default(false),
|
dockerCleanup: boolean("dockerCleanup").notNull().default(false),
|
||||||
serverThreshold: boolean("serverThreshold").notNull().default(false),
|
serverThreshold: boolean("serverThreshold").notNull().default(false),
|
||||||
@@ -49,6 +58,9 @@ export const notifications = pgTable("notification", {
|
|||||||
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
|
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
|
customId: text("customId").references(() => custom.customId, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
larkId: text("larkId").references(() => lark.larkId, {
|
larkId: text("larkId").references(() => lark.larkId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
@@ -120,6 +132,15 @@ export const ntfy = pgTable("ntfy", {
|
|||||||
priority: integer("priority").notNull().default(3),
|
priority: integer("priority").notNull().default(3),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const custom = pgTable("custom", {
|
||||||
|
customId: text("customId")
|
||||||
|
.notNull()
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => nanoid()),
|
||||||
|
endpoint: text("endpoint").notNull(),
|
||||||
|
headers: jsonb("headers").$type<Record<string, string>>(),
|
||||||
|
});
|
||||||
|
|
||||||
export const lark = pgTable("lark", {
|
export const lark = pgTable("lark", {
|
||||||
larkId: text("larkId")
|
larkId: text("larkId")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -153,6 +174,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
|
|||||||
fields: [notifications.ntfyId],
|
fields: [notifications.ntfyId],
|
||||||
references: [ntfy.ntfyId],
|
references: [ntfy.ntfyId],
|
||||||
}),
|
}),
|
||||||
|
custom: one(custom, {
|
||||||
|
fields: [notifications.customId],
|
||||||
|
references: [custom.customId],
|
||||||
|
}),
|
||||||
lark: one(lark, {
|
lark: one(lark, {
|
||||||
fields: [notifications.larkId],
|
fields: [notifications.larkId],
|
||||||
references: [lark.larkId],
|
references: [lark.larkId],
|
||||||
@@ -169,6 +194,7 @@ export const apiCreateSlack = notificationsSchema
|
|||||||
.pick({
|
.pick({
|
||||||
appBuildError: true,
|
appBuildError: true,
|
||||||
databaseBackup: true,
|
databaseBackup: true,
|
||||||
|
volumeBackup: true,
|
||||||
dokployRestart: true,
|
dokployRestart: true,
|
||||||
name: true,
|
name: true,
|
||||||
appDeploy: true,
|
appDeploy: true,
|
||||||
@@ -196,6 +222,7 @@ export const apiCreateTelegram = notificationsSchema
|
|||||||
.pick({
|
.pick({
|
||||||
appBuildError: true,
|
appBuildError: true,
|
||||||
databaseBackup: true,
|
databaseBackup: true,
|
||||||
|
volumeBackup: true,
|
||||||
dokployRestart: true,
|
dokployRestart: true,
|
||||||
name: true,
|
name: true,
|
||||||
appDeploy: true,
|
appDeploy: true,
|
||||||
@@ -225,6 +252,7 @@ export const apiCreateDiscord = notificationsSchema
|
|||||||
.pick({
|
.pick({
|
||||||
appBuildError: true,
|
appBuildError: true,
|
||||||
databaseBackup: true,
|
databaseBackup: true,
|
||||||
|
volumeBackup: true,
|
||||||
dokployRestart: true,
|
dokployRestart: true,
|
||||||
name: true,
|
name: true,
|
||||||
appDeploy: true,
|
appDeploy: true,
|
||||||
@@ -255,6 +283,7 @@ export const apiCreateEmail = notificationsSchema
|
|||||||
.pick({
|
.pick({
|
||||||
appBuildError: true,
|
appBuildError: true,
|
||||||
databaseBackup: true,
|
databaseBackup: true,
|
||||||
|
volumeBackup: true,
|
||||||
dokployRestart: true,
|
dokployRestart: true,
|
||||||
name: true,
|
name: true,
|
||||||
appDeploy: true,
|
appDeploy: true,
|
||||||
@@ -290,6 +319,7 @@ export const apiCreateGotify = notificationsSchema
|
|||||||
.pick({
|
.pick({
|
||||||
appBuildError: true,
|
appBuildError: true,
|
||||||
databaseBackup: true,
|
databaseBackup: true,
|
||||||
|
volumeBackup: true,
|
||||||
dokployRestart: true,
|
dokployRestart: true,
|
||||||
name: true,
|
name: true,
|
||||||
appDeploy: true,
|
appDeploy: true,
|
||||||
@@ -323,6 +353,7 @@ export const apiCreateNtfy = notificationsSchema
|
|||||||
.pick({
|
.pick({
|
||||||
appBuildError: true,
|
appBuildError: true,
|
||||||
databaseBackup: true,
|
databaseBackup: true,
|
||||||
|
volumeBackup: true,
|
||||||
dokployRestart: true,
|
dokployRestart: true,
|
||||||
name: true,
|
name: true,
|
||||||
appDeploy: true,
|
appDeploy: true,
|
||||||
@@ -355,6 +386,32 @@ export const apiFindOneNotification = notificationsSchema
|
|||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
|
export const apiCreateCustom = notificationsSchema
|
||||||
|
.pick({
|
||||||
|
appBuildError: true,
|
||||||
|
databaseBackup: true,
|
||||||
|
dokployRestart: true,
|
||||||
|
name: true,
|
||||||
|
appDeploy: true,
|
||||||
|
dockerCleanup: true,
|
||||||
|
serverThreshold: true,
|
||||||
|
})
|
||||||
|
.extend({
|
||||||
|
endpoint: z.string().min(1),
|
||||||
|
headers: z.record(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiUpdateCustom = apiCreateCustom.partial().extend({
|
||||||
|
notificationId: z.string().min(1),
|
||||||
|
customId: z.string().min(1),
|
||||||
|
organizationId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiTestCustomConnection = z.object({
|
||||||
|
endpoint: z.string().min(1),
|
||||||
|
headers: z.record(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const apiCreateLark = notificationsSchema
|
export const apiCreateLark = notificationsSchema
|
||||||
.pick({
|
.pick({
|
||||||
appBuildError: true,
|
appBuildError: true,
|
||||||
@@ -397,5 +454,7 @@ export const apiSendTest = notificationsSchema
|
|||||||
appToken: z.string(),
|
appToken: z.string(),
|
||||||
accessToken: z.string().optional(),
|
accessToken: z.string().optional(),
|
||||||
priority: z.number(),
|
priority: z.number(),
|
||||||
|
endpoint: z.string(),
|
||||||
|
headers: z.string(),
|
||||||
})
|
})
|
||||||
.partial();
|
.partial();
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export const registryRelations = relations(registry, ({ many }) => ({
|
|||||||
buildApplications: many(applications, {
|
buildApplications: many(applications, {
|
||||||
relationName: "applicationBuildRegistry",
|
relationName: "applicationBuildRegistry",
|
||||||
}),
|
}),
|
||||||
|
rollbackApplications: many(applications, {
|
||||||
|
relationName: "applicationRollbackRegistry",
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const createSchema = createInsertSchema(registry, {
|
const createSchema = createInsertSchema(registry, {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export const schedules = pgTable("schedule", {
|
|||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
timezone: text("timezone"),
|
||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
.$defaultFn(() => new Date().toISOString()),
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
|
|||||||
@@ -5,17 +5,24 @@ enum applicationStatus {
|
|||||||
error
|
error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum backupType {
|
||||||
|
database
|
||||||
|
compose
|
||||||
|
}
|
||||||
|
|
||||||
enum buildType {
|
enum buildType {
|
||||||
dockerfile
|
dockerfile
|
||||||
heroku_buildpacks
|
heroku_buildpacks
|
||||||
paketo_buildpacks
|
paketo_buildpacks
|
||||||
nixpacks
|
nixpacks
|
||||||
static
|
static
|
||||||
|
railpack
|
||||||
}
|
}
|
||||||
|
|
||||||
enum certificateType {
|
enum certificateType {
|
||||||
letsencrypt
|
letsencrypt
|
||||||
none
|
none
|
||||||
|
custom
|
||||||
}
|
}
|
||||||
|
|
||||||
enum composeType {
|
enum composeType {
|
||||||
@@ -28,6 +35,7 @@ enum databaseType {
|
|||||||
mariadb
|
mariadb
|
||||||
mysql
|
mysql
|
||||||
mongo
|
mongo
|
||||||
|
"web-server"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum deploymentStatus {
|
enum deploymentStatus {
|
||||||
@@ -61,6 +69,8 @@ enum notificationType {
|
|||||||
discord
|
discord
|
||||||
email
|
email
|
||||||
gotify
|
gotify
|
||||||
|
ntfy
|
||||||
|
custom
|
||||||
}
|
}
|
||||||
|
|
||||||
enum protocolType {
|
enum protocolType {
|
||||||
@@ -68,14 +78,21 @@ enum protocolType {
|
|||||||
udp
|
udp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum publishModeType {
|
||||||
|
ingress
|
||||||
|
host
|
||||||
|
}
|
||||||
|
|
||||||
enum RegistryType {
|
enum RegistryType {
|
||||||
selfHosted
|
selfHosted
|
||||||
cloud
|
cloud
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Roles {
|
enum scheduleType {
|
||||||
admin
|
application
|
||||||
user
|
compose
|
||||||
|
server
|
||||||
|
"dokploy-server"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum serverStatus {
|
enum serverStatus {
|
||||||
@@ -93,6 +110,11 @@ enum serviceType {
|
|||||||
compose
|
compose
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum shellType {
|
||||||
|
bash
|
||||||
|
sh
|
||||||
|
}
|
||||||
|
|
||||||
enum sourceType {
|
enum sourceType {
|
||||||
docker
|
docker
|
||||||
git
|
git
|
||||||
@@ -112,6 +134,11 @@ enum sourceTypeCompose {
|
|||||||
raw
|
raw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum triggerType {
|
||||||
|
push
|
||||||
|
tag
|
||||||
|
}
|
||||||
|
|
||||||
table account {
|
table account {
|
||||||
id text [pk, not null]
|
id text [pk, not null]
|
||||||
account_id text [not null]
|
account_id text [not null]
|
||||||
@@ -133,7 +160,39 @@ table account {
|
|||||||
confirmationExpiresAt text
|
confirmationExpiresAt text
|
||||||
}
|
}
|
||||||
|
|
||||||
table admin {
|
table ai {
|
||||||
|
aiId text [pk, not null]
|
||||||
|
name text [not null]
|
||||||
|
apiUrl text [not null]
|
||||||
|
apiKey text [not null]
|
||||||
|
model text [not null]
|
||||||
|
isEnabled boolean [not null, default: true]
|
||||||
|
organizationId text [not null]
|
||||||
|
createdAt text [not null]
|
||||||
|
}
|
||||||
|
|
||||||
|
table apikey {
|
||||||
|
id text [pk, not null]
|
||||||
|
name text
|
||||||
|
start text
|
||||||
|
prefix text
|
||||||
|
key text [not null]
|
||||||
|
user_id text [not null]
|
||||||
|
refill_interval integer
|
||||||
|
refill_amount integer
|
||||||
|
last_refill_at timestamp
|
||||||
|
enabled boolean
|
||||||
|
rate_limit_enabled boolean
|
||||||
|
rate_limit_time_window integer
|
||||||
|
rate_limit_max integer
|
||||||
|
request_count integer
|
||||||
|
remaining integer
|
||||||
|
last_request timestamp
|
||||||
|
expires_at timestamp
|
||||||
|
created_at timestamp [not null]
|
||||||
|
updated_at timestamp [not null]
|
||||||
|
permissions text
|
||||||
|
metadata text
|
||||||
}
|
}
|
||||||
|
|
||||||
table application {
|
table application {
|
||||||
@@ -143,14 +202,19 @@ table application {
|
|||||||
description text
|
description text
|
||||||
env text
|
env text
|
||||||
previewEnv text
|
previewEnv text
|
||||||
|
watchPaths text[]
|
||||||
previewBuildArgs text
|
previewBuildArgs text
|
||||||
|
previewLabels text[]
|
||||||
previewWildcard text
|
previewWildcard text
|
||||||
previewPort integer [default: 3000]
|
previewPort integer [default: 3000]
|
||||||
previewHttps boolean [not null, default: false]
|
previewHttps boolean [not null, default: false]
|
||||||
previewPath text [default: '/']
|
previewPath text [default: '/']
|
||||||
certificateType certificateType [not null, default: 'none']
|
certificateType certificateType [not null, default: 'none']
|
||||||
|
previewCustomCertResolver text
|
||||||
previewLimit integer [default: 3]
|
previewLimit integer [default: 3]
|
||||||
isPreviewDeploymentsActive boolean [default: false]
|
isPreviewDeploymentsActive boolean [default: false]
|
||||||
|
previewRequireCollaboratorPermissions boolean [default: true]
|
||||||
|
rollbackActive boolean [default: false]
|
||||||
buildArgs text
|
buildArgs text
|
||||||
memoryReservation text
|
memoryReservation text
|
||||||
memoryLimit text
|
memoryLimit text
|
||||||
@@ -167,6 +231,7 @@ table application {
|
|||||||
owner text
|
owner text
|
||||||
branch text
|
branch text
|
||||||
buildPath text [default: '/']
|
buildPath text [default: '/']
|
||||||
|
triggerType triggerType [default: 'push']
|
||||||
autoDeploy boolean
|
autoDeploy boolean
|
||||||
gitlabProjectId integer
|
gitlabProjectId integer
|
||||||
gitlabRepository text
|
gitlabRepository text
|
||||||
@@ -174,6 +239,10 @@ table application {
|
|||||||
gitlabBranch text
|
gitlabBranch text
|
||||||
gitlabBuildPath text [default: '/']
|
gitlabBuildPath text [default: '/']
|
||||||
gitlabPathNamespace text
|
gitlabPathNamespace text
|
||||||
|
giteaRepository text
|
||||||
|
giteaOwner text
|
||||||
|
giteaBranch text
|
||||||
|
giteaBuildPath text [default: '/']
|
||||||
bitbucketRepository text
|
bitbucketRepository text
|
||||||
bitbucketOwner text
|
bitbucketOwner text
|
||||||
bitbucketBranch text
|
bitbucketBranch text
|
||||||
@@ -186,6 +255,7 @@ table application {
|
|||||||
customGitBranch text
|
customGitBranch text
|
||||||
customGitBuildPath text
|
customGitBuildPath text
|
||||||
customGitSSHKeyId text
|
customGitSSHKeyId text
|
||||||
|
enableSubmodules boolean [not null, default: false]
|
||||||
dockerfile text
|
dockerfile text
|
||||||
dockerContextPath text
|
dockerContextPath text
|
||||||
dockerBuildStage text
|
dockerBuildStage text
|
||||||
@@ -201,52 +271,47 @@ table application {
|
|||||||
replicas integer [not null, default: 1]
|
replicas integer [not null, default: 1]
|
||||||
applicationStatus applicationStatus [not null, default: 'idle']
|
applicationStatus applicationStatus [not null, default: 'idle']
|
||||||
buildType buildType [not null, default: 'nixpacks']
|
buildType buildType [not null, default: 'nixpacks']
|
||||||
|
railpackVersion text [default: '0.2.2']
|
||||||
herokuVersion text [default: '24']
|
herokuVersion text [default: '24']
|
||||||
publishDirectory text
|
publishDirectory text
|
||||||
|
isStaticSpa boolean
|
||||||
createdAt text [not null]
|
createdAt text [not null]
|
||||||
registryId text
|
registryId text
|
||||||
projectId text [not null]
|
environmentId text [not null]
|
||||||
githubId text
|
githubId text
|
||||||
gitlabId text
|
gitlabId text
|
||||||
bitbucketId text
|
|
||||||
giteaId text
|
giteaId text
|
||||||
|
bitbucketId text
|
||||||
serverId text
|
serverId text
|
||||||
}
|
}
|
||||||
|
|
||||||
table auth {
|
|
||||||
id text [pk, not null]
|
|
||||||
email text [not null, unique]
|
|
||||||
password text [not null]
|
|
||||||
rol Roles [not null]
|
|
||||||
image text
|
|
||||||
secret text
|
|
||||||
token text
|
|
||||||
is2FAEnabled boolean [not null, default: false]
|
|
||||||
createdAt text [not null]
|
|
||||||
resetPasswordToken text
|
|
||||||
resetPasswordExpiresAt text
|
|
||||||
confirmationToken text
|
|
||||||
confirmationExpiresAt text
|
|
||||||
}
|
|
||||||
|
|
||||||
table backup {
|
table backup {
|
||||||
backupId text [pk, not null]
|
backupId text [pk, not null]
|
||||||
|
appName text [not null, unique]
|
||||||
schedule text [not null]
|
schedule text [not null]
|
||||||
enabled boolean
|
enabled boolean
|
||||||
database text [not null]
|
database text [not null]
|
||||||
prefix text [not null]
|
prefix text [not null]
|
||||||
|
serviceName text
|
||||||
destinationId text [not null]
|
destinationId text [not null]
|
||||||
|
keepLatestCount integer
|
||||||
|
backupType backupType [not null, default: 'database']
|
||||||
databaseType databaseType [not null]
|
databaseType databaseType [not null]
|
||||||
|
composeId text
|
||||||
postgresId text
|
postgresId text
|
||||||
mariadbId text
|
mariadbId text
|
||||||
mysqlId text
|
mysqlId text
|
||||||
mongoId text
|
mongoId text
|
||||||
|
userId text
|
||||||
|
metadata jsonb
|
||||||
}
|
}
|
||||||
|
|
||||||
table bitbucket {
|
table bitbucket {
|
||||||
bitbucketId text [pk, not null]
|
bitbucketId text [pk, not null]
|
||||||
bitbucketUsername text
|
bitbucketUsername text
|
||||||
|
bitbucketEmail text
|
||||||
appPassword text
|
appPassword text
|
||||||
|
apiToken text
|
||||||
bitbucketWorkspaceName text
|
bitbucketWorkspaceName text
|
||||||
gitProviderId text [not null]
|
gitProviderId text [not null]
|
||||||
}
|
}
|
||||||
@@ -258,7 +323,7 @@ table certificate {
|
|||||||
privateKey text [not null]
|
privateKey text [not null]
|
||||||
certificatePath text [not null, unique]
|
certificatePath text [not null, unique]
|
||||||
autoRenew boolean
|
autoRenew boolean
|
||||||
userId text
|
organizationId text [not null]
|
||||||
serverId text
|
serverId text
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,13 +356,17 @@ table compose {
|
|||||||
customGitBranch text
|
customGitBranch text
|
||||||
customGitSSHKeyId text
|
customGitSSHKeyId text
|
||||||
command text [not null, default: '']
|
command text [not null, default: '']
|
||||||
|
enableSubmodules boolean [not null, default: false]
|
||||||
composePath text [not null, default: './docker-compose.yml']
|
composePath text [not null, default: './docker-compose.yml']
|
||||||
suffix text [not null, default: '']
|
suffix text [not null, default: '']
|
||||||
randomize boolean [not null, default: false]
|
randomize boolean [not null, default: false]
|
||||||
isolatedDeployment boolean [not null, default: false]
|
isolatedDeployment boolean [not null, default: false]
|
||||||
|
isolatedDeploymentsVolume boolean [not null, default: false]
|
||||||
|
triggerType triggerType [default: 'push']
|
||||||
composeStatus applicationStatus [not null, default: 'idle']
|
composeStatus applicationStatus [not null, default: 'idle']
|
||||||
projectId text [not null]
|
environmentId text [not null]
|
||||||
createdAt text [not null]
|
createdAt text [not null]
|
||||||
|
watchPaths text[]
|
||||||
githubId text
|
githubId text
|
||||||
gitlabId text
|
gitlabId text
|
||||||
bitbucketId text
|
bitbucketId text
|
||||||
@@ -305,19 +374,32 @@ table compose {
|
|||||||
serverId text
|
serverId text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table custom {
|
||||||
|
customId text [pk, not null]
|
||||||
|
endpoint text [not null]
|
||||||
|
headers text
|
||||||
|
}
|
||||||
|
|
||||||
table deployment {
|
table deployment {
|
||||||
deploymentId text [pk, not null]
|
deploymentId text [pk, not null]
|
||||||
title text [not null]
|
title text [not null]
|
||||||
description text
|
description text
|
||||||
status deploymentStatus [default: 'running']
|
status deploymentStatus [default: 'running']
|
||||||
logPath text [not null]
|
logPath text [not null]
|
||||||
|
pid text
|
||||||
applicationId text
|
applicationId text
|
||||||
composeId text
|
composeId text
|
||||||
serverId text
|
serverId text
|
||||||
isPreviewDeployment boolean [default: false]
|
isPreviewDeployment boolean [default: false]
|
||||||
previewDeploymentId text
|
previewDeploymentId text
|
||||||
createdAt text [not null]
|
createdAt text [not null]
|
||||||
|
startedAt text
|
||||||
|
finishedAt text
|
||||||
errorMessage text
|
errorMessage text
|
||||||
|
scheduleId text
|
||||||
|
backupId text
|
||||||
|
rollbackId text
|
||||||
|
volumeBackupId text
|
||||||
}
|
}
|
||||||
|
|
||||||
table destination {
|
table destination {
|
||||||
@@ -329,7 +411,8 @@ table destination {
|
|||||||
bucket text [not null]
|
bucket text [not null]
|
||||||
region text [not null]
|
region text [not null]
|
||||||
endpoint text [not null]
|
endpoint text [not null]
|
||||||
userId text [not null]
|
organizationId text [not null]
|
||||||
|
createdAt timestamp [not null, default: `now()`]
|
||||||
}
|
}
|
||||||
|
|
||||||
table discord {
|
table discord {
|
||||||
@@ -349,9 +432,12 @@ table domain {
|
|||||||
uniqueConfigKey serial [not null, increment]
|
uniqueConfigKey serial [not null, increment]
|
||||||
createdAt text [not null]
|
createdAt text [not null]
|
||||||
composeId text
|
composeId text
|
||||||
|
customCertResolver text
|
||||||
applicationId text
|
applicationId text
|
||||||
previewDeploymentId text
|
previewDeploymentId text
|
||||||
certificateType certificateType [not null, default: 'none']
|
certificateType certificateType [not null, default: 'none']
|
||||||
|
internalPath text [default: '/']
|
||||||
|
stripPath boolean [not null, default: false]
|
||||||
}
|
}
|
||||||
|
|
||||||
table email {
|
table email {
|
||||||
@@ -364,12 +450,36 @@ table email {
|
|||||||
toAddress text[] [not null]
|
toAddress text[] [not null]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table environment {
|
||||||
|
environmentId text [pk, not null]
|
||||||
|
name text [not null]
|
||||||
|
description text
|
||||||
|
createdAt text [not null]
|
||||||
|
env text [not null, default: '']
|
||||||
|
projectId text [not null]
|
||||||
|
}
|
||||||
|
|
||||||
table git_provider {
|
table git_provider {
|
||||||
gitProviderId text [pk, not null]
|
gitProviderId text [pk, not null]
|
||||||
name text [not null]
|
name text [not null]
|
||||||
providerType gitProviderType [not null, default: 'github']
|
providerType gitProviderType [not null, default: 'github']
|
||||||
createdAt text [not null]
|
createdAt text [not null]
|
||||||
userId text
|
organizationId text [not null]
|
||||||
|
userId text [not null]
|
||||||
|
}
|
||||||
|
|
||||||
|
table gitea {
|
||||||
|
giteaId text [pk, not null]
|
||||||
|
giteaUrl text [not null, default: 'https://gitea.com']
|
||||||
|
redirect_uri text
|
||||||
|
client_id text
|
||||||
|
client_secret text
|
||||||
|
gitProviderId text [not null]
|
||||||
|
access_token text
|
||||||
|
refresh_token text
|
||||||
|
expires_at integer
|
||||||
|
scopes text [default: 'repo,repo:status,read:user,read:org']
|
||||||
|
last_authenticated_at integer
|
||||||
}
|
}
|
||||||
|
|
||||||
table github {
|
table github {
|
||||||
@@ -397,20 +507,6 @@ table gitlab {
|
|||||||
gitProviderId text [not null]
|
gitProviderId text [not null]
|
||||||
}
|
}
|
||||||
|
|
||||||
table gitea {
|
|
||||||
giteaId text [pk, not null]
|
|
||||||
giteaUrl text [not null, default: 'https://gitea.com']
|
|
||||||
redirect_uri text
|
|
||||||
client_id text [not null]
|
|
||||||
client_secret text [not null]
|
|
||||||
access_token text
|
|
||||||
refresh_token text
|
|
||||||
expires_at integer
|
|
||||||
gitProviderId text [not null]
|
|
||||||
scopes text [default: 'repo,repo:status,read:user,read:org']
|
|
||||||
last_authenticated_at integer
|
|
||||||
}
|
|
||||||
|
|
||||||
table gotify {
|
table gotify {
|
||||||
gotifyId text [pk, not null]
|
gotifyId text [pk, not null]
|
||||||
serverUrl text [not null]
|
serverUrl text [not null]
|
||||||
@@ -427,6 +523,7 @@ table invitation {
|
|||||||
status text [not null]
|
status text [not null]
|
||||||
expires_at timestamp [not null]
|
expires_at timestamp [not null]
|
||||||
inviter_id text [not null]
|
inviter_id text [not null]
|
||||||
|
team_id text
|
||||||
}
|
}
|
||||||
|
|
||||||
table mariadb {
|
table mariadb {
|
||||||
@@ -447,8 +544,17 @@ table mariadb {
|
|||||||
cpuLimit text
|
cpuLimit text
|
||||||
externalPort integer
|
externalPort integer
|
||||||
applicationStatus applicationStatus [not null, default: 'idle']
|
applicationStatus applicationStatus [not null, default: 'idle']
|
||||||
|
healthCheckSwarm json
|
||||||
|
restartPolicySwarm json
|
||||||
|
placementSwarm json
|
||||||
|
updateConfigSwarm json
|
||||||
|
rollbackConfigSwarm json
|
||||||
|
modeSwarm json
|
||||||
|
labelsSwarm json
|
||||||
|
networkSwarm json
|
||||||
|
replicas integer [not null, default: 1]
|
||||||
createdAt text [not null]
|
createdAt text [not null]
|
||||||
projectId text [not null]
|
environmentId text [not null]
|
||||||
serverId text
|
serverId text
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,6 +564,19 @@ table member {
|
|||||||
user_id text [not null]
|
user_id text [not null]
|
||||||
role text [not null]
|
role text [not null]
|
||||||
created_at timestamp [not null]
|
created_at timestamp [not null]
|
||||||
|
team_id text
|
||||||
|
canCreateProjects boolean [not null, default: false]
|
||||||
|
canAccessToSSHKeys boolean [not null, default: false]
|
||||||
|
canCreateServices boolean [not null, default: false]
|
||||||
|
canDeleteProjects boolean [not null, default: false]
|
||||||
|
canDeleteServices boolean [not null, default: false]
|
||||||
|
canAccessToDocker boolean [not null, default: false]
|
||||||
|
canAccessToAPI boolean [not null, default: false]
|
||||||
|
canAccessToGitProviders boolean [not null, default: false]
|
||||||
|
canAccessToTraefikFiles boolean [not null, default: false]
|
||||||
|
accesedProjects text[] [not null, default: `ARRAY[]::text[]`]
|
||||||
|
accessedEnvironments text[] [not null, default: `ARRAY[]::text[]`]
|
||||||
|
accesedServices text[] [not null, default: `ARRAY[]::text[]`]
|
||||||
}
|
}
|
||||||
|
|
||||||
table mongo {
|
table mongo {
|
||||||
@@ -476,8 +595,17 @@ table mongo {
|
|||||||
cpuLimit text
|
cpuLimit text
|
||||||
externalPort integer
|
externalPort integer
|
||||||
applicationStatus applicationStatus [not null, default: 'idle']
|
applicationStatus applicationStatus [not null, default: 'idle']
|
||||||
|
healthCheckSwarm json
|
||||||
|
restartPolicySwarm json
|
||||||
|
placementSwarm json
|
||||||
|
updateConfigSwarm json
|
||||||
|
rollbackConfigSwarm json
|
||||||
|
modeSwarm json
|
||||||
|
labelsSwarm json
|
||||||
|
networkSwarm json
|
||||||
|
replicas integer [not null, default: 1]
|
||||||
createdAt text [not null]
|
createdAt text [not null]
|
||||||
projectId text [not null]
|
environmentId text [not null]
|
||||||
serverId text
|
serverId text
|
||||||
replicaSets boolean [default: false]
|
replicaSets boolean [default: false]
|
||||||
}
|
}
|
||||||
@@ -518,8 +646,17 @@ table mysql {
|
|||||||
cpuLimit text
|
cpuLimit text
|
||||||
externalPort integer
|
externalPort integer
|
||||||
applicationStatus applicationStatus [not null, default: 'idle']
|
applicationStatus applicationStatus [not null, default: 'idle']
|
||||||
|
healthCheckSwarm json
|
||||||
|
restartPolicySwarm json
|
||||||
|
placementSwarm json
|
||||||
|
updateConfigSwarm json
|
||||||
|
rollbackConfigSwarm json
|
||||||
|
modeSwarm json
|
||||||
|
labelsSwarm json
|
||||||
|
networkSwarm json
|
||||||
|
replicas integer [not null, default: 1]
|
||||||
createdAt text [not null]
|
createdAt text [not null]
|
||||||
projectId text [not null]
|
environmentId text [not null]
|
||||||
serverId text
|
serverId text
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,7 +676,17 @@ table notification {
|
|||||||
discordId text
|
discordId text
|
||||||
emailId text
|
emailId text
|
||||||
gotifyId text
|
gotifyId text
|
||||||
userId text
|
ntfyId text
|
||||||
|
customId text
|
||||||
|
organizationId text [not null]
|
||||||
|
}
|
||||||
|
|
||||||
|
table ntfy {
|
||||||
|
ntfyId text [pk, not null]
|
||||||
|
serverUrl text [not null]
|
||||||
|
topic text [not null]
|
||||||
|
accessToken text [not null]
|
||||||
|
priority integer [not null, default: 3]
|
||||||
}
|
}
|
||||||
|
|
||||||
table organization {
|
table organization {
|
||||||
@@ -555,6 +702,7 @@ table organization {
|
|||||||
table port {
|
table port {
|
||||||
portId text [pk, not null]
|
portId text [pk, not null]
|
||||||
publishedPort integer [not null]
|
publishedPort integer [not null]
|
||||||
|
publishMode publishModeType [not null, default: 'host']
|
||||||
targetPort integer [not null]
|
targetPort integer [not null]
|
||||||
protocol protocolType [not null]
|
protocol protocolType [not null]
|
||||||
applicationId text [not null]
|
applicationId text [not null]
|
||||||
@@ -577,8 +725,17 @@ table postgres {
|
|||||||
cpuReservation text
|
cpuReservation text
|
||||||
cpuLimit text
|
cpuLimit text
|
||||||
applicationStatus applicationStatus [not null, default: 'idle']
|
applicationStatus applicationStatus [not null, default: 'idle']
|
||||||
|
healthCheckSwarm json
|
||||||
|
restartPolicySwarm json
|
||||||
|
placementSwarm json
|
||||||
|
updateConfigSwarm json
|
||||||
|
rollbackConfigSwarm json
|
||||||
|
modeSwarm json
|
||||||
|
labelsSwarm json
|
||||||
|
networkSwarm json
|
||||||
|
replicas integer [not null, default: 1]
|
||||||
createdAt text [not null]
|
createdAt text [not null]
|
||||||
projectId text [not null]
|
environmentId text [not null]
|
||||||
serverId text
|
serverId text
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,7 +760,7 @@ table project {
|
|||||||
name text [not null]
|
name text [not null]
|
||||||
description text
|
description text
|
||||||
createdAt text [not null]
|
createdAt text [not null]
|
||||||
userId text [not null]
|
organizationId text [not null]
|
||||||
env text [not null, default: '']
|
env text [not null, default: '']
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -633,7 +790,16 @@ table redis {
|
|||||||
externalPort integer
|
externalPort integer
|
||||||
createdAt text [not null]
|
createdAt text [not null]
|
||||||
applicationStatus applicationStatus [not null, default: 'idle']
|
applicationStatus applicationStatus [not null, default: 'idle']
|
||||||
projectId text [not null]
|
healthCheckSwarm json
|
||||||
|
restartPolicySwarm json
|
||||||
|
placementSwarm json
|
||||||
|
updateConfigSwarm json
|
||||||
|
rollbackConfigSwarm json
|
||||||
|
modeSwarm json
|
||||||
|
labelsSwarm json
|
||||||
|
networkSwarm json
|
||||||
|
replicas integer [not null, default: 1]
|
||||||
|
environmentId text [not null]
|
||||||
serverId text
|
serverId text
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,7 +812,34 @@ table registry {
|
|||||||
registryUrl text [not null, default: '']
|
registryUrl text [not null, default: '']
|
||||||
createdAt text [not null]
|
createdAt text [not null]
|
||||||
selfHosted RegistryType [not null, default: 'cloud']
|
selfHosted RegistryType [not null, default: 'cloud']
|
||||||
userId text [not null]
|
organizationId text [not null]
|
||||||
|
}
|
||||||
|
|
||||||
|
table rollback {
|
||||||
|
rollbackId text [pk, not null]
|
||||||
|
deploymentId text [not null]
|
||||||
|
version serial [not null, increment]
|
||||||
|
image text
|
||||||
|
createdAt text [not null]
|
||||||
|
fullContext jsonb
|
||||||
|
}
|
||||||
|
|
||||||
|
table schedule {
|
||||||
|
scheduleId text [pk, not null]
|
||||||
|
name text [not null]
|
||||||
|
cronExpression text [not null]
|
||||||
|
appName text [not null]
|
||||||
|
serviceName text
|
||||||
|
shellType shellType [not null, default: 'bash']
|
||||||
|
scheduleType scheduleType [not null, default: 'application']
|
||||||
|
command text [not null]
|
||||||
|
script text
|
||||||
|
applicationId text
|
||||||
|
composeId text
|
||||||
|
serverId text
|
||||||
|
userId text
|
||||||
|
enabled boolean [not null, default: true]
|
||||||
|
createdAt text [not null]
|
||||||
}
|
}
|
||||||
|
|
||||||
table security {
|
table security {
|
||||||
@@ -671,14 +864,14 @@ table server {
|
|||||||
appName text [not null]
|
appName text [not null]
|
||||||
enableDockerCleanup boolean [not null, default: false]
|
enableDockerCleanup boolean [not null, default: false]
|
||||||
createdAt text [not null]
|
createdAt text [not null]
|
||||||
userId text [not null]
|
organizationId text [not null]
|
||||||
serverStatus serverStatus [not null, default: 'active']
|
serverStatus serverStatus [not null, default: 'active']
|
||||||
command text [not null, default: '']
|
command text [not null, default: '']
|
||||||
sshKeyId text
|
sshKeyId text
|
||||||
metricsConfig jsonb [not null, default: `{"server":{"type":"Remote","refreshRate":60,"port":4500,"token":"","urlCallback":"","cronJob":"","retentionDays":2,"thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}`]
|
metricsConfig jsonb [not null, default: `{"server":{"type":"Remote","refreshRate":60,"port":4500,"token":"","urlCallback":"","cronJob":"","retentionDays":2,"thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}`]
|
||||||
}
|
}
|
||||||
|
|
||||||
table session {
|
table session_temp {
|
||||||
id text [pk, not null]
|
id text [pk, not null]
|
||||||
expires_at timestamp [not null]
|
expires_at timestamp [not null]
|
||||||
token text [not null, unique]
|
token text [not null, unique]
|
||||||
@@ -705,49 +898,49 @@ table "ssh-key" {
|
|||||||
description text
|
description text
|
||||||
createdAt text [not null]
|
createdAt text [not null]
|
||||||
lastUsedAt text
|
lastUsedAt text
|
||||||
userId text
|
organizationId text [not null]
|
||||||
}
|
}
|
||||||
|
|
||||||
table telegram {
|
table telegram {
|
||||||
telegramId text [pk, not null]
|
telegramId text [pk, not null]
|
||||||
botToken text [not null]
|
botToken text [not null]
|
||||||
chatId text [not null]
|
chatId text [not null]
|
||||||
|
messageThreadId text
|
||||||
}
|
}
|
||||||
|
|
||||||
table user {
|
table two_factor {
|
||||||
|
id text [pk, not null]
|
||||||
|
secret text [not null]
|
||||||
|
backup_codes text [not null]
|
||||||
|
user_id text [not null]
|
||||||
|
}
|
||||||
|
|
||||||
|
table user_temp {
|
||||||
id text [pk, not null]
|
id text [pk, not null]
|
||||||
name text [not null, default: '']
|
name text [not null, default: '']
|
||||||
token text [not null]
|
|
||||||
isRegistered boolean [not null, default: false]
|
isRegistered boolean [not null, default: false]
|
||||||
expirationDate text [not null]
|
expirationDate text [not null]
|
||||||
createdAt text [not null]
|
createdAt text [not null]
|
||||||
canCreateProjects boolean [not null, default: false]
|
created_at timestamp [default: `now()`]
|
||||||
canAccessToSSHKeys boolean [not null, default: false]
|
two_factor_enabled boolean
|
||||||
canCreateServices boolean [not null, default: false]
|
|
||||||
canDeleteProjects boolean [not null, default: false]
|
|
||||||
canDeleteServices boolean [not null, default: false]
|
|
||||||
canAccessToDocker boolean [not null, default: false]
|
|
||||||
canAccessToAPI boolean [not null, default: false]
|
|
||||||
canAccessToGitProviders boolean [not null, default: false]
|
|
||||||
canAccessToTraefikFiles boolean [not null, default: false]
|
|
||||||
accesedProjects text[] [not null, default: `ARRAY[]::text[]`]
|
|
||||||
accesedServices text[] [not null, default: `ARRAY[]::text[]`]
|
|
||||||
email text [not null, unique]
|
email text [not null, unique]
|
||||||
email_verified boolean [not null]
|
email_verified boolean [not null]
|
||||||
image text
|
image text
|
||||||
role text
|
|
||||||
banned boolean
|
banned boolean
|
||||||
ban_reason text
|
ban_reason text
|
||||||
ban_expires timestamp
|
ban_expires timestamp
|
||||||
updated_at timestamp [not null]
|
updated_at timestamp [not null]
|
||||||
serverIp text
|
serverIp text
|
||||||
certificateType certificateType [not null, default: 'none']
|
certificateType certificateType [not null, default: 'none']
|
||||||
|
https boolean [not null, default: false]
|
||||||
host text
|
host text
|
||||||
letsEncryptEmail text
|
letsEncryptEmail text
|
||||||
sshPrivateKey text
|
sshPrivateKey text
|
||||||
enableDockerCleanup boolean [not null, default: false]
|
enableDockerCleanup boolean [not null, default: false]
|
||||||
enableLogRotation boolean [not null, default: false]
|
logCleanupCron text [default: '0 0 * * *']
|
||||||
|
role text [not null, default: 'user']
|
||||||
enablePaidFeatures boolean [not null, default: false]
|
enablePaidFeatures boolean [not null, default: false]
|
||||||
|
allowImpersonation boolean [not null, default: false]
|
||||||
metricsConfig jsonb [not null, default: `{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}`]
|
metricsConfig jsonb [not null, default: `{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}`]
|
||||||
cleanupCacheApplications boolean [not null, default: false]
|
cleanupCacheApplications boolean [not null, default: false]
|
||||||
cleanupCacheOnPreviews boolean [not null, default: false]
|
cleanupCacheOnPreviews boolean [not null, default: false]
|
||||||
@@ -766,6 +959,29 @@ table verification {
|
|||||||
updated_at timestamp
|
updated_at timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table volume_backup {
|
||||||
|
volumeBackupId text [pk, not null]
|
||||||
|
name text [not null]
|
||||||
|
volumeName text [not null]
|
||||||
|
prefix text [not null]
|
||||||
|
serviceType serviceType [not null, default: 'application']
|
||||||
|
appName text [not null]
|
||||||
|
serviceName text
|
||||||
|
turnOff boolean [not null, default: false]
|
||||||
|
cronExpression text [not null]
|
||||||
|
keepLatestCount integer
|
||||||
|
enabled boolean
|
||||||
|
applicationId text
|
||||||
|
postgresId text
|
||||||
|
mariadbId text
|
||||||
|
mongoId text
|
||||||
|
mysqlId text
|
||||||
|
redisId text
|
||||||
|
composeId text
|
||||||
|
createdAt text [not null]
|
||||||
|
destinationId text [not null]
|
||||||
|
}
|
||||||
|
|
||||||
ref: mount.applicationId > application.applicationId
|
ref: mount.applicationId > application.applicationId
|
||||||
|
|
||||||
ref: mount.postgresId > postgres.postgresId
|
ref: mount.postgresId > postgres.postgresId
|
||||||
@@ -780,7 +996,13 @@ ref: mount.redisId > redis.redisId
|
|||||||
|
|
||||||
ref: mount.composeId > compose.composeId
|
ref: mount.composeId > compose.composeId
|
||||||
|
|
||||||
ref: application.projectId > project.projectId
|
ref: user_temp.id - account.user_id
|
||||||
|
|
||||||
|
ref: ai.organizationId - organization.id
|
||||||
|
|
||||||
|
ref: apikey.user_id > user_temp.id
|
||||||
|
|
||||||
|
ref: application.environmentId > environment.environmentId
|
||||||
|
|
||||||
ref: application.customGitSSHKeyId > "ssh-key".sshKeyId
|
ref: application.customGitSSHKeyId > "ssh-key".sshKeyId
|
||||||
|
|
||||||
@@ -790,6 +1012,8 @@ ref: application.githubId - github.githubId
|
|||||||
|
|
||||||
ref: application.gitlabId - gitlab.gitlabId
|
ref: application.gitlabId - gitlab.gitlabId
|
||||||
|
|
||||||
|
ref: application.giteaId - gitea.giteaId
|
||||||
|
|
||||||
ref: application.bitbucketId - bitbucket.bitbucketId
|
ref: application.bitbucketId - bitbucket.bitbucketId
|
||||||
|
|
||||||
ref: application.serverId > server.serverId
|
ref: application.serverId > server.serverId
|
||||||
@@ -804,13 +1028,17 @@ ref: backup.mysqlId > mysql.mysqlId
|
|||||||
|
|
||||||
ref: backup.mongoId > mongo.mongoId
|
ref: backup.mongoId > mongo.mongoId
|
||||||
|
|
||||||
|
ref: backup.userId > user_temp.id
|
||||||
|
|
||||||
|
ref: backup.composeId > compose.composeId
|
||||||
|
|
||||||
ref: git_provider.gitProviderId - bitbucket.gitProviderId
|
ref: git_provider.gitProviderId - bitbucket.gitProviderId
|
||||||
|
|
||||||
ref: certificate.serverId > server.serverId
|
ref: certificate.serverId > server.serverId
|
||||||
|
|
||||||
ref: certificate.userId - user.id
|
ref: certificate.organizationId - organization.id
|
||||||
|
|
||||||
ref: compose.projectId > project.projectId
|
ref: compose.environmentId > environment.environmentId
|
||||||
|
|
||||||
ref: compose.customGitSSHKeyId > "ssh-key".sshKeyId
|
ref: compose.customGitSSHKeyId > "ssh-key".sshKeyId
|
||||||
|
|
||||||
@@ -820,6 +1048,8 @@ ref: compose.gitlabId - gitlab.gitlabId
|
|||||||
|
|
||||||
ref: compose.bitbucketId - bitbucket.bitbucketId
|
ref: compose.bitbucketId - bitbucket.bitbucketId
|
||||||
|
|
||||||
|
ref: compose.giteaId - gitea.giteaId
|
||||||
|
|
||||||
ref: compose.serverId > server.serverId
|
ref: compose.serverId > server.serverId
|
||||||
|
|
||||||
ref: deployment.applicationId > application.applicationId
|
ref: deployment.applicationId > application.applicationId
|
||||||
@@ -830,7 +1060,15 @@ ref: deployment.serverId > server.serverId
|
|||||||
|
|
||||||
ref: deployment.previewDeploymentId > preview_deployments.previewDeploymentId
|
ref: deployment.previewDeploymentId > preview_deployments.previewDeploymentId
|
||||||
|
|
||||||
ref: destination.userId - user.id
|
ref: deployment.scheduleId > schedule.scheduleId
|
||||||
|
|
||||||
|
ref: deployment.backupId > backup.backupId
|
||||||
|
|
||||||
|
ref: rollback.deploymentId - deployment.deploymentId
|
||||||
|
|
||||||
|
ref: deployment.volumeBackupId > volume_backup.volumeBackupId
|
||||||
|
|
||||||
|
ref: destination.organizationId - organization.id
|
||||||
|
|
||||||
ref: domain.applicationId > application.applicationId
|
ref: domain.applicationId > application.applicationId
|
||||||
|
|
||||||
@@ -838,23 +1076,33 @@ ref: domain.composeId > compose.composeId
|
|||||||
|
|
||||||
ref: preview_deployments.domainId - domain.domainId
|
ref: preview_deployments.domainId - domain.domainId
|
||||||
|
|
||||||
|
ref: environment.projectId > project.projectId
|
||||||
|
|
||||||
ref: github.gitProviderId - git_provider.gitProviderId
|
ref: github.gitProviderId - git_provider.gitProviderId
|
||||||
|
|
||||||
ref: gitlab.gitProviderId - git_provider.gitProviderId
|
ref: gitlab.gitProviderId - git_provider.gitProviderId
|
||||||
|
|
||||||
ref: gitea.gitProviderId - git_provider.gitProviderId
|
ref: gitea.gitProviderId - git_provider.gitProviderId
|
||||||
|
|
||||||
ref: git_provider.userId - user.id
|
ref: git_provider.organizationId - organization.id
|
||||||
|
|
||||||
ref: mariadb.projectId > project.projectId
|
ref: git_provider.userId - user_temp.id
|
||||||
|
|
||||||
|
ref: invitation.organization_id - organization.id
|
||||||
|
|
||||||
|
ref: mariadb.environmentId > environment.environmentId
|
||||||
|
|
||||||
ref: mariadb.serverId > server.serverId
|
ref: mariadb.serverId > server.serverId
|
||||||
|
|
||||||
ref: mongo.projectId > project.projectId
|
ref: member.organization_id > organization.id
|
||||||
|
|
||||||
|
ref: member.user_id - user_temp.id
|
||||||
|
|
||||||
|
ref: mongo.environmentId > environment.environmentId
|
||||||
|
|
||||||
ref: mongo.serverId > server.serverId
|
ref: mongo.serverId > server.serverId
|
||||||
|
|
||||||
ref: mysql.projectId > project.projectId
|
ref: mysql.environmentId > environment.environmentId
|
||||||
|
|
||||||
ref: mysql.serverId > server.serverId
|
ref: mysql.serverId > server.serverId
|
||||||
|
|
||||||
@@ -868,30 +1116,58 @@ ref: notification.emailId - email.emailId
|
|||||||
|
|
||||||
ref: notification.gotifyId - gotify.gotifyId
|
ref: notification.gotifyId - gotify.gotifyId
|
||||||
|
|
||||||
ref: notification.userId - user.id
|
ref: notification.ntfyId - ntfy.ntfyId
|
||||||
|
|
||||||
|
ref: notification.customId - custom.customId
|
||||||
|
|
||||||
|
ref: notification.organizationId - organization.id
|
||||||
|
|
||||||
|
ref: organization.owner_id > user_temp.id
|
||||||
|
|
||||||
ref: port.applicationId > application.applicationId
|
ref: port.applicationId > application.applicationId
|
||||||
|
|
||||||
ref: postgres.projectId > project.projectId
|
ref: postgres.environmentId > environment.environmentId
|
||||||
|
|
||||||
ref: postgres.serverId > server.serverId
|
ref: postgres.serverId > server.serverId
|
||||||
|
|
||||||
ref: preview_deployments.applicationId > application.applicationId
|
ref: preview_deployments.applicationId > application.applicationId
|
||||||
|
|
||||||
ref: project.userId - user.id
|
ref: project.organizationId > organization.id
|
||||||
|
|
||||||
ref: redirect.applicationId > application.applicationId
|
ref: redirect.applicationId > application.applicationId
|
||||||
|
|
||||||
ref: redis.projectId > project.projectId
|
ref: redis.environmentId > environment.environmentId
|
||||||
|
|
||||||
ref: redis.serverId > server.serverId
|
ref: redis.serverId > server.serverId
|
||||||
|
|
||||||
ref: registry.userId - user.id
|
ref: schedule.applicationId - application.applicationId
|
||||||
|
|
||||||
|
ref: schedule.composeId > compose.composeId
|
||||||
|
|
||||||
|
ref: schedule.serverId > server.serverId
|
||||||
|
|
||||||
|
ref: schedule.userId > user_temp.id
|
||||||
|
|
||||||
ref: security.applicationId > application.applicationId
|
ref: security.applicationId > application.applicationId
|
||||||
|
|
||||||
ref: server.userId - user.id
|
|
||||||
|
|
||||||
ref: server.sshKeyId > "ssh-key".sshKeyId
|
ref: server.sshKeyId > "ssh-key".sshKeyId
|
||||||
|
|
||||||
ref: "ssh-key".userId - user.id
|
ref: server.organizationId > organization.id
|
||||||
|
|
||||||
|
ref: "ssh-key".organizationId - organization.id
|
||||||
|
|
||||||
|
ref: volume_backup.applicationId - application.applicationId
|
||||||
|
|
||||||
|
ref: volume_backup.postgresId - postgres.postgresId
|
||||||
|
|
||||||
|
ref: volume_backup.mariadbId - mariadb.mariadbId
|
||||||
|
|
||||||
|
ref: volume_backup.mongoId - mongo.mongoId
|
||||||
|
|
||||||
|
ref: volume_backup.mysqlId - mysql.mysqlId
|
||||||
|
|
||||||
|
ref: volume_backup.redisId - redis.redisId
|
||||||
|
|
||||||
|
ref: volume_backup.composeId - compose.composeId
|
||||||
|
|
||||||
|
ref: volume_backup.destinationId - destination.destinationId
|
||||||
@@ -31,7 +31,8 @@ export const user = pgTable("user", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.primaryKey()
|
.primaryKey()
|
||||||
.$defaultFn(() => nanoid()),
|
.$defaultFn(() => nanoid()),
|
||||||
name: text("name").notNull().default(""),
|
firstName: text("firstName").notNull().default(""),
|
||||||
|
lastName: text("lastName").notNull().default(""),
|
||||||
isRegistered: boolean("isRegistered").notNull().default(false),
|
isRegistered: boolean("isRegistered").notNull().default(false),
|
||||||
expirationDate: text("expirationDate")
|
expirationDate: text("expirationDate")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -56,7 +57,7 @@ export const user = pgTable("user", {
|
|||||||
host: text("host"),
|
host: text("host"),
|
||||||
letsEncryptEmail: text("letsEncryptEmail"),
|
letsEncryptEmail: text("letsEncryptEmail"),
|
||||||
sshPrivateKey: text("sshPrivateKey"),
|
sshPrivateKey: text("sshPrivateKey"),
|
||||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
|
||||||
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
|
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
|
||||||
role: text("role").notNull().default("user"),
|
role: text("role").notNull().default("user"),
|
||||||
// Metrics
|
// Metrics
|
||||||
@@ -332,6 +333,7 @@ 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(),
|
name: z.string().optional(),
|
||||||
|
lastName: z.string().optional(),
|
||||||
metricsConfig: z
|
metricsConfig: z
|
||||||
.object({
|
.object({
|
||||||
server: z.object({
|
server: z.object({
|
||||||
|
|||||||
123
packages/server/src/emails/emails/volume-backup.tsx
Normal file
123
packages/server/src/emails/emails/volume-backup.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Container,
|
||||||
|
Head,
|
||||||
|
Heading,
|
||||||
|
Html,
|
||||||
|
Img,
|
||||||
|
Preview,
|
||||||
|
Section,
|
||||||
|
Tailwind,
|
||||||
|
Text,
|
||||||
|
} from "@react-email/components";
|
||||||
|
|
||||||
|
export type TemplateProps = {
|
||||||
|
projectName: string;
|
||||||
|
applicationName: string;
|
||||||
|
volumeName: string;
|
||||||
|
serviceType:
|
||||||
|
| "application"
|
||||||
|
| "postgres"
|
||||||
|
| "mysql"
|
||||||
|
| "mongodb"
|
||||||
|
| "mariadb"
|
||||||
|
| "redis"
|
||||||
|
| "compose";
|
||||||
|
type: "error" | "success";
|
||||||
|
errorMessage?: string;
|
||||||
|
backupSize?: string;
|
||||||
|
date: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VolumeBackupEmail = ({
|
||||||
|
projectName = "dokploy",
|
||||||
|
applicationName = "frontend",
|
||||||
|
volumeName = "app-data",
|
||||||
|
serviceType = "application",
|
||||||
|
type = "success",
|
||||||
|
errorMessage,
|
||||||
|
backupSize,
|
||||||
|
date = "2023-05-01T00:00:00.000Z",
|
||||||
|
}: TemplateProps) => {
|
||||||
|
const previewText = `Volume backup for ${applicationName} was ${type === "success" ? "successful ✅" : "failed ❌"}`;
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Preview>{previewText}</Preview>
|
||||||
|
<Tailwind
|
||||||
|
config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
brand: "#007291",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Head />
|
||||||
|
|
||||||
|
<Body className="bg-white my-auto mx-auto font-sans px-2">
|
||||||
|
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
|
||||||
|
<Section className="mt-[32px]">
|
||||||
|
<Img
|
||||||
|
src={
|
||||||
|
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
|
||||||
|
}
|
||||||
|
width="100"
|
||||||
|
height="50"
|
||||||
|
alt="Dokploy"
|
||||||
|
className="my-0 mx-auto"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
|
||||||
|
Volume backup for <strong>{applicationName}</strong>
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
Hello,
|
||||||
|
</Text>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
Your volume backup for <strong>{applicationName}</strong> was{" "}
|
||||||
|
{type === "success"
|
||||||
|
? "successful ✅"
|
||||||
|
: "failed. Please check the error message below. ❌"}
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
<Section className="flex text-black text-[14px] leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
|
||||||
|
<Text className="!leading-3 font-bold">Details: </Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Project Name: <strong>{projectName}</strong>
|
||||||
|
</Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Application Name: <strong>{applicationName}</strong>
|
||||||
|
</Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Volume Name: <strong>{volumeName}</strong>
|
||||||
|
</Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Service Type: <strong>{serviceType}</strong>
|
||||||
|
</Text>
|
||||||
|
{backupSize && (
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Backup Size: <strong>{backupSize}</strong>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Date: <strong>{date}</strong>
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
{type === "error" && errorMessage ? (
|
||||||
|
<Section className="flex text-black text-[14px] mt-4 leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
|
||||||
|
<Text className="!leading-3 font-bold">Reason: </Text>
|
||||||
|
<Text className="text-[12px] leading-[24px]">
|
||||||
|
{errorMessage || "Error message not provided"}
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
) : null}
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VolumeBackupEmail;
|
||||||
@@ -132,11 +132,15 @@ const { handler, api } = betterAuth({
|
|||||||
const hutk = getHubSpotUTK(
|
const hutk = getHubSpotUTK(
|
||||||
context?.request?.headers?.get("cookie") || undefined,
|
context?.request?.headers?.get("cookie") || undefined,
|
||||||
);
|
);
|
||||||
|
// Cast to include additional fields
|
||||||
|
const userWithFields = user as typeof user & {
|
||||||
|
lastName?: string;
|
||||||
|
};
|
||||||
const hubspotSuccess = await submitToHubSpot(
|
const hubspotSuccess = await submitToHubSpot(
|
||||||
{
|
{
|
||||||
email: user.email,
|
email: user.email,
|
||||||
firstName: user.name,
|
firstName: user.name || "", // name is mapped to firstName column
|
||||||
lastName: user.name,
|
lastName: userWithFields.lastName || "",
|
||||||
},
|
},
|
||||||
hutk,
|
hutk,
|
||||||
);
|
);
|
||||||
@@ -204,6 +208,9 @@ const { handler, api } = betterAuth({
|
|||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
modelName: "user",
|
modelName: "user",
|
||||||
|
fields: {
|
||||||
|
name: "firstName", // Map better-auth's default 'name' field to 'firstName' column
|
||||||
|
},
|
||||||
additionalFields: {
|
additionalFields: {
|
||||||
role: {
|
role: {
|
||||||
type: "string",
|
type: "string",
|
||||||
@@ -220,6 +227,12 @@ const { handler, api } = betterAuth({
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
|
lastName: {
|
||||||
|
type: "string",
|
||||||
|
required: false,
|
||||||
|
input: true,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -316,16 +329,11 @@ export const validateRequest = async (request: IncomingMessage) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
// When accessing from DB, use actual column names
|
||||||
id,
|
const userFromDb = apiKeyRecord.user as typeof apiKeyRecord.user & {
|
||||||
name,
|
firstName: string;
|
||||||
email,
|
lastName: string;
|
||||||
emailVerified,
|
};
|
||||||
image,
|
|
||||||
createdAt,
|
|
||||||
updatedAt,
|
|
||||||
twoFactorEnabled,
|
|
||||||
} = apiKeyRecord.user;
|
|
||||||
|
|
||||||
const mockSession = {
|
const mockSession = {
|
||||||
session: {
|
session: {
|
||||||
@@ -333,14 +341,14 @@ export const validateRequest = async (request: IncomingMessage) => {
|
|||||||
activeOrganizationId: organizationId || "",
|
activeOrganizationId: organizationId || "",
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
id,
|
id: userFromDb.id,
|
||||||
name,
|
name: userFromDb.firstName, // Map firstName back to name for better-auth
|
||||||
email,
|
email: userFromDb.email,
|
||||||
emailVerified,
|
emailVerified: userFromDb.emailVerified,
|
||||||
image,
|
image: userFromDb.image,
|
||||||
createdAt,
|
createdAt: userFromDb.createdAt,
|
||||||
updatedAt,
|
updatedAt: userFromDb.updatedAt,
|
||||||
twoFactorEnabled,
|
twoFactorEnabled: userFromDb.twoFactorEnabled,
|
||||||
role: member?.role || "member",
|
role: member?.role || "member",
|
||||||
ownerId: member?.organization.ownerId || apiKeyRecord.user.id,
|
ownerId: member?.organization.ownerId || apiKeyRecord.user.id,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const isAdminPresent = async () => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const findAdmin = async () => {
|
export const findOwner = async () => {
|
||||||
const admin = await db.query.member.findFirst({
|
const admin = await db.query.member.findFirst({
|
||||||
where: eq(member.role, "owner"),
|
where: eq(member.role, "owner"),
|
||||||
with: {
|
with: {
|
||||||
@@ -107,11 +107,11 @@ export const getDokployUrl = async () => {
|
|||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return "https://app.dokploy.com";
|
return "https://app.dokploy.com";
|
||||||
}
|
}
|
||||||
const admin = await findAdmin();
|
const owner = await findOwner();
|
||||||
|
|
||||||
if (admin.user.host) {
|
if (owner.user.host) {
|
||||||
const protocol = admin.user.https ? "https" : "http";
|
const protocol = owner.user.https ? "https" : "http";
|
||||||
return `${protocol}://${admin.user.host}`;
|
return `${protocol}://${owner.user.host}`;
|
||||||
}
|
}
|
||||||
return `http://${admin.user.serverIp}:${process.env.PORT}`;
|
return `http://${owner.user.serverIp}:${process.env.PORT}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -104,14 +104,28 @@ export const suggestVariants = async ({
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
prompt: `
|
prompt: `
|
||||||
Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items).
|
Act as advanced DevOps engineer and analyze the user's request to determine the appropriate suggestions (up to 3 items).
|
||||||
|
|
||||||
|
CRITICAL - Read the user's request carefully and follow the appropriate strategy:
|
||||||
|
|
||||||
|
Strategy A - If the user specifies a PARTICULAR APPLICATION/SERVICE (e.g., "deploy Chatwoot", "install sendingtk/chatwoot:develop", "setup Bitwarden"):
|
||||||
|
- Generate different deployment VARIANTS of that SAME application
|
||||||
|
- Each variant should be a different configuration (minimal, full stack, with different databases, development vs production, etc.)
|
||||||
|
- Example: For "Chatwoot" → "Chatwoot with PostgreSQL", "Chatwoot Development", "Chatwoot Full Stack"
|
||||||
|
- The name MUST include the specific application name the user mentioned
|
||||||
|
|
||||||
|
Strategy B - If the user describes a GENERAL NEED or USE CASE (e.g., "personal blog", "project management tool", "chat application"):
|
||||||
|
- Suggest different open source projects that fulfill that need
|
||||||
|
- Each suggestion should be a different tool/platform that solves the same problem
|
||||||
|
- Example: For "personal blog" → "WordPress", "Ghost", "Hugo with Nginx"
|
||||||
|
- The name should be the actual project name
|
||||||
|
|
||||||
Return your response as a JSON object with the following structure:
|
Return your response as a JSON object with the following structure:
|
||||||
{
|
{
|
||||||
"suggestions": [
|
"suggestions": [
|
||||||
{
|
{
|
||||||
"id": "project-slug",
|
"id": "project-or-variant-slug",
|
||||||
"name": "Project Name",
|
"name": "Project Name or Variant Name",
|
||||||
"shortDescription": "Brief one-line description",
|
"shortDescription": "Brief one-line description",
|
||||||
"description": "Detailed description"
|
"description": "Detailed description"
|
||||||
}
|
}
|
||||||
@@ -120,10 +134,14 @@ export const suggestVariants = async ({
|
|||||||
|
|
||||||
Important rules for the response:
|
Important rules for the response:
|
||||||
1. Use slug format for the id field (lowercase, hyphenated)
|
1. Use slug format for the id field (lowercase, hyphenated)
|
||||||
2. The description field should ONLY contain a plain text description of the project, its features, and use cases
|
2. Determine which strategy to use based on whether the user specified a particular application or described a general need
|
||||||
3. Do NOT include any code snippets, configuration examples, or installation instructions in the description
|
3. For Strategy A (specific app): The name must include the app name and describe the variant configuration
|
||||||
4. The shortDescription should be a single-line summary focusing on the main technologies
|
4. For Strategy B (general need): The name should be the actual project/tool name that fulfills the need
|
||||||
5. All projects should be installable in docker and have docker compose support
|
5. The description field should ONLY contain a plain text description of the project or variant, its features, and use cases
|
||||||
|
6. Do NOT include any code snippets, configuration examples, or installation instructions in the description
|
||||||
|
7. The shortDescription should be a single-line summary focusing on key technologies or differentiators
|
||||||
|
8. All suggestions should be installable in docker and have docker compose support
|
||||||
|
9. Provide variety in your suggestions - different complexity levels, tech stacks, or approaches
|
||||||
|
|
||||||
User wants to create a new project with the following details:
|
User wants to create a new project with the following details:
|
||||||
|
|
||||||
@@ -186,6 +204,24 @@ export const suggestVariants = async ({
|
|||||||
6. If a service depends on a database or other service, INCLUDE that service in the docker-compose
|
6. If a service depends on a database or other service, INCLUDE that service in the docker-compose
|
||||||
7. Make sure all required services are defined in the docker-compose
|
7. Make sure all required services are defined in the docker-compose
|
||||||
|
|
||||||
|
Docker Image Rules (CRITICAL):
|
||||||
|
1. ALWAYS use 'image:' field, NEVER use 'build:' field
|
||||||
|
2. NEVER use 'build: .' or any build directive - we don't have local Dockerfiles
|
||||||
|
3. Use images from Docker Hub or other public registries (e.g., docker.io, ghcr.io, quay.io)
|
||||||
|
4. For dependencies (databases, redis, etc.), use official images (e.g., postgres:16, redis:7, etc.)
|
||||||
|
5. Always specify image tags - avoid using 'latest' tag, use specific versions when possible
|
||||||
|
6. Examples of correct image usage:
|
||||||
|
- image: sendingtk/chatwoot:develop
|
||||||
|
- image: postgres:16-alpine
|
||||||
|
- image: redis:7-alpine
|
||||||
|
- image: chatwoot/chatwoot:latest
|
||||||
|
7. Examples of INCORRECT usage (DO NOT USE):
|
||||||
|
- build: .
|
||||||
|
- build: ./app
|
||||||
|
- build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
|
||||||
Volume Mounting and Configuration Rules:
|
Volume Mounting and Configuration Rules:
|
||||||
1. DO NOT create configuration files unless the service CANNOT work without them
|
1. DO NOT create configuration files unless the service CANNOT work without them
|
||||||
2. Most services can work with just environment variables - USE THEM FIRST
|
2. Most services can work with just environment variables - USE THEM FIRST
|
||||||
@@ -214,6 +250,8 @@ export const suggestVariants = async ({
|
|||||||
- serviceName: the name of the service in the docker-compose
|
- serviceName: the name of the service in the docker-compose
|
||||||
2. Make sure the service is properly configured to work with the specified port
|
2. Make sure the service is properly configured to work with the specified port
|
||||||
|
|
||||||
|
User's original request: ${input}
|
||||||
|
|
||||||
Project details:
|
Project details:
|
||||||
${suggestion?.description}
|
${suggestion?.description}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ import {
|
|||||||
updatePreviewDeployment,
|
updatePreviewDeployment,
|
||||||
} from "./preview-deployment";
|
} from "./preview-deployment";
|
||||||
import { validUniqueServerAppName } from "./project";
|
import { validUniqueServerAppName } from "./project";
|
||||||
import { createRollback } from "./rollbacks";
|
|
||||||
export type Application = typeof applications.$inferSelect;
|
export type Application = typeof applications.$inferSelect;
|
||||||
|
|
||||||
export const createApplication = async (
|
export const createApplication = async (
|
||||||
@@ -113,6 +112,7 @@ export const findApplicationById = async (applicationId: string) => {
|
|||||||
server: true,
|
server: true,
|
||||||
previewDeployments: true,
|
previewDeployments: true,
|
||||||
buildRegistry: true,
|
buildRegistry: true,
|
||||||
|
rollbackRegistry: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!application) {
|
if (!application) {
|
||||||
@@ -198,7 +198,7 @@ export const deployApplication = async ({
|
|||||||
command += await buildRemoteDocker(application);
|
command += await buildRemoteDocker(application);
|
||||||
}
|
}
|
||||||
|
|
||||||
command += getBuildCommand(application);
|
command += await getBuildCommand(application);
|
||||||
|
|
||||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
@@ -211,17 +211,6 @@ export const deployApplication = async ({
|
|||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
await updateApplicationStatus(applicationId, "done");
|
await updateApplicationStatus(applicationId, "done");
|
||||||
|
|
||||||
if (application.rollbackActive) {
|
|
||||||
const tagImage =
|
|
||||||
application.sourceType === "docker"
|
|
||||||
? application.dockerImage
|
|
||||||
: application.appName;
|
|
||||||
await createRollback({
|
|
||||||
appName: tagImage || "",
|
|
||||||
deploymentId: deployment.deploymentId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await sendBuildSuccessNotifications({
|
await sendBuildSuccessNotifications({
|
||||||
projectName: application.environment.project.name,
|
projectName: application.environment.project.name,
|
||||||
applicationName: application.name,
|
applicationName: application.name,
|
||||||
@@ -299,7 +288,7 @@ export const rebuildApplication = async ({
|
|||||||
try {
|
try {
|
||||||
let command = "set -e;";
|
let command = "set -e;";
|
||||||
// Check case for docker only
|
// Check case for docker only
|
||||||
command += getBuildCommand(application);
|
command += await getBuildCommand(application);
|
||||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
await execAsyncRemote(serverId, commandWithLog);
|
await execAsyncRemote(serverId, commandWithLog);
|
||||||
@@ -310,17 +299,6 @@ export const rebuildApplication = async ({
|
|||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
await updateApplicationStatus(applicationId, "done");
|
await updateApplicationStatus(applicationId, "done");
|
||||||
|
|
||||||
if (application.rollbackActive) {
|
|
||||||
const tagImage =
|
|
||||||
application.sourceType === "docker"
|
|
||||||
? application.dockerImage
|
|
||||||
: application.appName;
|
|
||||||
await createRollback({
|
|
||||||
appName: tagImage || "",
|
|
||||||
deploymentId: deployment.deploymentId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await sendBuildSuccessNotifications({
|
await sendBuildSuccessNotifications({
|
||||||
projectName: application.environment.project.name,
|
projectName: application.environment.project.name,
|
||||||
applicationName: application.name,
|
applicationName: application.name,
|
||||||
@@ -423,6 +401,10 @@ export const deployPreviewApplication = async ({
|
|||||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||||
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||||
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||||
|
application.rollbackActive = false;
|
||||||
|
application.buildRegistry = null;
|
||||||
|
application.rollbackRegistry = null;
|
||||||
|
application.registry = null;
|
||||||
|
|
||||||
let command = "set -e;";
|
let command = "set -e;";
|
||||||
if (application.sourceType === "github") {
|
if (application.sourceType === "github") {
|
||||||
@@ -431,7 +413,7 @@ export const deployPreviewApplication = async ({
|
|||||||
appName: previewDeployment.appName,
|
appName: previewDeployment.appName,
|
||||||
branch: previewDeployment.branch,
|
branch: previewDeployment.branch,
|
||||||
});
|
});
|
||||||
command += getBuildCommand(application);
|
command += await getBuildCommand(application);
|
||||||
|
|
||||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||||
if (application.serverId) {
|
if (application.serverId) {
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import {
|
import {
|
||||||
|
type apiCreateCustom,
|
||||||
type apiCreateDiscord,
|
type apiCreateDiscord,
|
||||||
type apiCreateEmail,
|
type apiCreateEmail,
|
||||||
type apiCreateLark,
|
|
||||||
type apiCreateGotify,
|
type apiCreateGotify,
|
||||||
|
type apiCreateLark,
|
||||||
type apiCreateNtfy,
|
type apiCreateNtfy,
|
||||||
type apiCreateSlack,
|
type apiCreateSlack,
|
||||||
type apiCreateTelegram,
|
type apiCreateTelegram,
|
||||||
|
type apiUpdateCustom,
|
||||||
type apiUpdateDiscord,
|
type apiUpdateDiscord,
|
||||||
type apiUpdateEmail,
|
type apiUpdateEmail,
|
||||||
type apiUpdateLark,
|
|
||||||
type apiUpdateGotify,
|
type apiUpdateGotify,
|
||||||
|
type apiUpdateLark,
|
||||||
type apiUpdateNtfy,
|
type apiUpdateNtfy,
|
||||||
type apiUpdateSlack,
|
type apiUpdateSlack,
|
||||||
type apiUpdateTelegram,
|
type apiUpdateTelegram,
|
||||||
|
custom,
|
||||||
discord,
|
discord,
|
||||||
email,
|
email,
|
||||||
lark,
|
|
||||||
gotify,
|
gotify,
|
||||||
|
lark,
|
||||||
notifications,
|
notifications,
|
||||||
ntfy,
|
ntfy,
|
||||||
slack,
|
slack,
|
||||||
@@ -57,6 +60,7 @@ export const createSlackNotification = async (
|
|||||||
appDeploy: input.appDeploy,
|
appDeploy: input.appDeploy,
|
||||||
appBuildError: input.appBuildError,
|
appBuildError: input.appBuildError,
|
||||||
databaseBackup: input.databaseBackup,
|
databaseBackup: input.databaseBackup,
|
||||||
|
volumeBackup: input.volumeBackup,
|
||||||
dokployRestart: input.dokployRestart,
|
dokployRestart: input.dokployRestart,
|
||||||
dockerCleanup: input.dockerCleanup,
|
dockerCleanup: input.dockerCleanup,
|
||||||
notificationType: "slack",
|
notificationType: "slack",
|
||||||
@@ -88,6 +92,7 @@ export const updateSlackNotification = async (
|
|||||||
appDeploy: input.appDeploy,
|
appDeploy: input.appDeploy,
|
||||||
appBuildError: input.appBuildError,
|
appBuildError: input.appBuildError,
|
||||||
databaseBackup: input.databaseBackup,
|
databaseBackup: input.databaseBackup,
|
||||||
|
volumeBackup: input.volumeBackup,
|
||||||
dokployRestart: input.dokployRestart,
|
dokployRestart: input.dokployRestart,
|
||||||
dockerCleanup: input.dockerCleanup,
|
dockerCleanup: input.dockerCleanup,
|
||||||
organizationId: input.organizationId,
|
organizationId: input.organizationId,
|
||||||
@@ -148,6 +153,7 @@ export const createTelegramNotification = async (
|
|||||||
appDeploy: input.appDeploy,
|
appDeploy: input.appDeploy,
|
||||||
appBuildError: input.appBuildError,
|
appBuildError: input.appBuildError,
|
||||||
databaseBackup: input.databaseBackup,
|
databaseBackup: input.databaseBackup,
|
||||||
|
volumeBackup: input.volumeBackup,
|
||||||
dokployRestart: input.dokployRestart,
|
dokployRestart: input.dokployRestart,
|
||||||
dockerCleanup: input.dockerCleanup,
|
dockerCleanup: input.dockerCleanup,
|
||||||
notificationType: "telegram",
|
notificationType: "telegram",
|
||||||
@@ -179,6 +185,7 @@ export const updateTelegramNotification = async (
|
|||||||
appDeploy: input.appDeploy,
|
appDeploy: input.appDeploy,
|
||||||
appBuildError: input.appBuildError,
|
appBuildError: input.appBuildError,
|
||||||
databaseBackup: input.databaseBackup,
|
databaseBackup: input.databaseBackup,
|
||||||
|
volumeBackup: input.volumeBackup,
|
||||||
dokployRestart: input.dokployRestart,
|
dokployRestart: input.dokployRestart,
|
||||||
dockerCleanup: input.dockerCleanup,
|
dockerCleanup: input.dockerCleanup,
|
||||||
organizationId: input.organizationId,
|
organizationId: input.organizationId,
|
||||||
@@ -239,6 +246,7 @@ export const createDiscordNotification = async (
|
|||||||
appDeploy: input.appDeploy,
|
appDeploy: input.appDeploy,
|
||||||
appBuildError: input.appBuildError,
|
appBuildError: input.appBuildError,
|
||||||
databaseBackup: input.databaseBackup,
|
databaseBackup: input.databaseBackup,
|
||||||
|
volumeBackup: input.volumeBackup,
|
||||||
dokployRestart: input.dokployRestart,
|
dokployRestart: input.dokployRestart,
|
||||||
dockerCleanup: input.dockerCleanup,
|
dockerCleanup: input.dockerCleanup,
|
||||||
notificationType: "discord",
|
notificationType: "discord",
|
||||||
@@ -270,6 +278,7 @@ export const updateDiscordNotification = async (
|
|||||||
appDeploy: input.appDeploy,
|
appDeploy: input.appDeploy,
|
||||||
appBuildError: input.appBuildError,
|
appBuildError: input.appBuildError,
|
||||||
databaseBackup: input.databaseBackup,
|
databaseBackup: input.databaseBackup,
|
||||||
|
volumeBackup: input.volumeBackup,
|
||||||
dokployRestart: input.dokployRestart,
|
dokployRestart: input.dokployRestart,
|
||||||
dockerCleanup: input.dockerCleanup,
|
dockerCleanup: input.dockerCleanup,
|
||||||
organizationId: input.organizationId,
|
organizationId: input.organizationId,
|
||||||
@@ -333,6 +342,7 @@ export const createEmailNotification = async (
|
|||||||
appDeploy: input.appDeploy,
|
appDeploy: input.appDeploy,
|
||||||
appBuildError: input.appBuildError,
|
appBuildError: input.appBuildError,
|
||||||
databaseBackup: input.databaseBackup,
|
databaseBackup: input.databaseBackup,
|
||||||
|
volumeBackup: input.volumeBackup,
|
||||||
dokployRestart: input.dokployRestart,
|
dokployRestart: input.dokployRestart,
|
||||||
dockerCleanup: input.dockerCleanup,
|
dockerCleanup: input.dockerCleanup,
|
||||||
notificationType: "email",
|
notificationType: "email",
|
||||||
@@ -364,6 +374,7 @@ export const updateEmailNotification = async (
|
|||||||
appDeploy: input.appDeploy,
|
appDeploy: input.appDeploy,
|
||||||
appBuildError: input.appBuildError,
|
appBuildError: input.appBuildError,
|
||||||
databaseBackup: input.databaseBackup,
|
databaseBackup: input.databaseBackup,
|
||||||
|
volumeBackup: input.volumeBackup,
|
||||||
dokployRestart: input.dokployRestart,
|
dokployRestart: input.dokployRestart,
|
||||||
dockerCleanup: input.dockerCleanup,
|
dockerCleanup: input.dockerCleanup,
|
||||||
organizationId: input.organizationId,
|
organizationId: input.organizationId,
|
||||||
@@ -429,6 +440,7 @@ export const createGotifyNotification = async (
|
|||||||
appDeploy: input.appDeploy,
|
appDeploy: input.appDeploy,
|
||||||
appBuildError: input.appBuildError,
|
appBuildError: input.appBuildError,
|
||||||
databaseBackup: input.databaseBackup,
|
databaseBackup: input.databaseBackup,
|
||||||
|
volumeBackup: input.volumeBackup,
|
||||||
dokployRestart: input.dokployRestart,
|
dokployRestart: input.dokployRestart,
|
||||||
dockerCleanup: input.dockerCleanup,
|
dockerCleanup: input.dockerCleanup,
|
||||||
notificationType: "gotify",
|
notificationType: "gotify",
|
||||||
@@ -459,6 +471,7 @@ export const updateGotifyNotification = async (
|
|||||||
appDeploy: input.appDeploy,
|
appDeploy: input.appDeploy,
|
||||||
appBuildError: input.appBuildError,
|
appBuildError: input.appBuildError,
|
||||||
databaseBackup: input.databaseBackup,
|
databaseBackup: input.databaseBackup,
|
||||||
|
volumeBackup: input.volumeBackup,
|
||||||
dokployRestart: input.dokployRestart,
|
dokployRestart: input.dokployRestart,
|
||||||
dockerCleanup: input.dockerCleanup,
|
dockerCleanup: input.dockerCleanup,
|
||||||
organizationId: input.organizationId,
|
organizationId: input.organizationId,
|
||||||
@@ -519,6 +532,7 @@ export const createNtfyNotification = async (
|
|||||||
appDeploy: input.appDeploy,
|
appDeploy: input.appDeploy,
|
||||||
appBuildError: input.appBuildError,
|
appBuildError: input.appBuildError,
|
||||||
databaseBackup: input.databaseBackup,
|
databaseBackup: input.databaseBackup,
|
||||||
|
volumeBackup: input.volumeBackup,
|
||||||
dokployRestart: input.dokployRestart,
|
dokployRestart: input.dokployRestart,
|
||||||
dockerCleanup: input.dockerCleanup,
|
dockerCleanup: input.dockerCleanup,
|
||||||
notificationType: "ntfy",
|
notificationType: "ntfy",
|
||||||
@@ -549,6 +563,7 @@ export const updateNtfyNotification = async (
|
|||||||
appDeploy: input.appDeploy,
|
appDeploy: input.appDeploy,
|
||||||
appBuildError: input.appBuildError,
|
appBuildError: input.appBuildError,
|
||||||
databaseBackup: input.databaseBackup,
|
databaseBackup: input.databaseBackup,
|
||||||
|
volumeBackup: input.volumeBackup,
|
||||||
dokployRestart: input.dokployRestart,
|
dokployRestart: input.dokployRestart,
|
||||||
dockerCleanup: input.dockerCleanup,
|
dockerCleanup: input.dockerCleanup,
|
||||||
organizationId: input.organizationId,
|
organizationId: input.organizationId,
|
||||||
@@ -578,6 +593,94 @@ export const updateNtfyNotification = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createCustomNotification = async (
|
||||||
|
input: typeof apiCreateCustom._type,
|
||||||
|
organizationId: string,
|
||||||
|
) => {
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
const newCustom = await tx
|
||||||
|
.insert(custom)
|
||||||
|
.values({
|
||||||
|
endpoint: input.endpoint,
|
||||||
|
headers: input.headers,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.then((value) => value[0]);
|
||||||
|
|
||||||
|
if (!newCustom) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error input: Inserting custom",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDestination = await tx
|
||||||
|
.insert(notifications)
|
||||||
|
.values({
|
||||||
|
customId: newCustom.customId,
|
||||||
|
name: input.name,
|
||||||
|
appDeploy: input.appDeploy,
|
||||||
|
appBuildError: input.appBuildError,
|
||||||
|
databaseBackup: input.databaseBackup,
|
||||||
|
dokployRestart: input.dokployRestart,
|
||||||
|
dockerCleanup: input.dockerCleanup,
|
||||||
|
notificationType: "custom",
|
||||||
|
organizationId: organizationId,
|
||||||
|
serverThreshold: input.serverThreshold,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.then((value) => value[0]);
|
||||||
|
|
||||||
|
if (!newDestination) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error input: Inserting notification",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return newDestination;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateCustomNotification = async (
|
||||||
|
input: typeof apiUpdateCustom._type,
|
||||||
|
) => {
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
const newDestination = await tx
|
||||||
|
.update(notifications)
|
||||||
|
.set({
|
||||||
|
name: input.name,
|
||||||
|
appDeploy: input.appDeploy,
|
||||||
|
appBuildError: input.appBuildError,
|
||||||
|
databaseBackup: input.databaseBackup,
|
||||||
|
dokployRestart: input.dokployRestart,
|
||||||
|
dockerCleanup: input.dockerCleanup,
|
||||||
|
organizationId: input.organizationId,
|
||||||
|
serverThreshold: input.serverThreshold,
|
||||||
|
})
|
||||||
|
.where(eq(notifications.notificationId, input.notificationId))
|
||||||
|
.returning()
|
||||||
|
.then((value) => value[0]);
|
||||||
|
|
||||||
|
if (!newDestination) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error Updating notification",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx
|
||||||
|
.update(custom)
|
||||||
|
.set({
|
||||||
|
endpoint: input.endpoint,
|
||||||
|
headers: input.headers,
|
||||||
|
})
|
||||||
|
.where(eq(custom.customId, input.customId));
|
||||||
|
|
||||||
|
return newDestination;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const findNotificationById = async (notificationId: string) => {
|
export const findNotificationById = async (notificationId: string) => {
|
||||||
const notification = await db.query.notifications.findFirst({
|
const notification = await db.query.notifications.findFirst({
|
||||||
where: eq(notifications.notificationId, notificationId),
|
where: eq(notifications.notificationId, notificationId),
|
||||||
@@ -588,6 +691,7 @@ export const findNotificationById = async (notificationId: string) => {
|
|||||||
email: true,
|
email: true,
|
||||||
gotify: true,
|
gotify: true,
|
||||||
ntfy: true,
|
ntfy: true,
|
||||||
|
custom: true,
|
||||||
lark: true,
|
lark: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export function getMountPath(dockerImage: string): string {
|
|||||||
if (versionMatch?.[1]) {
|
if (versionMatch?.[1]) {
|
||||||
const version = Number.parseInt(versionMatch[1], 10);
|
const version = Number.parseInt(versionMatch[1], 10);
|
||||||
if (version >= 18) {
|
if (version >= 18) {
|
||||||
return `/var/lib/postgresql/${version}/data`;
|
// PostgreSQL 18+ uses /var/lib/postgresql/{version}/docker as the default PGDATA
|
||||||
|
return `/var/lib/postgresql/${version}/docker`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "/var/lib/postgresql/data";
|
return "/var/lib/postgresql/data";
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
deployments as deploymentsSchema,
|
deployments as deploymentsSchema,
|
||||||
rollbacks,
|
rollbacks,
|
||||||
} from "../db/schema";
|
} from "../db/schema";
|
||||||
import { type ApplicationNested, getAuthConfig } from "../utils/builders";
|
import type { ApplicationNested } from "../utils/builders";
|
||||||
|
import { getRegistryTag } from "../utils/cluster/upload";
|
||||||
import {
|
import {
|
||||||
calculateResources,
|
calculateResources,
|
||||||
generateBindMounts,
|
generateBindMounts,
|
||||||
@@ -22,11 +23,12 @@ import { findDeploymentById } from "./deployment";
|
|||||||
import type { Mount } from "./mount";
|
import type { Mount } from "./mount";
|
||||||
import type { Port } from "./port";
|
import type { Port } from "./port";
|
||||||
import type { Project } from "./project";
|
import type { Project } from "./project";
|
||||||
|
import type { Registry } from "./registry";
|
||||||
|
|
||||||
export const createRollback = async (
|
export const createRollback = async (
|
||||||
input: z.infer<typeof createRollbackSchema>,
|
input: z.infer<typeof createRollbackSchema>,
|
||||||
) => {
|
) => {
|
||||||
await db.transaction(async (tx) => {
|
return await db.transaction(async (tx) => {
|
||||||
const { fullContext, ...other } = input;
|
const { fullContext, ...other } = input;
|
||||||
const rollback = await tx
|
const rollback = await tx
|
||||||
.insert(rollbacks)
|
.insert(rollbacks)
|
||||||
@@ -70,9 +72,11 @@ export const createRollback = async (
|
|||||||
})
|
})
|
||||||
.where(eq(deploymentsSchema.deploymentId, rollback.deploymentId));
|
.where(eq(deploymentsSchema.deploymentId, rollback.deploymentId));
|
||||||
|
|
||||||
await createRollbackImage(rest, tagImage);
|
const updatedRollback = await tx.query.rollbacks.findFirst({
|
||||||
|
where: eq(rollbacks.rollbackId, rollback.rollbackId),
|
||||||
|
});
|
||||||
|
|
||||||
return rollback;
|
return updatedRollback;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,27 +107,6 @@ export const findRollbackById = async (rollbackId: string) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createRollbackImage = async (
|
|
||||||
application: ApplicationNested,
|
|
||||||
tagImage: string,
|
|
||||||
) => {
|
|
||||||
const docker = await getRemoteDocker(application.serverId);
|
|
||||||
|
|
||||||
const appTagName =
|
|
||||||
application.sourceType === "docker"
|
|
||||||
? application.dockerImage
|
|
||||||
: `${application.appName}:latest`;
|
|
||||||
|
|
||||||
const result = docker.getImage(appTagName || "");
|
|
||||||
|
|
||||||
const [repo, version] = tagImage.split(":");
|
|
||||||
|
|
||||||
await result.tag({
|
|
||||||
repo,
|
|
||||||
tag: version,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteRollbackImage = async (image: string, serverId?: string | null) => {
|
const deleteRollbackImage = async (image: string, serverId?: string | null) => {
|
||||||
const command = `docker image rm ${image} --force`;
|
const command = `docker image rm ${image} --force`;
|
||||||
|
|
||||||
@@ -179,7 +162,6 @@ export const rollback = async (rollbackId: string) => {
|
|||||||
if (!result.fullContext) {
|
if (!result.fullContext) {
|
||||||
throw new Error("Rollback context not found");
|
throw new Error("Rollback context not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the full context for rollback
|
// Use the full context for rollback
|
||||||
await rollbackApplication(
|
await rollbackApplication(
|
||||||
application.appName,
|
application.appName,
|
||||||
@@ -199,6 +181,7 @@ const rollbackApplication = async (
|
|||||||
};
|
};
|
||||||
mounts: Mount[];
|
mounts: Mount[];
|
||||||
ports: Port[];
|
ports: Port[];
|
||||||
|
rollbackRegistry?: Registry;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
if (!fullContext) {
|
if (!fullContext) {
|
||||||
@@ -245,16 +228,24 @@ const rollbackApplication = async (
|
|||||||
fullContext.environment.project.env,
|
fullContext.environment.project.env,
|
||||||
);
|
);
|
||||||
|
|
||||||
// For rollback, we use the provided image instead of calculating it
|
// Build the full registry image path if rollbackRegistry is available
|
||||||
const authConfig = getAuthConfig(fullContext as ApplicationNested);
|
// e.g., "appName:v5" -> "siumauricio/appName:v5" or "registry.com/prefix/appName:v5"
|
||||||
|
let rollbackImage = image;
|
||||||
|
if (fullContext.rollbackRegistry) {
|
||||||
|
rollbackImage = getRegistryTag(fullContext.rollbackRegistry, image);
|
||||||
|
}
|
||||||
|
|
||||||
const settings: CreateServiceOptions = {
|
const settings: CreateServiceOptions = {
|
||||||
authconfig: authConfig,
|
authconfig: {
|
||||||
|
password: fullContext.rollbackRegistry?.password || "",
|
||||||
|
username: fullContext.rollbackRegistry?.username || "",
|
||||||
|
serveraddress: fullContext.rollbackRegistry?.registryUrl || "",
|
||||||
|
},
|
||||||
Name: appName,
|
Name: appName,
|
||||||
TaskTemplate: {
|
TaskTemplate: {
|
||||||
ContainerSpec: {
|
ContainerSpec: {
|
||||||
HealthCheck,
|
HealthCheck,
|
||||||
Image: image,
|
Image: rollbackImage,
|
||||||
Env: envVariables,
|
Env: envVariables,
|
||||||
Mounts: [...volumesMount, ...bindsMount],
|
Mounts: [...volumesMount, ...bindsMount],
|
||||||
...(command
|
...(command
|
||||||
@@ -297,7 +288,8 @@ const rollbackApplication = async (
|
|||||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
await docker.createService(settings);
|
await docker.createService(settings);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -215,38 +215,6 @@ echo "$json_output"
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanupFullDocker = async (serverId?: string | null) => {
|
|
||||||
const cleanupImages = "docker image prune --force";
|
|
||||||
const cleanupVolumes = "docker volume prune --force";
|
|
||||||
const cleanupContainers = "docker container prune --force";
|
|
||||||
const cleanupSystem = "docker system prune --force --volumes";
|
|
||||||
const cleanupBuilder = "docker builder prune --force";
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (serverId) {
|
|
||||||
await execAsyncRemote(
|
|
||||||
serverId,
|
|
||||||
`
|
|
||||||
${cleanupImages}
|
|
||||||
${cleanupVolumes}
|
|
||||||
${cleanupContainers}
|
|
||||||
${cleanupSystem}
|
|
||||||
${cleanupBuilder}
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await execAsync(`
|
|
||||||
${cleanupImages}
|
|
||||||
${cleanupVolumes}
|
|
||||||
${cleanupContainers}
|
|
||||||
${cleanupSystem}
|
|
||||||
${cleanupBuilder}
|
|
||||||
`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDockerResourceType = async (
|
export const getDockerResourceType = async (
|
||||||
resourceName: string,
|
resourceName: string,
|
||||||
serverId?: string,
|
serverId?: string,
|
||||||
@@ -372,19 +340,27 @@ export const readPorts = async (
|
|||||||
publishedPort: number;
|
publishedPort: number;
|
||||||
protocol?: string;
|
protocol?: string;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
const seenPorts = new Set<string>();
|
||||||
for (const key in parsedResult) {
|
for (const key in parsedResult) {
|
||||||
if (Object.hasOwn(parsedResult, key)) {
|
if (Object.hasOwn(parsedResult, key)) {
|
||||||
const containerPortMapppings = parsedResult[key];
|
const containerPortMapppings = parsedResult[key];
|
||||||
const protocol = key.split("/")[1];
|
const protocol = key.split("/")[1];
|
||||||
const targetPort = Number.parseInt(key.split("/")[0] ?? "0", 10);
|
const targetPort = Number.parseInt(key.split("/")[0] ?? "0", 10);
|
||||||
|
|
||||||
containerPortMapppings.forEach((mapping: any) => {
|
// Take only the first mapping to avoid duplicates (IPv4 and IPv6)
|
||||||
ports.push({
|
const firstMapping = containerPortMapppings[0];
|
||||||
targetPort: targetPort,
|
if (firstMapping) {
|
||||||
publishedPort: Number.parseInt(mapping.HostPort, 10),
|
const publishedPort = Number.parseInt(firstMapping.HostPort, 10);
|
||||||
protocol: protocol,
|
const portKey = `${targetPort}-${publishedPort}-${protocol}`;
|
||||||
});
|
if (!seenPorts.has(portKey)) {
|
||||||
});
|
seenPorts.add(portKey);
|
||||||
|
ports.push({
|
||||||
|
targetPort: targetPort,
|
||||||
|
publishedPort: publishedPort,
|
||||||
|
protocol: protocol,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ports.filter(
|
return ports.filter(
|
||||||
@@ -392,6 +368,28 @@ export const readPorts = async (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkPortInUse = async (
|
||||||
|
port: number,
|
||||||
|
serverId?: string,
|
||||||
|
): Promise<{ isInUse: boolean; conflictingContainer?: string }> => {
|
||||||
|
try {
|
||||||
|
const command = `docker ps -a --format '{{.Names}}' | grep -v '^dokploy-traefik$' | while read name; do docker port "$name" 2>/dev/null | grep -q ':${port}' && echo "$name" && break; done || true`;
|
||||||
|
const { stdout } = serverId
|
||||||
|
? await execAsyncRemote(serverId, command)
|
||||||
|
: await execAsync(command);
|
||||||
|
|
||||||
|
const container = stdout.trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
isInUse: !!container,
|
||||||
|
conflictingContainer: container || undefined,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking port availability:", error);
|
||||||
|
return { isInUse: false };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const writeTraefikSetup = async (input: TraefikOptions) => {
|
export const writeTraefikSetup = async (input: TraefikOptions) => {
|
||||||
const resourceType = await getDockerResourceType(
|
const resourceType = await getDockerResourceType(
|
||||||
"dokploy-traefik",
|
"dokploy-traefik",
|
||||||
|
|||||||
@@ -12,13 +12,69 @@ export const findVolumeBackupById = async (volumeBackupId: string) => {
|
|||||||
const volumeBackup = await db.query.volumeBackups.findFirst({
|
const volumeBackup = await db.query.volumeBackups.findFirst({
|
||||||
where: eq(volumeBackups.volumeBackupId, volumeBackupId),
|
where: eq(volumeBackups.volumeBackupId, volumeBackupId),
|
||||||
with: {
|
with: {
|
||||||
application: true,
|
application: {
|
||||||
postgres: true,
|
with: {
|
||||||
mysql: true,
|
environment: {
|
||||||
mariadb: true,
|
with: {
|
||||||
mongo: true,
|
project: true,
|
||||||
redis: true,
|
},
|
||||||
compose: true,
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
postgres: {
|
||||||
|
with: {
|
||||||
|
environment: {
|
||||||
|
with: {
|
||||||
|
project: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mysql: {
|
||||||
|
with: {
|
||||||
|
environment: {
|
||||||
|
with: {
|
||||||
|
project: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mariadb: {
|
||||||
|
with: {
|
||||||
|
environment: {
|
||||||
|
with: {
|
||||||
|
project: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mongo: {
|
||||||
|
with: {
|
||||||
|
environment: {
|
||||||
|
with: {
|
||||||
|
project: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
redis: {
|
||||||
|
with: {
|
||||||
|
environment: {
|
||||||
|
with: {
|
||||||
|
project: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
compose: {
|
||||||
|
with: {
|
||||||
|
environment: {
|
||||||
|
with: {
|
||||||
|
project: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
destination: true,
|
destination: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export const serverSetup = async (
|
|||||||
export const defaultCommand = (isBuildServer = false) => {
|
export const defaultCommand = (isBuildServer = false) => {
|
||||||
const bashCommand = `
|
const bashCommand = `
|
||||||
set -e;
|
set -e;
|
||||||
DOCKER_VERSION=27.0.3
|
DOCKER_VERSION=28.5.0
|
||||||
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||||
SYS_ARCH=$(uname -m)
|
SYS_ARCH=$(uname -m)
|
||||||
CURRENT_USER=$USER
|
CURRENT_USER=$USER
|
||||||
@@ -524,7 +524,7 @@ if ! [ -x "$(command -v docker)" ]; then
|
|||||||
echo "Please install Docker manually."
|
echo "Please install Docker manually."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
curl -s https://releases.rancher.com/install-docker/$DOCKER_VERSION.sh | sh 2>&1
|
|
||||||
if ! [ -x "$(command -v docker)" ]; then
|
if ! [ -x "$(command -v docker)" ]; then
|
||||||
curl -s https://get.docker.com | sh -s -- --version $DOCKER_VERSION 2>&1
|
curl -s https://get.docker.com | sh -s -- --version $DOCKER_VERSION 2>&1
|
||||||
if ! [ -x "$(command -v docker)" ]; then
|
if ! [ -x "$(command -v docker)" ]; then
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
import { findAdmin } from "@dokploy/server/services/admin";
|
import { findOwner } from "@dokploy/server/services/admin";
|
||||||
import { updateUser } from "@dokploy/server/services/user";
|
import { updateUser } from "@dokploy/server/services/user";
|
||||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||||
import { execAsync } from "../process/execAsync";
|
import { execAsync } from "../process/execAsync";
|
||||||
@@ -29,9 +29,9 @@ export const startLogCleanup = async (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const admin = await findAdmin();
|
const owner = await findOwner();
|
||||||
if (admin) {
|
if (owner) {
|
||||||
await updateUser(admin.user.id, {
|
await updateUser(owner.user.id, {
|
||||||
logCleanupCron: cronExpression,
|
logCleanupCron: cronExpression,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -51,9 +51,9 @@ export const stopLogCleanup = async (): Promise<boolean> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update database
|
// Update database
|
||||||
const admin = await findAdmin();
|
const owner = await findOwner();
|
||||||
if (admin) {
|
if (owner) {
|
||||||
await updateUser(admin.user.id, {
|
await updateUser(owner.user.id, {
|
||||||
logCleanupCron: null,
|
logCleanupCron: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -69,8 +69,8 @@ export const getLogCleanupStatus = async (): Promise<{
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
cronExpression: string | null;
|
cronExpression: string | null;
|
||||||
}> => {
|
}> => {
|
||||||
const admin = await findAdmin();
|
const owner = await findOwner();
|
||||||
const cronExpression = admin?.user.logCleanupCron ?? null;
|
const cronExpression = owner?.user.logCleanupCron ?? null;
|
||||||
return {
|
return {
|
||||||
enabled: cronExpression !== null,
|
enabled: cronExpression !== null,
|
||||||
cronExpression,
|
cronExpression,
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ import { eq } from "drizzle-orm";
|
|||||||
import { scheduleJob } from "node-schedule";
|
import { scheduleJob } from "node-schedule";
|
||||||
import { db } from "../../db/index";
|
import { db } from "../../db/index";
|
||||||
import { startLogCleanup } from "../access-log/handler";
|
import { startLogCleanup } from "../access-log/handler";
|
||||||
import {
|
import { cleanupAll } from "../docker/utils";
|
||||||
cleanUpDockerBuilder,
|
|
||||||
cleanUpSystemPrune,
|
|
||||||
cleanUpUnusedImages,
|
|
||||||
} from "../docker/utils";
|
|
||||||
import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup";
|
import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup";
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
import { getS3Credentials, scheduleBackup } from "./utils";
|
import { getS3Credentials, scheduleBackup } from "./utils";
|
||||||
@@ -34,9 +30,9 @@ export const initCronJobs = async () => {
|
|||||||
console.log(
|
console.log(
|
||||||
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
|
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
|
||||||
);
|
);
|
||||||
await cleanUpUnusedImages();
|
|
||||||
await cleanUpDockerBuilder();
|
await cleanupAll();
|
||||||
await cleanUpSystemPrune();
|
|
||||||
await sendDockerCleanupNotifications(admin.user.id);
|
await sendDockerCleanupNotifications(admin.user.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -50,9 +46,9 @@ export const initCronJobs = async () => {
|
|||||||
console.log(
|
console.log(
|
||||||
`SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${name}`,
|
`SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${name}`,
|
||||||
);
|
);
|
||||||
await cleanUpUnusedImages(serverId);
|
|
||||||
await cleanUpDockerBuilder(serverId);
|
await cleanupAll(serverId);
|
||||||
await cleanUpSystemPrune(serverId);
|
|
||||||
await sendDockerCleanupNotifications(
|
await sendDockerCleanupNotifications(
|
||||||
admin.user.id,
|
admin.user.id,
|
||||||
`Docker cleanup for Server ${name} (${serverId})`,
|
`Docker cleanup for Server ${name} (${serverId})`,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
createDeploymentBackup,
|
createDeploymentBackup,
|
||||||
updateDeploymentStatus,
|
updateDeploymentStatus,
|
||||||
} from "@dokploy/server/services/deployment";
|
} from "@dokploy/server/services/deployment";
|
||||||
import type { Mariadb } from "@dokploy/server/services/mariadb";
|
|
||||||
import { findEnvironmentById } from "@dokploy/server/services/environment";
|
import { findEnvironmentById } from "@dokploy/server/services/environment";
|
||||||
|
import type { Mariadb } from "@dokploy/server/services/mariadb";
|
||||||
import { findProjectById } from "@dokploy/server/services/project";
|
import { findProjectById } from "@dokploy/server/services/project";
|
||||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
createDeploymentBackup,
|
createDeploymentBackup,
|
||||||
updateDeploymentStatus,
|
updateDeploymentStatus,
|
||||||
} from "@dokploy/server/services/deployment";
|
} from "@dokploy/server/services/deployment";
|
||||||
import type { Mongo } from "@dokploy/server/services/mongo";
|
|
||||||
import { findEnvironmentById } from "@dokploy/server/services/environment";
|
import { findEnvironmentById } from "@dokploy/server/services/environment";
|
||||||
|
import type { Mongo } from "@dokploy/server/services/mongo";
|
||||||
import { findProjectById } from "@dokploy/server/services/project";
|
import { findProjectById } from "@dokploy/server/services/project";
|
||||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
createDeploymentBackup,
|
createDeploymentBackup,
|
||||||
updateDeploymentStatus,
|
updateDeploymentStatus,
|
||||||
} from "@dokploy/server/services/deployment";
|
} from "@dokploy/server/services/deployment";
|
||||||
import type { MySql } from "@dokploy/server/services/mysql";
|
|
||||||
import { findEnvironmentById } from "@dokploy/server/services/environment";
|
import { findEnvironmentById } from "@dokploy/server/services/environment";
|
||||||
|
import type { MySql } from "@dokploy/server/services/mysql";
|
||||||
import { findProjectById } from "@dokploy/server/services/project";
|
import { findProjectById } from "@dokploy/server/services/project";
|
||||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
createDeploymentBackup,
|
createDeploymentBackup,
|
||||||
updateDeploymentStatus,
|
updateDeploymentStatus,
|
||||||
} from "@dokploy/server/services/deployment";
|
} from "@dokploy/server/services/deployment";
|
||||||
import type { Postgres } from "@dokploy/server/services/postgres";
|
|
||||||
import { findEnvironmentById } from "@dokploy/server/services/environment";
|
import { findEnvironmentById } from "@dokploy/server/services/environment";
|
||||||
|
import type { Postgres } from "@dokploy/server/services/postgres";
|
||||||
import { findProjectById } from "@dokploy/server/services/project";
|
import { findProjectById } from "@dokploy/server/services/project";
|
||||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
|
|||||||
@@ -62,16 +62,16 @@ export const getS3Credentials = (destination: Destination) => {
|
|||||||
const { accessKey, secretAccessKey, region, endpoint, provider } =
|
const { accessKey, secretAccessKey, region, endpoint, provider } =
|
||||||
destination;
|
destination;
|
||||||
const rcloneFlags = [
|
const rcloneFlags = [
|
||||||
`--s3-access-key-id=${accessKey}`,
|
`--s3-access-key-id="${accessKey}"`,
|
||||||
`--s3-secret-access-key=${secretAccessKey}`,
|
`--s3-secret-access-key="${secretAccessKey}"`,
|
||||||
`--s3-region=${region}`,
|
`--s3-region="${region}"`,
|
||||||
`--s3-endpoint=${endpoint}`,
|
`--s3-endpoint="${endpoint}"`,
|
||||||
"--s3-no-check-bucket",
|
"--s3-no-check-bucket",
|
||||||
"--s3-force-path-style",
|
"--s3-force-path-style",
|
||||||
];
|
];
|
||||||
|
|
||||||
if (provider) {
|
if (provider) {
|
||||||
rcloneFlags.unshift(`--s3-provider=${provider}`);
|
rcloneFlags.unshift(`--s3-provider="${provider}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return rcloneFlags;
|
return rcloneFlags;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
getDockerContextPath,
|
getDockerContextPath,
|
||||||
} from "../filesystem/directory";
|
} from "../filesystem/directory";
|
||||||
import type { ApplicationNested } from ".";
|
import type { ApplicationNested } from ".";
|
||||||
import { createEnvFileCommand } from "./utils";
|
|
||||||
|
|
||||||
export const getDockerCommand = (application: ApplicationNested) => {
|
export const getDockerCommand = (application: ApplicationNested) => {
|
||||||
const {
|
const {
|
||||||
@@ -68,21 +67,7 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
|||||||
commandArgs.push("--secret", `type=env,id=${key}`);
|
commandArgs.push("--secret", `type=env,id=${key}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
const command = `
|
||||||
Do not generate an environment file when publishDirectory is specified,
|
|
||||||
as it could be publicly exposed.
|
|
||||||
*/
|
|
||||||
let command = "";
|
|
||||||
if (!publishDirectory) {
|
|
||||||
command += createEnvFileCommand(
|
|
||||||
dockerFilePath,
|
|
||||||
env,
|
|
||||||
application.environment.project.env,
|
|
||||||
application.environment.env,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
command += `
|
|
||||||
echo "Building ${appName}" ;
|
echo "Building ${appName}" ;
|
||||||
cd ${dockerContextPath} || {
|
cd ${dockerContextPath} || {
|
||||||
echo "❌ The path ${dockerContextPath} does not exist" ;
|
echo "❌ The path ${dockerContextPath} does not exist" ;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { InferResultType } from "@dokploy/server/types/with";
|
import type { InferResultType } from "@dokploy/server/types/with";
|
||||||
import type { CreateServiceOptions } from "dockerode";
|
import type { CreateServiceOptions } from "dockerode";
|
||||||
import { uploadImageRemoteCommand } from "../cluster/upload";
|
import { getRegistryTag, uploadImageRemoteCommand } from "../cluster/upload";
|
||||||
import {
|
import {
|
||||||
calculateResources,
|
calculateResources,
|
||||||
generateBindMounts,
|
generateBindMounts,
|
||||||
@@ -30,39 +30,45 @@ export type ApplicationNested = InferResultType<
|
|||||||
ports: true;
|
ports: true;
|
||||||
registry: true;
|
registry: true;
|
||||||
buildRegistry: true;
|
buildRegistry: true;
|
||||||
|
rollbackRegistry: true;
|
||||||
|
deployments: true;
|
||||||
environment: { with: { project: true } };
|
environment: { with: { project: true } };
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const getBuildCommand = (application: ApplicationNested) => {
|
export const getBuildCommand = async (application: ApplicationNested) => {
|
||||||
let command = "";
|
let command = "";
|
||||||
const { buildType } = application;
|
|
||||||
|
|
||||||
if (application.sourceType === "docker") {
|
if (application.sourceType !== "docker") {
|
||||||
return "";
|
const { buildType } = application;
|
||||||
|
switch (buildType) {
|
||||||
|
case "nixpacks":
|
||||||
|
command = getNixpacksCommand(application);
|
||||||
|
break;
|
||||||
|
case "heroku_buildpacks":
|
||||||
|
command = getHerokuCommand(application);
|
||||||
|
break;
|
||||||
|
case "paketo_buildpacks":
|
||||||
|
command = getPaketoCommand(application);
|
||||||
|
break;
|
||||||
|
case "static":
|
||||||
|
command = getStaticCommand(application);
|
||||||
|
break;
|
||||||
|
case "dockerfile":
|
||||||
|
command = getDockerCommand(application);
|
||||||
|
break;
|
||||||
|
case "railpack":
|
||||||
|
command = getRailpackCommand(application);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
switch (buildType) {
|
|
||||||
case "nixpacks":
|
if (
|
||||||
command = getNixpacksCommand(application);
|
application.registry ||
|
||||||
break;
|
application.buildRegistry ||
|
||||||
case "heroku_buildpacks":
|
application.rollbackRegistry
|
||||||
command = getHerokuCommand(application);
|
) {
|
||||||
break;
|
command += await uploadImageRemoteCommand(application);
|
||||||
case "paketo_buildpacks":
|
|
||||||
command = getPaketoCommand(application);
|
|
||||||
break;
|
|
||||||
case "static":
|
|
||||||
command = getStaticCommand(application);
|
|
||||||
break;
|
|
||||||
case "dockerfile":
|
|
||||||
command = getDockerCommand(application);
|
|
||||||
break;
|
|
||||||
case "railpack":
|
|
||||||
command = getRailpackCommand(application);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (application.registry || application.buildRegistry) {
|
|
||||||
command += uploadImageRemoteCommand(application);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return command;
|
return command;
|
||||||
@@ -188,17 +194,11 @@ const getImageName = (application: ApplicationNested) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (registry) {
|
if (registry) {
|
||||||
const { registryUrl, imagePrefix, username } = registry;
|
const registryTag = getRegistryTag(registry, imageName);
|
||||||
const registryTag = imagePrefix
|
|
||||||
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
|
||||||
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
|
||||||
return registryTag;
|
return registryTag;
|
||||||
}
|
}
|
||||||
if (buildRegistry) {
|
if (buildRegistry) {
|
||||||
const { registryUrl, imagePrefix, username } = buildRegistry;
|
const registryTag = getRegistryTag(buildRegistry, imageName);
|
||||||
const registryTag = imagePrefix
|
|
||||||
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
|
||||||
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
|
||||||
return registryTag;
|
return registryTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
|
import { findAllDeploymentsByApplicationId } from "@dokploy/server/services/deployment";
|
||||||
import type { Registry } from "@dokploy/server/services/registry";
|
import type { Registry } from "@dokploy/server/services/registry";
|
||||||
|
import { createRollback } from "@dokploy/server/services/rollbacks";
|
||||||
import type { ApplicationNested } from "../builders";
|
import type { ApplicationNested } from "../builders";
|
||||||
|
|
||||||
export const uploadImageRemoteCommand = (application: ApplicationNested) => {
|
export const uploadImageRemoteCommand = async (
|
||||||
|
application: ApplicationNested,
|
||||||
|
) => {
|
||||||
const registry = application.registry;
|
const registry = application.registry;
|
||||||
const buildRegistry = application.buildRegistry;
|
const buildRegistry = application.buildRegistry;
|
||||||
|
const rollbackRegistry = application.rollbackRegistry;
|
||||||
|
|
||||||
if (!registry && !buildRegistry) {
|
if (!registry && !buildRegistry && !rollbackRegistry) {
|
||||||
throw new Error("No registry found");
|
throw new Error("No registry found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { appName } = application;
|
const { appName } = application;
|
||||||
const imageName = `${appName}:latest`;
|
const imageName =
|
||||||
|
application.sourceType === "docker"
|
||||||
|
? application.dockerImage || ""
|
||||||
|
: `${appName}:latest`;
|
||||||
|
|
||||||
const commands: string[] = [];
|
const commands: string[] = [];
|
||||||
if (registry) {
|
if (registry) {
|
||||||
@@ -35,16 +43,38 @@ export const uploadImageRemoteCommand = (application: ApplicationNested) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rollbackRegistry && application.rollbackActive) {
|
||||||
|
const deployment = await findAllDeploymentsByApplicationId(
|
||||||
|
application.applicationId,
|
||||||
|
);
|
||||||
|
if (!deployment || !deployment[0]) {
|
||||||
|
throw new Error("Deployment not found");
|
||||||
|
}
|
||||||
|
const deploymentId = deployment[0].deploymentId;
|
||||||
|
const rollback = await createRollback({
|
||||||
|
appName: appName,
|
||||||
|
deploymentId: deploymentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rollbackRegistryTag = getRegistryTag(
|
||||||
|
rollbackRegistry,
|
||||||
|
rollback?.image || "",
|
||||||
|
);
|
||||||
|
if (rollbackRegistryTag) {
|
||||||
|
commands.push(`echo "🔄 [Enabled Rollback Registry]"`);
|
||||||
|
commands.push(
|
||||||
|
getRegistryCommands(rollbackRegistry, imageName, rollbackRegistryTag),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return commands.join("\n");
|
return commands.join("\n");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const getRegistryTag = (registry: Registry | null, imageName: string) => {
|
export const getRegistryTag = (registry: Registry, imageName: string) => {
|
||||||
if (!registry) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const { registryUrl, imagePrefix, username } = registry;
|
const { registryUrl, imagePrefix, username } = registry;
|
||||||
return imagePrefix
|
return imagePrefix
|
||||||
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
||||||
|
|||||||
@@ -144,81 +144,116 @@ export const getContainerByName = (name: string): Promise<ContainerInfo> => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
export const cleanUpUnusedImages = async (serverId?: string) => {
|
|
||||||
try {
|
|
||||||
const command = "docker image prune --force";
|
|
||||||
if (serverId) {
|
|
||||||
await execAsyncRemote(serverId, command);
|
|
||||||
} else {
|
|
||||||
await execAsync(command);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cleanStoppedContainers = async (serverId?: string) => {
|
/**
|
||||||
|
* Docker commands passed through this method are held during Docker's build or pull process.
|
||||||
|
*
|
||||||
|
* https://github.com/Dokploy/dokploy/pull/3064
|
||||||
|
* https://github.com/fir4tozden
|
||||||
|
*/
|
||||||
|
export const dockerSafeExec = (exec: string) => `CHECK_INTERVAL=10
|
||||||
|
|
||||||
|
echo "Preparing for execution..."
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
PROCESSES=$(ps aux | grep -E "docker build|docker pull" | grep -v grep)
|
||||||
|
|
||||||
|
if [ -z "$PROCESSES" ]; then
|
||||||
|
echo "Docker is idle. Starting execution..."
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "Docker is busy. Will check again in $CHECK_INTERVAL seconds..."
|
||||||
|
sleep $CHECK_INTERVAL
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
${exec}
|
||||||
|
|
||||||
|
echo "Execution completed."`;
|
||||||
|
|
||||||
|
export const cleanupContainers = async (serverId?: string) => {
|
||||||
try {
|
try {
|
||||||
const command = "docker container prune --force";
|
const command = "docker container prune --force";
|
||||||
|
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
await execAsyncRemote(serverId, command);
|
await execAsyncRemote(serverId, dockerSafeExec(command));
|
||||||
} else {
|
} else {
|
||||||
await execAsync(command);
|
await execAsync(dockerSafeExec(command));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanUpUnusedVolumes = async (serverId?: string) => {
|
export const cleanupImages = async (serverId?: string) => {
|
||||||
try {
|
try {
|
||||||
const command = "docker volume prune --force";
|
const command = "docker image prune --all --force";
|
||||||
|
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
await execAsyncRemote(serverId, command);
|
await execAsyncRemote(serverId, dockerSafeExec(command));
|
||||||
|
} else await execAsync(dockerSafeExec(command));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cleanupVolumes = async (serverId?: string) => {
|
||||||
|
try {
|
||||||
|
const command = "docker volume prune --all --force";
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
await execAsyncRemote(serverId, dockerSafeExec(command));
|
||||||
} else {
|
} else {
|
||||||
await execAsync(command);
|
await execAsync(dockerSafeExec(command));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanUpInactiveContainers = async () => {
|
export const cleanupBuilders = async (serverId?: string) => {
|
||||||
try {
|
try {
|
||||||
const containers = await docker.listContainers({ all: true });
|
const command = "docker builder prune --all --force";
|
||||||
const inactiveContainers = containers.filter(
|
|
||||||
(container) => container.State !== "running",
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const container of inactiveContainers) {
|
if (serverId) {
|
||||||
await docker.getContainer(container.Id).remove({ force: true });
|
await execAsyncRemote(serverId, dockerSafeExec(command));
|
||||||
console.log(`Cleaning up inactive container: ${container.Id}`);
|
} else {
|
||||||
|
await execAsync(dockerSafeExec(command));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error cleaning up inactive containers:", error);
|
console.error(error);
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanUpDockerBuilder = async (serverId?: string) => {
|
export const cleanupSystem = async (serverId?: string) => {
|
||||||
const command = "docker builder prune --all --force";
|
try {
|
||||||
if (serverId) {
|
const command = "docker system prune --all --force";
|
||||||
await execAsyncRemote(serverId, command);
|
|
||||||
} else {
|
if (serverId) {
|
||||||
await execAsync(command);
|
await execAsyncRemote(serverId, dockerSafeExec(command));
|
||||||
|
} else {
|
||||||
|
await execAsync(dockerSafeExec(command));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanUpSystemPrune = async (serverId?: string) => {
|
export const cleanupAll = async (serverId?: string) => {
|
||||||
const command = "docker system prune --force --volumes";
|
await cleanupContainers(serverId);
|
||||||
if (serverId) {
|
await cleanupImages(serverId);
|
||||||
await execAsyncRemote(serverId, command);
|
await cleanupBuilders(serverId);
|
||||||
} else {
|
await cleanupSystem(serverId);
|
||||||
await execAsync(command);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const startService = async (appName: string) => {
|
export const startService = async (appName: string) => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { renderAsync } from "@react-email/components";
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
|
sendCustomNotification,
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
sendGotifyNotification,
|
sendGotifyNotification,
|
||||||
@@ -45,12 +46,13 @@ export const sendBuildErrorNotifications = async ({
|
|||||||
slack: true,
|
slack: true,
|
||||||
gotify: true,
|
gotify: true,
|
||||||
ntfy: true,
|
ntfy: true,
|
||||||
|
custom: true,
|
||||||
lark: true,
|
lark: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const notification of notificationList) {
|
for (const notification of notificationList) {
|
||||||
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
|
||||||
notification;
|
notification;
|
||||||
try {
|
try {
|
||||||
if (email) {
|
if (email) {
|
||||||
@@ -220,6 +222,22 @@ export const sendBuildErrorNotifications = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (custom) {
|
||||||
|
await sendCustomNotification(custom, {
|
||||||
|
title: "Build Error",
|
||||||
|
message: "Build failed with errors",
|
||||||
|
projectName,
|
||||||
|
applicationName,
|
||||||
|
applicationType,
|
||||||
|
errorMessage,
|
||||||
|
buildLink,
|
||||||
|
timestamp: date.toISOString(),
|
||||||
|
date: date.toLocaleString(),
|
||||||
|
status: "error",
|
||||||
|
type: "build",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (lark) {
|
if (lark) {
|
||||||
const limitCharacter = 800;
|
const limitCharacter = 800;
|
||||||
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
|
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user