mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-18 20:35:24 +02:00
Compare commits
1 Commits
v0.29.9
...
fix/projec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b354c3cb0f |
@@ -1,50 +0,0 @@
|
||||
import { redactRcloneCredentials } from "@dokploy/server/utils/backups/redact";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("redactRcloneCredentials (#4621)", () => {
|
||||
it("should redact access key in rclone command", () => {
|
||||
const cmd =
|
||||
'rclone rcat --s3-access-key-id="AKIAIOSFODNN7EXAMPLE" --s3-secret-access-key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" :s3:bucket/file.gz';
|
||||
const redacted = redactRcloneCredentials(cmd);
|
||||
expect(redacted).not.toContain("AKIAIOSFODNN7EXAMPLE");
|
||||
expect(redacted).toContain('--s3-access-key-id="[REDACTED]"');
|
||||
});
|
||||
|
||||
it("should redact secret access key in rclone command", () => {
|
||||
const cmd =
|
||||
'rclone rcat --s3-access-key-id="key" --s3-secret-access-key="supersecret" :s3:bucket/file.gz';
|
||||
const redacted = redactRcloneCredentials(cmd);
|
||||
expect(redacted).not.toContain("supersecret");
|
||||
expect(redacted).toContain('--s3-secret-access-key="[REDACTED]"');
|
||||
});
|
||||
|
||||
it("should redact both credentials simultaneously", () => {
|
||||
const cmd =
|
||||
'rclone lsf --s3-access-key-id="AKIA123" --s3-secret-access-key="secret456" --s3-region="us-east-1" :s3:bucket/';
|
||||
const redacted = redactRcloneCredentials(cmd);
|
||||
expect(redacted).not.toContain("AKIA123");
|
||||
expect(redacted).not.toContain("secret456");
|
||||
expect(redacted).toContain('--s3-region="us-east-1"');
|
||||
});
|
||||
|
||||
it("should not modify non-credential flags", () => {
|
||||
const cmd =
|
||||
'rclone rcat --s3-region="eu-west-1" --s3-endpoint="https://s3.example.com" --s3-no-check-bucket :s3:bucket/file.gz';
|
||||
const redacted = redactRcloneCredentials(cmd);
|
||||
expect(redacted).toBe(cmd);
|
||||
});
|
||||
|
||||
it("should handle commands with no credentials", () => {
|
||||
const cmd = "rclone lsf :s3:bucket/";
|
||||
expect(redactRcloneCredentials(cmd)).toBe(cmd);
|
||||
});
|
||||
|
||||
it("should handle error strings containing credentials", () => {
|
||||
const errorStr =
|
||||
'Error: Command failed: rclone lsf --s3-access-key-id="MYKEY" --s3-secret-access-key="MYSECRET" :s3:bucket/';
|
||||
const redacted = redactRcloneCredentials(errorStr);
|
||||
expect(redacted).not.toContain("MYKEY");
|
||||
expect(redacted).not.toContain("MYSECRET");
|
||||
expect(redacted).toContain("[REDACTED]");
|
||||
});
|
||||
});
|
||||
@@ -28,7 +28,7 @@ export const ShowInternalLibsqlCredentials = ({ libsqlId }: Props) => {
|
||||
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>User</Label>
|
||||
<Input enableCopyButton disabled value={data?.databaseUser} />
|
||||
<Input disabled value={data?.databaseUser} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Sqld Node</Label>
|
||||
@@ -71,7 +71,7 @@ export const ShowInternalLibsqlCredentials = ({ libsqlId }: Props) => {
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Internal Host</Label>
|
||||
<Input enableCopyButton disabled value={data?.appName} />
|
||||
<Input disabled value={data?.appName} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Enable Namespaces</Label>
|
||||
|
||||
@@ -25,11 +25,11 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>User</Label>
|
||||
<Input enableCopyButton disabled value={data?.databaseUser} />
|
||||
<Input disabled value={data?.databaseUser} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Database Name</Label>
|
||||
<Input enableCopyButton disabled value={data?.databaseName} />
|
||||
<Input disabled value={data?.databaseName} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
@@ -79,7 +79,7 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Internal Host</Label>
|
||||
<Input enableCopyButton disabled value={data?.appName} />
|
||||
<Input disabled value={data?.appName} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
|
||||
@@ -25,7 +25,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>User</Label>
|
||||
<Input enableCopyButton disabled value={data?.databaseUser} />
|
||||
<Input disabled value={data?.databaseUser} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -55,7 +55,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Internal Host</Label>
|
||||
<Input enableCopyButton disabled value={data?.appName} />
|
||||
<Input disabled value={data?.appName} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
|
||||
@@ -25,11 +25,11 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>User</Label>
|
||||
<Input enableCopyButton disabled value={data?.databaseUser} />
|
||||
<Input disabled value={data?.databaseUser} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Database Name</Label>
|
||||
<Input enableCopyButton disabled value={data?.databaseName} />
|
||||
<Input disabled value={data?.databaseName} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
@@ -79,7 +79,7 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Internal Host</Label>
|
||||
<Input enableCopyButton disabled value={data?.appName} />
|
||||
<Input disabled value={data?.appName} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
|
||||
@@ -25,11 +25,11 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>User</Label>
|
||||
<Input enableCopyButton disabled value={data?.databaseUser} />
|
||||
<Input disabled value={data?.databaseUser} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Database Name</Label>
|
||||
<Input enableCopyButton disabled value={data?.databaseName} />
|
||||
<Input disabled value={data?.databaseName} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
@@ -57,7 +57,7 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Internal Host</Label>
|
||||
<Input enableCopyButton disabled value={data?.appName} />
|
||||
<Input disabled value={data?.appName} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
@@ -62,7 +62,7 @@ const dockerImageDefaultPlaceholder: Record<DbType, string> = {
|
||||
mariadb: "mariadb:11",
|
||||
mysql: "mysql:8",
|
||||
postgres: "postgres:18",
|
||||
redis: "redis:8",
|
||||
redis: "redis:7",
|
||||
};
|
||||
|
||||
const databasesUserDefaultPlaceholder: Record<
|
||||
|
||||
@@ -47,7 +47,7 @@ interface Details {
|
||||
envVariables: EnvVariable[];
|
||||
shortDescription: string;
|
||||
domains: Domain[];
|
||||
configFiles?: Mount[] | null;
|
||||
configFiles?: Mount[];
|
||||
}
|
||||
|
||||
interface Mount {
|
||||
|
||||
@@ -25,7 +25,7 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
|
||||
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>User</Label>
|
||||
<Input enableCopyButton disabled value="default" />
|
||||
<Input disabled value="default" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
@@ -53,7 +53,7 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Internal Host</Label>
|
||||
<Input enableCopyButton disabled value={data?.appName} />
|
||||
<Input disabled value={data?.appName} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
|
||||
@@ -131,10 +131,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
const apiUrl = form.watch("apiUrl");
|
||||
const apiKey = form.watch("apiKey");
|
||||
|
||||
// Any Ollama instance on the default port 11434 is treated as no-auth
|
||||
// (covers localhost and self-hosted LAN deployments). Ollama Cloud
|
||||
// (ollama.com on 443) falls through and requires an API key.
|
||||
const isLocalOllama = apiUrl.includes(":11434");
|
||||
const isOllama = apiUrl.includes(":11434") || apiUrl.includes("ollama");
|
||||
const {
|
||||
data: models,
|
||||
isFetching: isLoadingServerModels,
|
||||
@@ -145,7 +142,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
apiKey: apiKey ?? "",
|
||||
},
|
||||
{
|
||||
enabled: !!apiUrl && (isLocalOllama || !!apiKey),
|
||||
enabled: !!apiUrl && (isOllama || !!apiKey),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -278,7 +275,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
{!isLocalOllama && (
|
||||
{!isOllama && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="apiKey"
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import copy from "copy-to-clipboard";
|
||||
import { Clipboard, EyeIcon, EyeOffIcon, RefreshCcw } from "lucide-react";
|
||||
import { EyeIcon, EyeOffIcon, RefreshCcw } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { generateRandomPassword } from "@/lib/password-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "./button";
|
||||
|
||||
export interface InputProps extends React.ComponentProps<"input"> {
|
||||
errorMessage?: string;
|
||||
enablePasswordGenerator?: boolean;
|
||||
passwordGeneratorLength?: number;
|
||||
enableCopyButton?: boolean;
|
||||
}
|
||||
|
||||
function Input({
|
||||
@@ -20,7 +16,6 @@ function Input({
|
||||
errorMessage,
|
||||
enablePasswordGenerator = false,
|
||||
passwordGeneratorLength,
|
||||
enableCopyButton = false,
|
||||
ref,
|
||||
...props
|
||||
}: InputProps) {
|
||||
@@ -70,67 +65,49 @@ function Input({
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
copy(inputRef.current?.value || "");
|
||||
toast.success("Value is copied to clipboard");
|
||||
};
|
||||
|
||||
const inputElement = (
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
type={inputType}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-10 w-full min-w-0 rounded-lg border border-input bg-transparent px-3 py-2 text-sm transition-colors outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
isPassword && (shouldShowGenerator ? "pr-16" : "pr-10"),
|
||||
className,
|
||||
)}
|
||||
ref={setRefs}
|
||||
{...props}
|
||||
/>
|
||||
{isPassword && (
|
||||
<div className="absolute inset-y-0 right-0 flex items-center gap-1 pr-3 text-muted-foreground">
|
||||
{shouldShowGenerator && (
|
||||
return (
|
||||
<>
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
type={inputType}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-10 w-full min-w-0 rounded-lg border border-input bg-transparent px-3 py-2 text-sm transition-colors outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
isPassword && (shouldShowGenerator ? "pr-16" : "pr-10"),
|
||||
className,
|
||||
)}
|
||||
ref={setRefs}
|
||||
{...props}
|
||||
/>
|
||||
{isPassword && (
|
||||
<div className="absolute inset-y-0 right-0 flex items-center gap-1 pr-3 text-muted-foreground">
|
||||
{shouldShowGenerator && (
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-foreground focus:outline-none"
|
||||
onClick={handleGeneratePassword}
|
||||
aria-label="Generate password"
|
||||
title="Generate password"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-foreground focus:outline-none"
|
||||
onClick={handleGeneratePassword}
|
||||
aria-label="Generate password"
|
||||
title="Generate password"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
{showPassword ? (
|
||||
<EyeOffIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<EyeIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-foreground focus:outline-none"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOffIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<EyeIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{enableCopyButton ? (
|
||||
<div className="flex w-full items-center space-x-2">
|
||||
{inputElement}
|
||||
<Button type="button" variant={"secondary"} onClick={handleCopy}>
|
||||
<Clipboard className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
inputElement
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{errorMessage && (
|
||||
<span className="text-sm text-red-600 text-secondary-foreground">
|
||||
{errorMessage}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.29.9",
|
||||
"version": "v0.29.8",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -54,20 +54,6 @@ const Page = ({ isCloud }: Props) => {
|
||||
</EnterpriseFeatureGate>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||
<div className="rounded-xl bg-background shadow-md">
|
||||
<EnterpriseFeatureGate
|
||||
lockedProps={{
|
||||
title: "Application Authentication",
|
||||
description:
|
||||
"Protect deployed applications behind an OIDC SSO gate (oauth2-proxy). Part of Dokploy Enterprise.",
|
||||
ctaLabel: "Go to License",
|
||||
}}
|
||||
>
|
||||
<ForwardAuthServers />
|
||||
</EnterpriseFeatureGate>
|
||||
</div>
|
||||
</Card>
|
||||
{!isCloud && (
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||
<div className="rounded-xl bg-background shadow-md">
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
findAllDeploymentsByServerId,
|
||||
findAllDeploymentsCentralized,
|
||||
findDeploymentById,
|
||||
findScheduleById,
|
||||
IS_CLOUD,
|
||||
removeDeployment,
|
||||
resolveServicePath,
|
||||
@@ -127,29 +126,9 @@ export const deploymentRouter = createTRPCRouter({
|
||||
allByType: protectedProcedure
|
||||
.input(apiFindAllByType)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (input.type === "schedule") {
|
||||
const schedule = await findScheduleById(input.id);
|
||||
const serviceId = schedule.applicationId || schedule.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
deployment: ["read"],
|
||||
});
|
||||
} else if (schedule.serverId) {
|
||||
const targetServer = await findServerById(schedule.serverId);
|
||||
if (
|
||||
targetServer.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this schedule.",
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||
deployment: ["read"],
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||
deployment: ["read"],
|
||||
});
|
||||
const deploymentsList = await db.query.deployments.findMany({
|
||||
where: eq(deployments[`${input.type}Id`], input.id),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
|
||||
@@ -24,7 +24,7 @@ interface DockerOutput {
|
||||
dockerCompose: string;
|
||||
envVariables: Array<{ name: string; value: string }>;
|
||||
domains: Array<{ host: string; port: number; serviceName: string }>;
|
||||
configFiles?: Array<{ content: string; filePath: string }> | null;
|
||||
configFiles?: Array<{ content: string; filePath: string }>;
|
||||
}
|
||||
|
||||
export const getAiSettingsByOrganizationId = async (organizationId: string) => {
|
||||
@@ -136,7 +136,7 @@ export const suggestVariants = async ({
|
||||
filePath: z.string(),
|
||||
}),
|
||||
)
|
||||
.nullable(),
|
||||
.optional(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
@@ -198,12 +198,12 @@ export const suggestVariants = async ({
|
||||
1. ALWAYS use 'image:' field, NEVER use 'build:' field
|
||||
2. NEVER use 'build: .' or any build directive - we don't have local Dockerfiles
|
||||
3. Use images from Docker Hub or other public registries (e.g., docker.io, ghcr.io, quay.io)
|
||||
4. For dependencies (databases, redis, etc.), use official images (e.g., postgres:16, redis:8, etc.)
|
||||
4. For dependencies (databases, redis, etc.), use official images (e.g., postgres:16, redis:7, etc.)
|
||||
5. Always specify image tags - avoid using 'latest' tag, use specific versions when possible
|
||||
6. Examples of correct image usage:
|
||||
- image: sendingtk/chatwoot:develop
|
||||
- image: postgres:16-alpine
|
||||
- image: redis:8-alpine
|
||||
- image: redis:7-alpine
|
||||
7. Examples of INCORRECT usage (DO NOT USE):
|
||||
- build: .
|
||||
- build: ./app
|
||||
|
||||
@@ -3,7 +3,7 @@ import { docker } from "../constants";
|
||||
import { pullImage } from "../utils/docker/utils";
|
||||
|
||||
export const initializeRedis = async () => {
|
||||
const imageName = "redis:8";
|
||||
const imageName = "redis:7";
|
||||
const containerName = "dokploy-redis";
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
|
||||
@@ -74,10 +74,8 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
|
||||
});
|
||||
case "ollama":
|
||||
return createOllama({
|
||||
// optional settings, e.g.
|
||||
baseURL: config.apiUrl,
|
||||
headers: config.apiKey
|
||||
? { Authorization: `Bearer ${config.apiKey}` }
|
||||
: undefined,
|
||||
});
|
||||
case "deepinfra":
|
||||
return createDeepInfra({
|
||||
|
||||
@@ -11,7 +11,6 @@ import { startLogCleanup } from "../access-log/handler";
|
||||
import { cleanupAll } from "../docker/utils";
|
||||
import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { redactRcloneCredentials } from "./redact";
|
||||
import { getS3Credentials, normalizeS3Path, scheduleBackup } from "./utils";
|
||||
|
||||
export const initCronJobs = async () => {
|
||||
@@ -154,6 +153,6 @@ export const keepLatestNBackups = async (
|
||||
await execAsync(rcloneCommand);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(redactRcloneCredentials(String(error)));
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* Redacts S3 credentials from rclone command strings.
|
||||
*
|
||||
* Used to prevent credential leakage in structured logs and error output.
|
||||
* Matches the flag format produced by `getS3Credentials()`:
|
||||
* --s3-access-key-id="VALUE" and --s3-secret-access-key="VALUE"
|
||||
*/
|
||||
export const redactRcloneCredentials = (command: string): string => {
|
||||
return command
|
||||
.replace(/(--s3-access-key-id=)"[^"]*"/g, '$1"[REDACTED]"')
|
||||
.replace(/(--s3-secret-access-key=)"[^"]*"/g, '$1"[REDACTED]"');
|
||||
};
|
||||
@@ -9,7 +9,6 @@ import { runMariadbBackup } from "./mariadb";
|
||||
import { runMongoBackup } from "./mongo";
|
||||
import { runMySqlBackup } from "./mysql";
|
||||
import { runPostgresBackup } from "./postgres";
|
||||
import { redactRcloneCredentials } from "./redact";
|
||||
import { runWebServerBackup } from "./web-server";
|
||||
|
||||
export const scheduleBackup = (backup: BackupSchedule) => {
|
||||
@@ -263,7 +262,7 @@ export const getBackupCommand = (
|
||||
{
|
||||
containerSearch,
|
||||
backupCommand,
|
||||
rcloneCommand: redactRcloneCredentials(rcloneCommand),
|
||||
rcloneCommand,
|
||||
logPath,
|
||||
},
|
||||
`Executing backup command: ${backup.databaseType} ${backup.backupType}`,
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||
import { sendDokployBackupNotifications } from "../notifications/dokploy-backup";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { redactRcloneCredentials } from "./redact";
|
||||
import { getBackupTimestamp, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
|
||||
function formatBytes(bytes?: number) {
|
||||
@@ -114,23 +113,20 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
try {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
} catch (cleanupError) {
|
||||
console.error(
|
||||
"Cleanup error:",
|
||||
redactRcloneCredentials(String(cleanupError)),
|
||||
);
|
||||
console.error("Cleanup error:", cleanupError);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const safeErrorMessage = redactRcloneCredentials(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
console.error("Backup error:", redactRcloneCredentials(String(error)));
|
||||
console.error("Backup error:", error);
|
||||
writeStream.write("Backup error❌\n");
|
||||
writeStream.write(`${safeErrorMessage}\n`);
|
||||
writeStream.write(
|
||||
error instanceof Error ? error.message : "Unknown error\n",
|
||||
);
|
||||
writeStream.end();
|
||||
await sendDokployBackupNotifications({
|
||||
type: "error",
|
||||
errorMessage: safeErrorMessage || "Error message not provided",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
backupSize: formatBytes(computedBackupSize),
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
|
||||
Reference in New Issue
Block a user