mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-20 22:55:22 +02:00
Compare commits
66 Commits
dosu/doc-u
...
2702-add-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f1bf2b14e | ||
|
|
2683ac2a1b | ||
|
|
4e11334940 | ||
|
|
82893598e0 | ||
|
|
86905fc5bf | ||
|
|
c7814bb752 | ||
|
|
c0d6eac35d | ||
|
|
6dfa762934 | ||
|
|
0e3bc444b9 | ||
|
|
fb7b7cff66 | ||
|
|
5e999f1c3c | ||
|
|
9e52b722f0 | ||
|
|
70418dd09b | ||
|
|
df95766807 | ||
|
|
e5aae15310 | ||
|
|
964773b44c | ||
|
|
7224436610 | ||
|
|
d6885c32ea | ||
|
|
4da3c468eb | ||
|
|
38a711776b | ||
|
|
4030049ee8 | ||
|
|
06b18aca08 | ||
|
|
86ba597d67 | ||
|
|
5978c4135e | ||
|
|
e9202bfb15 | ||
|
|
365e055005 | ||
|
|
9b108480a8 | ||
|
|
450d591c1a | ||
|
|
d90722a174 | ||
|
|
f9de42610c | ||
|
|
780406f9ef | ||
|
|
f49988498f | ||
|
|
565bc16f24 | ||
|
|
c7b5e73d1c | ||
|
|
8053ee7724 | ||
|
|
c4aca74aef | ||
|
|
dab13a52d6 | ||
|
|
4a7e9a200e | ||
|
|
f83ab2923d | ||
|
|
9a1bee5287 | ||
|
|
6d17f62942 | ||
|
|
815b8136fa | ||
|
|
290a03ccfb | ||
|
|
63aa60f7e2 | ||
|
|
fe9b0ebcea | ||
|
|
8ccdb66ced | ||
|
|
e38f07d286 | ||
|
|
035d39e3b7 | ||
|
|
82a908a865 | ||
|
|
4bbb2ece49 | ||
|
|
8ee374dc6b | ||
|
|
e575e50979 | ||
|
|
efedec70d6 | ||
|
|
8d11fb4ee8 | ||
|
|
b7f7027280 | ||
|
|
5d078f1d9f | ||
|
|
ac27aa1bba | ||
|
|
6a79ce8ff1 | ||
|
|
bf226f1af1 | ||
|
|
6b117551ae | ||
|
|
8c1153370c | ||
|
|
21fa21e9c0 | ||
|
|
91a385c302 | ||
|
|
9627af9cda | ||
|
|
90bd276ad4 | ||
|
|
84d311802f |
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -7,7 +7,7 @@ Please describe in a short paragraph what this PR is about.
|
||||
Before submitting this PR, please make sure that:
|
||||
|
||||
- [ ] You created a dedicated branch based on the `canary` branch.
|
||||
- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/MkinG2k0/dokploy/blob/canary/CONTRIBUTING.md#pull-request
|
||||
- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
|
||||
- [ ] You have tested this PR in your local instance. If you have not tested it yet, please do so before submitting. This helps avoid wasting maintainers' time reviewing code that has not been verified by you.
|
||||
|
||||
## Issues related (if applicable)
|
||||
|
||||
@@ -32,6 +32,7 @@ describe("Host rule format regression tests", () => {
|
||||
previewDeploymentId: "",
|
||||
internalPath: "/",
|
||||
stripPath: false,
|
||||
customEntrypoint: null,
|
||||
};
|
||||
|
||||
describe("Host rule format validation", () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ describe("createDomainLabels", () => {
|
||||
const baseDomain: Domain = {
|
||||
host: "example.com",
|
||||
port: 8080,
|
||||
customEntrypoint: null,
|
||||
https: false,
|
||||
uniqueConfigKey: 1,
|
||||
customCertResolver: null,
|
||||
@@ -240,4 +241,134 @@ describe("createDomainLabels", () => {
|
||||
"traefik.http.routers.test-app-1-websecure.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("should create basic labels for custom entrypoint", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
{ ...baseDomain, customEntrypoint: "custom" },
|
||||
"custom",
|
||||
);
|
||||
expect(labels).toEqual([
|
||||
"traefik.http.routers.test-app-1-custom.rule=Host(`example.com`)",
|
||||
"traefik.http.routers.test-app-1-custom.entrypoints=custom",
|
||||
"traefik.http.services.test-app-1-custom.loadbalancer.server.port=8080",
|
||||
"traefik.http.routers.test-app-1-custom.service=test-app-1-custom",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should create https labels for custom entrypoint", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
{
|
||||
...baseDomain,
|
||||
https: true,
|
||||
customEntrypoint: "custom",
|
||||
certificateType: "letsencrypt",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
expect(labels).toEqual([
|
||||
"traefik.http.routers.test-app-1-custom.rule=Host(`example.com`)",
|
||||
"traefik.http.routers.test-app-1-custom.entrypoints=custom",
|
||||
"traefik.http.services.test-app-1-custom.loadbalancer.server.port=8080",
|
||||
"traefik.http.routers.test-app-1-custom.service=test-app-1-custom",
|
||||
"traefik.http.routers.test-app-1-custom.tls.certresolver=letsencrypt",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should add stripPath middleware for custom entrypoint", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
{
|
||||
...baseDomain,
|
||||
customEntrypoint: "custom",
|
||||
path: "/api",
|
||||
stripPath: true,
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(labels).toContain(
|
||||
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
|
||||
);
|
||||
expect(labels).toContain(
|
||||
"traefik.http.routers.test-app-1-custom.middlewares=stripprefix-test-app-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("should add internalPath middleware for custom entrypoint", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
{
|
||||
...baseDomain,
|
||||
customEntrypoint: "custom",
|
||||
internalPath: "/hello",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(labels).toContain(
|
||||
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
|
||||
);
|
||||
expect(labels).toContain(
|
||||
"traefik.http.routers.test-app-1-custom.middlewares=addprefix-test-app-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("should add path prefix in rule for custom entrypoint", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
{
|
||||
...baseDomain,
|
||||
customEntrypoint: "custom",
|
||||
path: "/api",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(labels).toContain(
|
||||
"traefik.http.routers.test-app-1-custom.rule=Host(`example.com`) && PathPrefix(`/api`)",
|
||||
);
|
||||
});
|
||||
|
||||
it("should combine all middlewares for custom entrypoint", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
{
|
||||
...baseDomain,
|
||||
customEntrypoint: "custom",
|
||||
path: "/api",
|
||||
stripPath: true,
|
||||
internalPath: "/hello",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(labels).toContain(
|
||||
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
|
||||
);
|
||||
expect(labels).toContain(
|
||||
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
|
||||
);
|
||||
expect(labels).toContain(
|
||||
"traefik.http.routers.test-app-1-custom.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not add redirect-to-https for custom entrypoint even with https", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
{
|
||||
...baseDomain,
|
||||
customEntrypoint: "custom",
|
||||
https: true,
|
||||
certificateType: "letsencrypt",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
const middlewareLabel = labels.find((l) => l.includes(".middlewares="));
|
||||
// Should not contain redirect-to-https since there's only one router
|
||||
expect(middlewareLabel).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -292,7 +292,7 @@ networks:
|
||||
dokploy-network:
|
||||
`;
|
||||
|
||||
test("It shoudn't add suffix to dokploy-network", () => {
|
||||
test("It shouldn't add suffix to dokploy-network", () => {
|
||||
const composeData = parse(composeFile7) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -195,7 +195,7 @@ services:
|
||||
- dokploy-network
|
||||
`;
|
||||
|
||||
test("It shoudn't add suffix to dokploy-network in services", () => {
|
||||
test("It shouldn't add suffix to dokploy-network in services", () => {
|
||||
const composeData = parse(composeFile7) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
@@ -241,10 +241,10 @@ services:
|
||||
dokploy-network:
|
||||
aliases:
|
||||
- apid
|
||||
|
||||
|
||||
`;
|
||||
|
||||
test("It shoudn't add suffix to dokploy-network in services multiples cases", () => {
|
||||
test("It shouldn't add suffix to dokploy-network in services multiples cases", () => {
|
||||
const composeData = parse(composeFile8) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getEnviromentVariablesObject } from "@dokploy/server/index";
|
||||
import { getEnvironmentVariablesObject } from "@dokploy/server/index";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const projectEnv = `
|
||||
@@ -15,7 +15,7 @@ DATABASE_NAME=dev_database
|
||||
SECRET_KEY=env-secret-123
|
||||
`;
|
||||
|
||||
describe("getEnviromentVariablesObject with environment variables (Stack compose)", () => {
|
||||
describe("getEnvironmentVariablesObject with environment variables (Stack compose)", () => {
|
||||
it("resolves environment variables correctly for Stack compose", () => {
|
||||
const serviceEnv = `
|
||||
FOO=\${{environment.NODE_ENV}}
|
||||
@@ -23,7 +23,7 @@ BAR=\${{environment.API_URL}}
|
||||
BAZ=test
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
const result = getEnvironmentVariablesObject(
|
||||
serviceEnv,
|
||||
projectEnv,
|
||||
environmentEnv,
|
||||
@@ -45,7 +45,7 @@ DATABASE_URL=\${{project.DATABASE_URL}}
|
||||
SERVICE_PORT=4000
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
const result = getEnvironmentVariablesObject(
|
||||
serviceEnv,
|
||||
projectEnv,
|
||||
environmentEnv,
|
||||
@@ -72,7 +72,7 @@ PASSWORD=secret123
|
||||
DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(serviceEnv, "", multiRefEnv);
|
||||
const result = getEnvironmentVariablesObject(serviceEnv, "", multiRefEnv);
|
||||
|
||||
expect(result).toEqual({
|
||||
DATABASE_URL: "postgresql://postgres:secret123@localhost:5432/mydb",
|
||||
@@ -85,7 +85,7 @@ UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}}
|
||||
`;
|
||||
|
||||
expect(() =>
|
||||
getEnviromentVariablesObject(serviceWithUndefined, "", environmentEnv),
|
||||
getEnvironmentVariablesObject(serviceWithUndefined, "", environmentEnv),
|
||||
).toThrow("Invalid environment variable: environment.UNDEFINED_VAR");
|
||||
});
|
||||
|
||||
@@ -95,7 +95,7 @@ NODE_ENV=production
|
||||
API_URL=\${{environment.API_URL}}
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
const result = getEnvironmentVariablesObject(
|
||||
serviceOverrideEnv,
|
||||
"",
|
||||
environmentEnv,
|
||||
@@ -115,7 +115,7 @@ SERVICE_NAME=my-service
|
||||
COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}}
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
const result = getEnvironmentVariablesObject(
|
||||
complexServiceEnv,
|
||||
projectEnv,
|
||||
environmentEnv,
|
||||
@@ -150,7 +150,7 @@ ENV_VAR=\${{environment.API_URL}}
|
||||
DB_NAME=\${{environment.DATABASE_NAME}}
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
const result = getEnvironmentVariablesObject(
|
||||
serviceWithConflicts,
|
||||
conflictingProjectEnv,
|
||||
conflictingEnvironmentEnv,
|
||||
@@ -170,7 +170,7 @@ SERVICE_VAR=test
|
||||
PROJECT_VAR=\${{project.ENVIRONMENT}}
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
const result = getEnvironmentVariablesObject(
|
||||
serviceWithEmpty,
|
||||
projectEnv,
|
||||
"",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
enterpriseOnlyResources,
|
||||
statements,
|
||||
} from "@dokploy/server/lib/access-control";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const FREE_TIER_RESOURCES = [
|
||||
"organization",
|
||||
|
||||
@@ -137,6 +137,7 @@ const baseDomain: Domain = {
|
||||
https: false,
|
||||
path: null,
|
||||
port: null,
|
||||
customEntrypoint: null,
|
||||
serviceName: "",
|
||||
composeId: "",
|
||||
customCertResolver: null,
|
||||
@@ -276,6 +277,110 @@ test("CertificateType on websecure entrypoint", async () => {
|
||||
expect(router.tls?.certResolver).toBe("letsencrypt");
|
||||
});
|
||||
|
||||
test("Custom entrypoint on http domain", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{ ...baseDomain, https: false, customEntrypoint: "custom" },
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(router.entryPoints).toEqual(["custom"]);
|
||||
expect(router.middlewares).not.toContain("redirect-to-https");
|
||||
expect(router.tls).toBeUndefined();
|
||||
});
|
||||
|
||||
test("Custom entrypoint on https domain", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{
|
||||
...baseDomain,
|
||||
https: true,
|
||||
customEntrypoint: "custom",
|
||||
certificateType: "letsencrypt",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(router.entryPoints).toEqual(["custom"]);
|
||||
expect(router.middlewares).not.toContain("redirect-to-https");
|
||||
expect(router.tls?.certResolver).toBe("letsencrypt");
|
||||
});
|
||||
|
||||
test("Custom entrypoint with path includes PathPrefix in rule", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{ ...baseDomain, customEntrypoint: "custom", path: "/api" },
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(router.rule).toContain("PathPrefix(`/api`)");
|
||||
expect(router.entryPoints).toEqual(["custom"]);
|
||||
});
|
||||
|
||||
test("Custom entrypoint with stripPath adds stripprefix middleware", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{
|
||||
...baseDomain,
|
||||
customEntrypoint: "custom",
|
||||
path: "/api",
|
||||
stripPath: true,
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(router.middlewares).toContain("stripprefix--1");
|
||||
expect(router.entryPoints).toEqual(["custom"]);
|
||||
});
|
||||
|
||||
test("Custom entrypoint with internalPath adds addprefix middleware", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{
|
||||
...baseDomain,
|
||||
customEntrypoint: "custom",
|
||||
internalPath: "/hello",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(router.middlewares).toContain("addprefix--1");
|
||||
expect(router.entryPoints).toEqual(["custom"]);
|
||||
});
|
||||
|
||||
test("Custom entrypoint with https and custom cert resolver", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{
|
||||
...baseDomain,
|
||||
https: true,
|
||||
customEntrypoint: "custom",
|
||||
certificateType: "custom",
|
||||
customCertResolver: "myresolver",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(router.entryPoints).toEqual(["custom"]);
|
||||
expect(router.tls?.certResolver).toBe("myresolver");
|
||||
});
|
||||
|
||||
test("Custom entrypoint without https should not have tls", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{
|
||||
...baseDomain,
|
||||
https: false,
|
||||
customEntrypoint: "custom",
|
||||
certificateType: "letsencrypt",
|
||||
},
|
||||
"custom",
|
||||
);
|
||||
|
||||
expect(router.entryPoints).toEqual(["custom"]);
|
||||
expect(router.tls).toBeUndefined();
|
||||
});
|
||||
|
||||
/** IDN/Punycode */
|
||||
|
||||
test("Internationalized domain name is converted to punycode", async () => {
|
||||
|
||||
@@ -40,12 +40,12 @@ interface Props {
|
||||
type: "application" | "mariadb" | "mongo" | "mysql" | "postgres" | "redis";
|
||||
}
|
||||
|
||||
const AddRedirectchema = z.object({
|
||||
const AddRedirectSchema = z.object({
|
||||
replicas: z.number().min(1, "Replicas must be at least 1"),
|
||||
registryId: z.string().optional(),
|
||||
});
|
||||
|
||||
type AddCommand = z.infer<typeof AddRedirectchema>;
|
||||
type AddCommand = z.infer<typeof AddRedirectSchema>;
|
||||
|
||||
export const ShowClusterSettings = ({ id, type }: Props) => {
|
||||
const queryMap = {
|
||||
@@ -87,7 +87,7 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
|
||||
: {}),
|
||||
replicas: data?.replicas || 1,
|
||||
},
|
||||
resolver: zodResolver(AddRedirectchema),
|
||||
resolver: zodResolver(AddRedirectSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -37,13 +37,13 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const AddRedirectchema = z.object({
|
||||
const AddRedirectSchema = z.object({
|
||||
regex: z.string().min(1, "Regex required"),
|
||||
permanent: z.boolean().default(false),
|
||||
replacement: z.string().min(1, "Replacement required"),
|
||||
});
|
||||
|
||||
type AddRedirect = z.infer<typeof AddRedirectchema>;
|
||||
type AddRedirect = z.infer<typeof AddRedirectSchema>;
|
||||
|
||||
// Default presets
|
||||
const redirectPresets = [
|
||||
@@ -110,7 +110,7 @@ export const HandleRedirect = ({
|
||||
regex: "",
|
||||
replacement: "",
|
||||
},
|
||||
resolver: zodResolver(AddRedirectchema),
|
||||
resolver: zodResolver(AddRedirectSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -149,7 +149,7 @@ export const HandleRedirect = ({
|
||||
|
||||
const onDialogToggle = (open: boolean) => {
|
||||
setIsOpen(open);
|
||||
// commented for the moment because not reseting the form if accidentally closed the dialog can be considered as a feature instead of a bug
|
||||
// commented for the moment because not resetting the form if accidentally closed the dialog can be considered as a feature instead of a bug
|
||||
// setPresetSelected("");
|
||||
// form.reset();
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import copy from "copy-to-clipboard";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
} from "lucide-react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
|
||||
@@ -61,6 +61,8 @@ export const domain = z
|
||||
.min(1, { message: "Port must be at least 1" })
|
||||
.max(65535, { message: "Port must be 65535 or below" })
|
||||
.optional(),
|
||||
useCustomEntrypoint: z.boolean(),
|
||||
customEntrypoint: z.string().optional(),
|
||||
https: z.boolean().optional(),
|
||||
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||
customCertResolver: z.string().optional(),
|
||||
@@ -114,6 +116,14 @@ export const domain = z
|
||||
message: "Internal path must start with '/'",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.useCustomEntrypoint && !input.customEntrypoint) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["customEntrypoint"],
|
||||
message: "Custom entry point must be specified",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
type Domain = z.infer<typeof domain>;
|
||||
@@ -196,6 +206,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
internalPath: undefined,
|
||||
stripPath: false,
|
||||
port: undefined,
|
||||
useCustomEntrypoint: false,
|
||||
customEntrypoint: undefined,
|
||||
https: false,
|
||||
certificateType: undefined,
|
||||
customCertResolver: undefined,
|
||||
@@ -206,6 +218,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
});
|
||||
|
||||
const certificateType = form.watch("certificateType");
|
||||
const useCustomEntrypoint = form.watch("useCustomEntrypoint");
|
||||
const https = form.watch("https");
|
||||
const domainType = form.watch("domainType");
|
||||
const host = form.watch("host");
|
||||
@@ -220,6 +233,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
internalPath: data?.internalPath || undefined,
|
||||
stripPath: data?.stripPath || false,
|
||||
port: data?.port || undefined,
|
||||
useCustomEntrypoint: !!data.customEntrypoint,
|
||||
customEntrypoint: data.customEntrypoint || undefined,
|
||||
certificateType: data?.certificateType || undefined,
|
||||
customCertResolver: data?.customCertResolver || undefined,
|
||||
serviceName: data?.serviceName || undefined,
|
||||
@@ -234,6 +249,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
internalPath: undefined,
|
||||
stripPath: false,
|
||||
port: undefined,
|
||||
useCustomEntrypoint: false,
|
||||
customEntrypoint: undefined,
|
||||
https: false,
|
||||
certificateType: undefined,
|
||||
customCertResolver: undefined,
|
||||
@@ -635,6 +652,50 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useCustomEntrypoint"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Custom Entrypoint</FormLabel>
|
||||
<FormDescription>
|
||||
Use custom entrypoint for domina
|
||||
<br />
|
||||
"web" and/or "websecure" is used by default.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{useCustomEntrypoint && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="customEntrypoint"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Entrypoint Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter entrypoint name manually"
|
||||
{...field}
|
||||
className="w-full"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="https"
|
||||
|
||||
@@ -91,7 +91,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||
}, [option, services, containers]);
|
||||
|
||||
const isLoading = option === "native" ? containersLoading : servicesLoading;
|
||||
const containersLenght =
|
||||
const containersLength =
|
||||
option === "native" ? containers?.length : services?.length;
|
||||
|
||||
return (
|
||||
@@ -167,7 +167,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<SelectLabel>Containers ({containersLenght})</SelectLabel>
|
||||
<SelectLabel>Containers ({containersLength})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./show-patches";
|
||||
export * from "./patch-editor";
|
||||
export * from "./show-patches";
|
||||
|
||||
@@ -483,7 +483,7 @@ export const HandleVolumeBackups = ({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Choose the volume to backup, if you dont see the
|
||||
Choose the volume to backup. If you do not see the
|
||||
volume here, you can type the volume name manually
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
@@ -518,7 +518,7 @@ export const HandleVolumeBackups = ({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Choose the volume to backup, if you dont see the volume
|
||||
Choose the volume to backup. If you do not see the volume
|
||||
here, you can type the volume name manually
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
|
||||
@@ -77,7 +77,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
|
||||
}, [option, services, containers]);
|
||||
|
||||
const isLoading = option === "native" ? containersLoading : servicesLoading;
|
||||
const containersLenght =
|
||||
const containersLength =
|
||||
option === "native" ? containers?.length : services?.length;
|
||||
|
||||
return (
|
||||
@@ -152,7 +152,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<SelectLabel>Containers ({containersLenght})</SelectLabel>
|
||||
<SelectLabel>Containers ({containersLength})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -225,7 +225,7 @@ export const RestoreBackup = ({
|
||||
resolver: zodResolver(RestoreBackupSchema),
|
||||
});
|
||||
|
||||
const destionationId = form.watch("destinationId");
|
||||
const destinationId = form.watch("destinationId");
|
||||
const currentDatabaseType = form.watch("databaseType");
|
||||
const metadata = form.watch("metadata");
|
||||
|
||||
@@ -240,12 +240,12 @@ export const RestoreBackup = ({
|
||||
|
||||
const { data: files = [], isPending } = api.backup.listBackupFiles.useQuery(
|
||||
{
|
||||
destinationId: destionationId,
|
||||
destinationId: destinationId,
|
||||
search: debouncedSearchTerm,
|
||||
serverId: serverId ?? "",
|
||||
},
|
||||
{
|
||||
enabled: isOpen && !!destionationId,
|
||||
enabled: isOpen && !!destinationId,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import type { inferRouterOutputs } from "@trpc/server";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, ListTodo, Loader2, XCircle } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
||||
@@ -103,7 +103,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
||||
>
|
||||
{" "}
|
||||
<div className="flex items-start gap-x-2">
|
||||
{/* Icon to expand the log item maybe implement a colapsible later */}
|
||||
{/* Icon to expand the log item maybe implement a collapsible later */}
|
||||
{/* <Square className="size-4 text-muted-foreground opacity-0 group-hover/logitem:opacity-100 transition-opacity" /> */}
|
||||
{tooltip(color, rawTimestamp)}
|
||||
{!noTimestamp && (
|
||||
|
||||
@@ -130,7 +130,7 @@ export const columns: ColumnDef<Container>[] = [
|
||||
</DockerTerminalModal>
|
||||
<RemoveContainerDialog
|
||||
containerId={container.containerId}
|
||||
serverId={container.serverId}
|
||||
serverId={container.serverId ?? undefined}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { api, type RouterOutputs } from "@/utils/api";
|
||||
import { columns } from "./colums";
|
||||
import { columns } from "./columns";
|
||||
export type Container = NonNullable<
|
||||
RouterOutputs["docker"]["getContainers"]
|
||||
>[0];
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
||||
|
||||
interface Props {
|
||||
acummulativeData: DockerStatsJSON["block"];
|
||||
accumulativeData: DockerStatsJSON["block"];
|
||||
}
|
||||
|
||||
export const DockerBlockChart = ({ acummulativeData }: Props) => {
|
||||
const transformedData = acummulativeData.map((item, index) => {
|
||||
export const DockerBlockChart = ({ accumulativeData }: Props) => {
|
||||
const transformedData = accumulativeData.map((item, index) => {
|
||||
return {
|
||||
time: item.time,
|
||||
name: `Point ${index + 1}`,
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
||||
|
||||
interface Props {
|
||||
acummulativeData: DockerStatsJSON["cpu"];
|
||||
accumulativeData: DockerStatsJSON["cpu"];
|
||||
}
|
||||
|
||||
export const DockerCpuChart = ({ acummulativeData }: Props) => {
|
||||
const transformedData = acummulativeData.map((item, index) => {
|
||||
export const DockerCpuChart = ({ accumulativeData }: Props) => {
|
||||
const transformedData = accumulativeData.map((item, index) => {
|
||||
return {
|
||||
name: `Point ${index + 1}`,
|
||||
time: item.time,
|
||||
|
||||
@@ -11,12 +11,12 @@ import {
|
||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
||||
|
||||
interface Props {
|
||||
acummulativeData: DockerStatsJSON["disk"];
|
||||
accumulativeData: DockerStatsJSON["disk"];
|
||||
diskTotal: number;
|
||||
}
|
||||
|
||||
export const DockerDiskChart = ({ acummulativeData, diskTotal }: Props) => {
|
||||
const transformedData = acummulativeData.map((item, index) => {
|
||||
export const DockerDiskChart = ({ accumulativeData, diskTotal }: Props) => {
|
||||
const transformedData = accumulativeData.map((item, index) => {
|
||||
return {
|
||||
time: item.time,
|
||||
name: `Point ${index + 1}`,
|
||||
|
||||
@@ -12,15 +12,15 @@ import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
||||
import { convertMemoryToBytes } from "./show-free-container-monitoring";
|
||||
|
||||
interface Props {
|
||||
acummulativeData: DockerStatsJSON["memory"];
|
||||
accumulativeData: DockerStatsJSON["memory"];
|
||||
memoryLimitGB: number;
|
||||
}
|
||||
|
||||
export const DockerMemoryChart = ({
|
||||
acummulativeData,
|
||||
accumulativeData,
|
||||
memoryLimitGB,
|
||||
}: Props) => {
|
||||
const transformedData = acummulativeData.map((item, index) => {
|
||||
const transformedData = accumulativeData.map((item, index) => {
|
||||
return {
|
||||
time: item.time,
|
||||
name: `Point ${index + 1}`,
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
||||
|
||||
interface Props {
|
||||
acummulativeData: DockerStatsJSON["network"];
|
||||
accumulativeData: DockerStatsJSON["network"];
|
||||
}
|
||||
|
||||
export const DockerNetworkChart = ({ acummulativeData }: Props) => {
|
||||
const transformedData = acummulativeData.map((item, index) => {
|
||||
export const DockerNetworkChart = ({ accumulativeData }: Props) => {
|
||||
const transformedData = accumulativeData.map((item, index) => {
|
||||
return {
|
||||
time: item.time,
|
||||
name: `Point ${index + 1}`,
|
||||
|
||||
@@ -124,7 +124,7 @@ export const ContainerFreeMonitoring = ({
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
);
|
||||
const [acummulativeData, setAcummulativeData] = useState<DockerStatsJSON>({
|
||||
const [accumulativeData, setAccumulativeData] = useState<DockerStatsJSON>({
|
||||
cpu: [],
|
||||
memory: [],
|
||||
block: [],
|
||||
@@ -136,7 +136,7 @@ export const ContainerFreeMonitoring = ({
|
||||
useEffect(() => {
|
||||
setCurrentData(defaultData);
|
||||
|
||||
setAcummulativeData({
|
||||
setAccumulativeData({
|
||||
cpu: [],
|
||||
memory: [],
|
||||
block: [],
|
||||
@@ -155,7 +155,7 @@ export const ContainerFreeMonitoring = ({
|
||||
network: data.network[data.network.length - 1] ?? currentData.network,
|
||||
disk: data.disk[data.disk.length - 1] ?? currentData.disk,
|
||||
});
|
||||
setAcummulativeData({
|
||||
setAccumulativeData({
|
||||
block: data?.block || [],
|
||||
cpu: data?.cpu || [],
|
||||
disk: data?.disk || [],
|
||||
@@ -184,7 +184,7 @@ export const ContainerFreeMonitoring = ({
|
||||
setCurrentData(data);
|
||||
|
||||
const MAX_DATA_POINTS = 300;
|
||||
setAcummulativeData((prevData) => ({
|
||||
setAccumulativeData((prevData) => ({
|
||||
cpu: [...prevData.cpu, data.cpu].slice(-MAX_DATA_POINTS),
|
||||
memory: [...prevData.memory, data.memory].slice(-MAX_DATA_POINTS),
|
||||
block: [...prevData.block, data.block].slice(-MAX_DATA_POINTS),
|
||||
@@ -228,7 +228,7 @@ export const ContainerFreeMonitoring = ({
|
||||
)}
|
||||
className="w-[100%]"
|
||||
/>
|
||||
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
|
||||
<DockerCpuChart accumulativeData={accumulativeData.cpu} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -252,7 +252,7 @@ export const ContainerFreeMonitoring = ({
|
||||
className="w-[100%]"
|
||||
/>
|
||||
<DockerMemoryChart
|
||||
acummulativeData={acummulativeData.memory}
|
||||
accumulativeData={accumulativeData.memory}
|
||||
memoryLimitGB={
|
||||
// @ts-ignore
|
||||
convertMemoryToBytes(currentData.memory.value.total) /
|
||||
@@ -277,7 +277,7 @@ export const ContainerFreeMonitoring = ({
|
||||
className="w-[100%]"
|
||||
/>
|
||||
<DockerDiskChart
|
||||
acummulativeData={acummulativeData.disk}
|
||||
accumulativeData={accumulativeData.disk}
|
||||
diskTotal={currentData.disk.value.diskTotal}
|
||||
/>
|
||||
</div>
|
||||
@@ -294,7 +294,7 @@ export const ContainerFreeMonitoring = ({
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{`Read: ${currentData.block.value.readMb} / Write: ${currentData.block.value.writeMb} `}
|
||||
</span>
|
||||
<DockerBlockChart acummulativeData={acummulativeData.block} />
|
||||
<DockerBlockChart accumulativeData={accumulativeData.block} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -307,7 +307,7 @@ export const ContainerFreeMonitoring = ({
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{`In MB: ${currentData.network.value.inputMb} / Out MB: ${currentData.network.value.outputMb} `}
|
||||
</span>
|
||||
<DockerNetworkChart acummulativeData={acummulativeData.network} />
|
||||
<DockerNetworkChart accumulativeData={accumulativeData.network} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { AlertCircle, Link, Loader2, ShieldCheck, Trash2 } from "lucide-react";
|
||||
import {
|
||||
AlertCircle,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Link,
|
||||
Loader2,
|
||||
ShieldCheck,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
@@ -12,13 +21,19 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { AddCertificate } from "./add-certificate";
|
||||
import { getCertificateChainInfo, getExpirationStatus } from "./utils";
|
||||
import {
|
||||
extractLeafCommonName,
|
||||
getCertificateChainExpirationDetails,
|
||||
getCertificateChainInfo,
|
||||
getExpirationStatus,
|
||||
} from "./utils";
|
||||
|
||||
export const ShowCertificates = () => {
|
||||
const { mutateAsync, isPending: isRemoving } =
|
||||
api.certificates.remove.useMutation();
|
||||
const { data, isPending, refetch } = api.certificates.all.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const [expandedChains, setExpandedChains] = useState<Set<string>>(new Set());
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -66,6 +81,30 @@ export const ShowCertificates = () => {
|
||||
const chainInfo = getCertificateChainInfo(
|
||||
certificate.certificateData,
|
||||
);
|
||||
const commonName = extractLeafCommonName(
|
||||
certificate.certificateData,
|
||||
);
|
||||
const chainDetails = chainInfo.isChain
|
||||
? getCertificateChainExpirationDetails(
|
||||
certificate.certificateData,
|
||||
)
|
||||
: null;
|
||||
const isExpanded = expandedChains.has(
|
||||
certificate.certificateId,
|
||||
);
|
||||
|
||||
const toggleChain = () => {
|
||||
setExpandedChains((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(certificate.certificateId)) {
|
||||
next.delete(certificate.certificateId);
|
||||
} else {
|
||||
next.add(certificate.certificateId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={certificate.certificateId}
|
||||
@@ -77,12 +116,52 @@ export const ShowCertificates = () => {
|
||||
<span className="text-sm font-medium">
|
||||
{index + 1}. {certificate.name}
|
||||
</span>
|
||||
{commonName && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
CN: {commonName}
|
||||
</span>
|
||||
)}
|
||||
{chainInfo.isChain && (
|
||||
<div className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-muted/50">
|
||||
<Link className="size-3 text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Chain ({chainInfo.count})
|
||||
</span>
|
||||
<div className="flex flex-col gap-1.5 mt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleChain}
|
||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-muted/50 w-fit hover:bg-muted transition-colors"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="size-3 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="size-3 text-muted-foreground" />
|
||||
)}
|
||||
<Link className="size-3 text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Chain ({chainInfo.count} certificates)
|
||||
</span>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="flex flex-col gap-3 pl-2 border-l-2 border-muted">
|
||||
{chainDetails?.map((cert) => (
|
||||
<div
|
||||
key={cert.index}
|
||||
className="flex flex-col gap-1 p-2 rounded-md bg-muted/30"
|
||||
>
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
{cert.label}
|
||||
</span>
|
||||
{cert.commonName && (
|
||||
<span className="text-xs text-muted-foreground/80">
|
||||
CN: {cert.commonName}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={`text-xs ${cert.className}`}
|
||||
>
|
||||
{cert.message}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
// @ts-nocheck
|
||||
|
||||
// Split certificate chain into individual certificates
|
||||
export const splitCertificateChain = (certData: string): string[] => {
|
||||
const certRegex =
|
||||
/(-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----)/g;
|
||||
const matches = certData.match(certRegex);
|
||||
return matches || [];
|
||||
};
|
||||
|
||||
export const extractExpirationDate = (certData: string): Date | null => {
|
||||
try {
|
||||
// Decode PEM base64 to DER binary
|
||||
@@ -94,8 +102,156 @@ export const extractExpirationDate = (certData: string): Date | null => {
|
||||
}
|
||||
};
|
||||
|
||||
export const extractCommonName = (certData: string): string | null => {
|
||||
try {
|
||||
// Decode PEM base64 to DER binary
|
||||
const b64 = certData.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
|
||||
const binStr = atob(b64);
|
||||
const der = new Uint8Array(binStr.length);
|
||||
for (let i = 0; i < binStr.length; i++) {
|
||||
der[i] = binStr.charCodeAt(i);
|
||||
}
|
||||
|
||||
let offset = 0;
|
||||
|
||||
// Helper: read ASN.1 length field
|
||||
function readLength(pos: number): { length: number; offset: number } {
|
||||
// biome-ignore lint/style/noParameterAssign: <explanation>
|
||||
let len = der[pos++];
|
||||
if (len & 0x80) {
|
||||
const bytes = len & 0x7f;
|
||||
len = 0;
|
||||
for (let i = 0; i < bytes; i++) {
|
||||
// biome-ignore lint/style/noParameterAssign: <explanation>
|
||||
len = (len << 8) + der[pos++];
|
||||
}
|
||||
}
|
||||
return { length: len, offset: pos };
|
||||
}
|
||||
|
||||
// Helper: skip a field
|
||||
function skipField(pos: number): number {
|
||||
// biome-ignore lint/style/noParameterAssign: <explanation>
|
||||
pos++;
|
||||
const fieldLen = readLength(pos);
|
||||
return fieldLen.offset + fieldLen.length;
|
||||
}
|
||||
|
||||
// Skip the outer certificate sequence
|
||||
if (der[offset++] !== 0x30) throw new Error("Expected sequence");
|
||||
({ offset } = readLength(offset));
|
||||
|
||||
// Skip tbsCertificate sequence
|
||||
if (der[offset++] !== 0x30) throw new Error("Expected tbsCertificate");
|
||||
({ offset } = readLength(offset));
|
||||
|
||||
// Check for optional version field (context-specific tag [0])
|
||||
if (der[offset] === 0xa0) {
|
||||
offset++;
|
||||
const versionLen = readLength(offset);
|
||||
offset = versionLen.offset + versionLen.length;
|
||||
}
|
||||
|
||||
// Skip serialNumber
|
||||
offset = skipField(offset);
|
||||
|
||||
// Skip signature
|
||||
offset = skipField(offset);
|
||||
|
||||
// Skip issuer
|
||||
offset = skipField(offset);
|
||||
|
||||
// Skip validity
|
||||
offset = skipField(offset);
|
||||
|
||||
// Subject sequence - where we find the CN
|
||||
if (der[offset++] !== 0x30) throw new Error("Expected subject sequence");
|
||||
const subjectLen = readLength(offset);
|
||||
const subjectEnd = subjectLen.offset + subjectLen.length;
|
||||
offset = subjectLen.offset;
|
||||
|
||||
// Parse subject RDNs looking for CN (OID 2.5.4.3)
|
||||
while (offset < subjectEnd) {
|
||||
if (der[offset++] !== 0x31) continue; // SET
|
||||
const setLen = readLength(offset);
|
||||
offset = setLen.offset;
|
||||
|
||||
if (der[offset++] !== 0x30) continue; // SEQUENCE
|
||||
const seqLen = readLength(offset);
|
||||
offset = seqLen.offset;
|
||||
|
||||
if (der[offset++] !== 0x06) continue; // OID
|
||||
const oidLen = readLength(offset);
|
||||
offset = oidLen.offset;
|
||||
|
||||
// Check if OID is 2.5.4.3 (commonName)
|
||||
const oid = Array.from(der.slice(offset, offset + oidLen.length));
|
||||
offset += oidLen.length;
|
||||
|
||||
// OID 2.5.4.3 in DER: [0x55, 0x04, 0x03]
|
||||
if (
|
||||
oid.length === 3 &&
|
||||
oid[0] === 0x55 &&
|
||||
oid[1] === 0x04 &&
|
||||
oid[2] === 0x03
|
||||
) {
|
||||
// Next should be the string value
|
||||
const strType = der[offset++];
|
||||
const strLen = readLength(offset);
|
||||
const cnBytes = der.slice(strLen.offset, strLen.offset + strLen.length);
|
||||
return new TextDecoder().decode(cnBytes);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Error parsing certificate CN:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Extract the Common Name from the first (leaf) certificate in a chain
|
||||
export const extractLeafCommonName = (certData: string): string | null => {
|
||||
const certs = splitCertificateChain(certData);
|
||||
if (certs.length === 0) return null;
|
||||
return extractCommonName(certs[0]);
|
||||
};
|
||||
|
||||
// Extract expiration dates from all certificates in a chain
|
||||
export const extractAllExpirationDates = (
|
||||
certData: string,
|
||||
): Array<{
|
||||
cert: string;
|
||||
index: number;
|
||||
expirationDate: Date | null;
|
||||
commonName: string | null;
|
||||
}> => {
|
||||
const certs = splitCertificateChain(certData);
|
||||
return certs.map((cert, index) => ({
|
||||
cert,
|
||||
index,
|
||||
expirationDate: extractExpirationDate(cert),
|
||||
commonName: extractCommonName(cert),
|
||||
}));
|
||||
};
|
||||
|
||||
// Get the earliest expiration date from a certificate chain
|
||||
export const getEarliestExpirationDate = (certData: string): Date | null => {
|
||||
const expirationDates = extractAllExpirationDates(certData);
|
||||
const validDates = expirationDates
|
||||
.filter((item) => item.expirationDate !== null)
|
||||
.map((item) => item.expirationDate as Date);
|
||||
|
||||
if (validDates.length === 0) return null;
|
||||
|
||||
return new Date(Math.min(...validDates.map((date) => date.getTime())));
|
||||
};
|
||||
|
||||
export const getExpirationStatus = (certData: string) => {
|
||||
const expirationDate = extractExpirationDate(certData);
|
||||
const chainInfo = getCertificateChainInfo(certData);
|
||||
const expirationDate = chainInfo.isChain
|
||||
? getEarliestExpirationDate(certData)
|
||||
: extractExpirationDate(certData);
|
||||
|
||||
if (!expirationDate)
|
||||
return {
|
||||
@@ -153,3 +309,67 @@ export const getCertificateChainInfo = (certData: string) => {
|
||||
count: 1,
|
||||
};
|
||||
};
|
||||
|
||||
// Get detailed expiration information for all certificates in a chain
|
||||
export const getCertificateChainExpirationDetails = (certData: string) => {
|
||||
const allExpirations = extractAllExpirationDates(certData);
|
||||
const now = new Date();
|
||||
|
||||
return allExpirations.map(({ index, expirationDate, commonName }) => {
|
||||
if (!expirationDate) {
|
||||
return {
|
||||
index,
|
||||
label: `Certificate ${index + 1}`,
|
||||
commonName,
|
||||
status: "unknown" as const,
|
||||
className: "text-muted-foreground",
|
||||
message: "Could not determine expiration",
|
||||
expirationDate: null,
|
||||
};
|
||||
}
|
||||
|
||||
const daysUntilExpiration = Math.ceil(
|
||||
(expirationDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),
|
||||
);
|
||||
|
||||
let status: "expired" | "warning" | "valid";
|
||||
let className: string;
|
||||
let message: string;
|
||||
|
||||
if (daysUntilExpiration < 0) {
|
||||
status = "expired";
|
||||
className = "text-red-500";
|
||||
message = `Expired on ${expirationDate.toLocaleDateString([], {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}`;
|
||||
} else if (daysUntilExpiration <= 30) {
|
||||
status = "warning";
|
||||
className = "text-yellow-500";
|
||||
message = `Expires in ${daysUntilExpiration} days`;
|
||||
} else {
|
||||
status = "valid";
|
||||
className = "text-muted-foreground";
|
||||
message = `Expires ${expirationDate.toLocaleDateString([], {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}`;
|
||||
}
|
||||
|
||||
return {
|
||||
index,
|
||||
label:
|
||||
index === 0
|
||||
? `Certificate ${index + 1} (Leaf)`
|
||||
: `Certificate ${index + 1}`,
|
||||
commonName,
|
||||
status,
|
||||
className,
|
||||
message,
|
||||
expirationDate,
|
||||
daysUntilExpiration,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import {
|
||||
ADDITIONAL_FLAG_ERROR,
|
||||
ADDITIONAL_FLAG_REGEX,
|
||||
} from "@dokploy/server/db/validations/destination";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||
import { PenBoxIcon, PlusIcon, Trash2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
@@ -46,6 +50,16 @@ const addDestination = z.object({
|
||||
region: z.string(),
|
||||
endpoint: z.string().min(1, "Endpoint is required"),
|
||||
serverId: z.string().optional(),
|
||||
additionalFlags: z
|
||||
.array(
|
||||
z.object({
|
||||
value: z
|
||||
.string()
|
||||
.min(1, "Flag cannot be empty")
|
||||
.regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
type AddDestination = z.infer<typeof addDestination>;
|
||||
@@ -89,9 +103,16 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
||||
region: "",
|
||||
secretAccessKey: "",
|
||||
endpoint: "",
|
||||
additionalFlags: [],
|
||||
},
|
||||
resolver: zodResolver(addDestination),
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "additionalFlags",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (destination) {
|
||||
form.reset({
|
||||
@@ -102,6 +123,8 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
||||
bucket: destination.bucket,
|
||||
region: destination.region,
|
||||
endpoint: destination.endpoint,
|
||||
additionalFlags:
|
||||
destination.additionalFlags?.map((f) => ({ value: f })) ?? [],
|
||||
});
|
||||
} else {
|
||||
form.reset();
|
||||
@@ -118,6 +141,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
||||
region: data.region,
|
||||
secretAccessKey: data.secretAccessKey,
|
||||
destinationId: destinationId || "",
|
||||
additionalFlags: data.additionalFlags?.map((f) => f.value) ?? [],
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success(`Destination ${destinationId ? "Updated" : "Created"}`);
|
||||
@@ -127,9 +151,12 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
||||
}
|
||||
setOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((e) => {
|
||||
toast.error(
|
||||
`Error ${destinationId ? "Updating" : "Creating"} the Destination`,
|
||||
{
|
||||
description: e.message,
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -141,6 +168,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
||||
"secretAccessKey",
|
||||
"bucket",
|
||||
"endpoint",
|
||||
"additionalFlags",
|
||||
]);
|
||||
|
||||
if (!result) {
|
||||
@@ -179,6 +207,8 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
||||
region,
|
||||
secretAccessKey: secretKey,
|
||||
serverId,
|
||||
additionalFlags:
|
||||
form.getValues("additionalFlags")?.map((f) => f.value) ?? [],
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Connection Success");
|
||||
@@ -358,6 +388,48 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>Additional Flags (Optional)</FormLabel>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => append({ value: "" })}
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
Add Flag
|
||||
</Button>
|
||||
</div>
|
||||
{fields.map((field, index) => (
|
||||
<FormField
|
||||
key={field.id}
|
||||
control={form.control}
|
||||
name={`additionalFlags.${index}.value`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="--s3-sign-accept-encoding=false"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Trash2 className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter
|
||||
|
||||
@@ -283,7 +283,7 @@ export const AddGitlabProvider = () => {
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="For organization/group access use the slugish name of the group eg: my-org"
|
||||
placeholder="For organization/group access use the slug name of the group eg: my-org"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -192,7 +192,7 @@ export const EditGitlabProvider = ({ gitlabId }: Props) => {
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="For organization/group access use the slugish name of the group eg: my-org"
|
||||
placeholder="For organization/group access use the slug name of the group eg: my-org"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ImportIcon,
|
||||
Loader2,
|
||||
Trash2,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
@@ -24,6 +25,13 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
||||
@@ -39,6 +47,8 @@ export const ShowGitProviders = () => {
|
||||
const { data, isPending, refetch } = api.gitProvider.getAll.useQuery();
|
||||
const { mutateAsync, isPending: isRemoving } =
|
||||
api.gitProvider.remove.useMutation();
|
||||
const { mutateAsync: toggleShare, isPending: isToggling } =
|
||||
api.gitProvider.toggleShare.useMutation();
|
||||
const url = useUrl();
|
||||
|
||||
const getGitlabUrl = (
|
||||
@@ -154,10 +164,62 @@ export const ShowGitProviders = () => {
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{!gitProvider.isOwner && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-xs"
|
||||
>
|
||||
<Users className="size-3 mr-1" />
|
||||
Shared
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
{gitProvider.isOwner && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-1.5 mr-2">
|
||||
<Users className="size-4 text-muted-foreground" />
|
||||
<Switch
|
||||
disabled={isToggling}
|
||||
checked={
|
||||
gitProvider.sharedWithOrganization
|
||||
}
|
||||
onCheckedChange={async (
|
||||
checked,
|
||||
) => {
|
||||
await toggleShare({
|
||||
gitProviderId:
|
||||
gitProvider.gitProviderId,
|
||||
sharedWithOrganization: checked,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
checked
|
||||
? "Provider shared with organization"
|
||||
: "Provider unshared",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error updating sharing",
|
||||
);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Share with entire organization
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{isBitbucket &&
|
||||
gitProvider.bitbucket?.appPassword &&
|
||||
!gitProvider.bitbucket?.apiToken ? (
|
||||
@@ -222,62 +284,71 @@ export const ShowGitProviders = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isGithub && haveGithubRequirements && (
|
||||
<EditGithubProvider
|
||||
githubId={gitProvider.github?.githubId}
|
||||
/>
|
||||
)}
|
||||
{gitProvider.isOwner && (
|
||||
<>
|
||||
{isGithub && haveGithubRequirements && (
|
||||
<EditGithubProvider
|
||||
githubId={gitProvider.github?.githubId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isGitlab && (
|
||||
<EditGitlabProvider
|
||||
gitlabId={gitProvider.gitlab?.gitlabId}
|
||||
/>
|
||||
)}
|
||||
{isGitlab && (
|
||||
<EditGitlabProvider
|
||||
gitlabId={gitProvider.gitlab?.gitlabId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBitbucket && (
|
||||
<EditBitbucketProvider
|
||||
bitbucketId={
|
||||
gitProvider.bitbucket?.bitbucketId
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isBitbucket && (
|
||||
<EditBitbucketProvider
|
||||
bitbucketId={
|
||||
gitProvider.bitbucket?.bitbucketId
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isGitea && (
|
||||
<EditGiteaProvider
|
||||
giteaId={gitProvider.gitea?.giteaId}
|
||||
/>
|
||||
)}
|
||||
{isGitea && (
|
||||
<EditGiteaProvider
|
||||
giteaId={gitProvider.gitea?.giteaId}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DialogAction
|
||||
title="Delete Git Provider"
|
||||
description="Are you sure you want to delete this Git Provider?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
gitProviderId: gitProvider.gitProviderId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Git Provider deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting Git Provider",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
title="Delete Git Provider"
|
||||
description={
|
||||
gitProvider.sharedWithOrganization
|
||||
? "This provider is shared with the organization. Deleting it will remove access for all members. Are you sure?"
|
||||
: "Are you sure you want to delete this Git Provider?"
|
||||
}
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
gitProviderId:
|
||||
gitProvider.gitProviderId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Git Provider deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting Git Provider",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,9 +12,9 @@ import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
DiscordIcon,
|
||||
MattermostIcon,
|
||||
GotifyIcon,
|
||||
LarkIcon,
|
||||
MattermostIcon,
|
||||
NtfyIcon,
|
||||
PushoverIcon,
|
||||
ResendIcon,
|
||||
@@ -54,6 +54,7 @@ const notificationBaseSchema = z.object({
|
||||
appDeploy: z.boolean().default(false),
|
||||
appBuildError: z.boolean().default(false),
|
||||
databaseBackup: z.boolean().default(false),
|
||||
dokployBackup: z.boolean().default(false),
|
||||
volumeBackup: z.boolean().default(false),
|
||||
dokployRestart: z.boolean().default(false),
|
||||
dockerCleanup: z.boolean().default(false),
|
||||
@@ -355,6 +356,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: notification.appDeploy,
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
dokployBackup: notification.dokployBackup,
|
||||
volumeBackup: notification.volumeBackup,
|
||||
dockerCleanup: notification.dockerCleanup,
|
||||
webhookUrl: notification.slack?.webhookUrl,
|
||||
@@ -369,6 +371,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: notification.appDeploy,
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
dokployBackup: notification.dokployBackup,
|
||||
volumeBackup: notification.volumeBackup,
|
||||
botToken: notification.telegram?.botToken,
|
||||
messageThreadId: notification.telegram?.messageThreadId || "",
|
||||
@@ -384,6 +387,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: notification.appDeploy,
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
dokployBackup: notification.dokployBackup,
|
||||
volumeBackup: notification.volumeBackup,
|
||||
type: notification.notificationType,
|
||||
webhookUrl: notification.discord?.webhookUrl,
|
||||
@@ -398,6 +402,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: notification.appDeploy,
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
dokployBackup: notification.dokployBackup,
|
||||
volumeBackup: notification.volumeBackup,
|
||||
type: notification.notificationType,
|
||||
smtpServer: notification.email?.smtpServer,
|
||||
@@ -416,6 +421,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: notification.appDeploy,
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
dokployBackup: notification.dokployBackup,
|
||||
volumeBackup: notification.volumeBackup,
|
||||
type: notification.notificationType,
|
||||
apiKey: notification.resend?.apiKey,
|
||||
@@ -431,6 +437,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: notification.appDeploy,
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
dokployBackup: notification.dokployBackup,
|
||||
volumeBackup: notification.volumeBackup,
|
||||
type: notification.notificationType,
|
||||
appToken: notification.gotify?.appToken,
|
||||
@@ -446,6 +453,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: notification.appDeploy,
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
dokployBackup: notification.dokployBackup,
|
||||
volumeBackup: notification.volumeBackup,
|
||||
type: notification.notificationType,
|
||||
accessToken: notification.ntfy?.accessToken || "",
|
||||
@@ -462,6 +470,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: notification.appDeploy,
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
dokployBackup: notification.dokployBackup,
|
||||
volumeBackup: notification.volumeBackup,
|
||||
type: notification.notificationType,
|
||||
webhookUrl: notification.mattermost?.webhookUrl,
|
||||
@@ -477,6 +486,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: notification.appDeploy,
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
dokployBackup: notification.dokployBackup,
|
||||
type: notification.notificationType,
|
||||
webhookUrl: notification.lark?.webhookUrl,
|
||||
name: notification.name,
|
||||
@@ -490,6 +500,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: notification.appDeploy,
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
dokployBackup: notification.dokployBackup,
|
||||
volumeBackup: notification.volumeBackup,
|
||||
type: notification.notificationType,
|
||||
webhookUrl: notification.teams?.webhookUrl,
|
||||
@@ -503,6 +514,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: notification.appDeploy,
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
dokployBackup: notification.dokployBackup,
|
||||
type: notification.notificationType,
|
||||
endpoint: notification.custom?.endpoint || "",
|
||||
headers: notification.custom?.headers
|
||||
@@ -524,6 +536,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: notification.appDeploy,
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
dokployBackup: notification.dokployBackup,
|
||||
volumeBackup: notification.volumeBackup,
|
||||
type: notification.notificationType,
|
||||
userKey: notification.pushover?.userKey,
|
||||
@@ -562,6 +575,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy,
|
||||
dokployRestart,
|
||||
databaseBackup,
|
||||
dokployBackup,
|
||||
volumeBackup,
|
||||
dockerCleanup,
|
||||
serverThreshold,
|
||||
@@ -573,6 +587,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
dokployBackup: dokployBackup,
|
||||
volumeBackup: volumeBackup,
|
||||
webhookUrl: data.webhookUrl,
|
||||
channel: data.channel,
|
||||
@@ -588,6 +603,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
dokployBackup: dokployBackup,
|
||||
volumeBackup: volumeBackup,
|
||||
botToken: data.botToken,
|
||||
messageThreadId: data.messageThreadId || "",
|
||||
@@ -604,6 +620,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
dokployBackup: dokployBackup,
|
||||
volumeBackup: volumeBackup,
|
||||
webhookUrl: data.webhookUrl,
|
||||
decoration: data.decoration,
|
||||
@@ -619,6 +636,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
dokployBackup: dokployBackup,
|
||||
volumeBackup: volumeBackup,
|
||||
smtpServer: data.smtpServer,
|
||||
smtpPort: data.smtpPort,
|
||||
@@ -638,6 +656,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
dokployBackup: dokployBackup,
|
||||
volumeBackup: volumeBackup,
|
||||
apiKey: data.apiKey,
|
||||
fromAddress: data.fromAddress,
|
||||
@@ -654,6 +673,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
dokployBackup: dokployBackup,
|
||||
volumeBackup: volumeBackup,
|
||||
serverUrl: data.serverUrl,
|
||||
appToken: data.appToken,
|
||||
@@ -670,6 +690,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
dokployBackup: dokployBackup,
|
||||
volumeBackup: volumeBackup,
|
||||
serverUrl: data.serverUrl,
|
||||
accessToken: data.accessToken || "",
|
||||
@@ -686,6 +707,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
dokployBackup: dokployBackup,
|
||||
volumeBackup: volumeBackup,
|
||||
webhookUrl: data.webhookUrl,
|
||||
channel: data.channel || undefined,
|
||||
@@ -702,6 +724,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
dokployBackup: dokployBackup,
|
||||
volumeBackup: volumeBackup,
|
||||
webhookUrl: data.webhookUrl,
|
||||
name: data.name,
|
||||
@@ -716,6 +739,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
dokployBackup: dokployBackup,
|
||||
volumeBackup: volumeBackup,
|
||||
webhookUrl: data.webhookUrl,
|
||||
name: data.name,
|
||||
@@ -742,6 +766,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
dokployBackup: dokployBackup,
|
||||
volumeBackup: volumeBackup,
|
||||
endpoint: data.endpoint,
|
||||
headers: headersRecord,
|
||||
@@ -761,6 +786,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
dokployBackup: dokployBackup,
|
||||
volumeBackup: volumeBackup,
|
||||
userKey: data.userKey,
|
||||
apiToken: data.apiToken,
|
||||
@@ -1856,6 +1882,27 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dokployBackup"
|
||||
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>Dokploy Backup</FormLabel>
|
||||
<FormDescription>
|
||||
Trigger the action when a dokploy backup is created.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="volumeBackup"
|
||||
|
||||
@@ -161,7 +161,7 @@ export const SetupServer = ({ serverId, asButton = false }: Props) => {
|
||||
<ul>
|
||||
<li>
|
||||
1. Add the public SSH Key when you create a server in your
|
||||
preffered provider (Hostinger, Digital Ocean, Hetzner,
|
||||
preferred provider (Hostinger, Digital Ocean, Hetzner,
|
||||
etc){" "}
|
||||
</li>
|
||||
<li>2. Add The SSH Key to Server Manually</li>
|
||||
|
||||
@@ -48,7 +48,7 @@ import { ShowMonitoringModal } from "./show-monitoring-modal";
|
||||
import { ShowSchedulesModal } from "./show-schedules-modal";
|
||||
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
||||
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
||||
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
||||
import { WelcomeSubscription } from "./welcome-stripe/welcome-subscription";
|
||||
|
||||
export const ShowServers = () => {
|
||||
const router = useRouter();
|
||||
@@ -63,7 +63,7 @@ export const ShowServers = () => {
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{query?.success && isCloud && <WelcomeSuscription />}
|
||||
{query?.success && isCloud && <WelcomeSubscription />}
|
||||
<Card className="h-full p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||
<div className="rounded-xl bg-background shadow-md ">
|
||||
<CardHeader className="">
|
||||
|
||||
@@ -51,7 +51,7 @@ export const { useStepper, steps, Scoped } = defineStepper(
|
||||
{ id: "complete", title: "Complete", description: "Checkout complete" },
|
||||
);
|
||||
|
||||
export const WelcomeSuscription = () => {
|
||||
export const WelcomeSubscription = () => {
|
||||
const [showConfetti, setShowConfetti] = useState(false);
|
||||
const stepper = useStepper();
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { EnterpriseFeatureLocked } from "@/components/proprietary/enterprise-feature-gate";
|
||||
import { api, type RouterOutputs } from "@/utils/api";
|
||||
|
||||
/** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */
|
||||
@@ -170,6 +171,7 @@ const addPermissions = z.object({
|
||||
accessedProjects: z.array(z.string()).optional(),
|
||||
accessedEnvironments: z.array(z.string()).optional(),
|
||||
accessedServices: z.array(z.string()).optional(),
|
||||
accessedGitProviders: z.array(z.string()).optional(),
|
||||
canCreateProjects: z.boolean().optional().default(false),
|
||||
canCreateServices: z.boolean().optional().default(false),
|
||||
canDeleteProjects: z.boolean().optional().default(false),
|
||||
@@ -196,6 +198,15 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
|
||||
const { data: projects } = api.project.allForPermissions.useQuery(undefined, {
|
||||
enabled: isOpen,
|
||||
});
|
||||
const { data: haveValidLicense } =
|
||||
api.licenseKey.haveValidLicenseKey.useQuery();
|
||||
|
||||
const { data: gitProviders } = api.gitProvider.allForPermissions.useQuery(
|
||||
undefined,
|
||||
{
|
||||
enabled: isOpen && !!haveValidLicense,
|
||||
},
|
||||
);
|
||||
|
||||
const { data, refetch } = api.user.one.useQuery(
|
||||
{
|
||||
@@ -214,6 +225,7 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
|
||||
accessedProjects: [],
|
||||
accessedEnvironments: [],
|
||||
accessedServices: [],
|
||||
accessedGitProviders: [],
|
||||
canDeleteEnvironments: false,
|
||||
canCreateProjects: false,
|
||||
canCreateServices: false,
|
||||
@@ -235,6 +247,7 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
|
||||
accessedProjects: data.accessedProjects || [],
|
||||
accessedEnvironments: data.accessedEnvironments || [],
|
||||
accessedServices: data.accessedServices || [],
|
||||
accessedGitProviders: data.accessedGitProviders || [],
|
||||
canCreateProjects: data.canCreateProjects,
|
||||
canCreateServices: data.canCreateServices,
|
||||
canDeleteProjects: data.canDeleteProjects,
|
||||
@@ -262,6 +275,7 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
|
||||
accessedProjects: data.accessedProjects || [],
|
||||
accessedEnvironments: data.accessedEnvironments || [],
|
||||
accessedServices: data.accessedServices || [],
|
||||
accessedGitProviders: data.accessedGitProviders || [],
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
canAccessToAPI: data.canAccessToAPI,
|
||||
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||
@@ -870,6 +884,78 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{haveValidLicense ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accessedGitProviders"
|
||||
render={() => (
|
||||
<FormItem className="md:col-span-2">
|
||||
<div className="mb-4">
|
||||
<FormLabel className="text-base">Git Providers</FormLabel>
|
||||
<FormDescription>
|
||||
Select the Git Providers that the user can access
|
||||
</FormDescription>
|
||||
</div>
|
||||
{gitProviders?.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No git providers found
|
||||
</p>
|
||||
)}
|
||||
<div className="grid md:grid-cols-1 gap-2">
|
||||
{gitProviders?.map((provider) => (
|
||||
<FormField
|
||||
key={provider.gitProviderId}
|
||||
control={form.control}
|
||||
name="accessedGitProviders"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center space-x-3 space-y-0 rounded-lg border p-3">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(
|
||||
provider.gitProviderId,
|
||||
)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
field.onChange([
|
||||
...(field.value || []),
|
||||
provider.gitProviderId,
|
||||
]);
|
||||
} else {
|
||||
field.onChange(
|
||||
field.value?.filter(
|
||||
(v) => v !== provider.gitProviderId,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel className="text-sm cursor-pointer">
|
||||
{provider.name}
|
||||
</FormLabel>
|
||||
<span className="text-xs text-muted-foreground capitalize">
|
||||
({provider.providerType})
|
||||
</span>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className="md:col-span-2">
|
||||
<EnterpriseFeatureLocked
|
||||
compact
|
||||
title="Git Provider Assignment"
|
||||
description="Assign specific Git Providers to users with an Enterprise license."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2">
|
||||
<Button
|
||||
isLoading={isPending}
|
||||
|
||||
@@ -153,7 +153,7 @@ export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => {
|
||||
)}
|
||||
<br />
|
||||
<em className="text-muted-foreground text-xs">
|
||||
Note: Owner role is intransferible.
|
||||
Note: Owner role is nontransferable.
|
||||
</em>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
|
||||
@@ -122,7 +122,7 @@ export const ShowUsers = () => {
|
||||
// Can change role based on hierarchy:
|
||||
// - Owner: Can change anyone's role (except themselves and other owners)
|
||||
// - Admin: Can only change member/custom roles (not other admins or owners)
|
||||
// - Owner role is intransferible
|
||||
// - Owner role is nontransferable
|
||||
const canChangeRole =
|
||||
member.role !== "owner" &&
|
||||
member.user.id !== session?.user?.id &&
|
||||
|
||||
@@ -101,7 +101,7 @@ const BreadcrumbEllipsis = ({
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
|
||||
BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis";
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
|
||||
@@ -19,7 +19,7 @@ interface TreeDataItem {
|
||||
|
||||
type TreeProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
data: TreeDataItem[] | TreeDataItem;
|
||||
initialSlelectedItemId?: string;
|
||||
initialSelectedItemId?: string;
|
||||
onSelectChange?: (item: TreeDataItem | undefined) => void;
|
||||
expandAll?: boolean;
|
||||
folderIcon?: LucideIcon;
|
||||
@@ -30,7 +30,7 @@ const Tree = React.forwardRef<HTMLDivElement, TreeProps>(
|
||||
(
|
||||
{
|
||||
data,
|
||||
initialSlelectedItemId,
|
||||
initialSelectedItemId,
|
||||
onSelectChange,
|
||||
expandAll,
|
||||
folderIcon,
|
||||
@@ -42,7 +42,7 @@ const Tree = React.forwardRef<HTMLDivElement, TreeProps>(
|
||||
) => {
|
||||
const [selectedItemId, setSelectedItemId] = React.useState<
|
||||
string | undefined
|
||||
>(initialSlelectedItemId);
|
||||
>(initialSelectedItemId);
|
||||
|
||||
const handleSelectChange = React.useCallback(
|
||||
(item: TreeDataItem | undefined) => {
|
||||
@@ -55,7 +55,7 @@ const Tree = React.forwardRef<HTMLDivElement, TreeProps>(
|
||||
);
|
||||
|
||||
const expandedItemIds = React.useMemo(() => {
|
||||
if (!initialSlelectedItemId) {
|
||||
if (!initialSelectedItemId) {
|
||||
return [] as string[];
|
||||
}
|
||||
|
||||
@@ -81,9 +81,9 @@ const Tree = React.forwardRef<HTMLDivElement, TreeProps>(
|
||||
}
|
||||
}
|
||||
|
||||
walkTreeItems(data, initialSlelectedItemId);
|
||||
walkTreeItems(data, initialSelectedItemId);
|
||||
return ids;
|
||||
}, [data, initialSlelectedItemId]);
|
||||
}, [data, initialSelectedItemId]);
|
||||
|
||||
const { ref: refRoot } = useResizeObserver();
|
||||
|
||||
|
||||
1
apps/dokploy/drizzle/0130_abandoned_dagger.sql
Normal file
1
apps/dokploy/drizzle/0130_abandoned_dagger.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "domain" ADD COLUMN "customEntrypoint" text;
|
||||
1
apps/dokploy/drizzle/0155_careless_clea.sql
Normal file
1
apps/dokploy/drizzle/0155_careless_clea.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "destination" ADD COLUMN "additionalFlags" text[];
|
||||
1
apps/dokploy/drizzle/0156_fair_vargas.sql
Normal file
1
apps/dokploy/drizzle/0156_fair_vargas.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "notification" ADD COLUMN "dokployBackup" boolean DEFAULT false NOT NULL;
|
||||
2
apps/dokploy/drizzle/0157_stiff_misty_knight.sql
Normal file
2
apps/dokploy/drizzle/0157_stiff_misty_knight.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "member" ADD COLUMN "accessedGitProviders" text[] DEFAULT ARRAY[]::text[] NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "git_provider" ADD COLUMN "sharedWithOrganization" boolean DEFAULT false NOT NULL;
|
||||
8243
apps/dokploy/drizzle/meta/0155_snapshot.json
Normal file
8243
apps/dokploy/drizzle/meta/0155_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
8250
apps/dokploy/drizzle/meta/0156_snapshot.json
Normal file
8250
apps/dokploy/drizzle/meta/0156_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
8264
apps/dokploy/drizzle/meta/0157_snapshot.json
Normal file
8264
apps/dokploy/drizzle/meta/0157_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1086,6 +1086,27 @@
|
||||
"when": 1774337356154,
|
||||
"tag": "0154_careful_eternals",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 155,
|
||||
"version": "7",
|
||||
"when": 1774794547865,
|
||||
"tag": "0155_careless_clea",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 156,
|
||||
"version": "7",
|
||||
"when": 1774910955774,
|
||||
"tag": "0156_fair_vargas",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 157,
|
||||
"version": "7",
|
||||
"when": 1775246622387,
|
||||
"tag": "0157_stiff_misty_knight",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ssoClient } from "@better-auth/sso/client";
|
||||
import { apiKeyClient } from "@better-auth/api-key/client";
|
||||
import { ssoClient } from "@better-auth/sso/client";
|
||||
import {
|
||||
adminClient,
|
||||
inferAdditionalFields,
|
||||
|
||||
@@ -196,7 +196,7 @@ export default async function handler(
|
||||
return;
|
||||
}
|
||||
|
||||
const commitedPaths = await extractCommitedPaths(
|
||||
const committedPaths = await extractCommittedPaths(
|
||||
req.body,
|
||||
application.bitbucket,
|
||||
application.bitbucketRepositorySlug ||
|
||||
@@ -206,7 +206,7 @@ export default async function handler(
|
||||
|
||||
const shouldDeployPaths = shouldDeploy(
|
||||
application.watchPaths,
|
||||
commitedPaths,
|
||||
committedPaths,
|
||||
);
|
||||
|
||||
if (!shouldDeployPaths) {
|
||||
@@ -538,7 +538,7 @@ export const getProviderByHeader = (headers: any) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const extractCommitedPaths = async (
|
||||
export const extractCommittedPaths = async (
|
||||
body: any,
|
||||
bitbucket: Bitbucket | null,
|
||||
repository: string,
|
||||
@@ -548,7 +548,7 @@ export const extractCommitedPaths = async (
|
||||
const commitHashes = changes
|
||||
.map((change: any) => change.new?.target?.hash)
|
||||
.filter(Boolean);
|
||||
const commitedPaths: string[] = [];
|
||||
const committedPaths: string[] = [];
|
||||
const username =
|
||||
bitbucket?.bitbucketWorkspaceName || bitbucket?.bitbucketUsername || "";
|
||||
for (const commit of commitHashes) {
|
||||
@@ -559,7 +559,7 @@ export const extractCommitedPaths = async (
|
||||
});
|
||||
const data = await response.json();
|
||||
for (const value of data.values) {
|
||||
if (value?.new?.path) commitedPaths.push(value.new.path);
|
||||
if (value?.new?.path) committedPaths.push(value.new.path);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
@@ -571,5 +571,5 @@ export const extractCommitedPaths = async (
|
||||
}
|
||||
}
|
||||
|
||||
return commitedPaths;
|
||||
return committedPaths;
|
||||
};
|
||||
|
||||
@@ -8,8 +8,8 @@ import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import {
|
||||
extractBranchName,
|
||||
extractCommitedPaths,
|
||||
extractCommitMessage,
|
||||
extractCommittedPaths,
|
||||
extractHash,
|
||||
getProviderByHeader,
|
||||
} from "../[refreshToken]";
|
||||
@@ -97,7 +97,7 @@ export default async function handler(
|
||||
return;
|
||||
}
|
||||
|
||||
const commitedPaths = await extractCommitedPaths(
|
||||
const committedPaths = await extractCommittedPaths(
|
||||
req.body,
|
||||
composeResult.bitbucket,
|
||||
composeResult.bitbucketRepositorySlug ||
|
||||
@@ -107,7 +107,7 @@ export default async function handler(
|
||||
|
||||
const shouldDeployPaths = shouldDeploy(
|
||||
composeResult.watchPaths,
|
||||
commitedPaths,
|
||||
committedPaths,
|
||||
);
|
||||
|
||||
if (!shouldDeployPaths) {
|
||||
|
||||
@@ -174,27 +174,27 @@ export default async function handler(
|
||||
case "invoice.payment_succeeded": {
|
||||
const newInvoice = event.data.object as Stripe.Invoice;
|
||||
|
||||
const suscription = await stripe.subscriptions.retrieve(
|
||||
const subscription = await stripe.subscriptions.retrieve(
|
||||
newInvoice.subscription as string,
|
||||
);
|
||||
|
||||
if (suscription.status !== "active") {
|
||||
if (subscription.status !== "active") {
|
||||
console.log(
|
||||
`Skipping invoice.payment_succeeded for subscription ${suscription.id} with status ${suscription.status}`,
|
||||
`Skipping invoice.payment_succeeded for subscription ${subscription.id} with status ${subscription.status}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const serversQuantity = getSubscriptionServersQuantity(
|
||||
suscription?.items?.data ?? [],
|
||||
subscription?.items?.data ?? [],
|
||||
);
|
||||
await db
|
||||
.update(user)
|
||||
.set({ serversQuantity })
|
||||
.where(eq(user.stripeCustomerId, suscription.customer as string));
|
||||
.where(eq(user.stripeCustomerId, subscription.customer as string));
|
||||
|
||||
const admin = await findUserByStripeCustomerId(
|
||||
suscription.customer as string,
|
||||
subscription.customer as string,
|
||||
);
|
||||
|
||||
if (!admin) {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { ShowImport } from "@/components/dashboard/application/advanced/import/s
|
||||
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
||||
import { ShowDeployments } from "@/components/dashboard/application/deployments/show-deployments";
|
||||
import { ShowDomains } from "@/components/dashboard/application/domains/show-domains";
|
||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
||||
import { ShowPatches } from "@/components/dashboard/application/patches/show-patches";
|
||||
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
||||
import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups";
|
||||
|
||||
@@ -10,7 +10,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
||||
|
||||
@@ -10,7 +10,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
||||
|
||||
@@ -10,7 +10,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
||||
|
||||
@@ -10,7 +10,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
||||
|
||||
@@ -10,7 +10,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
||||
|
||||
@@ -10,7 +10,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
||||
|
||||
@@ -3,10 +3,10 @@ import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { ManageCustomRoles } from "@/components/proprietary/roles/manage-custom-roles";
|
||||
import { ShowInvitations } from "@/components/dashboard/settings/users/show-invitations";
|
||||
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { ManageCustomRoles } from "@/components/proprietary/roles/manage-custom-roles";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ import {
|
||||
suggestVariants,
|
||||
} from "@dokploy/server/services/ai";
|
||||
import { createComposeByTemplate } from "@dokploy/server/services/compose";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import {
|
||||
getProviderHeaders,
|
||||
getProviderName,
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
updateBackupById,
|
||||
} from "@dokploy/server";
|
||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||
import { runComposeBackup } from "@dokploy/server/utils/backups/compose";
|
||||
import {
|
||||
getS3Credentials,
|
||||
@@ -53,7 +54,6 @@ import {
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateBackup,
|
||||
@@ -545,52 +545,42 @@ export const backupRouter = createTRPCRouter({
|
||||
}
|
||||
const destination = await findDestinationById(input.destinationId);
|
||||
const queue: string[] = [];
|
||||
const done = false;
|
||||
if (input.backupType === "database") {
|
||||
if (input.databaseType === "postgres") {
|
||||
const postgres = await findPostgresById(input.databaseId);
|
||||
|
||||
restorePostgresBackup(postgres, destination, input, (log) => {
|
||||
queue.push(log);
|
||||
});
|
||||
let done = false;
|
||||
const onLog = (log: string) => queue.push(log);
|
||||
const runRestore = async () => {
|
||||
if (input.backupType === "database") {
|
||||
if (input.databaseType === "postgres") {
|
||||
const postgres = await findPostgresById(input.databaseId);
|
||||
await restorePostgresBackup(postgres, destination, input, onLog);
|
||||
} else if (input.databaseType === "mysql") {
|
||||
const mysql = await findMySqlById(input.databaseId);
|
||||
await restoreMySqlBackup(mysql, destination, input, onLog);
|
||||
} else if (input.databaseType === "mariadb") {
|
||||
const mariadb = await findMariadbById(input.databaseId);
|
||||
await restoreMariadbBackup(mariadb, destination, input, onLog);
|
||||
} else if (input.databaseType === "mongo") {
|
||||
const mongo = await findMongoById(input.databaseId);
|
||||
await restoreMongoBackup(mongo, destination, input, onLog);
|
||||
} else if (input.databaseType === "libsql") {
|
||||
const libsql = await findLibsqlById(input.databaseId);
|
||||
await restoreLibsqlBackup(libsql, destination, input, onLog);
|
||||
} else if (input.databaseType === "web-server") {
|
||||
await restoreWebServerBackup(destination, input.backupFile, onLog);
|
||||
}
|
||||
} else if (input.backupType === "compose") {
|
||||
const compose = await findComposeById(input.databaseId);
|
||||
await restoreComposeBackup(compose, destination, input, onLog);
|
||||
}
|
||||
|
||||
if (input.databaseType === "mysql") {
|
||||
const mysql = await findMySqlById(input.databaseId);
|
||||
restoreMySqlBackup(mysql, destination, input, (log) => {
|
||||
queue.push(log);
|
||||
});
|
||||
}
|
||||
if (input.databaseType === "mariadb") {
|
||||
const mariadb = await findMariadbById(input.databaseId);
|
||||
restoreMariadbBackup(mariadb, destination, input, (log) => {
|
||||
queue.push(log);
|
||||
});
|
||||
}
|
||||
if (input.databaseType === "mongo") {
|
||||
const mongo = await findMongoById(input.databaseId);
|
||||
restoreMongoBackup(mongo, destination, input, (log) => {
|
||||
queue.push(log);
|
||||
});
|
||||
}
|
||||
if (input.databaseType === "libsql") {
|
||||
const libsql = await findLibsqlById(input.databaseId);
|
||||
restoreLibsqlBackup(libsql, destination, input, (log) => {
|
||||
queue.push(log);
|
||||
});
|
||||
}
|
||||
if (input.databaseType === "web-server") {
|
||||
restoreWebServerBackup(destination, input.backupFile, (log) => {
|
||||
queue.push(log);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (input.backupType === "compose") {
|
||||
const compose = await findComposeById(input.databaseId);
|
||||
restoreComposeBackup(compose, destination, input, (log) => {
|
||||
queue.push(log);
|
||||
};
|
||||
runRestore()
|
||||
.catch((error) => {
|
||||
onLog(
|
||||
`Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
done = true;
|
||||
});
|
||||
}
|
||||
while (!done || queue.length > 0) {
|
||||
if (queue.length > 0) {
|
||||
yield queue.shift()!;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
createBitbucket,
|
||||
findBitbucketById,
|
||||
getAccessibleGitProviderIds,
|
||||
getBitbucketBranches,
|
||||
getBitbucketRepositories,
|
||||
testBitbucketConnection,
|
||||
@@ -54,6 +55,8 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
return await findBitbucketById(input.bitbucketId);
|
||||
}),
|
||||
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.bitbucket.findMany({
|
||||
with: {
|
||||
gitProvider: true,
|
||||
@@ -67,7 +70,7 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
return (
|
||||
provider.gitProvider.organizationId ===
|
||||
ctx.session.activeOrganizationId &&
|
||||
provider.gitProvider.userId === ctx.session.userId
|
||||
accessibleIds.has(provider.gitProvider.gitProviderId)
|
||||
);
|
||||
});
|
||||
return result;
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { createTRPCRouter, withPermission } from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateCertificate,
|
||||
apiFindCertificate,
|
||||
|
||||
@@ -31,13 +31,13 @@ import {
|
||||
updateCompose,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type CompleteTemplate,
|
||||
fetchTemplateFiles,
|
||||
@@ -75,8 +75,8 @@ import {
|
||||
} from "@/server/queues/queueSetup";
|
||||
import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
import { audit } from "../utils/audit";
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
import { audit } from "../utils/audit";
|
||||
|
||||
export const composeRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
createDestintation,
|
||||
createDestination,
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
findDestinationById,
|
||||
@@ -25,7 +25,7 @@ export const destinationRouter = createTRPCRouter({
|
||||
.input(apiCreateDestination)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const result = await createDestintation(
|
||||
const result = await createDestination(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
@@ -47,8 +47,15 @@ export const destinationRouter = createTRPCRouter({
|
||||
testConnection: withPermission("destination", "create")
|
||||
.input(apiCreateDestination)
|
||||
.mutation(async ({ input }) => {
|
||||
const { secretAccessKey, bucket, region, endpoint, accessKey, provider } =
|
||||
input;
|
||||
const {
|
||||
secretAccessKey,
|
||||
bucket,
|
||||
region,
|
||||
endpoint,
|
||||
accessKey,
|
||||
provider,
|
||||
additionalFlags,
|
||||
} = input;
|
||||
try {
|
||||
const rcloneFlags = [
|
||||
`--s3-access-key-id="${accessKey}"`,
|
||||
@@ -65,6 +72,9 @@ export const destinationRouter = createTRPCRouter({
|
||||
if (provider) {
|
||||
rcloneFlags.unshift(`--s3-provider="${provider}"`);
|
||||
}
|
||||
if (additionalFlags?.length) {
|
||||
rcloneFlags.push(...additionalFlags);
|
||||
}
|
||||
const rcloneDestination = `:s3:${bucket}`;
|
||||
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
|
||||
@@ -159,7 +169,14 @@ export const destinationRouter = createTRPCRouter({
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error?.message
|
||||
: "Error connecting to bucket",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
import { findGitProviderById, removeGitProvider } from "@dokploy/server";
|
||||
import {
|
||||
findGitProviderById,
|
||||
getAccessibleGitProviderIds,
|
||||
removeGitProvider,
|
||||
updateGitProvider,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { desc, eq, inArray } from "drizzle-orm";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiRemoveGitProvider,
|
||||
apiToggleShareGitProvider,
|
||||
gitProvider,
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const gitProviderRouter = createTRPCRouter({
|
||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||
return await db.query.gitProvider.findMany({
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
if (accessibleIds.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results = await db.query.gitProvider.findMany({
|
||||
with: {
|
||||
gitlab: true,
|
||||
bitbucket: true,
|
||||
@@ -20,12 +36,65 @@ export const gitProviderRouter = createTRPCRouter({
|
||||
gitea: true,
|
||||
},
|
||||
orderBy: desc(gitProvider.createdAt),
|
||||
where: and(
|
||||
eq(gitProvider.userId, ctx.session.userId),
|
||||
eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
|
||||
),
|
||||
where: inArray(gitProvider.gitProviderId, [...accessibleIds]),
|
||||
});
|
||||
|
||||
return results.map((r) => ({
|
||||
...r,
|
||||
isOwner: r.userId === ctx.session.userId,
|
||||
}));
|
||||
}),
|
||||
|
||||
toggleShare: protectedProcedure
|
||||
.input(apiToggleShareGitProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const provider = await findGitProviderById(input.gitProviderId);
|
||||
|
||||
if (
|
||||
provider.userId !== ctx.session.userId ||
|
||||
provider.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Only the owner can share this provider",
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "gitProvider",
|
||||
resourceId: provider.gitProviderId,
|
||||
resourceName: provider.name ?? provider.gitProviderId,
|
||||
});
|
||||
|
||||
return await updateGitProvider(input.gitProviderId, {
|
||||
sharedWithOrganization: input.sharedWithOrganization,
|
||||
});
|
||||
}),
|
||||
|
||||
allForPermissions: withPermission("member", "update")
|
||||
.use(async ({ ctx, next }) => {
|
||||
const licensed = await hasValidLicense(ctx.session.activeOrganizationId);
|
||||
if (!licensed) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Valid enterprise license required",
|
||||
});
|
||||
}
|
||||
return next();
|
||||
})
|
||||
.query(async ({ ctx }) => {
|
||||
return await db.query.gitProvider.findMany({
|
||||
columns: {
|
||||
gitProviderId: true,
|
||||
name: true,
|
||||
providerType: true,
|
||||
},
|
||||
orderBy: desc(gitProvider.createdAt),
|
||||
where: eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
|
||||
});
|
||||
}),
|
||||
|
||||
remove: withPermission("gitProviders", "delete")
|
||||
.input(apiRemoveGitProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
createGitea,
|
||||
findGiteaById,
|
||||
getAccessibleGitProviderIds,
|
||||
getGiteaBranches,
|
||||
getGiteaRepositories,
|
||||
haveGiteaRequirements,
|
||||
@@ -57,6 +58,8 @@ export const giteaRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
giteaProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.gitea.findMany({
|
||||
with: {
|
||||
gitProvider: true,
|
||||
@@ -67,7 +70,7 @@ export const giteaRouter = createTRPCRouter({
|
||||
(provider) =>
|
||||
provider.gitProvider.organizationId ===
|
||||
ctx.session.activeOrganizationId &&
|
||||
provider.gitProvider.userId === ctx.session.userId,
|
||||
accessibleIds.has(provider.gitProvider.gitProviderId),
|
||||
);
|
||||
|
||||
const filtered = result
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
findGithubById,
|
||||
getAccessibleGitProviderIds,
|
||||
getGithubBranches,
|
||||
getGithubRepositories,
|
||||
haveGithubRequirements,
|
||||
@@ -35,6 +36,8 @@ export const githubRouter = createTRPCRouter({
|
||||
return await getGithubBranches(input);
|
||||
}),
|
||||
githubProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.github.findMany({
|
||||
with: {
|
||||
gitProvider: true,
|
||||
@@ -45,7 +48,7 @@ export const githubRouter = createTRPCRouter({
|
||||
(provider) =>
|
||||
provider.gitProvider.organizationId ===
|
||||
ctx.session.activeOrganizationId &&
|
||||
provider.gitProvider.userId === ctx.session.userId,
|
||||
accessibleIds.has(provider.gitProvider.gitProviderId),
|
||||
);
|
||||
|
||||
const filtered = result
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
createGitlab,
|
||||
findGitlabById,
|
||||
getAccessibleGitProviderIds,
|
||||
getGitlabBranches,
|
||||
getGitlabRepositories,
|
||||
haveGitlabRequirements,
|
||||
@@ -54,6 +55,8 @@ export const gitlabRouter = createTRPCRouter({
|
||||
return await findGitlabById(input.gitlabId);
|
||||
}),
|
||||
gitlabProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.gitlab.findMany({
|
||||
with: {
|
||||
gitProvider: true,
|
||||
@@ -64,7 +67,7 @@ export const gitlabRouter = createTRPCRouter({
|
||||
return (
|
||||
provider.gitProvider.organizationId ===
|
||||
ctx.session.activeOrganizationId &&
|
||||
provider.gitProvider.userId === ctx.session.userId
|
||||
accessibleIds.has(provider.gitProvider.gitProviderId)
|
||||
);
|
||||
});
|
||||
const filtered = result
|
||||
|
||||
@@ -246,11 +246,15 @@ export const libsqlRouter = createTRPCRouter({
|
||||
deployment: ["create"],
|
||||
});
|
||||
const queue: string[] = [];
|
||||
const done = false;
|
||||
let done = false;
|
||||
|
||||
deployLibsql(input.libsqlId, (log) => {
|
||||
queue.push(log);
|
||||
});
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
done = true;
|
||||
});
|
||||
|
||||
while (!done || queue.length > 0) {
|
||||
if (queue.length > 0) {
|
||||
|
||||
@@ -17,18 +17,18 @@ import {
|
||||
stopServiceRemote,
|
||||
updateMongoById,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiChangeMongoStatus,
|
||||
apiCreateMongo,
|
||||
@@ -39,9 +39,10 @@ import {
|
||||
apiSaveEnvironmentVariablesMongo,
|
||||
apiSaveExternalPortMongo,
|
||||
apiUpdateMongo,
|
||||
environments,
|
||||
mongo as mongoTable,
|
||||
projects,
|
||||
} from "@/server/db/schema";
|
||||
import { environments, projects } from "@/server/db/schema";
|
||||
import { cancelJobs } from "@/server/utils/backup";
|
||||
export const mongoRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
@@ -228,11 +229,15 @@ export const mongoRouter = createTRPCRouter({
|
||||
deployment: ["create"],
|
||||
});
|
||||
const queue: string[] = [];
|
||||
const done = false;
|
||||
let done = false;
|
||||
|
||||
deployMongo(input.mongoId, (log) => {
|
||||
queue.push(log);
|
||||
});
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
done = true;
|
||||
});
|
||||
|
||||
while (!done || queue.length > 0) {
|
||||
if (queue.length > 0) {
|
||||
|
||||
@@ -17,18 +17,18 @@ import {
|
||||
stopServiceRemote,
|
||||
updateMySqlById,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiChangeMySqlStatus,
|
||||
apiCreateMySql,
|
||||
@@ -230,11 +230,15 @@ export const mysqlRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
const queue: string[] = [];
|
||||
const done = false;
|
||||
let done = false;
|
||||
|
||||
deployMySql(input.mysqlId, (log) => {
|
||||
queue.push(log);
|
||||
});
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
done = true;
|
||||
});
|
||||
|
||||
while (!done || queue.length > 0) {
|
||||
if (queue.length > 0) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { IS_CLOUD } from "@dokploy/server/index";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, exists } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
invitation,
|
||||
member,
|
||||
@@ -409,11 +409,11 @@ export const organizationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Owner role is intransferible - cannot change to or from owner
|
||||
// Owner role is nontransferable - cannot change to or from owner
|
||||
if (target.role === "owner" || input.role === "owner") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "The owner role is intransferible",
|
||||
message: "The owner role is nontransferable",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,18 +18,18 @@ import {
|
||||
stopServiceRemote,
|
||||
updatePostgresById,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiChangePostgresStatus,
|
||||
apiCreatePostgres,
|
||||
@@ -40,9 +40,10 @@ import {
|
||||
apiSaveEnvironmentVariablesPostgres,
|
||||
apiSaveExternalPortPostgres,
|
||||
apiUpdatePostgres,
|
||||
environments,
|
||||
postgres as postgresTable,
|
||||
projects,
|
||||
} from "@/server/db/schema";
|
||||
import { environments, projects } from "@/server/db/schema";
|
||||
import { cancelJobs } from "@/server/utils/backup";
|
||||
|
||||
export const postgresRouter = createTRPCRouter({
|
||||
@@ -233,11 +234,15 @@ export const postgresRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
const queue: string[] = [];
|
||||
const done = false;
|
||||
let done = false;
|
||||
|
||||
deployPostgres(input.postgresId, (log) => {
|
||||
queue.push(log);
|
||||
});
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
done = true;
|
||||
});
|
||||
|
||||
while (!done || queue.length > 0) {
|
||||
if (queue.length > 0) {
|
||||
|
||||
@@ -143,7 +143,12 @@ export const licenseKeyRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await deactivateLicenseKey(currentUser.licenseKey);
|
||||
try {
|
||||
await deactivateLicenseKey(currentUser.licenseKey);
|
||||
} catch (err) {
|
||||
console.error("Failed to deactivate license key remotely:", err);
|
||||
}
|
||||
|
||||
await db
|
||||
.update(user)
|
||||
.set({
|
||||
|
||||
@@ -16,18 +16,18 @@ import {
|
||||
stopServiceRemote,
|
||||
updateRedisById,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiChangeRedisStatus,
|
||||
apiCreateRedis,
|
||||
@@ -38,9 +38,10 @@ import {
|
||||
apiSaveEnvironmentVariablesRedis,
|
||||
apiSaveExternalPortRedis,
|
||||
apiUpdateRedis,
|
||||
environments,
|
||||
projects,
|
||||
redis as redisTable,
|
||||
} from "@/server/db/schema";
|
||||
import { environments, projects } from "@/server/db/schema";
|
||||
export const redisRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateRedis)
|
||||
@@ -251,11 +252,15 @@ export const redisRouter = createTRPCRouter({
|
||||
deployment: ["create"],
|
||||
});
|
||||
const queue: string[] = [];
|
||||
const done = false;
|
||||
let done = false;
|
||||
|
||||
deployRedis(input.redisId, (log) => {
|
||||
queue.push(log);
|
||||
});
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
done = true;
|
||||
});
|
||||
|
||||
while (!done || queue.length > 0) {
|
||||
if (queue.length > 0) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateRegistry,
|
||||
apiFindOneRegistry,
|
||||
@@ -19,7 +20,6 @@ import {
|
||||
apiUpdateRegistry,
|
||||
registry,
|
||||
} from "@/server/db/schema";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { createTRPCRouter, withPermission } from "../trpc";
|
||||
export const registryRouter = createTRPCRouter({
|
||||
create: withPermission("registry", "create")
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
updateScheduleSchema,
|
||||
} from "@dokploy/server/db/schema/schedule";
|
||||
import { runCommand } from "@dokploy/server/index";
|
||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||
import {
|
||||
createSchedule,
|
||||
deleteSchedule,
|
||||
@@ -14,11 +15,10 @@ import {
|
||||
updateSchedule,
|
||||
} from "@dokploy/server/services/schedule";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { asc, desc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { removeJob, schedule } from "@/server/utils/backup";
|
||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
export const scheduleRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
@@ -157,6 +157,7 @@ export const scheduleRouter = createTRPCRouter({
|
||||
};
|
||||
return db.query.schedules.findMany({
|
||||
where: where[input.scheduleType],
|
||||
orderBy: [asc(schedules.createdAt)],
|
||||
with: {
|
||||
application: true,
|
||||
server: true,
|
||||
|
||||
@@ -21,12 +21,12 @@ import { observable } from "@trpc/server/observable";
|
||||
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { updateServersBasedOnQuantity } from "@/pages/api/stripe/webhook";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateServer,
|
||||
apiFindOneServer,
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { createTRPCRouter, withPermission } from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateSshKey,
|
||||
apiFindOneSshKey,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
updateUser,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
||||
import {
|
||||
account,
|
||||
apiAssignPermissions,
|
||||
@@ -344,12 +345,19 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const { id, ...rest } = input;
|
||||
const { id, accessedGitProviders, ...rest } = input;
|
||||
|
||||
const licensed = await hasValidLicense(
|
||||
ctx.session?.activeOrganizationId || "",
|
||||
);
|
||||
|
||||
await db
|
||||
.update(member)
|
||||
.set({
|
||||
...rest,
|
||||
...(licensed && accessedGitProviders !== undefined
|
||||
? { accessedGitProviders }
|
||||
: {}),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
|
||||
@@ -106,7 +106,7 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
||||
* 2. INITIALIZATION
|
||||
*
|
||||
* This is where the tRPC API is initialized, connecting the context and transformer. We also parse
|
||||
* ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
|
||||
* ZodErrors so that you get type safety on the frontend if your procedure fails due to validation
|
||||
* errors on the backend.
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createAuditLog } from "@dokploy/server/services/proprietary/audit-log";
|
||||
import type { AuditAction, AuditResourceType } from "@dokploy/server/db/schema";
|
||||
import { createAuditLog } from "@dokploy/server/services/proprietary/audit-log";
|
||||
|
||||
interface AuditCtx {
|
||||
user: { id: string; email: string; role: string };
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
boolean,
|
||||
index,
|
||||
integer,
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
integer,
|
||||
index,
|
||||
uniqueIndex,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
|
||||
@@ -163,6 +163,10 @@ export const member = pgTable("member", {
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
accessedGitProviders: text("accessedGitProviders")
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
});
|
||||
|
||||
export const memberRelations = relations(member, ({ one }) => ({
|
||||
|
||||
@@ -3,6 +3,10 @@ import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
ADDITIONAL_FLAG_ERROR,
|
||||
ADDITIONAL_FLAG_REGEX,
|
||||
} from "../validations/destination";
|
||||
import { organization } from "./account";
|
||||
import { backups } from "./backups";
|
||||
|
||||
@@ -18,6 +22,7 @@ export const destinations = pgTable("destination", {
|
||||
bucket: text("bucket").notNull(),
|
||||
region: text("region").notNull(),
|
||||
endpoint: text("endpoint").notNull(),
|
||||
additionalFlags: text("additionalFlags").array(),
|
||||
organizationId: text("organizationId")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
@@ -44,6 +49,9 @@ const createSchema = createInsertSchema(destinations, {
|
||||
endpoint: z.string(),
|
||||
secretAccessKey: z.string(),
|
||||
region: z.string(),
|
||||
additionalFlags: z
|
||||
.array(z.string().regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR))
|
||||
.default([]),
|
||||
});
|
||||
|
||||
export const apiCreateDestination = createSchema
|
||||
@@ -55,6 +63,7 @@ export const apiCreateDestination = createSchema
|
||||
region: true,
|
||||
endpoint: true,
|
||||
secretAccessKey: true,
|
||||
additionalFlags: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
@@ -81,6 +90,7 @@ export const apiUpdateDestination = createSchema
|
||||
secretAccessKey: true,
|
||||
destinationId: true,
|
||||
provider: true,
|
||||
additionalFlags: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
|
||||
@@ -31,6 +31,7 @@ export const domains = pgTable("domain", {
|
||||
host: text("host").notNull(),
|
||||
https: boolean("https").notNull().default(false),
|
||||
port: integer("port").default(3000),
|
||||
customEntrypoint: text("customEntrypoint"),
|
||||
path: text("path").default("/"),
|
||||
serviceName: text("serviceName"),
|
||||
domainType: domainType("domainType").default("application"),
|
||||
@@ -80,6 +81,7 @@ export const apiCreateDomain = createSchema.pick({
|
||||
host: true,
|
||||
path: true,
|
||||
port: true,
|
||||
customEntrypoint: true,
|
||||
https: true,
|
||||
applicationId: true,
|
||||
certificateType: true,
|
||||
@@ -113,6 +115,7 @@ export const apiUpdateDomain = createSchema
|
||||
host: true,
|
||||
path: true,
|
||||
port: true,
|
||||
customEntrypoint: true,
|
||||
https: true,
|
||||
certificateType: true,
|
||||
customCertResolver: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { organization } from "./account";
|
||||
@@ -32,6 +32,9 @@ export const gitProvider = pgTable("git_provider", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
sharedWithOrganization: boolean("sharedWithOrganization")
|
||||
.notNull()
|
||||
.default(false),
|
||||
});
|
||||
|
||||
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
@@ -64,3 +67,8 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
export const apiRemoveGitProvider = z.object({
|
||||
gitProviderId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiToggleShareGitProvider = z.object({
|
||||
gitProviderId: z.string().min(1),
|
||||
sharedWithOrganization: z.boolean(),
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * from "./account";
|
||||
export * from "./ai";
|
||||
export * from "./audit-log";
|
||||
export * from "./application";
|
||||
export * from "./audit-log";
|
||||
export * from "./backups";
|
||||
export * from "./bitbucket";
|
||||
export * from "./certificate";
|
||||
|
||||
@@ -179,11 +179,9 @@ export const apiCreateLibsql = createSchema
|
||||
}
|
||||
});
|
||||
|
||||
export const apiFindOneLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOneLibsql = z.object({
|
||||
libsqlId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiChangeLibsqlStatus = createSchema
|
||||
.pick({
|
||||
|
||||
@@ -149,14 +149,13 @@ export const apiRemoveMount = createSchema
|
||||
// })
|
||||
.required();
|
||||
|
||||
export const apiFindMountByApplicationId = createSchema
|
||||
.pick({
|
||||
serviceType: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
serviceId: z.string().min(1),
|
||||
});
|
||||
export const apiFindMountByApplicationId = z.object({
|
||||
serviceType: z
|
||||
.string()
|
||||
.min(1)
|
||||
.transform((val) => val as ServiceType),
|
||||
serviceId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiUpdateMount = createSchema.partial().extend({
|
||||
mountId: z.string().min(1),
|
||||
|
||||
@@ -38,6 +38,7 @@ export const notifications = pgTable("notification", {
|
||||
databaseBackup: boolean("databaseBackup").notNull().default(false),
|
||||
volumeBackup: boolean("volumeBackup").notNull().default(false),
|
||||
dokployRestart: boolean("dokployRestart").notNull().default(false),
|
||||
dokployBackup: boolean("dokployBackup").notNull().default(false),
|
||||
dockerCleanup: boolean("dockerCleanup").notNull().default(false),
|
||||
serverThreshold: boolean("serverThreshold").notNull().default(false),
|
||||
notificationType: notificationType("notificationType").notNull(),
|
||||
@@ -266,6 +267,7 @@ export const apiCreateSlack = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -294,6 +296,7 @@ export const apiCreateTelegram = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -324,6 +327,7 @@ export const apiCreateDiscord = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -355,6 +359,7 @@ export const apiCreateEmail = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -391,6 +396,7 @@ export const apiCreateResend = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -421,6 +427,7 @@ export const apiCreateGotify = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -455,6 +462,7 @@ export const apiCreateNtfy = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -486,6 +494,7 @@ export const apiCreateMattermost = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -503,6 +512,7 @@ export const apiCreateMattermost = notificationsSchema
|
||||
webhookUrl: true,
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
appDeploy: true,
|
||||
@@ -535,6 +545,7 @@ export const apiCreateCustom = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -562,6 +573,7 @@ export const apiCreateLark = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -588,6 +600,7 @@ export const apiCreateTeams = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -614,6 +627,7 @@ export const apiCreatePushover = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
@@ -648,6 +662,7 @@ export const apiUpdatePushover = z.object({
|
||||
expire: z.number().min(1).max(10800).nullish(),
|
||||
appBuildError: z.boolean().optional(),
|
||||
databaseBackup: z.boolean().optional(),
|
||||
dokployBackup: z.boolean().optional(),
|
||||
volumeBackup: z.boolean().optional(),
|
||||
dokployRestart: z.boolean().optional(),
|
||||
name: z.string().optional(),
|
||||
|
||||
@@ -126,6 +126,7 @@ export const apiAssignPermissions = createSchema
|
||||
accessedProjects: z.array(z.string()).optional(),
|
||||
accessedEnvironments: z.array(z.string()).optional(),
|
||||
accessedServices: z.array(z.string()).optional(),
|
||||
accessedGitProviders: z.array(z.string()).optional(),
|
||||
canCreateProjects: z.boolean().optional(),
|
||||
canCreateServices: z.boolean().optional(),
|
||||
canDeleteProjects: z.boolean().optional(),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { serviceType } from "./mount";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@@ -11,6 +10,7 @@ import { destinations } from "./destination";
|
||||
import { libsql } from "./libsql";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { serviceType } from "./mount";
|
||||
import { mysql } from "./mysql";
|
||||
import { postgres } from "./postgres";
|
||||
import { redis } from "./redis";
|
||||
|
||||
3
packages/server/src/db/validations/destination.ts
Normal file
3
packages/server/src/db/validations/destination.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const ADDITIONAL_FLAG_REGEX = /^--[a-zA-Z0-9-]+(=[a-zA-Z0-9._:/@-]+)?$/;
|
||||
export const ADDITIONAL_FLAG_ERROR =
|
||||
"Invalid flag format. Must start with -- (e.g. --s3-sign-accept-encoding=false)";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user