Merge pull request #2498 from Dokploy/2456-cannot-back-up-mariadb-database-access-denied-error

feat(database): enhance password validation for database schemas and …
This commit is contained in:
Mauricio Siu
2025-09-01 16:21:22 -06:00
committed by GitHub
8 changed files with 101 additions and 39 deletions

View File

@@ -6,16 +6,13 @@ Please describe in a short paragraph what this PR is about.
Before submitting this PR, please make sure that: Before submitting this PR, please make sure that:
- [ ] You created a dedicated branch based on the `canary` branch. - [] You created a dedicated branch based on the `canary` branch.
- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request - [] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
- [ ] You have tested this PR in your local instance. - [] You have tested this PR in your local instance.
## Issues related (if applicable) ## Issues related (if applicable)
Close automatically the related issues using the keywords: `closes #ISSUE_NUMBER`, `fixes #ISSUE_NUMBER`, `resolves #ISSUE_NUMBER` closes #123
Example: `closes #123`
## Screenshots (if applicable) ## Screenshots (if applicable)
If you include a video or screenshot, would be awesome so we can see the changes in action.

View File

@@ -83,7 +83,13 @@ const baseDatabaseSchema = z.object({
message: message:
"App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'", "App name supports lowercase letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
}), }),
databasePassword: z.string(), databasePassword: z
.string()
.min(1, "Password is required")
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
dockerImage: z.string(), dockerImage: z.string(),
description: z.string().nullable(), description: z.string().nullable(),
serverId: z.string().nullable(), serverId: z.string().nullable(),
@@ -112,7 +118,12 @@ const mySchema = z.discriminatedUnion("type", [
z z
.object({ .object({
type: z.literal("mysql"), type: z.literal("mysql"),
databaseRootPassword: z.string().default(""), databaseRootPassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databaseUser: z.string().default("mysql"), databaseUser: z.string().default("mysql"),
databaseName: z.string().default("mysql"), databaseName: z.string().default("mysql"),
}) })
@@ -121,7 +132,12 @@ const mySchema = z.discriminatedUnion("type", [
.object({ .object({
type: z.literal("mariadb"), type: z.literal("mariadb"),
dockerImage: z.string().default("mariadb:4"), dockerImage: z.string().default("mariadb:4"),
databaseRootPassword: z.string().default(""), databaseRootPassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databaseUser: z.string().default("mariadb"), databaseUser: z.string().default("mariadb"),
databaseName: z.string().default("mariadb"), databaseName: z.string().default("mariadb"),
}) })

View File

@@ -1,25 +1,16 @@
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import { Clipboard, EyeIcon, EyeOffIcon } from "lucide-react"; import { Clipboard } from "lucide-react";
import { useRef, useState } from "react"; import { useRef } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { Input, type InputProps } from "../ui/input"; import { Input, type InputProps } from "../ui/input";
export const ToggleVisibilityInput = ({ ...props }: InputProps) => { export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const togglePasswordVisibility = () => {
setIsPasswordVisible((prevVisibility) => !prevVisibility);
};
return ( return (
<div className="flex w-full items-center space-x-2"> <div className="flex w-full items-center space-x-2">
<Input <Input ref={inputRef} type={"password"} {...props} />
ref={inputRef}
type={isPasswordVisible ? "text" : "password"}
{...props}
/>
<Button <Button
variant={"secondary"} variant={"secondary"}
onClick={() => { onClick={() => {
@@ -29,13 +20,13 @@ export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
> >
<Clipboard className="size-4 text-muted-foreground" /> <Clipboard className="size-4 text-muted-foreground" />
</Button> </Button>
<Button onClick={togglePasswordVisibility} variant={"secondary"}> {/* <Button onClick={togglePasswordVisibility} variant={"secondary"}>
{isPasswordVisible ? ( {isPasswordVisible ? (
<EyeOffIcon className="size-4 text-muted-foreground" /> <EyeOffIcon className="size-4 text-muted-foreground" />
) : ( ) : (
<EyeIcon className="size-4 text-muted-foreground" /> <EyeIcon className="size-4 text-muted-foreground" />
)} )}
</Button> </Button> */}
</div> </div>
); );
}; };

View File

@@ -1,3 +1,4 @@
import { EyeIcon, EyeOffIcon } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -8,18 +9,39 @@ export interface InputProps
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, errorMessage, type, ...props }, ref) => { ({ className, errorMessage, type, ...props }, ref) => {
const [showPassword, setShowPassword] = React.useState(false);
const isPassword = type === "password";
const inputType = isPassword ? (showPassword ? "text" : "password") : type;
return ( return (
<> <>
<input <div className="relative w-full">
type={type} <input
className={cn( type={inputType}
// bg-gray className={cn(
"flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-50", // bg-gray
className, "flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-50",
isPassword && "pr-10", // Add padding for the eye icon
className,
)}
ref={ref}
{...props}
/>
{isPassword && (
<button
type="button"
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground 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>
)} )}
ref={ref} </div>
{...props}
/>
{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

@@ -94,8 +94,20 @@ const createSchema = createInsertSchema(mariadb, {
createdAt: z.string(), createdAt: z.string(),
databaseName: z.string().min(1), databaseName: z.string().min(1),
databaseUser: z.string().min(1), databaseUser: z.string().min(1),
databasePassword: z.string(), databasePassword: z
databaseRootPassword: z.string().optional(), .string()
.min(1, "Password is required")
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databaseRootPassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
})
.optional(),
dockerImage: z.string().default("mariadb:6"), dockerImage: z.string().default("mariadb:6"),
command: z.string().optional(), command: z.string().optional(),
env: z.string().optional(), env: z.string().optional(),

View File

@@ -89,7 +89,13 @@ const createSchema = createInsertSchema(mongo, {
createdAt: z.string(), createdAt: z.string(),
mongoId: z.string(), mongoId: z.string(),
name: z.string().min(1), name: z.string().min(1),
databasePassword: z.string(), databasePassword: z
.string()
.min(1, "Password is required")
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databaseUser: z.string().min(1), databaseUser: z.string().min(1),
dockerImage: z.string().default("mongo:15"), dockerImage: z.string().default("mongo:15"),
command: z.string().optional(), command: z.string().optional(),

View File

@@ -92,8 +92,20 @@ const createSchema = createInsertSchema(mysql, {
name: z.string().min(1), name: z.string().min(1),
databaseName: z.string().min(1), databaseName: z.string().min(1),
databaseUser: z.string().min(1), databaseUser: z.string().min(1),
databasePassword: z.string(), databasePassword: z
databaseRootPassword: z.string().optional(), .string()
.min(1, "Password is required")
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databaseRootPassword: z
.string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
})
.optional(),
dockerImage: z.string().default("mysql:8"), dockerImage: z.string().default("mysql:8"),
command: z.string().optional(), command: z.string().optional(),
env: z.string().optional(), env: z.string().optional(),

View File

@@ -88,7 +88,13 @@ export const postgresRelations = relations(postgres, ({ one, many }) => ({
const createSchema = createInsertSchema(postgres, { const createSchema = createInsertSchema(postgres, {
postgresId: z.string(), postgresId: z.string(),
name: z.string().min(1), name: z.string().min(1),
databasePassword: z.string(), databasePassword: z
.string()
.min(1, "Password is required")
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}),
databaseName: z.string().min(1), databaseName: z.string().min(1),
databaseUser: z.string().min(1), databaseUser: z.string().min(1),
dockerImage: z.string().default("postgres:15"), dockerImage: z.string().default("postgres:15"),