Compare commits

..

1 Commits

Author SHA1 Message Date
Mauricio Siu
cd8230b0e5 refactor(add-domain): remove debug log statement from AddDomain component 2025-04-06 11:14:43 -06:00
80 changed files with 533 additions and 12342 deletions

View File

@@ -52,7 +52,7 @@ feat: add new feature
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch. Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory. We use Node v20.9.0
```bash ```bash
git clone https://github.com/dokploy/dokploy.git git clone https://github.com/dokploy/dokploy.git
@@ -87,8 +87,6 @@ pnpm run dokploy:dev
Go to http://localhost:3000 to see the development server Go to http://localhost:3000 to see the development server
Note: this project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off.
## Build ## Build
```bash ```bash
@@ -147,9 +145,11 @@ curl -sSL https://railpack.com/install.sh | sh
```bash ```bash
# Install Buildpacks # Install Buildpacks
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
``` ```
## Pull Request ## Pull Request
- The `main` branch is the source of truth and should always reflect the latest stable release. - The `main` branch is the source of truth and should always reflect the latest stable release.
@@ -167,6 +167,7 @@ Thank you for your contribution!
To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file. To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file.
### Recommendations ### Recommendations
- Use the same name of the folder as the id of the template. - Use the same name of the folder as the id of the template.

View File

@@ -49,7 +49,7 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
# Install Nixpacks and tsx # Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash # | VERBOSE=1 VERSION=1.21.0 bash
ARG NIXPACKS_VERSION=1.35.0 ARG NIXPACKS_VERSION=1.29.1
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& chmod +x install.sh \ && chmod +x install.sh \
&& ./install.sh \ && ./install.sh \

View File

@@ -34,7 +34,6 @@ const baseApp: ApplicationNested = {
giteaRepository: "", giteaRepository: "",
cleanCache: false, cleanCache: false,
watchPaths: [], watchPaths: [],
enableSubmodules: false,
applicationStatus: "done", applicationStatus: "done",
appName: "", appName: "",
autoDeploy: true, autoDeploy: true,

View File

@@ -51,35 +51,6 @@ describe("processTemplate", () => {
expect(result.domains).toHaveLength(0); expect(result.domains).toHaveLength(0);
expect(result.mounts).toHaveLength(0); expect(result.mounts).toHaveLength(0);
}); });
it("should allow creation of real jwt secret", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {
jwt_secret: "cQsdycq1hDLopQonF6jUTqgQc5WEZTwWLL02J6XJ",
anon_payload: JSON.stringify({
role: "tester",
iss: "dockploy",
iat: "${timestamps:2025-01-01T00:00:00Z}",
exp: "${timestamps:2030-01-01T00:00:00Z}",
}),
anon_key: "${jwt:jwt_secret:anon_payload}",
},
config: {
domains: [],
env: {
ANON_KEY: "${anon_key}",
},
},
};
const result = processTemplate(template, mockSchema);
expect(result.envs).toHaveLength(1);
expect(result.envs).toContain(
"ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNzM1Njg5NjAwIiwiZXhwIjoiMTg5MzQ1NjAwMCIsInJvbGUiOiJ0ZXN0ZXIiLCJpc3MiOiJkb2NrcGxveSJ9.BG5JoxL2_NaTFbPgyZdm3kRWenf_O3su_HIRKGCJ_kY",
);
expect(result.mounts).toHaveLength(0);
expect(result.domains).toHaveLength(0);
});
}); });
describe("domains processing", () => { describe("domains processing", () => {

View File

@@ -1,232 +0,0 @@
import type { Schema } from "@dokploy/server/templates";
import { processValue } from "@dokploy/server/templates/processors";
import { describe, expect, it } from "vitest";
describe("helpers functions", () => {
// Mock schema for testing
const mockSchema: Schema = {
projectName: "test",
serverIp: "127.0.0.1",
};
// some helpers to test jwt
type JWTParts = [string, string, string];
const jwtMatchExp = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/;
const jwtBase64Decode = (str: string) => {
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
const padding = "=".repeat((4 - (base64.length % 4)) % 4);
const decoded = Buffer.from(base64 + padding, "base64").toString("utf-8");
return JSON.parse(decoded);
};
const jwtCheckHeader = (jwtHeader: string) => {
const decodedHeader = jwtBase64Decode(jwtHeader);
expect(decodedHeader).toHaveProperty("alg");
expect(decodedHeader).toHaveProperty("typ");
expect(decodedHeader.alg).toEqual("HS256");
expect(decodedHeader.typ).toEqual("JWT");
};
describe("${domain}", () => {
it("should generate a random domain", () => {
const domain = processValue("${domain}", {}, mockSchema);
expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy();
expect(
domain.endsWith(
`${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`,
),
).toBeTruthy();
});
});
describe("${base64}", () => {
it("should generate a base64 string", () => {
const base64 = processValue("${base64}", {}, mockSchema);
expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/);
});
it.each([
[4, 8],
[8, 12],
[16, 24],
[32, 44],
[64, 88],
[128, 172],
])(
"should generate a base64 string from parameter %d bytes length",
(length, finalLength) => {
const base64 = processValue(`\${base64:${length}}`, {}, mockSchema);
expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/);
expect(base64.length).toBe(finalLength);
},
);
});
describe("${password}", () => {
it("should generate a password string", () => {
const password = processValue("${password}", {}, mockSchema);
expect(password).toMatch(/^[A-Za-z0-9]+$/);
});
it.each([6, 8, 12, 16, 32])(
"should generate a password string respecting parameter %d length",
(length) => {
const password = processValue(`\${password:${length}}`, {}, mockSchema);
expect(password).toMatch(/^[A-Za-z0-9]+$/);
expect(password.length).toBe(length);
},
);
});
describe("${hash}", () => {
it("should generate a hash string", () => {
const hash = processValue("${hash}", {}, mockSchema);
expect(hash).toMatch(/^[A-Za-z0-9]+$/);
});
it.each([6, 8, 12, 16, 32])(
"should generate a hash string respecting parameter %d length",
(length) => {
const hash = processValue(`\${hash:${length}}`, {}, mockSchema);
expect(hash).toMatch(/^[A-Za-z0-9]+$/);
expect(hash.length).toBe(length);
},
);
});
describe("${uuid}", () => {
it("should generate a UUID string", () => {
const uuid = processValue("${uuid}", {}, mockSchema);
expect(uuid).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
);
});
});
describe("${timestamp}", () => {
it("should generate a timestamp string in milliseconds", () => {
const timestamp = processValue("${timestamp}", {}, mockSchema);
const nowLength = Math.floor(Date.now()).toString().length;
expect(timestamp).toMatch(/^\d+$/);
expect(timestamp.length).toBe(nowLength);
});
});
describe("${timestampms}", () => {
it("should generate a timestamp string in milliseconds", () => {
const timestamp = processValue("${timestampms}", {}, mockSchema);
const nowLength = Date.now().toString().length;
expect(timestamp).toMatch(/^\d+$/);
expect(timestamp.length).toBe(nowLength);
});
it("should generate a timestamp string in milliseconds from parameter", () => {
const timestamp = processValue(
"${timestampms:2025-01-01}",
{},
mockSchema,
);
expect(timestamp).toEqual("1735689600000");
});
});
describe("${timestamps}", () => {
it("should generate a timestamp string in seconds", () => {
const timestamps = processValue("${timestamps}", {}, mockSchema);
const nowLength = Math.floor(Date.now() / 1000).toString().length;
expect(timestamps).toMatch(/^\d+$/);
expect(timestamps.length).toBe(nowLength);
});
it("should generate a timestamp string in seconds from parameter", () => {
const timestamps = processValue(
"${timestamps:2025-01-01}",
{},
mockSchema,
);
expect(timestamps).toEqual("1735689600");
});
});
describe("${randomPort}", () => {
it("should generate a random port string", () => {
const randomPort = processValue("${randomPort}", {}, mockSchema);
expect(randomPort).toMatch(/^\d+$/);
expect(Number(randomPort)).toBeLessThan(65536);
});
});
describe("${username}", () => {
it("should generate a username string", () => {
const username = processValue("${username}", {}, mockSchema);
expect(username).toMatch(/^[a-zA-Z0-9._-]{3,}$/);
});
});
describe("${email}", () => {
it("should generate an email string", () => {
const email = processValue("${email}", {}, mockSchema);
expect(email).toMatch(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/);
});
});
describe("${jwt}", () => {
it("should generate a JWT string", () => {
const jwt = processValue("${jwt}", {}, mockSchema);
expect(jwt).toMatch(jwtMatchExp);
const parts = jwt.split(".") as JWTParts;
const decodedPayload = jwtBase64Decode(parts[1]);
jwtCheckHeader(parts[0]);
expect(decodedPayload).toHaveProperty("iat");
expect(decodedPayload).toHaveProperty("iss");
expect(decodedPayload).toHaveProperty("exp");
expect(decodedPayload.iss).toEqual("dokploy");
});
it.each([6, 8, 12, 16, 32])(
"should generate a random hex string from parameter %d byte length",
(length) => {
const jwt = processValue(`\${jwt:${length}}`, {}, mockSchema);
expect(jwt).toMatch(/^[A-Za-z0-9-_.]+$/);
expect(jwt.length).toBeGreaterThanOrEqual(length); // bytes translated to hex can take up to 2x the length
expect(jwt.length).toBeLessThanOrEqual(length * 2);
},
);
});
describe("${jwt:secret}", () => {
it("should generate a JWT string respecting parameter secret from variable", () => {
const jwt = processValue(
"${jwt:secret}",
{ secret: "mysecret" },
mockSchema,
);
expect(jwt).toMatch(jwtMatchExp);
const parts = jwt.split(".") as JWTParts;
const decodedPayload = jwtBase64Decode(parts[1]);
jwtCheckHeader(parts[0]);
expect(decodedPayload).toHaveProperty("iat");
expect(decodedPayload).toHaveProperty("iss");
expect(decodedPayload).toHaveProperty("exp");
expect(decodedPayload.iss).toEqual("dokploy");
});
});
describe("${jwt:secret:payload}", () => {
it("should generate a JWT string respecting parameters secret and payload from variables", () => {
const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000);
const expiry = iat + 3600;
const jwt = processValue(
"${jwt:secret:payload}",
{
secret: "mysecret",
payload: `{"iss": "test-issuer", "iat": ${iat}, "exp": ${expiry}, "customprop": "customvalue"}`,
},
mockSchema,
);
expect(jwt).toMatch(jwtMatchExp);
const parts = jwt.split(".") as JWTParts;
jwtCheckHeader(parts[0]);
const decodedPayload = jwtBase64Decode(parts[1]);
expect(decodedPayload).toHaveProperty("iat");
expect(decodedPayload.iat).toEqual(iat);
expect(decodedPayload).toHaveProperty("iss");
expect(decodedPayload.iss).toEqual("test-issuer");
expect(decodedPayload).toHaveProperty("exp");
expect(decodedPayload.exp).toEqual(expiry);
expect(decodedPayload).toHaveProperty("customprop");
expect(decodedPayload.customprop).toEqual("customvalue");
expect(jwt).toEqual(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI",
);
});
});
});

View File

@@ -16,7 +16,6 @@ const baseApp: ApplicationNested = {
applicationStatus: "done", applicationStatus: "done",
appName: "", appName: "",
autoDeploy: true, autoDeploy: true,
enableSubmodules: false,
serverId: "", serverId: "",
branch: null, branch: null,
dockerBuildStage: "", dockerBuildStage: "",

View File

@@ -89,8 +89,6 @@ export const AddDomain = ({
serverId: application?.serverId || "", serverId: application?.serverId || "",
}); });
console.log("canGenerateTraefikMeDomains", canGenerateTraefikMeDomains);
const form = useForm<Domain>({ const form = useForm<Domain>({
resolver: zodResolver(domain), resolver: zodResolver(domain),
defaultValues: { defaultValues: {

View File

@@ -31,7 +31,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -59,7 +58,6 @@ const BitbucketProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().optional(),
}); });
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>; type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
@@ -86,7 +84,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
bitbucketId: "", bitbucketId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(BitbucketProviderSchema), resolver: zodResolver(BitbucketProviderSchema),
}); });
@@ -133,7 +130,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
buildPath: data.bitbucketBuildPath || "/", buildPath: data.bitbucketBuildPath || "/",
bitbucketId: data.bitbucketId || "", bitbucketId: data.bitbucketId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules || false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -147,7 +143,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
bitbucketId: data.bitbucketId, bitbucketId: data.bitbucketId,
applicationId, applicationId,
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules || false,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@@ -472,21 +467,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@@ -23,7 +23,6 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon, X } from "lucide-react"; import { KeyRoundIcon, LockIcon, X } from "lucide-react";
@@ -45,7 +44,6 @@ const GitProviderSchema = z.object({
branch: z.string().min(1, "Branch required"), branch: z.string().min(1, "Branch required"),
sshKey: z.string().optional(), sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GitProvider = z.infer<typeof GitProviderSchema>; type GitProvider = z.infer<typeof GitProviderSchema>;
@@ -69,7 +67,6 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
repositoryURL: "", repositoryURL: "",
sshKey: undefined, sshKey: undefined,
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GitProviderSchema), resolver: zodResolver(GitProviderSchema),
}); });
@@ -82,7 +79,6 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
buildPath: data.customGitBuildPath || "/", buildPath: data.customGitBuildPath || "/",
repositoryURL: data.customGitUrl || "", repositoryURL: data.customGitUrl || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -95,7 +91,6 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey, customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey,
applicationId, applicationId,
watchPaths: values.watchPaths || [], watchPaths: values.watchPaths || [],
enableSubmodules: values.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Git Provider Saved"); toast.success("Git Provider Saved");
@@ -299,22 +294,6 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex flex-row justify-end"> <div className="flex flex-row justify-end">

