fix: add constraint actions to db structure

This commit adds several missing "cascade delete" actions to relationships on database tables. This effectively fixes errors while trying to delete user accounts because of pending child records.

Additionally, the observers for applications and vacancies were removed, since they are now obsolete.

The account deletion system was also refactored.
This commit is contained in:
Miguel Nogueira 2022-03-07 18:14:42 +00:00
parent ec23c05c5f
commit a4f41b8f8d
27 changed files with 353 additions and 463 deletions

View File

@ -22,6 +22,7 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Services\AccountSuspensionService;
use App\User; use App\User;
use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -65,13 +66,16 @@ class LoginController extends Controller
// We can't customise the error message, since that would imply overriding the login method, which is large. // We can't customise the error message, since that would imply overriding the login method, which is large.
// Also, the user should never know that they're banned. // Also, the user should never know that they're banned.
public function attemptLogin(Request $request) public function attemptLogin(Request $request): bool
{ {
$service = new AccountSuspensionService;
$user = User::where('email', $request->email)->first(); $user = User::where('email', $request->email)->first();
if ($user) { if ($user) {
$isBanned = $user->isBanned(); $isBanned = $service->isSuspended($user);
if ($isBanned) { $isLocked = $service->isLocked($user);
if ($isBanned || $isLocked) {
return false; return false;
} else { } else {
return $this->originalAttemptLogin($request); return $this->originalAttemptLogin($request);

View File

@ -33,6 +33,7 @@ use App\Http\Requests\UpdateUserRequest;
use App\Notifications\ChangedPassword; use App\Notifications\ChangedPassword;
use App\Notifications\EmailChanged; use App\Notifications\EmailChanged;
use App\Traits\DisablesFeatures; use App\Traits\DisablesFeatures;
use App\Traits\HandlesAccountDeletion;
use App\Traits\ReceivesAccountTokens; use App\Traits\ReceivesAccountTokens;
use App\User; use App\User;
use Google2FA; use Google2FA;
@ -44,7 +45,7 @@ use Spatie\Permission\Models\Role;
class UserController extends Controller class UserController extends Controller
{ {
use ReceivesAccountTokens; use HandlesAccountDeletion;
public function showUsers() public function showUsers()
{ {

View File

@ -0,0 +1,51 @@
<?php
namespace App\Jobs;
use App\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class ProcessAccountDelete implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The user to work with
*
* @var User
*/
protected User $user;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
// It shouldn't need the suspension service, because if it was dispatched, the account was already locked
Log::alert('[Worker] Processing account deletion request', [
'email' => $this->user->email
]);
$this->user->delete();
}
}

View File

@ -30,27 +30,23 @@ class UserAccountDeleteConfirmation extends Mailable
{ {
use Queueable, SerializesModels; use Queueable, SerializesModels;
public $deleteToken; public string
$approveLink,
$cancelLink,
$name,
$userID;
public $cancelToken;
public $originalIP;
public $name;
public $userID;
/** /**
* Create a new message instance. * Create a new message instance.
* *
* @return void * @return void
*/ */
public function __construct(User $user, array $tokens, string $originalIP) public function __construct(User $user, array $links)
{ {
$this->deleteToken = $tokens['delete']; $this->approveLink = $links['approveURL'];
$this->cancelToken = $tokens['cancel']; $this->cancelLink = $links['cancelURL'];
$this->originalIP = $originalIP;
$this->name = $user->name; $this->name = $user->name;
$this->userID = $user->id; $this->userID = $user->id;
} }
@ -62,6 +58,7 @@ class UserAccountDeleteConfirmation extends Mailable
*/ */
public function build() public function build()
{ {
return $this->view('mail.deleted-account'); return $this->subject('[ACTION REQUIRED] Please confirm account removal')
->view('mail.deleted-account');
} }
} }

View File

@ -1,109 +0,0 @@
<?php
/*
* Copyright © 2020 Miguel Nogueira
*
* This file is part of Raspberry Staff Manager.
*
* Raspberry Staff Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Raspberry Staff Manager is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Raspberry Staff Manager. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Observers;
use App\Application;
use Illuminate\Support\Facades\Log;
class ApplicationObserver
{
/**
* Handle the application "created" event.
*
* @param \App\Application $application
* @return void
*/
public function created(Application $application)
{
//
}
/**
* Handle the application "updated" event.
*
* @param \App\Application $application
* @return void
*/
public function updated(Application $application)
{
//
}
public function deleting(Application $application)
{
$application->response()->delete();
$votes = $application->votes;
foreach ($votes as $vote) {
Log::debug('Referential integrity cleanup: Deleting and detaching vote '.$vote->id);
$vote->application()->detach($application->id);
$vote->delete();
}
if (! is_null($application->appointment)) {
Log::debug('RIC: Deleting appointment!');
$application->appointment()->delete();
}
if (! $application->comments->isEmpty()) {
Log::debug('RIC: Deleting comments!');
foreach ($application->comments as $comment) {
$comment->delete();
}
}
// application can now be deleted
}
/**
* Handle the application "deleted" event.
*
* @param \App\Application $application
* @return void
*/
public function deleted(Application $application)
{
//
}
/**
* Handle the application "restored" event.
*
* @param \App\Application $application
* @return void
*/
public function restored(Application $application)
{
//
}
/**
* Handle the application "force deleted" event.
*
* @param \App\Application $application
* @return void
*/
public function forceDeleted(Application $application)
{
//
}
}

View File

@ -40,6 +40,7 @@ class UserObserver
*/ */
public function created(User $user) public function created(User $user)
{ {
// TODO: Make sure that the profile is created, throw an exception if otherwise, or try to recreate the profile.
Profile::create([ Profile::create([
'profileShortBio' => 'Write a one-liner about you here!', 'profileShortBio' => 'Write a one-liner about you here!',
@ -61,29 +62,6 @@ class UserObserver
// //
} }
public function deleting(User $user)
{
Log::debug("Deleting observer running");
if ($user->isForceDeleting()) {
$user->profile->delete();
Log::debug('Referential integrity cleanup: Deleted profile!');
$applications = $user->applications;
if (! $applications->isEmpty()) {
Log::debug('RIC: Now trying to delete applications and responses...');
foreach ($applications as $application) {
// code moved to Application observer, where it gets rid of attached elements individually
Log::debug('RIC: Deleting application '.$application->id);
$application->delete();
}
}
} else {
Log::debug('RIC: Not cleaning up soft deleted models!');
}
Log::debug('RIC: Cleanup done!');
}
/** /**
* Handle the user "deleted" event. * Handle the user "deleted" event.
* *

View File

@ -1,94 +0,0 @@
<?php
/*
* Copyright © 2020 Miguel Nogueira
*
* This file is part of Raspberry Staff Manager.
*
* Raspberry Staff Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Raspberry Staff Manager is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Raspberry Staff Manager. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Observers;
use App\Application;
use App\Vacancy;
use Illuminate\Support\Facades\Log;
class VacancyObserver
{
/**
* Handle the vacancy "created" event.
*
* @param \App\Vacancy $vacancy
* @return void
*/
public function created(Vacancy $vacancy)
{
//
}
/**
* Handle the vacancy "updated" event.
*
* @param \App\Vacancy $vacancy
* @return void
*/
public function updated(Vacancy $vacancy)
{
//
}
public function deleting(Vacancy $vacancy)
{
foreach(Application::with('response.vacancy')->get() as $app) {
if ($app->response->vacancy->id == $vacancy->id)
{
$app->delete();
}
}
}
/**
* Handle the vacancy "deleted" event.
*
* @param \App\Vacancy $vacancy
* @return void
*/
public function deleted(Vacancy $vacancy)
{
}
/**
* Handle the vacancy "restored" event.
*
* @param \App\Vacancy $vacancy
* @return void
*/
public function restored(Vacancy $vacancy)
{
//
}
/**
* Handle the vacancy "force deleted" event.
*
* @param \App\Vacancy $vacancy
* @return void
*/
public function forceDeleted(Vacancy $vacancy)
{
//
}
}

View File

@ -38,6 +38,7 @@ use Sentry;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
/** /**
* Register any application services. * Register any application services.
* *
@ -62,11 +63,6 @@ class AppServiceProvider extends ServiceProvider
Schema::defaultStringLength(191); Schema::defaultStringLength(191);
Paginator::useBootstrap(); Paginator::useBootstrap();
// Register observers
User::observe(UserObserver::class);
Application::observe(ApplicationObserver::class);
Vacancy::observe(VacancyObserver::class);
$https = ($this->app->environment() != 'local'); $https = ($this->app->environment() != 'local');
$collect = true; $collect = true;
@ -78,6 +74,10 @@ class AppServiceProvider extends ServiceProvider
$collect = false; $collect = false;
} }
// Initialize user observer
User::observe(UserObserver::class);
$this->app['request']->server->set('HTTPS', $https); $this->app['request']->server->set('HTTPS', $https);
View::share('shouldCollect', $collect); View::share('shouldCollect', $collect);

View File

@ -21,9 +21,15 @@
namespace App\Providers; namespace App\Providers;
use App\Application;
use App\Listeners\LogAuthenticationFailure; use App\Listeners\LogAuthenticationFailure;
use App\Listeners\LogAuthenticationSuccess; use App\Listeners\LogAuthenticationSuccess;
use App\Listeners\OnUserRegistration; use App\Listeners\OnUserRegistration;
use App\Observers\ApplicationObserver;
use App\Observers\UserObserver;
use App\Observers\VacancyObserver;
use App\User;
use App\Vacancy;
use Illuminate\Auth\Events\Failed; use Illuminate\Auth\Events\Failed;
use Illuminate\Auth\Events\Login; use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Events\Registered;
@ -74,7 +80,5 @@ class EventServiceProvider extends ServiceProvider
public function boot() public function boot()
{ {
parent::boot(); parent::boot();
//
} }
} }

View File

@ -62,6 +62,47 @@ class AccountSuspensionService
$user->bans->delete(); $user->bans->delete();
} }
/**
* Sets an administrative lock on a user account.
* Used to prevent logins after a deletion process is initiated, but may be used for
* other things where a suspension is not necessary/warranted, such as a security breach event.
* These locks cannot be overridden manually be administrators.
*
* @param User $user The account to lock
* @return bool
*/
public function lockAccount(User $user): bool
{
Log::alert('User account locked!', [
'email' => $user->email
]);
$user->administratively_locked = 1;
return $user->save();
}
/**
* Unlocks a user account. Reverse of lockAccount().
*
* @param User $user
* @return bool
*/
public function unlockAccount(User $user): bool
{
Log::alert('User account unlocked!', [
'email' => $user->email
]);
$user->administratively_locked = 0;
return $user->save();
}
/** /**
* Checks whether a user is suspended * Checks whether a user is suspended
* *
@ -73,6 +114,16 @@ class AccountSuspensionService
} }
/**
* Checks whether an account is locked
*
* @param User $user The user to check
* @return bool Whether the mentioned account is locked
*/
public function isLocked(User $user): bool {
return $user->administratively_locked == 1;
}
/** /**
* Takes a suspension directly and makes it permanent. * Takes a suspension directly and makes it permanent.
* *

View File

@ -0,0 +1,137 @@
<?php
/*
* Copyright © 2020 Miguel Nogueira
*
* This file is part of Raspberry Staff Manager.
*
* Raspberry Staff Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Raspberry Staff Manager is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Raspberry Staff Manager. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Traits;
use App\Http\Requests\UserDeleteRequest;
use App\Jobs\ProcessAccountDelete;
use App\Mail\UserAccountDeleteConfirmation;
use App\Services\AccountSuspensionService;
use App\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\URL;
trait HandlesAccountDeletion
{
/**
* Starts the user account deletion process.
*
* @param AccountSuspensionService $suspensionService
* @param UserDeleteRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function userDelete(AccountSuspensionService $suspensionService, UserDeleteRequest $request)
{
if (config('demo.is_enabled'))
{
return redirect()
->back()
->with('error', 'This feature is disabled');
}
$links = [
'approveURL' => URL::temporarySignedRoute(
'processDeleteConfirmation', now()->addDays(7), ['accountID' => $request->user()->id, 'action' => 'confirm']
),
'cancelURL' => URL::temporarySignedRoute(
'processDeleteConfirmation', now()->addDays(7), ['accountID' => $request->user()->id, 'action' => 'cancel']
)
];
Mail::to($request->user())
->send(new UserAccountDeleteConfirmation($request->user(), $links));
// Only locked accounts can be deleted
$suspensionService->lockAccount($request->user());
Auth::logout();
$request->session()->flash('success', __('Please check your email to finish deleting your account.'));
return redirect()->to('/');
}
/**
* Dispatches the correct jobs and events to delete the specified user account
*
* @param Request $request
* @param AccountSuspensionService $suspensionService
* @param $accountID
* @param $action
* @return \Illuminate\Http\RedirectResponse|void
*/
public function processDeleteConfirmation(Request $request, AccountSuspensionService $suspensionService, $accountID, $action)
{
if (config('demo.is_enabled') || !$request->hasValidSignature())
{
abort(403);
}
// It's almost impossible for this to fail, unless the model has already been deleted by someone else, because:
// The request URL can't be tampered with and the request can't be initiated without a valid account in the first place
$account = User::find($accountID);
if (!is_null($account))
{
if (!$suspensionService->isLocked($account)) {
abort(403);
}
Log::alert('Signed account deletion request received!', [
'user' => $account->name,
'email' => $account->name,
'created_at' => $account->created_at,
'updated_at' => $account->updated_at,
'deleted_at' => Carbon::now(),
'ipAddress' => $request->ip(),
'userAgent' => $request->userAgent(),
]);
if ($action == 'confirm') {
// dispatch event (for notifications) and job (for async processing)
ProcessAccountDelete::dispatch($account);
$request->session()->flash('success', __('Thank you for confirming. Your account will now be deleted shortly.'));
return redirect()
->to('/');
}
$suspensionService->unlockAccount($account);
$request->session()->flash('success', __('Account removal request cancelled. Your account has been unlocked and you can now sign in.'));
return redirect()
->route('login');
}
Log::error("Cannot delete account that doesn't exist!", [
'validSignature' => $request->hasValidSignature()
]);
abort(400);
}
}

View File

@ -1,62 +0,0 @@
<?php
/*
* Copyright © 2020 Miguel Nogueira
*
* This file is part of Raspberry Staff Manager.
*
* Raspberry Staff Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Raspberry Staff Manager is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Raspberry Staff Manager. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Traits;
use Illuminate\Support\Facades\Hash;
trait HandlesAccountTokens
{
public function generateAccountTokens()
{
$deleteToken = bin2hex(openssl_random_pseudo_bytes(32));
$cancelToken = bin2hex(openssl_random_pseudo_bytes(32));
$tokens = [
'delete' => Hash::make($deleteToken),
'cancel' => Hash::make($cancelToken),
];
$this->account_tokens = json_encode($tokens);
$this->save();
return [
'delete' => $deleteToken,
'cancel' => $cancelToken,
];
}
public function verifyAccountToken(string $token, string $type): bool
{
$tokens = json_decode($this->account_tokens);
if ($type == 'deleteToken') {
return Hash::check($token, $tokens->delete);
} elseif ($type == 'cancelToken') {
return Hash::check($token, $tokens->cancel);
}
return false;
}
}

View File

@ -1,113 +0,0 @@
<?php
/*
* Copyright © 2020 Miguel Nogueira
*
* This file is part of Raspberry Staff Manager.
*
* Raspberry Staff Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Raspberry Staff Manager is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Raspberry Staff Manager. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Traits;
use App\Http\Requests\UserDeleteRequest;
use App\Mail\UserAccountDeleteConfirmation;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
trait ReceivesAccountTokens
{
public function userDelete(UserDeleteRequest $request)
{
//Fixme: TEMPORARY, PLEASE REMOVE UNTIL FIXED OR DURING DEVELOPMENT
return redirect()
->back()
->with('error', 'This feature is disabled');
if (config('demo.is_enabled'))
{
return redirect()
->back()
->with('error', 'This feature is disabled');
}
// a little verbose
$user = User::find(Auth::user()->id);
$tokens = $user->generateAccountTokens();
Mail::to($user)->send(new UserAccountDeleteConfirmation($user, $tokens, $request->ip()));
$user->delete();
Auth::logout();
$request->session()->flash('success', __('Please check your email to finish deleting your account.'));
return redirect()->to('/');
}
public function processDeleteConfirmation(Request $request, $ID, $action, $token)
{
if (config('demo.is_enabled'))
{
return redirect()
->back()
->with('error', 'This feature is disabled');
}
// We can't rely on Laravel's route model injection, because it'll ignore soft-deleted models,
// so we have to use a special scope to find them ourselves.
$user = User::withTrashed()->findOrFail($ID);
$email = $user->email;
switch ($action) {
case 'confirm':
if ($user->verifyAccountToken($token, 'deleteToken')) {
Log::info('SECURITY: User deleted account!', [
'confirmDeleteToken' => $token,
'ipAddress' => $request->ip(),
'email' => $user->email,
]);
$user->forceDelete();
$request->session()->flash('success', __('Account permanently deleted. Thank you for using our service.'));
return redirect()->to('/');
}
break;
case 'cancel':
if ($user->verifyAccountToken($token, 'cancelToken')) {
$user->restore();
$request->session()->flash('success', __('Account deletion cancelled! You may now login.'));
return redirect()->to(route('login'));
}
break;
default:
abort(404, __('The page you were trying to access may not exist or may be expired.'));
}
}
}

View File

@ -21,6 +21,7 @@
namespace App; namespace App;
use App\Services\AccountSuspensionService;
use App\Traits\HandlesAccountTokens; use App\Traits\HandlesAccountTokens;
use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@ -31,7 +32,7 @@ use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable implements MustVerifyEmail class User extends Authenticatable implements MustVerifyEmail
{ {
use UserHasTeams, Notifiable, HasRoles, SoftDeletes, HandlesAccountTokens; use UserHasTeams, Notifiable, HasRoles;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@ -101,7 +102,15 @@ class User extends Authenticatable implements MustVerifyEmail
// UTILITY LOGIC // UTILITY LOGIC
public function isBanned() /**
* Checks if a user is banned.
*
* @deprecated This method is obsolete, as it has been replaced by the suspension service.
* @see AccountSuspensionService::isSuspended()
*
* @return bool Whether the user is banned
*/
public function isBanned(): bool
{ {
return ! $this->bans()->get()->isEmpty(); return ! $this->bans()->get()->isEmpty();
} }

View File

@ -46,7 +46,8 @@ class CreateProfilesTable extends Migration
$table->foreign('userID') $table->foreign('userID')
->references('id') ->references('id')
->on('users'); ->on('users')
->cascadeOnDelete();
}); });
} }

View File

@ -53,7 +53,8 @@ class CreateApplicationsTable extends Migration
$table->foreign('applicantUserID') $table->foreign('applicantUserID')
->references('id') ->references('id')
->on('users'); ->on('users')
->cascadeOnDelete();
}); });
} }

