Merge pull request #4144 from Dokploy/4041-add-ability-to-change-services-password-like-mysql-and-whatnot

feat(database-credentials): add password update functionality for Mar…
This commit is contained in:
Mauricio Siu
2026-04-04 09:28:07 -06:00
committed by GitHub
17 changed files with 619 additions and 48 deletions

View File

@@ -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 (
<>
<div className="flex w-full flex-col gap-5 ">
@@ -28,20 +33,43 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
</div>
<div className="flex flex-col gap-2">
<Label>Password</Label>
<div className="flex flex-row gap-4">
<div className="flex flex-row gap-2 items-center">
<ToggleVisibilityInput
disabled
value={data?.databasePassword}
/>
<UpdateDatabasePassword
onUpdatePassword={async (newPassword) => {
await changePassword({
mariadbId,
password: newPassword,
type: "user",
});
toast.success("Password updated successfully");
utils.mariadb.one.invalidate({ mariadbId });
}}
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Label>Root Password</Label>
<div className="flex flex-row gap-4">
<div className="flex flex-row gap-2 items-center">
<ToggleVisibilityInput
disabled
value={data?.databaseRootPassword}
/>
<UpdateDatabasePassword
label="Root Password"
onUpdatePassword={async (newPassword) => {
await changePassword({
mariadbId,
password: newPassword,
type: "root",
});
toast.success("Root password updated successfully");
utils.mariadb.one.invalidate({ mariadbId });
}}
/>
</div>
</div>
<div className="flex flex-col gap-2">

View File

@@ -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 (
<>
<div className="flex w-full flex-col gap-5 ">
@@ -25,11 +30,21 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
<div className="flex flex-col gap-2">
<Label>Password</Label>
<div className="flex flex-row gap-4">
<div className="flex flex-row gap-2 items-center">
<ToggleVisibilityInput
disabled
value={data?.databasePassword}
/>
<UpdateDatabasePassword
onUpdatePassword={async (newPassword) => {
await changePassword({
mongoId,
password: newPassword,
});
toast.success("Password updated successfully");
utils.mongo.one.invalidate({ mongoId });
}}
/>
</div>
</div>

View File

@@ -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 (
<>
<div className="flex w-full flex-col gap-5 ">
@@ -28,20 +33,43 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
</div>
<div className="flex flex-col gap-2">
<Label>Password</Label>
<div className="flex flex-row gap-4">
<div className="flex flex-row gap-2 items-center">
<ToggleVisibilityInput
disabled
value={data?.databasePassword}
/>
<UpdateDatabasePassword
onUpdatePassword={async (newPassword) => {
await changePassword({
mysqlId,
password: newPassword,
type: "user",
});
toast.success("Password updated successfully");
utils.mysql.one.invalidate({ mysqlId });
}}
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Label>Root Password</Label>
<div className="flex flex-row gap-4">
<div className="flex flex-row gap-2 items-center">
<ToggleVisibilityInput
disabled
value={data?.databaseRootPassword}
/>
<UpdateDatabasePassword
label="Root Password"
onUpdatePassword={async (newPassword) => {
await changePassword({
mysqlId,
password: newPassword,
type: "root",
});
toast.success("Root password updated successfully");
utils.mysql.one.invalidate({ mysqlId });
}}
/>
</div>
</div>
<div className="flex flex-col gap-2">

View File

@@ -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 (
<>
<div className="flex w-full flex-col gap-5 ">
@@ -28,11 +33,21 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
</div>
<div className="flex flex-col gap-2">
<Label>Password</Label>
<div className="flex flex-row gap-4">
<div className="flex flex-row gap-2 items-center">
<ToggleVisibilityInput
value={data?.databasePassword}
disabled
/>
<UpdateDatabasePassword
onUpdatePassword={async (newPassword) => {
await changePassword({
postgresId,
password: newPassword,
});
toast.success("Password updated successfully");
utils.postgres.one.invalidate({ postgresId });
}}
/>
</div>
</div>
<div className="flex flex-col gap-2">

View File

@@ -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 (
<>
<div className="flex w-full flex-col gap-5 ">
@@ -24,11 +29,21 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
</div>
<div className="flex flex-col gap-2">
<Label>Password</Label>
<div className="flex flex-row gap-4">
<div className="flex flex-row gap-2 items-center">
<ToggleVisibilityInput
value={data?.databasePassword}
disabled
/>
<UpdateDatabasePassword
onUpdatePassword={async (newPassword) => {
await changePassword({
redisId,
password: newPassword,
});
toast.success("Password updated successfully");
utils.redis.one.invalidate({ redisId });
}}
/>
</div>
</div>
<div className="flex flex-col gap-2">

View File

@@ -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<typeof updatePasswordSchema>;
interface Props {
label?: string;
onUpdatePassword: (newPassword: string) => Promise<void>;
}
export const UpdateDatabasePassword = ({
label = "Password",
onUpdatePassword,
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isPending, setIsPending] = useState(false);
const form = useForm<UpdatePassword>({
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 (
<Dialog
open={isOpen}
onOpenChange={(open) => {
setIsOpen(open);
if (!open) {
form.reset();
setError(null);
}
}}
>
<DialogTrigger asChild>
<Button variant="ghost" size="icon">
<PenBox className="size-3.5 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Update {label}</DialogTitle>
<DialogDescription>
Enter the new {label.toLowerCase()} for the database
</DialogDescription>
</DialogHeader>
{error && <AlertBlock type="error">{error}</AlertBlock>}
<AlertBlock type="warning" className="my-4">
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.
</AlertBlock>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>New {label}</FormLabel>
<FormControl>
<Input
type="password"
placeholder={`Enter new ${label.toLowerCase()}`}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm {label}</FormLabel>
<FormControl>
<Input
type="password"
placeholder={`Confirm new ${label.toLowerCase()}`}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button isLoading={isPending} type="submit">
Update
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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),

View File

@@ -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"),

View File

@@ -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(),

View File

@@ -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"),

View File

@@ -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"),

View File

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