From 8942623bde87b358c51dd72bca85042f839cdeab Mon Sep 17 00:00:00 2001 From: Miguel N Date: Sun, 25 Jul 2021 22:54:15 +0100 Subject: [PATCH] Added services This commit moves most controller logic onto Services. Services are part of the Service-Repository pattern. The models act as repositories. Services are easily testable and are needed for the upcoming API, in order to avoid duplicated code and to maintain a single source of "truth". The User, Vacancy and Vote controllers still need their logic moved onto services. --- .idea/hrm-mcserver.iml | 154 ---------------- .idea/modules.xml | 2 +- .../ApplicationNotFoundException.php | 11 ++ app/Exceptions/EmptyFormException.php | 10 ++ app/Exceptions/EmptyOptionsException.php | 10 ++ app/Exceptions/FailedCaptchaException.php | 10 ++ app/Exceptions/FileUploadException.php | 10 ++ .../FormHasConstraintsException.php | 10 ++ .../IncompleteApplicationException.php | 10 ++ .../InvalidAppointmentException.php | 10 ++ .../InvalidAppointmentStatusException.php | 10 ++ .../InvalidGamePreferenceException.php | 10 ++ app/Exceptions/InvalidInviteException.php | 10 ++ .../OptionCategoryNotFoundException.php | 10 ++ app/Exceptions/OptionNotFoundException.php | 10 ++ app/Exceptions/ProfileNotFoundException.php | 10 ++ app/Exceptions/PublicTeamInviteException.php | 10 ++ .../UnavailableApplicationException.php | 10 ++ .../UserAlreadyInvitedException.php | 10 ++ app/Exceptions/VacancyNotFoundException.php | 11 ++ app/Helpers/Options.php | 12 +- .../Controllers/ApplicationController.php | 147 ++++------------ .../Controllers/AppointmentController.php | 116 ++++++------ app/Http/Controllers/CommentController.php | 21 +-- app/Http/Controllers/ContactController.php | 49 ++---- app/Http/Controllers/DashboardController.php | 2 + app/Http/Controllers/FormController.php | 93 ++++------ app/Http/Controllers/HomeController.php | 2 + app/Http/Controllers/OptionsController.php | 86 ++++----- app/Http/Controllers/ProfileController.php | 46 ++--- .../SecuritySettingsController.php | 45 ++--- app/Http/Controllers/TeamController.php | 155 ++++++---------- app/Http/Controllers/TeamFileController.php | 67 +++---- app/Services/ApplicationService.php | 158 +++++++++++++++++ app/Services/AppointmentService.php | 90 ++++++++++ app/Services/CommentService.php | 27 +++ app/Services/ConfigurationService.php | 75 ++++++++ app/Services/ContactService.php | 47 +++++ app/Services/FormManagementService.php | 76 ++++++++ app/Services/MeetingNoteService.php | 36 ++++ app/Services/ProfileService.php | 50 ++++++ app/Services/SecuritySettingsService.php | 54 ++++++ app/Services/TeamFileService.php | 42 +++++ app/Services/TeamService.php | 165 ++++++++++++++++++ 44 files changed, 1308 insertions(+), 691 deletions(-) delete mode 100755 .idea/hrm-mcserver.iml create mode 100644 app/Exceptions/ApplicationNotFoundException.php create mode 100644 app/Exceptions/EmptyFormException.php create mode 100644 app/Exceptions/EmptyOptionsException.php create mode 100644 app/Exceptions/FailedCaptchaException.php create mode 100644 app/Exceptions/FileUploadException.php create mode 100644 app/Exceptions/FormHasConstraintsException.php create mode 100644 app/Exceptions/IncompleteApplicationException.php create mode 100644 app/Exceptions/InvalidAppointmentException.php create mode 100644 app/Exceptions/InvalidAppointmentStatusException.php create mode 100644 app/Exceptions/InvalidGamePreferenceException.php create mode 100644 app/Exceptions/InvalidInviteException.php create mode 100644 app/Exceptions/OptionCategoryNotFoundException.php create mode 100644 app/Exceptions/OptionNotFoundException.php create mode 100644 app/Exceptions/ProfileNotFoundException.php create mode 100644 app/Exceptions/PublicTeamInviteException.php create mode 100644 app/Exceptions/UnavailableApplicationException.php create mode 100644 app/Exceptions/UserAlreadyInvitedException.php create mode 100644 app/Exceptions/VacancyNotFoundException.php create mode 100644 app/Services/ApplicationService.php create mode 100644 app/Services/AppointmentService.php create mode 100644 app/Services/CommentService.php create mode 100644 app/Services/ConfigurationService.php create mode 100644 app/Services/ContactService.php create mode 100644 app/Services/FormManagementService.php create mode 100644 app/Services/MeetingNoteService.php create mode 100644 app/Services/ProfileService.php create mode 100644 app/Services/SecuritySettingsService.php create mode 100644 app/Services/TeamFileService.php create mode 100644 app/Services/TeamService.php diff --git a/.idea/hrm-mcserver.iml b/.idea/hrm-mcserver.iml deleted file mode 100755 index 0fd52b5..0000000 --- a/.idea/hrm-mcserver.iml +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index 1a22200..7ab5d51 100755 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/app/Exceptions/ApplicationNotFoundException.php b/app/Exceptions/ApplicationNotFoundException.php new file mode 100644 index 0000000..1fc29a9 --- /dev/null +++ b/app/Exceptions/ApplicationNotFoundException.php @@ -0,0 +1,11 @@ +get(); if ($options->isEmpty()) { - throw new \Exception('There are no options in category ' . $category); + throw new EmptyOptionsException('There are no options in category ' . $category); } return $options; } @@ -52,13 +54,13 @@ class Options public function getOption(string $option): string { $value = Cache::get($option); - + if (is_null($value)) { Log::debug('Option '.$option.'not found in cache, refreshing from database'); $value = Option::where('option_name', $option)->first(); if (is_null($value)) { - throw new \Exception('This option does not exist.'); + throw new OptionNotFoundException('This option does not exist.'); } Cache::put($option, $value->option_value); Cache::put($option.'_desc', 'Undefined description'); @@ -118,7 +120,7 @@ class Options Cache::put('option_name', $newValue, now()->addDay()); } else { - throw new \Exception('This option does not exist.'); + throw new OptionNotFoundException('This option does not exist.'); } } diff --git a/app/Http/Controllers/ApplicationController.php b/app/Http/Controllers/ApplicationController.php index 1ec3400..e137503 100755 --- a/app/Http/Controllers/ApplicationController.php +++ b/app/Http/Controllers/ApplicationController.php @@ -22,35 +22,24 @@ namespace App\Http\Controllers; use App\Application; -use App\Events\ApplicationDeniedEvent; -use App\Facades\JSON; -use App\Notifications\ApplicationMoved; -use App\Notifications\NewApplicant; -use App\Response; -use App\User; -use App\Vacancy; -use ContextAwareValidator; +use App\Exceptions\IncompleteApplicationException; +use App\Exceptions\UnavailableApplicationException; +use App\Exceptions\VacancyNotFoundException; +use App\Services\ApplicationService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Log; class ApplicationController extends Controller { + private $applicationService; - private function canVote($votes): bool - { - $allvotes = collect([]); + public function __construct(ApplicationService $applicationService) { - foreach ($votes as $vote) { - if ($vote->userID == Auth::user()->id) { - $allvotes->push($vote); - } - } - - return !(($allvotes->count() == 1)); + $this->applicationService = $applicationService; } + public function showUserApps() { return view('dashboard.user.applications') @@ -70,7 +59,7 @@ class ApplicationController extends Controller 'structuredResponses' => json_decode($application->response->responseData, true), 'formStructure' => $application->response->form, 'vacancy' => $application->response->vacancy, - 'canVote' => $this->canVote($application->votes), + 'canVote' => $this->applicationService->canVote($application->votes), ] ); } else { @@ -90,91 +79,27 @@ class ApplicationController extends Controller } - public function renderApplicationForm(Request $request, $vacancySlug) + public function renderApplicationForm($vacancySlug) { - $vacancyWithForm = Vacancy::with('forms')->where('vacancySlug', $vacancySlug)->get(); - - $firstVacancy = $vacancyWithForm->first(); - - if (!$vacancyWithForm->isEmpty() && $firstVacancy->vacancyCount !== 0 && $firstVacancy->vacancyStatus == 'OPEN') { - return view('dashboard.application-rendering.apply') - ->with([ - 'vacancy' => $vacancyWithForm->first(), - 'preprocessedForm' => json_decode($vacancyWithForm->first()->forms->formStructure, true), - ]); - } else { - abort(404, __('The application you\'re looking for could not be found or it is currently unavailable.')); - } + return $this->applicationService->renderForm($vacancySlug); } public function saveApplicationAnswers(Request $request, $vacancySlug) { - $vacancy = Vacancy::with('forms')->where('vacancySlug', $vacancySlug)->get(); + try { - if ($vacancy->isEmpty()) { + $this->applicationService->fillForm(Auth::user(), $request->all(), $vacancySlug); + + } catch (VacancyNotFoundException | IncompleteApplicationException | UnavailableApplicationException $e) { return redirect() ->back() - ->with('error', __('This vacancy doesn\'t exist; Please use the proper buttons to apply to one.')); + ->with('error', $e->getMessage()); } - if ($vacancy->first()->vacancyCount == 0 || $vacancy->first()->vacancyStatus !== 'OPEN') { - - return redirect() - ->back() - ->with('error', __('This application is unavailable')); - } - - Log::info('Processing new application!'); - - $formStructure = json_decode($vacancy->first()->forms->formStructure, true); - $responseValidation = ContextAwareValidator::getResponseValidator($request->all(), $formStructure); - $applicant = Auth::user(); - - // API users may specify ID 1 for an anonymous application, but they'll have to submit contact details for it to become active. - // User ID 1 is exempt from application rate limits - - Log::info('Built response & validator structure!'); - - if (!$responseValidation->get('validator')->fails()) { - $response = Response::create([ - '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 - 'responseData' => $responseValidation->get('responseStructure'), - ]); - - Log::info('Registered form response!', [ - 'applicant' => $applicant->name, - 'vacancy' => $vacancy->first()->vacancyName - ]); - - $application = Application::create([ - 'applicantUserID' => $applicant->id, - 'applicantFormResponseID' => $response->id, - 'applicationStatus' => 'STAGE_SUBMITTED', - ]); - - Log::info('Submitted an application!', [ - 'responseID' => $response->id, - 'applicant' => $applicant->name - ]); - - foreach (User::all() as $user) { - if ($user->hasRole('admin')) { - $user->notify((new NewApplicant($application, $vacancy->first()))->delay(now()->addSeconds(10))); - } - } - - $request->session()->flash('success', __('Thank you for your application! It will be reviewed as soon as possible.')); - return redirect(route('showUserApps')); - - } - - Log::warning('Application form for ' . $applicant->name . ' contained errors, resetting!'); - return redirect() ->back() - ->with('error', __('There are one or more errors in your application. Please make sure none of your fields are empty, since they are all required.')); + ->with('success', __('Thank you! Your application has been processed and our team will get to it shortly.')); } public function updateApplicationStatus(Request $request, Application $application, $newStatus) @@ -182,36 +107,28 @@ class ApplicationController extends Controller $messageIsError = false; $this->authorize('update', Application::class); - - switch ($newStatus) { - case 'deny': - - event(new ApplicationDeniedEvent($application)); - $message = __("Application denied successfully."); - - break; - - case 'interview': - Log::info(' Moved application ID ' . $application->id . 'to interview stage!'); - $message = __('Application moved to interview stage!'); - - $application->setStatus('STAGE_INTERVIEW'); - $application->user->notify(new ApplicationMoved()); - - break; - - default: - $message = __("There are no suitable statuses to update to."); - $messageIsError = true; + try { + $status = $this->applicationService->updateStatus($application, $newStatus); + } catch (\LogicException $ex) + { + return redirect() + ->back() + ->with('error', $ex->getMessage()); } - return redirect()->back(); + return redirect() + ->back() + ->with('success', $status); } + /** + * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws \Exception + */ public function delete(Request $request, Application $application) { $this->authorize('delete', $application); - $application->delete(); // observers will run, cleaning it up + $this->applicationService->delete($application); return redirect() ->back() diff --git a/app/Http/Controllers/AppointmentController.php b/app/Http/Controllers/AppointmentController.php index e1d1858..18efe1d 100755 --- a/app/Http/Controllers/AppointmentController.php +++ b/app/Http/Controllers/AppointmentController.php @@ -23,85 +23,79 @@ namespace App\Http\Controllers; use App\Application; use App\Appointment; +use App\Exceptions\InvalidAppointmentException; +use App\Exceptions\InvalidAppointmentStatusException; use App\Http\Requests\SaveNotesRequest; -use App\Notifications\ApplicationMoved; -use App\Notifications\AppointmentScheduled; +use App\Services\AppointmentService; +use App\Services\MeetingNoteService; use Carbon\Carbon; +use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Log; class AppointmentController extends Controller { - private $allowedPlatforms = [ - 'ZOOM', - 'DISCORD', - 'SKYPE', - 'MEET', - 'TEAMSPEAK', + private $appointmentService; + private $meetingNoteService; - ]; - public function saveAppointment(Request $request, Application $application) - { - $this->authorize('create', Appointment::class); - $appointmentDate = Carbon::parse($request->appointmentDateTime); + public function __construct(AppointmentService $appointmentService, MeetingNoteService $meetingNoteService) { - $appointment = Appointment::create([ - 'appointmentDescription' => $request->appointmentDescription, - 'appointmentDate' => $appointmentDate->toDateTimeString(), - 'applicationID' => $application->id, - 'appointmentLocation' => (in_array($request->appointmentLocation, $this->allowedPlatforms)) ? $request->appointmentLocation : 'DISCORD', - ]); - $application->setStatus('STAGE_INTERVIEW_SCHEDULED'); - - Log::info('User '.Auth::user()->name.' has scheduled an appointment with '.$application->user->name.' for application ID'.$application->id, [ - 'datetime' => $appointmentDate->toDateTimeString(), - 'scheduled' => now(), - ]); - - $application->user->notify(new AppointmentScheduled($appointment)); - $request->session()->flash('success', __('Appointment successfully scheduled @ :appointmentTime', ['appointmentTime', $appointmentDate->toDateTimeString()])); - - return redirect()->back(); + $this->appointmentService = $appointmentService; + $this->meetingNoteService = $meetingNoteService; } - public function updateAppointment(Request $request, Application $application, $status) + public function saveAppointment(Request $request, Application $application): RedirectResponse + { + $this->authorize('create', Appointment::class); + + $appointmentDate = Carbon::parse($request->appointmentDateTime); + $this->appointmentService->createAppointment($application, $appointmentDate, $request->appointmentDescription, $request->appointmentLocation); + + return redirect() + ->back() + ->with('success',__('Appointment successfully scheduled @ :appointmentTime', ['appointmentTime', $appointmentDate->toDateTimeString()])); + } + + /** + * @throws AuthorizationException + */ + public function updateAppointment(Application $application, $status): RedirectResponse { $this->authorize('update', $application->appointment); - $validStatuses = [ - 'SCHEDULED', - 'CONCLUDED', - ]; + try { + $this->appointmentService->updateAppointment($application, $status); - // NOTE: This is a little confusing, refactor - $application->appointment->appointmentStatus = (in_array($status, $validStatuses)) ? strtoupper($status) : 'SCHEDULED'; - $application->appointment->save(); + return redirect() + ->back() + ->with('success', __("Interview finished! Staff members can now vote on it.")); - $application->setStatus('STAGE_PEERAPPROVAL'); - $application->user->notify(new ApplicationMoved()); - - $request->session()->flash('success', __('Interview finished! Staff members can now vote on it.')); - - return redirect()->back(); - } - - // also updates - public function saveNotes(SaveNotesRequest $request, Application $application) - { - if (! is_null($application)) { - $application->load('appointment'); - - $application->appointment->meetingNotes = $request->noteText; - $application->appointment->save(); - - $request->session()->flash('success', __('Meeting notes have been saved.')); - } else { - $request->session()->flash('error', __('There\'s no appointment to save notes to!')); + } + catch (InvalidAppointmentStatusException $ex) { + return redirect() + ->back() + ->with('error', $ex->getMessage()); } - return redirect()->back(); + + } + + public function saveNotes(SaveNotesRequest $request, Application $application) + { + try { + + $this->meetingNoteService->addToApplication($application, $request->noteText); + + return redirect() + ->back() + ->with('success', 'Saved notes.'); + + } catch (InvalidAppointmentException $ex) { + return redirect() + ->back() + ->with('error', $ex->getMessage()); + } } } diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php index ca2e9c6..609c0e3 100755 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -24,21 +24,22 @@ namespace App\Http\Controllers; use App\Application; use App\Comment; use App\Http\Requests\NewCommentRequest; +use App\Services\CommentService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class CommentController extends Controller { + private $commentService; + + public function __construct(CommentService $commentService) { + $this->commentService = $commentService; + } public function insert(NewCommentRequest $request, Application $application) { $this->authorize('create', Comment::class); - - $comment = Comment::create([ - 'authorID' => Auth::user()->id, - 'applicationID' => $application->id, - 'text' => $request->comment, - ]); + $comment = $this->commentService->addComment($application, $request->comment); if ($comment) { $request->session()->flash('success', __('Comment posted!')); @@ -52,10 +53,10 @@ class CommentController extends Controller public function delete(Request $request, Comment $comment) { $this->authorize('delete', $comment); + $this->commentService->deleteComment($comment); - $comment->delete(); - $request->session()->flash('success', __('Comment deleted!')); - - return redirect()->back(); + return redirect() + ->back() + ->with('success', __('Comment deleted!')); } } diff --git a/app/Http/Controllers/ContactController.php b/app/Http/Controllers/ContactController.php index 43c9e99..b081aee 100755 --- a/app/Http/Controllers/ContactController.php +++ b/app/Http/Controllers/ContactController.php @@ -21,7 +21,9 @@ namespace App\Http\Controllers; +use App\Exceptions\FailedCaptchaException; use App\Notifications\NewContact; +use App\Services\ContactService; use App\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Http; @@ -30,47 +32,32 @@ class ContactController extends Controller { protected $users; - public function __construct(User $users) + private $contactService; + + public function __construct(User $users, ContactService $contactService) { + $this->contactService = $contactService; $this->users = $users; } public function create(Request $request) { - $name = $request->name; - $email = $request->email; - $subject = $request->subject; - $msg = $request->msg; + try { - $challenge = $request->input('captcha'); + $email = $request->email; + $msg = $request->msg; + $challenge = $request->input('captcha'); - // TODO: now: add middleware for this verification, move to invisible captcha - $verifyrequest = Http::asForm()->post(config('recaptcha.verify.apiurl'), [ - 'secret' => config('recaptcha.keys.secret'), - 'response' => $challenge, - 'remoteip' => $request->ip(), - ]); + $this->contactService->sendMessage($request->ip(), $msg, $email, $challenge); - $response = json_decode($verifyrequest->getBody(), true); + return redirect() + ->back() + ->with('success',__('Message sent successfully! We usually respond within 48 hours.')); - if (! $response['success']) { - $request->session()->flash('error', __('Beep beep boop... Robot? Submission failed.')); - - return redirect()->back(); + } catch (FailedCaptchaException $ex) { + return redirect() + ->back() + ->with('error', $ex->getMessage()); } - - 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.')); - - return redirect()->back(); } } diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 573ae61..df46bd2 100755 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -27,6 +27,8 @@ use App\Vacancy; class DashboardController extends Controller { + // Note: The dashboard doesn't need a service because it doesn't contain any significant business logic + public function index() { $totalPeerReview = Application::where('applicationStatus', 'STAGE_PEERAPPROVAL')->get()->count(); diff --git a/app/Http/Controllers/FormController.php b/app/Http/Controllers/FormController.php index 7ccf9f6..82848fe 100755 --- a/app/Http/Controllers/FormController.php +++ b/app/Http/Controllers/FormController.php @@ -21,12 +21,20 @@ namespace App\Http\Controllers; +use App\Exceptions\FormHasConstraintsException; use App\Form; +use App\Services\FormManagementService; use ContextAwareValidator; use Illuminate\Http\Request; class FormController extends Controller { + private $formService; + + public function __construct(FormManagementService $formService) { + $this->formService = $formService; + } + public function index() { $forms = Form::all(); @@ -45,60 +53,38 @@ class FormController extends Controller public function saveForm(Request $request) { - $this->authorize('create', Form::class); - $fields = $request->all(); + $form = $this->formService->addForm($request->all()); - if (count($fields) == 2) { - // form is probably empty, since forms with fields will alawys have more than 2 items - - $request->session()->flash('error', __('Sorry, but you may not create empty forms.')); - - return redirect()->to(route('showForms')); + // Form is boolean or array + if ($form) + { + return redirect() + ->back() + ->with('success', __('Form created!')); } - $contextValidation = ContextAwareValidator::getValidator($fields, true, true); - - if (! $contextValidation->get('validator')->fails()) { - $storableFormStructure = $contextValidation->get('structure'); - - Form::create( - [ - 'formName' => $fields['formName'], - 'formStructure' => $storableFormStructure, - 'formStatus' => 'ACTIVE', - ] - ); - - $request->session()->flash('success', __('Form created! You can now link this form to a vacancy.')); - - return redirect()->to(route('showForms')); - } - - $request->session()->flash('errors', $contextValidation->get('validator')->errors()->getMessages()); - - return redirect()->back(); + return redirect() + ->back() + ->with('errors', $form); } public function destroy(Request $request, Form $form) { $this->authorize('delete', $form); + try { - $deletable = true; + $this->formService->deleteForm($form); + return redirect() + ->back() + ->with('success', __('Form deleted successfuly')); + + } catch (FormHasConstraintsException $ex) { + + return redirect() + ->back() + ->with('error', $ex->getMessage()); - if (! is_null($form) && ! is_null($form->vacancies) && $form->vacancies->count() !== 0 || ! is_null($form->responses)) { - $deletable = false; } - - if ($deletable) { - $form->delete(); - - $request->session()->flash('success', __('Form deleted successfully.')); - } else { - $request->session()->flash('error', __('You cannot delete this form because it\'s tied to one or more applications and ranks, or because it doesn\'t exist.')); - } - - return redirect()->back(); - } public function preview(Request $request, Form $form) @@ -124,22 +110,15 @@ class FormController extends Controller public function update(Request $request, Form $form) { $this->authorize('update', $form); + $updatedForm = $this->formService->updateForm($form, $request->all()); - $contextValidation = ContextAwareValidator::getValidator($request->all(), true); - $this->authorize('update', $form); - - if (! $contextValidation->get('validator')->fails()) { - // Add the new structure into the form. New, subsquent fields will be identified by the "new" prefix - // This prefix doesn't actually change the app's behavior when it receives applications. - // Additionally, old applications won't of course display new and updated fields, because we can't travel into the past and get data for them - $form->formStructure = $contextValidation->get('structure'); - $form->save(); - - $request->session()->flash('success', __('Hooray! Your form was updated. New applications for it\'s vacancy will use it.')); - } else { - $request->session()->flash('errors', $contextValidation->get('validator')->errors()->getMessages()); + if ($updatedForm instanceof Form) { + return redirect()->to(route('previewForm', ['form' => $updatedForm->id])); } - return redirect()->to(route('previewForm', ['form' => $form->id])); + // array of errors + return redirect() + ->back() + ->with('errors', $updatedForm); } } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 2696b59..9293dc9 100755 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -25,6 +25,8 @@ use App\Vacancy; class HomeController extends Controller { + // doesn't need a service, because it doesn't contain major logic. + /** * Show the application dashboard. * diff --git a/app/Http/Controllers/OptionsController.php b/app/Http/Controllers/OptionsController.php index 1189301..10b03e7 100755 --- a/app/Http/Controllers/OptionsController.php +++ b/app/Http/Controllers/OptionsController.php @@ -21,14 +21,25 @@ namespace App\Http\Controllers; +use App\Exceptions\InvalidGamePreferenceException; +use App\Exceptions\OptionNotFoundException; use App\Facades\Options; use App\Options as Option; +use App\Services\ConfigurationService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; class OptionsController extends Controller { + private $configurationService; + + public function __construct(ConfigurationService $configurationService) { + + $this->configurationService = $configurationService; + + } + /** * Display a listing of the resource. * @@ -36,7 +47,7 @@ class OptionsController extends Controller */ public function index() { - + // TODO: Replace with settings package return view('dashboard.administration.settings') ->with([ 'options' => Options::getCategory('notifications'), @@ -51,65 +62,44 @@ class OptionsController extends Controller ]); } - public function saveSettings(Request $request) + public function saveSettings(Request $request): \Illuminate\Http\RedirectResponse { - if (Auth::user()->can('admin.settings.edit')) { - Log::debug('Updating application options', [ - 'ip' => $request->ip(), - 'ua' => $request->userAgent(), - 'username' => Auth::user()->username, - ]); - foreach ($request->all() as $optionName => $option) { - try { - Log::debug('Going through option '.$optionName); - if (Options::optionExists($optionName)) { - Log::debug('Option exists, updating to new values', [ - 'opt' => $optionName, - 'new_value' => $option, - ]); - Options::changeOption($optionName, $option); - } - } catch (\Exception $ex) { - Log::error('Unable to update options!', [ - 'msg' => $ex->getMessage(), - 'trace' => $ex->getTraceAsString(), - ]); - report($ex); + try { - $errorCond = true; - $request->session()->flash('error', __('An error occurred while trying to save settings: :message ', ['message' => $ex->getMessage()])); - } + if (Auth::user()->can('admin.settings.edit')) { + $this->configurationService->saveConfiguration($request->all()); + + return redirect() + ->back() + ->with('success', __('Options updated successfully!')); } - if (! isset($errorCond)) { - $request->session()->flash('success', __('Settings saved successfully!')); - } - } else { - $request->session()->flash('error', __('You do not have permission to update this resource.')); + } catch (OptionNotFoundException | \Exception $ex) { + + return redirect() + ->back() + ->with('error', $ex->getMessage()); + } - return redirect()->back(); + return redirect() + ->back() + ->with('error', __('You do not have permission to update this resource.')); } public function saveGameIntegration(Request $request) { - $supportedGames = [ - 'RUST', - 'MINECRAFT', - 'SE', - 'GMOD' - ]; + try { - if (!is_null($request->gamePref) && in_array($request->gamePref, $supportedGames)) - { - Options::changeOption('currentGame', $request->gamePref); - $request->session()->flash('success', __('Updated current game.')); + $this->configurationService->saveGameIntegration($request->gamePref); + return redirect() + ->back() + ->with('success', __('Game preference updated.')); - return redirect()->back(); + } catch (InvalidGamePreferenceException $ex) { + return redirect() + ->back() + ->with('error', $ex->getMessage()); } - - $request->session()->flash('error', __('Unsupported game :game.', ['game' => $request->gamePref ])); - - return redirect()->back(); } } diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index aba3e8b..d77b028 100755 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -23,6 +23,7 @@ namespace App\Http\Controllers; use App\Facades\IP; use App\Http\Requests\ProfileSave; +use App\Services\ProfileService; use App\User; use Carbon\Carbon; use Illuminate\Http\Request; @@ -31,6 +32,12 @@ use Spatie\Permission\Models\Role; class ProfileController extends Controller { + private $profileService; + + public function __construct(ProfileService $profileService) { + $this->profileService = $profileService; + } + public function index() { return view('dashboard.user.directory') @@ -39,6 +46,7 @@ class ProfileController extends Controller public function showProfile() { + // TODO: Come up with cleaner social media solution, e.g. social media object $socialLinks = Auth::user()->profile->socialLinks ?? '[]'; $socialMediaProfiles = json_decode($socialLinks, true); @@ -52,8 +60,7 @@ class ProfileController extends Controller ]); } - // Route model binding - public function showSingleProfile(Request $request, User $user) + public function showSingleProfile(User $user) { $socialMediaProfiles = json_decode($user->profile->socialLinks, true); $createdDate = Carbon::parse($user->created_at); @@ -102,36 +109,9 @@ class ProfileController extends Controller public function saveProfile(ProfileSave $request) { - $profile = User::find(Auth::user()->id)->profile; - $social = []; - - if (! is_null($profile)) { - switch ($request->avatarPref) { - case 'MOJANG': - $avatarPref = 'crafatar'; - - break; - case 'GRAVATAR': - $avatarPref = strtolower($request->avatarPref); - - break; - } - - $social['links']['github'] = $request->socialGithub; - $social['links']['twitter'] = $request->socialTwitter; - $social['links']['insta'] = $request->socialInsta; - $social['links']['discord'] = $request->socialDiscord; - - $profile->profileShortBio = $request->shortBio; - $profile->profileAboutMe = $request->aboutMe; - $profile->avatarPreference = $avatarPref; - $profile->socialLinks = json_encode($social); - - $newProfile = $profile->save(); - - $request->session()->flash('success', __('Profile settings saved successfully.')); - } - - return redirect()->back(); + $this->profileService->updateProfile(Auth::user()->id, $request); + return redirect() + ->back() + ->with('success', __('Profile updated.')); } } diff --git a/app/Http/Controllers/SecuritySettingsController.php b/app/Http/Controllers/SecuritySettingsController.php index febe168..fbe8e7d 100644 --- a/app/Http/Controllers/SecuritySettingsController.php +++ b/app/Http/Controllers/SecuritySettingsController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers; use App\Facades\Options; use App\Http\Requests\SaveSecuritySettings; +use App\Services\SecuritySettingsService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; @@ -11,38 +12,24 @@ use function PHPSTORM_META\map; class SecuritySettingsController extends Controller { + private $securityService; + + public function __construct(SecuritySettingsService $securityService) { + $this->securityService = $securityService; + } + public function save(SaveSecuritySettings $request) { - $validPolicies = [ - 'off', - 'low', - 'medium', - 'high' - ]; + $this->securityService->save($request->secPolicy, [ + 'graceperiod' => $request->graceperiod, + 'pwExpiry' => $request->pwExpiry, + 'enforce2fa' => $request->enforce2fa, + 'requirePMC' => $request->requirePMC + ]); - if (in_array($request->secPolicy, $validPolicies)) - { - Options::changeOption('pw_security_policy', $request->secPolicy); - - Log::debug('[Options] Changing option pw_security_policy', [ - 'new_value' => $request->secPolicy - ]); - } - else - { - Log::debug('[WARN] Ignoring bogus policy', [ - 'avaliable' => $validPolicies, - 'given' >= $request->secPolicy - ]); - } - - Options::changeOption('graceperiod', $request->graceperiod); - Options::changeOption('password_expiry', $request->pwExpiry); - Options::changeOption('force2fa', $request->enforce2fa); - Options::changeOption('requireGameLicense', $request->requirePMC); - - $request->session()->flash('success', __('Settings saved successfully.')); - return redirect()->back(); + return redirect() + ->back() + ->with('success', __('Settings saved.')); } } diff --git a/app/Http/Controllers/TeamController.php b/app/Http/Controllers/TeamController.php index bbcb1f6..91565f7 100755 --- a/app/Http/Controllers/TeamController.php +++ b/app/Http/Controllers/TeamController.php @@ -21,13 +21,18 @@ namespace App\Http\Controllers; +use App\Exceptions\InvalidInviteException; +use App\Exceptions\PublicTeamInviteException; +use App\Exceptions\UserAlreadyInvitedException; use App\Http\Requests\EditTeamRequest; use App\Http\Requests\NewTeamRequest; use App\Http\Requests\SendInviteRequest; use App\Mail\InviteToTeam; +use App\Services\TeamService; use App\Team; use App\User; use App\Vacancy; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Mail; @@ -37,10 +42,15 @@ use Mpociot\Teamwork\TeamInvite; class TeamController extends Controller { + private $teamService; + + public function __construct(TeamService $teamService) { + $this->teamService = $teamService; + } + /** * Display a listing of the resource. * - * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\Response */ public function index() { @@ -56,23 +66,17 @@ class TeamController extends Controller * Store a newly created resource in storage. * * @param NewTeamRequest $request - * @return \Illuminate\Http\RedirectResponse + * @return RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function store(NewTeamRequest $request) { $this->authorize('create', Team::class); + $this->teamService->createTeam($request->teamName, Auth::user()->id); - $team = Team::create([ - 'name' => $request->teamName, - 'owner_id' => Auth::user()->id, - ]); - - Auth::user()->teams()->attach($team->id); - - $request->session()->flash('success', __('Team successfully created.')); - - return redirect()->back(); + return redirect() + ->back() + ->with('success', __('Team successfully created.')); } /** @@ -98,21 +102,24 @@ class TeamController extends Controller * * @param EditTeamRequest $request * @param Team $team - * @return \Illuminate\Http\Response + * @return RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ - public function update(EditTeamRequest $request, Team $team): \Illuminate\Http\Response + public function update(EditTeamRequest $request, Team $team): RedirectResponse { $this->authorize('update', $team); + $team = $this->teamService->updateTeam($team, $request->teamDescription, $team->joinType); - $team->description = $request->teamDescription; - $team->openJoin = $request->joinType; + if ($team) { + return redirect() + ->to(route('teams.index')) + ->with('success', __('Team updated.')); + } - $team->save(); - $request->session()->flash('success', __('Team edited successfully.')); - - return redirect()->to(route('teams.index')); + return redirect() + ->back() + ->with('error', __('An error ocurred while trying to update this team.')); } /** @@ -126,68 +133,45 @@ class TeamController extends Controller // wip } - public function invite(SendInviteRequest $request, Team $team): \Illuminate\Http\RedirectResponse + public function invite(SendInviteRequest $request, Team $team): RedirectResponse { $this->authorize('invite', $team); - $user = User::findOrFail($request->user); + try { - if (! $team->openJoin) { - if (! Teamwork::hasPendingInvite($user->email, $team)) { - Teamwork::inviteToTeam($user, $team, function (TeamInvite $invite) use ($user) { - Mail::to($user)->send(new InviteToTeam($invite)); - }); + $this->teamService->inviteUser($team, $request->user); - $request->session()->flash('success', __('Invite sent! They can now accept or deny it.')); - } else { - $request->session()->flash('error', __('This user has already been invited.')); - } - } else { - $request->session()->flash('error', __('You can\'t invite users to public teams.')); + return redirect() + ->back() + ->with('success', __('User invited successfully!')); + + } catch (UserAlreadyInvitedException | PublicTeamInviteException $ex) { + return redirect() + ->back() + ->with('error', $ex->getMessage()); } - - return redirect()->back(); } - public function processInviteAction(Request $request, $action, $token): \Illuminate\Http\RedirectResponse + public function processInviteAction(Request $request, $action, $token): RedirectResponse { - switch ($action) { - case 'accept': + try { - $invite = Teamwork::getInviteFromAcceptToken($token); + $this->teamService->processInvite(Auth::user(), $action, $token); - if ($invite && $invite->user->is(Auth::user())) { - Teamwork::acceptInvite($invite); - $request->session()->flash('success', __('Invite accepted! You have now joined :teamName.', ['teamName' => $invite->team->name])); - } else { - $request->session()->flash('error', __('Invalid or expired invite URL.')); - } + return redirect() + ->to(route('teams.index')) + ->with('success', __('Invite processed successfully!')); - break; + } catch (InvalidInviteException $e) { - case 'deny': - - $invite = Teamwork::getInviteFromDenyToken($token); - - if ($invite && $invite->user->is(Auth::user())) { - Teamwork::denyInvite($invite); - $request->session()->flash('success', __('Invite denied! Ask for another invite if this isn\'t what you meant.')); - } else { - $request->session()->flash('error', __('Invalid or expired invite URL.')); - } - - break; - - default: - $request->session()->flash('error', 'Sorry, but the invite URL you followed was malformed. Try asking for another invite, or submit a bug report.'); + return redirect() + ->back() + ->with('error', $e->getMessage()); } - - // This page will show the user's current teams - return redirect()->to(route('teams.index')); } - public function switchTeam(Request $request, Team $team): \Illuminate\Http\RedirectResponse + public function switchTeam(Request $request, Team $team): RedirectResponse { $this->authorize('switchTeam', $team); @@ -203,44 +187,13 @@ class TeamController extends Controller } // Since it's a separate form, we shouldn't use the same update method - public function assignVacancies(Request $request, Team $team): \Illuminate\Http\RedirectResponse + public function assignVacancies(Request $request, Team $team): RedirectResponse { $this->authorize('update', $team); + $message = $this->teamService->updateVacancies($team, $request->assocVacancies); - // P.S. To future developers - // This method gave me a lot of trouble lol. It's hard to write code when you're half asleep. - // There may be an n+1 query in the view and I don't think there's a way to avoid that without writing a lot of extra code. - - $requestVacancies = $request->assocVacancies; - $currentVacancies = $team->vacancies->pluck('id')->all(); - - if (is_null($requestVacancies)) { - foreach ($team->vacancies as $vacancy) { - $team->vacancies()->detach($vacancy->id); - } - - $request->session()->flash('success', __('Removed all vacancy associations.')); - - return redirect()->back(); - } - - $vacancyDiff = array_diff($requestVacancies, $currentVacancies); - $deselectedDiff = array_diff($currentVacancies, $requestVacancies); - - if (! empty($vacancyDiff) || ! empty($deselectedDiff)) { - foreach ($vacancyDiff as $selectedVacancy) { - $team->vacancies()->attach($selectedVacancy); - } - - foreach ($deselectedDiff as $deselectedVacancy) { - $team->vacancies()->detach($deselectedVacancy); - } - } else { - $team->vacancies()->attach($requestVacancies); - } - - $request->session()->flash('success', __('Assignments changed successfully.')); - - return redirect()->back(); + return redirect() + ->back() + ->with('success', $message); } } diff --git a/app/Http/Controllers/TeamFileController.php b/app/Http/Controllers/TeamFileController.php index 56b6786..b6fbdc6 100755 --- a/app/Http/Controllers/TeamFileController.php +++ b/app/Http/Controllers/TeamFileController.php @@ -5,6 +5,8 @@ namespace App\Http\Controllers; // Most of these namespaces have no effect on the code, however, they're used by IDEs so they can resolve return types and for PHPDocumentor as well +use App\Exceptions\FileUploadException; +use App\Services\TeamFileService; use App\TeamFile; use App\Http\Requests\UploadFileRequest; @@ -24,11 +26,16 @@ use Illuminate\Http\Response; class TeamFileController extends Controller { + private $fileService; + + public function __construct(TeamFileService $fileService) { + $this->fileService = $fileService; + } + /** * Display a listing of the resource. * * @param Request $request - * @return Application|Factory|View|Response */ public function index(Request $request) { @@ -55,33 +62,24 @@ class TeamFileController extends Controller { $this->authorize('store', TeamFile::class); - $upload = $request->file('file'); + try { + $caption = $request->caption; + $description = $request->description; - $file = $upload->store('uploads'); - $originalFileName = $upload->getClientOriginalName(); - $originalFileExtension = $upload->extension(); - $originalFileSize = $upload->getSize(); + $this->fileService->addFile($request->file('file'), Auth::user()->id, Auth::user()->currentTeam->id, $caption, $description); - $fileEntry = TeamFile::create([ - 'uploaded_by' => Auth::user()->id, - 'team_id' => Auth::user()->currentTeam->id, - 'name' => $originalFileName, - 'caption' => $request->caption, - 'description' => $request->description, - 'fs_location' => $file, - 'extension' => $originalFileExtension, - 'size' => $originalFileSize - ]); + return redirect() + ->back() + ->with('success', __('File uploaded successfully.')); + + } catch (FileUploadException $uploadException) { + + return redirect() + ->back() + ->with('error', $uploadException->getMessage()); - if ($fileEntry && !is_bool($file)) - { - $request->session()->flash('success', 'File uploaded successfully!'); - return redirect()->back(); } - $request->session()->flash('error', 'There was an unknown error whilst trying to upload your file.'); - return redirect()->back(); - } @@ -101,29 +99,6 @@ class TeamFileController extends Controller } } - /** - * Show the form for editing the specified resource. - * - * @param \App\TeamFile $teamFile - * @return Response - */ - public function edit(TeamFile $teamFile) - { - // - } - - /** - * Update the specified resource in storage. - * - * @param \Illuminate\Http\Request $request - * @param \App\TeamFile $teamFile - * @return Response - */ - public function update(Request $request, TeamFile $teamFile) - { - // - } - /** * Remove the specified resource from storage. * diff --git a/app/Services/ApplicationService.php b/app/Services/ApplicationService.php new file mode 100644 index 0000000..f7defa6 --- /dev/null +++ b/app/Services/ApplicationService.php @@ -0,0 +1,158 @@ +where('vacancySlug', $vacancySlug)->get(); + + $firstVacancy = $vacancyWithForm->first(); + + if (!$vacancyWithForm->isEmpty() && $firstVacancy->vacancyCount !== 0 && $firstVacancy->vacancyStatus == 'OPEN') { + return view('dashboard.application-rendering.apply') + ->with([ + 'vacancy' => $vacancyWithForm->first(), + 'preprocessedForm' => json_decode($vacancyWithForm->first()->forms->formStructure, true), + ]); + } else { + + throw new ApplicationNotFoundException('The application you\'re looking for could not be found or it is currently unavailable.', 404); + + } + } + + /** + * Fills a vacancy's form with submitted data. + * + * @throws UnavailableApplicationException Thrown when the application has no vacancies or is closed + * @throws VacancyNotFoundException Thrown when the associated vacancy is not found + * @throws IncompleteApplicationException Thrown when there are missing fields + */ + public function fillForm(Authenticatable $applicant, array $formData, $vacancySlug): bool + { + $vacancy = Vacancy::with('forms')->where('vacancySlug', $vacancySlug)->get(); + + if ($vacancy->isEmpty()) { + + throw new VacancyNotFoundException('This vacancy doesn\'t exist; Please use the proper buttons to apply to one.', 404); + + } + + if ($vacancy->first()->vacancyCount == 0 || $vacancy->first()->vacancyStatus !== 'OPEN') { + + throw new UnavailableApplicationException("This application is unavailable."); + } + + Log::info('Processing new application!'); + + $formStructure = json_decode($vacancy->first()->forms->formStructure, true); + $responseValidation = ContextAwareValidator::getResponseValidator($formData, $formStructure); + + + Log::info('Built response & validator structure!'); + + if (!$responseValidation->get('validator')->fails()) { + $response = Response::create([ + '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 + 'responseData' => $responseValidation->get('responseStructure'), + ]); + + Log::info('Registered form response!', [ + 'applicant' => $applicant->name, + 'vacancy' => $vacancy->first()->vacancyName + ]); + + $application = Application::create([ + 'applicantUserID' => $applicant->id, + 'applicantFormResponseID' => $response->id, + 'applicationStatus' => 'STAGE_SUBMITTED', + ]); + + Log::info('Submitted an application!', [ + 'responseID' => $response->id, + 'applicant' => $applicant->name + ]); + + foreach (User::all() as $user) { + if ($user->hasRole('admin')) { + $user->notify((new NewApplicant($application, $vacancy->first()))->delay(now()->addSeconds(10))); + } + } + + return true; + + } + + Log::warning('Application form for ' . $applicant->name . ' contained errors, resetting!'); + + throw new IncompleteApplicationException('There are one or more errors in your application. Please make sure none of your fields are empty, since they are all required.'); + } + + public function updateStatus(Application $application, $newStatus) + { + switch ($newStatus) { + case 'deny': + + event(new ApplicationDeniedEvent($application)); + $message = __("Application denied successfully."); + + break; + + case 'interview': + Log::info(' Moved application ID ' . $application->id . 'to interview stage!'); + $message = __('Application moved to interview stage!'); + + $application->setStatus('STAGE_INTERVIEW'); + $application->user->notify(new ApplicationMoved()); + + break; + + default: + throw new \LogicException("Wrong status parameter. Please notify a developer."); + } + + return $message; + } + + /** + * @throws \Exception + */ + public function delete(Application $application): ?bool + { + return $application->delete(); + } + + + public function canVote($votes): bool + { + $allvotes = collect([]); + + foreach ($votes as $vote) { + if ($vote->userID == Auth::user()->id) { + $allvotes->push($vote); + } + } + + return !(($allvotes->count() == 1)); + } +} diff --git a/app/Services/AppointmentService.php b/app/Services/AppointmentService.php new file mode 100644 index 0000000..35f6de3 --- /dev/null +++ b/app/Services/AppointmentService.php @@ -0,0 +1,90 @@ + $appointmentDescription, + 'appointmentDate' => $appointmentDate->toDateTimeString(), + 'applicationID' => $application->id, + 'appointmentLocation' => (in_array($appointmentLocation, $this->allowedPlatforms)) ? $appointmentLocation : 'DISCORD', + ]); + $application->setStatus('STAGE_INTERVIEW_SCHEDULED'); + + Log::info('User '.Auth::user()->name.' has scheduled an appointment with '.$application->user->name.' for application ID'.$application->id, [ + 'datetime' => $appointmentDate->toDateTimeString(), + 'scheduled' => now(), + ]); + + $application->user->notify(new AppointmentScheduled($appointment)); + + + return true; + } + + /** + * Updates the appointment with the new $status. + * It also sets the application's status to peer approval. + * + * Set $updateApplication to false to only update its status + * + * @throws InvalidAppointmentStatusException + */ + public function updateAppointment(Application $application, $status, $updateApplication = true) + { + $validStatuses = [ + 'SCHEDULED', + 'CONCLUDED', + ]; + + if ($status == 'SCHEDULED' || $status == 'CONCLUDED') + { + $application->appointment->appointmentStatus = strtoupper($status); + $application->appointment->save(); + + if ($updateApplication) + { + $application->setStatus('STAGE_PEERAPPROVAL'); + $application->user->notify(new ApplicationMoved()); + } + } + else + { + throw new InvalidAppointmentStatusException("Invalid appointment status!"); + } + + } + + /** + * @return string[] + */ + public function getAllowedPlatforms(): array + { + return $this->allowedPlatforms; + } + +} diff --git a/app/Services/CommentService.php b/app/Services/CommentService.php new file mode 100644 index 0000000..545d3d5 --- /dev/null +++ b/app/Services/CommentService.php @@ -0,0 +1,27 @@ + Auth::user()->id, + 'applicationID' => $application->id, + 'text' => $comment, + ]); + } + + public function deleteComment(Comment $comment): ?bool + { + return $comment->delete(); + } + +} diff --git a/app/Services/ConfigurationService.php b/app/Services/ConfigurationService.php new file mode 100644 index 0000000..249a24d --- /dev/null +++ b/app/Services/ConfigurationService.php @@ -0,0 +1,75 @@ + $option) { + try { + + Log::debug('Going through option '.$optionName); + if (Options::optionExists($optionName)) { + Log::debug('Option exists, updating to new values', [ + 'opt' => $optionName, + 'new_value' => $option, + ]); + Options::changeOption($optionName, $option); + } + + } catch (\Exception $ex) { + + Log::error('Unable to update options!', [ + 'msg' => $ex->getMessage(), + 'trace' => $ex->getTraceAsString(), + ]); + + // Let service caller handle this without failing here + throw $ex; + } + } + } + + /** + * Saves the chosen game integration + * + * @throws InvalidGamePreferenceException + * @returns bool + */ + public function saveGameIntegration($gamePreference): bool + { + + // TODO: Find solution to dynamically support games + + $supportedGames = [ + 'RUST', + 'MINECRAFT', + 'SE', + 'GMOD' + ]; + + if (!is_null($gamePreference) && in_array($gamePreference, $supportedGames)) + { + Options::changeOption('currentGame', $gamePreference); + return true; + } + + throw new InvalidGamePreferenceException("Unsupported game " . $gamePreference); + } + +} diff --git a/app/Services/ContactService.php b/app/Services/ContactService.php new file mode 100644 index 0000000..86c7938 --- /dev/null +++ b/app/Services/ContactService.php @@ -0,0 +1,47 @@ +post(config('recaptcha.verify.apiurl'), [ + 'secret' => config('recaptcha.keys.secret'), + 'response' => $challenge, + 'remoteip' => $ipAddress, + ]); + + $response = json_decode($verifyrequest->getBody(), true); + + if (! $response['success']) { + throw new FailedCaptchaException('Beep beep boop... Robot? Submission failed.'); + } + + foreach (User::all() as $user) { + if ($user->hasRole('admin')) { + $user->notify(new NewContact(collect([ + 'message' => $message, + 'ip' => $ipAddress, + 'email' => $email, + ]))); + } + } + } + + +} diff --git a/app/Services/FormManagementService.php b/app/Services/FormManagementService.php new file mode 100644 index 0000000..a7e9ce0 --- /dev/null +++ b/app/Services/FormManagementService.php @@ -0,0 +1,76 @@ +get('validator')->fails()) { + $storableFormStructure = $contextValidation->get('structure'); + + Form::create( + [ + 'formName' => $fields['formName'], + 'formStructure' => $storableFormStructure, + 'formStatus' => 'ACTIVE', + ] + ); + return true; + } + return $contextValidation->get('validator')->errors()->getMessages(); + } + + public function deleteForm(Form $form) { + + $deletable = true; + + if (! is_null($form) && ! is_null($form->vacancies) && $form->vacancies->count() !== 0 || ! is_null($form->responses)) { + $deletable = false; + } + + if ($deletable) { + + $form->delete(); + return true; + + } else { + + throw new FormHasConstraintsException(__('You cannot delete this form because it\'s tied to one or more applications and ranks, or because it doesn\'t exist.')); + + } + } + + public function updateForm(Form $form, $fields) { + + $contextValidation = ContextAwareValidator::getValidator($fields, true); + + if (! $contextValidation->get('validator')->fails()) { + // Add the new structure into the form. New, subsquent fields will be identified by the "new" prefix + // This prefix doesn't actually change the app's behavior when it receives applications. + // Additionally, old applications won't of course display new and updated fields, because we can't travel into the past and get data for them + $form->formStructure = $contextValidation->get('structure'); + $form->save(); + + return $form; + + } else { + return $contextValidation->get('validator')->errors()->getMessages(); + } + } + +} diff --git a/app/Services/MeetingNoteService.php b/app/Services/MeetingNoteService.php new file mode 100644 index 0000000..e1136e9 --- /dev/null +++ b/app/Services/MeetingNoteService.php @@ -0,0 +1,36 @@ +load('appointment'); + + $application->appointment->meetingNotes = $noteText; + $application->appointment->save(); + + return true; + + } else { + throw new InvalidAppointmentException('There\'s no appointment to save notes to!'); + } + + } + +} diff --git a/app/Services/ProfileService.php b/app/Services/ProfileService.php new file mode 100644 index 0000000..438527b --- /dev/null +++ b/app/Services/ProfileService.php @@ -0,0 +1,50 @@ +profile; + $social = []; + + if (! is_null($profile)) { + switch ($request->avatarPref) { + case 'MOJANG': + $avatarPref = 'crafatar'; + + break; + case 'GRAVATAR': + $avatarPref = strtolower($request->avatarPref); + + break; + } + + $social['links']['github'] = $request->socialGithub; + $social['links']['twitter'] = $request->socialTwitter; + $social['links']['insta'] = $request->socialInsta; + $social['links']['discord'] = $request->socialDiscord; + + $profile->profileShortBio = $request->shortBio; + $profile->profileAboutMe = $request->aboutMe; + $profile->avatarPreference = $avatarPref; + $profile->socialLinks = json_encode($social); + + return $profile->save(); + } + + throw new ProfileNotFoundException("This profile does not exist."); + } + +} diff --git a/app/Services/SecuritySettingsService.php b/app/Services/SecuritySettingsService.php new file mode 100644 index 0000000..43329b4 --- /dev/null +++ b/app/Services/SecuritySettingsService.php @@ -0,0 +1,54 @@ + $policy + ]); + } + else + { + Log::debug('[WARN] Ignoring bogus policy', [ + 'avaliable' => $validPolicies, + 'given' => $policy + ]); + } + + Options::changeOption('graceperiod', $options['graceperiod']); + Options::changeOption('password_expiry', $options['pwexpiry']); + Options::changeOption('force2fa', $options['enforce2fa']); + Options::changeOption('requireGameLicense', $options['requirePMC']); + + return true; + + } + +} diff --git a/app/Services/TeamFileService.php b/app/Services/TeamFileService.php new file mode 100644 index 0000000..c94719c --- /dev/null +++ b/app/Services/TeamFileService.php @@ -0,0 +1,42 @@ +store('uploads'); + $originalFileName = $upload->getClientOriginalName(); + $originalFileExtension = $upload->extension(); + $originalFileSize = $upload->getSize(); + + $fileEntry = TeamFile::create([ + 'uploaded_by' => $uploader, + 'team_id' => $team, + 'name' => $originalFileName, + 'caption' => $caption, + 'description' => $description, + 'fs_location' => $file, + 'extension' => $originalFileExtension, + 'size' => $originalFileSize + ]); + + if ($fileEntry && !is_bool($file)) + { + return $fileEntry; + } + + throw new FileUploadException("There was an unknown error whilst trying to upload your file."); + + } + +} diff --git a/app/Services/TeamService.php b/app/Services/TeamService.php new file mode 100644 index 0000000..9e7c8c0 --- /dev/null +++ b/app/Services/TeamService.php @@ -0,0 +1,165 @@ + $teamName, + 'owner_id' => $ownerID, + ]); + + Auth::user()->teams()->attach($team->id); + + return $team; + } + + public function updateTeam(Team $team, $teamDescription, $joinType): bool + { + + $team->description = $teamDescription; + $team->openJoin = $joinType; + + return $team->save(); + } + + /** + * Invites a user to a $team. + * + * @throws PublicTeamInviteException Thrown when trying to invite a user to a public team + * @throws UserAlreadyInvitedException Thrown when a user is already invited + */ + public function inviteUser(Team $team, $userID): bool + { + + $user = User::findOrFail($userID); + + if (! $team->openJoin) { + if (! Teamwork::hasPendingInvite($user->email, $team)) { + Teamwork::inviteToTeam($user, $team, function (TeamInvite $invite) use ($user) { + Mail::to($user)->send(new InviteToTeam($invite)); + }); + return true; + } else { + throw new UserAlreadyInvitedException('This user has already been invited.'); + } + } else { + throw new PublicTeamInviteException('You can\'t invite users to public teams.'); + } + + } + + /** + * Accepts or denies a user invite + * + * @param Authenticatable $user + * @param $action + * @param $token + * @return bool True on success or exception on failure + * @throws InvalidInviteException Thrown when the invite code / url is invalid + */ + public function processInvite(Authenticatable $user, $action, $token): bool { + + switch ($action) { + case 'accept': + + $invite = Teamwork::getInviteFromAcceptToken($token); + + if ($invite && $invite->user->is($user)) { + Teamwork::acceptInvite($invite); + + } else { + + throw new InvalidInviteException('Invalid or expired invite URL.'); + } + + break; + + case 'deny': + + $invite = Teamwork::getInviteFromDenyToken($token); + + if ($invite && $invite->user->is($user)) { + + Teamwork::denyInvite($invite); + + } else { + + throw new InvalidInviteException('Invalid or expired invite URL.'); + } + + break; + + default: + throw new InvalidInviteException('Sorry, but the invite URL you followed was malformed.'); + } + + return true; + + } + + + /** + * @param Team $team + * @param $associatedVacancies + * @return string The success message, exception/bool if error + */ + public function updateVacancies(Team $team, $associatedVacancies): string + { + + // P.S. To future developers + // This method gave me a lot of trouble lol. It's hard to write code when you're half asleep. + // There may be an n+1 query in the view and I don't think there's a way to avoid that without writing a lot of extra code. + + $requestVacancies = $associatedVacancies; + $currentVacancies = $team->vacancies->pluck('id')->all(); + + if (is_null($requestVacancies)) { + foreach ($team->vacancies as $vacancy) { + $team->vacancies()->detach($vacancy->id); + } + + return 'Removed all vacancy associations.'; + } + + $vacancyDiff = array_diff($requestVacancies, $currentVacancies); + $deselectedDiff = array_diff($currentVacancies, $requestVacancies); + + if (! empty($vacancyDiff) || ! empty($deselectedDiff)) { + foreach ($vacancyDiff as $selectedVacancy) { + $team->vacancies()->attach($selectedVacancy); + } + + foreach ($deselectedDiff as $deselectedVacancy) { + $team->vacancies()->detach($deselectedVacancy); + } + } else { + $team->vacancies()->attach($requestVacancies); + } + return 'Assignments changed successfully.'; + } +}