diff --git a/apps/dokploy/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx b/apps/dokploy/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx index 1702692693..8a99300ad9 100644 --- a/apps/dokploy/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx +++ b/apps/dokploy/components/dashboard/mariadb/general/show-internal-mariadb-credentials.tsx @@ -1,14 +1,19 @@ import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; +import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; +import { toast } from "sonner"; interface Props { mariadbId: string; } export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => { const { data } = api.mariadb.one.useQuery({ mariadbId }); + const utils = api.useUtils(); + const { mutateAsync: changePassword } = + api.mariadb.changePassword.useMutation(); return ( <>
@@ -28,20 +33,43 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
-
+
+ { + await changePassword({ + mariadbId, + password: newPassword, + type: "user", + }); + toast.success("Password updated successfully"); + utils.mariadb.one.invalidate({ mariadbId }); + }} + />
-
+
+ { + await changePassword({ + mariadbId, + password: newPassword, + type: "root", + }); + toast.success("Root password updated successfully"); + utils.mariadb.one.invalidate({ mariadbId }); + }} + />
diff --git a/apps/dokploy/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx b/apps/dokploy/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx index e66ea8c36a..e8c73e3497 100644 --- a/apps/dokploy/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx +++ b/apps/dokploy/components/dashboard/mongo/general/show-internal-mongo-credentials.tsx @@ -1,14 +1,19 @@ import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; +import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; +import { toast } from "sonner"; interface Props { mongoId: string; } export const ShowInternalMongoCredentials = ({ mongoId }: Props) => { const { data } = api.mongo.one.useQuery({ mongoId }); + const utils = api.useUtils(); + const { mutateAsync: changePassword } = + api.mongo.changePassword.useMutation(); return ( <>
@@ -25,11 +30,21 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
-
+
+ { + await changePassword({ + mongoId, + password: newPassword, + }); + toast.success("Password updated successfully"); + utils.mongo.one.invalidate({ mongoId }); + }} + />
diff --git a/apps/dokploy/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx b/apps/dokploy/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx index 3f1872371f..4f91c7efce 100644 --- a/apps/dokploy/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx +++ b/apps/dokploy/components/dashboard/mysql/general/show-internal-mysql-credentials.tsx @@ -1,14 +1,19 @@ import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; +import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; +import { toast } from "sonner"; interface Props { mysqlId: string; } export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => { const { data } = api.mysql.one.useQuery({ mysqlId }); + const utils = api.useUtils(); + const { mutateAsync: changePassword } = + api.mysql.changePassword.useMutation(); return ( <>
@@ -28,20 +33,43 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
-
+
+ { + await changePassword({ + mysqlId, + password: newPassword, + type: "user", + }); + toast.success("Password updated successfully"); + utils.mysql.one.invalidate({ mysqlId }); + }} + />
-
+
+ { + await changePassword({ + mysqlId, + password: newPassword, + type: "root", + }); + toast.success("Root password updated successfully"); + utils.mysql.one.invalidate({ mysqlId }); + }} + />
diff --git a/apps/dokploy/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx b/apps/dokploy/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx index 545150f871..30e265577e 100644 --- a/apps/dokploy/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx +++ b/apps/dokploy/components/dashboard/postgres/general/show-internal-postgres-credentials.tsx @@ -1,14 +1,19 @@ import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; +import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; +import { toast } from "sonner"; interface Props { postgresId: string; } export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => { const { data } = api.postgres.one.useQuery({ postgresId }); + const utils = api.useUtils(); + const { mutateAsync: changePassword } = + api.postgres.changePassword.useMutation(); return ( <>
@@ -28,11 +33,21 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
-
+
+ { + await changePassword({ + postgresId, + password: newPassword, + }); + toast.success("Password updated successfully"); + utils.postgres.one.invalidate({ postgresId }); + }} + />
diff --git a/apps/dokploy/components/dashboard/redis/general/show-internal-redis-credentials.tsx b/apps/dokploy/components/dashboard/redis/general/show-internal-redis-credentials.tsx index 47ad0df0b7..2245e724e6 100644 --- a/apps/dokploy/components/dashboard/redis/general/show-internal-redis-credentials.tsx +++ b/apps/dokploy/components/dashboard/redis/general/show-internal-redis-credentials.tsx @@ -1,14 +1,19 @@ import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; +import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; +import { toast } from "sonner"; interface Props { redisId: string; } export const ShowInternalRedisCredentials = ({ redisId }: Props) => { const { data } = api.redis.one.useQuery({ redisId }); + const utils = api.useUtils(); + const { mutateAsync: changePassword } = + api.redis.changePassword.useMutation(); return ( <>
@@ -24,11 +29,21 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
-
+
+ { + 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 ( + { + setIsOpen(open); + if (!open) { + form.reset(); + setError(null); + } + }} + > + + + + + + Update {label} + + Enter the new {label.toLowerCase()} for the database + + + {error && {error}} + + This will change the {label.toLowerCase()} both in the running + database container and in Dokploy. The container must be running for + this operation to succeed. + +
+ + ( + + New {label} + + + + + + )} + /> + ( + + Confirm {label} + + + + + + )} + /> + + + + + +
+
+ ); +}; 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, "-");