diff --git a/apps/web/package.json b/apps/web/package.json
index 7708635a8..f493a3b22 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -20,6 +20,9 @@
"@auth/prisma-adapter": "^2.7.4",
"@aws-sdk/client-s3": "^3.645.0",
"@aws-sdk/s3-request-presigner": "^3.645.0",
+ "@casl/ability": "^6.7.3",
+ "@casl/prisma": "^1.5.1",
+ "@casl/react": "^5.0.0",
"@hono-rate-limiter/redis": "^0.1.4",
"@hono/zod-validator": "^0.5.0",
"@hookform/resolvers": "^3.3.1",
diff --git a/apps/web/public/locales/en/app.json b/apps/web/public/locales/en/app.json
index 13a8afee6..b31fa3021 100644
--- a/apps/web/public/locales/en/app.json
+++ b/apps/web/public/locales/en/app.json
@@ -330,9 +330,6 @@
"canceledEventsEmptyStateDescription": "Canceled events will show up here.",
"setupFormTitle": "Setup",
"setupFormDescription": "Finish setting up your account.",
- "errorNotAuthenticated": "Not authenticated",
- "errorInvalidFields": "Invalid fields. Please check your input.",
- "errorDatabaseUpdateFailed": "Database error: Failed to update settings.",
"pending": "Pending",
"helpUsImprove": "Help us improve",
"helpUsImproveDesc": "Take a few minutes to share your feedback and help us shape the future of Rallly.",
@@ -361,9 +358,6 @@
"users": "Users",
"userCount": "{count, number, ::compact-short}",
"unlicensed": "Unlicensed",
- "deleteUser": "Delete User",
- "areYouSureYouWantToDeleteThisUser": "Are you sure you want to delete this user?",
- "typeTheEmailAddressOfTheUserYouWantToDelete": "Type the email address of the user you want to delete.",
"emailDoesNotMatch": "The email address does not match.",
"noUsers": "No users found",
"noUsersDescription": "Try adjusting your search",
@@ -395,5 +389,11 @@
"disableUserRegistrationDescription": "Prevent new users from registering an account.",
"authenticationAndSecurity": "Authentication & Security",
"authenticationAndSecurityDescription": "Manage authentication and security settings",
- "youHaveUnsavedChanges": "You have unsaved changes"
+ "youHaveUnsavedChanges": "You have unsaved changes",
+ "unexpectedError": "Unexpected Error",
+ "unexpectedErrorDescription": "There was an unexpected error. Please try again later.",
+ "actionErrorUnauthorized": "You are not authorized to perform this action",
+ "actionErrorNotFound": "The resource was not found",
+ "actionErrorForbidden": "You are not allowed to perform this action",
+ "actionErrorInternalServerError": "An internal server error occurred"
}
diff --git a/apps/web/src/app/[locale]/(space)/settings/profile/actions.ts b/apps/web/src/app/[locale]/(space)/settings/profile/actions.ts
new file mode 100644
index 000000000..1163dedc5
--- /dev/null
+++ b/apps/web/src/app/[locale]/(space)/settings/profile/actions.ts
@@ -0,0 +1,40 @@
+"use server";
+
+import { ActionError, authActionClient } from "@/features/safe-action/server";
+import { subject } from "@casl/ability";
+import { prisma } from "@rallly/database";
+
+export const deleteCurrentUserAction = authActionClient.action(
+ async ({ ctx }) => {
+ const userId = ctx.user.id;
+
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ include: { spaces: { include: { subscription: true } } },
+ });
+
+ if (!user) {
+ throw new ActionError({
+ code: "NOT_FOUND",
+ message: "User not found",
+ });
+ }
+
+ if (ctx.ability.cannot("delete", subject("User", user))) {
+ throw new ActionError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to delete this user",
+ });
+ }
+
+ await prisma.user.delete({
+ where: {
+ id: userId,
+ },
+ });
+
+ return {
+ success: true,
+ };
+ },
+);
diff --git a/apps/web/src/app/[locale]/(space)/settings/profile/delete-account-dialog.tsx b/apps/web/src/app/[locale]/(space)/settings/profile/delete-account-dialog.tsx
index b8dfe1080..9b705fdf6 100644
--- a/apps/web/src/app/[locale]/(space)/settings/profile/delete-account-dialog.tsx
+++ b/apps/web/src/app/[locale]/(space)/settings/profile/delete-account-dialog.tsx
@@ -17,8 +17,9 @@ import { signOut } from "next-auth/react";
import { useForm } from "react-hook-form";
import { Trans } from "@/components/trans";
+import { useSafeAction } from "@/features/safe-action/client";
import { useTranslation } from "@/i18n/client";
-import { trpc } from "@/trpc/client";
+import { deleteCurrentUserAction } from "./actions";
export function DeleteAccountDialog({
email,
@@ -32,16 +33,18 @@ export function DeleteAccountDialog({
email: "",
},
});
- const { t } = useTranslation();
+
const posthog = usePostHog();
- const deleteAccount = trpc.user.delete.useMutation({
- onSuccess() {
+
+ const deleteUser = useSafeAction(deleteCurrentUserAction, {
+ onSuccess: () => {
posthog?.capture("delete account");
signOut({
redirectTo: "/login",
});
},
});
+ const { t } = useTranslation();
return (
-
-
- );
-}
diff --git a/apps/web/src/app/[locale]/control-panel/users/dialogs/delete-user-dialog.tsx b/apps/web/src/app/[locale]/control-panel/users/dialogs/delete-user-dialog.tsx
index 5c234ebc4..dc05972bc 100644
--- a/apps/web/src/app/[locale]/control-panel/users/dialogs/delete-user-dialog.tsx
+++ b/apps/web/src/app/[locale]/control-panel/users/dialogs/delete-user-dialog.tsx
@@ -1,5 +1,7 @@
"use client";
import { Trans } from "@/components/trans";
+import { useSafeAction } from "@/features/safe-action/client";
+import { deleteUserAction } from "@/features/user/actions";
import { useTranslation } from "@/i18n/client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@rallly/ui/button";
@@ -23,10 +25,8 @@ import {
} from "@rallly/ui/form";
import { Input } from "@rallly/ui/input";
import { useRouter } from "next/navigation";
-import { useTransition } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
-import { deleteUser } from "../actions";
const useSchema = (email: string) => {
const { t } = useTranslation();
@@ -55,13 +55,20 @@ export function DeleteUserDialog({
}) {
const router = useRouter();
const schema = useSchema(email);
- const [isPending, startTransition] = useTransition();
const form = useForm({
resolver: zodResolver(schema),
defaultValues: {
email: "",
},
});
+
+ const deleteUser = useSafeAction(deleteUserAction, {
+ onSuccess: () => {
+ router.refresh();
+ onOpenChange(false);
+ },
+ });
+
return (