From 10a66fa0654543afdf2cc896ae4273c2fd1fb974 Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Wed, 27 May 2026 18:56:57 +0200 Subject: [PATCH] move user delete to api endpoint --- e2e/profile.spec.ts | 37 ++++++++++ .../Pages/Profile/Partials/DeleteUserForm.vue | 69 +++++++++++-------- .../packages/api/src/openapi.json.client.ts | 39 +++++++++++ resources/js/utils/useUserQuery.ts | 23 +++++++ 4 files changed, 141 insertions(+), 27 deletions(-) diff --git a/e2e/profile.spec.ts b/e2e/profile.spec.ts index 16827a3f..f71782f0 100644 --- a/e2e/profile.spec.ts +++ b/e2e/profile.spec.ts @@ -297,6 +297,43 @@ test('visiting the verification link while logged out redirects to login', async } }); +test('delete account shows an error when the password is wrong', async ({ page }) => { + await goToProfilePage(page); + await page.getByRole('button', { name: 'Delete Account' }).click(); + const dialog = page.getByRole('dialog'); + await dialog.getByPlaceholder('Password').fill('not-the-real-password'); + await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/user/confirm-password') && + response.request().method() === 'POST' && + response.status() === 422 + ), + dialog.getByRole('button', { name: 'Delete Account' }).click(), + ]); + await expect(dialog.getByRole('alert')).toBeVisible(); + await expect(dialog).toBeVisible(); +}); + +test('delete account succeeds with the correct password and logs the user out', async ({ + page, +}) => { + await goToProfilePage(page); + await page.getByRole('button', { name: 'Delete Account' }).click(); + const dialog = page.getByRole('dialog'); + await dialog.getByPlaceholder('Password').fill(TEST_USER_PASSWORD); + await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/api/v1/users/') && + response.request().method() === 'DELETE' && + response.status() === 204 + ), + dialog.getByRole('button', { name: 'Delete Account' }).click(), + ]); + await page.waitForURL(/\/login/); +}); + async function createNewApiToken(page) { await page.getByLabel('API Key Name').fill('NEW API KEY'); await Promise.all([ diff --git a/resources/js/Pages/Profile/Partials/DeleteUserForm.vue b/resources/js/Pages/Profile/Partials/DeleteUserForm.vue index 187dc1b7..ae5cd48b 100644 --- a/resources/js/Pages/Profile/Partials/DeleteUserForm.vue +++ b/resources/js/Pages/Profile/Partials/DeleteUserForm.vue @@ -1,40 +1,57 @@ @@ -84,8 +99,8 @@ const closeModal = () => { Delete Account diff --git a/resources/js/packages/api/src/openapi.json.client.ts b/resources/js/packages/api/src/openapi.json.client.ts index ffb46365..581ffe4a 100644 --- a/resources/js/packages/api/src/openapi.json.client.ts +++ b/resources/js/packages/api/src/openapi.json.client.ts @@ -4534,6 +4534,45 @@ The report is considered public if the `is_public` field is set to &#x }, ], }, + { + method: 'delete', + path: '/v1/users/:user', + alias: 'deleteUser', + description: `This endpoint is independent of the organization.`, + requestFormat: 'json', + parameters: [ + { + name: 'user', + type: 'Path', + schema: z.string(), + }, + ], + response: z.void(), + errors: [ + { + status: 400, + description: `API exception`, + schema: z + .object({ error: z.boolean(), key: z.string(), message: z.string() }) + .passthrough(), + }, + { + status: 401, + description: `Unauthenticated`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + ], + }, { method: 'post', path: '/v1/users/:user/resend-email-verification', diff --git a/resources/js/utils/useUserQuery.ts b/resources/js/utils/useUserQuery.ts index b0ea4c5d..fda78d92 100644 --- a/resources/js/utils/useUserQuery.ts +++ b/resources/js/utils/useUserQuery.ts @@ -57,6 +57,29 @@ export function useUpdateUserMutation() { }); } +export function useDeleteUserMutation() { + const { addNotification } = useNotificationsStore(); + + return useMutation({ + mutationFn: async (userId: string) => { + try { + await api.deleteUser(undefined, { params: { user: userId } }); + } catch (error) { + if (!axios.isAxiosError(error) || error.response?.status !== 422) { + addNotification( + 'error', + 'Failed to delete account', + axios.isAxiosError(error) + ? (error.response?.data?.message ?? 'Please try again later.') + : 'Please try again later.' + ); + } + throw error; + } + }, + }); +} + export function useResendUserEmailVerificationMutation() { const { handleApiRequestNotifications } = useNotificationsStore();