Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import { Loader2, Pen } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
Expand Down Expand Up @@ -28,11 +41,12 @@ 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-4 items-center">
<ToggleVisibilityInput
value={data?.databasePassword}
disabled
/>
<UpdatePassword postgresId={postgresId} />
</div>
</div>
<div className="flex flex-col gap-2">
Expand All @@ -59,4 +73,68 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
</>
);
};
// ReplyError: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-w

const UpdatePassword = ({ postgresId }: { postgresId: string }) => {
const [open, setOpen] = useState(false);
const [password, setPassword] = useState("");
const utils = api.useUtils();

const { mutateAsync, isLoading } = api.postgres.changePassword.useMutation();

const onSave = async () => {
if (!password) {
toast.error("Password is required");
return;
}
try {
await mutateAsync({
postgresId,
databasePassword: password,
});
await utils.postgres.one.invalidate({ postgresId });
toast.success("Password updated successfully");
setOpen(false);
setPassword("");
} catch (error) {
toast.error("Error updating password");
}
};

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="icon">
<Pen className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Update Password</DialogTitle>
<DialogDescription>
This will update the password in the database and in the dependent
applications.
</DialogDescription>
</DialogHeader>

<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label>New Password</Label>
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter new password"
type="password"
/>
</div>
</div>

<DialogFooter>
<Button onClick={onSave} disabled={isLoading}>
{isLoading && <Loader2 className="animate-spin size-4 mr-2" />}
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
20 changes: 20 additions & 0 deletions apps/dokploy/server/api/routers/postgres.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
addNewService,
changePostgresPassword,
checkServiceAccess,
createMount,
createPostgres,
Expand All @@ -26,6 +27,7 @@ import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiChangePostgresPassword,
apiChangePostgresStatus,
apiCreatePostgres,
apiDeployPostgres,
Expand Down Expand Up @@ -454,4 +456,22 @@ export const postgresRouter = createTRPCRouter({

return true;
}),
changePassword: protectedProcedure
.input(apiChangePostgresPassword)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this Postgres password",
});
}
return await changePostgresPassword(
input.postgresId,
input.databasePassword,
);
}),
});
7 changes: 7 additions & 0 deletions packages/server/src/db/schema/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,10 @@ export const apiRebuildPostgres = createSchema
postgresId: true,
})
.required();

export const apiChangePostgresPassword = createSchema
.pick({
postgresId: true,
databasePassword: true,
})
.required();
88 changes: 87 additions & 1 deletion packages/server/src/services/postgres.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { db } from "@dokploy/server/db";
import {
type apiCreatePostgres,
applications,
backups,
buildAppName,
environments,
postgres,
projects,
} from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates";
import { buildPostgres } from "@dokploy/server/utils/databases/postgres";
import { pullImage } from "@dokploy/server/utils/docker/utils";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { updateApplication } from "./application";
import { validUniqueServerAppName } from "./project";

export function getMountPath(dockerImage: string): string {
Expand Down Expand Up @@ -173,3 +180,82 @@ export const deployPostgres = async (
}
return postgres;
};

export const changePostgresPassword = async (
postgresId: string,
newPassword: string,
) => {
const postgres = await findPostgresById(postgresId);

// 1. Update in Docker Container
let command: string;
if (postgres.serverId) {
const { stdout: containerId } = await execAsyncRemote(
postgres.serverId,
`docker ps -q -f name=${postgres.appName}`,
);
if (!containerId) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Container not found for service: ${postgres.appName}`,
});
}
command = `docker exec ${containerId.trim()} psql -U ${postgres.databaseUser} -c "ALTER USER ${postgres.databaseUser} WITH PASSWORD '${newPassword}';"`;
await execAsyncRemote(postgres.serverId, command);
} else {
const { stdout: containerId } = await execAsync(
`docker ps -q -f name=${postgres.appName}`,
);
if (!containerId) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Container not found for service: ${postgres.appName}`,
});
}
command = `docker exec ${containerId.trim()} psql -U ${postgres.databaseUser} -c "ALTER USER ${postgres.databaseUser} WITH PASSWORD '${newPassword}';"`;
await execAsync(command);
}

// 2. Update in Dokploy Database
await updatePostgresById(postgresId, {
databasePassword: newPassword,
});

// 3. Update Dependent Applications
const project = postgres.environment.project;

// Find all applications in the same organization
const allApplications = await db
.select({
applicationId: applications.applicationId,
env: applications.env,
name: applications.name,
})
.from(applications)
.innerJoin(
environments,
eq(applications.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(eq(projects.organizationId, project.organizationId));

// Update applications that have the old connection string
const oldPassword = postgres.databasePassword;
let updatedCount = 0;

for (const app of allApplications) {
if (app.env?.includes(oldPassword)) {
// Replace all occurrences of the old password in the env string if it's part of a connection string context ideally,
// but for now simple replacement as per requirement.
// To be safer we could look for the specific connection string format, but 'includes' check + replace is standard for this scope.
const newEnv = app.env.replace(new RegExp(oldPassword, "g"), newPassword);

await updateApplication(app.applicationId, {
env: newEnv,
});
updatedCount++;
}
}

return { success: true, updatedApplications: updatedCount };
};