diff --git a/app/Classes/VATUSADiscord.php b/app/Classes/VATUSADiscord.php
new file mode 100644
index 00000000..0525bb71
--- /dev/null
+++ b/app/Classes/VATUSADiscord.php
@@ -0,0 +1,232 @@
+
+ */
+
+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;
+
+ /**
+ * 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();
+
+ return $record ? $record->option : 0;
+ }
+
+ /**
+ * 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();
+ $return = array();
+ foreach ($records as $record) {
+ $return[$record->type] = $record->option;
+ }
+
+ 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();
+ $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 [];
+ }
+
+ /**
+ * 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");
+ } 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/Console/Commands/SendAcademyRatingExamEmails.php b/app/Console/Commands/SendAcademyRatingExamEmails.php
index d6bbcf58..e6171f9a 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,12 +42,13 @@ public function __construct()
{
parent::__construct();
$this->moodle = new VATUSAMoodle();
+ $this->notify = new VATUSADiscord();
}
/**
* Execute the console command.
*
- * @return mixed
+ * @return void
* @throws \Exception
*/
public function handle()
@@ -54,8 +57,16 @@ 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) : [];
+ $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,
@@ -90,14 +101,40 @@ 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);
+ $hasUser = true;
+ }
+ if ($ta && $this->notify->userWantsNotification($ta, "academyExamResult", "email")) {
+ $hasUser ? $mail->cc($instructor) : $mail->to($ta);
+ }
- $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;
+ $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->facilityObj->discord_guild, $channel);
+ }
if ($passed) {
$assignment->delete();
@@ -153,13 +190,23 @@ 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));
-
- if($passed) {
+ $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/FacilityNotificationChannel.php b/app/FacilityNotificationChannel.php
new file mode 100644
index 00000000..bd26362d
--- /dev/null
+++ b/app/FacilityNotificationChannel.php
@@ -0,0 +1,10 @@
+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/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/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 7c237df4..45a78b99 100644
--- a/app/Http/Controllers/API/v2/ExamController.php
+++ b/app/Http/Controllers/API/v2/ExamController.php
@@ -2,20 +2,26 @@
namespace App\Http\Controllers\API\v2;
-use App\Action;
+use App\Classes\Helper;
+use App\Classes\VATUSADiscord;
use App\ExamAssignment;
use App\ExamQuestions;
use App\ExamReassignment;
use App\ExamResults;
use App\ExamResultsData;
+use App\Facility;
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\Auth;
+use Illuminate\Support\Facades\Mail;
class ExamController extends APIController
{
@@ -179,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) {
@@ -212,44 +218,77 @@ public function postSubmit(Request $request)
$result->save();
// Done... let's send some emails
- $to[] = \Auth::user()->email;
- if ($assign->instructor_id > 111111) {
- $instructor = User::find($assign->instructor_id);
- if ($instructor) {
+ $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 ($instructor = User::find($assign->instructor_id)) {
+ 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->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 = [
- 'exam_name' => "(" . $exam->facility_id . ") " . $exam->name,
- 'instructor_name' => (isset($instructor)) ? $instructor->fullname() : 'N/A',
+ 'passed' => $result->passed,
+ 'exam_name' => "($exam->facility_id) $exam->name",
+ '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) {
+ $notify->sendNotification("legacyExamResult", "dm",
+ array_merge($data, compact('studentId', 'instructorId')));
+ }
+ if ($taId) {
+ $notify->sendNotification("legacyExamResult", "dm",
+ array_merge($data, compact(['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));
}
- EmailHelper::sendEmailFacilityTemplate($to, "Exam Passed", $fac, "exampassed", $data);
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."]);
@@ -259,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;
@@ -268,9 +307,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));
}
- EmailHelper::sendEmailFacilityTemplate($to, "Exam Not Passed", $fac, "examfailed", $data);
return response()->api(['results' => "Not Passed."]);
}
@@ -308,8 +349,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);
}
@@ -397,8 +440,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())) {
@@ -448,8 +494,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())) {
@@ -497,8 +546,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);
@@ -570,8 +622,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);
}
@@ -679,8 +734,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);
}
@@ -761,8 +819,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);
}
@@ -814,7 +876,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
@@ -851,9 +913,13 @@ public function putExamQuestion(Request $request, $examid, $questionid)
*
* @return \Illuminate\Http\Response
*/
- public function postExamAssign(Request $request, $examid, $cid)
- {
- if (!\Auth::check()) {
+ public
+ function postExamAssign(
+ Request $request,
+ $examid,
+ $cid
+ ) {
+ if (!Auth::check()) {
return response()->api(generate_error("Unauthorized"), 401);
}
if (!RoleHelper::isSeniorStaff() &&
@@ -866,7 +932,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'),
@@ -880,14 +946,17 @@ public function postExamAssign(Request $request, $examid, $cid)
}
$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) {
@@ -895,24 +964,59 @@ public function postExamAssign(Request $request, $examid, $cid)
}
$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;
+
+ $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") {
- $to[] = $exam->facility_id . "-TA@vatusa.net";
+ if ($ta && $notify->userWantsNotification($ta, "legacyExamAssigned", "email")) {
+ $to[] = $ta->email;
+ }
}
- EmailHelper::sendEmailFacilityTemplate($to, "Exam Assigned", $exam->facility_id, "examassigned", $data);
+ if (count($to)) {
+ Mail::to($to)->queue(new ExamAssigned($data));
+ }
- log_action($cid, "Exam (" . $exam->facility_id . ") " . $exam->name .
- " assigned by " . \Auth::user()->fullname() . ", expires " . $data['end_date']);
+ $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 {$data['exam_name']} assigned by {$data['instructor_name']}, expires {$endDate->format('m/d/Y H:i')}.");
}
return response()->api(['status' => 'OK']);
@@ -955,8 +1059,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);
}
@@ -1033,8 +1141,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/Http/Controllers/API/v2/SupportController.php b/app/Http/Controllers/API/v2/SupportController.php
index bfc6b0c4..ce12b71d 100644
--- a/app/Http/Controllers/API/v2/SupportController.php
+++ b/app/Http/Controllers/API/v2/SupportController.php
@@ -2,12 +2,20 @@
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\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;
/**
* Class SupportController
@@ -38,7 +46,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 +64,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 +91,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 +124,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 +154,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 +225,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 +267,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 +301,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 +348,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 +383,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 +413,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 +478,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 +533,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 +542,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 +580,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 +614,163 @@ 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 TicketAssigned($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 && $request->cid) {
+ return response()->api(generate_error("Invalid user."), 404);
+ }
+
+ if (RoleHelper::isVATUSAStaff($user->cid, false, true) || 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();
+ }
+
+ return response()->ok();
+ }
+
+ return response()->api(generate_error("You do not have permission to assign this ticket."), 403);
+ }
}
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/Controllers/API/v2/UserController.php b/app/Http/Controllers/API/v2/UserController.php
index f5467888..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",
@@ -1158,7 +1159,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 +1242,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/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/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/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/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
new file mode 100644
index 00000000..d29d8f32
--- /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("(Ticket #{$this->ticket->id}) Ticket Closed")
+ ->view('emails.help.closed');
+ }
+}
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/app/NotificationSetting.php b/app/NotificationSetting.php
new file mode 100644
index 00000000..54763d4e
--- /dev/null
+++ b/app/NotificationSetting.php
@@ -0,0 +1,10 @@
+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/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/composer.json b/composer.json
index 91b688c5..ada88899 100644
--- a/composer.json
+++ b/composer.json
@@ -5,13 +5,14 @@
"license": "MIT",
"type": "project",
"require": {
- "php": ">=7.2",
+ "php": ">=7.4",
"ext-json": "*",
"aws/aws-sdk-php-laravel": "^3.1",
"bonroyage/oauth": "1.*",
"darkaonline/l5-swagger": "6.*",
"doctrine/dbal": "^2.5",
"fideloper/proxy": "~4.0",
+ "firebase/php-jwt": "^5.4",
"fruitcake/laravel-cors": "^2.0",
"fzaninotto/faker": "~1.4",
"guzzlehttp/psr7": "^1.5",
diff --git a/composer.lock b/composer.lock
index ca9c83ed..94a0e022 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "3ea87c7c7dc3f5a0df875cc1892b0de0",
+ "content-hash": "e66c2b47629b447f5ea044c9b6b0b56f",
"packages": [
{
"name": "asm89/stack-cors",
@@ -1402,6 +1402,63 @@
},
"time": "2020-10-22T13:48:01+00:00"
},
+ {
+ "name": "firebase/php-jwt",
+ "version": "v5.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/firebase/php-jwt.git",
+ "reference": "d2113d9b2e0e349796e72d2a63cf9319100382d2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d2113d9b2e0e349796e72d2a63cf9319100382d2",
+ "reference": "d2113d9b2e0e349796e72d2a63cf9319100382d2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=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",
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 6c4ff987..3fdccdcf 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']->toDayDateTimeString() }} US Central Time to complete the examination before it expires.
- 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.
diff --git a/routes/api-v2.php b/routes/api-v2.php
index 2a847acb..a3ae1baa 100644
--- a/routes/api-v2.php
+++ b/routes/api-v2.php
@@ -200,9 +200,18 @@
*/
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::post('{ticket}/assign', 'SupportController@assignTicket');
+ });
+
+ 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');
@@ -238,6 +247,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}',