Merge pull request #13 from GamesClubOficial/patch-stability-upgrades

App stability patches
This commit is contained in:
Miguel Nogueira 2022-03-07 22:00:04 +00:00 committed by GitHub
commit 913911eaf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 794 additions and 3037 deletions

3
.gitignore vendored
View File

@ -1,4 +1,7 @@
/public/css/app.css
/public/css/mixed.css
/public/js/app.js
/public/js/app.js.LICENSE.txt
/node_modules
/public/hot
/public/storage

View File

@ -22,6 +22,7 @@
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Services\AccountSuspensionService;
use App\User;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
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.
// 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();
if ($user) {
$isBanned = $user->isBanned();
if ($isBanned) {
$isBanned = $service->isSuspended($user);
$isLocked = $service->isLocked($user);
if ($isBanned || $isLocked) {
return false;
} else {
return $this->originalAttemptLogin($request);

View File

@ -33,6 +33,7 @@ use App\Http\Requests\UpdateUserRequest;
use App\Notifications\ChangedPassword;
use App\Notifications\EmailChanged;
use App\Traits\DisablesFeatures;
use App\Traits\HandlesAccountDeletion;
use App\Traits\ReceivesAccountTokens;
use App\User;
use Google2FA;
@ -44,7 +45,7 @@ use Spatie\Permission\Models\Role;
class UserController extends Controller
{
use ReceivesAccountTokens;
use HandlesAccountDeletion;
public function showUsers()
{
@ -66,7 +67,7 @@ class UserController extends Controller
$matchingUsers = User::query()
->where('name', 'LIKE', "%{$searchTerm}%")
->orWhere('email', 'LIKE', "%{$searchTerm}%")
->get();
->paginate(6);
if (! $matchingUsers->isEmpty()) {
$request->session()->flash('success', __('There were :usersCount user(s) matching your search.', ['usersCount' => $matchingUsers->count()]));

View File

@ -0,0 +1,62 @@
<?php
namespace App\Jobs;
use App\Notifications\AccountDeleted;
use App\Notifications\UserDeletedAccount;
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;
use Illuminate\Support\Facades\Notification;
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()
{
Log::alert('[Worker] Processing account deletion request', [
'email' => $this->user->email
]);
$email = $this->user->email;
$name = $this->user->name;
if ($this->user->delete()) {
Notification::route('mail', [
$email => $name
])->notify(new AccountDeleted($name));
// Notify admins
Notification::send(User::role('admin')->get(), new UserDeletedAccount($email));
}
}
}

View File

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

View File

@ -0,0 +1,69 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class AccountDeleted extends Notification implements ShouldQueue
{
use Queueable;
public string $name;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct($name)
{
$this->name = $name;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
// Adjust to notify external user
return (new MailMessage)
->greeting('Hi ' . $this->name . ',')
->from(config('notification.sender.address'), config('notification.sender.name'))
->subject(config('app.name').' - account deleted permanently')
->line('Thank you for confirming your account deletion request. We\'re sorry to see you go!')
->line('Unless you sign up again, this is the last email you\'ll be receiving from us.')
->line('Please let us know if there\'s any feedback you\'d like to share. You can use the feedback widget located on the left-hand side of our website, or the chat widget located on the lower right corner.')
->line('See you around!')
->salutation('The team at ' . config('app.name'));
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class AccountLocked extends Notification implements ShouldQueue
{
use Queueable;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->from(config('notification.sender.address'), config('notification.sender.name'))
->subject(config('app.name').' - account locked')
->markdown('mail.account-locked', ['name' => $notifiable->name]);
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class AccountUnlocked extends Notification implements ShouldQueue
{
use Queueable;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->greeting('Hi ' . $notifiable->name . ',')
->from(config('notification.sender.address'), config('notification.sender.name'))
->subject(config('app.name').' - account unlocked')
->line('We wanted to let you know that your account at ' . config('app.name') . ' is now unlocked. This means the circumstances surrounding your account\'s standing are now resolved.')
->line('You can sign in and use the app normally again.')
->line('If there\'s anything we can help you with, don\'t hesitate to reach out.')
->action('Sign in', url(route('login')))
->salutation('The team at ' . config('app.name'));
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@ -47,12 +47,12 @@ class NewUser extends Notification implements ShouldQueue
$this->user = $user;
}
public function channels($notifiable)
public function channels()
{
return $this->chooseChannelsViaOptions();
}
public function optOut($notifiable)
public function optOut()
{
return Options::getOption('notify_new_user') != 1;
}
@ -69,9 +69,10 @@ class NewUser extends Notification implements ShouldQueue
->greeting('Hi ' . $notifiable->name . ',')
->from(config('notification.sender.address'), config('notification.sender.name'))
->subject(config('app.name').' - New user')
->line($this->user->name.' has just registered to our site.')
->line('You are receiving this email because you opted to receive new user notifications.')
->action('View profile', url(route('showSingleProfile', ['user' => $this->user->id])))
->line($this->user->name.' has created a new account.')
->line('This request came from the IP address ' . $this->user->originalIP . '.')
->line('You are receiving this email because you\'re a site admin, and the app is configured to send new user notifications.')
->action('View user', url(route('showSingleProfile', ['user' => $this->user->id])))
->salutation('The team at ' . config('app.name'));
}

View File

@ -70,7 +70,7 @@ class UserBanned extends Notification implements ShouldQueue
->greeting('Hi ' . $notifiable->name . ',')
->from(config('notification.sender.address'), config('notification.sender.name'))
->line('Hello, ')
->line('Moderators have just banned user '.$this->user->name.' for '.$this->ban->reason)
->line('Moderators have just suspended user '.$this->user->name.' for '.$this->ban->reason)
->line('This ban will remain in effect until '.$this->ban->bannedUntil.'.')
->action('View profile', url(route('showSingleProfile', ['user' => $this->user->id])))
->salutation('The team at ' . config('app.name'));

View File

@ -0,0 +1,70 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class UserDeletedAccount extends Notification implements ShouldQueue
{
use Queueable;
/**
* @var string The email belonging to the user who wiped their acct.
*/
public string $deletedEmail;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct($deletedEmail)
{
$this->deletedEmail = $deletedEmail;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->greeting('Hi ' . $notifiable->name . ',')
->from(config('notification.sender.address'), config('notification.sender.name'))
->subject(config('app.name').' - someone deleted their account')
->line("The user {$this->deletedEmail} has just deleted their account. You may wish to review the situation.")
->line('You are receiving this email because you\'re a site admin.')
->action('View current users', url(route('registeredPlayerList')))
->salutation('The team at ' . config('app.name'));
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

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

@ -29,7 +29,7 @@ class UserObserver
{
public function __construct()
{
Log::debug('User observer has been initialised and ready for use!');
//
}
/**
@ -40,6 +40,7 @@ class UserObserver
*/
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([
'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.
*

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

View File

@ -44,7 +44,9 @@ use App\TeamFile;
use App\User;
use App\Vacancy;
use App\Vote;
use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Notifications\Messages\MailMessage;
class AuthServiceProvider extends ServiceProvider
{
@ -77,6 +79,16 @@ class AuthServiceProvider extends ServiceProvider
public function boot()
{
$this->registerPolicies();
//
VerifyEmail::toMailUsing(function ($notifiable, $url) {
return (new MailMessage)
->greeting("Hi {$notifiable->name}! Welcome to " . config('app.name') . ".")
->line('To finish setting up your account, you must verify your email. This is to ensure only real users access our website.')
->line('If you didn\'t sign up for an account, you can safely ignore this email.')
->action('Verify account', $url)
->salutation('The team at ' . config('app.name'));
});
}
}

View File

@ -21,9 +21,15 @@
namespace App\Providers;
use App\Application;
use App\Listeners\LogAuthenticationFailure;
use App\Listeners\LogAuthenticationSuccess;
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\Login;
use Illuminate\Auth\Events\Registered;
@ -74,7 +80,5 @@ class EventServiceProvider extends ServiceProvider
public function boot()
{
parent::boot();
//
}
}

View File

@ -4,6 +4,8 @@
namespace App\Services;
use App\Ban;
use App\Notifications\AccountLocked;
use App\Notifications\AccountUnlocked;
use App\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
@ -62,6 +64,51 @@ class AccountSuspensionService
$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;
$user->notify(new AccountLocked);
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;
$user->notify(new AccountUnlocked);
return $user->save();
}
/**
* Checks whether a user is suspended
*
@ -73,6 +120,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.
*

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;
use App\Services\AccountSuspensionService;
use App\Traits\HandlesAccountTokens;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -31,7 +32,7 @@ use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable implements MustVerifyEmail
{
use UserHasTeams, Notifiable, HasRoles, SoftDeletes, HandlesAccountTokens;
use UserHasTeams, Notifiable, HasRoles;
/**
* The attributes that are mass assignable.
@ -101,7 +102,15 @@ class User extends Authenticatable implements MustVerifyEmail
// 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();
}

View File

@ -602,22 +602,6 @@ return [
],
],
[
'name' => 'BootstrapSwitch',
'active' => true,
'files' => [
[
'type' => 'js',
'asset' => false,
'location' => 'https://cdn.jsdelivr.net/gh/gitbrent/bootstrap4-toggle@3.6.1/js/bootstrap4-toggle.min.js'
],
[
'type' => 'css',
'asset' => false,
'location' => 'https://cdn.jsdelivr.net/gh/gitbrent/bootstrap4-toggle@3.6.1/css/bootstrap4-toggle.min.css'
]
]
],
[
'name' => 'BootstrapToggleButton',
'active' => true,

View File

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

View File

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

View File

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

View File

@ -54,7 +54,8 @@ class CreateAppointmentsTable extends Migration
$table->foreign('applicationID')
->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
$table->foreign('responseFormID')
->references('id')
->on('forms');
->on('forms')
->cascadeOnDelete();
});
}

View File

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

View File

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

View File

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

View File

@ -27,11 +27,13 @@ class CreateAbsencesTable extends Migration
$table->foreign('requesterID')
->references('id')
->on('users');
->on('users')
->onDelete('cascade');
$table->foreign('reviewer')
->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();
});
}
};

2649
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@
"laravel-mix": "^6.0.43",
"lodash": "^4.17.13",
"popper.js": "^1.12",
"resolve-url-loader": "^2.3.1",
"resolve-url-loader": "^5.0.0",
"sass": "^1.15.2",
"sass-loader": "^8.0.0",
"vue": "^2.5.17",

8
public/css/app.css vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

View File

@ -157,7 +157,7 @@
<img class="d-inline mb-4" src="{{ asset('img/403.svg') }}" width="350px" alt="403 illustration">
<h1>{{ __('403 - Forbidden') }}</h1>
<p>{{ __('Hey there :accountName! It looks like you don\'t have permission to access this resource. Believe this is a mistake? Contact us and we\'ll sort it out!', ['accountName' => Auth::user()->name]) }}</p>
<p>{{ __('Hey there! It looks like you don\'t have permission to access this resource. This may be because you don\'t have the appropriate roles, or because you\'ve been suspended. Believe this is a mistake? Contact us and we\'ll sort it out!') }}</p>
@break;
@case(503)

View File

@ -48,13 +48,7 @@
<li>{{ __('Server logs of your visits, including IP addresses') }}</li>
</ul>
<x-alert alert-type="danger">
<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>
<p>{{ __("Note: After you verify your identity, you'll receive an email with more information asking you to confirm this request.") }}</p>
<form id="deleteAccountForm" method="POST" action="{{ route('userDelete') }}">
@ -63,7 +57,7 @@
<div class="form-group">
<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>
</div>
@ -71,7 +65,7 @@
<div class="form-group mt-5">
<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>
</div>
@ -81,7 +75,7 @@
<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>

View File

@ -0,0 +1,18 @@
@component('mail::message')
# Hi {{ $name }},
We wanted to let you know that your account at {{ config('app.name') }} has been locked due to security concerns. Don't worry! You haven't been suspended! We lock accounts for a number of reasons, including, but not limited to:
- Suspicious activity was detected;
- You failed to activate 2FA within the required timeframe;
- Your password was detected on a 3rd party security breach;
- You started an account deletion request;
- Your password expired.
Please note that your account may be locked for reasons other than those listed above; If you think this was an error, please let us know, but keep in mind that this is an automated process and we can't manually unlock accounts. You will not be able to sign in or use the app while your account is locked.
Usually, you will receive another email with more information regarding your specific circumstances.
Thank you,<br>
The team at {{ config('app.name') }}
@endcomponent

View File

@ -3,7 +3,7 @@
<head>
<meta name="viewport" content="width=device-width">
<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>
/* -------------------------------------
INLINED WITH htmlemail.io/inline
@ -95,7 +95,7 @@
</style>
</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%;">
<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;">
<tr>
<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%;">
<tr>
<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;">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;">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;">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;">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;">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;">Hi {{ $name }},</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;">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;">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 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;">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>
<tr>
<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;">
<tbody>
<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="{{ 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="{{ $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="{{ $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>
</tbody>
</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%;">
<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;">
<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>
</tr>
</table>
@ -164,4 +164,4 @@
</tr>
</table>
</body>
</html>
</html>

View File

@ -94,7 +94,7 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo
Route::post('/form/contact', [ContactController::class, 'create'])
->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');
Route::group(['middleware' => ['auth', 'forcelogout', 'passwordexpiration', '2fa', 'verified']], function () {