Signed-off-by: miguel456 <me@nogueira.codes>
This commit is contained in:
2022-10-24 01:01:10 +01:00
parent 614410e7b7
commit 0bc6c20a6d
166 changed files with 4250 additions and 1833 deletions

View File

@@ -6,7 +6,9 @@ use App\Absence;
use App\Exceptions\AbsenceNotActionableException;
use App\Http\Requests\StoreAbsenceRequest;
use App\Http\Requests\UpdateAbsenceRequest;
use App\Services\AbsenceService;
use App\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Carbon;
@@ -15,28 +17,12 @@ use Illuminate\Support\Facades\Auth;
class AbsenceController extends Controller
{
/**
* Determines whether someone already has an active leave of absence request
*
* @param User $user The user to check
* @return bool Their status
*/
private function hasActiveRequest(Authenticatable $user): bool {
private AbsenceService $absenceService;
$absences = Absence::where('requesterID', $user->id)->get();
public function __construct (AbsenceService $absenceService) {
foreach ($absences as $absence) {
$this->absenceService = $absenceService;
// Or we could adjust the query (using a model scope) to only return valid absences;
// If there are any, refuse to store more, but this approach also works
// A model scope that only returns cancelled, declined and ended absences could also be implemented for future use
if (in_array($absence->getRawOriginal('status'), ['PENDING', 'APPROVED']))
{
return true;
}
}
return false;
}
/**
@@ -57,7 +43,7 @@ class AbsenceController extends Controller
* Display a listing of absences belonging to the current user.
*
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws AuthorizationException
*/
public function showUserAbsences()
{
@@ -76,14 +62,14 @@ class AbsenceController extends Controller
/**
* Show the form for creating a new absence request.
*
* @return \Illuminate\Http\Response
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function create()
{
$this->authorize('create', Absence::class);
return view('dashboard.absences.create')
->with('activeRequest', $this->hasActiveRequest(Auth::user()));
->with('activeRequest', $this->absenceService->hasActiveRequest(Auth::user()));
}
/**
@@ -96,21 +82,13 @@ class AbsenceController extends Controller
{
$this->authorize('create', Absence::class);
if ($this->hasActiveRequest(Auth::user())) {
if ($this->absenceService->hasActiveRequest(Auth::user())) {
return redirect()
->back()
->with('error', __('You already have an active request. Cancel it or let it expire first.'));
}
$absence = Absence::create([
'requesterID' => Auth::user()->id,
'start' => $request->start_date,
'predicted_end' => $request->predicted_end,
'available_assist' => $request->available_assist == "on",
'reason' => $request->reason,
'status' => 'PENDING',
]);
$absence = $this->absenceService->createAbsence(Auth::user(), $request);
return redirect()
->to(route('absences.show', ['absence' => $absence->id]))
@@ -120,7 +98,8 @@ class AbsenceController extends Controller
/**
* Display the specified absence request.
*
* @param \App\Absence $absence
* @param \App\Absence $absence
* @throws AuthorizationException
*/
public function show(Absence $absence)
{
@@ -138,7 +117,7 @@ class AbsenceController extends Controller
*
* @param Absence $absence
* @return RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws AuthorizationException
*/
public function approveAbsence(Absence $absence): RedirectResponse
{
@@ -146,7 +125,7 @@ class AbsenceController extends Controller
try
{
$absence->setApproved();
$this->absenceService->approveAbsence($absence);
}
catch (AbsenceNotActionableException $notActionableException)
{
@@ -166,7 +145,7 @@ class AbsenceController extends Controller
*
* @param Absence $absence
* @return RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws AuthorizationException
*/
public function declineAbsence(Absence $absence): RedirectResponse
{
@@ -174,7 +153,7 @@ class AbsenceController extends Controller
try
{
$absence->setDeclined();
$this->absenceService->declineAbsence($absence);
} catch (AbsenceNotActionableException $notActionableException)
{
return redirect()
@@ -193,7 +172,7 @@ class AbsenceController extends Controller
*
* @param Absence $absence
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws AuthorizationException
*/
public function cancelAbsence(Absence $absence): \Illuminate\Http\RedirectResponse
{
@@ -201,7 +180,7 @@ class AbsenceController extends Controller
try
{
$absence->setCancelled();
$this->absenceService->cancelAbsence($absence);
}
catch (AbsenceNotActionableException $notActionableException)
{
@@ -225,7 +204,7 @@ class AbsenceController extends Controller
{
$this->authorize('delete', $absence);
if ($absence->delete()) {
if ($this->absenceService->removeAbsence($absence)) {
return redirect()
->to(route('absences.index'))
->with('success', __('Absence request deleted.'));

View File

@@ -23,11 +23,15 @@ namespace App\Http\Controllers;
use App\Application;
use App\Exceptions\ApplicationNotFoundException;
use App\Exceptions\DiscordAccountRequiredException;
use App\Exceptions\IncompatibleAgeException;
use App\Exceptions\IncompleteApplicationException;
use App\Exceptions\InvalidAgeException;
use App\Exceptions\UnavailableApplicationException;
use App\Exceptions\VacancyNotFoundException;
use App\Facades\IP;
use App\Services\ApplicationService;
use App\Vacancy;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -82,6 +86,13 @@ class ApplicationController extends Controller
}
public function discordApply(Request $request, $vacancySlug) {
$request->session()->put('discordApplicationRedirectedSlug', $vacancySlug);
return redirect(route('discordRedirect'));
}
public function renderApplicationForm($vacancySlug)
{
try {
@@ -91,25 +102,47 @@ class ApplicationController extends Controller
return redirect()
->back()
->with('error', $ex->getMessage());
} catch (DiscordAccountRequiredException $e) {
\Log::info('Redirecting user: ' . $e->getMessage(), [
'user' => Auth::user()->email
]);
request()->session()->put('discordApplicationRedirectedSlug', $vacancySlug);
return redirect(route('discordRedirect'));
} catch (IncompatibleAgeException $e) {
return redirect()
->to(route('dashboard'))
->with('error', $e->getMessage());
} catch (InvalidAgeException $e) {
return view('dashboard.application-rendering.add-age');
}
}
public function saveApplicationAnswers(Request $request, $vacancySlug)
{
try {
if (Auth::user()->isEligible()) {
try {
$this->applicationService->fillForm(Auth::user(), $request->all(), $vacancySlug);
$this->applicationService->fillForm(Auth::user(), $request->all(), $vacancySlug);
} catch (VacancyNotFoundException | IncompleteApplicationException | UnavailableApplicationException $e) {
} catch (VacancyNotFoundException | IncompleteApplicationException | UnavailableApplicationException $e) {
return redirect()
->back()
->with('error', $e->getMessage());
}
return redirect()
->back()
->with('error', $e->getMessage());
->to(route('showUserApps'))
->with('success', __('Thank you! Your application has been processed and our team will get to it shortly.'));
}
return redirect()
->to(route('showUserApps'))
->with('success', __('Thank you! Your application has been processed and our team will get to it shortly.'));
->with('error', __('Your account is not eligible to submit a new application.'));
}
public function updateApplicationStatus(Request $request, Application $application, $newStatus)

View File

@@ -0,0 +1,101 @@
<?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\Http\Controllers\Auth;
use App\Facades\Discord;
use App\Facades\Options;
use App\Http\Controllers\Controller;
use App\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;
use Laravel\Socialite\Two\InvalidStateException;
class DiscordController extends Controller
{
public function discordRedirect() {
return Socialite::driver('discord')
->scopes(['email', 'guilds.join', 'guilds.members.read', 'guilds'])
->redirect();
}
public function discordCallback() {
try {
$discordUser = Socialite::driver('discord')->user();
} catch (InvalidStateException $stateException) {
Log::warning('Invalid state for social authentication: ', [
'message' => $stateException->getMessage(),
'ua' => request()->userAgent(),
'ip' => request()->ip()
]);
return redirect(route('discordRedirect'));
}
$appUser = User::where('email', $discordUser->getEmail())->first();
if ($appUser) {
$appUser->discord_token = $discordUser->token;
$appUser->discord_refresh_token = $discordUser->refreshToken;
$appUser->discord_user_id = $discordUser->getId();
$appUser->discord_pfp = $discordUser->getAvatar();
$appUser->save();
Auth::login($appUser, true);
} else {
$oAuthUser = User::create([
'uuid' => null,
'name' => $discordUser->getName(),
'email' => $discordUser->getEmail(),
'email_verified_at' => now(), // verify the account since it came from a trusted provider
'username' => $discordUser->getNickname(),
'currentIp' => \request()->ip(),
'registrationIp' => request()->ip(),
'discord_user_id' => $discordUser->getId(),
'discord_pfp' => $discordUser->getAvatar(),
'discord_token' => $discordUser->token,
'discord_refresh_token' => $discordUser->refreshToken
]);
$oAuthUser->assignRole('user');
Auth::login($oAuthUser, true);
}
if (session()->has('discordApplicationRedirectedSlug')) {
return redirect(route('renderApplicationForm', ['vacancySlug' => session()->pull('discordApplicationRedirectedSlug')]));
}
return redirect()
->route('dashboard');
}
}

View File

@@ -26,8 +26,11 @@ use App\Services\AccountSuspensionService;
use App\User;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;
use App\Facades\IP;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;
class LoginController extends Controller
@@ -76,6 +79,14 @@ class LoginController extends Controller
$isLocked = $service->isLocked($user);
if ($isBanned || $isLocked) {
Log::alert('Restricted user attempting to login.', [
'ip' => $request->ip(),
'email' => $user->email,
'isBanned' => $isBanned,
'isLocked' => $isLocked
]);
return false;
} else {
return $this->originalAttemptLogin($request);
@@ -94,17 +105,11 @@ class LoginController extends Controller
'prev' => $user->originalIP,
'new' => $request->ip()
]);
$user->originalIP = $request->ip();
$user->currentIp = $request->ip();
$user->save();
}
}
}
public function discordRedirect() {
return Socialite::driver('discord')->redirect();
}
public function discordCallback() {
// TODO;
}
}

View File

@@ -23,11 +23,13 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Profile;
use App\Services\AccountSuspensionService;
use App\User;
use App\Facades\Options;
use App\Facades\IP;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
class RegisterController extends Controller
@@ -62,19 +64,6 @@ class RegisterController extends Controller
$this->middleware('guest');
}
public function showRegistrationForm()
{
$users = User::where('originalIP', \request()->ip())->get();
foreach ($users as $user) {
if ($user && $user->isBanned()) {
abort(403, 'You do not have permission to access this page.');
}
}
return view('auth.register');
}
/**
* Get a validator for an incoming registration request.
*
@@ -106,9 +95,14 @@ class RegisterController extends Controller
'uuid' => (Options::getOption('requireGameLicense') && Options::getOption('currentGame') == 'MINECRAFT') ? ['required', 'string', 'unique:users', 'min:32', 'max:32'] : ['nullable', 'string'],
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'dob' => ['required', 'string', 'date_format:Y-m-d', 'before:-13 years'],
'acceptTerms' => ['required', 'accepted'],
'password' => $password,
], [
'uuid.required' => 'Please enter a valid (and Premium) Minecraft username! We do not support cracked users.',
'dob.before' => __('You must be 13 years of age or older in order to sign up for an account.'),
'dob.required' => __('Please enter your date of birth.'),
'uuid.required' => __('Please enter a valid (and Premium) Minecraft username! We do not support cracked users.'),
'acceptTerms.required' => __('Please accept the Community Guidelines, Terms of Service and Privacy Policy to continue.')
]);
}
@@ -120,12 +114,16 @@ class RegisterController extends Controller
*/
protected function create(array $data)
{
$ip = IP::shouldCollect() ? request()->ip() : '0.0.0.0';
$user = User::create([
'uuid' => $data['uuid'] ?? "disabled",
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
'originalIP' => IP::shouldCollect() ? request()->ip() : '0.0.0.0',
'registrationIp' => $ip,
'currentIp' => $ip,
'dob' => $data['dob']
]);
$user->assignRole('user');

View File

@@ -1,88 +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\Http\Controllers;
use App\Ban;
use App\Events\UserBannedEvent;
use App\Http\Requests\BanUserRequest;
use App\Services\AccountSuspensionService;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class BanController extends Controller
{
protected $suspensionService;
public function __construct(AccountSuspensionService $suspensionService)
{
// Inject the service via DI
$this->suspensionService = $suspensionService;
}
public function insert(BanUserRequest $request, User $user)
{
if (config('demo.is_enabled')) {
return redirect()
->back()
->with('error', __('This feature is disabled'));
}
$this->authorize('create', [Ban::class, $user]);
if (!$this->suspensionService->isSuspended($user)) {
$this->suspensionService->suspend($request->reason, $request->duration, $user, $request->suspensionType);
$request->session()->flash('success', __('Account suspended.'));
} else {
$request->session()->flash('error', __('Account already suspended!'));
}
return redirect()->back();
}
public function delete(Request $request, User $user)
{
if (config('demo.is_enabled')) {
return redirect()
->back()
->with('error', __('This feature is disabled'));
}
$this->authorize('delete', $user->bans);
if ($this->suspensionService->isSuspended($user)) {
$this->suspensionService->unsuspend($user);
$request->session()->flash('success', __('Account unsuspended successfully!'));
} else {
$request->session()->flash('error', __('This account isn\'t suspended!'));
}
return redirect()->back();
}
}

View File

@@ -1,64 +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\Http\Controllers;
use App\Exceptions\FailedCaptchaException;
use App\Http\Requests\HomeContactRequest;
use App\Notifications\NewContact;
use App\Services\ContactService;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class ContactController extends Controller
{
protected $users;
private $contactService;
public function __construct(User $users, ContactService $contactService)
{
$this->contactService = $contactService;
$this->users = $users;
}
public function create(HomeContactRequest $request)
{
try {
$email = $request->email;
$msg = $request->msg;
$challenge = $request->input('captcha');
$this->contactService->sendMessage($request->ip(), $msg, $email, $challenge);
return redirect()
->back()
->with('success',__('Message sent successfully! We usually respond within 48 hours.'));
} catch (FailedCaptchaException $ex) {
return redirect()
->back()
->with('error', $ex->getMessage());
}
}
}

View File

@@ -24,10 +24,12 @@ namespace App\Http\Controllers;
use App\Application;
use App\Events\ApplicationApprovedEvent;
use App\Events\ApplicationDeniedEvent;
use App\Services\AbsenceService;
use App\Services\AccountSuspensionService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class DevToolsController extends Controller
{
@@ -105,4 +107,16 @@ class DevToolsController extends Controller
->with('error', __('There were no expired suspensions (or no suspensions at all) to purge.'));
}
public function endAbsencesNow(AbsenceService $service)
{
$this->singleAuthorise();
$service->endExpired();
Log::alert('(absence cleaner) Forcefully started absence expiration check!');
return redirect()
->back()
->with('success', 'Cleaned up expired absences.');
}
}

View File

@@ -41,11 +41,4 @@ class HomeController extends Controller
return view('home')
->with('positions', $positions);
}
public function pageGiveaway()
{
return view('giveaway');
}
}

View File

@@ -21,8 +21,12 @@
namespace App\Http\Controllers;
use App\Exceptions\ProfileAlreadyExistsException;
use App\Exceptions\ProfileCreationFailedException;
use App\Exceptions\ProfileNotFoundException;
use App\Facades\IP;
use App\Http\Requests\ProfileSave;
use App\Services\AccountSuspensionService;
use App\Services\ProfileService;
use App\User;
use Carbon\Carbon;
@@ -32,18 +36,12 @@ use Spatie\Permission\Models\Role;
class ProfileController extends Controller
{
private $profileService;
private ProfileService $profileService;
public function __construct(ProfileService $profileService) {
$this->profileService = $profileService;
}
public function index()
{
return view('dashboard.user.directory')
->with('users', User::with('profile', 'bans')->paginate(9));
}
public function showProfile()
{
// TODO: Come up with cleaner social media solution, e.g. social media object
@@ -60,26 +58,23 @@ class ProfileController extends Controller
]);
}
public function showSingleProfile(User $user)
public function showSingleProfile(AccountSuspensionService $accountSuspensionService, User $user)
{
if (is_null($user->profile)) {
return redirect()
->back()
->with('error', "This user doesn't have a profile.");
}
$socialMediaProfiles = json_decode($user->profile->socialLinks, true);
$createdDate = Carbon::parse($user->created_at);
$systemRoles = Role::all()->pluck('name')->all();
$userRoles = $user->roles->pluck('name')->all();
$roleList = [];
foreach ($systemRoles as $role) {
if (in_array($role, $userRoles)) {
$roleList[$role] = true;
} else {
$roleList[$role] = false;
}
}
$suspensionInfo = null;
if ($user->isBanned())
if ($accountSuspensionService->isSuspended($user))
{
$suspensionInfo = [
@@ -98,8 +93,7 @@ class ProfileController extends Controller
'insta' => $socialMediaProfiles['links']['insta'] ?? 'UpdateMe',
'discord' => $socialMediaProfiles['links']['discord'] ?? 'UpdateMe#12345',
'since' => $createdDate->englishMonth.' '.$createdDate->year,
'ipInfo' => IP::lookup($user->originalIP),
'roles' => $roleList,
'ipInfo' => IP::lookup($user->currentIp),
'suspensionInfo' => $suspensionInfo
]);
} else {
@@ -114,4 +108,44 @@ class ProfileController extends Controller
->back()
->with('success', __('Profile updated.'));
}
public function createProfile(Request $request)
{
try {
$this->profileService->createProfile($request->user());
} catch (\Exception $e) {
return redirect()
->back()
->with('error', $e->getMessage());
}
return redirect()
->back()
->with('success', __('Your profile has been created.'));
}
public function deleteProfile(Request $request)
{
try {
$this->profileService->deleteProfile($request->user());
} catch (ProfileNotFoundException $e) {
return redirect()
->back()
->with('error', $e->getMessage());
}
return redirect()
->back()
->with('success', __('Profile deleted successfully.'));
}
}

View File

@@ -22,22 +22,36 @@
namespace App\Http\Controllers;
use App\Ban;
use App\Facades\IP;
use App\Facades\Options;
use App\Http\Requests\Add2FASecretRequest;
use App\Http\Requests\AddDobRequest;
use App\Http\Requests\BanUserRequest;
use App\Http\Requests\ChangeEmailRequest;
use App\Http\Requests\ChangePasswordRequest;
use App\Http\Requests\DeleteUserRequest;
use App\Http\Requests\FlushSessionsRequest;
use App\Http\Requests\Remove2FASecretRequest;
use App\Http\Requests\Reset2FASecretRequest;
use App\Http\Requests\SearchPlayerRequest;
use App\Http\Requests\SetNewPasswordRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Notifications\ChangedPassword;
use App\Notifications\EmailChanged;
use App\Notifications\PasswordAdminResetNotification;
use App\Notifications\TwoFactorResetNotification;
use App\Services\AccountSuspensionService;
use App\Services\DiscordService;
use App\Traits\DisablesFeatures;
use App\Traits\HandlesAccountDeletion;
use App\Traits\ReceivesAccountTokens;
use App\User;
use Google2FA;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Redirector;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
@@ -45,13 +59,20 @@ use Spatie\Permission\Models\Role;
class UserController extends Controller
{
use HandlesAccountDeletion;
use HandlesAccountDeletion, DisablesFeatures;
/**
* Shows list of users
*
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function showUsers()
{
$this->authorize('viewPlayers', User::class);
return view('dashboard.administration.players')
return view('dashboard.administration.users')
->with([
'users' => User::with('roles')->paginate('6'),
'numUsers' => count(User::all()),
@@ -59,6 +80,15 @@ class UserController extends Controller
]);
}
/**
* Searches for a player with the given search query.
*
* @deprecated Until Algolia implementation
* @param SearchPlayerRequest $request
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function showPlayersLike(SearchPlayerRequest $request)
{
$this->authorize('viewPlayers', User::class);
@@ -72,7 +102,7 @@ class UserController extends Controller
if (! $matchingUsers->isEmpty()) {
$request->session()->flash('success', __('There were :usersCount user(s) matching your search.', ['usersCount' => $matchingUsers->count()]));
return view('dashboard.administration.players')
return view('dashboard.administration.users')
->with([
'users' => $matchingUsers,
'numUsers' => count(User::all()),
@@ -85,6 +115,16 @@ class UserController extends Controller
}
}
/**
* Shows the user account's settings page
*
* @param Request $request
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
*/
public function showAccount(Request $request)
{
$QRCode = null;
@@ -109,6 +149,58 @@ class UserController extends Controller
->with('twofaQRCode', $QRCode);
}
/**
* Show account management screen
*
* @param AccountSuspensionService $suspensionService
* @param Request $request
* @param User $user
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function showAcocuntManagement(AccountSuspensionService $suspensionService, Request $request, User $user)
{
$this->authorize('adminEdit', $user);
$systemRoles = Role::all()->pluck('name')->all();
$userRoles = $user->roles->pluck('name')->all();
$roleList = [];
foreach ($systemRoles as $role) {
if (in_array($role, $userRoles)) {
$roleList[$role] = true;
} else {
$roleList[$role] = false;
}
}
return view('dashboard.user.manage')
->with([
'user' => $user,
'roles' => $roleList,
'isVerified' => $user->isVerified(),
'isLocked' => $suspensionService->isLocked($user),
'isSuspended' => $suspensionService->isSuspended($user),
'hasDiscord' => $user->hasDiscordConnection(),
'hasPassword' => $user->hasPassword(),
'requireLicense' => Options::getOption('requireGameLicense'),
'suspensionReason' => $suspensionService->getSuspensionReason($user),
'suspensionDuration' => $suspensionService->getSuspensionDuration($user),
'has2FA' => $user->has2FA(),
'applications' => $user->applications()->get()
]);
}
/**
* Log out other sessions for the current user
*
* @param FlushSessionsRequest $request
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\AuthenticationException
*/
public function flushSessions(FlushSessionsRequest $request)
{
// TODO: Move all log calls to a listener, which binds to an event fired by each significant event, such as this one
@@ -127,6 +219,14 @@ class UserController extends Controller
return redirect()->back();
}
/**
* Change the current user's password
*
* @param ChangePasswordRequest $request
* @return \Illuminate\Http\RedirectResponse|void
*/
public function changePassword(ChangePasswordRequest $request)
{
if (config('demo.is_enabled')) {
@@ -155,13 +255,80 @@ class UserController extends Controller
}
}
/**
* Sets a new password for the user.
*
* @param SetNewPasswordRequest $request
* @return Application|RedirectResponse|Redirector
*/
public function setPassword(SetNewPasswordRequest $request) {
if (!Auth::user()->hasPassword()) {
Auth::user()->password = Hash::make($request->newpass);
Auth::user()->save();
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect(route('login'));
}
return redirect()
->back()
->with('error', __('Your account already has a password.'));
}
/**
* Sets a user's password and removes their discord information from storage
*
* @param User $user
* @param SetNewPasswordRequest $request
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function unlinkDiscordAccount(Request $request, DiscordService $discordService)
{
if ($request->user()->hasPassword()) {
try {
$discordService->revokeAccountTokens(Auth::user());
Log::warning('Revoking social account tokens, user initiated', [
'user' => Auth::user()->email
]);
} catch (RequestException $requestException) {
if ($requestException->getCode() == 401) {
return redirect(route('discordRedirect'));
}
Log::error('Error while trying to revoke Discord credentials', [$requestException->getMessage()]);
return redirect()
->back()
->with('error', __('An unknown error ocurred. Please try again later.'));
}
$request->session()->flash('success', __('Discord account unlinked successfully. Link it again by re-authorizing the app with the same account in the login screen, or through your account settings.'));
return redirect()->back();
}
return redirect()
->back()
->with('error', __('Please set a password for your account first before trying to unlink Discord.'));
}
/**
* Change the current user's email address
*
* @param ChangeEmailRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function changeEmail(ChangeEmailRequest $request)
{
if (config('demo.is_enabled')) {
return redirect()
->back()
->with('error', __('This feature is disabled'));
}
$this->disable();
$user = User::find(Auth::user()->id);
@@ -184,14 +351,69 @@ class UserController extends Controller
return redirect()->back();
}
public function delete(DeleteUserRequest $request, User $user)
{
if (config('demo.is_enabled')) {
/**
* Removes the user's password and notifies them.
*
* @param User $user The user to remove the password for
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function forcePasswordReset(User $user) {
$this->authorize('adminEdit', $user);
if ($user->hasPassword()) {
$user->notify(new PasswordAdminResetNotification());
$user->password = null;
$user->save();
Log::alert("Removed account password", [
'target' => $user,
'actor' => Auth::user()
]);
return redirect()
->back()
->with('error', _('This feature is disabled'));
->with('success', __('Account password removed.'));
}
return redirect()
->back()
->with('error', __('This user doesn\'t have a password to reset.'));
}
/**
* Adds a user's date of birth if they don't have one.
*
* @param AddDobRequest $request
* @return RedirectResponse
*/
public function addDob(AddDobRequest $request) {
Auth::user()->dob = $request->dob;
Auth::user()->save();
return redirect()
->back();
}
/**
* Delete the given user's account
*
* @param DeleteUserRequest $request
* @param User $user
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function delete(DeleteUserRequest $request, User $user)
{
$this->disable();
$this->authorize('delete', $user);
if ($request->confirmPrompt == 'DELETE ACCOUNT') {
@@ -204,14 +426,19 @@ class UserController extends Controller
return redirect()->route('registeredPlayerList');
}
/**
* Update a given user's details
*
* @param UpdateUserRequest $request
* @param User $user
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(UpdateUserRequest $request, User $user)
{
if (config('demo.is_enabled')) {
return redirect()
->back()
->with('error', __('This feature is disabled'));
}
$this->authorize('adminEdit', $user);
$this->disable();
// Mass update would not be possible here without extra code, making route model binding useless
$user->email = $request->email;
@@ -243,6 +470,16 @@ class UserController extends Controller
return redirect()->back();
}
/**
* Generate and add a 2FA secret for the current user
*
* @param Add2FASecretRequest $request
* @return \Illuminate\Http\RedirectResponse
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
*/
public function add2FASecret(Add2FASecretRequest $request)
{
if (config('demo.is_enabled')) {
@@ -285,6 +522,13 @@ class UserController extends Controller
return redirect()->back();
}
/**
* Remove the current user's two factor secret key
*
* @param Remove2FASecretRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function remove2FASecret(Remove2FASecretRequest $request)
{
Log::warning('SECURITY: Disabling two factor authentication (user initiated)', [
@@ -300,34 +544,94 @@ class UserController extends Controller
return redirect()->back();
}
public function terminate(Request $request, User $user)
{
$this->authorize('terminate', User::class);
if (config('demo.is_enabled')) {
/**
* Remove the given user's two factor secret key
*
* @param Reset2FASecretRequest $request
* @param User $user
* @return \Illuminate\Http\RedirectResponse
*/
public function reset2FASecret(Reset2FASecretRequest $request, User $user) {
// note: could invalidate other sessions for increased security
if ($user->has2FA()) {
Log::warning('SECURITY: Disabling two factor authentication (admin initiated)', [
'initiator' => $request->user()->email,
'target' => $user->email,
'ip' => $request->ip(),
]);
$user->twofa_secret = null;
$user->password = null;
$user->save();
$user->notify(new TwoFactorResetNotification());
return redirect()
->back()
->with('error', __('This feature is disabled'));
->with('success', __('Two factor removed & user notified.'));
}
// TODO: move logic to policy
if (! $user->isStaffMember() || $user->is(Auth::user())) {
$request->session()->flash('error', __('You cannot terminate this user.'));
return redirect()
->back()
->with('error', 'This user does not have two-factor authentication enabled.');
}
return redirect()->back();
/**
* Suspend the given user
*
* @param AccountSuspensionService $suspensionService
* @param BanUserRequest $request
* @param User $user
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function suspend(AccountSuspensionService $suspensionService, BanUserRequest $request, User $user)
{
$this->authorize('create', [Ban::class, $user]);
$this->disable();
if ($suspensionService->isSuspended($user))
{
return redirect()
->back()
->with('error', __('Account already suspended.'));
}
foreach ($user->roles as $role) {
if ($role->name == 'user') {
continue;
}
$user->removeRole($role->name);
if ($request->suspensionType = "on") {
$suspensionService->suspend($user, $request->reason, $request->duration);
}
else {
$suspensionService->suspend($user, $request->reason);
}
Log::info('User '.$user->name.' has just been demoted.');
$request->session()->flash('success', __('User terminated successfully.'));
//TODO: Dispatch event
return redirect()->back();
}
/**
* Unsuspend the given user
*
* @param AccountSuspensionService $suspensionService
* @param Request $request
* @param User $user
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function unsuspend(AccountSuspensionService $suspensionService, Request $request, User $user)
{
$this->authorize('delete', $user->bans);
$this->disable();
if ($suspensionService->isSuspended($user)) {
$suspensionService->unsuspend($user);
$request->session()->flash('success', __('Account unsuspended successfully!'));
} else {
$request->session()->flash('error', __('This account isn\'t suspended!'));
}
return redirect()->back();
}
}

View File

@@ -70,6 +70,8 @@ class VacancyController extends Controller
'discordRoleID' => $request->discordRole,
'vacancyFormID' => $request->vacancyFormID,
'vacancyCount' => $request->vacancyCount,
'requiresDiscord' => $request->requireDiscordAccount == 'on',
'requiredAge' => $request->requiredAge
]);
@@ -142,6 +144,8 @@ class VacancyController extends Controller
$vacancy->vacancyFullDescription = $request->vacancyFullDescription;
$vacancy->vacancyDescription = $request->vacancyDescription;
$vacancy->vacancyCount = $request->vacancyCount;
$vacancy->requiresDiscord = $request->requireDiscordAccount == 'on';
$vacancy->requiredAge = $request->requiredAge;
$vacancy->save();
@@ -153,10 +157,18 @@ class VacancyController extends Controller
public function delete(Request $request, Vacancy $vacancy)
{
$this->authorize('delete', $vacancy);
$vacancy->delete();
if ($vacancy->teams->isEmpty()) {
$vacancy->delete();
return redirect()
->back()
->with('success', __('Vacancy deleted. All applications associated with it are now gone too.'));
}
return redirect()
->back()
->with('success', __('Vacancy deleted. All applications associated with it are now gone too.'));
->with('error', __('Please detach any teams that may be using this vacancy first.'));
}
}

View File

@@ -89,10 +89,10 @@ class Kernel extends HttpKernel
'2fa' => \PragmaRX\Google2FALaravel\Middleware::class,
'passwordexpiration' => \App\Http\Middleware\PasswordExpirationMiddleware::class,
'passwordredirect' => \App\Http\Middleware\PasswordExpirationRedirectMiddleware::class,
'localize' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRoutes::class,
'localizationRedirect' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRedirectFilter::class,
'localeSessionRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleSessionRedirect::class,
'localeCookieRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleCookieRedirect::class,
'localeViewPath' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationViewPath::class,
'localize' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRoutes::class,
'localizationRedirect' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRedirectFilter::class,
'localeSessionRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleSessionRedirect::class,
'localeCookieRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleCookieRedirect::class,
'localeViewPath' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationViewPath::class,
];
}

View File

@@ -22,8 +22,11 @@
namespace App\Http\Middleware;
use App\Application;
use App\User;
use Carbon\Carbon;
use Closure;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\View;
@@ -33,34 +36,42 @@ class ApplicationEligibility
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @deprecated Deprecated in 0.9.0
* @see User::isEligible()
* @param Request $request
* @param Closure $next
* @return mixed
* @throws \Exception
* @throws Exception
*/
public function handle($request, Closure $next)
{
$curtime = new Carbon(now());
$eligible = false;
$daysRemaining = __('N/A');
if (Auth::check()) {
$applications = Application::where('applicantUserID', Auth::user()->id)->get();
$eligible = true;
$daysRemaining = 0;
$lastApplication = Application::where('applicantUserID', Auth::user()->id)->latest()->first();
if (! $applications->isEmpty()) {
foreach ($applications as $application) {
$appTime = Carbon::parse($application->created_at);
if ($appTime->isSameMonth($curtime)) {
Log::warning('Notice: Application ID '.$application->id.' was found to be in the same month as today\'s time, making the user '.Auth::user()->name.' ineligible for application');
$eligible = false;
}
}
if (is_null($lastApplication)) {
View::share('isEligibleForApplication', true);
View::share('eligibilityDaysRemaining', 0);
$allowedTime = Carbon::parse($applications->last()->created_at)->addMonth();
$daysRemaining = $allowedTime->diffInDays(now());
return $next($request);
}
$daysRemaining = $lastApplication->created_at->addMonth()->diffInDays(now());
if ($lastApplication->created_at->diffInMonths(now()) > 1 && in_array($lastApplication->applicationStatus, ['DENIED', 'APPROVED'])) {
$eligible = true;
}
Log::debug('Perfomed application eligibility check', [
'eligible' => $eligible,
'daysRemaining' => $daysRemaining,
'ipAddress' => Auth::user()->originalIP,
'checkUserID' => Auth::user()->id
]);
View::share('isEligibleForApplication', $eligible);
View::share('eligibilityDaysRemaining', $daysRemaining);
}

View File

@@ -21,12 +21,21 @@
namespace App\Http\Middleware;
use App\Services\AccountSuspensionService;
use Closure;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\View;
class Bancheck
{
private $suspensionService;
public function __construct(AccountSuspensionService $suspensionService) {
$this->suspensionService = $suspensionService;
}
/**
* Handle an incoming request.
*
@@ -37,11 +46,11 @@ class Bancheck
public function handle($request, Closure $next)
{
$userIP = $request->ip();
$anonymousUser = User::where('ipAddress', $userIP)->get();
$anonymousUser = User::where('currentIp', $userIP)->get();
if (Auth::check() && Auth::user()->isBanned()) {
if (Auth::check() && $this->suspensionService->isSuspended($anonymousUser)) {
View::share('isBanned', true);
} elseif (! $anonymousUser->isEmpty() && User::find($anonymousUser->id)->isBanned()) {
} elseif (! $anonymousUser->isEmpty() && $this->suspensionService->isSuspended(User::find($anonymousUser->id))) {
View::share('isBanned', true);
} else {
View::share('isBanned', false);

View File

@@ -21,6 +21,7 @@
namespace App\Http\Middleware;
use App\Services\AccountSuspensionService;
use Closure;
use Illuminate\Support\Facades\Auth;
@@ -35,10 +36,10 @@ class ForceLogoutMiddleware
*/
public function handle($request, Closure $next)
{
if (Auth::user()->isBanned()) {
Auth::logout();
$request->session()->flash('error', __('Your account is suspended. You will not be able to login or register until the suspension is lifted.'));
if ((new AccountSuspensionService())->isSuspended(Auth::user())) {
Auth::logout();
$request->session()->flash('error', __('Your account is suspended. If you think this was a mistake, please contact an admin.'));
return redirect('/');
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class AddDobRequest extends FormRequest
{
public function rules(): array
{
return [
'dob' => 'required|string|date_format:Y-m-d|before:-13 years',
];
}
public function authorize(): bool
{
if (is_null(Auth::user()->dob)) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class AdminPasswordResetRequest extends FormRequest
{
public function rules(): array
{
if (Auth::user()->has2FA()) {
return [
'currentPassword' => 'required|current_password:web',
'otp' => 'required|integer|max:6',
];
}
return [
'currentPassword' => 'required|current_password:web',
];
}
public function authorize(): bool
{
return true;
}
}

View File

@@ -44,7 +44,6 @@ class Remove2FASecretRequest extends FormRequest
{
return [
'currentPassword' => 'required|current_password',
'consent' => 'required|accepted',
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class Reset2FASecretRequest extends FormRequest
{
public function rules(): array
{
return [
'currentPassword' => 'required|current_password',
];
}
public function authorize(): bool
{
return true;
}
}

View File

@@ -4,7 +4,7 @@ namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class HomeContactRequest extends FormRequest
class SetNewPasswordRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
@@ -13,7 +13,11 @@ class HomeContactRequest extends FormRequest
*/
public function authorize()
{
return true;
if (\Auth::user()->hasDiscordConnection()) {
return true;
}
return false;
}
/**
@@ -24,9 +28,7 @@ class HomeContactRequest extends FormRequest
public function rules()
{
return [
'email' => 'required|email',
'msg' => 'required|string',
'captcha' => 'required|string'
'newpass' => 'required|string|min:10|confirmed',
];
}
}

View File

@@ -46,7 +46,7 @@ class UpdateUserRequest extends FormRequest
return [
'email' => 'required|email',
'name' => 'required|string',
'uuid' => 'required|max:32|min:32',
'uuid' => 'nullable|max:32|min:32',
'roles' => 'required_without_all',
];
}

View File

@@ -47,6 +47,8 @@ class VacancyEditRequest extends FormRequest
'vacancyDescription' => 'required|string',
'vacancyFullDescription' => 'nullable|string',
'vacancyCount' => 'required|integer|min:1',
'requireDiscordAccount' => 'required|string',
'requiredAge' => 'required|integer|numeric|min:13|max:100'
];
}
}

View File

@@ -25,6 +25,8 @@ use Illuminate\Foundation\Http\FormRequest;
class VacancyRequest extends FormRequest
{
public mixed $requiresDiscordAccount;
/**
* Determine if the user is authorized to make this request.
*
@@ -46,10 +48,12 @@ class VacancyRequest extends FormRequest
'vacancyName' => 'required|string',
'vacancyDescription' => 'required|string',
'vacancyFullDescription' => 'nullable|string',
'permissionGroup' => 'required|string',
'discordRole' => 'required|string',
'permissionGroup' => 'nullable|string',
'discordRole' => 'nullable|string',
'vacancyCount' => 'required|integer',
'vacancyFormID' => 'required|integer',
'requireDiscordAccount' => 'required|string',
'requiredAge' => 'required|integer|numeric|min:13|max:100'
];
}
}