Compare commits

..

1 Commits

Author SHA1 Message Date
Mauricio Siu
b354c3cb0f fix(projects): make project cards grid fill available width on wide screens 2026-07-05 15:06:18 -06:00
21 changed files with 77 additions and 208 deletions

View File

@@ -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]");
});
});

View File

@@ -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="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input enableCopyButton disabled value={data?.databaseUser} /> <Input disabled value={data?.databaseUser} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Sqld Node</Label> <Label>Sqld Node</Label>
@@ -71,7 +71,7 @@ export const ShowInternalLibsqlCredentials = ({ libsqlId }: Props) => {
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input enableCopyButton disabled value={data?.appName} /> <Input disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Enable Namespaces</Label> <Label>Enable Namespaces</Label>

View File

@@ -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="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input enableCopyButton disabled value={data?.databaseUser} /> <Input disabled value={data?.databaseUser} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Database Name</Label> <Label>Database Name</Label>
<Input enableCopyButton disabled value={data?.databaseName} /> <Input disabled value={data?.databaseName} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Password</Label> <Label>Password</Label>
@@ -79,7 +79,7 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input enableCopyButton disabled value={data?.appName} /> <Input disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2 md:col-span-2"> <div className="flex flex-col gap-2 md:col-span-2">

View File

@@ -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="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input enableCopyButton disabled value={data?.databaseUser} /> <Input disabled value={data?.databaseUser} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@@ -55,7 +55,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input enableCopyButton disabled value={data?.appName} /> <Input disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2 md:col-span-2"> <div className="flex flex-col gap-2 md:col-span-2">

View File

@@ -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="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input enableCopyButton disabled value={data?.databaseUser} /> <Input disabled value={data?.databaseUser} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Database Name</Label> <Label>Database Name</Label>
<Input enableCopyButton disabled value={data?.databaseName} /> <Input disabled value={data?.databaseName} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Password</Label> <Label>Password</Label>
@@ -79,7 +79,7 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input enableCopyButton disabled value={data?.appName} /> <Input disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2 md:col-span-2"> <div className="flex flex-col gap-2 md:col-span-2">

View File

@@ -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="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input enableCopyButton disabled value={data?.databaseUser} /> <Input disabled value={data?.databaseUser} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Database Name</Label> <Label>Database Name</Label>
<Input enableCopyButton disabled value={data?.databaseName} /> <Input disabled value={data?.databaseName} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Password</Label> <Label>Password</Label>
@@ -57,7 +57,7 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input enableCopyButton disabled value={data?.appName} /> <Input disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">

View File

@@ -62,7 +62,7 @@ const dockerImageDefaultPlaceholder: Record<DbType, string> = {
mariadb: "mariadb:11", mariadb: "mariadb:11",
mysql: "mysql:8", mysql: "mysql:8",
postgres: "postgres:18", postgres: "postgres:18",
redis: "redis:8", redis: "redis:7",
}; };
const databasesUserDefaultPlaceholder: Record< const databasesUserDefaultPlaceholder: Record<

View File