View File

@ -43,7 +43,8 @@ class CreateVotesTable extends Migration
$table->foreign('userID') $table->foreign('userID')
->references('id') ->references('id')
->on('users'); ->on('users')
->cascadeOnDelete();
}); });
} }

View File

@ -54,7 +54,8 @@ class CreateAppointmentsTable extends Migration
$table->foreign('applicationID') $table->foreign('applicationID')
->references('id') ->references('id')
->on('applications'); ->on('applications')
->cascadeOnDelete();
}); });
} }

View File

@ -41,7 +41,8 @@ class CreateResponsesTable extends Migration
// A better way would be to link responses directly to vacancies, that subsquently have a form // A better way would be to link responses directly to vacancies, that subsquently have a form
$table->foreign('responseFormID') $table->foreign('responseFormID')
->references('id') ->references('id')
->on('forms'); ->on('forms')
->cascadeOnDelete();
}); });
} }

View File

@ -38,8 +38,8 @@ class VotesHasApplication extends Migration
$table->bigInteger('application_id')->unsigned(); $table->bigInteger('application_id')->unsigned();
$table->timestamps(); $table->timestamps();
$table->foreign('vote_id')->references('id')->on('votes'); $table->foreign('vote_id')->references('id')->on('votes')->cascadeOnDelete();
$table->foreign('application_id')->references('id')->on('applications'); $table->foreign('application_id')->references('id')->on('applications')->cascadeOnDelete();
}); });
} }

