feat: improve user profile update and password change functionality

This commit adds enhanced password change validation and handling:

- Add password change validation in user update route
- Implement password verification before allowing changes
- Update user schema to support optional password fields
- Fix token display in generate token component
- Disable migration script temporarily
This commit is contained in:
Mauricio Siu
2025-02-22 22:37:57 -06:00
parent 0478419f7c
commit 47f7648cb3
5 changed files with 176 additions and 143 deletions

View File

@@ -51,7 +51,7 @@ export const GenerateToken = () => {
<Label>Token</Label> <Label>Token</Label>
<ToggleVisibilityInput <ToggleVisibilityInput
placeholder="Token" placeholder="Token"
value={data || ""} value={data?.id || ""}
disabled disabled
/> />
</div> </div>

View File

@@ -103,9 +103,9 @@ export const ProfileForm = () => {
const onSubmit = async (values: Profile) => { const onSubmit = async (values: Profile) => {
await mutateAsync({ await mutateAsync({
email: values.email.toLowerCase(), email: values.email.toLowerCase(),
password: values.password, password: values.password || undefined,
image: values.image, image: values.image,
currentPassword: values.currentPassword, currentPassword: values.currentPassword || undefined,
}) })
.then(async () => { .then(async () => {
await refetch(); await refetch();

View File

@@ -1,149 +1,149 @@
import { drizzle } from "drizzle-orm/postgres-js"; // import { drizzle } from "drizzle-orm/postgres-js";
import { nanoid } from "nanoid"; // import { nanoid } from "nanoid";
import postgres from "postgres"; // import postgres from "postgres";
import * as schema from "./server/db/schema"; // import * as schema from "./server/db/schema";
const connectionString = process.env.DATABASE_URL!; // const connectionString = process.env.DATABASE_URL!;
const sql = postgres(connectionString, { max: 1 }); // const sql = postgres(connectionString, { max: 1 });
const db = drizzle(sql, { // const db = drizzle(sql, {
schema, // schema,
}); // });
await db // await db
.transaction(async (db) => { // .transaction(async (db) => {
const admins = await db.query.admins.findMany({ // const admins = await db.query.admins.findMany({
with: { // with: {
auth: true, // auth: true,
users: { // users: {
with: { // with: {
auth: true, // auth: true,
}, // },
}, // },
}, // },
}); // });
for (const admin of admins) { // for (const admin of admins) {
const user = await db // const user = await db
.insert(schema.users_temp) // .insert(schema.users_temp)
.values({ // .values({
id: admin.adminId, // id: admin.adminId,
email: admin.auth.email, // email: admin.auth.email,
token: admin.auth.token || "", // token: admin.auth.token || "",
emailVerified: true, // emailVerified: true,
updatedAt: new Date(), // updatedAt: new Date(),
role: "admin", // role: "admin",
serverIp: admin.serverIp, // serverIp: admin.serverIp,
image: admin.auth.image, // image: admin.auth.image,
certificateType: admin.certificateType, // certificateType: admin.certificateType,
host: admin.host, // host: admin.host,
letsEncryptEmail: admin.letsEncryptEmail, // letsEncryptEmail: admin.letsEncryptEmail,
sshPrivateKey: admin.sshPrivateKey, // sshPrivateKey: admin.sshPrivateKey,
enableDockerCleanup: admin.enableDockerCleanup, // enableDockerCleanup: admin.enableDockerCleanup,
enableLogRotation: admin.enableLogRotation, // enableLogRotation: admin.enableLogRotation,
enablePaidFeatures: admin.enablePaidFeatures, // enablePaidFeatures: admin.enablePaidFeatures,
metricsConfig: admin.metricsConfig, // metricsConfig: admin.metricsConfig,
cleanupCacheApplications: admin.cleanupCacheApplications, // cleanupCacheApplications: admin.cleanupCacheApplications,
cleanupCacheOnPreviews: admin.cleanupCacheOnPreviews, // cleanupCacheOnPreviews: admin.cleanupCacheOnPreviews,
cleanupCacheOnCompose: admin.cleanupCacheOnCompose, // cleanupCacheOnCompose: admin.cleanupCacheOnCompose,
stripeCustomerId: admin.stripeCustomerId, // stripeCustomerId: admin.stripeCustomerId,
stripeSubscriptionId: admin.stripeSubscriptionId, // stripeSubscriptionId: admin.stripeSubscriptionId,
serversQuantity: admin.serversQuantity, // serversQuantity: admin.serversQuantity,
}) // })
.returning() // .returning()
.then((user) => user[0]); // .then((user) => user[0]);
await db.insert(schema.account).values({ // await db.insert(schema.account).values({
providerId: "credential", // providerId: "credential",
userId: user?.id || "", // userId: user?.id || "",
password: admin.auth.password, // password: admin.auth.password,
is2FAEnabled: admin.auth.is2FAEnabled || false, // is2FAEnabled: admin.auth.is2FAEnabled || false,
createdAt: new Date(admin.auth.createdAt) || new Date(), // createdAt: new Date(admin.auth.createdAt) || new Date(),
updatedAt: new Date(admin.auth.createdAt) || new Date(), // updatedAt: new Date(admin.auth.createdAt) || new Date(),
}); // });
const organization = await db // const organization = await db
.insert(schema.organization) // .insert(schema.organization)
.values({ // .values({
name: "My Organization", // name: "My Organization",
slug: nanoid(), // slug: nanoid(),
ownerId: user?.id || "", // ownerId: user?.id || "",
createdAt: new Date(admin.createdAt) || new Date(), // createdAt: new Date(admin.createdAt) || new Date(),
}) // })
.returning() // .returning()
.then((organization) => organization[0]); // .then((organization) => organization[0]);
for (const member of admin.users) { // for (const member of admin.users) {
const userTemp = await db // const userTemp = await db
.insert(schema.users_temp) // .insert(schema.users_temp)
.values({ // .values({
id: member.userId, // id: member.userId,
email: member.auth.email, // email: member.auth.email,
token: member.token || "", // token: member.token || "",
emailVerified: true, // emailVerified: true,
updatedAt: new Date(admin.createdAt) || new Date(), // updatedAt: new Date(admin.createdAt) || new Date(),
role: "user", // role: "user",
image: member.auth.image, // image: member.auth.image,
createdAt: admin.createdAt, // createdAt: admin.createdAt,
canAccessToAPI: member.canAccessToAPI || false, // canAccessToAPI: member.canAccessToAPI || false,
canAccessToDocker: member.canAccessToDocker || false, // canAccessToDocker: member.canAccessToDocker || false,
canAccessToGitProviders: member.canAccessToGitProviders || false, // canAccessToGitProviders: member.canAccessToGitProviders || false,
canAccessToSSHKeys: member.canAccessToSSHKeys || false, // canAccessToSSHKeys: member.canAccessToSSHKeys || false,
canAccessToTraefikFiles: member.canAccessToTraefikFiles || false, // canAccessToTraefikFiles: member.canAccessToTraefikFiles || false,
canCreateProjects: member.canCreateProjects || false, // canCreateProjects: member.canCreateProjects || false,
canCreateServices: member.canCreateServices || false, // canCreateServices: member.canCreateServices || false,
canDeleteProjects: member.canDeleteProjects || false, // canDeleteProjects: member.canDeleteProjects || false,
canDeleteServices: member.canDeleteServices || false, // canDeleteServices: member.canDeleteServices || false,
accessedProjects: member.accessedProjects || [], // accessedProjects: member.accessedProjects || [],
accessedServices: member.accessedServices || [], // accessedServices: member.accessedServices || [],
}) // })
.returning() // .returning()
.then((userTemp) => userTemp[0]); // .then((userTemp) => userTemp[0]);
await db.insert(schema.account).values({ // await db.insert(schema.account).values({
providerId: "credential", // providerId: "credential",
userId: member?.userId || "", // userId: member?.userId || "",
password: member.auth.password, // password: member.auth.password,
is2FAEnabled: member.auth.is2FAEnabled || false, // is2FAEnabled: member.auth.is2FAEnabled || false,
createdAt: new Date(member.auth.createdAt) || new Date(), // createdAt: new Date(member.auth.createdAt) || new Date(),
updatedAt: new Date(member.auth.createdAt) || new Date(), // updatedAt: new Date(member.auth.createdAt) || new Date(),
}); // });
await db.insert(schema.member).values({ // await db.insert(schema.member).values({
organizationId: organization?.id || "", // organizationId: organization?.id || "",
userId: userTemp?.id || "", // userId: userTemp?.id || "",
role: "admin", // role: "admin",
createdAt: new Date(member.createdAt) || new Date(), // createdAt: new Date(member.createdAt) || new Date(),
}); // });
} // }
} // }
}) // })
.then(() => { // .then(() => {
console.log("Migration finished"); // console.log("Migration finished");
}) // })
.catch((error) => { // .catch((error) => {
console.error(error); // console.error(error);
}); // });
await db // await db
.transaction(async (db) => { // .transaction(async (db) => {
const projects = await db.query.projects.findMany({ // const projects = await db.query.projects.findMany({
with: { // with: {
user: { // user: {
with: { // with: {
organizations: true, // organizations: true,
}, // },
}, // },
}, // },
}); // });
for (const project of projects) { // for (const project of projects) {
const _user = await db.update(schema.projects).set({ // const _user = await db.update(schema.projects).set({
organizationId: project.user.organizations[0]?.id || "", // organizationId: project.user.organizations[0]?.id || "",
}); // });
} // }
}) // })
.then(() => { // .then(() => {
console.log("Migration finished"); // console.log("Migration finished");
}) // })
.catch((error) => { // .catch((error) => {
console.error(error); // console.error(error);
}); // });

View File

@@ -8,12 +8,14 @@ import {
} from "@dokploy/server"; } from "@dokploy/server";
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { import {
account,
apiAssignPermissions, apiAssignPermissions,
apiFindOneToken, apiFindOneToken,
apiUpdateUser, apiUpdateUser,
invitation, invitation,
member, member,
} from "@dokploy/server/db/schema"; } from "@dokploy/server/db/schema";
import * as bcrypt from "bcrypt";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { and, asc, eq, gt } from "drizzle-orm"; import { and, asc, eq, gt } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
@@ -81,6 +83,35 @@ export const userRouter = createTRPCRouter({
update: protectedProcedure update: protectedProcedure
.input(apiUpdateUser) .input(apiUpdateUser)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
if (input.password || input.currentPassword) {
const currentAuth = await db.query.account.findFirst({
where: eq(account.userId, ctx.user.id),
});
const correctPassword = bcrypt.compareSync(
input.currentPassword || "",
currentAuth?.password || "",
);
if (!correctPassword) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Current password is incorrect",
});
}
if (!input.password) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "New password is required",
});
}
await db
.update(account)
.set({
password: bcrypt.hashSync(input.password, 10),
})
.where(eq(account.userId, ctx.user.id));
}
return await updateUser(ctx.user.id, input); return await updateUser(ctx.user.id, input);
}), }),
getUserByToken: publicProcedure getUserByToken: publicProcedure

View File

@@ -278,6 +278,8 @@ export const apiUpdateWebServerMonitoring = z.object({
}); });
export const apiUpdateUser = createSchema.partial().extend({ export const apiUpdateUser = createSchema.partial().extend({
password: z.string().optional(),
currentPassword: z.string().optional(),
metricsConfig: z metricsConfig: z
.object({ .object({
server: z.object({ server: z.object({