aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-07-13 09:54:51 +0000
committerMohamed Bassem <me@mbassem.com>2025-07-13 09:54:51 +0000
commit845ccf1ad46c8635782f8e10280b07c48c08eaf5 (patch)
treeb6b257d07532ff2b1a300c28d6f57623d9f1cdc0 /packages
parentf8ae986692f82efe8c1f3940907aab553e4f5a49 (diff)
downloadkarakeep-845ccf1ad46c8635782f8e10280b07c48c08eaf5.tar.zst
feat: Add delete account support
Diffstat (limited to 'packages')
-rw-r--r--packages/open-api/karakeep-openapi-spec.json6
-rw-r--r--packages/shared-react/hooks/users.ts10
-rw-r--r--packages/shared/types/users.ts1
-rw-r--r--packages/trpc/routers/users.ts53
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)