View File

@ -43,7 +43,8 @@ class CreateBansTable extends Migration
$table->foreign('userID') $table->foreign('userID')
->references('id') ->references('id')
->on('users'); ->on('users')
->cascadeOnDelete();
}); });
} }

View File

@ -41,11 +41,13 @@ class CreateCommentsTable extends Migration
$table->foreign('authorID') $table->foreign('authorID')
->references('id') ->references('id')
->on('users'); ->on('users')
->cascadeOnDelete();
$table->foreign('applicationID') $table->foreign('applicationID')
->references('id') ->references('id')
->on('applications'); ->on('applications')
->cascadeOnDelete();
}); });
} }

View File

@ -27,11 +27,13 @@ class CreateAbsencesTable extends Migration
$table->foreign('requesterID') $table->foreign('requesterID')
->references('id') ->references('id')
->on('users'); ->on('users')
->onDelete('cascade');
$table->foreign('reviewer') $table->foreign('reviewer')
->references('id') ->references('id')
->on('users'); ->on('users')
->onDelete('set null');
}); });
} }

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('account_tokens');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->string('account_tokens')->after('password')->nullable();
});
}
};

View File

@ -48,13 +48,7 @@
<li>{{ __('Server logs of your visits, including IP addresses') }}</li> <li>{{ __('Server logs of your visits, including IP addresses') }}</li>
</ul> </ul>
<x-alert alert-type="danger"> <p>{{ __("Note: After you verify your identity, you'll receive an email with more information asking you to confirm this request.") }}</p>
<p class="text-bold"><i class="fas fa-exclamation-triangle"></i> {{ __('Feature temporarily unavailable') }}</p>
<p>This feature has been temporarily made unavailable while we work to fix underlying issues that are causing our backoffice to crash. We apologize for the inconvenience, and any account/data deletion requests should be forwarded to our data protection officer below.</p>
<p><i class="fas fa-user"></i> <a href="mailto:dpo@gamescluboficial.com.br?subject=GDPR%20Request%20-%20Games%20Club">dpo@gamescluboficial.com.br</a></p>
</x-alert>
<form id="deleteAccountForm" method="POST" action="{{ route('userDelete') }}"> <form id="deleteAccountForm" method="POST" action="{{ route('userDelete') }}">
@ -63,7 +57,7 @@
<div class="form-group"> <div class="form-group">
<label for="currentPassword">{{ __('Re-enter your password') }}</label> <label for="currentPassword">{{ __('Re-enter your password') }}</label>
<input disabled class="form-control" autocomplete="current-password" type="password" name="currentPassword" id="currentPassword" required> <input class="form-control" autocomplete="current-password" type="password" name="currentPassword" id="currentPassword" required>
<p class="text-muted text-sm"><i class="fas fa-info-circle"></i> {{ __('For your security, your password is always required for sensitive operations.') }} <a href="{{ route('password.request') }}">{{ __('Forgot your password?') }}</a></p> <p class="text-muted text-sm"><i class="fas fa-info-circle"></i> {{ __('For your security, your password is always required for sensitive operations.') }} <a href="{{ route('password.request') }}">{{ __('Forgot your password?') }}</a></p>
</div> </div>
@ -71,7 +65,7 @@
<div class="form-group mt-5"> <div class="form-group mt-5">
<label for="otp">{{ __('Two-factor authentication code') }}</label> <label for="otp">{{ __('Two-factor authentication code') }}</label>
<input disabled type="text" id="otp" name="otp" class="form-control"> <input type="text" id="otp" name="otp" class="form-control">
<p class="text-muted text-sm"><i class="fas fa-info-circle"></i> {{ __('You cannot recover lost 2FA secrets.') }}</p> <p class="text-muted text-sm"><i class="fas fa-info-circle"></i> {{ __('You cannot recover lost 2FA secrets.') }}</p>
</div> </div>
@ -81,7 +75,7 @@
<x-slot name="modalFooter"> <x-slot name="modalFooter">
<button {{ ($demoActive) ? 'disabled' : 'disabled' }} onclick="$('#deleteAccountForm').submit()" type="button" class="btn btn-warning"><i class="fas fa-exclamation-triangle"></i> {{ __('Continue') }}</button> <button {{ ($demoActive) ? 'disabled' : '' }} onclick="$('#deleteAccountForm').submit()" type="button" class="btn btn-warning"><i class="fas fa-exclamation-triangle"></i> {{ __('Continue') }}</button>
</x-slot> </x-slot>

