diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-07-13 09:54:51 +0000 |
|---|---|---|
| committer | Mohamed Bassem <me@mbassem.com> | 2025-07-13 09:54:51 +0000 |
| commit | 845ccf1ad46c8635782f8e10280b07c48c08eaf5 (patch) | |
| tree | b6b257d07532ff2b1a300c28d6f57623d9f1cdc0 /packages | |
| parent | f8ae986692f82efe8c1f3940907aab553e4f5a49 (diff) | |
| download | karakeep-845ccf1ad46c8635782f8e10280b07c48c08eaf5.tar.zst | |
feat: Add delete account support
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/open-api/karakeep-openapi-spec.json | 6 | ||||
| -rw-r--r-- | packages/shared-react/hooks/users.ts | 10 | ||||
| -rw-r--r-- | packages/shared/types/users.ts | 1 | ||||
| -rw-r--r-- | packages/trpc/routers/users.ts | 53 |
4 files changed, 68 insertions, 2 deletions
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<typeof api.users.deleteAccount.useMutation> +) { + 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) |