@@ -47,7 +47,7 @@ interface Details {
envVariables: EnvVariable[]; envVariables: EnvVariable[];
shortDescription: string; shortDescription: string;
domains: Domain[]; domains: Domain[];
configFiles?: Mount[] | null; configFiles?: Mount[];
} }
interface Mount { interface Mount {

View File

@@ -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="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>User</Label> <Label>User</Label>
<Input enableCopyButton disabled value="default" /> <Input disabled value="default" />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Password</Label> <Label>Password</Label>
@@ -53,7 +53,7 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Internal Host</Label> <Label>Internal Host</Label>
<Input enableCopyButton disabled value={data?.appName} /> <Input disabled value={data?.appName} />
</div> </div>
<div className="flex flex-col gap-2 md:col-span-2"> <div className="flex flex-col gap-2 md:col-span-2">

View File

@@ -131,10 +131,7 @@ export const HandleAi = ({ aiId }: Props) => {
const apiUrl = form.watch("apiUrl"); const apiUrl = form.watch("apiUrl");
const apiKey = form.watch("apiKey"); const apiKey = form.watch("apiKey");
// Any Ollama instance on the default port 11434 is treated as no-auth const isOllama = apiUrl.includes(":11434") || apiUrl.includes("ollama");
// (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 { const {
data: models, data: models,
isFetching: isLoadingServerModels, isFetching: isLoadingServerModels,
@@ -145,7 +142,7 @@ export const HandleAi = ({ aiId }: Props) => {
apiKey: apiKey ?? "", apiKey: apiKey ?? "",
}, },
{ {
enabled: !!apiUrl && (isLocalOllama || !!apiKey), enabled: !!apiUrl && (isOllama || !!apiKey),
}, },
); );
@@ -278,7 +275,7 @@ export const HandleAi = ({ aiId }: Props) => {
)} )}
/> />
{!isLocalOllama && ( {!isOllama && (
<FormField <FormField
control={form.control} control={form.control}
name="apiKey" name="apiKey"

View File

@@ -1,17 +1,13 @@
import copy from "copy-to-clipboard"; import { EyeIcon, EyeOffIcon, RefreshCcw } from "lucide-react";
import { Clipboard, EyeIcon, EyeOffIcon, RefreshCcw } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { toast } from "sonner";
import { generateRandomPassword } from "@/lib/password-utils"; import { generateRandomPassword } from "@/lib/password-utils";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "./button";
export interface InputProps extends React.ComponentProps<"input"> { export interface InputProps extends React.ComponentProps<"input"> {
errorMessage?: string; errorMessage?: string;
enablePasswordGenerator?: boolean; enablePasswordGenerator?: boolean;
passwordGeneratorLength?: number; passwordGeneratorLength?: number;
enableCopyButton?: boolean;
} }
function Input({ function Input({
@@ -20,7 +16,6 @@ function Input({
errorMessage, errorMessage,
enablePasswordGenerator = false, enablePasswordGenerator = false,
passwordGeneratorLength, passwordGeneratorLength,
enableCopyButton = false,
ref, ref,
...props ...props
}: InputProps) { }: InputProps) {
@@ -70,67 +65,49 @@ function Input({
input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("input", { bubbles: true }));
}; };
const handleCopy = () => { return (
copy(inputRef.current?.value || ""); <>
toast.success("Value is copied to clipboard"); <div className="relative w-full">
}; <input
type={inputType}
const inputElement = ( data-slot="input"
<div className="relative w-full"> className={cn(
<input "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",
type={inputType} isPassword && (shouldShowGenerator ? "pr-16" : "pr-10"),
data-slot="input" className,
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", ref={setRefs}
isPassword && (shouldShowGenerator ? "pr-16" : "pr-10"), {...props}
className, />
)} {isPassword && (
ref={setRefs} <div className="absolute inset-y-0 right-0 flex items-center gap-1 pr-3 text-muted-foreground">
{...props} {shouldShowGenerator && (
/> <button
{isPassword && ( type="button"
<div className="absolute inset-y-0 right-0 flex items-center gap-1 pr-3 text-muted-foreground"> className="hover:text-foreground focus:outline-none"
{shouldShowGenerator && ( onClick={handleGeneratePassword}
aria-label="Generate password"
title="Generate password"
tabIndex={-1}
>
<RefreshCcw className="h-4 w-4" />
</button>
)}
<button <button
type="button" type="button"
className="hover:text-foreground focus:outline-none" className="hover:text-foreground focus:outline-none"
onClick={handleGeneratePassword} onClick={() => setShowPassword(!showPassword)}
aria-label="Generate password"
title="Generate password"
tabIndex={-1} tabIndex={-1}
> >
<RefreshCcw className="h-4 w-4" /> {showPassword ? (
<EyeOffIcon className="h-4 w-4" />
) : (
<EyeIcon className="h-4 w-4" />
)}
</button> </button>
)} </div>
<button )}
type="button" </div>
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
)}
{errorMessage && ( {errorMessage && (
<span className="text-sm text-red-600 text-secondary-foreground"> <span className="text-sm text-red-600 text-secondary-foreground">
{errorMessage} {errorMessage}

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.29.9", "version": "v0.29.8",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",

View File

@@ -54,20 +54,6 @@ const Page = ({ isCloud }: Props) => {
</EnterpriseFeatureGate> </EnterpriseFeatureGate>
</div> </div>
</Card> </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 && ( {!isCloud && (
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full"> <Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
<div className="rounded-xl bg-background shadow-md"> <div className="rounded-xl bg-background shadow-md">

View File

@@ -6,7 +6,6 @@ import {
findAllDeploymentsByServerId, findAllDeploymentsByServerId,
findAllDeploymentsCentralized, findAllDeploymentsCentralized,
findDeploymentById, findDeploymentById,
findScheduleById,
IS_CLOUD, IS_CLOUD,
removeDeployment, removeDeployment,
resolveServicePath, resolveServicePath,
@@ -127,29 +126,9 @@ export const deploymentRouter = createTRPCRouter({
allByType: protectedProcedure allByType: protectedProcedure
.input(apiFindAllByType) .input(apiFindAllByType)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
if (input.type === "schedule") { await checkServicePermissionAndAccess(ctx, input.id, {
const schedule = await findScheduleById(input.id); deployment: ["read"],
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"],
});
}
const deploymentsList = await db.query.deployments.findMany({ const deploymentsList = await db.query.deployments.findMany({
where: eq(deployments[`${input.type}Id`], input.id), where: eq(deployments[`${input.type}Id`], input.id),
orderBy: desc(deployments.createdAt), orderBy: desc(deployments.createdAt),

View File

@@ -24,7 +24,7 @@ interface DockerOutput {
dockerCompose: string; dockerCompose: string;
envVariables: Array<{ name: string; value: string }>; envVariables: Array<{ name: string; value: string }>;
domains: Array<{ host: string; port: number; serviceName: 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) => { export const getAiSettingsByOrganizationId = async (organizationId: string) => {
@@ -136,7 +136,7 @@ export const suggestVariants = async ({
filePath: z.string(), filePath: z.string(),
}), }),
) )
.nullable(), .optional(),
}), }),
), ),
}); });
@@ -198,12 +198,12 @@ export const suggestVariants = async ({
1. ALWAYS use 'image:' field, NEVER use 'build:' field 1. ALWAYS use 'image:' field, NEVER use 'build:' field
2. NEVER use 'build: .' or any build directive - we don't have local Dockerfiles 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) 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 5. Always specify image tags - avoid using 'latest' tag, use specific versions when possible
6. Examples of correct image usage: 6. Examples of correct image usage:
- image: sendingtk/chatwoot:develop - image: sendingtk/chatwoot:develop
- image: postgres:16-alpine - image: postgres:16-alpine
- image: redis:8-alpine - image: redis:7-alpine
7. Examples of INCORRECT usage (DO NOT USE): 7. Examples of INCORRECT usage (DO NOT USE):
- build: . - build: .
- build: ./app - build: ./app

View File

@@ -3,7 +3,7 @@ import { docker } from "../constants";
import { pullImage } from "../utils/docker/utils"; import { pullImage } from "../utils/docker/utils";
export const initializeRedis = async () => { export const initializeRedis = async () => {
const imageName = "redis:8"; const imageName = "redis:7";
const containerName = "dokploy-redis"; const containerName = "dokploy-redis";
const settings: CreateServiceOptions = { const settings: CreateServiceOptions = {

View File

@@ -74,10 +74,8 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
}); });
case "ollama": case "ollama":
return createOllama({ return createOllama({
// optional settings, e.g.
baseURL: config.apiUrl, baseURL: config.apiUrl,
headers: config.apiKey
? { Authorization: `Bearer ${config.apiKey}` }
: undefined,
}); });
case "deepinfra": case "deepinfra":
return createDeepInfra({ return createDeepInfra({

View File

@@ -11,7 +11,6 @@ import { startLogCleanup } from "../access-log/handler";
import { cleanupAll } from "../docker/utils"; import { cleanupAll } from "../docker/utils";
import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"; import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { redactRcloneCredentials } from "./redact";
import { getS3Credentials, normalizeS3Path, scheduleBackup } from "./utils"; import { getS3Credentials, normalizeS3Path, scheduleBackup } from "./utils";
export const initCronJobs = async () => { export const initCronJobs = async () => {
@@ -154,6 +153,6 @@ export const keepLatestNBackups = async (
await execAsync(rcloneCommand); await execAsync(rcloneCommand);
} }
} catch (error) { } catch (error) {
console.error(redactRcloneCredentials(String(error))); console.error(error);
} }
}; };

View File

@@ -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]"');
};

View File

@@ -9,7 +9,6 @@ import { runMariadbBackup } from "./mariadb";
import { runMongoBackup } from "./mongo"; import { runMongoBackup } from "./mongo";
import { runMySqlBackup } from "./mysql"; import { runMySqlBackup } from "./mysql";
import { runPostgresBackup } from "./postgres"; import { runPostgresBackup } from "./postgres";
import { redactRcloneCredentials } from "./redact";
import { runWebServerBackup } from "./web-server"; import { runWebServerBackup } from "./web-server";
export const scheduleBackup = (backup: BackupSchedule) => { export const scheduleBackup = (backup: BackupSchedule) => {
@@ -263,7 +262,7 @@ export const getBackupCommand = (
{ {
containerSearch, containerSearch,
backupCommand, backupCommand,
rcloneCommand: redactRcloneCredentials(rcloneCommand), rcloneCommand,
logPath, logPath,
}, },
`Executing backup command: ${backup.databaseType} ${backup.backupType}`, `Executing backup command: ${backup.databaseType} ${backup.backupType}`,

View File

@@ -11,7 +11,6 @@ import {
import { findDestinationById } from "@dokploy/server/services/destination"; import { findDestinationById } from "@dokploy/server/services/destination";
import { sendDokployBackupNotifications } from "../notifications/dokploy-backup"; import { sendDokployBackupNotifications } from "../notifications/dokploy-backup";
import { execAsync } from "../process/execAsync"; import { execAsync } from "../process/execAsync";
import { redactRcloneCredentials } from "./redact";
import { getBackupTimestamp, getS3Credentials, normalizeS3Path } from "./utils"; import { getBackupTimestamp, getS3Credentials, normalizeS3Path } from "./utils";
function formatBytes(bytes?: number) { function formatBytes(bytes?: number) {
@@ -114,23 +113,20 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
try { try {
await rm(tempDir, { recursive: true, force: true }); await rm(tempDir, { recursive: true, force: true });
} catch (cleanupError) { } catch (cleanupError) {
console.error( console.error("Cleanup error:", cleanupError);
"Cleanup error:",
redactRcloneCredentials(String(cleanupError)),
);
} }
} }
} catch (error) { } catch (error) {
const safeErrorMessage = redactRcloneCredentials( console.error("Backup error:", error);
error instanceof Error ? error.message : String(error),
);
console.error("Backup error:", redactRcloneCredentials(String(error)));
writeStream.write("Backup error❌\n"); writeStream.write("Backup error❌\n");
writeStream.write(`${safeErrorMessage}\n`); writeStream.write(
error instanceof Error ? error.message : "Unknown error\n",
);
writeStream.end(); writeStream.end();
await sendDokployBackupNotifications({ await sendDokployBackupNotifications({
type: "error", type: "error",
errorMessage: safeErrorMessage || "Error message not provided", // @ts-ignore
errorMessage: error?.message || "Error message not provided",
backupSize: formatBytes(computedBackupSize), backupSize: formatBytes(computedBackupSize),
}); });
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");