View File

@ -3,7 +3,7 @@
<head> <head>
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Your account has been deleted</title> <title>Action required: please confirm whether you'd like to delete your account</title>
<style> <style>
/* ------------------------------------- /* -------------------------------------
INLINED WITH htmlemail.io/inline INLINED WITH htmlemail.io/inline
@ -95,7 +95,7 @@
</style> </style>
</head> </head>
<body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"> <body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Your account has just been deleted!</span> <span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Your account has just been locked</span>
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;"> <table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;">
<tr> <tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td> <td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td>
@ -111,23 +111,23 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;"> <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr> <tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;"> <td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Hi {{ $name }},</p> <p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Hi {{ $name }},</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Someone (hopefully you) has requested that your account at {{ config('app.name') }} be deleted.</p> <p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">We're sorry to see you go! Please let us know if there is any feedback you'd like to share, or if there's anything we can help you with.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">As a security measure, an email is always sent out to make sure you really want to delete your account.</p> <p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">To prevent any accidental loss of data, please confirm whether you really want to delete your account.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">This is the IP address the request was made from: {{ $originalIP }}.</p> <p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You have 7 days to make a decision, after which the links below will become invalid. You will not be able to sign in or use your account during this time period.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you don't do anything, your account will automatically be permanently deleted in 30 days, and will remain unaccessible for that time period.</p> <p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you don't make any decision after 7 days, your account will remain permanently locked; Locked accounts are automatically deleted after 30 days. You may, however, contact us after this time period to unlock your account if you have changed your mind.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Click one of the buttons below to make a decision.</p> <p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Do you really want to delete your account?</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;"> <table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tbody> <tbody>
<tr> <tr>
<td align="left" style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;"> <td align="left" style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;"> <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<tbody> <tbody>
<tr> <tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; background-color: #3498db; border-radius: 5px; text-align: center; margin-left: 5px;"> <a href="{{ route('processDeleteConfirmation', ['ID' => $userID, 'action' => 'confirm', 'token' => $deleteToken]) }}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #3498db; border: solid 1px #3498db; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #3498db;">Delete Account</a> </td> <td style="font-family: sans-serif; font-size: 14px; vertical-align: top; background-color: #3498db; border-radius: 5px; text-align: center; margin-left: 5px;"> <a href="{{ $cancelLink }}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #3498db; border: solid 1px #3498db; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #3498db;">No, unlock and keep my account</a> </td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; background-color: #3498db; border-radius: 5px; text-align: center; margin-left: 5px;"> <a href="{{ route('processDeleteConfirmation', ['ID' => $userID, 'action' => 'cancel', 'token' => $cancelToken]) }}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #3498db; border: solid 1px #3498db; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #3498db;">Cancel Deletion</a> </td> <td style="font-family: sans-serif; font-size: 14px; vertical-align: top; background-color: #3498db; border-radius: 5px; text-align: center; margin-left: 5px;"> <a href="{{ $approveLink }}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #3498db; border: solid 1px #3498db; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #3498db;">Yes, irreversibly delete my account</a> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -150,7 +150,7 @@
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;"> <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr> <tr>
<td class="content-block" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;"> <td class="content-block" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
<span class="apple-link" style="color: #999999; font-size: 12px; text-align: center;">Staff Manager</span> <span class="apple-link" style="color: #999999; font-size: 12px; text-align: center;">RBRecruiter | GC</span>
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -94,7 +94,7 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo
Route::post('/form/contact', [ContactController::class, 'create']) Route::post('/form/contact', [ContactController::class, 'create'])
->name('sendSubmission'); ->name('sendSubmission');
Route::get('/accounts/danger-zone/{ID}/{action}/{token}', [UserController::class, 'processDeleteConfirmation']) Route::get('/accounts/{accountID}/dg/process-delete/{action}', [UserController::class, 'processDeleteConfirmation'])
->name('processDeleteConfirmation'); ->name('processDeleteConfirmation');
Route::group(['middleware' => ['auth', 'forcelogout', 'passwordexpiration', '2fa', 'verified']], function () { Route::group(['middleware' => ['auth', 'forcelogout', 'passwordexpiration', '2fa', 'verified']], function () {