From dba47afb0101c6d435ce5fabcf5ec14f95186846 Mon Sep 17 00:00:00 2001 From: Blake Nahin Date: Mon, 18 Oct 2021 02:19:39 -0500 Subject: [PATCH 1/9] Legacy Exam Mailables --- .../Controllers/API/v2/ExamController.php | 10 ++++-- app/Mail/AcademyExamSubmitted.php | 1 - app/Mail/AcademyRatingCourseEnrolled.php | 1 - app/Mail/ExamAssigned.php | 34 ++++++++++++++++++ app/Mail/LegacyExamResult.php | 36 +++++++++++++++++++ app/Mail/SurveyAssignment.php | 1 - app/Mail/TransferRequested.php | 1 - resources/views/emails/exam/assign.blade.php | 14 ++++---- resources/views/emails/exam/failed.blade.php | 10 +++--- resources/views/emails/exam/passed.blade.php | 6 ++-- 10 files changed, 93 insertions(+), 21 deletions(-) create mode 100644 app/Mail/ExamAssigned.php create mode 100644 app/Mail/LegacyExamResult.php diff --git a/app/Http/Controllers/API/v2/ExamController.php b/app/Http/Controllers/API/v2/ExamController.php index 7c237df4..24595fea 100644 --- a/app/Http/Controllers/API/v2/ExamController.php +++ b/app/Http/Controllers/API/v2/ExamController.php @@ -11,11 +11,14 @@ use App\Helpers\EmailHelper; use App\Helpers\RoleHelper; use App\Helpers\AuthHelper; +use App\Mail\ExamAssigned; +use App\Mail\LegacyExamResult; use App\TrainingBlock; use App\User; use Carbon\Carbon; use Illuminate\Http\Request; use App\Exam; +use Illuminate\Support\Facades\Mail; class ExamController extends APIController { @@ -246,7 +249,7 @@ public function postSubmit(Request $request) if ($fac == "ZAE") { $fac = \Auth::user()->facility; } - EmailHelper::sendEmailFacilityTemplate($to, "Exam Passed", $fac, "exampassed", $data); + Mail::to($to)->queue(new LegacyExamResult($data, true)); if ($exam->id == config('exams.BASIC.legacyId')) { \Auth::user()->flag_needbasic = 0; \Auth::user()->save(); @@ -270,7 +273,7 @@ public function postSubmit(Request $request) if ($fac == "ZAE") { $fac = \Auth::user()->facility; } - EmailHelper::sendEmailFacilityTemplate($to, "Exam Not Passed", $fac, "examfailed", $data); + Mail::to($to)->queue(new LegacyExamResult($data, false)); return response()->api(['results' => "Not Passed."]); } @@ -909,6 +912,9 @@ public function postExamAssign(Request $request, $examid, $cid) $to[] = $exam->facility_id . "-TA@vatusa.net"; } + + Mail::to($to)->queue(new ExamAssigned($data)); + EmailHelper::sendEmailFacilityTemplate($to, "Exam Assigned", $exam->facility_id, "examassigned", $data); log_action($cid, "Exam (" . $exam->facility_id . ") " . $exam->name . diff --git a/app/Mail/AcademyExamSubmitted.php b/app/Mail/AcademyExamSubmitted.php index 468c4bdf..f0ca0c08 100644 --- a/app/Mail/AcademyExamSubmitted.php +++ b/app/Mail/AcademyExamSubmitted.php @@ -5,7 +5,6 @@ use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; -use Illuminate\Contracts\Queue\ShouldQueue; class AcademyExamSubmitted extends Mailable { diff --git a/app/Mail/AcademyRatingCourseEnrolled.php b/app/Mail/AcademyRatingCourseEnrolled.php index daf3c021..764aeb81 100644 --- a/app/Mail/AcademyRatingCourseEnrolled.php +++ b/app/Mail/AcademyRatingCourseEnrolled.php @@ -6,7 +6,6 @@ use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; -use Illuminate\Contracts\Queue\ShouldQueue; class AcademyRatingCourseEnrolled extends Mailable { diff --git a/app/Mail/ExamAssigned.php b/app/Mail/ExamAssigned.php new file mode 100644 index 00000000..544cd3b1 --- /dev/null +++ b/app/Mail/ExamAssigned.php @@ -0,0 +1,34 @@ +data = $data; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + return $this->subject('[VATUSA] Exam Assigned')->view('emails.exam.assign'); + } +} diff --git a/app/Mail/LegacyExamResult.php b/app/Mail/LegacyExamResult.php new file mode 100644 index 00000000..3d77e021 --- /dev/null +++ b/app/Mail/LegacyExamResult.php @@ -0,0 +1,36 @@ +data = $data; + $this->passed = $passed; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + return $this->subject('[VATUSA] Exam ' . ($this->passed ? "Passed" : "Failed"))->view($this->passed ? "emails.exam.passed" : "emails.exam.failed"); + } +} diff --git a/app/Mail/SurveyAssignment.php b/app/Mail/SurveyAssignment.php index 84b05460..6483e5bd 100644 --- a/app/Mail/SurveyAssignment.php +++ b/app/Mail/SurveyAssignment.php @@ -7,7 +7,6 @@ use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; -use Illuminate\Contracts\Queue\ShouldQueue; class SurveyAssignment extends Mailable { diff --git a/app/Mail/TransferRequested.php b/app/Mail/TransferRequested.php index 3d93219e..9be443c0 100644 --- a/app/Mail/TransferRequested.php +++ b/app/Mail/TransferRequested.php @@ -6,7 +6,6 @@ use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; -use Illuminate\Contracts\Queue\ShouldQueue; class TransferRequested extends Mailable { diff --git a/resources/views/emails/exam/assign.blade.php b/resources/views/emails/exam/assign.blade.php index 6c4ff987..aae47041 100644 --- a/resources/views/emails/exam/assign.blade.php +++ b/resources/views/emails/exam/assign.blade.php @@ -1,10 +1,10 @@ @extends('emails.layout') @section('title','Exam Assigned') @section('content') - Hello {{ $student_name }}, + Hello {{ $data['student_name'] }},

- This email is to inform you that you have been assigned exam {{ $exam_name }} by instructor {{ $instructor_name }}. You have - until {{ $end_date }} US Central Time to complete the examination before it expires. + This email is to inform you that you have been assigned exam {{ $data['exam_name'] }} by instructor {{ $data['instructor_name'] }}. You have + until {{ $data['end_date'] }} US Central Time to complete the examination before it expires.

@@ -22,12 +22,12 @@


- Prior to taking the exam, be sure to read all materials assigned to you in your {{ $facility }} welcome email. - @if($cbt_required) + Prior to taking the exam, be sure to read all materials assigned to you in your {{ $data['facility'] }} welcome email. + @if($data['cbt_required'])

- Before attempting the exam, you must complete {{$cbt_facility}}'s {{$cbt_block}} Computer Based Training (CBT) course. You + Before attempting the exam, you must complete {{$data['cbt_facility']}}'s {{$data['cbt_block']}} Computer Based Training (CBT) course. You can access that by visiting https://www.vatusa.net/cbt/{{$cbt_facility}}. + href="https://www.vatusa.net/cbt/{{$data['cbt_facility']}}">https://www.vatusa.net/cbt/{{$data['cbt_facility']}}. @endif

If you have any questions, please contact your instructor. diff --git a/resources/views/emails/exam/failed.blade.php b/resources/views/emails/exam/failed.blade.php index bc15dac7..6632858a 100644 --- a/resources/views/emails/exam/failed.blade.php +++ b/resources/views/emails/exam/failed.blade.php @@ -1,15 +1,15 @@ @extends('emails.layout') @section('title','Exam Failed') @section('content') - Dear {{ $student_name }}, + Dear {{ $data['student_name'] }},

This email is to notify you that you did not pass your assigned exam.

- Exam: {{ $exam_name }}
- Score: {{ $correct }}/{{ $possible }} ({{$score}}%) + Exam: {{ $data['exam_name'] }}
+ Score: {{ $data['correct'] }}/{{ $data['possible'] }} ({{$data['score']}}%)

- @if($reassign > 0) - Your exam will be reassigned in {{$reassign}} day(s). + @if($data['reassign'] > 0) + Your exam will be reassigned in {{$data['reassign']}} day(s). @else Your exam will be reassigned by your training staff. @endif diff --git a/resources/views/emails/exam/passed.blade.php b/resources/views/emails/exam/passed.blade.php index e3e59d07..27ff243f 100644 --- a/resources/views/emails/exam/passed.blade.php +++ b/resources/views/emails/exam/passed.blade.php @@ -1,12 +1,12 @@ @extends('emails.layout') @section('title','Exam Passed') @section('content') - Dear {{ $student_name }}, + Dear {{ $data['student_name'] }},

This email is to notify you that you passed your assigned exam!

- Exam: {{ $exam_name }}
- Score: {{ $correct }}/{{ $possible }} ({{$score}}%) + Exam: {{ $data['exam_name'] }}
+ Score: {{ $data['correct'] }}/{{ $data['possible'] }} ({{$data['score']}}%)

A copy of this has also been sent to your training staff.

From c8719d6c574210b2eb2ecd5199c574dc8bb14a40 Mon Sep 17 00:00:00 2001 From: Blake Nahin Date: Sat, 23 Oct 2021 03:00:34 -0500 Subject: [PATCH 2/9] Legacy Exam Result Notifications --- app/Classes/VATUSADiscord.php | 198 ++++++++++++++++++ app/FacilityNotificationChannel.php | 10 + .../Controllers/API/v2/ExamController.php | 142 +++++++++---- app/NotificationSetting.php | 10 + composer.json | 1 + composer.lock | 59 +++++- 6 files changed, 384 insertions(+), 36 deletions(-) create mode 100644 app/Classes/VATUSADiscord.php create mode 100644 app/FacilityNotificationChannel.php create mode 100644 app/NotificationSetting.php diff --git a/app/Classes/VATUSADiscord.php b/app/Classes/VATUSADiscord.php new file mode 100644 index 00000000..68230069 --- /dev/null +++ b/app/Classes/VATUSADiscord.php @@ -0,0 +1,198 @@ + + */ + +namespace App\Classes; + +use App\Facility; +use App\FacilityNotificationChannel; +use App\NotificationSetting; +use App\User; +use Exception; +use Firebase\JWT\JWT; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; +use Illuminate\Support\Carbon; +use Psr\Http\Message\ResponseInterface; + +class VATUSADiscord +{ + private $guzzle; + + public const NOTIFY_EMAIL = 1; + public const NOTIFY_DISCORD = 2; + public const NOTIFY_BOTH = 3; + public const NOTFIY_NONE = 0; + + public function __construct() + { + $this->guzzle = new Client(['base_uri' => config('services.discord.botServer')]); + } + + public function getNotificationOption(User $user, string $type): int + { + $record = NotificationSetting::where('cid', $user->cid)->where('type', $type)->first(); + + return $record ? $record->option : 0; + } + + public function getFacilityNotificationChannel(Facility $facility, string $type) + { + $record = FacilityNotificationChannel::where('facility', $facility->id)->where('type', $type)->first(); + + return $record && $facility->discord_guild ? $record->channel : 0; + } + + public function getAllUserNotificationOptions(User $user): array + { + $records = NotificationSetting::where('cid', $user->cid)->get(); + $return = array(); + foreach ($records as $record) { + $return[$record->type] = $record->option; + } + + return $return; + } + + public function getAllFacilityNotificationChannels(Facility $facility): array + { + $records = FacilityNotificationChannel::where('facility', $facility->id)->get(); + $return = array(); + foreach ($records as $record) { + $return[$record->type] = $record->channel; + } + + return $return; + } + + /** + * Send Notification to Bot Server + * + * @param string $type The notification identifier. + * @param string $medium The medium of notification, dm | discord. + * @param array $data The notification data. + * @param string|null $guildId The guild's ID + * @param string|null $channelId The channel's ID + * @param string|null $userId The user's ID. + * + * @return bool + */ + public function sendNotification( + string $type, + string $medium, + array $data, + ?string $guildId = null, + ?string $channelId = null, + ?string $userId = null + ): bool { + if ($guildId && $channelId) { + $data = array_merge($data, compact('guildId', 'channelId')); + } + if ($userId) { + $data = array_merge($data, compact('userId')); + } + try { + $this->sendRequest('POST', "notifications/$medium/$type", ['json' => $data]); + } catch (Exception $e) { + return 0; + } + + return 1; + } + + /** + * Determine if the User has configured the Notification. + * + * @param \App\User|\Illuminate\Contracts\Auth\Authenticatable $user + * @param string $type + * @param string $medium + * + * @return bool + */ + public function userWantsNotification(User $user, string $type, string $medium): bool + { + if (!$user->discord_id) { + return false; + } + $option = $this->getNotificationOption($user, $type); + if ($option === self::NOTIFY_BOTH) { + return true; + } + + switch (strtolower($medium)) { + case "discord": + return $option === self::NOTIFY_DISCORD; + case "email": + return $option === self::NOTIFY_EMAIL; + default: + return false; + } + } + + /** + * Get an array of all the Guilds that the User is an admin in + * and that the Bot is a member of. + * + * @param \App\User $user + * + * @return array + */ + public function getUserAdminGuilds(User $user): array + { + try { + $response = $this->sendRequest("GET", "/guilds/" . $user->discord_id); + } catch (Exception $e) { + return []; + } + + if ($response->getStatusCode() === 200) { + return json_decode($response->getBody(), true); + } + + return []; + } + + public function getGuildChannels(string $guild) + { + try { + $response = $this->sendRequest("GET", "/guild/$guild/channels"); + } catch (Exception $e) { + return []; + } + if ($response->getStatusCode() === 200) { + return json_decode($response->getBody(), true); + } + + return []; + } + + /** + * Send request to the Bot Server. + * + * @param string $method The request method. + * @param string $uri The request URI. + * @param array|null $data The request body. + * + * @return \Psr\Http\Message\ResponseInterface + * @throws \Exception + */ + private function sendRequest(string $method, string $uri, ?array $data = null): ResponseInterface + { + $iss = Carbon::now(); + $jwt = JWT::encode([ + 'iat' => $iss->getTimestamp(), + 'iss' => config('app.url'), + 'aud' => config('services.discord.botServer'), + 'nbf' => $iss->getTimestamp(), + 'exp' => $iss->addMinute()->getTimestamp() + ], config('services.discord.botSecret'), 'HS512'); + try { + return $this->guzzle->request($method, $uri, + ['json' => $data ?? [], 'headers' => ['Authorization' => 'Bearer ' . $jwt]]); + } catch (GuzzleException $e) { + throw new Exception("Unable to make request to the Discord Bot Server. " . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/FacilityNotificationChannel.php b/app/FacilityNotificationChannel.php new file mode 100644 index 00000000..bd26362d --- /dev/null +++ b/app/FacilityNotificationChannel.php @@ -0,0 +1,10 @@ +save(); // Done... let's send some emails - $to[] = \Auth::user()->email; + $to = []; + $facility = Facility::find($exam->facility_id); + $instructor = null; + $ta = $facility->ta(); + if (!$ta) { + $ta = $facility->datm(); + } + if (!$ta) { + $ta = $facility->atm(); + } + + $notify = new VATUSADiscord(); + if ($notify->userWantsNotification(Auth::user(), "legacyExamResult", "email")) { + $to[] = Auth::user()->email; + } if ($assign->instructor_id > 111111) { $instructor = User::find($assign->instructor_id); if ($instructor) { - $to[] = $instructor->email; + if ($notify->userWantsNotification($instructor, "legacyExamResult", "email")) { + $to[] = $instructor->email; + } } } - if ($exam->facility_id != "ZAE") { - $to[] = $exam->facility_id . "-TA@vatusa.net"; + if ($exam->facility_id != "ZAE" && $ta && $notify->userWantsNotification($ta, "legacyExamResult", "email")) { + $to[] = $ta->email; } $log = new Action(); - $log->to = \Auth::user()->cid; + $log->to = Auth::user()->cid; $log->log = "Exam (" . $exam->facility_id . ") " . $exam->name . " completed. Score $correct/$possible ($score%)."; $log->log .= ($result->passed) ? " Passed." : " Not Passed."; $log->save(); $data = [ + 'passed' => $result->passed, 'exam_name' => "(" . $exam->facility_id . ") " . $exam->name, - 'instructor_name' => (isset($instructor)) ? $instructor->fullname() : 'N/A', + 'result_id' => $result->id, + 'instructor_name' => $instructor ? $instructor->fullname() : 'N/A', 'correct' => $correct, 'possible' => $possible, 'score' => $score, - 'student_name' => \Auth::user()->fullname(), + 'student_name' => Auth::user()->fullname(), 'reassign' => 0, 'reassign_date' => null ]; + $studentId = $notify->userWantsNotification(Auth::user(), "legacyExamResult", + "discord") ? Auth::user()->discord_id : 0; + $instructorId = $instructor && $notify->userWantsNotification($instructor, "legacyExamResult", + "discord") ? $instructor->discord_id : 0; + $taId = $ta && (!$instructor || $ta->cid !== $instructor->cid) && $notify->userWantsNotification($ta, + "legacyExamResult", + "discord") ? $ta->discord_id : 0; + if ($studentId || $instructorId || $taId) { + $notify->sendNotification("legacyExamResult", "dm", + array_merge($data, compact('studentId', 'instructorId', 'taId'))); + } + + if ($channel = $notify->getFacilityNotificationChannel($facility, "legacyExamResult")) { + $notify->sendNotification("legacyExamResult", "channel", $data, $facility->discord_guild, $channel); + } if ($result->passed) { $assign->delete(); $fac = $exam->facility_id; if ($fac == "ZAE") { - $fac = \Auth::user()->facility; + $fac = Auth::user()->facility; + } + if (count($to)) { + Mail::to($to)->queue(new LegacyExamResult($data, true)); } - Mail::to($to)->queue(new LegacyExamResult($data, true)); if ($exam->id == config('exams.BASIC.legacyId')) { - \Auth::user()->flag_needbasic = 0; - \Auth::user()->save(); + Auth::user()->flag_needbasic = 0; + Auth::user()->save(); } return response()->api(['results' => "Passed."]); @@ -271,9 +309,11 @@ public function postSubmit(Request $request) $assign->delete(); $fac = $exam->facility_id; if ($fac == "ZAE") { - $fac = \Auth::user()->facility; + $fac = Auth::user()->facility; + } + if (count($to)) { + Mail::to($to)->queue(new LegacyExamResult($data, false)); } - Mail::to($to)->queue(new LegacyExamResult($data, false)); return response()->api(['results' => "Not Passed."]); } @@ -311,8 +351,10 @@ public function postSubmit(Request $request) * ) * ) */ - public function getRequest(Request $request) - { + public + function getRequest( + Request $request + ) { if (!\Cache::has('exam.queue.' . \Auth::user()->cid)) { return response()->api(generate_error("No exam queued", true), 404); } @@ -400,8 +442,11 @@ public function getRequest(Request $request) * * @return \Illuminate\Http\Response */ - public function getExams(Request $request, $facility = null) - { + public + function getExams( + Request $request, + $facility = null + ) { if (\Auth::check() && !(RoleHelper::isSeniorStaff() || RoleHelper::isVATUSAStaff() || RoleHelper::isInstructor())) { @@ -451,8 +496,11 @@ public function getExams(Request $request, $facility = null) * * @return \Illuminate\Http\Response */ - public function getExambyId(Request $request, $id) - { + public + function getExambyId( + Request $request, + $id + ) { if (\Auth::check() && !(RoleHelper::isSeniorStaff() || RoleHelper::isVATUSAStaff() || RoleHelper::isInstructor())) { @@ -500,8 +548,11 @@ public function getExambyId(Request $request, $id) * * @return \Illuminate\Http\Response */ - public function getExamQuestions(Request $request, $id) - { + public + function getExamQuestions( + Request $request, + $id + ) { $exam = Exam::find($id); if (!$exam) { return response()->api(generate_error("Not found"), 404); @@ -573,8 +624,11 @@ public function getExamQuestions(Request $request, $id) * * @return \Illuminate\Http\Response */ - public function putExam(Request $request, string $id) - { + public + function putExam( + Request $request, + string $id + ) { if (!\Auth::check()) { return response()->api(generate_error("Unauthorized"), 401); } @@ -682,8 +736,11 @@ public function putExam(Request $request, string $id) * * @return \Illuminate\Http\Response */ - public function postExamQuestion(Request $request, $examid) - { + public + function postExamQuestion( + Request $request, + $examid + ) { if (!\Auth::check()) { return response()->api(generate_error("Unauthorized"), 401); } @@ -764,8 +821,12 @@ public function postExamQuestion(Request $request, $examid) * * @return */ - public function putExamQuestion(Request $request, $examid, $questionid) - { + public + function putExamQuestion( + Request $request, + $examid, + $questionid + ) { if (!\Auth::check()) { return response()->api(generate_error("Unauthorized"), 401); } @@ -817,7 +878,7 @@ public function putExamQuestion(Request $request, $examid, $questionid) * path="/exam/(id)/assign/(cid)", * summary="Assign exam. [Auth]", * description="Assign exam to specified controller. Requires JWT or Session Cookie. Must be instructor, senior - staff or VATUSA staff.", tags={"user","exam"}, produces={"application/json"}, + staff or VATUSA staff.", tags={"user","exam"}, produces={"application/json"}, * @SWG\Parameter(name="id", in="path", type="integer", description="Exam ID"), * @SWG\Parameter(name="cid", in="path", type="integer", description="VATSIM ID"), * @SWG\Parameter(name="expire", in="formData", type="integer", description="Days until expiration, 7 @@ -854,8 +915,12 @@ public function putExamQuestion(Request $request, $examid, $questionid) * * @return \Illuminate\Http\Response */ - public function postExamAssign(Request $request, $examid, $cid) - { + public + function postExamAssign( + Request $request, + $examid, + $cid + ) { if (!\Auth::check()) { return response()->api(generate_error("Unauthorized"), 401); } @@ -869,7 +934,7 @@ public function postExamAssign(Request $request, $examid, $cid) if (!$exam) { return response()->api(generate_error("Not found"), 404); } - if(in_array($exam->id, [ + if (in_array($exam->id, [ config('exams.BASIC.legacyId'), config('exams.S2.legacyId'), config('exams.S3.legacyId'), @@ -961,8 +1026,12 @@ public function postExamAssign(Request $request, $examid, $cid) * @return \Illuminate\Http\Response * @throws \Exception */ - public function deleteExamAssignment(Request $request, $examid, $cid) - { + public + function deleteExamAssignment( + Request $request, + $examid, + $cid + ) { if (!\Auth::check()) { return response()->api(generate_error("Unauthorized"), 401); } @@ -1039,8 +1108,11 @@ public function deleteExamAssignment(Request $request, $examid, $cid) * @return \Illuminate\Http\Response * @throws \Exception */ - public function getResult(Request $request, $id) - { + public + function getResult( + Request $request, + $id + ) { $apikey = AuthHelper::validApiKeyv2($request->input('apikey', null)); if (!$apikey && !\Auth::check()) { return response()->api(generate_error("Unauthorized"), 401); diff --git a/app/NotificationSetting.php b/app/NotificationSetting.php new file mode 100644 index 00000000..54763d4e --- /dev/null +++ b/app/NotificationSetting.php @@ -0,0 +1,10 @@ +=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": ">=4.8 <=9" + }, + "suggest": { + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v5.4.0" + }, + "time": "2021-06-23T19:00:23+00:00" + }, { "name": "fruitcake/laravel-cors", "version": "v2.0.4", From 9844419ae549a41dddd12bdfda617366fc659b0e Mon Sep 17 00:00:00 2001 From: Blake Nahin Date: Sat, 23 Oct 2021 17:19:17 -0500 Subject: [PATCH 3/9] Academy Exam Result Notifications --- app/Classes/VATUSADiscord.php | 40 +++++++++++++++++-- .../Commands/SendAcademyRatingExamEmails.php | 34 ++++++++++++---- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/app/Classes/VATUSADiscord.php b/app/Classes/VATUSADiscord.php index 68230069..0525bb71 100644 --- a/app/Classes/VATUSADiscord.php +++ b/app/Classes/VATUSADiscord.php @@ -24,13 +24,22 @@ class VATUSADiscord public const NOTIFY_EMAIL = 1; public const NOTIFY_DISCORD = 2; public const NOTIFY_BOTH = 3; - public const NOTFIY_NONE = 0; + /** + * VATUSA Discord constructor. + */ public function __construct() { $this->guzzle = new Client(['base_uri' => config('services.discord.botServer')]); } + /** + * Get the user's Notification option for a type. + * @param \App\User $user + * @param string $type + * + * @return int + */ public function getNotificationOption(User $user, string $type): int { $record = NotificationSetting::where('cid', $user->cid)->where('type', $type)->first(); @@ -38,13 +47,26 @@ public function getNotificationOption(User $user, string $type): int return $record ? $record->option : 0; } - public function getFacilityNotificationChannel(Facility $facility, string $type) + /** + * Get the facility's Notification channel for a type. + * @param \App\Facility $facility + * @param string $type + * + * @return int + */ + public function getFacilityNotificationChannel(Facility $facility, string $type): int { $record = FacilityNotificationChannel::where('facility', $facility->id)->where('type', $type)->first(); return $record && $facility->discord_guild ? $record->channel : 0; } + /** + * Get an array of all the user's notification options. + * @param \App\User $user + * + * @return array + */ public function getAllUserNotificationOptions(User $user): array { $records = NotificationSetting::where('cid', $user->cid)->get(); @@ -56,6 +78,12 @@ public function getAllUserNotificationOptions(User $user): array return $return; } + /** + * Get an array of all the facility's notification channels. + * @param \App\Facility $facility + * + * @return array + */ public function getAllFacilityNotificationChannels(Facility $facility): array { $records = FacilityNotificationChannel::where('facility', $facility->id)->get(); @@ -154,7 +182,13 @@ public function getUserAdminGuilds(User $user): array return []; } - public function getGuildChannels(string $guild) + /** + * Get an array of all the channels in a Guild. + * @param string $guild + * + * @return array + */ + public function getGuildChannels(string $guild): array { try { $response = $this->sendRequest("GET", "/guild/$guild/channels"); diff --git a/app/Console/Commands/SendAcademyRatingExamEmails.php b/app/Console/Commands/SendAcademyRatingExamEmails.php index d6bbcf58..d987413f 100644 --- a/app/Console/Commands/SendAcademyRatingExamEmails.php +++ b/app/Console/Commands/SendAcademyRatingExamEmails.php @@ -4,8 +4,9 @@ use App\AcademyBasicExamEmail; use App\AcademyExamAssignment; +use App\Classes\VATUSADiscord; use App\Classes\VATUSAMoodle; -use App\Http\Middleware\PrivateCORS; +use App\Facility; use App\Mail\AcademyExamSubmitted; use App\User; use Carbon\Carbon; @@ -30,6 +31,7 @@ class SendAcademyRatingExamEmails extends Command protected $description = 'Checks for final exam attempts and sends emails.'; private $moodle; + private $notify; /** * Create a new command instance. @@ -40,6 +42,7 @@ public function __construct() { parent::__construct(); $this->moodle = new VATUSAMoodle(); + $this->notify = new VATUSADiscord(); } /** @@ -54,6 +57,7 @@ public function handle() $student = $assignment->student; $studentName = $student->name; $instructor = $assignment->instructor; + $instructorName = $instructor->fullname(); $quizId = $assignment->quiz_id; $attemptEmailsSent = $assignment->attempt_emails_sent ? explode(',', $assignment->attempt_emails_sent) : []; @@ -90,14 +94,30 @@ public function handle() $grade = $attempt['grade']; $passed = $grade >= $passingGrade; - $result = compact('testName', 'studentName', 'attemptNum', 'grade', + $result = compact('testName', 'studentName', 'instructorName', 'attemptNum', 'grade', 'passed', 'passingGrade', 'attemptId'); + $mail = Mail::bcc(['vatusa3@vatusa.net', 'vatusa13@vatusa.net']); + if ($hasUser = $this->notify->userWantsNotification($student, "academyExamResult", "email")) { + $mail->to($student); + } + if ($this->notify->userWantsNotification($instructor, "academyExamResult", "email")) { + $hasUser ? $mail->cc($instructor) : $mail->to($instructor); + } - $mail = Mail::to($student)->cc($instructor); - //if ($attemptNum == 3 && !$passed) { - $mail->bcc(['vatusa3@vatusa.net', 'vatusa13@vatusa.net']); - //} $mail->queue(new AcademyExamSubmitted($result)); + $studentId = $this->notify->userWantsNotification($student, "academyExamResult", + "discord") ? $student->discord_id : 0; + $instructorId = $this->notify->userWantsNotification($instructor, "academyExamResult", + "discord") ? $instructor->discord_id : 0; + if ($studentId || $instructorId) { + $this->notify->sendNotification("academyExamResult", "dm", + array_merge($result, compact('studentId', 'instructorId'))); + } + if ($channel = $this->notify->getFacilityNotificationChannel(Facility::find($student->facility), + "academyExamResult")) { + $this->notify->sendNotification("academyExamResult", "channel", $result, + $student->facility->discord_guild, $channel); + } if ($passed) { $assignment->delete(); @@ -159,7 +179,7 @@ public function handle() //} $mail->queue(new AcademyExamSubmitted($result)); - if($passed) { + if ($passed) { $student->flag_needbasic = 0; $student->save(); } From f225cf93263fbce2d176c11496593c4b3cbc4dfa Mon Sep 17 00:00:00 2001 From: Blake Nahin Date: Sun, 7 Nov 2021 18:49:25 -0600 Subject: [PATCH 4/9] Finish Exam Notifications --- .../Commands/SendAcademyRatingExamEmails.php | 41 ++++++-- app/Exam.php | 8 +- app/Facility.php | 2 +- .../Controllers/API/v2/AcademyController.php | 47 ++++++++- .../Controllers/API/v2/ExamController.php | 97 +++++++++++++------ config/services.php | 25 +++-- resources/views/emails/exam/assign.blade.php | 2 +- 7 files changed, 165 insertions(+), 57 deletions(-) diff --git a/app/Console/Commands/SendAcademyRatingExamEmails.php b/app/Console/Commands/SendAcademyRatingExamEmails.php index d987413f..e6171f9a 100644 --- a/app/Console/Commands/SendAcademyRatingExamEmails.php +++ b/app/Console/Commands/SendAcademyRatingExamEmails.php @@ -48,7 +48,7 @@ public function __construct() /** * Execute the console command. * - * @return mixed + * @return void * @throws \Exception */ public function handle() @@ -60,6 +60,13 @@ public function handle() $instructorName = $instructor->fullname(); $quizId = $assignment->quiz_id; $attemptEmailsSent = $assignment->attempt_emails_sent ? explode(',', $assignment->attempt_emails_sent) : []; + $ta = $assignment->student->facilityObj->ta(); + if (!$ta) { + $ta = $assignment->student->facilityObj->datm(); + } + if (!$ta) { + $ta = $assignment->student->facilityObj->atm(); + } if ($assignment->created_at->diffInDays(Carbon::now()) > 30) { log_action($assignment->student->cid, @@ -102,6 +109,10 @@ public function handle() } if ($this->notify->userWantsNotification($instructor, "academyExamResult", "email")) { $hasUser ? $mail->cc($instructor) : $mail->to($instructor); + $hasUser = true; + } + if ($ta && $this->notify->userWantsNotification($ta, "academyExamResult", "email")) { + $hasUser ? $mail->cc($instructor) : $mail->to($ta); } $mail->queue(new AcademyExamSubmitted($result)); @@ -109,14 +120,20 @@ public function handle() "discord") ? $student->discord_id : 0; $instructorId = $this->notify->userWantsNotification($instructor, "academyExamResult", "discord") ? $instructor->discord_id : 0; + $taId = $ta && $this->notify->userWantsNotification($instructor, "academyExamResult", + "discord") ? $ta->discord_id : 0; if ($studentId || $instructorId) { $this->notify->sendNotification("academyExamResult", "dm", array_merge($result, compact('studentId', 'instructorId'))); } + if ($taId) { + $this->notify->sendNotification("academyExamResult", "dm", + array_merge($result, ['instructorId' => $taId])); + } if ($channel = $this->notify->getFacilityNotificationChannel(Facility::find($student->facility), "academyExamResult")) { $this->notify->sendNotification("academyExamResult", "channel", $result, - $student->facility->discord_guild, $channel); + $student->facilityObj->discord_guild, $channel); } if ($passed) { @@ -173,12 +190,22 @@ public function handle() $result = compact('testName', 'studentName', 'attemptNum', 'grade', 'passed', 'passingGrade', 'attemptId'); - $mail = Mail::to($student); - //if ($attemptNum == 3 && !$passed) { - $mail->bcc(['vatusa3@vatusa.net', 'vatusa13@vatusa.net']); - //} + $mail = Mail::bcc(['vatusa3@vatusa.net', 'vatusa13@vatusa.net']); + if ($hasUser = $this->notify->userWantsNotification($student, "academyExamResult", "email")) { + $mail->to($student); + } $mail->queue(new AcademyExamSubmitted($result)); - + $studentId = $this->notify->userWantsNotification($student, "academyExamResult", + "discord") ? $student->discord_id : 0; + if ($studentId) { + $this->notify->sendNotification("academyExamResult", "dm", + array_merge($result, compact('studentId'))); + } + if ($channel = $this->notify->getFacilityNotificationChannel(Facility::find($student->facility), + "academyExamResult")) { + $this->notify->sendNotification("academyExamResult", "channel", $result, + $student->facilityObj->discord_guild, $channel); + } if ($passed) { $student->flag_needbasic = 0; $student->save(); diff --git a/app/Exam.php b/app/Exam.php index 8413724c..3dd2de30 100644 --- a/app/Exam.php +++ b/app/Exam.php @@ -24,19 +24,19 @@ class Exam extends Model protected $table = "exams"; public function questions() { - return $this->hasMany('App\ExamQuestions', 'exam_id'); + return $this->hasMany(ExamQuestions::class, 'exam_id'); } public function facility() { - return $this->hasOne('App\Facility', 'id', 'facility_id'); + return $this->hasOne(Facility::class, 'id', 'facility_id'); } public function results() { - return $this->hasMany('App\ExamResults', 'exam_id', 'id'); + return $this->hasMany(ExamResults::class, 'exam_id', 'id'); } public function CBT() { - return $this->hasOne("App\TrainingBlock", "id", "cbt_required"); + return $this->hasOne(TrainingBlock::class, 'id', 'cbt_required'); } public function CBTComplete(User $user = null) { diff --git a/app/Facility.php b/app/Facility.php index ebd6f0d6..90783259 100644 --- a/app/Facility.php +++ b/app/Facility.php @@ -54,7 +54,7 @@ public function datm() public function ta() { - return $this->hasOne('App\User', 'cid', 'ta')->first(); + return $this->hasOne(User::class, 'cid', 'ta')->first(); } public function ec() diff --git a/app/Http/Controllers/API/v2/AcademyController.php b/app/Http/Controllers/API/v2/AcademyController.php index d24f868a..711ca521 100644 --- a/app/Http/Controllers/API/v2/AcademyController.php +++ b/app/Http/Controllers/API/v2/AcademyController.php @@ -5,10 +5,10 @@ use App\AcademyExamAssignment; use App\Action; +use App\Classes\VATUSADiscord; use App\Classes\VATUSAMoodle; use App\Facility; use App\Helpers\AuthHelper; -use App\Helpers\EmailHelper; use App\Helpers\Helper; use App\Helpers\RoleHelper; use App\Mail\AcademyRatingCourseEnrolled; @@ -156,9 +156,46 @@ public function postEnroll(Request $request, int $courseId): Response } $assignment->save(); - Mail::to($user->email) - ->cc(Auth::user()->email) - ->queue(new AcademyRatingCourseEnrolled($assignment)); + $notify = new VATUSADiscord(); + $to = []; + $facility = Auth::user()->facility(); + $ta = $facility->ta(); + if (!$ta) { + $ta = $facility->datm(); + } + if (!$ta) { + $ta = $facility->atm(); + } + if (!$ta || $ta->cid == Auth::user()->cid) { + $ta = null; + } + if ($notify->userWantsNotification($user, "academyExamCourseEnrolled", "email")) { + $to[] = $user->email; + } + if ($notify->userWantsNotification(Auth::user(), "academyExamCourseEnrolled", "email")) { + $to[] = Auth::user()->email; + } + if ($ta && $notify->userWantsNotification($ta, "academyExamCourseEnrolled", "email")) { + $to[] = $ta->email; + } + if (count($to)) { + Mail::to($to)->queue(new AcademyRatingCourseEnrolled($assignment)); + } + + $studentId = $notify->userWantsNotification($user, "academyExamCourseEnrolled", + "discord") ? $user->discord_id : 0; + $staffId = $ta && $notify->userWantsNotification($ta, "academyExamCourseEnrolled", + "discord") ? $ta->discord_id : 0; + if ($studentId || $staffId) { + $notify->sendNotification("academyExamCourseEnrolled", "dm", + array_merge($assignment->load(['rating', 'student', 'instructor'])->toArray(), + compact('studentId', 'staffId'))); + } + if ($channel = $notify->getFacilityNotificationChannel($facility, "academyExamCourseEnrolled")) { + $notify->sendNotification("academyExamCourseEnrolled", "channel", $assignment->toArray(), + $facility->discord_guild, + $channel); + } $log = new Action(); $log->to = $user->cid; @@ -226,7 +263,7 @@ public function getTranscript(Request $request, User $user) } if (!$validKeyHome && !$validKeyVisit && !(Auth::check() && ($user->facility == Auth::user()->facility || $user->visits()->where('facility', - Auth::user()->facility)->exists()) && (RoleHelper::isMentor() || RoleHelper::isInstructor() || RoleHelper::isSeniorStaff()) || RoleHelper::isVATUSAStaff())) { + Auth::user()->facility)->exists()) && (RoleHelper::isMentor() || RoleHelper::isInstructor() || RoleHelper::isSeniorStaff()) || RoleHelper::isVATUSAStaff())) { return response()->forbidden(); } diff --git a/app/Http/Controllers/API/v2/ExamController.php b/app/Http/Controllers/API/v2/ExamController.php index 880fa315..45a78b99 100644 --- a/app/Http/Controllers/API/v2/ExamController.php +++ b/app/Http/Controllers/API/v2/ExamController.php @@ -2,7 +2,7 @@ namespace App\Http\Controllers\API\v2; -use App\Action; +use App\Classes\Helper; use App\Classes\VATUSADiscord; use App\ExamAssignment; use App\ExamQuestions; @@ -185,7 +185,7 @@ public function postSubmit(Request $request) $result->exam_id = $questions['id']; $result->exam_name = $questions['name']; $result->cid = \Auth::user()->cid; - $result->date = \Carbon\Carbon::now(); + $result->date = Carbon::now(); $result->save(); foreach ($questions['questions'] as $question) { @@ -233,27 +233,21 @@ public function postSubmit(Request $request) if ($notify->userWantsNotification(Auth::user(), "legacyExamResult", "email")) { $to[] = Auth::user()->email; } - if ($assign->instructor_id > 111111) { - $instructor = User::find($assign->instructor_id); - if ($instructor) { - if ($notify->userWantsNotification($instructor, "legacyExamResult", "email")) { - $to[] = $instructor->email; - } + if ($instructor = User::find($assign->instructor_id)) { + if ($notify->userWantsNotification($instructor, "legacyExamResult", "email")) { + $to[] = $instructor->email; } } if ($exam->facility_id != "ZAE" && $ta && $notify->userWantsNotification($ta, "legacyExamResult", "email")) { $to[] = $ta->email; } - $log = new Action(); - $log->to = Auth::user()->cid; - $log->log = "Exam (" . $exam->facility_id . ") " . $exam->name . " completed. Score $correct/$possible ($score%)."; - $log->log .= ($result->passed) ? " Passed." : " Not Passed."; - $log->save(); + log_action(Auth::user()->cid, + "Exam ($exam->facility_id) $exam->name completed. Score $correct/$possible ($score%). " . ($result->passed ? " Passed." : " Not Passed.")); $data = [ 'passed' => $result->passed, - 'exam_name' => "(" . $exam->facility_id . ") " . $exam->name, + 'exam_name' => "($exam->facility_id) $exam->name", 'result_id' => $result->id, 'instructor_name' => $instructor ? $instructor->fullname() : 'N/A', 'correct' => $correct, @@ -270,9 +264,13 @@ public function postSubmit(Request $request) $taId = $ta && (!$instructor || $ta->cid !== $instructor->cid) && $notify->userWantsNotification($ta, "legacyExamResult", "discord") ? $ta->discord_id : 0; - if ($studentId || $instructorId || $taId) { + if ($studentId || $instructorId) { + $notify->sendNotification("legacyExamResult", "dm", + array_merge($data, compact('studentId', 'instructorId'))); + } + if ($taId) { $notify->sendNotification("legacyExamResult", "dm", - array_merge($data, compact('studentId', 'instructorId', 'taId'))); + array_merge($data, compact(['instructorId' => $taId]))); } if ($channel = $notify->getFacilityNotificationChannel($facility, "legacyExamResult")) { @@ -300,7 +298,7 @@ public function postSubmit(Request $request) $reassign->cid = $assign->cid; $reassign->instructor_id = $assign->instructor_id; $reassign->exam_id = $assign->exam_id; - $reassign->reassign_date = \Carbon\Carbon::now()->addDays($exam->retake_period); + $reassign->reassign_date = Carbon::now()->addDays($exam->retake_period); $reassign->save(); $data['reassign'] = $exam->retake_period; @@ -921,7 +919,7 @@ function postExamAssign( $examid, $cid ) { - if (!\Auth::check()) { + if (!Auth::check()) { return response()->api(generate_error("Unauthorized"), 401); } if (!RoleHelper::isSeniorStaff() && @@ -948,14 +946,17 @@ function postExamAssign( } $days = $request->input("expire", 7); + $instructor = Auth::user(); + $student = User::find($cid); + $facility = $exam->facility; if (!isTest()) { $ea = new ExamAssignment(); $ea->cid = $cid; - $ea->instructor_id = \Auth::user()->cid; + $ea->instructor_id = $instructor->cid; $ea->exam_id = $examid; $ea->assigned_date = Carbon::now(); - $ea->expire_date = Carbon::create()->addDays($days); + $ea->expire_date = $endDate = Carbon::now()->addDays($days); $ea->save(); if ($exam->cbt_required > 0) { @@ -963,27 +964,59 @@ function postExamAssign( } $data = [ - 'exam_name' => "(" . $exam->facility_id . ") " . $exam->name, - 'instructor_name' => \Auth::user()->fullname(), - 'end_date' => Carbon::create()->addDays($days)->toDayDateTimeString(), - 'student_name' => User::find($cid)->fullname(), + 'exam_name' => "(" . $exam->facility->id . ") " . $exam->name, + 'instructor_name' => $instructor->fullname(), + 'end_date' => $endDate, + 'student_name' => $student->fullname(), 'cbt_required' => $exam->cbt_required, 'cbt_facility' => (isset($cbt)) ? $cbt->facility_id : null, 'cbt_block' => (isset($cbt)) ? $exam->cbt_reuqired : null ]; - $to[] = User::find($cid)->email; - $to[] = \Auth::user()->email; - if ($exam->facility_id != "ZAE") { - $to[] = $exam->facility_id . "-TA@vatusa.net"; + + $notify = new VATUSADiscord(); + $to = array(); + + $ta = $facility->ta(); + if (!$ta) { + $ta = $facility->datm(); + } + if (!$ta) { + $ta = $facility->atm(); + } + if (!$ta || $ta->cid == $instructor->cid) { + $ta = null; } + if ($notify->userWantsNotification($student, "legacyExamAssigned", "email")) { + $to[] = $student->email; + } + if ($notify->userWantsNotification($instructor, "legacyExamAssigned", "email")) { + $to[] = $instructor->email; + } + if ($exam->facility_id != "ZAE") { + if ($ta && $notify->userWantsNotification($ta, "legacyExamAssigned", "email")) { + $to[] = $ta->email; + } + } - Mail::to($to)->queue(new ExamAssigned($data)); + if (count($to)) { + Mail::to($to)->queue(new ExamAssigned($data)); + } - EmailHelper::sendEmailFacilityTemplate($to, "Exam Assigned", $exam->facility_id, "examassigned", $data); + $student_id = $notify->userWantsNotification($student, "legacyExamAssigned", + "discord") ? $student->discord_id : 0; + $staff_id = $ta && $notify->userWantsNotification($ta, "legacyExamAssigned", + "discord") ? $ta->discord_id : 0; + if ($student_id || $staff_id) { + $notify->sendNotification('legacyExamAssigned', "dm", + array_merge($data, compact('staff_id', 'student_id'))); + } + if ($channel = $notify->getFacilityNotificationChannel($facility, "legacyExamAssigned")) { + $notify->sendNotification("legacyExamAssigned", "channel", $data, $facility->discord_guild, $channel); + } - log_action($cid, "Exam (" . $exam->facility_id . ") " . $exam->name . - " assigned by " . \Auth::user()->fullname() . ", expires " . $data['end_date']); + log_action($cid, + "Exam {$data['exam_name']} assigned by {$data['instructor_name']}, expires {$endDate->format('m/d/Y H:i')}."); } return response()->api(['status' => 'OK']); diff --git a/config/services.php b/config/services.php index 36f88176..10e2a287 100644 --- a/config/services.php +++ b/config/services.php @@ -20,7 +20,7 @@ ], 'ses' => [ - 'key' => env('SES_KEY'), + 'key' => env('SES_KEY'), 'secret' => env('SES_SECRET'), 'region' => 'us-east-1', ], @@ -30,15 +30,26 @@ ], 'stripe' => [ - 'model' => App\User::class, - 'key' => env('STRIPE_KEY'), + 'model' => App\User::class, + 'key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET'), ], - 'moodle' => [ - 'url' => env('MOODLE_URL', 'https://academy.vatusa.net'), - 'token' => env('MOODLE_TOKEN'), + 'moodle' => [ + 'url' => env('MOODLE_URL', 'https://academy.vatusa.net'), + 'token' => env('MOODLE_TOKEN'), 'token_sso' => env('MOODLE_TOKEN_SSO') - ] + ], + + 'discord' => [ + 'client_id' => env('DISCORD_CLIENT_ID'), + 'client_secret' => env('DISCORD_CLIENT_SECRET'), + 'redirect' => env('DISCORD_REDIRECT'), + 'botServer' => env('DISCORD_BOT_SERVER', 'http://discord-bot:3000'), + 'guildId' => env('DISCORD_GUILD_ID'), + 'botToken' => env('DISCORD_BOT_TOKEN'), + 'botPermissions' => env('DISCORD_BOT_PERMISSIONS'), + 'botSecret' => env('DISCORD_BOT_SERVER_SECRET') + ], ]; diff --git a/resources/views/emails/exam/assign.blade.php b/resources/views/emails/exam/assign.blade.php index aae47041..3fdccdcf 100644 --- a/resources/views/emails/exam/assign.blade.php +++ b/resources/views/emails/exam/assign.blade.php @@ -4,7 +4,7 @@ Hello {{ $data['student_name'] }},

This email is to inform you that you have been assigned exam {{ $data['exam_name'] }} by instructor {{ $data['instructor_name'] }}. You have - until {{ $data['end_date'] }} US Central Time to complete the examination before it expires. + until {{ $data['end_date']->toDayDateTimeString() }} US Central Time to complete the examination before it expires.

From 5abef9dbf3b49640ecdad06e64927d35f4ac47ce Mon Sep 17 00:00:00 2001 From: Blake Nahin Date: Mon, 6 Dec 2021 21:36:10 -0600 Subject: [PATCH 5/9] Begin Ticket Endpoints --- app/Http/Controllers/API/v2/APIController.php | 10 + .../Controllers/API/v2/SupportController.php | 299 +++++++++++++++--- app/Http/Kernel.php | 35 +- app/Ticket.php | 69 ++++ app/TicketHistory.php | 15 + app/TicketNotes.php | 15 + app/TicketReplies.php | 28 ++ composer.json | 2 +- routes/api-v2.php | 14 +- 9 files changed, 424 insertions(+), 63 deletions(-) create mode 100644 app/Ticket.php create mode 100644 app/TicketHistory.php create mode 100644 app/TicketNotes.php create mode 100644 app/TicketReplies.php diff --git a/app/Http/Controllers/API/v2/APIController.php b/app/Http/Controllers/API/v2/APIController.php index 84671096..ca836d7a 100644 --- a/app/Http/Controllers/API/v2/APIController.php +++ b/app/Http/Controllers/API/v2/APIController.php @@ -23,6 +23,7 @@

Method security, if applicable, is indicated in brackets at the end of each endpoint title.

Security classification:

  • Private: CORS Restricted (Internal)
  • +
  • Bot: Restricted to the Discord Bot by JWT
  • Auth: Accepts Session Cookie or JWT
  • Key: Accepts API Key, Session Cookie, or JWT

@@ -73,6 +74,15 @@ public function __construct() * description="JSON Web Token translated from Laravel session" * ) */ +/** + * @SWG\SecurityScheme( + * securityDefinition="bot", + * type="apiKey", + * in="header", + * name="JSON Web Token for Discord Bot", + * description="JSON Web Token issued to the Discord Bot" + * ) + */ /** * @SWG\SecurityScheme( * securityDefinition="session", diff --git a/app/Http/Controllers/API/v2/SupportController.php b/app/Http/Controllers/API/v2/SupportController.php index bfc6b0c4..1fc31773 100644 --- a/app/Http/Controllers/API/v2/SupportController.php +++ b/app/Http/Controllers/API/v2/SupportController.php @@ -2,12 +2,19 @@ namespace App\Http\Controllers\API\v2; +use App\Classes\EmailHelper; +use App\Classes\VATUSADiscord; use App\Facility; use App\Helpers\RoleHelper; use App\KnowledgebaseQuestions; +use App\Mail\TicketClosed; use App\Role; +use App\Ticket; +use App\TicketHistory; +use App\User; use Illuminate\Http\Request; use App\KnowledgebaseCategories; +use Illuminate\Support\Facades\Mail; /** * Class SupportController @@ -38,7 +45,8 @@ class SupportController extends APIController * * @return \Illuminate\Http\JsonResponse */ - public function getKBs(Request $request) { + public function getKBs(Request $request) + { return response()->ok(KnowledgebaseCategories::orderBy('name')->get()->toArray()); } @@ -55,7 +63,8 @@ public function getKBs(Request $request) { * response="400", * description="Malformed request, check format of position, expDate", * @SWG\Schema(ref="#/definitions/error"), - * examples={{"application/json":{"status"="error","message"="Invalid position"}},{"application/json":{"status"="error","message"="Invalid expDate"}}}, + * examples={{"application/json":{"status"="error","message"="Invalid + * position"}},{"application/json":{"status"="error","message"="Invalid expDate"}}}, * ), * @SWG\Response( * response="401", @@ -81,10 +90,17 @@ public function getKBs(Request $request) { * * @return \Illuminate\Http\JsonResponse */ - public function postKB(Request $request) { - if (!$request->has("name")) return response()->malformed(); - if (!\Auth::check()) return response()->unauthorized(); - if (!RoleHelper::isVATUSAStaff()) return response()->forbidden(); + public function postKB(Request $request) + { + if (!$request->has("name")) { + return response()->malformed(); + } + if (!\Auth::check()) { + return response()->unauthorized(); + } + if (!RoleHelper::isVATUSAStaff()) { + return response()->forbidden(); + } $cat = new KnowledgebaseCategories(); $cat->name = $request->input("name"); @@ -107,7 +123,8 @@ public function postKB(Request $request) { * response="400", * description="Malformed request, check format of position, expDate", * @SWG\Schema(ref="#/definitions/error"), - * examples={{"application/json":{"status"="error","message"="Invalid position"}},{"application/json":{"status"="error","message"="Invalid expDate"}}}, + * examples={{"application/json":{"status"="error","message"="Invalid + * position"}},{"application/json":{"status"="error","message"="Invalid expDate"}}}, * ), * @SWG\Response( * response="401", @@ -136,13 +153,22 @@ public function postKB(Request $request) { * ) * ) */ - public function putKB(Request $request, int $id) { - if (!$request->has("name")) return response()->malformed(); - if (!\Auth::check()) return response()->unauthorized(); - if (!RoleHelper::isVATUSAStaff()) return response()->forbidden(); + public function putKB(Request $request, int $id) + { + if (!$request->has("name")) { + return response()->malformed(); + } + if (!\Auth::check()) { + return response()->unauthorized(); + } + if (!RoleHelper::isVATUSAStaff()) { + return response()->forbidden(); + } $cat = KnowledgebaseCategories::find($id); - if (!$cat) return response()->notfound(); + if (!$cat) { + return response()->notfound(); + } $cat->name = $request->input("name"); $cat->save(); @@ -198,18 +224,27 @@ public function putKB(Request $request, int $id) { * @return \Illuminate\Http\JsonResponse * @throws \Exception */ - public function deleteKB(Request $request, int $id) { - if (!$request->has("name")) return response()->malformed(); - if (!\Auth::check()) return response()->unauthorized(); - if (!RoleHelper::isVATUSAStaff()) return response()->forbidden(); + public function deleteKB(Request $request, int $id) + { + if (!$request->has("name")) { + return response()->malformed(); + } + if (!\Auth::check()) { + return response()->unauthorized(); + } + if (!RoleHelper::isVATUSAStaff()) { + return response()->forbidden(); + } $cat = KnowledgebaseCategories::find($id); - foreach($cat->questions as $q) { + foreach ($cat->questions as $q) { $q->delete(); } - if (!$cat) return response()->notfound(); + if (!$cat) { + return response()->notfound(); + } $cat->delete(); @@ -231,7 +266,8 @@ public function deleteKB(Request $request, int $id) { * response="400", * description="Malformed request, check format of position, expDate", * @SWG\Schema(ref="#/definitions/error"), - * examples={{"application/json":{"status"="error","message"="Invalid position"}},{"application/json":{"status"="error","message"="Invalid expDate"}}}, + * examples={{"application/json":{"status"="error","message"="Invalid + * position"}},{"application/json":{"status"="error","message"="Invalid expDate"}}}, * ), * @SWG\Response( * response="401", @@ -264,12 +300,21 @@ public function deleteKB(Request $request, int $id) { * * @return \Illuminate\Http\JsonResponse */ - public function postKBQuestion(Request $request, $id) { - if (!$request->has("name")) return response()->malformed(); - if (!\Auth::check()) return response()->unauthorized(); - if (!RoleHelper::isVATUSAStaff()) return response()->forbidden(); + public function postKBQuestion(Request $request, $id) + { + if (!$request->has("name")) { + return response()->malformed(); + } + if (!\Auth::check()) { + return response()->unauthorized(); + } + if (!RoleHelper::isVATUSAStaff()) { + return response()->forbidden(); + } $cat = KnowledgebaseCategories::find($id); - if (!$cat) return response()->notfound(); + if (!$cat) { + return response()->notfound(); + } $lastQ = KnowledgebaseQuestions::where('category_id', $cat->id)->orderBy('order', 'DESC')->first(); @@ -302,7 +347,8 @@ public function postKBQuestion(Request $request, $id) { * response="400", * description="Malformed request, check format of position, expDate", * @SWG\Schema(ref="#/definitions/error"), - * examples={{"application/json":{"status"="error","message"="Invalid position"}},{"application/json":{"status"="error","message"="Invalid expDate"}}}, + * examples={{"application/json":{"status"="error","message"="Invalid + * position"}},{"application/json":{"status"="error","message"="Invalid expDate"}}}, * ), * @SWG\Response( * response="401", @@ -336,14 +382,25 @@ public function postKBQuestion(Request $request, $id) { * * @return \Illuminate\Http\JsonResponse */ - public function putKBQuestion(Request $request, int $cid, int $qid) { - if (!$request->has("name")) return response()->malformed(); - if (!\Auth::check()) return response()->unauthorized(); - if (!RoleHelper::isVATUSAStaff()) return response()->forbidden(); + public function putKBQuestion(Request $request, int $cid, int $qid) + { + if (!$request->has("name")) { + return response()->malformed(); + } + if (!\Auth::check()) { + return response()->unauthorized(); + } + if (!RoleHelper::isVATUSAStaff()) { + return response()->forbidden(); + } $cat = KnowledgebaseCategories::find($cid); - if (!$cat) return response()->notfound(); + if (!$cat) { + return response()->notfound(); + } $q = KnowledgebaseQuestions::find($qid); - if (!$q || $q->category_id != $cat->id) return response()->notFound(); + if (!$q || $q->category_id != $cat->id) { + return response()->notFound(); + } if ($request->has("question")) { $q->question = $request->input("question"); @@ -355,7 +412,9 @@ public function putKBQuestion(Request $request, int $cid, int $qid) { if ($request->has("category")) { $nc = KnowledgebaseCategories::find($request->input("category")); - if (!$nc) return response()->notfound(); + if (!$nc) { + return response()->notfound(); + } $q->cat_id = $nc->id; } @@ -418,15 +477,26 @@ public function putKBQuestion(Request $request, int $cid, int $qid) { * @return \Illuminate\Http\JsonResponse * @throws \Exception */ - public function deleteKBQuestion(Request $request, int $categoryid, int $questionid) { - if (!$request->has("name")) return response()->malformed(); - if (!\Auth::check()) return response()->unauthorized(); - if (!RoleHelper::isVATUSAStaff()) return response()->forbidden(); + public function deleteKBQuestion(Request $request, int $categoryid, int $questionid) + { + if (!$request->has("name")) { + return response()->malformed(); + } + if (!\Auth::check()) { + return response()->unauthorized(); + } + if (!RoleHelper::isVATUSAStaff()) { + return response()->forbidden(); + } $cat = KnowledgebaseCategories::find($categoryid); - if (!$cat) return response()->notfound(); + if (!$cat) { + return response()->notfound(); + } $q = KnowledgebaseQuestions::find($questionid); - if (!$q || $q->category_id != $cat->id) return response()->notfound(); + if (!$q || $q->category_id != $cat->id) { + return response()->notfound(); + } $q->delete(); @@ -462,7 +532,8 @@ public function deleteKBQuestion(Request $request, int $categoryid, int $questio * * @return \Illuminate\Http\JsonResponse */ - public function getTicketDepts(Request $request) { + public function getTicketDepts(Request $request) + { $depts = [ ["id" => "ZHQ", "name" => "VATUSA Headquarters"] ]; @@ -470,7 +541,7 @@ public function getTicketDepts(Request $request) { $f = Facility::where('active', 1)->orderBy('name')->get(); foreach ($f as $fac) { $depts[] = [ - "id" => $fac->id, + "id" => $fac->id, "name" => $fac->name ]; } @@ -508,21 +579,26 @@ public function getTicketDepts(Request $request) { * * @return \Illuminate\Http\JsonResponse */ - public function getTicketDeptStaff(Request $request, string $dept) { + public function getTicketDeptStaff(Request $request, string $dept) + { $fac = Facility::find($dept); - if (!$fac) return response()->notfound(); + if (!$fac) { + return response()->notfound(); + } - $staff = []; $chked = []; $i = 0; + $staff = []; + $chked = []; + $i = 0; foreach ( Role::where('facility', $fac->id) - ->orderBy(\DB::raw('field(role, "ATM","DATM","TA","EC","FE","WM","INS","MTR")')) - ->orderBy('role')->get() + ->orderBy(\DB::raw('field(role, "ATM","DATM","TA","EC","FE","WM","INS","MTR")')) + ->orderBy('role')->get() as $role) { if (!isset($chked[$role->cid])) { $staff[$i] = [ - 'cid' => $role->cid, + 'cid' => $role->cid, 'role' => $role->role, 'name' => $role->user->fullname() ]; @@ -537,4 +613,135 @@ public function getTicketDeptStaff(Request $request, string $dept) { return response()->ok(["staff" => $staff]); } // + + + /** + * @SWG\Delete( + * path="/support/tickets/{id}", + * summary="Close ticket. [Bot]", + * description="Close ticket.", + * produces={"application/json"}, + * tags={"support"}, + * security={"bot"}, + * @SWG\Parameter(in="path", name="id", type="integer", description="Ticket ID"), + * @SWG\Parameter(in="formData", name="user_id", type="integer", description="User ID"), + * @SWG\Response( + * response="401", + * description="Unauthorized", + * @SWG\Schema(ref="#/definitions/error"), + * examples={"application/json":{"status"="error","msg"="Unauthorized"}}, + * ), + * @SWG\Response( + * response="403", + * description="Forbidden", + * @SWG\Schema(ref="#/definitions/error"), + * examples={"application/json":{"status"="error","msg"="Forbidden"}}, + * ), + * @SWG\Response( + * response="200", + * description="OK", + * @SWG\Schema( + * ref="#/definitions/OK", + * ), + * ) + * ) + * @param \Illuminate\Http\Request $request + * @param \App\Ticket $ticket + * + * @return \Illuminate\Http\JsonResponse + */ + public function closeTicket(Request $request, Ticket $ticket) + { + $userId = $request->user_id; + $user = User::where('discord_id', $userId)->first(); + if (!$user) { + return response()->api(generate_error("Your Discord account is not linked to a VATUSA account."), 403); + } + + if ($ticket->submitter->cid == $user->cid || RoleHelper::isFacilityStaff($user->cid, + $ticket->facility) || RoleHelper::isInstructor($user->cid, $ticket->facility)) { + $ticket->status = "Closed"; + $history = new TicketHistory(); + $history->ticket_id = $ticket->id; + $history->entry = $user->fullname() . " (" . $user->cid . ") closed the ticket [via Discord]."; + $history->save(); + $ticket->save(); + + $discord = new VATUSADiscord(); + if ($discord->userWantsNotification($ticket->submitter, "ticketClosed", "email")) { + Mail::to($ticket->submitter->email)->queue(new TicketClosed($ticket)); + } + if ($discord->userWantsNotification($ticket->submitter, "ticketClosed", "discord")) { + $discord->sendNotification("ticketClosed", "dm", + array_merge($ticket->toArray(), ['userId' => $ticket->submitter->discord_id])); + } + if ($channel = $discord->getFacilityNotificationChannel($ticket->facility()->first(), "ticketClosed")) { + $discord->sendNotification("ticketClosed", "channel", $ticket->toArray(), + $ticket->facility === "ZHQ" ? config('services.discord.guildId') : $ticket->facility()->discord_guild_id, + $channel); + } + + return response()->ok(); + } + + return response()->api(generate_error("You do not have permission to close this ticket."), 403); + } + + /** + * @SWG\Put( + * path="/support/tickets/{id}", + * summary="Assign ticket. [Bot]", + * description="Assign ticket.", + * produces={"application/json"}, + * tags={"support"}, + * security={"bot"}, + * @SWG\Parameter(in="path", name="id", type="integer", description="Ticket ID"), + * @SWG\Parameter(in="formData", name="cid", type="integer", description="CID to assign ticket to"), + * @SWG\Parameter(in="formData", name="user_id", type="integer", description="User ID"), + * @SWG\Response( + * response="401", + * description="Unauthorized", + * @SWG\Schema(ref="#/definitions/error"), + * examples={"application/json":{"status"="error","msg"="Unauthorized"}}, + * ), + * @SWG\Response( + * response="403", + * description="Forbidden", + * @SWG\Schema(ref="#/definitions/error"), + * examples={"application/json":{"status"="error","msg"="Forbidden"}}, + * ), + * @SWG\Response( + * response="200", + * description="OK", + * @SWG\Schema( + * ref="#/definitions/OK", + * ), + * ) + * ) + * @param \Illuminate\Http\Request $request + * @param \App\Ticket $ticket + * + * @return \Illuminate\Http\JsonResponse + */ + public function assignTicket(Request $request, Ticket $ticket) + { + $userId = $request->user_id; + $user = User::where('discord_id', $userId)->first(); + if (!$user) { + return response()->api(generate_error("Your Discord account is not linked to a VATUSA account."), 403); + } + + $aUser = User::find($request->cid); + if (!$aUser) { + return response()->api(generate_error("Invalid user."), 404); + } + + if(RoleHelper::isFacilityStaff($user->cid, + $ticket->facility) || RoleHelper::isInstructor($user->cid, $ticket->facility)) { + //Assign ticket + + } + + return response()->api(generate_error("You do not have permission to assign this ticket."), 403); + } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 53175de0..56d35d92 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -2,6 +2,14 @@ namespace App\Http; +use App\Http\Middleware\APIKey; +use App\Http\Middleware\APIKeyv2; +use App\Http\Middleware\BotJWT; +use App\Http\Middleware\PrivateCORS; +use App\Http\Middleware\PublicCORS; +use App\Http\Middleware\RedirectIfAuthenticated; +use App\Http\Middleware\SemiPrivateCORS; +use App\Http\Middleware\Subdomain; use Illuminate\Foundation\Http\Kernel as HttpKernel; class Kernel extends HttpKernel @@ -29,7 +37,7 @@ class Kernel extends HttpKernel */ protected $middlewareGroups = [ 'web' => [ - // \App\Http\Middleware\EncryptCookies::class, + // \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, //\Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\Session\Middleware\StartSession::class, @@ -42,24 +50,24 @@ class Kernel extends HttpKernel ], 'APIKey' => [ - \App\Http\Middleware\APIKey::class + APIKey::class ], 'semiprivate' => [ - \App\Http\Middleware\SemiPrivateCORS::class, + SemiPrivateCORS::class, ], 'public' => [ - \App\Http\Middleware\PublicCORS::class, + PublicCORS::class, ], 'private' => [ - \App\Http\Middleware\PrivateCORS::class, + PrivateCORS::class, ], 'api' => [ - \App\Http\Middleware\Subdomain::class, - // 'throttle:60,1', + Subdomain::class, + // 'throttle:60,1', 'bindings', ], ]; @@ -72,12 +80,13 @@ class Kernel extends HttpKernel * @var array */ protected $routeMiddleware = [ - 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, + 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, - 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, - 'can' => \Illuminate\Auth\Middleware\Authorize::class, - 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, - 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, - 'apikeyv2' => Middleware\APIKeyv2::class, + 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, + 'botkey' => BotJWT::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'guest' => RedirectIfAuthenticated::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'apikeyv2' => APIKeyv2::class, ]; } diff --git a/app/Ticket.php b/app/Ticket.php new file mode 100644 index 00000000..8d10fecd --- /dev/null +++ b/app/Ticket.php @@ -0,0 +1,69 @@ +hasMany(TicketReplies::class)->orderBy('created_at'); + } + + public function lastreply() + { + return $this->hasOne(TicketReplies::class)->orderByDesc('created_at'); + } + + public function notes() + { + return $this->hasMany(TicketNotes::class); + } + + public function submitter() + { + return $this->hasOne(User::class, 'cid', 'cid'); + } + + public function history() + { + return $this->hasMany(TicketHistory::class, 'ticket_id', 'id')->orderBy('created_at',); + } + + public function facility() + { + return $this->belongsTo(Facility::class, 'facility', 'id'); + } + + public function assignedto() + { + if ($this->assigned_to != 0) { + return $this->hasOne(User::class, 'cid', 'assigned_to'); + } else { + return null; + } + } + + public function assignee() + { + return $this->hasOne(User::class, 'cid', 'assigned_to'); + } + + public function lastreplier() + { + if (count($this->replies) == 0) { + return false; + } else { + return $this->lastreply->submitter->fullname(); + } + } + + public function viewbody() + { + $url = '@(http)?(s)?(://)?(([a-zA-Z])([-\w]+\.)+([^\s\.]+[^\s]*)+[^,.\s])@'; + $string = preg_replace($url, '$0', $this->body); + + return nl2br($string, false); + } +} \ No newline at end of file diff --git a/app/TicketHistory.php b/app/TicketHistory.php new file mode 100644 index 00000000..6876e540 --- /dev/null +++ b/app/TicketHistory.php @@ -0,0 +1,15 @@ +belongsTo(Ticket::class, 'id', 'ticket_id'); + } +} \ No newline at end of file diff --git a/app/TicketNotes.php b/app/TicketNotes.php new file mode 100644 index 00000000..725776f4 --- /dev/null +++ b/app/TicketNotes.php @@ -0,0 +1,15 @@ +belongsTo(Ticket::class, 'id', 'ticket_id'); + } +} \ No newline at end of file diff --git a/app/TicketReplies.php b/app/TicketReplies.php new file mode 100644 index 00000000..0f52bad2 --- /dev/null +++ b/app/TicketReplies.php @@ -0,0 +1,28 @@ +belongsTo(Ticket::class, 'id', 'ticket_id'); + } + + public function submitter() + { + return $this->hasOne(User::class, 'cid', 'cid'); + } + + public function viewbody() + { + $url = '@(http)?(s)?(://)?(([a-zA-Z])([-\w]+\.)+([^\s\.]+[^\s]*)+[^,.\s])@'; + $string = preg_replace($url, '$0', $this->body); + + return nl2br($string, false); + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index dc415f4b..ada88899 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "MIT", "type": "project", "require": { - "php": ">=7.2", + "php": ">=7.4", "ext-json": "*", "aws/aws-sdk-php-laravel": "^3.1", "bonroyage/oauth": "1.*", diff --git a/routes/api-v2.php b/routes/api-v2.php index 2a847acb..28090c5a 100644 --- a/routes/api-v2.php +++ b/routes/api-v2.php @@ -200,9 +200,17 @@ */ Route::group(['prefix' => '/support'], function () { - Route::get('/support/kb', 'SupportController@getKBs'); - Route::get('/tickets/depts', 'SupportController@getTicketDepts'); - Route::get('/tickets/depts/{dept}/staff', 'SupportController@getTicketDeptStaff'); + Route::get('kb', 'SupportController@getKBs'); + Route::group(['prefix' => '/tickets'], function () { + Route::group(['middleware' => 'botkey'], function () { + Route::put('{ticket}/close', 'SupportController@closeTicket'); + }); + + Route::group(['prefix' => '/depts'], function () { + Route::get('/', 'SupportController@getTicketDepts'); + Route::get('{dept}/staff', 'SupportController@getTicketDeptStaff'); + }); + }); Route::group(['middleware' => 'auth:web,jwt'], function () { Route::post('/kb', 'SupportController@postKB'); From d22bee63af441f1b7059a7cea79656f2e55b2fd1 Mon Sep 17 00:00:00 2001 From: Blake Nahin Date: Thu, 9 Dec 2021 12:20:33 -0600 Subject: [PATCH 6/9] Add Discord user endpoints --- app/Helpers/RatingHelper.php | 47 +++++++------------ .../Controllers/API/v2/UserController.php | 24 +++++++++- app/User.php | 11 +++-- routes/api-v2.php | 1 + 4 files changed, 48 insertions(+), 35 deletions(-) diff --git a/app/Helpers/RatingHelper.php b/app/Helpers/RatingHelper.php index a3ced08a..dd2a0733 100644 --- a/app/Helpers/RatingHelper.php +++ b/app/Helpers/RatingHelper.php @@ -26,41 +26,28 @@ namespace App\Helpers; -class RatingHelper { - public static $ratings = [ - "", "OBS", "S1", "S2", "S3", "C1", "C2", "C3", "I1", "I2", "I3", "SUP", "ADM" - ]; - public static $rating_titles = [ - "", - "Observer", - "Student 1", - "Student 2", - "Student 3", - "Controller", - "not used", - "Senior Controller", - "Instructor", - "not used", - "Senior Instructor", - "Supervisor", - "Administrator" - ]; +use App\Rating; - public static function intToShort($rating) { - if (isset(static::$ratings[$rating])) { return static::$ratings[$rating]; } - else { return false; } +class RatingHelper +{ + public static function intToShort(int $rating): ?string + { + $rating = Rating::find($rating); + + return $rating ? $rating->short : null; } - public static function intToLong($rating) { - if (isset(static::$rating_titles[$rating])) { return static::$rating_titles[$rating]; } - else { return false; } + public static function intToLong(int $rating): ?string + { + $rating = Rating::find($rating); + + return $rating ? $rating->long : null; } - public static function shortToInt($short) { - foreach (static::$ratings as $key => $value) { - if ($short == $value) { return $key; } - } + public static function shortToInt($short): ?int + { + $rating = Rating::where('short', $short)->first(); - return false; + return $rating ? $rating->id : null; } } \ No newline at end of file diff --git a/app/Http/Controllers/API/v2/UserController.php b/app/Http/Controllers/API/v2/UserController.php index f5467888..5dc9a612 100644 --- a/app/Http/Controllers/API/v2/UserController.php +++ b/app/Http/Controllers/API/v2/UserController.php @@ -1158,7 +1158,8 @@ public function getExamHistory($cid) * @SWG\Property(property="lname", type="string"), * ) * ), - * examples={"application/json":{"0":{"cid":1391803,"fname":"Michael","lname":"Romashov"},"1":{"cid":1391802,"fname":"Sankara","lname":"Narayanan "}}} + * examples={"application/json":{"0":{"cid":1391803,"fname":"Michael","lname":"Romashov"},"1":{"cid":1391802,"fname":"Sankara","lname":"Narayanan + * "}}} * ) * ) */ @@ -1240,4 +1241,25 @@ public function filterUsersLName($partialLName) return response()->api($return); } + + /** + * + * @SWG\Get( + * path="/user/getAllDiscord", + * summary="Get all users with a Discord account linked. [Bot]", + * description="Get all users with a Discord account linked.", + * produces={"application/json"}, tags={"user"}, security={"jwt"} + * @SWG\Response( + * response="200", + * description="OK", + * @SWG\Schema(ref="#/definitions/User") + * ) + * ) + */ + public function getAllDiscord(Request $request) + { + $users = User::where('discord_id', '!=', null)->get(); + + return response()->json($users->pluck('discord_id')); + } } diff --git a/app/User.php b/app/User.php index fc7a95e5..7aaa2abe 100644 --- a/app/User.php +++ b/app/User.php @@ -84,7 +84,7 @@ class User extends Model implements AuthenticatableContract, JWTSubject //public $timestamps = ["created_at", "updated_at", "prefname_date", "facility_join"]; protected $dates = ["lastactivity", "facility_join", "prefname_date"]; - + /** * @var array */ @@ -109,7 +109,8 @@ class User extends Model implements AuthenticatableContract, JWTSubject 'flag_xferOverride' => 'boolean', 'flag_homecontroller' => 'boolean', 'flag_broadcastOptedIn' => 'boolean', - 'flag_preventStaffAssign' => 'boolean' + 'flag_preventStaffAssign' => 'boolean', + 'discord_id' => 'string' ]; @@ -682,11 +683,13 @@ public function getTransferEligibleAttribute() } } - public function getNameAttribute() { + public function getNameAttribute() + { return $this->fullname(); } - public function getFullNameAttribute() { + public function getFullNameAttribute() + { return $this->fullname(); } diff --git a/routes/api-v2.php b/routes/api-v2.php index 28090c5a..6c856554 100644 --- a/routes/api-v2.php +++ b/routes/api-v2.php @@ -246,6 +246,7 @@ Route::get('/filtercid/{partialCid}', 'UserController@filterUsersCid')->where('partialCid', '[0-9]+'); Route::get('/filterlname/{partialLName}', 'UserController@filterUsersLName')->where('partialLName', '[A-Za-z0-9]+'); Route::get('/{cid}', 'UserController@getIndex')->where('cid', '[0-9]+'); + Route::get('/getAllDiscord', 'UserController@getAllDiscord')->middleware('botkey'); Route::get('/roles/{facility}/{role}', 'UserController@getRoleUsers')->where([ 'facility' => '[A-Za-z]{3}', From 56e38da630569390d921555b2464cb00c2872b87 Mon Sep 17 00:00:00 2001 From: Blake Nahin Date: Sat, 11 Dec 2021 15:54:57 -0600 Subject: [PATCH 7/9] Add Bot JWT --- .../Controllers/API/v2/TrainingController.php | 2 +- app/Http/Middleware/BotJWT.php | 37 ++++++++++++++++++ app/Mail/TicketClosed.php | 38 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 app/Http/Middleware/BotJWT.php create mode 100644 app/Mail/TicketClosed.php diff --git a/app/Http/Controllers/API/v2/TrainingController.php b/app/Http/Controllers/API/v2/TrainingController.php index 2b57d362..e640bfce 100644 --- a/app/Http/Controllers/API/v2/TrainingController.php +++ b/app/Http/Controllers/API/v2/TrainingController.php @@ -1154,7 +1154,7 @@ function canCreate( User $user ) { $hasApiKey = AuthHelper::validApiKeyv2($request->input('apikey', null), $user->facility); - + //Check Visiting Facilities $apiKeyVisitor = false; $keyFac = Facility::where("apikey", $request->apikey) diff --git a/app/Http/Middleware/BotJWT.php b/app/Http/Middleware/BotJWT.php new file mode 100644 index 00000000..52318245 --- /dev/null +++ b/app/Http/Middleware/BotJWT.php @@ -0,0 +1,37 @@ +bearerToken(); + if (!$token) { + abort(401, 'No token provided'); + } + + JWT::$leeway = 60; + try { + JWT::decode($token, config('services.discord.botSecret'), ['HS512']); + } catch (Exception $e) { + abort(403, 'Invalid token'); + } + + return $next($request); + } +} diff --git a/app/Mail/TicketClosed.php b/app/Mail/TicketClosed.php new file mode 100644 index 00000000..8a44d0fa --- /dev/null +++ b/app/Mail/TicketClosed.php @@ -0,0 +1,38 @@ +ticket = $ticket; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + return $this->from('support@vatusa.net', 'VATUSA Help Desk') + ->subject("[VATUSA Help Desk] (Ticket #{$this->ticket->id}) Ticket Closed") + ->view('emails.help.closed'); + } +} From debc8938e63025529df58563d0c8a4636973615f Mon Sep 17 00:00:00 2001 From: Blake Nahin Date: Sun, 19 Dec 2021 21:57:07 -0600 Subject: [PATCH 8/9] Assign Ticket --- .../Controllers/API/v2/SupportController.php | 30 ++++++++++++++++++- .../Controllers/API/v2/UserController.php | 3 +- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/API/v2/SupportController.php b/app/Http/Controllers/API/v2/SupportController.php index 1fc31773..a4d7db52 100644 --- a/app/Http/Controllers/API/v2/SupportController.php +++ b/app/Http/Controllers/API/v2/SupportController.php @@ -732,13 +732,41 @@ public function assignTicket(Request $request, Ticket $ticket) } $aUser = User::find($request->cid); - if (!$aUser) { + if (!$aUser && $request->cid) { return response()->api(generate_error("Invalid user."), 404); } if(RoleHelper::isFacilityStaff($user->cid, $ticket->facility) || RoleHelper::isInstructor($user->cid, $ticket->facility)) { //Assign ticket + if($aUser) { + $ticket->assigned_to = $aUser->cid; + $ticket->save(); + + $history = new TicketHistory(); + $history->ticket_id = $ticket->id; + $history->entry = $user->fullname() . " (" . $user->cid . ") assigned the ticket to " . $aUser->fullname() . " (" . $aUser->cid . ") [Discord]."; + $history->save(); + + $discord = new VATUSADiscord(); + if ($discord->userWantsNotification($aUser, "ticketAssigned", "email")) { + Mail::to($aUser)->queue(new TicketAssigned($ticket)); + } + if ($discord->userWantsNotification($aUser, "ticketAssigned", "discord")) { + $discord->sendNotification("ticketAssigned", "dm", + array_merge($ticket->toArray(), ['userId' => $aUser->discord_id])); + } + } + else { + //Unassign ticket + $ticket->assigned_to = 0; + $ticket->save(); + + $history = new TicketHistory(); + $history->ticket_id = $ticket->id; + $history->entry = $user->fullname() . " (" . $user->cid . ") set ticket to unassigned [Discord]."; + $history->save(); + } } diff --git a/app/Http/Controllers/API/v2/UserController.php b/app/Http/Controllers/API/v2/UserController.php index 5dc9a612..f24cce1b 100644 --- a/app/Http/Controllers/API/v2/UserController.php +++ b/app/Http/Controllers/API/v2/UserController.php @@ -33,9 +33,10 @@ class UserController extends APIController * path="/user/(cid)", * summary="Get user's information.", * description="Get user's information. Email field, broadcast opt-in status, and visiting facilities require authentication as staff member or API key. - Prevent staff assigment flag requires authentication as senior staff.", + Prevent staff assigment flag requires authentication as senior staff. If the "d" QSP is included, the user will be retrieved by Discord ID.", * produces={"application/json"}, tags={"user"}, * @SWG\Parameter(name="cid",in="path",required=true,type="string",description="Cert ID"), + * @SWG\Parameter(name="d",in="query",required=false,type="string",description="The id given is a Discord ID"), * @SWG\Response( * response="404", * description="Not found", From 1f30c42069080794643d42b5bb2941371f8bce14 Mon Sep 17 00:00:00 2001 From: Blake Nahin Date: Thu, 23 Dec 2021 00:12:47 -0800 Subject: [PATCH 9/9] Bot Ticket Assign --- .../Controllers/API/v2/SupportController.php | 13 ++++--- app/Mail/TicketAssigned.php | 38 +++++++++++++++++++ app/Mail/TicketClosed.php | 2 +- routes/api-v2.php | 1 + 4 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 app/Mail/TicketAssigned.php diff --git a/app/Http/Controllers/API/v2/SupportController.php b/app/Http/Controllers/API/v2/SupportController.php index a4d7db52..ce12b71d 100644 --- a/app/Http/Controllers/API/v2/SupportController.php +++ b/app/Http/Controllers/API/v2/SupportController.php @@ -7,11 +7,12 @@ use App\Facility; use App\Helpers\RoleHelper; use App\KnowledgebaseQuestions; -use App\Mail\TicketClosed; +use App\Mail\TicketAssigned; use App\Role; use App\Ticket; use App\TicketHistory; use App\User; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use App\KnowledgebaseCategories; use Illuminate\Support\Facades\Mail; @@ -669,7 +670,7 @@ public function closeTicket(Request $request, Ticket $ticket) $discord = new VATUSADiscord(); if ($discord->userWantsNotification($ticket->submitter, "ticketClosed", "email")) { - Mail::to($ticket->submitter->email)->queue(new TicketClosed($ticket)); + Mail::to($ticket->submitter->email)->queue(new TicketAssigned($ticket)); } if ($discord->userWantsNotification($ticket->submitter, "ticketClosed", "discord")) { $discord->sendNotification("ticketClosed", "dm", @@ -736,10 +737,10 @@ public function assignTicket(Request $request, Ticket $ticket) return response()->api(generate_error("Invalid user."), 404); } - if(RoleHelper::isFacilityStaff($user->cid, + if (RoleHelper::isVATUSAStaff($user->cid, false, true) || RoleHelper::isFacilityStaff($user->cid, $ticket->facility) || RoleHelper::isInstructor($user->cid, $ticket->facility)) { //Assign ticket - if($aUser) { + if ($aUser) { $ticket->assigned_to = $aUser->cid; $ticket->save(); @@ -756,8 +757,7 @@ public function assignTicket(Request $request, Ticket $ticket) $discord->sendNotification("ticketAssigned", "dm", array_merge($ticket->toArray(), ['userId' => $aUser->discord_id])); } - } - else { + } else { //Unassign ticket $ticket->assigned_to = 0; $ticket->save(); @@ -768,6 +768,7 @@ public function assignTicket(Request $request, Ticket $ticket) $history->save(); } + return response()->ok(); } return response()->api(generate_error("You do not have permission to assign this ticket."), 403); diff --git a/app/Mail/TicketAssigned.php b/app/Mail/TicketAssigned.php new file mode 100644 index 00000000..ba321cf6 --- /dev/null +++ b/app/Mail/TicketAssigned.php @@ -0,0 +1,38 @@ +ticket = $ticket; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + return $this->from('support@vatusa.net', 'VATUSA Help Desk') + ->subject("(Ticket #{$this->ticket->id}) Ticket Assigned to You") + ->view('emails.help.assigned'); + } +} diff --git a/app/Mail/TicketClosed.php b/app/Mail/TicketClosed.php index 8a44d0fa..d29d8f32 100644 --- a/app/Mail/TicketClosed.php +++ b/app/Mail/TicketClosed.php @@ -32,7 +32,7 @@ public function __construct(Ticket $ticket) public function build() { return $this->from('support@vatusa.net', 'VATUSA Help Desk') - ->subject("[VATUSA Help Desk] (Ticket #{$this->ticket->id}) Ticket Closed") + ->subject("(Ticket #{$this->ticket->id}) Ticket Closed") ->view('emails.help.closed'); } } diff --git a/routes/api-v2.php b/routes/api-v2.php index 6c856554..a3ae1baa 100644 --- a/routes/api-v2.php +++ b/routes/api-v2.php @@ -204,6 +204,7 @@ Route::group(['prefix' => '/tickets'], function () { Route::group(['middleware' => 'botkey'], function () { Route::put('{ticket}/close', 'SupportController@closeTicket'); + Route::post('{ticket}/assign', 'SupportController@assignTicket'); }); Route::group(['prefix' => '/depts'], function () {