feat(api): add DELETE /users/me account deletion endpoint

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-04-29 15:40:16 +02:00
parent a8bae0c1c3
commit e21f82fd8f
No known key found for this signature in database
2 changed files with 73 additions and 0 deletions

View file

@ -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(

View file

@ -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);