fix api security issues and update pages issue

This commit is contained in:
root
2026-06-04 16:41:19 -04:00
parent feb6be0610
commit 5e5fe3794a
32 changed files with 1628 additions and 37 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
@@ -6,6 +6,8 @@ use App\Http\Controllers\Api\Core\BaseApiController;
use App\Http\Requests\Roles\RoleSwitchRequest;
use App\Services\Roles\RoleSwitchService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpFoundation\Response;
class RoleSwitcherController extends BaseApiController
@@ -40,7 +42,13 @@ class RoleSwitcherController extends BaseApiController
}
$role = (string) $request->validated()['role'];
$route = $this->switchService->switchRole($role);
try {
$route = $this->switchService->switchRole($guard, $role);
} catch (AccessDeniedHttpException $e) {
return $this->error($e->getMessage(), Response::HTTP_FORBIDDEN);
} catch (NotFoundHttpException $e) {
return $this->error($e->getMessage(), Response::HTTP_NOT_FOUND);
}
return $this->success([
'role' => $role,
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\Grading;
use App\Http\Controllers\Api\Core\BaseApiController;
use App\Http\Requests\Grading\BelowSixtyEmailRequest;
use App\Http\Requests\Grading\AllDecisionsRequest;
use App\Http\Requests\Grading\BelowSixtyDecisionDetailsRequest;
use App\Http\Requests\Grading\BelowSixtyDecisionListRequest;
use App\Http\Requests\Grading\BelowSixtyDecisionSaveRequest;
@@ -13,6 +14,7 @@ use App\Http\Requests\Grading\BelowSixtyMeetingSaveRequest;
use App\Http\Requests\Grading\BelowSixtySendRequest;
use App\Http\Requests\Grading\BelowSixtyStatusRequest;
use App\Http\Requests\Grading\BelowSixtyListRequest;
use App\Http\Requests\Grading\GenerateAllDecisionsRequest;
use App\Http\Requests\Grading\GradingLockAllRequest;
use App\Http\Requests\Grading\GradingLockRequest;
use App\Http\Requests\Grading\GradingOverviewRequest;
@@ -316,6 +318,27 @@ class GradingController extends BaseApiController
return response()->json(['ok' => true] + $data);
}
public function allDecisions(AllDecisionsRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->belowSixtyService->listAllDecisions((string) $payload['school_year']);
return response()->json(['ok' => true] + $data);
}
public function generateAllDecisions(GenerateAllDecisionsRequest $request): JsonResponse
{
$userId = $this->authenticatedUserIdOrUnauthorized();
if ($userId instanceof JsonResponse) {
return $userId;
}
$payload = $request->validated();
$count = $this->belowSixtyService->generateAllDecisions((string) $payload['school_year'], $userId);
return response()->json(['ok' => true, 'saved_count' => $count]);
}
public function belowSixtyDecisions(BelowSixtyDecisionListRequest $request): JsonResponse
{
$payload = $request->validated();
@@ -47,7 +47,7 @@ class PrintRequestsController extends BaseApiController
}
$validator = Validator::make($request->all(), [
'file' => ['required', 'file', 'max:5120', 'mimes:pdf,jpg,jpeg,png,doc,docx,txt'],
'file' => ['required', 'file', 'max:5120', 'mimes:pdf,jpg,jpeg,png,doc,docx,txt', 'mimetypes:application/pdf,image/jpeg,image/png,text/plain,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
'page_selection' => ['nullable', 'string', 'regex:/^\s*\d+(?:\s*-\s*\d+)?(?:\s*,\s*\d+(?:\s*-\s*\d+)?)*\s*$/'],
'num_copies' => ['required', 'integer', 'min:1'],
'required_by' => ['required', 'date'],
@@ -97,7 +97,7 @@ class PrintRequestsController extends BaseApiController
];
if ($request->hasFile('file') && $request->file('file')->isValid()) {
$rules['file'] = ['required', 'file', 'max:5120', 'mimes:pdf,jpg,jpeg,png,doc,docx,txt'];
$rules['file'] = ['required', 'file', 'max:5120', 'mimes:pdf,jpg,jpeg,png,doc,docx,txt', 'mimetypes:application/pdf,image/jpeg,image/png,text/plain,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
}
$validator = Validator::make($request->all(), $rules);
@@ -5,6 +5,8 @@ namespace App\Http\Controllers\Api;
use App\Http\Requests\Roles\RoleSwitchRequest;
use App\Services\Roles\RoleSwitchService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpFoundation\Response;
class RoleSwitcherController extends BaseApiController
@@ -33,8 +35,19 @@ class RoleSwitcherController extends BaseApiController
public function switch(RoleSwitchRequest $request): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
if ($userId <= 0) {
return $this->error('Please log in first.', Response::HTTP_UNAUTHORIZED);
}
$role = (string) $request->validated()['role'];
$route = $this->switchService->switchRole($role);
try {
$route = $this->switchService->switchRole($userId, $role);
} catch (AccessDeniedHttpException $e) {
return $this->error($e->getMessage(), Response::HTTP_FORBIDDEN);
} catch (NotFoundHttpException $e) {
return $this->error($e->getMessage(), Response::HTTP_NOT_FOUND);
}
return $this->success([
'role' => $role,
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
/** @var Response $response */
$response = $next($request);
$headers = $response->headers;
$headers->set('X-Content-Type-Options', 'nosniff');
$headers->set('X-Frame-Options', 'DENY');
$headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
if ($request->isSecure()) {
$headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
return $response;
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests\Grading;
use App\Http\Requests\ApiFormRequest;
class AllDecisionsRequest extends ApiFormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'school_year' => ['required', 'string', 'max:50'],
];
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests\Grading;
use App\Http\Requests\ApiFormRequest;
class GenerateAllDecisionsRequest extends ApiFormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'school_year' => ['required', 'string', 'max:50'],
];
}
}
+2
View File
@@ -90,6 +90,8 @@ class User extends Authenticatable implements JWTSubject
{
return DB::table('user_roles')
->join('roles', 'roles.id', '=', 'user_roles.role_id')
->whereNull('user_roles.deleted_at')
->where('roles.is_active', 1)
->where('user_roles.user_id', $this->id)
->pluck('roles.name')
->unique()
@@ -17,6 +17,14 @@ class BroadcastEmailImageService
public function store(UploadedFile $file): array
{
if (! $file->isValid()) {
return [
'ok' => false,
'status' => 400,
'error' => 'Invalid upload',
];
}
$mime = strtolower((string) $file->getMimeType());
if (!isset($this->allowedTypes[$mime])) {
return [
@@ -40,7 +48,7 @@ class BroadcastEmailImageService
}
$ext = $this->allowedTypes[$mime];
$newName = uniqid('em_', true) . '.' . $ext;
$newName = 'em_' . bin2hex(random_bytes(16)) . '.' . $ext;
$file->move($targetDir, $newName);
return [
@@ -22,6 +22,7 @@ class DashboardRouteService
];
}
$currentRole = strtolower(trim((string) session('role', '')));
$roles = $user->roles()
->pluck('roles.name')
->map(fn ($name) => strtolower((string) $name))
@@ -40,7 +41,12 @@ class DashboardRouteService
];
}
$rows = Role::findByNamesOrSlugs($roles);
$lookupRoles = $roles;
if ($currentRole !== '' && in_array($currentRole, $roles, true)) {
$lookupRoles = [$currentRole];
}
$rows = Role::findByNamesOrSlugs($lookupRoles);
if (!empty($rows)) {
$role = $rows[0];
@@ -51,6 +57,7 @@ class DashboardRouteService
'role' => $role->name ?? null,
'role_slug' => $role->slug ?? null,
'roles' => $roles,
'current_role' => $currentRole !== '' ? $currentRole : null,
];
}
@@ -64,6 +71,7 @@ class DashboardRouteService
'role' => null,
'role_slug' => null,
'roles' => $roles,
'current_role' => $currentRole !== '' ? $currentRole : null,
];
}
}
+17 -1
View File
@@ -103,12 +103,28 @@ class EventManagementService
return null;
}
$allowed = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'application/pdf' => 'pdf',
];
$mime = strtolower((string) $file->getMimeType());
if (! isset($allowed[$mime])) {
throw new \InvalidArgumentException('Unsupported flyer file type.');
}
if ((int) $file->getSize() > 5 * 1024 * 1024) {
throw new \InvalidArgumentException('Flyer file too large. Max 5MB.');
}
$dir = public_path('uploads/event_flyers');
if (!File::exists($dir)) {
File::makeDirectory($dir, 0755, true);
}
$name = $file->hashName();
$name = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
$file->move($dir, $name);
return 'event_flyers/' . $name;
@@ -14,8 +14,30 @@ class ExpenseReceiptService
public function storeReceipt(UploadedFile $file): string
{
$stored = $file->store('receipts');
return basename($stored);
if (! $file->isValid()) {
throw new \InvalidArgumentException('Invalid receipt upload.');
}
$allowed = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'application/pdf' => 'pdf',
];
$mime = strtolower((string) $file->getMimeType());
if (! isset($allowed[$mime])) {
throw new \InvalidArgumentException('Unsupported receipt file type.');
}
if ((int) $file->getSize() > 5 * 1024 * 1024) {
throw new \InvalidArgumentException('Receipt file too large. Max 5MB.');
}
$filename = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
$file->storeAs('receipts', $filename);
return $filename;
}
public function receiptUrl(?string $filename): ?string
+2 -5
View File
@@ -39,13 +39,10 @@ class FileServeService
'Content-Length' => (string) $meta['size'],
'ETag' => $meta['etag'],
'Last-Modified' => $meta['last_modified'],
'Cache-Control' => 'public, max-age=86400',
'Cache-Control' => 'private, max-age=300',
'X-Content-Type-Options' => 'nosniff',
];
if ($nosniff) {
$headers['X-Content-Type-Options'] = 'nosniff';
}
return response(file_get_contents($meta['path']), 200, $headers);
}
@@ -584,6 +584,308 @@ class GradingBelowSixtyService
$this->sendEmail($payload);
}
public function listAllDecisions(string $schoolYear): array
{
$saved = StudentDecision::query()
->where('school_year', $schoolYear)
->get();
$savedMap = [];
foreach ($saved as $row) {
$studentId = (int) $row->student_id;
if ($studentId > 0) {
$savedMap[$studentId] = $row;
}
}
$allScoreRows = DB::table('semester_scores as ss')
->select([
's.id as student_id',
's.school_id',
's.firstname',
's.lastname',
's.gender',
'ss.class_section_id',
'cs.class_section_name',
DB::raw('LOWER(TRIM(ss.semester)) as sem_key'),
'ss.semester_score',
])
->join('students as s', 's.id', '=', 'ss.student_id')
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'ss.class_section_id')
->where('s.is_active', 1)
->where('ss.school_year', $schoolYear)
->whereRaw('LOWER(TRIM(ss.semester)) IN (?, ?)', ['fall', 'spring'])
->whereNotNull('ss.semester_score')
->orderBy('cs.class_section_name')
->orderBy('s.lastname')
->orderBy('s.firstname')
->get();
$studentMap = [];
foreach ($allScoreRows as $row) {
$studentId = (int) ($row->student_id ?? 0);
if ($studentId <= 0) {
continue;
}
if (!isset($studentMap[$studentId])) {
$studentMap[$studentId] = [
'school_id' => $row->school_id,
'firstname' => $row->firstname,
'lastname' => $row->lastname,
'gender' => $row->gender,
'class_section_id' => (int) ($row->class_section_id ?? 0),
'class_section_name' => $row->class_section_name,
'fall_score' => null,
'spring_score' => null,
];
}
$semesterKey = strtolower(trim((string) ($row->sem_key ?? '')));
$score = is_numeric($row->semester_score ?? null) ? (float) $row->semester_score : null;
if ($semesterKey === 'fall') {
$studentMap[$studentId]['fall_score'] = $score;
} elseif ($semesterKey === 'spring') {
$studentMap[$studentId]['spring_score'] = $score;
}
}
$belowRows = BelowSixtyDecision::query()
->where('school_year', $schoolYear)
->where('semester', 'year')
->get();
$belowMap = [];
foreach ($belowRows as $row) {
$studentId = (int) ($row->student_id ?? 0);
if ($studentId <= 0) {
continue;
}
if (!isset($belowMap[$studentId]) || trim((string) ($belowMap[$studentId]->decision ?? '')) === '') {
$belowMap[$studentId] = $row;
}
}
$rows = [];
foreach ($studentMap as $studentId => $info) {
$fall = $info['fall_score'];
$spring = $info['spring_score'];
if ($fall !== null && $spring !== null) {
$yearScore = round(($fall + $spring) / 2, 2);
} elseif ($fall !== null) {
$yearScore = round((float) $fall, 2);
} elseif ($spring !== null) {
$yearScore = round((float) $spring, 2);
} else {
$yearScore = null;
}
if (isset($savedMap[$studentId])) {
$savedRow = $savedMap[$studentId];
$decision = trim((string) ($savedRow->decision ?? ''));
$source = trim((string) ($savedRow->source ?? 'pending'));
$notes = (string) ($savedRow->notes ?? '');
if (is_numeric($savedRow->year_score ?? null)) {
$yearScore = round((float) $savedRow->year_score, 2);
}
} elseif ($yearScore !== null && $yearScore >= 60) {
$decision = 'Pass';
$source = 'auto';
$notes = '';
} elseif ($yearScore !== null && isset($belowMap[$studentId])) {
$decision = trim((string) ($belowMap[$studentId]->decision ?? ''));
$source = $decision !== '' ? 'manual' : 'pending';
$notes = (string) ($belowMap[$studentId]->notes ?? '');
} else {
$decision = '';
$source = 'pending';
$notes = '';
}
$rows[] = [
'student_id' => $studentId,
'school_id' => $info['school_id'],
'firstname' => $info['firstname'],
'lastname' => $info['lastname'],
'gender' => $info['gender'] ?? '',
'class_section_id' => (int) ($info['class_section_id'] ?? 0),
'class_section_name' => $info['class_section_name'],
'fall_score' => $fall,
'spring_score' => $spring,
'year_score' => $yearScore,
'decision' => $decision,
'source' => $source,
'notes' => $notes,
'saved' => isset($savedMap[$studentId]),
'is_trophy' => false,
];
}
$rowsByClass = [];
foreach ($rows as $index => $row) {
$classSectionId = (int) ($row['class_section_id'] ?? 0);
if ($classSectionId <= 0) {
continue;
}
$rowsByClass[$classSectionId][] = $index;
}
foreach ($rowsByClass as $classIndexes) {
$scores = [];
foreach ($classIndexes as $rowIndex) {
$yearScore = $rows[$rowIndex]['year_score'] ?? null;
if (is_numeric($yearScore)) {
$scores[] = (float) $yearScore;
}
}
$thresholdInfo = $this->calculateTrophyThreshold($scores, 75.0);
$threshold = $thresholdInfo['threshold'];
if ($threshold === null) {
continue;
}
foreach ($classIndexes as $rowIndex) {
$yearScore = $rows[$rowIndex]['year_score'] ?? null;
$rows[$rowIndex]['is_trophy'] = is_numeric($yearScore) && (float) $yearScore >= $threshold;
}
}
return [
'rows' => $rows,
'generated' => !empty($savedMap),
'school_year' => $schoolYear,
];
}
public function generateAllDecisions(string $schoolYear, ?int $userId): int
{
$allScoreRows = DB::table('semester_scores as ss')
->select([
's.id as student_id',
's.firstname',
's.lastname',
'cs.class_section_name',
DB::raw('LOWER(TRIM(ss.semester)) as sem_key'),
'ss.semester_score',
])
->join('students as s', 's.id', '=', 'ss.student_id')
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'ss.class_section_id')
->where('s.is_active', 1)
->where('ss.school_year', $schoolYear)
->whereRaw('LOWER(TRIM(ss.semester)) IN (?, ?)', ['fall', 'spring'])
->whereNotNull('ss.semester_score')
->get();
if ($allScoreRows->isEmpty()) {
throw new RuntimeException('No semester scores found for this school year.');
}
$studentMap = [];
foreach ($allScoreRows as $row) {
$studentId = (int) ($row->student_id ?? 0);
if ($studentId <= 0) {
continue;
}
if (!isset($studentMap[$studentId])) {
$studentMap[$studentId] = [
'firstname' => $row->firstname,
'lastname' => $row->lastname,
'class_section_name' => $row->class_section_name,
'fall_score' => null,
'spring_score' => null,
];
}
$semesterKey = strtolower(trim((string) ($row->sem_key ?? '')));
$score = is_numeric($row->semester_score ?? null) ? (float) $row->semester_score : null;
if ($semesterKey === 'fall') {
$studentMap[$studentId]['fall_score'] = $score;
} elseif ($semesterKey === 'spring') {
$studentMap[$studentId]['spring_score'] = $score;
}
}
$belowRows = BelowSixtyDecision::query()
->where('school_year', $schoolYear)
->where('semester', 'year')
->get();
$belowMap = [];
foreach ($belowRows as $row) {
$studentId = (int) ($row->student_id ?? 0);
if ($studentId <= 0) {
continue;
}
if (!isset($belowMap[$studentId]) || trim((string) ($belowMap[$studentId]->decision ?? '')) === '') {
$belowMap[$studentId] = $row;
}
}
$existingRows = StudentDecision::query()
->where('school_year', $schoolYear)
->get();
$existingMap = [];
foreach ($existingRows as $row) {
$studentId = (int) ($row->student_id ?? 0);
if ($studentId > 0) {
$existingMap[$studentId] = $row;
}
}
$savedCount = 0;
foreach ($studentMap as $studentId => $info) {
$fall = $info['fall_score'];
$spring = $info['spring_score'];
if ($fall !== null && $spring !== null) {
$yearScore = round(($fall + $spring) / 2, 2);
} elseif ($fall !== null) {
$yearScore = round((float) $fall, 2);
} elseif ($spring !== null) {
$yearScore = round((float) $spring, 2);
} else {
continue;
}
if ($yearScore >= 60) {
$decision = 'Pass';
$source = 'auto';
$notes = null;
} elseif (isset($belowMap[$studentId]) && trim((string) ($belowMap[$studentId]->decision ?? '')) !== '') {
$decision = trim((string) $belowMap[$studentId]->decision);
$source = 'manual';
$notes = trim((string) ($belowMap[$studentId]->notes ?? ''));
$notes = $notes !== '' ? $notes : null;
} else {
$decision = null;
$source = 'pending';
$notes = null;
}
$payload = [
'student_id' => $studentId,
'school_year' => $schoolYear,
'class_section_name' => $info['class_section_name'] ?? null,
'year_score' => $yearScore,
'decision' => $decision,
'source' => $source,
'notes' => $notes,
'generated_by' => $userId,
];
if (isset($existingMap[$studentId])) {
$existingMap[$studentId]->fill($payload)->save();
} else {
StudentDecision::query()->create($payload);
}
$savedCount++;
}
return $savedCount;
}
private function fetchBelowSixtyEmailRow(int $studentId, string $schoolYear, string $semester): array
{
$semesterKey = strtolower(trim($semester));
@@ -866,6 +1168,108 @@ class GradingBelowSixtyService
return $html;
}
private function calculateTrophyThreshold(array $scores, float $percentile = 75.0): array
{
$scores = array_values(array_filter(
$scores,
static fn ($value): bool => is_numeric($value) && $value !== null
));
$scores = array_map('floatval', $scores);
sort($scores);
$count = count($scores);
if ($count === 0) {
return ['threshold' => null, 'winners' => 0, 'method' => 'empty'];
}
$minWinners = 3;
$maxWinners = max($minWinners, (int) floor($count * (1 - $percentile / 100)));
$threshold = $this->empiricalTrophyPercentile($scores, $percentile);
$winners = $this->countScoresAtOrAbove($scores, $threshold);
if ($winners < $minWinners) {
$target = min($minWinners, $count);
$descending = array_reverse($scores);
$threshold = $descending[$target - 1];
$winners = $this->countScoresAtOrAbove($scores, $threshold);
return ['threshold' => $threshold, 'winners' => $winners, 'method' => 'min3_reduced'];
}
if ($winners <= $maxWinners) {
return ['threshold' => $threshold, 'winners' => $winners, 'method' => 'empirical_percentile'];
}
$result = $this->capTrophyThresholdByRank($scores, $maxWinners);
if ($result['winners'] < $minWinners) {
$target = min($minWinners, $count);
$descending = array_reverse($scores);
$threshold = $descending[$target - 1];
$winners = $this->countScoresAtOrAbove($scores, $threshold);
return ['threshold' => $threshold, 'winners' => $winners, 'method' => 'min3_after_cap'];
}
return $result;
}
private function capTrophyThresholdByRank(array $sortedScores, int $max): array
{
$descending = array_reverse($sortedScores);
$threshold = $descending[$max - 1];
$winners = $this->countScoresAtOrAbove($sortedScores, $threshold);
if ($winners <= $max) {
return ['threshold' => $threshold, 'winners' => $winners, 'method' => 'capped_25pct'];
}
$uniqueHigherScores = array_values(array_unique(array_filter(
$sortedScores,
static fn ($score): bool => $score > $threshold
)));
sort($uniqueHigherScores);
foreach ($uniqueHigherScores as $candidate) {
$winnerCount = $this->countScoresAtOrAbove($sortedScores, $candidate);
if ($winnerCount <= $max) {
return ['threshold' => $candidate, 'winners' => $winnerCount, 'method' => 'capped_25pct'];
}
}
return ['threshold' => $sortedScores[0], 'winners' => count($sortedScores), 'method' => 'all_equal'];
}
private function empiricalTrophyPercentile(array $sortedScores, float $percentile): float
{
$count = count($sortedScores);
if ($count === 0) {
return 0.0;
}
if ($count === 1) {
return (float) $sortedScores[0];
}
$rank = ($percentile / 100) * ($count - 1);
$lowerIndex = (int) floor($rank);
$upperIndex = (int) ceil($rank);
$weight = $rank - $lowerIndex;
$lower = (float) $sortedScores[$lowerIndex];
$upper = (float) $sortedScores[$upperIndex];
return $lower + (($upper - $lower) * $weight);
}
private function countScoresAtOrAbove(array $scores, float $threshold): int
{
return count(array_filter(
$scores,
static fn ($score): bool => is_numeric($score) && (float) $score >= $threshold
));
}
private function buildDecisionEmailDetailsHtml(array $semesters): string
{
if (empty($semesters)) {
@@ -387,7 +387,7 @@ class PrintRequestsPortalService
public function storeTeacherUpload(array $data, UploadedFile $file, int $teacherId): PrintRequest
{
$this->ensureStorageDirectoryExists();
$ext = $file->getClientOriginalExtension();
$ext = $this->safeExtensionForUpload($file);
$stored = Str::uuid()->toString().($ext !== '' ? '.'.$ext : '');
$file->move($this->storageDirectory(), $stored);
@@ -432,7 +432,7 @@ class PrintRequestsPortalService
}
}
}
$ext = $newFile->getClientOriginalExtension();
$ext = $this->safeExtensionForUpload($newFile);
$stored = Str::uuid()->toString().($ext !== '' ? '.'.$ext : '');
$newFile->move($this->storageDirectory(), $stored);
$update['file_path'] = $stored;
@@ -445,6 +445,29 @@ class PrintRequestsPortalService
return $request->fresh();
}
private function safeExtensionForUpload(UploadedFile $file): string
{
$allowed = [
'application/pdf' => 'pdf',
'image/jpeg' => 'jpg',
'image/png' => 'png',
'text/plain' => 'txt',
'application/msword' => 'doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
];
$mime = strtolower((string) $file->getMimeType());
if (! isset($allowed[$mime])) {
throw new \InvalidArgumentException('Unsupported upload type.');
}
if ((int) $file->getSize() > 5 * 1024 * 1024) {
throw new \InvalidArgumentException('Uploaded file too large. Max 5MB.');
}
return $allowed[$mime];
}
public function applyAdminStatus(PrintRequest $request, string $newStatus, int $adminUserId): PrintRequest
{
$current = (string) $request->status;
@@ -1081,8 +1081,6 @@ class ReportCardService
private function formatReportPDF(\FPDF $pdf, array $data): void
{
require_once base_path('app/ThirdParty/fpdf/fpdf.php');
$w = $pdf->GetPageWidth();
$pdf->SetMargins(10, 10, 10);
$bottomMarginVal = 3;
+22 -2
View File
@@ -12,13 +12,33 @@ class RoleQueryService
public function listRoles(array $filters): LengthAwarePaginator
{
$query = Role::query()
->select('roles.*')
->select([
'roles.id',
'roles.name',
'roles.slug',
'roles.description',
'roles.dashboard_route',
'roles.priority',
'roles.is_active',
'roles.created_at',
'roles.updated_at',
])
->leftJoin('user_roles', function ($join) {
$join->on('user_roles.role_id', '=', 'roles.id')
->whereNull('user_roles.deleted_at');
})
->selectRaw('COUNT(DISTINCT user_roles.user_id) AS user_count')
->groupBy('roles.id');
->groupBy([
'roles.id',
'roles.name',
'roles.slug',
'roles.description',
'roles.dashboard_route',
'roles.priority',
'roles.is_active',
'roles.created_at',
'roles.updated_at',
]);
if (!empty($filters['search'])) {
$search = trim((string) $filters['search']);
+26 -2
View File
@@ -3,6 +3,8 @@
namespace App\Services\Roles;
use App\Models\UserRole;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class RoleSwitchService
{
@@ -16,8 +18,30 @@ class RoleSwitchService
return array_values(array_filter(array_map(fn ($r) => (string) ($r['role_name'] ?? ''), $roles)));
}
public function switchRole(string $role): string
public function switchRole(int $userId, string $role): string
{
return $this->dashboardService->bestDashboardRouteFor([$role]);
$requested = strtolower(trim($role));
if ($requested === '') {
throw new NotFoundHttpException('Role not found.');
}
$roles = $this->getUserRoleNames($userId);
$matchedRole = null;
foreach ($roles as $assignedRole) {
$normalized = strtolower(trim($assignedRole));
if ($normalized === $requested) {
$matchedRole = $assignedRole;
break;
}
}
if ($matchedRole === null) {
throw new AccessDeniedHttpException('Invalid role selected.');
}
session()->put('role', $matchedRole);
return $this->dashboardService->bestDashboardRouteFor([$matchedRole]);
}
}