diff --git a/apps/api/src/__tests__/routes/user.test.ts b/apps/api/src/__tests__/routes/user.test.ts index 753c550..857f5db 100644 --- a/apps/api/src/__tests__/routes/user.test.ts +++ b/apps/api/src/__tests__/routes/user.test.ts @@ -163,6 +163,28 @@ describe("User Endpoint", () => { }); }); + describe("DELETE /me - Delete Account", () => { + it("should return 401 when unauthenticated", async () => { + const res = await client.users.me.$delete({}); + expect(res.status).toBe(401); + }); + + it("should delete the authenticated user's account", async () => { + const res = await client.users.me.$delete( + {}, + { + headers: { + Authorization: `Bearer ${ownerUser.accessToken}`, + "Content-Type": "application/json", + }, + } + ); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toEqual({ message: "Account deleted successfully" }); + }); + }); + describe("DELETE /profile/avatar - Delete Avatar", () => { it("should delete avatar for owner user", async () => { const res = await client.users.profile.avatar.$delete( diff --git a/apps/api/src/routers/user.ts b/apps/api/src/routers/user.ts index f5cdde4..c072c22 100644 --- a/apps/api/src/routers/user.ts +++ b/apps/api/src/routers/user.ts @@ -801,10 +801,61 @@ const removeOrganizationMember = factory.createHandlers(async (c) => { return c.json({ message: "Member removed successfully" }); }); +const deleteMe = factory.createHandlers(async (c) => { + const user = c.get("user"); + const supabase = c.get("supabase"); + + // Fetch profile to get organization_id + const { data: profile, error: profileError } = await supabase + .from("profiles") + .select("organization_id") + .eq("id", user.id) + .single(); + + if (profileError || !profile) { + return c.json({ error: "User not found" }, 404); + } + + const typedProfile = profile as typeof profile & { organization_id: number | null }; + + // If user is sole org member, soft-delete the org too + if (typedProfile.organization_id) { + const { count } = await supabase + .from("profiles") + .select("id", { count: "exact", head: true }) + .eq("organization_id", typedProfile.organization_id); + + if ((count ?? 0) === 1) { + await (supabase.from("organizations") as any) + .update({ deleted_at: new Date().toISOString() }) + .eq("id", typedProfile.organization_id); + } + } + + // Soft-delete the profile + await (supabase.from("profiles") as any) + .update({ deleted_at: new Date().toISOString() }) + .eq("id", user.id); + + // Hard-delete the Supabase auth user (revokes all sessions immediately) + const { error: authDeleteError } = await supabase.auth.admin.deleteUser(user.id); + + if (authDeleteError) { + // Roll back the soft delete + await (supabase.from("profiles") as any) + .update({ deleted_at: null }) + .eq("id", user.id); + return c.json({ error: "Failed to delete account" }, 500); + } + + return c.json({ message: "Account deleted successfully" }); +}); + export const getUserRouter = () => { const userRouter = new Hono(); userRouter.get("/me", ...getMe); + userRouter.delete("/me", ...deleteMe); userRouter.post("/mark-temporary", ...markTemporary); userRouter.post("/profile/avatar", ...uploadAvatar); userRouter.delete("/profile/avatar", ...deleteAvatar);