View File

@@ -31,7 +31,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -75,7 +74,6 @@ const GiteaProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"), giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).default([]), watchPaths: z.array(z.string()).default([]),
enableSubmodules: z.boolean().optional(),
}); });
type GiteaProvider = z.infer<typeof GiteaProviderSchema>; type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
@@ -101,7 +99,6 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
giteaId: "", giteaId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GiteaProviderSchema), resolver: zodResolver(GiteaProviderSchema),
}); });
@@ -155,7 +152,6 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
buildPath: data.giteaBuildPath || "/", buildPath: data.giteaBuildPath || "/",
giteaId: data.giteaId || "", giteaId: data.giteaId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules || false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -169,7 +165,6 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
giteaId: data.giteaId, giteaId: data.giteaId,
applicationId, applicationId,
watchPaths: data.watchPaths, watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules || false,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provider Saved"); toast.success("Service Provider Saved");
@@ -503,21 +498,6 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@@ -30,7 +30,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -58,7 +57,6 @@ const GithubProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
githubId: z.string().min(1, "Github Provider is required"), githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GithubProvider = z.infer<typeof GithubProviderSchema>; type GithubProvider = z.infer<typeof GithubProviderSchema>;
@@ -83,7 +81,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
}, },
githubId: "", githubId: "",
branch: "", branch: "",
enableSubmodules: false,
}, },
resolver: zodResolver(GithubProviderSchema), resolver: zodResolver(GithubProviderSchema),
}); });
@@ -127,7 +124,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
buildPath: data.buildPath || "/", buildPath: data.buildPath || "/",
githubId: data.githubId || "", githubId: data.githubId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -141,7 +137,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
buildPath: data.buildPath, buildPath: data.buildPath,
githubId: data.githubId, githubId: data.githubId,
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@@ -463,22 +458,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@@ -31,7 +31,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -61,7 +60,6 @@ const GitlabProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
gitlabId: z.string().min(1, "Gitlab Provider is required"), gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GitlabProvider = z.infer<typeof GitlabProviderSchema>; type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
@@ -88,7 +86,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
}, },
gitlabId: "", gitlabId: "",
branch: "", branch: "",
enableSubmodules: false,
}, },
resolver: zodResolver(GitlabProviderSchema), resolver: zodResolver(GitlabProviderSchema),
}); });
@@ -138,7 +135,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
buildPath: data.gitlabBuildPath || "/", buildPath: data.gitlabBuildPath || "/",
gitlabId: data.gitlabId || "", gitlabId: data.gitlabId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -154,7 +150,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
gitlabProjectId: data.repository.id, gitlabProjectId: data.repository.id,
gitlabPathNamespace: data.repository.gitlabPathNamespace, gitlabPathNamespace: data.repository.gitlabPathNamespace,
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@@ -488,21 +483,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@@ -65,7 +65,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
setSab(e as TabState); setSab(e as TabState);
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden"> <TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
<TabsTrigger <TabsTrigger
value="github" value="github"

View File

@@ -298,11 +298,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
}) })
.then(() => { .then(() => {
refetch(); refetch();
toast.success( toast.success("Preview deployments enabled");
checked
? "Preview deployments enabled"
: "Preview deployments disabled",
);
}) })
.catch((error) => { .catch((error) => {
toast.error(error.message); toast.error(error.message);

View File

@@ -79,22 +79,6 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
toast.error("Error updating the Compose config"); toast.error("Error updating the Compose config");
}); });
}; };
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [form, onSubmit, isLoading]);
return ( return (
<> <>
<div className="w-full flex flex-col gap-4 "> <div className="w-full flex flex-col gap-4 ">

View File

@@ -31,7 +31,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -59,7 +58,6 @@ const BitbucketProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>; type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
@@ -86,7 +84,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
bitbucketId: "", bitbucketId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(BitbucketProviderSchema), resolver: zodResolver(BitbucketProviderSchema),
}); });
@@ -133,7 +130,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath, composePath: data.composePath,
bitbucketId: data.bitbucketId || "", bitbucketId: data.bitbucketId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -149,7 +145,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
sourceType: "bitbucket", sourceType: "bitbucket",
composeStatus: "idle", composeStatus: "idle",
watchPaths: data.watchPaths, watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@@ -474,21 +469,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@@ -19,7 +19,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -44,7 +43,6 @@ const GitProviderSchema = z.object({
branch: z.string().min(1, "Branch required"), branch: z.string().min(1, "Branch required"),
sshKey: z.string().optional(), sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GitProvider = z.infer<typeof GitProviderSchema>; type GitProvider = z.infer<typeof GitProviderSchema>;
@@ -67,7 +65,6 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
composePath: "./docker-compose.yml", composePath: "./docker-compose.yml",
sshKey: undefined, sshKey: undefined,
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GitProviderSchema), resolver: zodResolver(GitProviderSchema),
}); });
@@ -80,7 +77,6 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
repositoryURL: data.customGitUrl || "", repositoryURL: data.customGitUrl || "",
composePath: data.composePath, composePath: data.composePath,
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -95,7 +91,6 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
composePath: values.composePath, composePath: values.composePath,
composeStatus: "idle", composeStatus: "idle",
watchPaths: values.watchPaths || [], watchPaths: values.watchPaths || [],
enableSubmodules: values.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Git Provider Saved"); toast.success("Git Provider Saved");
@@ -300,21 +295,6 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex flex-row justify-end"> <div className="flex flex-row justify-end">

View File

