feat(password-validation): enhance password validation across database routers

- Updated password validation in MariaDB, MongoDB, MySQL, Postgres, and Redis routers to enforce a regex pattern that restricts invalid characters.
- Introduced a consistent error message for invalid passwords to improve user guidance and ensure database compatibility.
- Refactored password validation logic in the schema files to utilize shared constants for regex and messages, promoting code reuse and maintainability.
This commit is contained in:
Mauricio Siu
2026-04-04 09:27:06 -06:00
parent 3d838aa074
commit 0cb5ee49e0
12 changed files with 106 additions and 38 deletions
@@ -24,9 +24,17 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
const DATABASE_PASSWORD_REGEX = /^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/;
const updatePasswordSchema = z const updatePasswordSchema = z
.object({ .object({
password: z.string().min(1, "Password is required"), password: z
.string()
.min(1, "Password is required")
.regex(DATABASE_PASSWORD_REGEX, {
message:
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters",
}),
confirmPassword: z.string().min(1, "Please confirm the password"), confirmPassword: z.string().min(1, "Please confirm the password"),
}) })
.refine((data) => data.password === data.confirmPassword, { .refine((data) => data.password === data.confirmPassword, {
@@ -63,15 +71,12 @@ export const UpdateDatabasePassword = ({
setIsOpen(false); setIsOpen(false);
} catch (e) { } catch (e) {
const raw = e instanceof Error ? e.message : "Error updating password"; const raw = e instanceof Error ? e.message : "Error updating password";
const noContainer = raw.match(/No running container found for \S+/); if (/No running container found/i.test(raw)) {
if (noContainer) {
setError( setError(
"The database container is not running. Please start the service before changing the password.", "The database container is not running. Please start the service before changing the password.",
); );
} else { } else {
setError( setError(raw);
"Error updating password. Please check that the container is running and try again.",
);
} }
} finally { } finally {
setIsPending(false); setIsPending(false);
@@ -101,7 +106,7 @@ export const UpdateDatabasePassword = ({
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{error && <AlertBlock type="error">{error}</AlertBlock>} {error && <AlertBlock type="error">{error}</AlertBlock>}
<AlertBlock type="warning"> <AlertBlock type="warning" className="my-4">
This will change the {label.toLowerCase()} both in the running This will change the {label.toLowerCase()} both in the running
database container and in Dokploy. The container must be running for database container and in Dokploy. The container must be running for
this operation to succeed. this operation to succeed.
+8 -1
View File
@@ -43,6 +43,8 @@ import {
apiSaveEnvironmentVariablesMariaDB, apiSaveEnvironmentVariablesMariaDB,
apiSaveExternalPortMariaDB, apiSaveExternalPortMariaDB,
apiUpdateMariaDB, apiUpdateMariaDB,
DATABASE_PASSWORD_MESSAGE,
DATABASE_PASSWORD_REGEX,
environments, environments,
mariadb as mariadbTable, mariadb as mariadbTable,
projects, projects,
@@ -375,7 +377,12 @@ export const mariadbRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
mariadbId: z.string().min(1), mariadbId: z.string().min(1),
password: z.string().min(1), password: z
.string()
.min(1)
.regex(DATABASE_PASSWORD_REGEX, {
message: DATABASE_PASSWORD_MESSAGE,
}),
type: z.enum(["user", "root"]).default("user"), type: z.enum(["user", "root"]).default("user"),
}), }),
) )
+8 -1
View File
@@ -42,6 +42,8 @@ import {
apiSaveEnvironmentVariablesMongo, apiSaveEnvironmentVariablesMongo,
apiSaveExternalPortMongo, apiSaveExternalPortMongo,
apiUpdateMongo, apiUpdateMongo,
DATABASE_PASSWORD_MESSAGE,
DATABASE_PASSWORD_REGEX,
environments, environments,
mongo as mongoTable, mongo as mongoTable,
projects, projects,
@@ -397,7 +399,12 @@ export const mongoRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
mongoId: z.string().min(1), mongoId: z.string().min(1),
password: z.string().min(1), password: z
.string()
.min(1)
.regex(DATABASE_PASSWORD_REGEX, {
message: DATABASE_PASSWORD_MESSAGE,
}),
}), }),
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
+8 -1
View File
@@ -42,6 +42,8 @@ import {
apiSaveEnvironmentVariablesMySql, apiSaveEnvironmentVariablesMySql,
apiSaveExternalPortMySql, apiSaveExternalPortMySql,
apiUpdateMySql, apiUpdateMySql,
DATABASE_PASSWORD_MESSAGE,
DATABASE_PASSWORD_REGEX,
environments, environments,
mysql as mysqlTable, mysql as mysqlTable,
projects, projects,
@@ -394,7 +396,12 @@ export const mysqlRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
mysqlId: z.string().min(1), mysqlId: z.string().min(1),
password: z.string().min(1), password: z
.string()
.min(1)
.regex(DATABASE_PASSWORD_REGEX, {
message: DATABASE_PASSWORD_MESSAGE,
}),
type: z.enum(["user", "root"]).default("user"), type: z.enum(["user", "root"]).default("user"),
}), }),
) )
+8 -1
View File
@@ -43,6 +43,8 @@ import {
apiSaveEnvironmentVariablesPostgres, apiSaveEnvironmentVariablesPostgres,
apiSaveExternalPortPostgres, apiSaveExternalPortPostgres,
apiUpdatePostgres, apiUpdatePostgres,
DATABASE_PASSWORD_MESSAGE,
DATABASE_PASSWORD_REGEX,
environments, environments,
postgres as postgresTable, postgres as postgresTable,
projects, projects,
@@ -403,7 +405,12 @@ export const postgresRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
postgresId: z.string().min(1), postgresId: z.string().min(1),
password: z.string().min(1), password: z
.string()
.min(1)
.regex(DATABASE_PASSWORD_REGEX, {
message: DATABASE_PASSWORD_MESSAGE,
}),
}), }),
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
+8 -1
View File
@@ -41,6 +41,8 @@ import {
apiSaveEnvironmentVariablesRedis, apiSaveEnvironmentVariablesRedis,
apiSaveExternalPortRedis, apiSaveExternalPortRedis,
apiUpdateRedis, apiUpdateRedis,
DATABASE_PASSWORD_MESSAGE,
DATABASE_PASSWORD_REGEX,
environments, environments,
projects, projects,
redis as redisTable, redis as redisTable,
@@ -384,7 +386,12 @@ export const redisRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
redisId: z.string().min(1), redisId: z.string().min(1),
password: z.string().min(1), password: z
.string()
.min(1)
.regex(DATABASE_PASSWORD_REGEX, {
message: DATABASE_PASSWORD_MESSAGE,
}),
}), }),
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
+7 -4
View File
@@ -34,7 +34,11 @@ import {
type UpdateConfigSwarm, type UpdateConfigSwarm,
UpdateConfigSwarmSchema, UpdateConfigSwarmSchema,
} from "./shared"; } from "./shared";
import { generateAppName } from "./utils"; import {
DATABASE_PASSWORD_MESSAGE,
DATABASE_PASSWORD_REGEX,
generateAppName,
} from "./utils";
export const libsql = pgTable("libsql", { export const libsql = pgTable("libsql", {
libsqlId: text("libsqlId") libsqlId: text("libsqlId")
@@ -111,9 +115,8 @@ const createSchema = createInsertSchema(libsql, {
databaseUser: z.string().min(1), databaseUser: z.string().min(1),
databasePassword: z databasePassword: z
.string() .string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { .regex(DATABASE_PASSWORD_REGEX, {
message: message: DATABASE_PASSWORD_MESSAGE,
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}), }),
sqldNode: z.enum(sqldNode.enumValues), sqldNode: z.enum(sqldNode.enumValues),
sqldPrimaryUrl: z.string().nullable(), sqldPrimaryUrl: z.string().nullable(),
+11 -7
View File
@@ -28,7 +28,13 @@ import {
type UpdateConfigSwarm, type UpdateConfigSwarm,
UpdateConfigSwarmSchema, UpdateConfigSwarmSchema,
} from "./shared"; } from "./shared";
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils"; import {
APP_NAME_MESSAGE,
APP_NAME_REGEX,
DATABASE_PASSWORD_MESSAGE,
DATABASE_PASSWORD_REGEX,
generateAppName,
} from "./utils";
export const mariadb = pgTable("mariadb", { export const mariadb = pgTable("mariadb", {
mariadbId: text("mariadbId") mariadbId: text("mariadbId")
@@ -110,15 +116,13 @@ const createSchema = createInsertSchema(mariadb, {
databaseUser: z.string().min(1), databaseUser: z.string().min(1),
databasePassword: z databasePassword: z
.string() .string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { .regex(DATABASE_PASSWORD_REGEX, {
message: message: DATABASE_PASSWORD_MESSAGE,
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}), }),
databaseRootPassword: z databaseRootPassword: z
.string() .string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { .regex(DATABASE_PASSWORD_REGEX, {
message: message: DATABASE_PASSWORD_MESSAGE,
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}) })
.optional(), .optional(),
dockerImage: z.string().default("mariadb:6"), dockerImage: z.string().default("mariadb:6"),
+9 -4
View File
@@ -35,7 +35,13 @@ import {
type UpdateConfigSwarm, type UpdateConfigSwarm,
UpdateConfigSwarmSchema, UpdateConfigSwarmSchema,
} from "./shared"; } from "./shared";
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils"; import {
APP_NAME_MESSAGE,
APP_NAME_REGEX,
DATABASE_PASSWORD_MESSAGE,
DATABASE_PASSWORD_REGEX,
generateAppName,
} from "./utils";
export const mongo = pgTable("mongo", { export const mongo = pgTable("mongo", {
mongoId: text("mongoId") mongoId: text("mongoId")
@@ -112,9 +118,8 @@ const createSchema = createInsertSchema(mongo, {
name: z.string().min(1), name: z.string().min(1),
databasePassword: z databasePassword: z
.string() .string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { .regex(DATABASE_PASSWORD_REGEX, {
message: message: DATABASE_PASSWORD_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"),
+11 -7
View File
@@ -28,7 +28,13 @@ import {
type UpdateConfigSwarm, type UpdateConfigSwarm,
UpdateConfigSwarmSchema, UpdateConfigSwarmSchema,
} from "./shared"; } from "./shared";
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils"; import {
APP_NAME_MESSAGE,
APP_NAME_REGEX,
DATABASE_PASSWORD_MESSAGE,
DATABASE_PASSWORD_REGEX,
generateAppName,
} from "./utils";
export const mysql = pgTable("mysql", { export const mysql = pgTable("mysql", {
mysqlId: text("mysqlId") mysqlId: text("mysqlId")
@@ -108,15 +114,13 @@ const createSchema = createInsertSchema(mysql, {
databaseUser: z.string().min(1), databaseUser: z.string().min(1),
databasePassword: z databasePassword: z
.string() .string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { .regex(DATABASE_PASSWORD_REGEX, {
message: message: DATABASE_PASSWORD_MESSAGE,
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}), }),
databaseRootPassword: z databaseRootPassword: z
.string() .string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { .regex(DATABASE_PASSWORD_REGEX, {
message: message: DATABASE_PASSWORD_MESSAGE,
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
}) })
.optional(), .optional(),
dockerImage: z.string().default("mysql:8"), dockerImage: z.string().default("mysql:8"),
+9 -4
View File
@@ -28,7 +28,13 @@ import {
type UpdateConfigSwarm, type UpdateConfigSwarm,
UpdateConfigSwarmSchema, UpdateConfigSwarmSchema,
} from "./shared"; } from "./shared";
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils"; import {
APP_NAME_MESSAGE,
APP_NAME_REGEX,
DATABASE_PASSWORD_MESSAGE,
DATABASE_PASSWORD_REGEX,
generateAppName,
} from "./utils";
export const postgres = pgTable("postgres", { export const postgres = pgTable("postgres", {
postgresId: text("postgresId") postgresId: text("postgresId")
@@ -105,9 +111,8 @@ const createSchema = createInsertSchema(postgres, {
.optional(), .optional(),
databasePassword: z databasePassword: z
.string() .string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { .regex(DATABASE_PASSWORD_REGEX, {
message: message: DATABASE_PASSWORD_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),
+7
View File
@@ -12,6 +12,13 @@ export const APP_NAME_REGEX = /^[a-zA-Z0-9._-]+$/;
export const APP_NAME_MESSAGE = export const APP_NAME_MESSAGE =
"App name can only contain letters, numbers, dots, underscores and hyphens"; "App name can only contain letters, numbers, dots, underscores and hyphens";
/** Database password: blocks shell-dangerous characters like $ ! ' " \ / and spaces. */
export const DATABASE_PASSWORD_REGEX =
/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/;
export const DATABASE_PASSWORD_MESSAGE =
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility";
export const generateAppName = (type: string) => { export const generateAppName = (type: string) => {
const verb = faker.hacker.verb().replace(/ /g, "-"); const verb = faker.hacker.verb().replace(/ /g, "-");
const adjective = faker.hacker.adjective().replace(/ /g, "-"); const adjective = faker.hacker.adjective().replace(/ /g, "-");