+
+ {
+ await changePassword({
+ redisId,
+ password: newPassword,
+ });
+ toast.success("Password updated successfully");
+ utils.redis.one.invalidate({ redisId });
+ }}
+ />
diff --git a/apps/dokploy/components/shared/update-database-password.tsx b/apps/dokploy/components/shared/update-database-password.tsx
new file mode 100644
index 0000000000..b22dbae7c3
--- /dev/null
+++ b/apps/dokploy/components/shared/update-database-password.tsx
@@ -0,0 +1,163 @@
+import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
+import { PenBox } from "lucide-react";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+import { AlertBlock } from "@/components/shared/alert-block";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+
+const DATABASE_PASSWORD_REGEX = /^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/;
+
+const updatePasswordSchema = z
+ .object({
+ 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"),
+ })
+ .refine((data) => data.password === data.confirmPassword, {
+ message: "Passwords do not match",
+ path: ["confirmPassword"],
+ });
+
+type UpdatePassword = z.infer
;
+
+interface Props {
+ label?: string;
+ onUpdatePassword: (newPassword: string) => Promise;
+}
+
+export const UpdateDatabasePassword = ({
+ label = "Password",
+ onUpdatePassword,
+}: Props) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [error, setError] = useState(null);
+ const [isPending, setIsPending] = useState(false);
+
+ const form = useForm({
+ defaultValues: { password: "", confirmPassword: "" },
+ resolver: zodResolver(updatePasswordSchema),
+ });
+
+ const onSubmit = async (formData: UpdatePassword) => {
+ setIsPending(true);
+ setError(null);
+ try {
+ await onUpdatePassword(formData.password);
+ form.reset();
+ setIsOpen(false);
+ } catch (e) {
+ const raw = e instanceof Error ? e.message : "Error updating password";
+ if (/No running container found/i.test(raw)) {
+ setError(
+ "The database container is not running. Please start the service before changing the password.",
+ );
+ } else {
+ setError(raw);
+ }
+ } finally {
+ setIsPending(false);
+ }
+ };
+ return (
+
+ );
+};
diff --git a/apps/dokploy/server/api/routers/mariadb.ts b/apps/dokploy/server/api/routers/mariadb.ts
index bb739a43b8..d1b2a38965 100644
--- a/apps/dokploy/server/api/routers/mariadb.ts
+++ b/apps/dokploy/server/api/routers/mariadb.ts
@@ -3,10 +3,13 @@ import {
createMariadb,
createMount,
deployMariadb,
+ execAsync,
+ execAsyncRemote,
findBackupsByDbId,
findEnvironmentById,
findMariadbById,
findProjectById,
+ getServiceContainerCommand,
IS_CLOUD,
rebuildDatabase,
removeMariadbById,
@@ -40,6 +43,8 @@ import {
apiSaveEnvironmentVariablesMariaDB,
apiSaveExternalPortMariaDB,
apiUpdateMariaDB,
+ DATABASE_PASSWORD_MESSAGE,
+ DATABASE_PASSWORD_REGEX,
environments,
mariadb as mariadbTable,
projects,
@@ -366,6 +371,63 @@ export const mariadbRouter = createTRPCRouter({
resourceId: mariadbId,
resourceName: service.appName,
});
+ return true;
+ }),
+ changePassword: protectedProcedure
+ .input(
+ z.object({
+ mariadbId: z.string().min(1),
+ password: z.string().min(1).regex(DATABASE_PASSWORD_REGEX, {
+ message: DATABASE_PASSWORD_MESSAGE,
+ }),
+ type: z.enum(["user", "root"]).default("user"),
+ }),
+ )
+ .mutation(async ({ input, ctx }) => {
+ const { mariadbId, password, type } = input;
+ await checkServicePermissionAndAccess(ctx, mariadbId, {
+ service: ["create"],
+ });
+
+ const maria = await findMariadbById(mariadbId);
+ const { appName, serverId, databaseUser, databaseRootPassword } = maria;
+
+ const containerCmd = getServiceContainerCommand(appName);
+ const targetUser = type === "root" ? "root" : databaseUser;
+
+ const command = `
+ CONTAINER_ID=$(${containerCmd})
+ if [ -z "$CONTAINER_ID" ]; then
+ echo "No running container found for ${appName}" >&2
+ exit 1
+ fi
+ docker exec "$CONTAINER_ID" mariadb -u root -p'${databaseRootPassword}' -e "ALTER USER '${targetUser}'@'%' IDENTIFIED BY '${password}'; FLUSH PRIVILEGES;"
+ `;
+
+ await db.transaction(async (tx) => {
+ const setData =
+ type === "root"
+ ? { databaseRootPassword: password }
+ : { databasePassword: password };
+ await tx
+ .update(mariadbTable)
+ .set(setData)
+ .where(eq(mariadbTable.mariadbId, mariadbId));
+
+ if (serverId) {
+ await execAsyncRemote(serverId, command);
+ } else {
+ await execAsync(command, { shell: "/bin/bash" });
+ }
+ });
+
+ await audit(ctx, {
+ action: "update",
+ resourceType: "service",
+ resourceId: mariadbId,
+ resourceName: appName,
+ });
+
return true;
}),
move: protectedProcedure
diff --git a/apps/dokploy/server/api/routers/mongo.ts b/apps/dokploy/server/api/routers/mongo.ts
index de9e1f36fb..edd552fe81 100644
--- a/apps/dokploy/server/api/routers/mongo.ts
+++ b/apps/dokploy/server/api/routers/mongo.ts
@@ -3,10 +3,13 @@ import {
createMongo,
createMount,
deployMongo,
+ execAsync,
+ execAsyncRemote,
findBackupsByDbId,
findEnvironmentById,
findMongoById,
findProjectById,
+ getServiceContainerCommand,
IS_CLOUD,
rebuildDatabase,
removeMongoById,
@@ -39,6 +42,8 @@ import {
apiSaveEnvironmentVariablesMongo,
apiSaveExternalPortMongo,
apiUpdateMongo,
+ DATABASE_PASSWORD_MESSAGE,
+ DATABASE_PASSWORD_REGEX,
environments,
mongo as mongoTable,
projects,
@@ -388,6 +393,56 @@ export const mongoRouter = createTRPCRouter({
resourceId: mongoId,
resourceName: service.appName,
});
+ return true;
+ }),
+ changePassword: protectedProcedure
+ .input(
+ z.object({
+ mongoId: z.string().min(1),
+ password: z.string().min(1).regex(DATABASE_PASSWORD_REGEX, {
+ message: DATABASE_PASSWORD_MESSAGE,
+ }),
+ }),
+ )
+ .mutation(async ({ input, ctx }) => {
+ const { mongoId, password } = input;
+ await checkServicePermissionAndAccess(ctx, mongoId, {
+ service: ["create"],
+ });
+
+ const mongo = await findMongoById(mongoId);
+ const { appName, serverId, databaseUser, databasePassword } = mongo;
+
+ const containerCmd = getServiceContainerCommand(appName);
+ const command = `
+ CONTAINER_ID=$(${containerCmd})
+ if [ -z "$CONTAINER_ID" ]; then
+ echo "No running container found for ${appName}" >&2
+ exit 1
+ fi
+ docker exec "$CONTAINER_ID" mongosh -u '${databaseUser}' -p '${databasePassword}' --authenticationDatabase admin --eval "db.getSiblingDB('admin').changeUserPassword('${databaseUser}', '${password}')"
+ `;
+
+ await db.transaction(async (tx) => {
+ await tx
+ .update(mongoTable)
+ .set({ databasePassword: password })
+ .where(eq(mongoTable.mongoId, mongoId));
+
+ if (serverId) {
+ await execAsyncRemote(serverId, command);
+ } else {
+ await execAsync(command, { shell: "/bin/bash" });
+ }
+ });
+
+ await audit(ctx, {
+ action: "update",
+ resourceType: "service",
+ resourceId: mongoId,
+ resourceName: appName,
+ });
+
return true;
}),
move: protectedProcedure
diff --git a/apps/dokploy/server/api/routers/mysql.ts b/apps/dokploy/server/api/routers/mysql.ts
index b834f52c20..6635c8dfbe 100644
--- a/apps/dokploy/server/api/routers/mysql.ts
+++ b/apps/dokploy/server/api/routers/mysql.ts
@@ -3,10 +3,13 @@ import {
createMount,
createMysql,
deployMySql,
+ execAsync,
+ execAsyncRemote,
findBackupsByDbId,
findEnvironmentById,
findMySqlById,
findProjectById,
+ getServiceContainerCommand,
IS_CLOUD,
rebuildDatabase,
removeMySqlById,
@@ -39,6 +42,8 @@ import {
apiSaveEnvironmentVariablesMySql,
apiSaveExternalPortMySql,
apiUpdateMySql,
+ DATABASE_PASSWORD_MESSAGE,
+ DATABASE_PASSWORD_REGEX,
environments,
mysql as mysqlTable,
projects,
@@ -385,6 +390,63 @@ export const mysqlRouter = createTRPCRouter({
resourceId: mysqlId,
resourceName: service.appName,
});
+ return true;
+ }),
+ changePassword: protectedProcedure
+ .input(
+ z.object({
+ mysqlId: z.string().min(1),
+ password: z.string().min(1).regex(DATABASE_PASSWORD_REGEX, {
+ message: DATABASE_PASSWORD_MESSAGE,
+ }),
+ type: z.enum(["user", "root"]).default("user"),
+ }),
+ )
+ .mutation(async ({ input, ctx }) => {
+ const { mysqlId, password, type } = input;
+ await checkServicePermissionAndAccess(ctx, mysqlId, {
+ service: ["create"],
+ });
+
+ const my = await findMySqlById(mysqlId);
+ const { appName, serverId, databaseUser, databaseRootPassword } = my;
+
+ const containerCmd = getServiceContainerCommand(appName);
+ const targetUser = type === "root" ? "root" : databaseUser;
+
+ const command = `
+ CONTAINER_ID=$(${containerCmd})
+ if [ -z "$CONTAINER_ID" ]; then
+ echo "No running container found for ${appName}" >&2
+ exit 1
+ fi
+ docker exec "$CONTAINER_ID" mysql -u root -p'${databaseRootPassword}' -e "ALTER USER '${targetUser}'@'%' IDENTIFIED BY '${password}'; FLUSH PRIVILEGES;"
+ `;
+
+ await db.transaction(async (tx) => {
+ const setData =
+ type === "root"
+ ? { databaseRootPassword: password }
+ : { databasePassword: password };
+ await tx
+ .update(mysqlTable)
+ .set(setData)
+ .where(eq(mysqlTable.mysqlId, mysqlId));
+
+ if (serverId) {
+ await execAsyncRemote(serverId, command);
+ } else {
+ await execAsync(command, { shell: "/bin/bash" });
+ }
+ });
+
+ await audit(ctx, {
+ action: "update",
+ resourceType: "service",
+ resourceId: mysqlId,
+ resourceName: appName,
+ });
+
return true;
}),
move: protectedProcedure
diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts
index 78e0e1284a..5ebb9861f0 100644
--- a/apps/dokploy/server/api/routers/postgres.ts
+++ b/apps/dokploy/server/api/routers/postgres.ts
@@ -3,11 +3,14 @@ import {
createMount,
createPostgres,
deployPostgres,
+ execAsync,
+ execAsyncRemote,
findBackupsByDbId,
findEnvironmentById,
findPostgresById,
findProjectById,
getMountPath,
+ getServiceContainerCommand,
IS_CLOUD,
rebuildDatabase,
removePostgresById,
@@ -40,6 +43,8 @@ import {
apiSaveEnvironmentVariablesPostgres,
apiSaveExternalPortPostgres,
apiUpdatePostgres,
+ DATABASE_PASSWORD_MESSAGE,
+ DATABASE_PASSWORD_REGEX,
environments,
postgres as postgresTable,
projects,
@@ -394,6 +399,56 @@ export const postgresRouter = createTRPCRouter({
resourceId: postgresId,
resourceName: service.appName,
});
+ return true;
+ }),
+ changePassword: protectedProcedure
+ .input(
+ z.object({
+ postgresId: z.string().min(1),
+ password: z.string().min(1).regex(DATABASE_PASSWORD_REGEX, {
+ message: DATABASE_PASSWORD_MESSAGE,
+ }),
+ }),
+ )
+ .mutation(async ({ input, ctx }) => {
+ const { postgresId, password } = input;
+ await checkServicePermissionAndAccess(ctx, postgresId, {
+ service: ["create"],
+ });
+
+ const pg = await findPostgresById(postgresId);
+ const { appName, serverId, databaseUser } = pg;
+
+ const containerCmd = getServiceContainerCommand(appName);
+ const command = `
+ CONTAINER_ID=$(${containerCmd})
+ if [ -z "$CONTAINER_ID" ]; then
+ echo "No running container found for ${appName}" >&2
+ exit 1
+ fi
+ docker exec "$CONTAINER_ID" psql -U ${databaseUser} -c "ALTER USER \\"${databaseUser}\\" WITH PASSWORD '${password}';"
+ `;
+
+ await db.transaction(async (tx) => {
+ await tx
+ .update(postgresTable)
+ .set({ databasePassword: password })
+ .where(eq(postgresTable.postgresId, postgresId));
+
+ if (serverId) {
+ await execAsyncRemote(serverId, command);
+ } else {
+ await execAsync(command, { shell: "/bin/bash" });
+ }
+ });
+
+ await audit(ctx, {
+ action: "update",
+ resourceType: "service",
+ resourceId: postgresId,
+ resourceName: appName,
+ });
+
return true;
}),
move: protectedProcedure
diff --git a/apps/dokploy/server/api/routers/redis.ts b/apps/dokploy/server/api/routers/redis.ts
index efc98bf777..026ec8b2a0 100644
--- a/apps/dokploy/server/api/routers/redis.ts
+++ b/apps/dokploy/server/api/routers/redis.ts
@@ -3,9 +3,12 @@ import {
createMount,
createRedis,
deployRedis,
+ execAsync,
+ execAsyncRemote,
findEnvironmentById,
findProjectById,
findRedisById,
+ getServiceContainerCommand,
IS_CLOUD,
rebuildDatabase,
removeRedisById,
@@ -38,6 +41,8 @@ import {
apiSaveEnvironmentVariablesRedis,
apiSaveExternalPortRedis,
apiUpdateRedis,
+ DATABASE_PASSWORD_MESSAGE,
+ DATABASE_PASSWORD_REGEX,
environments,
projects,
redis as redisTable,
@@ -375,6 +380,56 @@ export const redisRouter = createTRPCRouter({
resourceId: redisId,
resourceName: redis.appName,
});
+ return true;
+ }),
+ changePassword: protectedProcedure
+ .input(
+ z.object({
+ redisId: z.string().min(1),
+ password: z.string().min(1).regex(DATABASE_PASSWORD_REGEX, {
+ message: DATABASE_PASSWORD_MESSAGE,
+ }),
+ }),
+ )
+ .mutation(async ({ input, ctx }) => {
+ const { redisId, password } = input;
+ await checkServicePermissionAndAccess(ctx, redisId, {
+ service: ["create"],
+ });
+
+ const rd = await findRedisById(redisId);
+ const { appName, serverId, databasePassword } = rd;
+
+ const containerCmd = getServiceContainerCommand(appName);
+ const command = `
+ CONTAINER_ID=$(${containerCmd})
+ if [ -z "$CONTAINER_ID" ]; then
+ echo "No running container found for ${appName}" >&2
+ exit 1
+ fi
+ docker exec "$CONTAINER_ID" redis-cli -a '${databasePassword}' CONFIG SET requirepass '${password}'
+ `;
+
+ await db.transaction(async (tx) => {
+ await tx
+ .update(redisTable)
+ .set({ databasePassword: password })
+ .where(eq(redisTable.redisId, redisId));
+
+ if (serverId) {
+ await execAsyncRemote(serverId, command);
+ } else {
+ await execAsync(command, { shell: "/bin/bash" });
+ }
+ });
+
+ await audit(ctx, {
+ action: "update",
+ resourceType: "service",
+ resourceId: redisId,
+ resourceName: appName,
+ });
+
return true;
}),
move: protectedProcedure
diff --git a/packages/server/src/db/schema/libsql.ts b/packages/server/src/db/schema/libsql.ts
index 770ed2355e..0d5e98b787 100644
--- a/packages/server/src/db/schema/libsql.ts
+++ b/packages/server/src/db/schema/libsql.ts
@@ -34,7 +34,11 @@ import {
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
-import { generateAppName } from "./utils";
+import {
+ DATABASE_PASSWORD_MESSAGE,
+ DATABASE_PASSWORD_REGEX,
+ generateAppName,
+} from "./utils";
export const libsql = pgTable("libsql", {
libsqlId: text("libsqlId")
@@ -109,12 +113,9 @@ const createSchema = createInsertSchema(libsql, {
appName: z.string().min(1),
createdAt: z.string(),
databaseUser: z.string().min(1),
- databasePassword: z
- .string()
- .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
- message:
- "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
- }),
+ databasePassword: z.string().regex(DATABASE_PASSWORD_REGEX, {
+ message: DATABASE_PASSWORD_MESSAGE,
+ }),
sqldNode: z.enum(sqldNode.enumValues),
sqldPrimaryUrl: z.string().nullable(),
enableNamespaces: z.boolean().default(false),
diff --git a/packages/server/src/db/schema/mariadb.ts b/packages/server/src/db/schema/mariadb.ts
index 2659c29786..0b58fcf547 100644
--- a/packages/server/src/db/schema/mariadb.ts
+++ b/packages/server/src/db/schema/mariadb.ts
@@ -28,7 +28,13 @@ import {
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} 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", {
mariadbId: text("mariadbId")
@@ -108,17 +114,13 @@ const createSchema = createInsertSchema(mariadb, {
createdAt: z.string(),
databaseName: z.string().min(1),
databaseUser: z.string().min(1),
- databasePassword: z
- .string()
- .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
- message:
- "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
- }),
+ databasePassword: z.string().regex(DATABASE_PASSWORD_REGEX, {
+ message: DATABASE_PASSWORD_MESSAGE,
+ }),
databaseRootPassword: z
.string()
- .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
- message:
- "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
+ .regex(DATABASE_PASSWORD_REGEX, {
+ message: DATABASE_PASSWORD_MESSAGE,
})
.optional(),
dockerImage: z.string().default("mariadb:6"),
diff --git a/packages/server/src/db/schema/mongo.ts b/packages/server/src/db/schema/mongo.ts
index 4599cedb2c..3546c9aacf 100644
--- a/packages/server/src/db/schema/mongo.ts
+++ b/packages/server/src/db/schema/mongo.ts
@@ -35,7 +35,13 @@ import {
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} 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", {
mongoId: text("mongoId")
@@ -110,12 +116,9 @@ const createSchema = createInsertSchema(mongo, {
createdAt: z.string(),
mongoId: z.string(),
name: z.string().min(1),
- databasePassword: z
- .string()
- .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
- message:
- "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
- }),
+ databasePassword: z.string().regex(DATABASE_PASSWORD_REGEX, {
+ message: DATABASE_PASSWORD_MESSAGE,
+ }),
databaseUser: z.string().min(1),
dockerImage: z.string().default("mongo:15"),
command: z.string().optional(),
diff --git a/packages/server/src/db/schema/mysql.ts b/packages/server/src/db/schema/mysql.ts
index 90b38e6fa2..b555214f31 100644
--- a/packages/server/src/db/schema/mysql.ts
+++ b/packages/server/src/db/schema/mysql.ts
@@ -28,7 +28,13 @@ import {
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} 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", {
mysqlId: text("mysqlId")
@@ -106,17 +112,13 @@ const createSchema = createInsertSchema(mysql, {
name: z.string().min(1),
databaseName: z.string().min(1),
databaseUser: z.string().min(1),
- databasePassword: z
- .string()
- .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
- message:
- "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
- }),
+ databasePassword: z.string().regex(DATABASE_PASSWORD_REGEX, {
+ message: DATABASE_PASSWORD_MESSAGE,
+ }),
databaseRootPassword: z
.string()
- .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
- message:
- "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
+ .regex(DATABASE_PASSWORD_REGEX, {
+ message: DATABASE_PASSWORD_MESSAGE,
})
.optional(),
dockerImage: z.string().default("mysql:8"),
diff --git a/packages/server/src/db/schema/postgres.ts b/packages/server/src/db/schema/postgres.ts
index 5cb3015ce9..8f96caa925 100644
--- a/packages/server/src/db/schema/postgres.ts
+++ b/packages/server/src/db/schema/postgres.ts
@@ -28,7 +28,13 @@ import {
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} 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", {
postgresId: text("postgresId")
@@ -103,12 +109,9 @@ const createSchema = createInsertSchema(postgres, {
.max(63)
.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
.optional(),
- databasePassword: z
- .string()
- .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
- message:
- "Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
- }),
+ databasePassword: z.string().regex(DATABASE_PASSWORD_REGEX, {
+ message: DATABASE_PASSWORD_MESSAGE,
+ }),
databaseName: z.string().min(1),
databaseUser: z.string().min(1),
dockerImage: z.string().default("postgres:18"),
diff --git a/packages/server/src/db/schema/utils.ts b/packages/server/src/db/schema/utils.ts
index 811d3f767a..30babea6dd 100644
--- a/packages/server/src/db/schema/utils.ts
+++ b/packages/server/src/db/schema/utils.ts
@@ -12,6 +12,13 @@ export const APP_NAME_REGEX = /^[a-zA-Z0-9._-]+$/;
export const APP_NAME_MESSAGE =
"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) => {
const verb = faker.hacker.verb().replace(/ /g, "-");
const adjective = faker.hacker.adjective().replace(/ /g, "-");