@@ -31,7 +31,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -60,7 +59,6 @@ const GiteaProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"), giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GiteaProvider = z.infer<typeof GiteaProviderSchema>; type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
@@ -85,7 +83,6 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
giteaId: "", giteaId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GiteaProviderSchema), resolver: zodResolver(GiteaProviderSchema),
}); });
@@ -139,7 +136,6 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath || "./docker-compose.yml", composePath: data.composePath || "./docker-compose.yml",
giteaId: data.giteaId || "", giteaId: data.giteaId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -155,7 +151,6 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
sourceType: "gitea", sourceType: "gitea",
composeStatus: "idle", composeStatus: "idle",
watchPaths: data.watchPaths, watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
} as any) } as any)
.then(async () => { .then(async () => {
toast.success("Service Provider Saved"); toast.success("Service Provider Saved");
@@ -474,21 +469,6 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">

View File

@@ -30,7 +30,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -58,7 +57,6 @@ const GithubProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
githubId: z.string().min(1, "Github Provider is required"), githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GithubProvider = z.infer<typeof GithubProviderSchema>; type GithubProvider = z.infer<typeof GithubProviderSchema>;
@@ -84,7 +82,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
githubId: "", githubId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GithubProviderSchema), resolver: zodResolver(GithubProviderSchema),
}); });
@@ -128,7 +125,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath, composePath: data.composePath,
githubId: data.githubId || "", githubId: data.githubId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -144,7 +140,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
sourceType: "github", sourceType: "github",
composeStatus: "idle", composeStatus: "idle",
watchPaths: data.watchPaths, watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@@ -465,21 +460,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@@ -31,7 +31,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -61,7 +60,6 @@ const GitlabProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
gitlabId: z.string().min(1, "Gitlab Provider is required"), gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GitlabProvider = z.infer<typeof GitlabProviderSchema>; type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
@@ -89,7 +87,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
gitlabId: "", gitlabId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GitlabProviderSchema), resolver: zodResolver(GitlabProviderSchema),
}); });
@@ -139,7 +136,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath, composePath: data.composePath,
gitlabId: data.gitlabId || "", gitlabId: data.gitlabId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -157,7 +153,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
sourceType: "gitlab", sourceType: "gitlab",
composeStatus: "idle", composeStatus: "idle",
watchPaths: data.watchPaths, watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@@ -490,21 +485,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@@ -77,14 +77,6 @@ const RestoreBackupSchema = z.object({
type RestoreBackup = z.infer<typeof RestoreBackupSchema>; type RestoreBackup = z.infer<typeof RestoreBackupSchema>;
const formatBytes = (bytes: number): string => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
};
export const RestoreBackup = ({ export const RestoreBackup = ({
databaseId, databaseId,
databaseType, databaseType,
@@ -92,7 +84,6 @@ export const RestoreBackup = ({
}: Props) => { }: Props) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const { data: destinations = [] } = api.destination.all.useQuery(); const { data: destinations = [] } = api.destination.all.useQuery();
@@ -108,18 +99,13 @@ export const RestoreBackup = ({
const destionationId = form.watch("destinationId"); const destionationId = form.watch("destinationId");
const debouncedSetSearch = debounce((value: string) => { const debouncedSetSearch = debounce((value: string) => {
setDebouncedSearchTerm(value);
}, 350);
const handleSearchChange = (value: string) => {
setSearch(value); setSearch(value);
debouncedSetSearch(value); }, 300);
};
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery( const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
{ {
destinationId: destionationId, destinationId: destionationId,
search: debouncedSearchTerm, search,
serverId: serverId ?? "", serverId: serverId ?? "",
}, },
{ {
@@ -279,7 +265,7 @@ export const RestoreBackup = ({
</Badge> </Badge>
)} )}
</FormLabel> </FormLabel>
<Popover modal> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<FormControl> <FormControl>
<Button <Button
@@ -298,8 +284,7 @@ export const RestoreBackup = ({
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search backup files..." placeholder="Search backup files..."
value={search} onValueChange={debouncedSetSearch}
onValueChange={handleSearchChange}
className="h-9" className="h-9"
/> />
{isLoading ? ( {isLoading ? (
@@ -316,51 +301,26 @@ export const RestoreBackup = ({
</div> </div>
) : ( ) : (
<ScrollArea className="h-64"> <ScrollArea className="h-64">
<CommandGroup className="w-96"> <CommandGroup>
{files?.map((file) => ( {files.map((file) => (
<CommandItem <CommandItem
value={file.Path} value={file}
key={file.Path} key={file}
onSelect={() => { onSelect={() => {
form.setValue("backupFile", file.Path); form.setValue("backupFile", file);
if (file.IsDir) {
setSearch(`${file.Path}/`);
setDebouncedSearchTerm(`${file.Path}/`);
} else {
setSearch(file.Path);
setDebouncedSearchTerm(file.Path);
}
}} }}
> >
<div className="flex w-full flex-col gap-1"> <div className="flex w-full justify-between">
<div className="flex w-full justify-between"> <span>{file}</span>
<span className="font-medium">
{file.Path}
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
file.Path === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span>
Size: {formatBytes(file.Size)}
</span>
{file.IsDir && (
<span className="text-blue-500">
Directory
</span>
)}
{file.Hashes?.MD5 && (
<span>MD5: {file.Hashes.MD5}</span>
)}
</div>
</div> </div>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
file === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>

View File

@@ -55,7 +55,7 @@ export const AiForm = () => {
key={config.aiId} key={config.aiId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div> <div>
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{config.name} {config.name}

View File

@@ -70,7 +70,7 @@ export const ShowCertificates = () => {
key={certificate.certificateId} key={certificate.certificateId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex gap-2 flex-col"> <div className="flex gap-2 flex-col">
<span className="text-sm font-medium"> <span className="text-sm font-medium">

View File

@@ -13,65 +13,53 @@ export const extractExpirationDate = (certData: string): Date | null => {
bytes[i] = binaryStr.charCodeAt(i); bytes[i] = binaryStr.charCodeAt(i);
} }
// ASN.1 tag for UTCTime is 0x17, GeneralizedTime is 0x18 let dateFound = 0;
// We need to find the second occurrence of either tag as it's the "not after" (expiration) date
let dateFound = false;
for (let i = 0; i < bytes.length - 2; i++) { for (let i = 0; i < bytes.length - 2; i++) {
// Look for sequence containing validity period (0x30) if (bytes[i] === 0x17 || bytes[i] === 0x18) {
if (bytes[i] === 0x30) { const dateType = bytes[i];
// Check next bytes for UTCTime or GeneralizedTime const dateLength = bytes[i + 1];
let j = i + 1; if (typeof dateLength === "undefined") continue;
while (j < bytes.length - 2) {
if (bytes[j] === 0x17 || bytes[j] === 0x18) {
const dateType = bytes[j];
const dateLength = bytes[j + 1];
if (typeof dateLength === "undefined") break;
if (!dateFound) { if (dateFound === 0) {
// Skip "not before" date dateFound++;
dateFound = true; i += dateLength + 1;
j += dateLength + 2; continue;
continue;
}
// Found "not after" date
let dateStr = "";
for (let k = 0; k < dateLength; k++) {
const charCode = bytes[j + 2 + k];
if (typeof charCode === "undefined") continue;
dateStr += String.fromCharCode(charCode);
}
if (dateType === 0x17) {
// UTCTime (YYMMDDhhmmssZ)
const year = Number.parseInt(dateStr.slice(0, 2));
const fullYear = year >= 50 ? 1900 + year : 2000 + year;
return new Date(
Date.UTC(
fullYear,
Number.parseInt(dateStr.slice(2, 4)) - 1,
Number.parseInt(dateStr.slice(4, 6)),
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
),
);
}
// GeneralizedTime (YYYYMMDDhhmmssZ)
return new Date(
Date.UTC(
Number.parseInt(dateStr.slice(0, 4)),
Number.parseInt(dateStr.slice(4, 6)) - 1,
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
Number.parseInt(dateStr.slice(12, 14)),
),
);
}
j++;
} }
let dateStr = "";
for (let j = 0; j < dateLength; j++) {
const charCode = bytes[i + 2 + j];
if (typeof charCode === "undefined") continue;
dateStr += String.fromCharCode(charCode);
}
if (dateType === 0x17) {
// UTCTime (YYMMDDhhmmssZ)
const year = Number.parseInt(dateStr.slice(0, 2));
const fullYear = year >= 50 ? 1900 + year : 2000 + year;
return new Date(
Date.UTC(
fullYear,
Number.parseInt(dateStr.slice(2, 4)) - 1,
Number.parseInt(dateStr.slice(4, 6)),
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
),
);
}
// GeneralizedTime (YYYYMMDDhhmmssZ)
return new Date(
Date.UTC(
Number.parseInt(dateStr.slice(0, 4)),
Number.parseInt(dateStr.slice(4, 6)) - 1,
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
Number.parseInt(dateStr.slice(12, 14)),
),
);
} }
} }
return null; return null;

View File

@@ -54,7 +54,7 @@ export const ShowRegistry = () => {
key={registry.registryId} key={registry.registryId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex gap-2 flex-col"> <div className="flex gap-2 flex-col">
<span className="text-sm font-medium"> <span className="text-sm font-medium">

View File

@@ -55,7 +55,7 @@ export const ShowDestinations = () => {
key={destination.destinationId} key={destination.destinationId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="text-sm"> <span className="text-sm">
{index + 1}. {destination.name} {index + 1}. {destination.name}

View File

@@ -248,9 +248,7 @@ export const AddGitlabProvider = () => {
name="groupName" name="groupName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>Group Name (Optional)</FormLabel>
Group Name (Optional, Comma-Separated List)
</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="For organization/group access use the slugish name of the group eg: my-org" placeholder="For organization/group access use the slugish name of the group eg: my-org"

View File

@@ -156,9 +156,7 @@ export const EditGitlabProvider = ({ gitlabId }: Props) => {
name="groupName" name="groupName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>Group Name (Optional)</FormLabel>
Group Name (Optional, Comma-Separated List)
</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="For organization/group access use the slugish name of the group eg: my-org" placeholder="For organization/group access use the slugish name of the group eg: my-org"

View File

@@ -61,7 +61,7 @@ export const ShowNotifications = () => {
key={notification.notificationId} key={notification.notificationId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<span className="text-sm flex flex-row items-center gap-4"> <span className="text-sm flex flex-row items-center gap-4">
{notification.notificationType === "slack" && ( {notification.notificationType === "slack" && (
<div className="flex items-center justify-center rounded-lg"> <div className="flex items-center justify-center rounded-lg">

View File

@@ -36,7 +36,6 @@ const PasswordSchema = z.object({
password: z.string().min(8, { password: z.string().min(8, {
message: "Password is required", message: "Password is required",
}), }),
issuer: z.string().optional(),
}); });
const PinSchema = z.object({ const PinSchema = z.object({
@@ -61,86 +60,12 @@ export const Enable2FA = () => {
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [step, setStep] = useState<"password" | "verify">("password"); const [step, setStep] = useState<"password" | "verify">("password");
const [isPasswordLoading, setIsPasswordLoading] = useState(false); const [isPasswordLoading, setIsPasswordLoading] = useState(false);
const [otpValue, setOtpValue] = useState("");
const handleVerifySubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const result = await authClient.twoFactor.verifyTotp({
code: otpValue,
});
if (result.error) {
if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") {
toast.error("Invalid verification code");
return;
}
throw result.error;
}
if (!result.data) {
throw new Error("No response received from server");
}
toast.success("2FA configured successfully");
utils.user.get.invalidate();
setIsDialogOpen(false);
} catch (error) {
if (error instanceof Error) {
const errorMessage =
error.message === "Failed to fetch"
? "Connection error. Please check your internet connection."
: error.message;
toast.error(errorMessage);
} else {
toast.error("Error verifying 2FA code", {
description: error instanceof Error ? error.message : "Unknown error",
});
}
}
};
const passwordForm = useForm<PasswordForm>({
resolver: zodResolver(PasswordSchema),
defaultValues: {
password: "",
},
});
const pinForm = useForm<PinForm>({
resolver: zodResolver(PinSchema),
defaultValues: {
pin: "",
},
});
useEffect(() => {
if (!isDialogOpen) {
setStep("password");
setData(null);
setBackupCodes([]);
setOtpValue("");
passwordForm.reset({
password: "",
issuer: "",
});
}
}, [isDialogOpen, passwordForm]);
useEffect(() => {
if (step === "verify") {
setOtpValue("");
}
}, [step]);
const handlePasswordSubmit = async (formData: PasswordForm) => { const handlePasswordSubmit = async (formData: PasswordForm) => {
setIsPasswordLoading(true); setIsPasswordLoading(true);
try { try {
const { data: enableData, error } = await authClient.twoFactor.enable({ const { data: enableData, error } = await authClient.twoFactor.enable({
password: formData.password, password: formData.password,
issuer: formData.issuer,
}); });
if (!enableData) { if (!enableData) {
@@ -178,6 +103,75 @@ export const Enable2FA = () => {
} }
}; };
const handleVerifySubmit = async (formData: PinForm) => {
try {
const result = await authClient.twoFactor.verifyTotp({
code: formData.pin,
});
if (result.error) {
if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") {
pinForm.setError("pin", {
message: "Invalid code. Please try again.",
});
toast.error("Invalid verification code");
return;
}
throw result.error;
}
if (!result.data) {
throw new Error("No response received from server");
}
toast.success("2FA configured successfully");
utils.user.get.invalidate();
setIsDialogOpen(false);
} catch (error) {
if (error instanceof Error) {
const errorMessage =
error.message === "Failed to fetch"
? "Connection error. Please check your internet connection."
: error.message;
pinForm.setError("pin", {
message: errorMessage,
});
toast.error(errorMessage);
} else {
pinForm.setError("pin", {
message: "Error verifying code",
});
toast.error("Error verifying 2FA code");
}
}
};
const passwordForm = useForm<PasswordForm>({
resolver: zodResolver(PasswordSchema),
defaultValues: {
password: "",
},
});
const pinForm = useForm<PinForm>({
resolver: zodResolver(PinSchema),
defaultValues: {
pin: "",
},
});
useEffect(() => {
if (!isDialogOpen) {
setStep("password");
setData(null);
setBackupCodes([]);
passwordForm.reset();
pinForm.reset();
}
}, [isDialogOpen, passwordForm, pinForm]);
return ( return (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
@@ -223,27 +217,6 @@ export const Enable2FA = () => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={passwordForm.control}
name="issuer"
render={({ field }) => (
<FormItem>
<FormLabel>Issuer</FormLabel>
<FormControl>
<Input
type="text"
placeholder="Enter your issuer"
{...field}
/>
</FormControl>
<FormDescription>
Use a custom issuer to identify the service you're
authenticating with.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button <Button
type="submit" type="submit"
className="w-full" className="w-full"
@@ -255,7 +228,11 @@ export const Enable2FA = () => {
</Form> </Form>
) : ( ) : (
<Form {...pinForm}> <Form {...pinForm}>
<form onSubmit={handleVerifySubmit} className="space-y-6"> <form
id="pin-form"
onSubmit={pinForm.handleSubmit(handleVerifySubmit)}
className="space-y-6"
>
<div className="flex flex-col gap-6 justify-center items-center"> <div className="flex flex-col gap-6 justify-center items-center">
{data?.qrCodeUrl ? ( {data?.qrCodeUrl ? (
<> <>
@@ -307,33 +284,36 @@ export const Enable2FA = () => {
)} )}
</div> </div>
<div className="flex flex-col justify-center items-center"> <FormField
<FormLabel>Verification Code</FormLabel> control={pinForm.control}
<InputOTP name="pin"
maxLength={6} render={({ field }) => (
value={otpValue} <FormItem className="flex flex-col justify-center items-center">
onChange={setOtpValue} <FormLabel>Verification Code</FormLabel>
autoComplete="off" <FormControl>
> <InputOTP maxLength={6} {...field}>
<InputOTPGroup> <InputOTPGroup>
<InputOTPSlot index={0} /> <InputOTPSlot index={0} />
<InputOTPSlot index={1} /> <InputOTPSlot index={1} />
<InputOTPSlot index={2} /> <InputOTPSlot index={2} />
<InputOTPSlot index={3} /> <InputOTPSlot index={3} />
<InputOTPSlot index={4} /> <InputOTPSlot index={4} />
<InputOTPSlot index={5} /> <InputOTPSlot index={5} />
</InputOTPGroup> </InputOTPGroup>
</InputOTP> </InputOTP>
<FormDescription> </FormControl>
Enter the 6-digit code from your authenticator app <FormDescription>
</FormDescription> Enter the 6-digit code from your authenticator app
</div> </FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button <Button
type="submit" type="submit"
className="w-full" className="w-full"
isLoading={isPasswordLoading} isLoading={isPasswordLoading}
disabled={otpValue.length !== 6}
> >
Enable 2FA Enable 2FA
</Button> </Button>

View File

@@ -56,7 +56,6 @@ const randomImages = [
export const ProfileForm = () => { export const ProfileForm = () => {
const _utils = api.useUtils(); const _utils = api.useUtils();
const { data, refetch, isLoading } = api.user.get.useQuery(); const { data, refetch, isLoading } = api.user.get.useQuery();
const { const {
mutateAsync, mutateAsync,
isLoading: isUpdating, isLoading: isUpdating,
@@ -85,17 +84,12 @@ export const ProfileForm = () => {
useEffect(() => { useEffect(() => {
if (data) { if (data) {
form.reset( form.reset({
{ email: data?.user?.email || "",
email: data?.user?.email || "", password: "",
password: form.getValues("password") || "", image: data?.user?.image || "",
image: data?.user?.image || "", currentPassword: "",
currentPassword: form.getValues("currentPassword") || "", });
},
{
keepValues: true,
},
);
if (data.user.email) { if (data.user.email) {
generateSHA256Hash(data.user.email).then((hash) => { generateSHA256Hash(data.user.email).then((hash) => {
@@ -103,7 +97,8 @@ export const ProfileForm = () => {
}); });
} }
} }
}, [form, data]); form.reset();
}, [form, form.reset, data]);
const onSubmit = async (values: Profile) => { const onSubmit = async (values: Profile) => {
await mutateAsync({ await mutateAsync({
@@ -115,12 +110,7 @@ export const ProfileForm = () => {
.then(async () => { .then(async () => {
await refetch(); await refetch();
toast.success("Profile Updated"); toast.success("Profile Updated");
form.reset({ form.reset();
email: values.email,
password: "",
image: values.image,
currentPassword: "",
});
}) })
.catch(() => { .catch(() => {
toast.error("Error updating the profile"); toast.error("Error updating the profile");

View File

@@ -22,9 +22,6 @@ export const ShowDokployActions = () => {
const { mutateAsync: reloadServer, isLoading } = const { mutateAsync: reloadServer, isLoading } =
api.settings.reloadServer.useMutation(); api.settings.reloadServer.useMutation();
const { mutateAsync: cleanRedis } = api.settings.cleanRedis.useMutation();
const { mutateAsync: reloadRedis } = api.settings.reloadRedis.useMutation();
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild disabled={isLoading}> <DropdownMenuTrigger asChild disabled={isLoading}>
@@ -72,36 +69,6 @@ export const ShowDokployActions = () => {
{t("settings.server.webServer.updateServerIp")} {t("settings.server.webServer.updateServerIp")}
</DropdownMenuItem> </DropdownMenuItem>
</UpdateServerIp> </UpdateServerIp>
<DropdownMenuItem
className="cursor-pointer"
onClick={async () => {
await cleanRedis()
.then(async () => {
toast.success("Redis cleaned");
})
.catch(() => {
toast.error("Error cleaning Redis");
});
}}
>
Clean Redis
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={async () => {
await reloadRedis()
.then(async () => {
toast.success("Redis reloaded");
})
.catch(() => {
toast.error("Error reloading Redis");
});
}}
>
Reload Redis
</DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>

View File

@@ -56,7 +56,7 @@ export const ShowDestinations = () => {
key={sshKey.sshKeyId} key={sshKey.sshKeyId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-sm font-medium"> <span className="text-sm font-medium">

View File

@@ -133,6 +133,17 @@ export const UserNav = () => {
Servers Servers
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{data?.role === "owner" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/settings");
}}
>
Settings
</DropdownMenuItem>
)}
</> </>
)} )}
</DropdownMenuGroup> </DropdownMenuGroup>

View File

@@ -26,20 +26,15 @@ const dockerComposeServices = [
{ label: "secrets", type: "keyword", info: "Define secrets" }, { label: "secrets", type: "keyword", info: "Define secrets" },
].map((opt) => ({ ].map((opt) => ({
...opt, ...opt,
apply: ( apply: (view: EditorView, completion: Completion) => {
view: EditorView,
completion: Completion,
from: number,
to: number,
) => {
const insert = `${completion.label}:`; const insert = `${completion.label}:`;
view.dispatch({ view.dispatch({
changes: { changes: {
from, from: view.state.selection.main.from,
to, to: view.state.selection.main.to,
insert, insert,
}, },
selection: { anchor: from + insert.length }, selection: { anchor: view.state.selection.main.from + insert.length },
}); });
}, },
})); }));
@@ -79,20 +74,15 @@ const dockerComposeServiceOptions = [
{ label: "networks", type: "keyword", info: "Networks to join" }, { label: "networks", type: "keyword", info: "Networks to join" },
].map((opt) => ({ ].map((opt) => ({
...opt, ...opt,
apply: ( apply: (view: EditorView, completion: Completion) => {
view: EditorView,
completion: Completion,
from: number,
to: number,
) => {
const insert = `${completion.label}: `; const insert = `${completion.label}: `;
view.dispatch({ view.dispatch({
changes: { changes: {
from, from: view.state.selection.main.from,
to, to: view.state.selection.main.to,
insert, insert,
}, },
selection: { anchor: from + insert.length }, selection: { anchor: view.state.selection.main.from + insert.length },
}); });
}, },
})); }));
@@ -109,7 +99,6 @@ function dockerComposeComplete(
const line = context.state.doc.lineAt(context.pos); const line = context.state.doc.lineAt(context.pos);
const indentation = /^\s*/.exec(line.text)?.[0].length || 0; const indentation = /^\s*/.exec(line.text)?.[0].length || 0;
// If we're at the root level
if (indentation === 0) { if (indentation === 0) {
return { return {
from: word.from, from: word.from,

View File

@@ -1,2 +0,0 @@
ALTER TABLE "application" ADD COLUMN "enableSubmodules" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "enableSubmodules" boolean DEFAULT false;

View File

@@ -1,2 +0,0 @@
ALTER TABLE "application" ALTER COLUMN "enableSubmodules" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "compose" ALTER COLUMN "enableSubmodules" SET NOT NULL;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -596,20 +596,6 @@
"when": 1743923992280, "when": 1743923992280,
"tag": "0084_thin_iron_lad", "tag": "0084_thin_iron_lad",
"breakpoints": true "breakpoints": true
},
{
"idx": 85,
"version": "7",
"when": 1745705609181,
"tag": "0085_equal_captain_stacy",
"breakpoints": true
},
{
"idx": 86,
"version": "7",
"when": 1745706676004,
"tag": "0086_rainy_gertrude_yorkes",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,27 +1,23 @@
/**
* Sorted list based off of population of the country / speakers of the language.
*/
export const Languages = { export const Languages = {
english: { code: "en", name: "English" }, english: { code: "en", name: "English" },
spanish: { code: "es", name: "Español" },
chineseSimplified: { code: "zh-Hans", name: "简体中文" },
chineseTraditional: { code: "zh-Hant", name: "繁體中文" },
portuguese: { code: "pt-br", name: "Português" },
russian: { code: "ru", name: "Русский" },
japanese: { code: "ja", name: "日本語" },
german: { code: "de", name: "Deutsch" },
korean: { code: "ko", name: "한국어" },
french: { code: "fr", name: "Français" },
turkish: { code: "tr", name: "Türkçe" },
italian: { code: "it", name: "Italiano" },
polish: { code: "pl", name: "Polski" }, polish: { code: "pl", name: "Polski" },
ukrainian: { code: "uk", name: "Українська" }, ukrainian: { code: "uk", name: "Українська" },
persian: { code: "fa", name: "فارسی" }, russian: { code: "ru", name: "Русский" },
dutch: { code: "nl", name: "Nederlands" }, french: { code: "fr", name: "Français" },
indonesian: { code: "id", name: "Bahasa Indonesia" }, german: { code: "de", name: "Deutsch" },
chineseTraditional: { code: "zh-Hant", name: "繁體中文" },
chineseSimplified: { code: "zh-Hans", name: "简体中文" },
turkish: { code: "tr", name: "Türkçe" },
kazakh: { code: "kz", name: "Қазақ" }, kazakh: { code: "kz", name: "Қазақ" },
persian: { code: "fa", name: "فارسی" },
korean: { code: "ko", name: "한국어" },
portuguese: { code: "pt-br", name: "Português" },
italian: { code: "it", name: "Italiano" },
japanese: { code: "ja", name: "日本語" },
spanish: { code: "es", name: "Español" },
norwegian: { code: "no", name: "Norsk" }, norwegian: { code: "no", name: "Norsk" },
azerbaijani: { code: "az", name: "Azərbaycan" }, azerbaijani: { code: "az", name: "Azərbaycan" },
indonesian: { code: "id", name: "Bahasa Indonesia" },
malayalam: { code: "ml", name: "മലയാളം" }, malayalam: { code: "ml", name: "മലയാളം" },
}; };

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.21.8", "version": "v0.21.3",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
@@ -92,7 +92,7 @@
"adm-zip": "^0.5.14", "adm-zip": "^0.5.14",
"ai": "^4.0.23", "ai": "^4.0.23",
"bcrypt": "5.1.1", "bcrypt": "5.1.1",
"better-auth": "1.2.6", "better-auth": "1.2.4",
"bl": "6.0.11", "bl": "6.0.11",
"boxen": "^7.1.1", "boxen": "^7.1.1",
"bullmq": "5.4.2", "bullmq": "5.4.2",

View File

@@ -365,9 +365,7 @@ const Project = (
switch (service.type) { switch (service.type) {
case "application": case "application":
await applicationActions.start.mutateAsync({ await applicationActions.start.mutateAsync({ applicationId: serviceId });
applicationId: serviceId,
});
break; break;
case "compose": case "compose":
await composeActions.start.mutateAsync({ composeId: serviceId }); await composeActions.start.mutateAsync({ composeId: serviceId });
@@ -412,9 +410,7 @@ const Project = (
switch (service.type) { switch (service.type) {
case "application": case "application":
await applicationActions.stop.mutateAsync({ await applicationActions.stop.mutateAsync({ applicationId: serviceId });
applicationId: serviceId,
});
break; break;
case "compose": case "compose":
await composeActions.stop.mutateAsync({ composeId: serviceId }); await composeActions.stop.mutateAsync({ composeId: serviceId });

View File

@@ -215,7 +215,7 @@ const Service = (
router.push(newPath); router.push(newPath);
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll"> <div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList <TabsList
className={cn( className={cn(
"flex gap-8 justify-start max-xl:overflow-x-scroll overflow-y-hidden", "flex gap-8 justify-start max-xl:overflow-x-scroll overflow-y-hidden",

View File

@@ -212,15 +212,15 @@ const Service = (
router.push(newPath); router.push(newPath);
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll"> <div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList <TabsList
className={cn( className={cn(
"lg:grid lg:w-fit max-md:overflow-y-scroll justify-start", "md:grid md:w-fit max-md:overflow-y-scroll justify-start",
isCloud && data?.serverId isCloud && data?.serverId
? "lg:grid-cols-7" ? "md:grid-cols-7"
: data?.serverId : data?.serverId
? "lg:grid-cols-6" ? "md:grid-cols-6"
: "lg:grid-cols-7", : "md:grid-cols-7",
)} )}
> >
<TabsTrigger value="general">General</TabsTrigger> <TabsTrigger value="general">General</TabsTrigger>

View File

@@ -182,7 +182,7 @@ const Mariadb = (
router.push(newPath, undefined, { shallow: true }); router.push(newPath, undefined, { shallow: true });
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll"> <div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList <TabsList
className={cn( className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start", "md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@@ -183,7 +183,7 @@ const Mongo = (
router.push(newPath, undefined, { shallow: true }); router.push(newPath, undefined, { shallow: true });
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll"> <div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList <TabsList
className={cn( className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start", "md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@@ -183,7 +183,7 @@ const MySql = (
router.push(newPath, undefined, { shallow: true }); router.push(newPath, undefined, { shallow: true });
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll"> <div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList <TabsList
className={cn( className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start ", "md:grid md:w-fit max-md:overflow-y-scroll justify-start ",

View File

@@ -182,7 +182,7 @@ const Postgresql = (
router.push(newPath, undefined, { shallow: true }); router.push(newPath, undefined, { shallow: true });
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll"> <div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList <TabsList
className={cn( className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start", "md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@@ -182,7 +182,7 @@ const Redis = (
router.push(newPath, undefined, { shallow: true }); router.push(newPath, undefined, { shallow: true });
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll"> <div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList <TabsList
className={cn( className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start", "md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

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

View File

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

View File

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

View File

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

View File

@@ -355,7 +355,6 @@ export const applicationRouter = createTRPCRouter({
applicationStatus: "idle", applicationStatus: "idle",
githubId: input.githubId, githubId: input.githubId,
watchPaths: input.watchPaths, watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
}); });
return true; return true;
@@ -383,7 +382,6 @@ export const applicationRouter = createTRPCRouter({
gitlabProjectId: input.gitlabProjectId, gitlabProjectId: input.gitlabProjectId,
gitlabPathNamespace: input.gitlabPathNamespace, gitlabPathNamespace: input.gitlabPathNamespace,
watchPaths: input.watchPaths, watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
}); });
return true; return true;
@@ -409,7 +407,6 @@ export const applicationRouter = createTRPCRouter({
applicationStatus: "idle", applicationStatus: "idle",
bitbucketId: input.bitbucketId, bitbucketId: input.bitbucketId,
watchPaths: input.watchPaths, watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
}); });
return true; return true;
@@ -435,7 +432,6 @@ export const applicationRouter = createTRPCRouter({
applicationStatus: "idle", applicationStatus: "idle",
giteaId: input.giteaId, giteaId: input.giteaId,
watchPaths: input.watchPaths, watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
}); });
return true; return true;
@@ -483,7 +479,6 @@ export const applicationRouter = createTRPCRouter({
sourceType: "git", sourceType: "git",
applicationStatus: "idle", applicationStatus: "idle",
watchPaths: input.watchPaths, watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
}); });
return true; return true;

View File

@@ -50,18 +50,6 @@ import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import { z } from "zod"; import { z } from "zod";
interface RcloneFile {
Path: string;
Name: string;
Size: number;
IsDir: boolean;
Tier?: string;
Hashes?: {
MD5?: string;
SHA1?: string;
};
}
export const backupRouter = createTRPCRouter({ export const backupRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure
.input(apiCreateBackup) .input(apiCreateBackup)
@@ -280,7 +268,7 @@ export const backupRouter = createTRPCRouter({
: input.search; : input.search;
const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath; const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath;
const listCommand = `rclone lsjson ${rcloneFlags.join(" ")} "${searchPath}" --no-mimetype --no-modtime 2>/dev/null`; const listCommand = `rclone lsf ${rcloneFlags.join(" ")} "${searchPath}" | head -n 100`;
let stdout = ""; let stdout = "";
@@ -292,35 +280,20 @@ export const backupRouter = createTRPCRouter({
stdout = result.stdout; stdout = result.stdout;
} }
let files: RcloneFile[] = []; const files = stdout.split("\n").filter(Boolean);
try {
files = JSON.parse(stdout) as RcloneFile[];
} catch (error) {
console.error("Error parsing JSON response:", error);
console.error("Raw stdout:", stdout);
throw new Error("Failed to parse backup files list");
}
// Limit to first 100 files
const results = baseDir const results = baseDir
? files.map((file) => ({ ? files.map((file) => `${baseDir}${file}`)
...file,
Path: `${baseDir}${file.Path}`,
}))
: files; : files;
if (searchTerm) { if (searchTerm) {
return results return results.filter((file) =>
.filter((file) => file.toLowerCase().includes(searchTerm.toLowerCase()),
file.Path.toLowerCase().includes(searchTerm.toLowerCase()), );
)
.slice(0, 100);
} }
return results.slice(0, 100); return results;
} catch (error) { } catch (error) {
console.error("Error in listBackupFiles:", error);
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: message:

View File

@@ -31,6 +31,7 @@ export const mountRouter = createTRPCRouter({
update: protectedProcedure update: protectedProcedure
.input(apiUpdateMount) .input(apiUpdateMount)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return await updateMount(input.mountId, input); await updateMount(input.mountId, input);
return true;
}), }),
}); });

View File

@@ -79,33 +79,6 @@ export const settingsRouter = createTRPCRouter({
await execAsync(`docker service update --force ${stdout.trim()}`); await execAsync(`docker service update --force ${stdout.trim()}`);
return true; return true;
}), }),
cleanRedis: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
const { stdout: containerId } = await execAsync(
`docker ps --filter "name=dokploy-redis" --filter "status=running" -q | head -n 1`,
);
if (!containerId) {
throw new Error("Redis container not found");
}
const redisContainerId = containerId.trim();
await execAsync(`docker exec -i ${redisContainerId} redis-cli flushall`);
return true;
}),
reloadRedis: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
await execAsync("docker service scale dokploy-redis=0");
await execAsync("docker service scale dokploy-redis=1");
return true;
}),
reloadTraefik: adminProcedure reloadTraefik: adminProcedure
.input(apiServerSchema) .input(apiServerSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {

View File

@@ -21,7 +21,6 @@ import { setupTerminalWebSocketServer } from "./wss/terminal";
config({ path: ".env" }); config({ path: ".env" });
const PORT = Number.parseInt(process.env.PORT || "3000", 10); const PORT = Number.parseInt(process.env.PORT || "3000", 10);
const HOST = process.env.HOST || "0.0.0.0";
const dev = process.env.NODE_ENV !== "production"; const dev = process.env.NODE_ENV !== "production";
const app = next({ dev, turbopack: process.env.TURBOPACK === "1" }); const app = next({ dev, turbopack: process.env.TURBOPACK === "1" });
const handle = app.getRequestHandler(); const handle = app.getRequestHandler();
@@ -56,8 +55,8 @@ void app.prepare().then(async () => {
await migration(); await migration();
} }
server.listen(PORT, HOST); server.listen(PORT);
console.log(`Server Started on: http://${HOST}:${PORT}`); console.log("Server Started:", PORT);
if (!IS_CLOUD) { if (!IS_CLOUD) {
console.log("Starting Deployment Worker"); console.log("Starting Deployment Worker");
const { deploymentWorker } = await import("./queues/deployments-queue"); const { deploymentWorker } = await import("./queues/deployments-queue");

View File

@@ -36,11 +36,11 @@
"@ai-sdk/mistral": "^1.0.6", "@ai-sdk/mistral": "^1.0.6",
"@ai-sdk/openai": "^1.0.12", "@ai-sdk/openai": "^1.0.12",
"@ai-sdk/openai-compatible": "^0.0.13", "@ai-sdk/openai-compatible": "^0.0.13",
"@better-auth/utils": "0.2.4", "@better-auth/utils": "0.2.3",
"@oslojs/encoding": "1.1.0", "@oslojs/encoding": "1.1.0",
"@oslojs/crypto": "1.0.1", "@oslojs/crypto": "1.0.1",
"drizzle-dbml-generator": "0.10.0", "drizzle-dbml-generator": "0.10.0",
"better-auth": "1.2.6", "better-auth": "1.2.4",
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^8.4.1",
"@octokit/auth-app": "^6.0.4", "@octokit/auth-app": "^6.0.4",
"@react-email/components": "^0.0.21", "@react-email/components": "^0.0.21",

View File

@@ -182,7 +182,6 @@ export const applications = pgTable("application", {
onDelete: "set null", onDelete: "set null",
}, },
), ),
enableSubmodules: boolean("enableSubmodules").notNull().default(false),
dockerfile: text("dockerfile"), dockerfile: text("dockerfile"),
dockerContextPath: text("dockerContextPath"), dockerContextPath: text("dockerContextPath"),
dockerBuildStage: text("dockerBuildStage"), dockerBuildStage: text("dockerBuildStage"),
@@ -471,7 +470,6 @@ export const apiSaveGithubProvider = createSchema
buildPath: true, buildPath: true,
githubId: true, githubId: true,
watchPaths: true, watchPaths: true,
enableSubmodules: true,
}) })
.required(); .required();
@@ -486,7 +484,6 @@ export const apiSaveGitlabProvider = createSchema
gitlabProjectId: true, gitlabProjectId: true,
gitlabPathNamespace: true, gitlabPathNamespace: true,
watchPaths: true, watchPaths: true,
enableSubmodules: true,
}) })
.required(); .required();
@@ -499,7 +496,6 @@ export const apiSaveBitbucketProvider = createSchema
bitbucketId: true, bitbucketId: true,
applicationId: true, applicationId: true,
watchPaths: true, watchPaths: true,
enableSubmodules: true,
}) })
.required(); .required();
@@ -512,7 +508,6 @@ export const apiSaveGiteaProvider = createSchema
giteaRepository: true, giteaRepository: true,
giteaId: true, giteaId: true,
watchPaths: true, watchPaths: true,
enableSubmodules: true,
}) })
.required(); .required();
@@ -533,7 +528,6 @@ export const apiSaveGitProvider = createSchema
customGitBuildPath: true, customGitBuildPath: true,
customGitUrl: true, customGitUrl: true,
watchPaths: true, watchPaths: true,
enableSubmodules: true,
}) })
.required() .required()
.merge( .merge(

View File

@@ -72,7 +72,6 @@ export const compose = pgTable("compose", {
), ),
command: text("command").notNull().default(""), command: text("command").notNull().default(""),
// //
enableSubmodules: boolean("enableSubmodules").notNull().default(false),
composePath: text("composePath").notNull().default("./docker-compose.yml"), composePath: text("composePath").notNull().default("./docker-compose.yml"),
suffix: text("suffix").notNull().default(""), suffix: text("suffix").notNull().default(""),
randomize: boolean("randomize").notNull().default(false), randomize: boolean("randomize").notNull().default(false),

View File

@@ -201,7 +201,7 @@ const { handler, api } = betterAuth({
const host = const host =
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? "http://localhost:3000" ? "http://localhost:3000"
: "https://app.dokploy.com"; : "https://dokploy.com";
const inviteLink = `${host}/invitation?token=${data.id}`; const inviteLink = `${host}/invitation?token=${data.id}`;
await sendEmail({ await sendEmail({

View File

@@ -356,7 +356,6 @@ export const deployRemoteCompose = async ({
deployment.logPath, deployment.logPath,
true, true,
); );
console.log(command);
} else if (compose.sourceType === "raw") { } else if (compose.sourceType === "raw") {
command += getCreateComposeFileCommand(compose, deployment.logPath); command += getCreateComposeFileCommand(compose, deployment.logPath);
} else if (compose.sourceType === "gitea") { } else if (compose.sourceType === "gitea") {

View File

@@ -144,8 +144,7 @@ export const updateMount = async (
await deleteFileMount(mountId); await deleteFileMount(mountId);
await createFileMount(mountId); await createFileMount(mountId);
} }
return mount;
return await findMountById(mountId);
}); });
}; };

View File

@@ -76,7 +76,7 @@ CURRENT_USER=$USER
echo "Installing requirements for: OS: $OS_TYPE" echo "Installing requirements for: OS: $OS_TYPE"
if [ $EUID != 0 ]; then if [ $EUID != 0 ]; then
echo "Please run this script as root or with sudo ❌" echo "Please run this script as root or with sudo ❌"
exit exit
fi fi
@@ -263,7 +263,7 @@ const setupMainDirectory = () => `
# Create the /etc/dokploy directory # Create the /etc/dokploy directory
mkdir -p /etc/dokploy mkdir -p /etc/dokploy
chmod 777 /etc/dokploy chmod 777 /etc/dokploy
echo "Directory /etc/dokploy created ✅" echo "Directory /etc/dokploy created ✅"
fi fi
`; `;
@@ -276,16 +276,16 @@ export const setupSwarm = () => `
# Get IP address # Get IP address
get_ip() { get_ip() {
local ip="" local ip=""
# Try IPv4 with multiple services # Try IPv4 with multiple services
# First attempt: ifconfig.io # First attempt: ifconfig.io
ip=\$(curl -4s --connect-timeout 5 https://ifconfig.io 2>/dev/null) ip=\$(curl -4s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
# Second attempt: icanhazip.com # Second attempt: icanhazip.com
if [ -z "\$ip" ]; then if [ -z "\$ip" ]; then
ip=\$(curl -4s --connect-timeout 5 https://icanhazip.com 2>/dev/null) ip=\$(curl -4s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
fi fi
# Third attempt: ipecho.net # Third attempt: ipecho.net
if [ -z "\$ip" ]; then if [ -z "\$ip" ]; then
ip=\$(curl -4s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null) ip=\$(curl -4s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
@@ -295,12 +295,12 @@ export const setupSwarm = () => `
if [ -z "\$ip" ]; then if [ -z "\$ip" ]; then
# Try IPv6 with ifconfig.io # Try IPv6 with ifconfig.io
ip=\$(curl -6s --connect-timeout 5 https://ifconfig.io 2>/dev/null) ip=\$(curl -6s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
# Try IPv6 with icanhazip.com # Try IPv6 with icanhazip.com
if [ -z "\$ip" ]; then if [ -z "\$ip" ]; then
ip=\$(curl -6s --connect-timeout 5 https://icanhazip.com 2>/dev/null) ip=\$(curl -6s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
fi fi
# Try IPv6 with ipecho.net # Try IPv6 with ipecho.net
if [ -z "\$ip" ]; then if [ -z "\$ip" ]; then
ip=\$(curl -6s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null) ip=\$(curl -6s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
@@ -549,7 +549,7 @@ export const createTraefikInstance = () => {
sleep 8 sleep 8
echo "Traefik migrated to Standalone ✅" echo "Traefik migrated to Standalone ✅"
fi fi
if docker inspect dokploy-traefik > /dev/null 2>&1; then if docker inspect dokploy-traefik > /dev/null 2>&1; then
echo "Traefik already exists ✅" echo "Traefik already exists ✅"
else else
@@ -577,7 +577,7 @@ const installNixpacks = () => `
if command_exists nixpacks; then if command_exists nixpacks; then
echo "Nixpacks already installed ✅" echo "Nixpacks already installed ✅"
else else
export NIXPACKS_VERSION=1.35.0 export NIXPACKS_VERSION=1.29.1
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)" bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅" echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi fi

View File

@@ -1,4 +1,4 @@
import { randomBytes, createHmac } from "node:crypto"; import { randomBytes } from "node:crypto";
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises"; import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
@@ -24,12 +24,6 @@ export interface Template {
domains: DomainSchema[]; domains: DomainSchema[];
} }
export interface GenerateJWTOptions {
length?: number;
secret?: string;
payload?: Record<string, unknown> | undefined;
}
export const generateRandomDomain = ({ export const generateRandomDomain = ({
serverIp, serverIp,
projectName, projectName,
@@ -67,48 +61,8 @@ export function generateBase64(bytes = 32): string {
return randomBytes(bytes).toString("base64"); return randomBytes(bytes).toString("base64");
} }
function safeBase64(str: string): string { export function generateJwt(length = 256): string {
return str.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_"); return randomBytes(length).toString("hex");
}
function objToJWTBase64(obj: any): string {
return safeBase64(
Buffer.from(JSON.stringify(obj), "utf8").toString("base64"),
);
}
export function generateJwt(options: GenerateJWTOptions = {}): string {
let { length, secret, payload = {} } = options;
if (length) {
return randomBytes(length).toString("hex");
}
const encodedHeader = objToJWTBase64({
alg: "HS256",
typ: "JWT",
});
if (!payload.iss) {
payload.iss = "dokploy";
}
if (!payload.iat) {
payload.iat = Math.floor(Date.now() / 1000);
}
if (!payload.exp) {
payload.exp = Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000);
}
const encodedPayload = objToJWTBase64({
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000),
...payload,
});
if (!secret) {
secret = randomBytes(32).toString("hex");
}
const signature = safeBase64(
createHmac("SHA256", secret)
.update(`${encodedHeader}.${encodedPayload}`)
.digest("base64"),
);
return `${encodedHeader}.${encodedPayload}.${signature}`;
} }
/** /**

View File

@@ -65,7 +65,7 @@ export interface Template {
/** /**
* Process a string value and replace variables * Process a string value and replace variables
*/ */
export function processValue( function processValue(
value: string, value: string,
variables: Record<string, string>, variables: Record<string, string>,
schema: Schema, schema: Schema,
@@ -84,11 +84,11 @@ export function processValue(
const length = Number.parseInt(varName.split(":")[1], 10) || 32; const length = Number.parseInt(varName.split(":")[1], 10) || 32;
return generateBase64(length); return generateBase64(length);
} }
if (varName.startsWith("password:")) { if (varName.startsWith("password:")) {
const length = Number.parseInt(varName.split(":")[1], 10) || 16; const length = Number.parseInt(varName.split(":")[1], 10) || 16;
return generatePassword(length); return generatePassword(length);
} }
if (varName === "password") { if (varName === "password") {
return generatePassword(16); return generatePassword(16);
} }
@@ -97,31 +97,14 @@ export function processValue(
const length = Number.parseInt(varName.split(":")[1], 10) || 8; const length = Number.parseInt(varName.split(":")[1], 10) || 8;
return generateHash(length); return generateHash(length);
} }
if (varName === "hash") {
return generateHash();
}
if (varName === "uuid") { if (varName === "uuid") {
return crypto.randomUUID(); return crypto.randomUUID();
} }
if (varName === "timestamp" || varName === "timestampms") { if (varName === "timestamp") {
return Date.now().toString(); return Date.now().toString();
} }
if (varName === "timestamps") {
return Math.round(Date.now() / 1000).toString();
}
if (varName.startsWith("timestampms:")) {
return new Date(varName.slice(12)).getTime().toString();
}
if (varName.startsWith("timestamps:")) {
return Math.round(
new Date(varName.slice(11)).getTime() / 1000,
).toString();
}
if (varName === "randomPort") { if (varName === "randomPort") {
return Math.floor(Math.random() * 65535).toString(); return Math.floor(Math.random() * 65535).toString();
} }
@@ -131,34 +114,8 @@ export function processValue(
} }
if (varName.startsWith("jwt:")) { if (varName.startsWith("jwt:")) {
const params: string[] = varName.split(":").slice(1); const length = Number.parseInt(varName.split(":")[1], 10) || 256;
if (params.length === 1 && params[0] && params[0].match(/^\d{1,3}$/)) { return generateJwt(length);
return generateJwt({ length: Number.parseInt(params[0], 10) });
}
let [secret, payload] = params;
if (typeof payload === "string" && variables[payload]) {
payload = variables[payload];
}
if (
typeof payload === "string" &&
payload.startsWith("{") &&
payload.endsWith("}")
) {
try {
payload = JSON.parse(payload);
} catch (e) {
// If payload is not a valid JSON, invalid it
payload = undefined;
console.error("Invalid JWT payload", e);
}
}
if (typeof payload !== "object") {
payload = undefined;
}
return generateJwt({
secret: secret ? variables[secret] || secret : undefined,
payload: payload as any,
});
} }
if (varName === "username") { if (varName === "username") {
@@ -190,7 +147,7 @@ export function processVariables(
): Record<string, string> { ): Record<string, string> {
const variables: Record<string, string> = {}; const variables: Record<string, string> = {};
// First pass: Process some variables that don't depend on other variables // First pass: Process variables that don't depend on other variables
for (const [key, value] of Object.entries(template.variables)) { for (const [key, value] of Object.entries(template.variables)) {
if (typeof value !== "string") continue; if (typeof value !== "string") continue;
@@ -204,8 +161,6 @@ export function processVariables(
const match = value.match(/\${password:(\d+)}/); const match = value.match(/\${password:(\d+)}/);
const length = match?.[1] ? Number.parseInt(match[1], 10) : 16; const length = match?.[1] ? Number.parseInt(match[1], 10) : 16;
variables[key] = generatePassword(length); variables[key] = generatePassword(length);
} else if (value === "${hash}") {
variables[key] = generateHash();
} else if (value.startsWith("${hash:")) { } else if (value.startsWith("${hash:")) {
const match = value.match(/\${hash:(\d+)}/); const match = value.match(/\${hash:(\d+)}/);
const length = match?.[1] ? Number.parseInt(match[1], 10) : 8; const length = match?.[1] ? Number.parseInt(match[1], 10) : 8;

View File

@@ -23,25 +23,13 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
try { try {
await execAsync(`mkdir -p ${tempDir}/filesystem`); await execAsync(`mkdir -p ${tempDir}/filesystem`);
// First get the container ID const postgresCommand = `docker exec $(docker ps --filter "name=dokploy-postgres" -q) pg_dump -v -Fc -U dokploy -d dokploy > ${tempDir}/database.sql`;
const { stdout: containerId } = await execAsync(
`docker ps --filter "name=dokploy-postgres" --filter "status=running" -q | head -n 1`,
);
if (!containerId) {
throw new Error("PostgreSQL container not found");
}
const postgresContainerId = containerId.trim();
const postgresCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`;
await execAsync(postgresCommand); await execAsync(postgresCommand);
await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`); await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`);
await execAsync( await execAsync(
// Zip all .sql files since we created more than one `cd ${tempDir} && zip -r ${backupFileName} database.sql filesystem/ > /dev/null 2>&1`,
`cd ${tempDir} && zip -r ${backupFileName} *.sql filesystem/ > /dev/null 2>&1`,
); );
const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`; const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`;

View File

@@ -84,7 +84,7 @@ export const buildRailpack = async (
for (const envVar of envVariables) { for (const envVar of envVariables) {
const [key, value] = envVar.split("="); const [key, value] = envVar.split("=");
if (key && value) { if (key && value) {
buildArgs.push("--secret", `id=${key},env='${key}'`); buildArgs.push("--secret", `id=${key},env=${key}`);
env[key] = value; env[key] = value;
} }
} }
@@ -132,7 +132,7 @@ export const getRailpackCommand = (
]; ];
for (const env of envVariables) { for (const env of envVariables) {
prepareArgs.push("--env", `'${env}'`); prepareArgs.push("--env", env);
} }
// Calculate secrets hash for layer invalidation // Calculate secrets hash for layer invalidation
@@ -164,7 +164,7 @@ export const getRailpackCommand = (
for (const envVar of envVariables) { for (const envVar of envVariables) {
const [key, value] = envVar.split("="); const [key, value] = envVar.split("=");
if (key && value) { if (key && value) {
buildArgs.push("--secret", `id=${key},env='${key}'`); buildArgs.push("--secret", `id=${key},env=${key}`);
exportEnvs.push(`export ${key}=${value}`); exportEnvs.push(`export ${key}=${value}`);
} }
} }

View File

@@ -249,11 +249,6 @@ export const addDomainToCompose = async (
labels.unshift("traefik.enable=true"); labels.unshift("traefik.enable=true");
} }
labels.unshift(...httpLabels); labels.unshift(...httpLabels);
if (!compose.isolatedDeployment) {
if (!labels.includes("traefik.docker.network=dokploy-network")) {
labels.unshift("traefik.docker.network=dokploy-network");
}
}
} }
if (!compose.isolatedDeployment) { if (!compose.isolatedDeployment) {

View File

@@ -40,84 +40,68 @@ export const sendDokployRestartNotifications = async () => {
const decorate = (decoration: string, text: string) => const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim(); `${discord.decoration ? decoration : ""} ${text}`.trim();
try { await sendDiscordNotification(discord, {
await sendDiscordNotification(discord, { title: decorate(">", "`✅` Dokploy Server Restarted"),
title: decorate(">", "`✅` Dokploy Server Restarted"), color: 0x57f287,
color: 0x57f287, fields: [
fields: [ {
{ name: decorate("`📅`", "Date"),
name: decorate("`📅`", "Date"), value: `<t:${unixDate}:D>`,
value: `<t:${unixDate}:D>`, inline: true,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Restart Notification",
}, },
}); {
} catch (error) { name: decorate("`⌚`", "Time"),
console.log(error); value: `<t:${unixDate}:t>`,
} inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Restart Notification",
},
});
} }
if (gotify) { if (gotify) {
const decorate = (decoration: string, text: string) => const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`; `${gotify.decoration ? decoration : ""} ${text}\n`;
try { await sendGotifyNotification(
await sendGotifyNotification( gotify,
gotify, decorate("✅", "Dokploy Server Restarted"),
decorate("", "Dokploy Server Restarted"), `${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`, );
);
} catch (error) {
console.log(error);
}
} }
if (telegram) { if (telegram) {
try { await sendTelegramNotification(
await sendTelegramNotification( telegram,
telegram, `<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`, );
);
} catch (error) {
console.log(error);
}
} }
if (slack) { if (slack) {
const { channel } = slack; const { channel } = slack;
try { await sendSlackNotification(slack, {
await sendSlackNotification(slack, { channel: channel,
channel: channel, attachments: [
attachments: [ {
{ color: "#00FF00",
color: "#00FF00", pretext: ":white_check_mark: *Dokploy Server Restarted*",
pretext: ":white_check_mark: *Dokploy Server Restarted*", fields: [
fields: [ {
{ title: "Time",
title: "Time", value: date.toLocaleString(),
value: date.toLocaleString(), short: true,
short: true, },
}, ],
], },
}, ],
], });
});
} catch (error) {
console.log(error);
}
} }
} }
}; };

View File

@@ -37,7 +37,6 @@ export const cloneBitbucketRepository = async (
bitbucketBranch, bitbucketBranch,
bitbucketId, bitbucketId,
bitbucket, bitbucket,
enableSubmodules,
} = entity; } = entity;
if (!bitbucketId) { if (!bitbucketId) {
@@ -54,23 +53,25 @@ export const cloneBitbucketRepository = async (
const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`; const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`;
try { try {
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`); writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
const cloneArgs = [ await spawnAsync(
"clone", "git",
"--branch", [
bitbucketBranch!, "clone",
"--depth", "--branch",
"1", bitbucketBranch!,
...(enableSubmodules ? ["--recurse-submodules"] : []), "--depth",
cloneUrl, "1",
outputPath, "--recurse-submodules",
"--progress", cloneUrl,
]; outputPath,
"--progress",
await spawnAsync("git", cloneArgs, (data) => { ],
if (writeStream.writable) { (data) => {
writeStream.write(data); if (writeStream.writable) {
} writeStream.write(data);
}); }
},
);
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`); writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
} catch (error) { } catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`); writeStream.write(`ERROR Clonning: ${error}: ❌`);
@@ -88,7 +89,6 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
bitbucketOwner, bitbucketOwner,
bitbucketBranch, bitbucketBranch,
bitbucketId, bitbucketId,
enableSubmodules,
} = entity; } = entity;
if (!bitbucketId) { if (!bitbucketId) {
@@ -106,19 +106,17 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`; const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
try { try {
const cloneArgs = [ await spawnAsync("git", [
"clone", "clone",
"--branch", "--branch",
bitbucketBranch!, bitbucketBranch!,
"--depth", "--depth",
"1", "1",
...(enableSubmodules ? ["--recurse-submodules"] : []), "--recurse-submodules",
cloneUrl, cloneUrl,
outputPath, outputPath,
"--progress", "--progress",
]; ]);
await spawnAsync("git", cloneArgs);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -133,7 +131,6 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
bitbucketBranch, bitbucketBranch,
bitbucketId, bitbucketId,
serverId, serverId,
enableSubmodules,
} = compose; } = compose;
if (!serverId) { if (!serverId) {
@@ -156,11 +153,11 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`; const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
try { try {
const cloneCommand = ` const command = `
rm -rf ${outputPath}; rm -rf ${outputPath};
git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} git clone --branch ${bitbucketBranch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath}
`; `;
await execAsyncRemote(serverId, cloneCommand); await execAsyncRemote(serverId, command);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -179,7 +176,6 @@ export const getBitbucketCloneCommand = async (
bitbucketBranch, bitbucketBranch,
bitbucketId, bitbucketId,
serverId, serverId,
enableSubmodules,
} = entity; } = entity;
if (!serverId) { if (!serverId) {
@@ -211,7 +207,7 @@ export const getBitbucketCloneCommand = async (
const cloneCommand = ` const cloneCommand = `
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
if ! git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then if ! git clone --branch ${bitbucketBranch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath}; echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
exit 1; exit 1;
fi fi

View File

@@ -17,19 +17,12 @@ export const cloneGitRepository = async (
customGitUrl?: string | null; customGitUrl?: string | null;
customGitBranch?: string | null; customGitBranch?: string | null;
customGitSSHKeyId?: string | null; customGitSSHKeyId?: string | null;
enableSubmodules?: boolean;
}, },
logPath: string, logPath: string,
isCompose = false, isCompose = false,
) => { ) => {
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(); const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths();
const { const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
appName,
customGitUrl,
customGitBranch,
customGitSSHKeyId,
enableSubmodules,
} = entity;
if (!customGitUrl || !customGitBranch) { if (!customGitUrl || !customGitBranch) {
throw new TRPCError({ throw new TRPCError({
@@ -77,21 +70,19 @@ export const cloneGitRepository = async (
} }
const { port } = sanitizeRepoPathSSH(customGitUrl); const { port } = sanitizeRepoPathSSH(customGitUrl);
const cloneArgs = [
"clone",
"--branch",
customGitBranch,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
customGitUrl,
outputPath,
"--progress",
];
await spawnAsync( await spawnAsync(
"git", "git",
cloneArgs, [
"clone",
"--branch",
customGitBranch,
"--depth",
"1",
"--recurse-submodules",
customGitUrl,
outputPath,
"--progress",
],
(data) => { (data) => {
if (writeStream.writable) { if (writeStream.writable) {
writeStream.write(data); writeStream.write(data);
@@ -123,7 +114,6 @@ export const getCustomGitCloneCommand = async (
customGitBranch?: string | null; customGitBranch?: string | null;
customGitSSHKeyId?: string | null; customGitSSHKeyId?: string | null;
serverId: string | null; serverId: string | null;
enableSubmodules: boolean;
}, },
logPath: string, logPath: string,
isCompose = false, isCompose = false,
@@ -135,7 +125,6 @@ export const getCustomGitCloneCommand = async (
customGitBranch, customGitBranch,
customGitSSHKeyId, customGitSSHKeyId,
serverId, serverId,
enableSubmodules,
} = entity; } = entity;
if (!customGitUrl || !customGitBranch) { if (!customGitUrl || !customGitBranch) {
@@ -192,7 +181,7 @@ export const getCustomGitCloneCommand = async (
} }
command.push( command.push(
`if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then `if ! git clone --branch ${customGitBranch} --depth 1 --recurse-submodules --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath}; echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath};
exit 1; exit 1;
fi fi
@@ -272,15 +261,8 @@ export const cloneGitRawRepository = async (entity: {
customGitUrl?: string | null; customGitUrl?: string | null;
customGitBranch?: string | null; customGitBranch?: string | null;
customGitSSHKeyId?: string | null; customGitSSHKeyId?: string | null;
enableSubmodules?: boolean;
}) => { }) => {
const { const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
appName,
customGitUrl,
customGitBranch,
customGitSSHKeyId,
enableSubmodules,
} = entity;
if (!customGitUrl || !customGitBranch) { if (!customGitUrl || !customGitBranch) {
throw new TRPCError({ throw new TRPCError({
@@ -325,26 +307,29 @@ export const cloneGitRawRepository = async (entity: {
} }
const { port } = sanitizeRepoPathSSH(customGitUrl); const { port } = sanitizeRepoPathSSH(customGitUrl);
const cloneArgs = [ await spawnAsync(
"clone", "git",
"--branch", [
customGitBranch, "clone",
"--depth", "--branch",
"1", customGitBranch,
...(enableSubmodules ? ["--recurse-submodules"] : []), "--depth",
customGitUrl, "1",
outputPath, "--recurse-submodules",
"--progress", customGitUrl,
]; outputPath,
"--progress",
await spawnAsync("git", cloneArgs, (_data) => {}, { ],
env: { (_data) => {},
...process.env, {
...(customGitSSHKeyId && { env: {
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`, ...process.env,
}), ...(customGitSSHKeyId && {
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
}),
},
}, },
}); );
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -357,7 +342,6 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
customGitUrl, customGitUrl,
customGitSSHKeyId, customGitSSHKeyId,
serverId, serverId,
enableSubmodules,
} = compose; } = compose;
if (!serverId) { if (!serverId) {
@@ -412,7 +396,7 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
} }
command.push( command.push(
`if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} ; then `if ! git clone --branch ${customGitBranch} --depth 1 --recurse-submodules --progress ${customGitUrl} ${outputPath} ; then
echo "[ERROR] Fail to clone the repository "; echo "[ERROR] Fail to clone the repository ";
exit 1; exit 1;
fi fi

View File

@@ -119,7 +119,6 @@ export const getGiteaCloneCommand = async (
giteaRepository, giteaRepository,
serverId, serverId,
gitea, gitea,
enableSubmodules,
} = entity; } = entity;
if (!serverId) { if (!serverId) {
@@ -156,7 +155,7 @@ export const getGiteaCloneCommand = async (
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
if ! git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then if ! git clone --branch ${giteaBranch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath}; echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath};
exit 1; exit 1;
fi fi
@@ -175,14 +174,7 @@ export const cloneGiteaRepository = async (
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(); const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
const writeStream = createWriteStream(logPath, { flags: "a" }); const writeStream = createWriteStream(logPath, { flags: "a" });
const { const { appName, giteaBranch, giteaId, giteaOwner, giteaRepository } = entity;
appName,
giteaBranch,
giteaId,
giteaOwner,
giteaRepository,
enableSubmodules,
} = entity;
if (!giteaId) { if (!giteaId) {
throw new TRPCError({ throw new TRPCError({
@@ -219,7 +211,7 @@ export const cloneGiteaRepository = async (
giteaBranch!, giteaBranch!,
"--depth", "--depth",
"1", "1",
...(enableSubmodules ? ["--recurse-submodules"] : []), "--recurse-submodules",
cloneUrl, cloneUrl,
outputPath, outputPath,
"--progress", "--progress",
@@ -240,14 +232,7 @@ export const cloneGiteaRepository = async (
}; };
export const cloneRawGiteaRepository = async (entity: Compose) => { export const cloneRawGiteaRepository = async (entity: Compose) => {
const { const { appName, giteaRepository, giteaOwner, giteaBranch, giteaId } = entity;
appName,
giteaRepository,
giteaOwner,
giteaBranch,
giteaId,
enableSubmodules,
} = entity;
const { COMPOSE_PATH } = paths(); const { COMPOSE_PATH } = paths();
if (!giteaId) { if (!giteaId) {
@@ -280,7 +265,7 @@ export const cloneRawGiteaRepository = async (entity: Compose) => {
giteaBranch!, giteaBranch!,
"--depth", "--depth",
"1", "1",
...(enableSubmodules ? ["--recurse-submodules"] : []), "--recurse-submodules",
cloneUrl, cloneUrl,
outputPath, outputPath,
"--progress", "--progress",
@@ -298,7 +283,6 @@ export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
giteaBranch, giteaBranch,
giteaId, giteaId,
serverId, serverId,
enableSubmodules,
} = compose; } = compose;
if (!serverId) { if (!serverId) {
@@ -323,7 +307,7 @@ export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
try { try {
const command = ` const command = `
rm -rf ${outputPath}; rm -rf ${outputPath};
git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} git clone --branch ${giteaBranch} --depth 1 ${cloneUrl} ${outputPath}
`; `;
await execAsyncRemote(serverId, command); await execAsyncRemote(serverId, command);
} catch (error) { } catch (error) {

View File

@@ -83,7 +83,6 @@ interface CloneGithubRepository {
repository: string | null; repository: string | null;
logPath: string; logPath: string;
type?: "application" | "compose"; type?: "application" | "compose";
enableSubmodules: boolean;
} }
export const cloneGithubRepository = async ({ export const cloneGithubRepository = async ({
logPath, logPath,
@@ -93,8 +92,7 @@ export const cloneGithubRepository = async ({
const isCompose = type === "compose"; const isCompose = type === "compose";
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(); const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
const writeStream = createWriteStream(logPath, { flags: "a" }); const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, repository, owner, branch, githubId, enableSubmodules } = const { appName, repository, owner, branch, githubId } = entity;
entity;
if (!githubId) { if (!githubId) {
throw new TRPCError({ throw new TRPCError({
@@ -130,23 +128,25 @@ export const cloneGithubRepository = async ({
try { try {
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`); writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
const cloneArgs = [ await spawnAsync(
"clone", "git",
"--branch", [
branch!, "clone",
"--depth", "--branch",
"1", branch!,
...(enableSubmodules ? ["--recurse-submodules"] : []), "--depth",
cloneUrl, "1",
outputPath, "--recurse-submodules",
"--progress", cloneUrl,
]; outputPath,
"--progress",
await spawnAsync("git", cloneArgs, (data) => { ],
if (writeStream.writable) { (data) => {
writeStream.write(data); if (writeStream.writable) {
} writeStream.write(data);
}); }
},
);
writeStream.write(`\nCloned ${repoclone}: ✅\n`); writeStream.write(`\nCloned ${repoclone}: ✅\n`);
} catch (error) { } catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`); writeStream.write(`ERROR Clonning: ${error}: ❌`);
@@ -161,15 +161,7 @@ export const getGithubCloneCommand = async ({
type = "application", type = "application",
...entity ...entity
}: CloneGithubRepository & { serverId: string }) => { }: CloneGithubRepository & { serverId: string }) => {
const { const { appName, repository, owner, branch, githubId, serverId } = entity;
appName,
repository,
owner,
branch,
githubId,
serverId,
enableSubmodules,
} = entity;
const isCompose = type === "compose"; const isCompose = type === "compose";
if (!serverId) { if (!serverId) {
throw new TRPCError({ throw new TRPCError({
@@ -224,7 +216,7 @@ export const getGithubCloneCommand = async ({
const cloneCommand = ` const cloneCommand = `
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
if ! git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then if ! git clone --branch ${branch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath}; echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath};
exit 1; exit 1;
fi fi
@@ -235,8 +227,7 @@ echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
}; };
export const cloneRawGithubRepository = async (entity: Compose) => { export const cloneRawGithubRepository = async (entity: Compose) => {
const { appName, repository, owner, branch, githubId, enableSubmodules } = const { appName, repository, owner, branch, githubId } = entity;
entity;
if (!githubId) { if (!githubId) {
throw new TRPCError({ throw new TRPCError({
@@ -254,33 +245,24 @@ export const cloneRawGithubRepository = async (entity: Compose) => {
await recreateDirectory(outputPath); await recreateDirectory(outputPath);
const cloneUrl = `https://oauth2:${token}@${repoclone}`; const cloneUrl = `https://oauth2:${token}@${repoclone}`;
try { try {
const cloneArgs = [ await spawnAsync("git", [
"clone", "clone",
"--branch", "--branch",
branch!, branch!,
"--depth", "--depth",
"1", "1",
...(enableSubmodules ? ["--recurse-submodules"] : []), "--recurse-submodules",
cloneUrl, cloneUrl,
outputPath, outputPath,
"--progress", "--progress",
]; ]);
await spawnAsync("git", cloneArgs);
} catch (error) { } catch (error) {
throw error; throw error;
} }
}; };
export const cloneRawGithubRepositoryRemote = async (compose: Compose) => { export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
const { const { appName, repository, owner, branch, githubId, serverId } = compose;
appName,
repository,
owner,
branch,
githubId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) { if (!serverId) {
throw new TRPCError({ throw new TRPCError({
@@ -306,7 +288,7 @@ export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
try { try {
const command = ` const command = `
rm -rf ${outputPath}; rm -rf ${outputPath};
git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}
`; `;
await execAsyncRemote(serverId, command); await execAsyncRemote(serverId, command);
} catch (error) { } catch (error) {

View File

@@ -90,14 +90,8 @@ export const cloneGitlabRepository = async (
isCompose = false, isCompose = false,
) => { ) => {
const writeStream = createWriteStream(logPath, { flags: "a" }); const writeStream = createWriteStream(logPath, { flags: "a" });
const { const { appName, gitlabBranch, gitlabId, gitlab, gitlabPathNamespace } =
appName, entity;
gitlabBranch,
gitlabId,
gitlab,
gitlabPathNamespace,
enableSubmodules,
} = entity;
if (!gitlabId) { if (!gitlabId) {
throw new TRPCError({ throw new TRPCError({
@@ -133,23 +127,25 @@ export const cloneGitlabRepository = async (
try { try {
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`); writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
const cloneArgs = [ await spawnAsync(
"clone", "git",
"--branch", [
gitlabBranch!, "clone",
"--depth", "--branch",
"1", gitlabBranch!,
...(enableSubmodules ? ["--recurse-submodules"] : []), "--depth",
cloneUrl, "1",
outputPath, "--recurse-submodules",
"--progress", cloneUrl,
]; outputPath,
"--progress",
await spawnAsync("git", cloneArgs, (data) => { ],
if (writeStream.writable) { (data) => {
writeStream.write(data); if (writeStream.writable) {
} writeStream.write(data);
}); }
},
);
writeStream.write(`\nCloned ${repoclone}: ✅\n`); writeStream.write(`\nCloned ${repoclone}: ✅\n`);
} catch (error) { } catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`); writeStream.write(`ERROR Clonning: ${error}: ❌`);
@@ -171,7 +167,6 @@ export const getGitlabCloneCommand = async (
gitlabId, gitlabId,
serverId, serverId,
gitlab, gitlab,
enableSubmodules,
} = entity; } = entity;
if (!serverId) { if (!serverId) {
@@ -227,7 +222,7 @@ export const getGitlabCloneCommand = async (
const cloneCommand = ` const cloneCommand = `
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
if ! git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then if ! git clone --branch ${gitlabBranch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath}; echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
exit 1; exit 1;
fi fi
@@ -269,11 +264,7 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
const groupName = gitlabProvider.groupName?.toLowerCase(); const groupName = gitlabProvider.groupName?.toLowerCase();
if (groupName) { if (groupName) {
const isIncluded = groupName return full_path.toLowerCase().includes(groupName) && kind === "group";
.split(",")
.some((name) => full_path.toLowerCase().includes(name));
return isIncluded && kind === "group";
} }
return kind === "user"; return kind === "user";
}); });
@@ -335,13 +326,7 @@ export const getGitlabBranches = async (input: {
}; };
export const cloneRawGitlabRepository = async (entity: Compose) => { export const cloneRawGitlabRepository = async (entity: Compose) => {
const { const { appName, gitlabBranch, gitlabId, gitlabPathNamespace } = entity;
appName,
gitlabBranch,
gitlabId,
gitlabPathNamespace,
enableSubmodules,
} = entity;
if (!gitlabId) { if (!gitlabId) {
throw new TRPCError({ throw new TRPCError({
@@ -362,32 +347,24 @@ export const cloneRawGitlabRepository = async (entity: Compose) => {
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`; const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
try { try {
const cloneArgs = [ await spawnAsync("git", [
"clone", "clone",
"--branch", "--branch",
gitlabBranch!, gitlabBranch!,
"--depth", "--depth",
"1", "1",
...(enableSubmodules ? ["--recurse-submodules"] : []), "--recurse-submodules",
cloneUrl, cloneUrl,
outputPath, outputPath,
"--progress", "--progress",
]; ]);
await spawnAsync("git", cloneArgs);
} catch (error) { } catch (error) {
throw error; throw error;
} }
}; };
export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => { export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
const { const { appName, gitlabPathNamespace, branch, gitlabId, serverId } = compose;
appName,
gitlabPathNamespace,
branch,
gitlabId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) { if (!serverId) {
throw new TRPCError({ throw new TRPCError({
@@ -411,7 +388,7 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
try { try {
const command = ` const command = `
rm -rf ${outputPath}; rm -rf ${outputPath};
git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} git clone --branch ${branch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath}
`; `;
await execAsyncRemote(serverId, command); await execAsyncRemote(serverId, command);
} catch (error) { } catch (error) {
@@ -454,9 +431,7 @@ export const testGitlabConnection = async (
const { full_path, kind } = repo.namespace; const { full_path, kind } = repo.namespace;
if (groupName) { if (groupName) {
return groupName return full_path.toLowerCase().includes(groupName) && kind === "group";
.split(",")
.some((name) => full_path.toLowerCase().includes(name));
} }
return kind === "user"; return kind === "user";
}); });

View File

@@ -83,54 +83,44 @@ export const restoreWebServerBackup = async (
throw new Error("Database file not found after extraction"); throw new Error("Database file not found after extraction");
} }
const { stdout: postgresContainer } = await execAsync(
`docker ps --filter "name=dokploy-postgres" --filter "status=running" -q | head -n 1`,
);
if (!postgresContainer) {
throw new Error("Dokploy Postgres container not found");
}
const postgresContainerId = postgresContainer.trim();
// Drop and recreate database // Drop and recreate database
emit("Disconnecting all users from database..."); emit("Disconnecting all users from database...");
await execAsync( await execAsync(
`docker exec ${postgresContainerId} psql -U dokploy postgres -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'dokploy' AND pid <> pg_backend_pid();"`, `docker exec $(docker ps --filter "name=dokploy-postgres" -q) psql -U dokploy postgres -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'dokploy' AND pid <> pg_backend_pid();"`,
); );
emit("Dropping existing database..."); emit("Dropping existing database...");
await execAsync( await execAsync(
`docker exec ${postgresContainerId} psql -U dokploy postgres -c "DROP DATABASE IF EXISTS dokploy;"`, `docker exec $(docker ps --filter "name=dokploy-postgres" -q) psql -U dokploy postgres -c "DROP DATABASE IF EXISTS dokploy;"`,
); );
emit("Creating fresh database..."); emit("Creating fresh database...");
await execAsync( await execAsync(
`docker exec ${postgresContainerId} psql -U dokploy postgres -c "CREATE DATABASE dokploy;"`, `docker exec $(docker ps --filter "name=dokploy-postgres" -q) psql -U dokploy postgres -c "CREATE DATABASE dokploy;"`,
); );
// Copy the backup file into the container // Copy the backup file into the container
emit("Copying backup file into container..."); emit("Copying backup file into container...");
await execAsync( await execAsync(
`docker cp ${tempDir}/database.sql ${postgresContainerId}:/tmp/database.sql`, `docker cp ${tempDir}/database.sql $(docker ps --filter "name=dokploy-postgres" -q):/tmp/database.sql`,
); );
// Verify file in container // Verify file in container
emit("Verifying file in container..."); emit("Verifying file in container...");
await execAsync( await execAsync(
`docker exec ${postgresContainerId} ls -l /tmp/database.sql`, `docker exec $(docker ps --filter "name=dokploy-postgres" -q) ls -l /tmp/database.sql`,
); );
// Restore from the copied file // Restore from the copied file
emit("Running database restore..."); emit("Running database restore...");
await execAsync( await execAsync(
`docker exec ${postgresContainerId} pg_restore -v -U dokploy -d dokploy /tmp/database.sql`, `docker exec $(docker ps --filter "name=dokploy-postgres" -q) pg_restore -v -U dokploy -d dokploy /tmp/database.sql`,
); );
// Cleanup the temporary file in the container // Cleanup the temporary file in the container
emit("Cleaning up container temp file..."); emit("Cleaning up container temp file...");
await execAsync( await execAsync(
`docker exec ${postgresContainerId} rm /tmp/database.sql`, `docker exec $(docker ps --filter "name=dokploy-postgres" -q) rm /tmp/database.sql`,
); );
emit("Restore completed successfully!"); emit("Restore completed successfully!");

View File

@@ -37,9 +37,9 @@ export const updateServerTraefik = (
servers: [ servers: [
{ {
url: `http://dokploy:${process.env.PORT || 3000}`, url: `http://dokploy:${process.env.PORT || 3000}`,
passHostHeader: true,
}, },
], ],
passHostHeader: true,
}, },
}, },
}; };

88
pnpm-lock.yaml generated
View File

@@ -17,7 +17,7 @@ importers:
version: 1.9.4 version: 1.9.4
'@commitlint/cli': '@commitlint/cli':
specifier: ^19.3.0 specifier: ^19.3.0
version: 19.3.0(@types/node@18.19.42)(typescript@5.8.3) version: 19.3.0(@types/node@18.19.42)(typescript@5.7.2)
'@commitlint/config-conventional': '@commitlint/config-conventional':
specifier: ^19.2.2 specifier: ^19.2.2
version: 19.2.2 version: 19.2.2
@@ -266,8 +266,8 @@ importers:
specifier: 5.1.1 specifier: 5.1.1
version: 5.1.1(encoding@0.1.13) version: 5.1.1(encoding@0.1.13)
better-auth: better-auth:
specifier: 1.2.6 specifier: 1.2.4
version: 1.2.6 version: 1.2.4(typescript@5.5.3)
bl: bl:
specifier: 6.0.11 specifier: 6.0.11
version: 6.0.11 version: 6.0.11
@@ -607,8 +607,8 @@ importers:
specifier: ^0.0.13 specifier: ^0.0.13
version: 0.0.13(zod@3.23.8) version: 0.0.13(zod@3.23.8)
'@better-auth/utils': '@better-auth/utils':
specifier: 0.2.4 specifier: 0.2.3
version: 0.2.4 version: 0.2.3
'@faker-js/faker': '@faker-js/faker':
specifier: ^8.4.1 specifier: ^8.4.1
version: 8.4.1 version: 8.4.1
@@ -640,8 +640,8 @@ importers:
specifier: 5.1.1 specifier: 5.1.1
version: 5.1.1(encoding@0.1.13) version: 5.1.1(encoding@0.1.13)
better-auth: better-auth:
specifier: 1.2.6 specifier: 1.2.4
version: 1.2.6 version: 1.2.4(typescript@5.5.3)
bl: bl:
specifier: 6.0.11 specifier: 6.0.11
version: 6.0.11 version: 6.0.11
@@ -924,11 +924,11 @@ packages:
'@balena/dockerignore@1.0.2': '@balena/dockerignore@1.0.2':
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
'@better-auth/utils@0.2.4': '@better-auth/utils@0.2.3':
resolution: {integrity: sha512-ayiX87Xd5sCHEplAdeMgwkA0FgnXsEZBgDn890XHHwSWNqqRZDYOq3uj2Ei2leTv1I2KbG5HHn60Ah1i2JWZjQ==} resolution: {integrity: sha512-Ap1GaSmo6JYhJhxJOpUB0HobkKPTNzfta+bLV89HfpyCAHN7p8ntCrmNFHNAVD0F6v0mywFVEUg1FUhNCc81Rw==}
'@better-fetch/fetch@1.1.18': '@better-fetch/fetch@1.1.15':
resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} resolution: {integrity: sha512-0Bl8YYj1f8qCTNHeSn5+1DWv2hy7rLBrQ8rS8Y9XYloiwZEfc3k4yspIG0llRxafxqhGCwlGRg+F8q1HZRCMXA==}
'@biomejs/biome@1.9.4': '@biomejs/biome@1.9.4':
resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==}
@@ -3836,11 +3836,11 @@ packages:
before-after-hook@2.2.3: before-after-hook@2.2.3:
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
better-auth@1.2.6: better-auth@1.2.4:
resolution: {integrity: sha512-RVy6nfNCXpohx49zP2ChUO3zN0nvz5UXuETJIhWU+dshBKpFMk4P4hAQauM3xqTJdd9hfeB5y+segmG1oYGTJQ==} resolution: {integrity: sha512-/ZK2jbUjm8JwdeCLFrUWUBmexPyI9PkaLVXWLWtN60sMDHTY8B5G72wcHglo1QMFBaw4G0qFkP5ayl9k6XfDaA==}
better-call@1.0.7: better-call@1.0.3:
resolution: {integrity: sha512-p5kEthErx3HsW9dCCvvEx+uuEdncn0ZrlqrOG3TkR1aVYgynpwYbTVU90nY8/UwfMhROzqZWs8vryainSQxrNg==} resolution: {integrity: sha512-DUKImKoDIy5UtCvQbHTg0wuBRse6gu1Yvznn7+1B3I5TeY8sclRPFce0HI+4WF2bcb+9PqmkET8nXZubrHQh9A==}
binary-extensions@2.3.0: binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
@@ -7093,8 +7093,8 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
typescript@5.8.3: typescript@5.7.2:
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
@@ -7210,6 +7210,14 @@ packages:
v8-compile-cache-lib@3.0.1: v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
valibot@1.0.0-beta.15:
resolution: {integrity: sha512-BKy8XosZkDHWmYC+cJG74LBzP++Gfntwi33pP3D3RKztz2XV9jmFWnkOi21GoqARP8wAWARwhV6eTr1JcWzjGw==}
peerDependencies:
typescript: '>=5'
peerDependenciesMeta:
typescript:
optional: true
vfile-message@4.0.2: vfile-message@4.0.2:
resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==}
@@ -7559,12 +7567,11 @@ snapshots:
'@balena/dockerignore@1.0.2': {} '@balena/dockerignore@1.0.2': {}
'@better-auth/utils@0.2.4': '@better-auth/utils@0.2.3':
dependencies: dependencies:
typescript: 5.8.3
uncrypto: 0.1.3 uncrypto: 0.1.3
'@better-fetch/fetch@1.1.18': {} '@better-fetch/fetch@1.1.15': {}
'@biomejs/biome@1.9.4': '@biomejs/biome@1.9.4':
optionalDependencies: optionalDependencies:
@@ -7671,11 +7678,11 @@ snapshots:
style-mod: 4.1.2 style-mod: 4.1.2
w3c-keyname: 2.2.8 w3c-keyname: 2.2.8
'@commitlint/cli@19.3.0(@types/node@18.19.42)(typescript@5.8.3)': '@commitlint/cli@19.3.0(@types/node@18.19.42)(typescript@5.7.2)':
dependencies: dependencies:
'@commitlint/format': 19.3.0 '@commitlint/format': 19.3.0
'@commitlint/lint': 19.2.2 '@commitlint/lint': 19.2.2
'@commitlint/load': 19.2.0(@types/node@18.19.42)(typescript@5.8.3) '@commitlint/load': 19.2.0(@types/node@18.19.42)(typescript@5.7.2)
'@commitlint/read': 19.2.1 '@commitlint/read': 19.2.1
'@commitlint/types': 19.0.3 '@commitlint/types': 19.0.3
execa: 8.0.1 execa: 8.0.1
@@ -7722,15 +7729,15 @@ snapshots:
'@commitlint/rules': 19.0.3 '@commitlint/rules': 19.0.3
'@commitlint/types': 19.0.3 '@commitlint/types': 19.0.3
'@commitlint/load@19.2.0(@types/node@18.19.42)(typescript@5.8.3)': '@commitlint/load@19.2.0(@types/node@18.19.42)(typescript@5.7.2)':
dependencies: dependencies:
'@commitlint/config-validator': 19.0.3 '@commitlint/config-validator': 19.0.3
'@commitlint/execute-rule': 19.0.0 '@commitlint/execute-rule': 19.0.0
'@commitlint/resolve-extends': 19.1.0 '@commitlint/resolve-extends': 19.1.0
'@commitlint/types': 19.0.3 '@commitlint/types': 19.0.3
chalk: 5.3.0 chalk: 5.3.0
cosmiconfig: 9.0.0(typescript@5.8.3) cosmiconfig: 9.0.0(typescript@5.7.2)
cosmiconfig-typescript-loader: 5.0.0(@types/node@18.19.42)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3) cosmiconfig-typescript-loader: 5.0.0(@types/node@18.19.42)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2)
lodash.isplainobject: 4.0.6 lodash.isplainobject: 4.0.6
lodash.merge: 4.6.2 lodash.merge: 4.6.2
lodash.uniq: 4.5.0 lodash.uniq: 4.5.0
@@ -10540,24 +10547,27 @@ snapshots:
before-after-hook@2.2.3: {} before-after-hook@2.2.3: {}
better-auth@1.2.6: better-auth@1.2.4(typescript@5.5.3):
dependencies: dependencies:
'@better-auth/utils': 0.2.4 '@better-auth/utils': 0.2.3
'@better-fetch/fetch': 1.1.18 '@better-fetch/fetch': 1.1.15
'@noble/ciphers': 0.6.0 '@noble/ciphers': 0.6.0
'@noble/hashes': 1.7.1 '@noble/hashes': 1.7.1
'@simplewebauthn/browser': 13.1.0 '@simplewebauthn/browser': 13.1.0
'@simplewebauthn/server': 13.1.1 '@simplewebauthn/server': 13.1.1
better-call: 1.0.7 better-call: 1.0.3
defu: 6.1.4 defu: 6.1.4
jose: 5.9.6 jose: 5.9.6
kysely: 0.27.6 kysely: 0.27.6
nanostores: 0.11.3 nanostores: 0.11.3
valibot: 1.0.0-beta.15(typescript@5.5.3)
zod: 3.24.1 zod: 3.24.1
transitivePeerDependencies:
- typescript
better-call@1.0.7: better-call@1.0.3:
dependencies: dependencies:
'@better-fetch/fetch': 1.1.18 '@better-fetch/fetch': 1.1.15
rou3: 0.5.1 rou3: 0.5.1
set-cookie-parser: 2.7.1 set-cookie-parser: 2.7.1
uncrypto: 0.1.3 uncrypto: 0.1.3
@@ -10932,21 +10942,21 @@ snapshots:
core-js@3.39.0: {} core-js@3.39.0: {}
cosmiconfig-typescript-loader@5.0.0(@types/node@18.19.42)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3): cosmiconfig-typescript-loader@5.0.0(@types/node@18.19.42)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2):
dependencies: dependencies:
'@types/node': 18.19.42 '@types/node': 18.19.42
cosmiconfig: 9.0.0(typescript@5.8.3) cosmiconfig: 9.0.0(typescript@5.7.2)
jiti: 1.21.6 jiti: 1.21.6
typescript: 5.8.3 typescript: 5.7.2
cosmiconfig@9.0.0(typescript@5.8.3): cosmiconfig@9.0.0(typescript@5.7.2):
dependencies: dependencies:
env-paths: 2.2.1 env-paths: 2.2.1
import-fresh: 3.3.0 import-fresh: 3.3.0
js-yaml: 4.1.0 js-yaml: 4.1.0
parse-json: 5.2.0 parse-json: 5.2.0
optionalDependencies: optionalDependencies:
typescript: 5.8.3 typescript: 5.7.2
cpu-features@0.0.10: cpu-features@0.0.10:
dependencies: dependencies:
@@ -14161,7 +14171,7 @@ snapshots:
typescript@5.5.3: {} typescript@5.5.3: {}
typescript@5.8.3: {} typescript@5.7.2: {}
ufo@1.5.4: {} ufo@1.5.4: {}
@@ -14282,6 +14292,10 @@ snapshots:
v8-compile-cache-lib@3.0.1: v8-compile-cache-lib@3.0.1:
optional: true optional: true
valibot@1.0.0-beta.15(typescript@5.5.3):
optionalDependencies:
typescript: 5.5.3
vfile-message@4.0.2: vfile-message@4.0.2:
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3