mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
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:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
163
apps/dokploy/components/shared/update-database-password.tsx
Normal file
163
apps/dokploy/components/shared/update-database-password.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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, "-");
|
||||
|
||||
Reference in New Issue
Block a user