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 @@
@@ -66,16 +83,14 @@ const closeModal = () => {
- {{
- form.errors.password
- }}
+ {{ passwordError }}
@@ -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
},
],
},
+ {
+ 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();