Code review

This commit fixes some superficial instances of Broken Access Control 
(https://owasp.org/www-project-top-ten/OWASP_Top_Ten_2017/Top_10-2017_A5-Broken_Access_Control).
There may be some more instances of this, as authorization was only done 
after most of the controllers were done (big mistake).

Some refactoring was also performed, where Route Model Binding with DI 
(dependency injection) was used whenever possible, to increase 
testability of the codebase.
Some reused code was also moved to Helper classes as to enforce DRY; 
There may be some lines of code that are still copy-pasted from other 
parts of the codebase for reuse.

Non-breaking refactoring changes were made, but the app as a whole still 
needs full manual testing, and customised responses to HTTP 500 
responses. Some errors are also not handled gracefully and this wasn't 
checked in this commit.
This commit is contained in:
Miguel Nogueira 2020-07-16 21:21:28 +01:00
parent 9e2d571298
commit 5f1f92a9ce
30 changed files with 310 additions and 203 deletions

View File

@ -16,11 +16,6 @@ class IP
public function lookup(string $IP): object public function lookup(string $IP): object
{ {
if (empty($IP))
{
throw new LogicException(__METHOD__ . 'is missing parameter IP!');
}
$params = [ $params = [
'apiKey' => config('general.keys.ipapi.apikey'), 'apiKey' => config('general.keys.ipapi.apikey'),
'ip' => $IP 'ip' => $IP

View File

@ -7,6 +7,29 @@ use Illuminate\Support\Collection;
class ContextAwareValidator class ContextAwareValidator
{ {
/**
* The excludedNames array will make the validator ignore any of these names when including names into the rules.
* @var array
*/
private $excludedNames = [
'_token',
'_method',
'formName'
];
/**
* Utility wrapper for json_encode.
*
* @param array $value The array to be converted.
* @return string The JSON representation of $value
*/
private function encode(array $value) : string
{
return json_encode($value);
}
/** /**
* The getValidator() method will take an array of fields from the request body, iterates through them, * The getValidator() method will take an array of fields from the request body, iterates through them,
* and dynamically adds validation rules for them. Depending on parameters, it may or may not generate * and dynamically adds validation rules for them. Depending on parameters, it may or may not generate
@ -30,12 +53,6 @@ class ContextAwareValidator
$formStructure = []; $formStructure = [];
$validator = []; $validator = [];
$excludedNames = [
'_token',
'_method',
'formName'
];
if ($includeFormName) if ($includeFormName)
{ {
$validator['formName'] = 'required|string|max:100'; $validator['formName'] = 'required|string|max:100';
@ -43,7 +60,7 @@ class ContextAwareValidator
foreach ($fields as $fieldName => $field) foreach ($fields as $fieldName => $field)
{ {
if(!in_array($fieldName, $excludedNames)) if(!in_array($fieldName, $this->excludedNames))
{ {
$validator[$fieldName . ".0"] = 'required|string'; $validator[$fieldName . ".0"] = 'required|string';
$validator[$fieldName . ".1"] = 'required|string'; $validator[$fieldName . ".1"] = 'required|string';
@ -62,11 +79,60 @@ class ContextAwareValidator
return ($generateStructure) ? return ($generateStructure) ?
collect([ collect([
'validator' => $validatorInstance, 'validator' => $validatorInstance,
'structure' => json_encode($formStructure) 'structure' => $this->encode($formStructure)
]) ])
: $validatorInstance; : $validatorInstance;
} }
/**
* The getResponseValidator method is similar to the getValidator method; It basically takes
* an array of fields from a previous form (that probably went through the other method) and adds validation
* to the field names.
*
* Also generates the storable response structure if you tell it to.
*
* @param array $fields The received fields
* @param array $formStructure The form structure - You must supply this if you want the response structure
* @param bool $generateResponseStructure Whether to generate the response structure
* @return Validator|Collection A collection or a validator, depending on the args. Will return validatior if only fields are supplied.
*/
public function getResponseValidator(array $fields, array $formStructure = [], bool $generateResponseStructure = true)
{
$responseStructure = [];
$validator = [];
if (empty($formStructure) && $generateResponseStructure)
{
throw new \InvalidArgumentException('Illegal combination of arguments supplied! Please check the method\'s documentation.');
}
foreach($fields as $fieldName => $value)
{
if(!in_array($fieldName, $this->excludedNames))
{
$validator[$fieldName] = 'required|string';
if ($generateResponseStructure)
{
$responseStructure['responses'][$fieldName]['type'] = $formStructure['fields'][$fieldName]['type'] ?? 'Unavailable';
$responseStructure['responses'][$fieldName]['title'] = $formStructure['fields'][$fieldName]['title'];
$responseStructure['responses'][$fieldName]['response'] = $value;
}
}
}
$validatorInstance = Validator::make($fields, $validator);
return ($generateResponseStructure) ?
collect([
'validator' => $validatorInstance,
'responseStructure' => $this->encode($responseStructure)
])
: $validatorInstance;
}
} }

View File

@ -18,8 +18,11 @@ use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use ContextAwareValidator;
class ApplicationController extends Controller class ApplicationController extends Controller
{ {
private function canVote($votes) private function canVote($votes)
{ {
$allvotes = collect([]); $allvotes = collect([]);
@ -45,11 +48,8 @@ class ApplicationController extends Controller
} }
public function showUserApp(Request $request, $applicationID) public function showUserApp(Request $request, Application $application)
{ {
// TODO: Inject it instead (do this where there is no injection, not just here)
$application = Application::find($applicationID);
$this->authorize('view', $application); $this->authorize('view', $application);
if (!is_null($application)) if (!is_null($application))
@ -78,6 +78,8 @@ class ApplicationController extends Controller
public function showAllApps() public function showAllApps()
{ {
$this->authorize('viewAny', Application::class);
return view('dashboard.appmanagement.all') return view('dashboard.appmanagement.all')
->with('applications', Application::paginate(6)); ->with('applications', Application::paginate(6));
} }
@ -186,36 +188,16 @@ class ApplicationController extends Controller
Log::info('Processing new application!'); Log::info('Processing new application!');
$formStructure = json_decode($vacancy->first()->forms->formStructure, true); $formStructure = json_decode($vacancy->first()->forms->formStructure, true);
$responseStructure = []; $responseValidation = ContextAwareValidator::getResponseValidator($request->all(), $formStructure);
$excludedNames = [
'_token',
];
$validator = [];
foreach($request->all() as $fieldName => $value)
{
if(!in_array($fieldName, $excludedNames))
{
$validator[$fieldName] = 'required|string';
$responseStructure['responses'][$fieldName]['type'] = $formStructure['fields'][$fieldName]['type'] ?? 'Unavailable';
$responseStructure['responses'][$fieldName]['title'] = $formStructure['fields'][$fieldName]['title'];
$responseStructure['responses'][$fieldName]['response'] = $value;
}
}
Log::info('Built response & validator structure!'); Log::info('Built response & validator structure!');
$validation = Validator::make($request->all(), $validator); if (!$responseValidation->get('validator')->fails())
if (!$validation->fails())
{ {
$response = Response::create([ $response = Response::create([
'responseFormID' => $vacancy->first()->forms->id, 'responseFormID' => $vacancy->first()->forms->id,
'associatedVacancyID' => $vacancy->first()->id, // Since a form can be used by multiple vacancies, we can only know which specific vacancy this response ties to by using a vacancy ID 'associatedVacancyID' => $vacancy->first()->id, // Since a form can be used by multiple vacancies, we can only know which specific vacancy this response ties to by using a vacancy ID
'responseData' => json_encode($responseStructure) 'responseData' => $responseValidation->get('responseStructure')
]); ]);
Log::info('Registered form response for user ' . Auth::user()->name . ' for vacancy ' . $vacancy->first()->vacancyName); Log::info('Registered form response for user ' . Auth::user()->name . ' for vacancy ' . $vacancy->first()->vacancyName);
@ -249,35 +231,27 @@ class ApplicationController extends Controller
return redirect()->back(); return redirect()->back();
} }
public function updateApplicationStatus(Request $request, $applicationID, $newStatus) public function updateApplicationStatus(Request $request, $application, $newStatus)
{ {
$application = Application::find($applicationID);
$this->authorize('update', Application::class); $this->authorize('update', Application::class);
if (!is_null($application)) switch ($newStatus)
{ {
switch ($newStatus) case 'deny':
{
case 'deny':
event(new ApplicationDeniedEvent($application)); event(new ApplicationDeniedEvent($application));
break; break;
case 'interview': case 'interview':
Log::info('User ' . Auth::user()->name . ' has moved application ID ' . $application->id . 'to interview stage'); Log::info('User ' . Auth::user()->name . ' has moved application ID ' . $application->id . 'to interview stage');
$request->session()->flash('success', 'Application moved to interview stage! (:'); $request->session()->flash('success', 'Application moved to interview stage! (:');
$application->setStatus('STAGE_INTERVIEW'); $application->setStatus('STAGE_INTERVIEW');
$application->user->notify(new ApplicationMoved()); $application->user->notify(new ApplicationMoved());
break; break;
default: default:
$request->session()->flash('error', 'There are no suitable statuses to update to. Do not mess with the URL.'); $request->session()->flash('error', 'There are no suitable statuses to update to. Do not mess with the URL.');
}
}
else
{
$request->session()->flash('The application you\'re trying to update does not exist.');
} }
return redirect()->back(); return redirect()->back();

View File

@ -24,84 +24,56 @@ class AppointmentController extends Controller
]; ];
public function saveAppointment(Request $request, $applicationID) public function saveAppointment(Request $request, Application $application)
{ {
// Unrelated TODO: change if's in application page to a switch statement, & have the row encompass it
$this->authorize('create', Appointment::class); $this->authorize('create', Appointment::class);
$appointmentDate = Carbon::parse($request->appointmentDateTime);
$app = Application::find($applicationID); $appointment = Appointment::create([
'appointmentDescription' => $request->appointmentDescription,
if (!is_null($app)) 'appointmentDate' => $appointmentDate->toDateTimeString(),
{ 'applicationID' => $application->id,
// make sure this is a valid date by parsing it first 'appointmentLocation' => (in_array($request->appointmentLocation, $this->allowedPlatforms)) ? $request->appointmentLocation : 'DISCORD',
$appointmentDate = Carbon::parse($request->appointmentDateTime); ]);
$application->setStatus('STAGE_INTERVIEW_SCHEDULED');
$appointment = Appointment::create([ Log::info('User ' . Auth::user()->name . ' has scheduled an appointment with ' . $application->user->name . ' for application ID' . $application->id, [
'appointmentDescription' => $request->appointmentDescription, 'datetime' => $appointmentDate->toDateTimeString(),
'appointmentDate' => $appointmentDate->toDateTimeString(), 'scheduled' => now()
'applicationID' => $applicationID, ]);
'appointmentLocation' => (in_array($request->appointmentLocation, $this->allowedPlatforms)) ? $request->appointmentLocation : 'DISCORD',
]);
$app->setStatus('STAGE_INTERVIEW_SCHEDULED');
$application->user->notify(new AppointmentScheduled($appointment));
$request->session()->flash('success', 'Appointment successfully scheduled @ ' . $appointmentDate->toDateTimeString());
Log::info('User ' . Auth::user()->name . ' has scheduled an appointment with ' . $app->user->name . ' for application ID' . $app->id, [
'datetime' => $appointmentDate->toDateTimeString(),
'scheduled' => now()
]);
$app->user->notify(new AppointmentScheduled($appointment));
$request->session()->flash('success', 'Appointment successfully scheduled @ ' . $appointmentDate->toDateTimeString());
}
else
{
$request->session()->flash('error', 'Cant\'t schedule an appointment for an application that doesn\'t exist.');
}
return redirect()->back(); return redirect()->back();
} }
public function updateAppointment(Request $request, $applicationID, $status) public function updateAppointment(Request $request, Application $application, $status)
{ {
$this->authorize('update', $application->appointment);
$application = Application::find($applicationID);
$validStatuses = [ $validStatuses = [
'SCHEDULED', 'SCHEDULED',
'CONCLUDED' 'CONCLUDED'
]; ];
$this->authorize('update', $application->appointment); // NOTE: This is a little confusing, refactor
$application->appointment->appointmentStatus = (in_array($status, $validStatuses)) ? strtoupper($status) : 'SCHEDULED';
$application->appointment->save();
$application->setStatus('STAGE_PEERAPPROVAL');
$application->user->notify(new ApplicationMoved());
$request->session()->flash('success', 'Interview finished! Staff members can now vote on it.');
if (!is_null($application))
{
// NOTE: This is a little confusing, refactor
$application->appointment->appointmentStatus = (in_array($status, $validStatuses)) ? strtoupper($status) : 'SCHEDULED';
$application->appointment->save();
$application->setStatus('STAGE_PEERAPPROVAL');
$application->user->notify(new ApplicationMoved());
$request->session()->flash('success', 'Interview finished! Staff members can now vote on it.');
}
else
{
$request->session()->flash('error', 'The application you\'re trying to update doesn\'t exist or have an appointment.');
}
return redirect()->back(); return redirect()->back();
} }
// also updates // also updates
public function saveNotes(SaveNotesRequest $request, $applicationID) public function saveNotes(SaveNotesRequest $request, $application)
{ {
$application = Application::find($applicationID);
if (!is_null($application)) if (!is_null($application))
{ {
$application->appointment->meetingNotes = $request->noteText; $application->appointment->meetingNotes = $request->noteText;
@ -111,7 +83,7 @@ class AppointmentController extends Controller
} }
else else
{ {
$request->session()->flash('error', 'Sanity check failed: There\'s no appointment to save notes to!'); $request->session()->flash('error', 'There\'s no appointment to save notes to!');
} }
return redirect()->back(); return redirect()->back();

View File

@ -15,11 +15,7 @@ class BanController extends Controller
public function insert(BanUserRequest $request, User $user) public function insert(BanUserRequest $request, User $user)
{ {
if ($user->is(Auth::user())) $this->authorize('create', Ban::class);
{
$request->session()->flash('error', 'You can\'t ban yourself!');
return redirect()->back();
}
if (is_null($user->bans)) if (is_null($user->bans))
{ {

View File

@ -32,14 +32,6 @@ class CommentController extends Controller
if ($comment) if ($comment)
{ {
foreach (User::all() as $user)
{
if ($user->isStaffMember())
{
$user->notify(new NewComment($comment, $application));
}
}
$request->session()->flash('success', 'Comment posted! (:'); $request->session()->flash('success', 'Comment posted! (:');
} }
else else

View File

@ -4,11 +4,23 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use GuzzleHttp; use GuzzleHttp;
use App\Notifications\NewContact;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use App\User;
class ContactController extends Controller class ContactController extends Controller
{ {
protected $users;
public function __construct(User $users)
{
$this->users = $users;
}
public function create(Request $request) public function create(Request $request)
{ {
$name = $request->name; $name = $request->name;
@ -18,12 +30,14 @@ class ContactController extends Controller
$challenge = $request->input('captcha'); $challenge = $request->input('captcha');
// TODO: now: add middleware for this verification, move to invisible captcha
$verifyrequest = Http::asForm()->post(config('recaptcha.verify.apiurl'), [ $verifyrequest = Http::asForm()->post(config('recaptcha.verify.apiurl'), [
'secret' => config('recaptcha.keys.secret'), 'secret' => config('recaptcha.keys.secret'),
'response' => $challenge, 'response' => $challenge,
'remoteip' => $_SERVER['REMOTE_ADDR'] 'remoteip' => $request->ip()
]); ]);
$response = json_decode($verifyrequest->getBody(), true); $response = json_decode($verifyrequest->getBody(), true);
if (!$response['success']) if (!$response['success'])
@ -32,7 +46,18 @@ class ContactController extends Controller
return redirect()->back(); return redirect()->back();
} }
// TODO: Send mail
foreach(User::all() as $user)
{
if ($user->hasRole('admin'))
{
$user->notify(new NewContact(collect([
'message' => $msg,
'ip' => $request->ip(),
'email' => $email
])));
}
}
$request->session()->flash('success', 'Message sent successfully! We usually respond within 48 hours.'); $request->session()->flash('success', 'Message sent successfully! We usually respond within 48 hours.');
return redirect()->back(); return redirect()->back();

View File

@ -6,16 +6,30 @@ use App\Application;
use App\Events\ApplicationApprovedEvent; use App\Events\ApplicationApprovedEvent;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class DevToolsController extends Controller class DevToolsController extends Controller
{ {
// The use case for Laravel's gate and/or validation Requests is so tiny here that a full-blown policy would be overkill.
protected function isolatedAuthorise()
{
if (!Auth::user()->can('admin.developertools.use'))
{
abort(403, 'You\'re not authorized to access this page.');
}
}
public function index() public function index()
{ {
$this->isolatedAuthorise();
return view('dashboard.administration.devtools') return view('dashboard.administration.devtools')
->with('applications', Application::where('applicationStatus', 'STAGE_PEERAPPROVAL')->get()); ->with('applications', Application::where('applicationStatus', 'STAGE_PEERAPPROVAL')->get());
} }
public function forceVoteCount(Request $request) public function forceVoteCount(Request $request)
{ {
$this->isolatedAuthorise();
$application = Application::find($request->application); $application = Application::find($request->application);
if (!is_null($application)) if (!is_null($application))

View File

@ -55,10 +55,8 @@ class FormController extends Controller
return redirect()->back(); return redirect()->back();
} }
public function destroy(Request $request, $id) public function destroy(Request $request, Form $form)
{ {
$form = Form::find($id);
$this->authorize('delete', $form); $this->authorize('delete', $form);
$deletable = true; $deletable = true;
@ -85,6 +83,8 @@ class FormController extends Controller
public function preview(Request $request, Form $form) public function preview(Request $request, Form $form)
{ {
$this->authorize('viewAny', Form::class);
return view('dashboard.administration.formpreview') return view('dashboard.administration.formpreview')
->with('form', json_decode($form->formStructure, true)) ->with('form', json_decode($form->formStructure, true))
->with('title', $form->formName) ->with('title', $form->formName)
@ -93,6 +93,8 @@ class FormController extends Controller
public function edit(Request $request, Form $form) public function edit(Request $request, Form $form)
{ {
$this->authorize('update', $form);
return view('dashboard.administration.editform') return view('dashboard.administration.editform')
->with('formStructure', json_decode($form->formStructure, true)) ->with('formStructure', json_decode($form->formStructure, true))
->with('title', $form->formName) ->with('title', $form->formName)
@ -101,6 +103,8 @@ class FormController extends Controller
public function update(Request $request, Form $form) public function update(Request $request, Form $form)
{ {
$this->authorize('update', $form);
$contextValidation = ContextAwareValidator::getValidator($request->all(), true); $contextValidation = ContextAwareValidator::getValidator($request->all(), true);
$this->authorize('update', $form); $this->authorize('update', $form);

View File

@ -87,7 +87,6 @@ class ProfileController extends Controller
public function saveProfile(ProfileSave $request) public function saveProfile(ProfileSave $request)
{ {
// TODO: Switch to route model binding
$profile = User::find(Auth::user()->id)->profile; $profile = User::find(Auth::user()->id)->profile;
$social = []; $social = [];
@ -120,19 +119,6 @@ class ProfileController extends Controller
$request->session()->flash('success', 'Profile settings saved successfully.'); $request->session()->flash('success', 'Profile settings saved successfully.');
} }
else
{
$gm = 'Guru Meditation #' . rand(0, 1000);
Log::alert('[GURU MEDITATION]: Could not find profile for authenticated user ' . Auth::user()->name . 'whilst trying to update it! Please verify that profiles are being created automatically during signup.',
[
'uuid' => Auth::user()->uuid,
'timestamp' => now(),
'route' => $request->route()->getName(),
'gmcode' => $gm // If this error is reported, the GM code, denoting a severe error, will help us find this entry in the logs
]);
$request->session()->flash('error', 'A technical error has occurred whilst trying to save your profile. Incident details have been recorded. Please report this incident to administrators with the following case number: ' . $gm);
}
return redirect()->back(); return redirect()->back();

View File

@ -189,6 +189,9 @@ class UserController extends Controller
public function delete(DeleteUserRequest $request, User $user) public function delete(DeleteUserRequest $request, User $user)
{ {
$this->authorize('delete', $user);
if ($request->confirmPrompt == 'DELETE ACCOUNT') if ($request->confirmPrompt == 'DELETE ACCOUNT')
{ {
$user->delete(); $user->delete();
@ -206,6 +209,8 @@ class UserController extends Controller
public function update(UpdateUserRequest $request, User $user) public function update(UpdateUserRequest $request, User $user)
{ {
$this->authorize('adminEdit', $user);
// Mass update would not be possible here without extra code, making route model binding useless // Mass update would not be possible here without extra code, making route model binding useless
$user->email = $request->email; $user->email = $request->email;
$user->name = $request->name; $user->name = $request->name;

View File

@ -64,10 +64,9 @@ class VacancyController extends Controller
} }
public function updatePositionAvailability(Request $request, $status, $id) public function updatePositionAvailability(Request $request, $status, Vacancy $vacancy)
{ {
$vacancy = Vacancy::find($id);
$this->authorize('update', $vacancy); $this->authorize('update', $vacancy);
if (!is_null($vacancy)) if (!is_null($vacancy))

View File

@ -13,33 +13,23 @@ use Illuminate\Support\Facades\Log;
class VoteController extends Controller class VoteController extends Controller
{ {
public function vote(VoteRequest $voteRequest, $applicationID) public function vote(VoteRequest $voteRequest, Application $application)
{ {
$application = Application::find($applicationID);
$this->authorize('create', Vote::class); $this->authorize('create', Vote::class);
if (!is_null($application)) $vote = Vote::create([
{ 'userID' => Auth::user()->id,
$vote = Vote::create([ 'allowedVoteType' => $voteRequest->voteType,
'userID' => Auth::user()->id, ]);
'allowedVoteType' => $voteRequest->voteType, $vote->application()->attach($applicationID);
]);
$vote->application()->attach($applicationID);
Log::info('User ' . Auth::user()->name . ' has voted in applicant ' . $application->user->name . '\'s application', [ Log::info('User ' . Auth::user()->name . ' has voted in applicant ' . $application->user->name . '\'s application', [
'voteType' => $voteRequest->voteType 'voteType' => $voteRequest->voteType
]); ]);
$voteRequest->session()->flash('success', 'Your vote has been registered!');
$voteRequest->session()->flash('success', 'Your vote has been registered! You will now be notified about the outcome of this application.');
}
else
{
$voteRequest->session()->flash('error', 'Can\t vote a non existant application!');
}
// Cron job will run command that processes votes // Cron job will run command that processes votes
return redirect()->back(); return redirect()->back();
} }
} }

View File

@ -52,7 +52,7 @@ class ApplicationDenied extends Notification implements ShouldQueue
->line('Your most recent application has been denied.') ->line('Your most recent application has been denied.')
->line('Our review team denies applications for several reasons, including poor answers.') ->line('Our review team denies applications for several reasons, including poor answers.')
->line('Please review your application and try again in 30 days.') ->line('Please review your application and try again in 30 days.')
->action('Review application', url(route('showUserApp', ['id' => $this->application->id]))) ->action('Review application', url(route('showUserApp', ['application' => $this->application->id])))
->line('Better luck next time!'); ->line('Better luck next time!');
} }

View File

@ -55,7 +55,7 @@ class NewApplicant extends Notification implements ShouldQueue
->subject(config('app.name') . ' - New application') ->subject(config('app.name') . ' - New application')
->line('Someone has just applied for a position. Check it out!') ->line('Someone has just applied for a position. Check it out!')
->line('You are receiving this because you\'re a staff member at ' . config('app.name') . '.') ->line('You are receiving this because you\'re a staff member at ' . config('app.name') . '.')
->action('View Application', url(route('showUserApp', ['id' => $this->application->id]))) ->action('View Application', url(route('showUserApp', ['application' => $this->application->id])))
->line('Thank you!'); ->line('Thank you!');
} }
@ -67,7 +67,7 @@ class NewApplicant extends Notification implements ShouldQueue
$vacancyDetails['name'] = $this->vacancy->vacancyName; $vacancyDetails['name'] = $this->vacancy->vacancyName;
$vacancyDetails['slots'] = $this->vacancy->vacancyCount; $vacancyDetails['slots'] = $this->vacancy->vacancyCount;
$url = route('showUserApp', ['id' => $this->application->id]); $url = route('showUserApp', ['application' => $this->application->id]);
$applicant = $this->application->user->name; $applicant = $this->application->user->name;
return (new SlackMessage) return (new SlackMessage)

View File

@ -50,7 +50,7 @@ class NewComment extends Notification implements ShouldQueue
->subject(config('app.name') . ' - New comment') ->subject(config('app.name') . ' - New comment')
->line('Someone has just posted a new comment on an application you follow.') ->line('Someone has just posted a new comment on an application you follow.')
->line('You\'re receiving this email because you\'ve voted/commented on this application.') ->line('You\'re receiving this email because you\'ve voted/commented on this application.')
->action('Check it out', url(route('showUserApp', ['id' => $this->application->id]))) ->action('Check it out', url(route('showUserApp', ['application' => $this->application->id])))
->line('Thank you!'); ->line('Thank you!');
} }

View File

@ -0,0 +1,78 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Collection;
class NewContact extends Notification
{
use Queueable;
public $message;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Collection $message)
{
$this->message = $message;
}
/**
* 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)
{
if ($this->message->has([
'message',
'ip',
'email'
]))
{
return (new MailMessage)
->line('We\'ve received a new contact form submission in the StaffManagement app center.')
->line('This is what they sent: ')
->line('')
->line($this->message->get('message'))
->line('')
->line('This message was received from ' . $this->message->get('ip') . ' and submitted by ' . $this->message->get('email') . '.')
->action('Sign in', url(route('login')))
->line('Thank you!');
}
throw new \InvalidArgumentException("Invalid arguments supplied to NewContact!");
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@ -41,7 +41,7 @@ class BanPolicy
*/ */
public function create(User $user) public function create(User $user)
{ {
// return $user->hasRole('admin') && $user->isNot(Auth::user());
} }
/** /**
@ -53,7 +53,7 @@ class BanPolicy
*/ */
public function update(User $user, Ban $ban) public function update(User $user, Ban $ban)
{ {
// return $user->hasRole('admin');
} }
/** /**

View File

@ -24,6 +24,12 @@ class UserPolicy
return $authUser->is($user) || $authUser->hasRole('admin'); return $authUser->is($user) || $authUser->hasRole('admin');
} }
// This refers to the admin tools that let staff update more information than users themselves can
public function adminEdit(User $authUser, User $user)
{
return $authUser->hasRole('admin') && $authUser->isNot($user);
}
public function viewStaff(User $user) public function viewStaff(User $user)
{ {
return $user->can('admin.stafflist'); return $user->can('admin.stafflist');
@ -38,4 +44,9 @@ class UserPolicy
{ {
return $authUser->hasRole('admin'); return $authUser->hasRole('admin');
} }
public function delete(User $authUser, User $subject)
{
return $authUser->hasRole('admin') && $authUser->isNot($subject);
}
} }

View File

@ -133,7 +133,7 @@
@if($vacancy->vacancyStatus == 'OPEN') @if($vacancy->vacancyStatus == 'OPEN')
<form method="POST" action="{{ route('updatePositionAvailability', ['id' => $vacancy->id, 'status' => 'close']) }}" style="display: inline"> <form method="POST" action="{{ route('updatePositionAvailability', ['vacancy' => $vacancy->id, 'status' => 'close']) }}" style="display: inline">
@method('PATCH') @method('PATCH')
@csrf @csrf
<button type="submit" class="ml-4 btn btn-danger"><i class="fas fa-ban"></i> Close Position</button> <button type="submit" class="ml-4 btn btn-danger"><i class="fas fa-ban"></i> Close Position</button>

View File

@ -54,7 +54,7 @@
<td>{{$form->created_at}}</td> <td>{{$form->created_at}}</td>
<td>{{ $form->updated_at }}</td> <td>{{ $form->updated_at }}</td>
<td> <td>
<form style="display: inline-block; white-space: nowrap" action="{{route('destroyForm', ['id' => $form->id])}}" method="POST"> <form style="display: inline-block; white-space: nowrap" action="{{route('destroyForm', ['form' => $form->id])}}" method="POST">
@method('DELETE') @method('DELETE')
@csrf @csrf

View File

@ -195,7 +195,7 @@
@if ($vacancy->vacancyStatus == 'OPEN') @if ($vacancy->vacancyStatus == 'OPEN')
<form action="{{route('updatePositionAvailability', ['status' => 'close', 'id' => $vacancy->id])}}" method="POST" id="closePosition" style="display: inline"> <form action="{{route('updatePositionAvailability', ['status' => 'close', 'vacancy' => $vacancy->id])}}" method="POST" id="closePosition" style="display: inline">
@csrf @csrf
@method('PATCH') @method('PATCH')
<button type="submit" class="btn btn-sm btn-danger"><i class="fa fa-ban"></i></button> <button type="submit" class="btn btn-sm btn-danger"><i class="fa fa-ban"></i></button>
@ -203,7 +203,7 @@
@else @else
<form action="{{route('updatePositionAvailability', ['status' => 'open', 'id' => $vacancy->id])}}" method="POST" id="openPosition" style="display: inline"> <form action="{{route('updatePositionAvailability', ['status' => 'open', 'vacancy' => $vacancy->id])}}" method="POST" id="openPosition" style="display: inline">
@csrf @csrf
@method('PATCH') @method('PATCH')
<button type="submit" class="btn btn-sm btn-success"><i class="fa fa-check"></i></button> <button type="submit" class="btn btn-sm btn-success"><i class="fa fa-check"></i></button>

View File

@ -191,7 +191,7 @@
</td> </td>
<td>{{ $application->created_at }}</td> <td>{{ $application->created_at }}</td>
<td> <td>
<button type="button" class="btn btn-success btn-sm" onclick="window.location.href='{{ route('showUserApp', ['id' => $application->id]) }}'"><i class="fas fa-eye"></i> View</button> <button type="button" class="btn btn-success btn-sm" onclick="window.location.href='{{ route('showUserApp', ['application' => $application->id]) }}'"><i class="fas fa-eye"></i> View</button>
<button type="button" class="btn btn-danger btn-sm ml-2" onclick="$('#deletionConfirmationModal-{{ $application->id }}').modal('show')"><i class="fa fa-trash"></i> Delete</button> <button type="button" class="btn btn-danger btn-sm ml-2" onclick="$('#deletionConfirmationModal-{{ $application->id }}').modal('show')"><i class="fa fa-trash"></i> Delete</button>
</td> </td>
</tr> </tr>

View File

@ -77,7 +77,7 @@
<td>{{$application->user->name}}</td> <td>{{$application->user->name}}</td>
<td><span class="badge-warning badge">{{($application->applicationStatus == 'STAGE_INTERVIEW') ? 'Pending Interview' : 'Unknown Status'}}</span></td> <td><span class="badge-warning badge">{{($application->applicationStatus == 'STAGE_INTERVIEW') ? 'Pending Interview' : 'Unknown Status'}}</span></td>
<td> <td>
<button type="button" class="btn btn-sm btn-success" onclick="window.location.href='{{route('showUserApp', ['id' => $application->id])}}'"><i class="fa fa-eye"></i> View</button> <button type="button" class="btn btn-sm btn-success" onclick="window.location.href='{{route('showUserApp', ['application' => $application->id])}}'"><i class="fa fa-eye"></i> View</button>
<button type="button" class="btn btn-sm btn-warning"><i class="fa fa-clock"></i> Schedule</button> <button type="button" class="btn btn-sm btn-warning"><i class="fa fa-clock"></i> Schedule</button>
</td> </td>
</tr> </tr>
@ -151,7 +151,7 @@
<td><span class="badge badge-success"><i class="fa fa-check"></i> {{ucfirst(strtolower($upcomingApp->appointment->appointmentLocation))}}</span></td> <td><span class="badge badge-success"><i class="fa fa-check"></i> {{ucfirst(strtolower($upcomingApp->appointment->appointmentLocation))}}</span></td>
@endif @endif
<td> <td>
<button type="button" class="btn btn-sm btn-success" onclick="window.location.href='{{route('showUserApp', ['id' => $upcomingApp->id])}}'"><i class="fa fa-eye"></i> View Details</button> <button type="button" class="btn btn-sm btn-success" onclick="window.location.href='{{route('showUserApp', ['application' => $upcomingApp->id])}}'"><i class="fa fa-eye"></i> View Details</button>
</td> </td>
</tr> </tr>

View File

@ -70,7 +70,7 @@
<td>{{$application->created_at}}</td> <td>{{$application->created_at}}</td>
<td>{{$application->updated_at}}</td> <td>{{$application->updated_at}}</td>
<td> <td>
<button type="button" class="btn btn-sm btn-warning" onclick="window.location.href='{{route('showUserApp', ['id' => $application->id])}}'"><i class="fas fa-clipboard-check"></i> Review</button> <button type="button" class="btn btn-sm btn-warning" onclick="window.location.href='{{route('showUserApp', ['application' => $application->id])}}'"><i class="fas fa-clipboard-check"></i> Review</button>
</td> </td>
</tr> </tr>

View File

@ -66,7 +66,7 @@
<td>{{$application->created_at}}</td> <td>{{$application->created_at}}</td>
<td><span class="badge badge-warning">{{($application->applicationStatus == 'STAGE_PEERAPPROVAL') ? 'Peer Review' : 'Unknown'}}</span></td> <td><span class="badge badge-warning">{{($application->applicationStatus == 'STAGE_PEERAPPROVAL') ? 'Peer Review' : 'Unknown'}}</span></td>
<td> <td>
<button type="button" class="btn btn-info btn-sm" onclick="window.location.href='{{route('showUserApp', ['id' => $application->id])}}'"><i class="far fa-clipboard"></i> Review</button> <button type="button" class="btn btn-info btn-sm" onclick="window.location.href='{{route('showUserApp', ['application' => $application->id])}}'"><i class="far fa-clipboard"></i> Review</button>
</td> </td>
@endforeach @endforeach

View File

@ -109,7 +109,7 @@
</td> </td>
<td> <td>
<button type="button" class="btn btn-success" onclick="window.location.href='{{route('showUserApp', ['id' => $application->id])}}'"><i class="fa fa-eye"></i> View</button> <button type="button" class="btn btn-success" onclick="window.location.href='{{route('showUserApp', ['application' => $application->id])}}'"><i class="fa fa-eye"></i> View</button>
</td> </td>
</tr> </tr>

View File

@ -38,7 +38,7 @@
<x-modal id="notes" modal-label="notes" modal-title="Shared Notepad" include-close-button="true"> <x-modal id="notes" modal-label="notes" modal-title="Shared Notepad" include-close-button="true">
<form id="meetingNotes" method="POST" action="{{route('saveNotes', ['applicationID' => $application->id])}}"> <form id="meetingNotes" method="POST" action="{{route('saveNotes', ['application' => $application->id])}}">
@csrf @csrf
@method('PATCH') @method('PATCH')
<textarea name="noteText" rows="5" class="form-control">{{$application->appointment->meetingNotes ?? 'There are no notes yet. Add some!'}}</textarea> <textarea name="noteText" rows="5" class="form-control">{{$application->appointment->meetingNotes ?? 'There are no notes yet. Add some!'}}</textarea>
@ -62,7 +62,7 @@
<x-slot name="modalFooter"> <x-slot name="modalFooter">
<form id="updateApplication" action="{{route('updateApplicationStatus', ['id' => $application->id, 'newStatus' => 'deny'])}}" method="POST"> <form id="updateApplication" action="{{route('updateApplicationStatus', ['application' => $application->id, 'newStatus' => 'deny'])}}" method="POST">
@csrf @csrf
@method('PATCH') @method('PATCH')
<button type="submit" class="btn btn-danger">Confirm: Deny Applicant</button> <button type="submit" class="btn btn-danger">Confirm: Deny Applicant</button>
@ -200,7 +200,7 @@
</div> </div>
<div class="col"> <div class="col">
<form method="POST" action="{{route('updateApplicationStatus', ['id' => $application->id, 'newStatus' => 'interview'])}}"> <form method="POST" action="{{route('updateApplicationStatus', ['application' => $application->id, 'newStatus' => 'interview'])}}">
@csrf @csrf
@method('PATCH') @method('PATCH')
<button type="submit" class="btn btn-success" {{($application->applicationStatus == 'DENIED') ? 'disabled' : ''}}><i class="fas fa-arrow-right" ></i> Move to next stage</button> <button type="submit" class="btn btn-success" {{($application->applicationStatus == 'DENIED') ? 'disabled' : ''}}><i class="fas fa-arrow-right" ></i> Move to next stage</button>
@ -230,7 +230,7 @@
</x-slot> </x-slot>
<form id="scheduleAppointment" action="{{route('scheduleAppointment', ['applicationID' => $application->id])}}" method="POST"> <form id="scheduleAppointment" action="{{route('scheduleAppointment', ['application' => $application->id])}}" method="POST">
@csrf @csrf
@ -286,7 +286,7 @@
<x-slot name="cardFooter"> <x-slot name="cardFooter">
@can('appointments.schedule.edit') @can('appointments.schedule.edit')
<form style="white-space: nowrap;display:inline-block" class="footer-button" action="{{route('updateAppointment', ['applicationID' => $application->id, 'status' => 'concluded'])}}" method="POST"> <form style="white-space: nowrap;display:inline-block" class="footer-button" action="{{route('updateAppointment', ['application' => $application->id, 'status' => 'concluded'])}}" method="POST">
@csrf @csrf
@method('PATCH') @method('PATCH')
<button type="submit" class="btn btn-success">Finish Meeting</button> <button type="submit" class="btn btn-success">Finish Meeting</button>
@ -322,12 +322,12 @@
@if($canVote) @if($canVote)
<form class="d-inline-block" method="POST" action="{{route('voteApplication', ['id' => $application->id])}}"> <form class="d-inline-block" method="POST" action="{{route('voteApplication', ['application' => $application->id])}}">
@csrf @csrf
<input type="hidden" name="voteType" value="VOTE_APPROVE"> <input type="hidden" name="voteType" value="VOTE_APPROVE">
<button type="submit" class="btn btn-sm btn-warning">Vote: Approve Applicant</button> <button type="submit" class="btn btn-sm btn-warning">Vote: Approve Applicant</button>
</form> </form>
<form class="d-inline-block" method="POST" action="{{route('voteApplication', ['id' => $application->id])}}"> <form class="d-inline-block" method="POST" action="{{route('voteApplication', ['application' => $application->id])}}">
@csrf @csrf
<input type="hidden" name="voteType" value="VOTE_DENY"> <input type="hidden" name="voteType" value="VOTE_DENY">
<button type="submit" class="btn btn-sm btn-warning">Vote: Deny Applicant</button> <button type="submit" class="btn btn-sm btn-warning">Vote: Deny Applicant</button>

View File

@ -270,7 +270,7 @@
<div class="md-form"> <div class="md-form">
<textarea rows="3" name="message" id="message" class="md-textarea form-control"></textarea> <textarea rows="3" name="msg" id="message" class="md-textarea form-control"></textarea>
</div> </div>

View File

@ -40,7 +40,7 @@ Route::group(['middleware' => ['auth', 'forcelogout']], function(){
->name('showUserApps') ->name('showUserApps')
->middleware('eligibility'); ->middleware('eligibility');
Route::get('/view/{id}', 'ApplicationController@showUserApp') Route::get('/view/{application}', 'ApplicationController@showUserApp')
->name('showUserApp'); ->name('showUserApp');
Route::post('/{application}/comments', 'CommentController@insert') Route::post('/{application}/comments', 'CommentController@insert')
@ -54,7 +54,7 @@ Route::group(['middleware' => ['auth', 'forcelogout']], function(){
->name('saveNotes'); ->name('saveNotes');
Route::patch('/update/{id}/{newStatus}', 'ApplicationController@updateApplicationStatus') Route::patch('/update/{application}/{newStatus}', 'ApplicationController@updateApplicationStatus')
->name('updateApplicationStatus'); ->name('updateApplicationStatus');
Route::delete('{application}/delete', 'ApplicationController@delete') Route::delete('{application}/delete', 'ApplicationController@delete')
@ -78,7 +78,7 @@ Route::group(['middleware' => ['auth', 'forcelogout']], function(){
Route::post('{id}/staff/vote', 'VoteController@vote') Route::post('{application}/staff/vote', 'VoteController@vote')
->name('voteApplication'); ->name('voteApplication');
@ -86,10 +86,10 @@ Route::group(['middleware' => ['auth', 'forcelogout']], function(){
Route::group(['prefix' => 'appointments'], function (){ Route::group(['prefix' => 'appointments'], function (){
Route::post('schedule/appointments/{applicationID}', 'AppointmentController@saveAppointment') Route::post('schedule/appointments/{application}', 'AppointmentController@saveAppointment')
->name('scheduleAppointment'); ->name('scheduleAppointment');
Route::patch('update/appointments/{applicationID}/{status}', 'AppointmentController@updateAppointment') Route::patch('update/appointments/{application}/{status}', 'AppointmentController@updateAppointment')
->name('updateAppointment'); ->name('updateAppointment');
}); });
@ -156,6 +156,8 @@ Route::group(['middleware' => ['auth', 'forcelogout']], function(){
Route::delete('players/unban/{user}', 'BanController@delete') Route::delete('players/unban/{user}', 'BanController@delete')
->name('unbanUser'); ->name('unbanUser');
Route::delete('players/delete/{user}', 'UserController@delete') Route::delete('players/delete/{user}', 'UserController@delete')
->name('deleteUser'); ->name('deleteUser');
@ -178,7 +180,7 @@ Route::group(['middleware' => ['auth', 'forcelogout']], function(){
->name('updatePosition'); ->name('updatePosition');
Route::patch('positions/availability/{status}/{id}', 'VacancyController@updatePositionAvailability') Route::patch('positions/availability/{status}/{vacancy}', 'VacancyController@updatePositionAvailability')
->name('updatePositionAvailability'); ->name('updatePositionAvailability');
@ -214,5 +216,3 @@ Route::group(['middleware' => ['auth', 'forcelogout']], function(){
}); });
}); });
//Route::get('/dashboard/login', '');