From afedeede16ca74a48903fd743d5e62552dd40f93 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:45:02 -0600 Subject: [PATCH] feat: add self remove accounts --- .../settings/profile/profile-form.tsx | 28 +++- .../settings/profile/remove-self-account.tsx | 130 ++++++++++++++++++ .../servers/welcome-stripe/create-server.tsx | 2 +- .../components/layouts/settings-layout.tsx | 3 +- .../pages/dashboard/settings/profile.tsx | 5 + apps/dokploy/pages/register.tsx | 2 +- apps/dokploy/server/api/routers/auth.ts | 69 +++++++++- packages/server/src/db/schema/auth.ts | 1 + packages/server/src/services/admin.ts | 21 +++ 9 files changed, 254 insertions(+), 7 deletions(-) create mode 100644 apps/dokploy/components/dashboard/settings/profile/remove-self-account.tsx diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index 1d4daa53..65ccff0e 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -26,10 +26,12 @@ import { toast } from "sonner"; import { z } from "zod"; import { Disable2FA } from "./disable-2fa"; import { Enable2FA } from "./enable-2fa"; +import { AlertBlock } from "@/components/shared/alert-block"; const profileSchema = z.object({ email: z.string(), password: z.string().nullable(), + currentPassword: z.string().nullable(), image: z.string().optional(), }); @@ -52,7 +54,8 @@ const randomImages = [ export const ProfileForm = () => { const { data, refetch } = api.auth.get.useQuery(); - const { mutateAsync, isLoading } = api.auth.update.useMutation(); + const { mutateAsync, isLoading, isError, error } = + api.auth.update.useMutation(); const { t } = useTranslation("settings"); const [gravatarHash, setGravatarHash] = useState(null); @@ -68,6 +71,7 @@ export const ProfileForm = () => { email: data?.email || "", password: "", image: data?.image || "", + currentPassword: "", }, resolver: zodResolver(profileSchema), }); @@ -78,6 +82,7 @@ export const ProfileForm = () => { email: data?.email || "", password: "", image: data?.image || "", + currentPassword: "", }); if (data.email) { @@ -94,6 +99,7 @@ export const ProfileForm = () => { email: values.email.toLowerCase(), password: values.password, image: values.image, + currentPassword: values.currentPassword, }) .then(async () => { await refetch(); @@ -116,6 +122,8 @@ export const ProfileForm = () => { {!data?.is2FAEnabled ? : } + {isError && {error?.message}} +
@@ -135,6 +143,24 @@ export const ProfileForm = () => { )} /> + ( + + Current Password + + + + + + )} + /> ; + +export const RemoveSelfAccount = () => { + const { data } = api.auth.get.useQuery(); + const { mutateAsync, isLoading, error, isError } = + api.auth.removeSelfAccount.useMutation(); + const { t } = useTranslation("settings"); + const router = useRouter(); + + const form = useForm({ + defaultValues: { + password: "", + }, + resolver: zodResolver(profileSchema), + }); + + useEffect(() => { + if (data) { + form.reset({ + password: "", + }); + } + form.reset(); + }, [form, form.reset, data]); + + const onSubmit = async (values: Profile) => { + await mutateAsync({ + password: values.password, + }) + .then(async () => { + toast.success("Profile Deleted"); + router.push("/"); + }) + .catch(() => {}); + }; + + return ( + + +
+ Remove Self Account + + If you want to remove your account, you can do it here + +
+
+ + {isError && {error?.message}} + + + e.preventDefault()} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + } + }} + className="grid gap-4" + > +
+ ( + + {t("settings.profile.password")} + + + + + + )} + /> +
+ + +
+ form.handleSubmit(onSubmit)()} + > + + +
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-server.tsx b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-server.tsx index a44876f8..39edad71 100644 --- a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-server.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-server.tsx @@ -270,7 +270,7 @@ export const CreateServer = ({ stepper }: Props) => {
)} - {data && ( + {data?.type === "cloud" && ( Registration succesfuly, Please check your inbox or spam diff --git a/apps/dokploy/server/api/routers/auth.ts b/apps/dokploy/server/api/routers/auth.ts index 256be48b..ef9db4da 100644 --- a/apps/dokploy/server/api/routers/auth.ts +++ b/apps/dokploy/server/api/routers/auth.ts @@ -20,6 +20,8 @@ import { getUserByToken, lucia, luciaToken, + removeAdminByAuthId, + removeUserByAuthId, sendDiscordNotification, sendEmailNotification, updateAuthById, @@ -59,14 +61,20 @@ export const authRouter = createTRPCRouter({ if (IS_CLOUD) { await sendDiscordNotificationWelcome(newAdmin); await sendVerificationEmail(newAdmin.id); - return true; + return { + status: "success", + type: "cloud", + }; } const session = await lucia.createSession(newAdmin.id || "", {}); ctx.res.appendHeader( "Set-Cookie", lucia.createSessionCookie(session.id).serialize(), ); - return true; + return { + status: "success", + type: "selfhosted", + }; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", @@ -178,6 +186,20 @@ export const authRouter = createTRPCRouter({ update: protectedProcedure .input(apiUpdateAuth) .mutation(async ({ ctx, input }) => { + const currentAuth = await findAuthByEmail(ctx.user.email); + + if (input.password) { + const correctPassword = bcrypt.compareSync( + input.password, + currentAuth?.password || "", + ); + if (!correctPassword) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Current password is incorrect", + }); + } + } const auth = await updateAuthById(ctx.user.authId, { ...(input.email && { email: input.email.toLowerCase() }), ...(input.password && { @@ -188,6 +210,47 @@ export const authRouter = createTRPCRouter({ return auth; }), + removeSelfAccount: protectedProcedure + .input( + z.object({ + password: z.string().min(1), + }), + ) + .mutation(async ({ ctx, input }) => { + if (!IS_CLOUD) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "This feature is only available in the cloud version", + }); + } + const currentAuth = await findAuthByEmail(ctx.user.email); + + const correctPassword = bcrypt.compareSync( + input.password, + currentAuth?.password || "", + ); + + if (!correctPassword) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Password is incorrect", + }); + } + const { req, res } = ctx; + const { session } = await validateRequest(req, res); + if (!session) return false; + + await lucia.invalidateSession(session.id); + res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize()); + + if (ctx.user.rol === "admin") { + await removeAdminByAuthId(ctx.user.authId); + } else { + await removeUserByAuthId(ctx.user.authId); + } + + return true; + }), generateToken: protectedProcedure.mutation(async ({ ctx, input }) => { const auth = await findAuthById(ctx.user.authId); @@ -440,7 +503,7 @@ export const sendDiscordNotificationWelcome = async (newAdmin: Auth) => { webhookUrl: process.env.DISCORD_WEBHOOK_URL || "", }, { - title: "✅ New User Registered", + title: " New User Registered", color: 0x00ff00, fields: [ { diff --git a/packages/server/src/db/schema/auth.ts b/packages/server/src/db/schema/auth.ts index 0f8640fc..3e16c68e 100644 --- a/packages/server/src/db/schema/auth.ts +++ b/packages/server/src/db/schema/auth.ts @@ -92,6 +92,7 @@ export const apiUpdateAuth = createSchema.partial().extend({ email: z.string().nullable(), password: z.string().nullable(), image: z.string().optional(), + currentPassword: z.string().nullable(), }); export const apiUpdateAuthByAdmin = createSchema.partial().extend({ diff --git a/packages/server/src/services/admin.ts b/packages/server/src/services/admin.ts index a52b1a6e..2e7c5735 100644 --- a/packages/server/src/services/admin.ts +++ b/packages/server/src/services/admin.ts @@ -88,6 +88,9 @@ export const isAdminPresent = async () => { export const findAdminByAuthId = async (authId: string) => { const admin = await db.query.admins.findFirst({ where: eq(admins.authId, authId), + with: { + users: true, + }, }); if (!admin) { throw new TRPCError({ @@ -141,6 +144,24 @@ export const removeUserByAuthId = async (authId: string) => { .then((res) => res[0]); }; +export const removeAdminByAuthId = async (authId: string) => { + const admin = await findAdminByAuthId(authId); + if (!admin) return null; + + // First delete all associated users + const users = admin.users; + + for (const user of users) { + await removeUserByAuthId(user.authId); + } + // Then delete the auth record which will cascade delete the admin + return await db + .delete(auth) + .where(eq(auth.id, authId)) + .returning() + .then((res) => res[0]); +}; + export const getDokployUrl = async () => { if (IS_CLOUD) { return "https://app.dokploy.com";