From 058ee0f844440be250694fc392698510d2607407 Mon Sep 17 00:00:00 2001 From: TR Tajim Date: Sun, 18 Oct 2026 14:22:23 +0600 Subject: [PATCH 1/4] feat(account): implement signed account deletion workflow and refined 4-step wizard --- .../Controllers/AccountDeletionController.php | 61 ++ app/Http/Controllers/Admin/UserController.php | 25 + app/Mail/AccountDeletionLinkMail.php | 49 ++ resources/js/components/admin/UserRow.vue | 153 +++- .../js/pages/Account/ConfirmDeletion.vue | 149 ++++ resources/js/pages/Account/Delete.vue | 746 ++++++++++++------ routes/admin.php | 1 + routes/web.php | 7 + .../Feature/AccountDeletionSignedUrlTest.php | 210 +++++ 9 files changed, 1158 insertions(+), 243 deletions(-) create mode 100644 app/Http/Controllers/AccountDeletionController.php create mode 100644 app/Mail/AccountDeletionLinkMail.php create mode 100644 resources/js/pages/Account/ConfirmDeletion.vue create mode 100644 tests/Feature/AccountDeletionSignedUrlTest.php diff --git a/app/Http/Controllers/AccountDeletionController.php b/app/Http/Controllers/AccountDeletionController.php new file mode 100644 index 00000000..74010ab4 --- /dev/null +++ b/app/Http/Controllers/AccountDeletionController.php @@ -0,0 +1,61 @@ +email), (string) $request->query('hash'))) { + abort(403, 'Invalid deletion token.'); + } + + return Inertia::render('Account/ConfirmDeletion', [ + 'user' => [ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + ], + 'confirmUrl' => $request->fullUrl(), + ]); + } + + /** + * Process the permanent deletion requested via signed URL. + */ + public function confirmDelete(Request $request, User $user) + { + if (! hash_equals(sha1($user->email), (string) $request->query('hash'))) { + abort(403, 'Invalid deletion token.'); + } + + // If currently authenticated as this user, log them out and invalidate session + if (Auth::id() === $user->id) { + Auth::logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + } + + $email = $user->email; + $name = $user->name; + + if (! empty($email)) { + Mail::to($email)->queue(new AccountDeletedMail($name)); + } + + $user->delete(); + + return redirect('/') + ->with('success', 'আপনার অ্যাকাউন্টটি সফলভাবে মুছে ফেলা হয়েছে।'); + } +} diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 7b2f420d..cade5e36 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -6,12 +6,14 @@ use App\Http\Requests\User\StoreUserRequest; use App\Http\Requests\User\UpdateUserRequest; use App\Mail\AccountDeletedMail; +use App\Mail\AccountDeletionLinkMail; use App\Models\User; use App\Notifications\WelcomeNotification; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Facades\URL; use Inertia\Inertia; use Spatie\Permission\Models\Permission; @@ -170,6 +172,29 @@ public function destroy(User $user) ->with('success', 'User deleted successfully.'); } + /** + * Send a temporary signed account deletion link to the user. + */ + public function sendDeleteLink(User $user) + { + if (empty($user->email)) { + return back()->with('error', 'This user does not have an email address.'); + } + + $signedUrl = URL::temporarySignedRoute( + 'account.delete.verify', + now()->addDays(7), + [ + 'user' => $user->id, + 'hash' => sha1($user->email), + ] + ); + + Mail::to($user->email)->queue(new AccountDeletionLinkMail($user, $signedUrl)); + + return back()->with('success', "Deletion link sent successfully to {$user->email}."); + } + /** * Log in as the specified user. */ diff --git a/app/Mail/AccountDeletionLinkMail.php b/app/Mail/AccountDeletionLinkMail.php new file mode 100644 index 00000000..81d68bcd --- /dev/null +++ b/app/Mail/AccountDeletionLinkMail.php @@ -0,0 +1,49 @@ +user->name ? "Hello {$this->user->name}," : 'প্রিয় শিক্ষার্থী,'; + + return new Content( + view: 'emails.default', + with: [ + 'subject' => 'আপনার অ্যাকাউন্ট মুছে ফেলার কনফার্মেশন লিংক', + 'greeting' => $name, + 'lines' => [ + 'অ্যাডমিন আপনার অ্যাকাউন্ট স্থায়ীভাবে মুছে ফেলার জন্য একটি কনফার্মেশন লিংক পাঠিয়েছেন।', + 'আপনি যদি আপনার HSCStack অ্যাকাউন্টটি ডিলিট করতে চান, তাহলে নিচের বাটনে ক্লিক করে কনফার্মেশন সম্পন্ন করুন।', + 'সতর্কতা: এই লিংকের মেয়াদ আগামী ৭ দিন পর্যন্ত কার্যকর থাকবে। অ্যাকাউন্ট মুছে ফেলার পর আপনার সব ডাটা স্থায়ীভাবে ডিলিট হয়ে যাবে এবং তা আর পুনরুদ্ধার করা সম্ভব হবে না।', + 'আপনি যদি অ্যাকাউন্ট ডিলিট করতে না চান, তবে এই ইমেইলটি উপেক্ষা করুন। আপনার অ্যাকাউন্ট সুরক্ষিত থাকবে।', + ], + 'actionText' => 'অ্যাকাউন্ট ডিলিট করুন (Confirm Deletion)', + 'actionUrl' => $this->signedUrl, + ], + ); + } +} diff --git a/resources/js/components/admin/UserRow.vue b/resources/js/components/admin/UserRow.vue index af7221c0..37f7c3e3 100644 --- a/resources/js/components/admin/UserRow.vue +++ b/resources/js/components/admin/UserRow.vue @@ -1,7 +1,8 @@ diff --git a/resources/js/pages/Account/ConfirmDeletion.vue b/resources/js/pages/Account/ConfirmDeletion.vue new file mode 100644 index 00000000..b0861168 --- /dev/null +++ b/resources/js/pages/Account/ConfirmDeletion.vue @@ -0,0 +1,149 @@ + + + diff --git a/resources/js/pages/Account/Delete.vue b/resources/js/pages/Account/Delete.vue index 978838fb..34df7af2 100644 --- a/resources/js/pages/Account/Delete.vue +++ b/resources/js/pages/Account/Delete.vue @@ -4,14 +4,17 @@ import { AlertTriangle, Trash2, ArrowLeft, + ArrowRight, Loader2, ChevronDown, User, - MessageSquare, ShieldAlert, + Clock, + MailCheck, + CheckCircle2, + FileText, } from 'lucide-vue-next'; import { computed, ref } from 'vue'; -import BaseModal from '@/components/BaseModal.vue'; interface UserInfo { id: number; @@ -33,10 +36,12 @@ const authUser = computed( const isTicketLimitReached = computed(() => (props.openTicketsCount ?? 0) >= 3); +// Stepper State (1: সতর্কতা, 2: কারণ, 3: প্রক্রিয়া, 4: নিশ্চিতকরণ) +const currentStep = ref(1); + const selectedReason = ref(''); const feedback = ref(''); const isConfirmed = ref(false); -const showConfirmModal = ref(false); const reasons = [ { @@ -72,16 +77,20 @@ const form = useForm({ general: '' as string | undefined, }); -const openConfirmModal = () => { - if (isTicketLimitReached.value || !isConfirmed.value) { - return; +const goToNextStep = () => { + if (currentStep.value < 4) { + currentStep.value++; } +}; - showConfirmModal.value = true; +const goToPrevStep = () => { + if (currentStep.value > 1) { + currentStep.value--; + } }; const submitDeleteTicket = () => { - if (isTicketLimitReached.value) { + if (isTicketLimitReached.value || !isConfirmed.value) { return; } @@ -98,7 +107,7 @@ const submitDeleteTicket = () => { `Reason: ${reasonLabel}`, `Feedback/Note: ${feedback.value.trim() || 'None provided'}`, '', - 'User confirmed permanent account deletion request after reviewing legal policies and timeline notice.', + 'User completed 4-step deletion request flow agreeing to 48h signed email confirmation.', ].join('\n'); form.category = 'account_issue'; @@ -107,9 +116,6 @@ const submitDeleteTicket = () => { form.post('/support/tickets', { preserveScroll: true, - onSuccess: () => { - showConfirmModal.value = false; - }, }); }; @@ -123,14 +129,14 @@ const submitDeleteTicket = () => { /> -
- -
+
+ +
- + অ্যাকাউন্ট সেটিংসে ফিরে যান
@@ -138,26 +144,25 @@ const submitDeleteTicket = () => {
-
+
-
-

- আপনার ইতিমধ্যে ৩টি খোলা সাপোর্ট টিকেট রয়েছে। -

-

- নতুন টিকেট খোলার আগে অনুগ্রহ করে পূর্ববর্তী টিকেটের - সমাধান হওয়া পর্যন্ত অপেক্ষা করুন। +

+

সাপোর্ট টিকিট সীমা পূর্ণ হয়েছে

+

+ আপনার ইতিমধ্যে ৩টি সক্রিয় সাপোর্ট টিকিট উন্মুক্ত + রয়েছে। নতুন রিকোয়েস্ট পাঠাতে পূর্বের টিকিট নিষ্পত্তি + হওয়া পর্যন্ত অপেক্ষা করুন।

- - আমার টিকেটসমূহ + আমার টিকিটগুলো দেখুন +
@@ -165,43 +170,180 @@ const submitDeleteTicket = () => {
-
+

অ্যাকাউন্ট ডিলিট রিকোয়েস্ট

-

- অ্যাকাউন্ট ও সংশ্লিষ্ট সকল ডেটা স্থায়ীভাবে মুছে ফেলার - জন্য সরাসরি সাপোর্ট টিকেট তৈরি হবে। +

+ অ্যাকাউন্ট ও সংশ্লিষ্ট সকল ডেটা স্থায়ীভাবে মুছে ফেলার + জন্য আবেদন জমা নেওয়া হবে।

+ +
+
+ +
+
+ + ১ +
+ + সতর্কতা + +
+ +
+ + +
+
+ + ২ +
+ + কারণ + +
+ +
+ + +
+
+ + ৩ +
+ + প্রক্রিয়া + +
+ +
+ + +
+
+ ৪ +
+ + নিশ্চিতকরণ + +
+
+
+
- +

{ @{{ authUser?.username }} • {{ authUser.email }} @@ -223,174 +365,296 @@ const submitDeleteTicket = () => {

- -
-
- {{ form.errors.general }} -
+ +
+ {{ form.errors.general }} +
- -
-