Merge pull request 'feature/account-deletion' (#73) from feature/account-deletion into develop

Reviewed-on: https://gitea.fernandovideira.com/SyntaxSquad/DADProject/pulls/73
Reviewed-by: KZix <[email protected]>
Reviewed-by: FernandoJVideira <[email protected]>
This commit was merged in pull request #73.
This commit is contained in:
2025-12-24 10:52:01 +00:00
3 changed files with 173 additions and 20 deletions
+12 -18
View File
@@ -2,7 +2,6 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
@@ -11,30 +10,27 @@ use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable, HasApiTokens, SoftDeletes;
use HasApiTokens, HasFactory, Notifiable, SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var list<string>
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
'nickname',
'photo_avatar_filename',
'type',
'blocked',
'photo_avatar_filename',
'coins_balance',
'custom',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
* @var array<int, string>
*/
protected $hidden = [
'password',
@@ -42,16 +38,14 @@ class User extends Authenticatable
];
/**
* Get the attributes that should be cast.
* The attributes that should be cast.
*
* @return array<string, string>
* @var array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'blocked' => 'boolean',
];
}
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'blocked' => 'boolean',
'deleted_at' => 'datetime',
];
}
+15 -1
View File
@@ -1,5 +1,4 @@
<?php
use App\Http\Controllers\AuthController;
use App\Http\Controllers\GameController;
use App\Http\Controllers\MatchController;
@@ -65,10 +64,12 @@ Route::middleware('auth:sanctum')->group(function () {
$user = $request->user();
// Delete old avatar if exists
if ($user->photo_avatar_filename) {
\Storage::disk('public')->delete('photos_avatars/' . $user->photo_avatar_filename);
}
// Store new avatar
$file = $request->file('avatar');
$filename = str_pad($user->id, 5, '0', STR_PAD_LEFT) . '_' . \Str::random(10) . '.' . $file->extension();
$file->storeAs('photos_avatars', $filename, 'public');
@@ -79,6 +80,19 @@ Route::middleware('auth:sanctum')->group(function () {
return response()->json($user);
});
// Delete account
Route::delete('/me', function (Request $request) {
$user = $request->user();
$user->tokens()->delete();
$user->delete();
return response()->json([
'message' => 'Account deleted successfully'
]);
});
Route::get('/{user}/matches', [UserController::class, 'getMatches']);
});
+146 -1
View File
@@ -87,6 +87,12 @@
>
Change Password
</button>
<button
:class="['flex-1 p-4 bg-transparent border-none border-b-2 border-transparent cursor-pointer text-sm font-medium text-gray-600 transition-all hover:bg-gray-100 hover:text-black', { 'border-b-black text-black bg-white': activeTab === 'delete' }]"
@click="activeTab = 'delete'"
>
Delete Account
</button>
</div>
<!-- Profile Information Tab -->
@@ -275,11 +281,124 @@
</div>
</form>
</div>
<!-- Delete Account Tab -->
<div v-if="activeTab === 'delete'" class="p-8 flex flex-col items-center">
<div class="max-w-lg">
<div class="border border-red-600 bg-red-50 p-6 mb-6">
<div class="flex items-start gap-3">
<svg class="w-6 h-6 text-red-600 flex-shrink-0 mt-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
<div>
<h3 class="text-lg font-semibold text-red-900 mb-2">Danger Zone</h3>
<p class="text-sm text-red-800 mb-2">
Deleting your account is permanent and cannot be undone. This action will:
</p>
<ul class="text-sm text-red-800 list-disc list-inside space-y-1">
<li>Permanently delete all your account data</li>
<li>Forfeit your current coin balance of <strong>{{ authStore.currentUser.coins_balance }} coins</strong></li>
<li>Remove your access to all games and matches</li>
<li>Delete your profile and all associated information</li>
</ul>
</div>
</div>
</div>
<div class="bg-white border border-gray-300 p-6">
<h4 class="text-base font-semibold text-gray-900 mb-4">
Are you absolutely sure?
</h4>
<p class="text-sm text-gray-600 mb-4">
To confirm deletion, please type your email address: <strong class="text-black">{{ authStore.currentUser.email }}</strong>
</p>
<input
v-model="deleteConfirmEmail"
type="text"
placeholder="Enter your email to confirm"
class="w-full px-3 py-3 border border-gray-300 text-base mb-4 focus:outline-none focus:border-red-600"
/>
<div class="flex gap-4">
<button
type="button"
@click="activeTab = 'info'"
class="flex-1 px-6 py-3 border border-gray-300 cursor-pointer text-base font-medium transition-all bg-white text-gray-700 hover:bg-gray-50"
>
Cancel
</button>
<button
@click="showDeleteConfirmation = true"
:disabled="deleteConfirmEmail !== authStore.currentUser.email"
class="flex-1 px-6 py-3 border border-red-600 cursor-pointer text-base font-medium transition-all bg-red-600 text-white hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-red-600"
>
Delete My Account
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div
v-if="showDeleteConfirmation"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
@click.self="showDeleteConfirmation = false"
>
<div class="bg-white border border-gray-300 max-w-md w-full shadow-xl">
<div class="p-6 border-b border-gray-300">
<h3 class="text-xl font-semibold text-gray-900">Final Confirmation</h3>
</div>
<div class="p-6">
<div class="flex items-start gap-3 mb-4">
<svg class="w-12 h-12 text-red-600 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<div>
<p class="text-base text-gray-900 font-semibold mb-2">
This is your last chance to cancel.
</p>
<p class="text-sm text-gray-600">
Once you click "Yes, Delete My Account", your account and all associated data will be permanently deleted. You will lose your <strong>{{ authStore.currentUser.coins_balance }} coins</strong> forever.
</p>
</div>
</div>
<div v-if="deletingAccount" class="text-center py-4">
<div class="spinner mx-auto mb-3"></div>
<p class="text-sm text-gray-600">Deleting your account...</p>
</div>
</div>
<div class="p-6 border-t border-gray-300 flex gap-3">
<button
@click="showDeleteConfirmation = false"
:disabled="deletingAccount"
class="flex-1 px-6 py-3 border border-gray-300 cursor-pointer text-base font-medium transition-all bg-white text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Cancel
</button>
<button
@click="deleteAccount"
:disabled="deletingAccount"
class="flex-1 px-6 py-3 border border-red-600 cursor-pointer text-base font-medium transition-all bg-red-600 text-white hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Yes, Delete My Account
</button>
</div>
</div>
</div>
<!-- No Data State -->
<div v-else class="bg-white border border-gray-300 p-12 text-center shadow-sm max-w-md w-full">
<div v-else-if="!authStore.isLoggedIn" class="bg-white border border-gray-300 p-12 text-center shadow-sm max-w-md w-full">
<div class="flex flex-col items-center gap-6">
<div class="spinner"></div>
<p>Redirecting to login...</p>
@@ -324,6 +443,11 @@ const updatingPassword = ref(false)
const passwordMessage = ref('')
const passwordMessageType = ref('')
// Delete account
const deleteConfirmEmail = ref('')
const showDeleteConfirmation = ref(false)
const deletingAccount = ref(false)
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleDateString('en-US', {
@@ -457,6 +581,27 @@ const handleAvatarUpload = async (event) => {
}
}
const deleteAccount = async () => {
deletingAccount.value = true
try {
await axios.delete(`${API_BASE_URL}/users/me`)
// Logout user
await authStore.logout()
// Close modal
showDeleteConfirmation.value = false
// Redirect to home/login
router.push('/login')
} catch (err) {
deletingAccount.value = false
showDeleteConfirmation.value = false
alert(err.response?.data?.message || 'Failed to delete account')
}
}
onMounted(async () => {
if (!authStore.isLoggedIn) {
router.push('/login')