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:
+8
-14
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
@@ -11,30 +10,27 @@ use Laravel\Sanctum\HasApiTokens;
|
|||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
use HasApiTokens, HasFactory, Notifiable, SoftDeletes;
|
||||||
use HasFactory, Notifiable, HasApiTokens, SoftDeletes;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
*
|
*
|
||||||
* @var list<string>
|
* @var array<int, string>
|
||||||
*/
|
*/
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'name',
|
||||||
'email',
|
'email',
|
||||||
'password',
|
'password',
|
||||||
'nickname',
|
'nickname',
|
||||||
'photo_avatar_filename',
|
|
||||||
'type',
|
'type',
|
||||||
'blocked',
|
'photo_avatar_filename',
|
||||||
'coins_balance',
|
'coins_balance',
|
||||||
'custom',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that should be hidden for serialization.
|
* The attributes that should be hidden for serialization.
|
||||||
*
|
*
|
||||||
* @var list<string>
|
* @var array<int, string>
|
||||||
*/
|
*/
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
'password',
|
'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
|
protected $casts = [
|
||||||
{
|
|
||||||
return [
|
|
||||||
'email_verified_at' => 'datetime',
|
'email_verified_at' => 'datetime',
|
||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
'blocked' => 'boolean',
|
'blocked' => 'boolean',
|
||||||
|
'deleted_at' => 'datetime',
|
||||||
];
|
];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-1
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\AuthController;
|
use App\Http\Controllers\AuthController;
|
||||||
use App\Http\Controllers\GameController;
|
use App\Http\Controllers\GameController;
|
||||||
use App\Http\Controllers\MatchController;
|
use App\Http\Controllers\MatchController;
|
||||||
@@ -65,10 +64,12 @@ Route::middleware('auth:sanctum')->group(function () {
|
|||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
|
// Delete old avatar if exists
|
||||||
if ($user->photo_avatar_filename) {
|
if ($user->photo_avatar_filename) {
|
||||||
\Storage::disk('public')->delete('photos_avatars/' . $user->photo_avatar_filename);
|
\Storage::disk('public')->delete('photos_avatars/' . $user->photo_avatar_filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store new avatar
|
||||||
$file = $request->file('avatar');
|
$file = $request->file('avatar');
|
||||||
$filename = str_pad($user->id, 5, '0', STR_PAD_LEFT) . '_' . \Str::random(10) . '.' . $file->extension();
|
$filename = str_pad($user->id, 5, '0', STR_PAD_LEFT) . '_' . \Str::random(10) . '.' . $file->extension();
|
||||||
$file->storeAs('photos_avatars', $filename, 'public');
|
$file->storeAs('photos_avatars', $filename, 'public');
|
||||||
@@ -79,6 +80,19 @@ Route::middleware('auth:sanctum')->group(function () {
|
|||||||
return response()->json($user);
|
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']);
|
Route::get('/{user}/matches', [UserController::class, 'getMatches']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,12 @@
|
|||||||
>
|
>
|
||||||
Change Password
|
Change Password
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Profile Information Tab -->
|
<!-- Profile Information Tab -->
|
||||||
@@ -275,11 +281,124 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- No Data State -->
|
<!-- 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="flex flex-col items-center gap-6">
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
<p>Redirecting to login...</p>
|
<p>Redirecting to login...</p>
|
||||||
@@ -324,6 +443,11 @@ const updatingPassword = ref(false)
|
|||||||
const passwordMessage = ref('')
|
const passwordMessage = ref('')
|
||||||
const passwordMessageType = ref('')
|
const passwordMessageType = ref('')
|
||||||
|
|
||||||
|
// Delete account
|
||||||
|
const deleteConfirmEmail = ref('')
|
||||||
|
const showDeleteConfirmation = ref(false)
|
||||||
|
const deletingAccount = ref(false)
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString) => {
|
||||||
const date = new Date(dateString)
|
const date = new Date(dateString)
|
||||||
return date.toLocaleDateString('en-US', {
|
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 () => {
|
onMounted(async () => {
|
||||||
if (!authStore.isLoggedIn) {
|
if (!authStore.isLoggedIn) {
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
|
|||||||
Reference in New Issue
Block a user