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:
+12
-18
@@ -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
@@ -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']);
|
||||
});
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user