From 845ccf1ad46c8635782f8e10280b07c48c08eaf5 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 13 Jul 2025 09:54:51 +0000 Subject: feat: Add delete account support --- packages/open-api/karakeep-openapi-spec.json | 6 +++- packages/shared-react/hooks/users.ts | 10 ++++++ packages/shared/types/users.ts | 1 + packages/trpc/routers/users.ts | 53 +++++++++++++++++++++++++++- 4 files changed, 68 insertions(+), 2 deletions(-) (limited to 'packages') diff --git a/packages/open-api/karakeep-openapi-spec.json b/packages/open-api/karakeep-openapi-spec.json index 69bf27f7..3d0fc721 100644 --- a/packages/open-api/karakeep-openapi-spec.json +++ b/packages/open-api/karakeep-openapi-spec.json @@ -3003,10 +3003,14 @@ "email": { "type": "string", "nullable": true + }, + "localUser": { + "type": "boolean" } }, "required": [ - "id" + "id", + "localUser" ] } } diff --git a/packages/shared-react/hooks/users.ts b/packages/shared-react/hooks/users.ts index e896f8e4..31018f0b 100644 --- a/packages/shared-react/hooks/users.ts +++ b/packages/shared-react/hooks/users.ts @@ -12,3 +12,13 @@ export function useUpdateUserSettings( }, }); } + +export function useDeleteAccount( + ...opts: Parameters +) { + return api.users.deleteAccount.useMutation(opts[0]); +} + +export function useWhoAmI() { + return api.users.whoami.useQuery(); +} diff --git a/packages/shared/types/users.ts b/packages/shared/types/users.ts index e4b4315b..758b757d 100644 --- a/packages/shared/types/users.ts +++ b/packages/shared/types/users.ts @@ -35,6 +35,7 @@ export const zWhoAmIResponseSchema = z.object({ id: z.string(), name: z.string().nullish(), email: z.string().nullish(), + localUser: z.boolean(), }); export const zUserStatsResponseSchema = z.object({ diff --git a/packages/trpc/routers/users.ts b/packages/trpc/routers/users.ts index 8d6db6c7..4531875c 100644 --- a/packages/trpc/routers/users.ts +++ b/packages/trpc/routers/users.ts @@ -289,6 +289,52 @@ export const usersAppRouter = router({ } await deleteUserAssets({ userId: input.userId }); }), + deleteAccount: authedProcedure + .input( + z.object({ + password: z.string().optional(), + }), + ) + .mutation(async ({ input, ctx }) => { + invariant(ctx.user.email, "A user always has an email specified"); + + // Check if user has a password (local account) + const user = await ctx.db.query.users.findFirst({ + where: eq(users.id, ctx.user.id), + }); + + if (!user) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + + // If user has a password, verify it before allowing account deletion + if (user.password) { + if (!input.password) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Password is required for local accounts", + }); + } + + try { + await validatePassword(ctx.user.email, input.password); + } catch { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Invalid password", + }); + } + } + + // Delete the user account + const res = await ctx.db.delete(users).where(eq(users.id, ctx.user.id)); + if (res.changes == 0) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + + // Delete user assets + await deleteUserAssets({ userId: ctx.user.id }); + }), whoami: authedProcedure .output(zWhoAmIResponseSchema) .query(async ({ ctx }) => { @@ -301,7 +347,12 @@ export const usersAppRouter = router({ if (!userDb) { throw new TRPCError({ code: "UNAUTHORIZED" }); } - return { id: ctx.user.id, name: ctx.user.name, email: ctx.user.email }; + return { + id: ctx.user.id, + name: ctx.user.name, + email: ctx.user.email, + localUser: userDb.password !== null, + }; }), stats: authedProcedure .output(zUserStatsResponseSchema) -- cgit v1.2.3-70-g09d2