--- .env --- school_api_orig/school_api/.env 2026-06-04 07:50:20.000000000 +0000 +++ school_api_work/school_api/.env 2026-06-04 17:35:04.127545553 +0000 @@ -1,9 +1,9 @@ APP_NAME=Alrahma_API -APP_ENV=local -APP_KEY=base64:RfIZKqgXC9seghl2Jqs2MgLh1X1Z7APRsnhUK8CgWx8= -APP_DEBUG=true +APP_ENV=production +APP_KEY=base64:CHANGE_ME_GENERATE_WITH_php_artisan_key_generate +APP_DEBUG=false APP_TIMEZONE=America/New_York -APP_URL=http://localhost:8080 +APP_URL=https://example.com APP_LOCALE=en APP_FALLBACK_LOCALE=en @@ -22,18 +22,21 @@ DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=school_api -DB_USERNAME=root -DB_PASSWORD=root -APP_URL=http://127.0.0.1:8000 +DB_USERNAME=school_api_user +DB_PASSWORD=CHANGE_ME_DB_PASSWORD +APP_URL=https://example.com # ---------------------------- # SESSION # ---------------------------- SESSION_DRIVER=file SESSION_LIFETIME=120 -SESSION_ENCRYPT=false +SESSION_ENCRYPT=true SESSION_PATH=/ SESSION_DOMAIN=null +SESSION_SECURE_COOKIE=true +SESSION_HTTP_ONLY=true +SESSION_SAME_SITE=strict # ---------------------------- # CACHE & QUEUE @@ -56,10 +59,10 @@ MAIL_PROFILE_DEFAULT=default MAIL_DEFAULT_HOST=smtp.gmail.com MAIL_DEFAULT_PORT=587 -MAIL_DEFAULT_USER=alrahma.sunday.school@gmail.com -MAIL_DEFAULT_PASS="psnp emdq dykw ypul" +MAIL_DEFAULT_USER=CHANGE_ME_SMTP_USER +MAIL_DEFAULT_PASS=CHANGE_ME_SMTP_PASSWORD MAIL_DEFAULT_ENCRYPTION=tls -MAIL_DEFAULT_FROM_EMAIL="alrahma.sunday.school@gmail.com" +MAIL_DEFAULT_FROM_EMAIL="no-reply@example.com" MAIL_DEFAULT_FROM_NAME="Alrahma API" MAIL_DEFAULT_REPLY_TO="alrahma.isgl@gmail.com" MAIL_DEFAULT_REPLY_TO_NAME="Al Rahma Sunday School" @@ -102,7 +105,7 @@ # ---------------------------- # JWT # ---------------------------- -JWT_SECRET=Uj8rGnYcXMgeRc5qCIn9Wn03tYo1pCsBz1Biou8T9zWtaNxBYi3P4NP5vuFiXHmd +JWT_SECRET=CHANGE_ME_GENERATE_WITH_php_artisan_jwt_secret JWT_ALGO=HS256 JWT_TTL=60 JWT_REFRESH_TTL=20160 --- routes/web.php --- school_api_orig/school_api/routes/web.php 2026-05-29 00:28:37.000000000 +0000 +++ school_api_work/school_api/routes/web.php 2026-06-04 17:35:04.129001398 +0000 @@ -61,7 +61,7 @@ Route::middleware('web')->group(function () { Route::get('login', [AuthSessionController::class, 'loginMask'])->name('login'); Route::post('user/login', [AuthSessionController::class, 'login'])->name('auth.session.login'); - Route::get('logout', [AuthSessionController::class, 'logout'])->name('auth.session.logout'); + Route::post('logout', [AuthSessionController::class, 'logout'])->name('auth.session.logout'); Route::get('select-role', [AuthSessionController::class, 'selectRole'])->name('auth.session.select-role'); Route::post('set-role', [AuthSessionController::class, 'setRole'])->name('auth.session.set-role'); --- routes/api.php --- school_api_orig/school_api/routes/api.php 2026-06-04 17:05:37.000000000 +0000 +++ school_api_work/school_api/routes/api.php 2026-06-04 17:35:04.130570862 +0000 @@ -126,8 +126,8 @@ use App\Http\Controllers\Api\System\AccessDeniedController; use App\Http\Controllers\Api\Certificates\CertificateController; // Public auth aliases without the `/v1` prefix. -Route::post('login', [AuthController::class, 'login']); -Route::post('register', [RegisterController::class, 'store']); +Route::post('login', [AuthController::class, 'login'])->middleware('throttle:10,1'); +Route::post('register', [RegisterController::class, 'store'])->middleware('throttle:5,1'); /* | LanguageTool proxy for legacy and current clients. @@ -160,12 +160,15 @@ }); Route::get('confirm_authorized_user', [AuthorizedUserInviteController::class, 'confirm']) + ->middleware('throttle:20,1') ->name('api.invite.confirm'); Route::get('set_authorized_user_password/{authorizedUserId}', [AuthorizedUserInviteController::class, 'setPasswordForm']) ->whereNumber('authorizedUserId') + ->middleware('throttle:20,1') ->name('api.invite.set-password-form'); Route::post('set_authorized_user_password/{authorizedUserId}', [AuthorizedUserInviteController::class, 'savePassword']) ->whereNumber('authorizedUserId') + ->middleware('throttle:10,1') ->name('api.invite.set-password'); /* @@ -184,13 +187,13 @@ Route::prefix('v1')->group(function () { // Public auth aliases without the `/auth` prefix. - Route::post('login', [AuthController::class, 'login']); - Route::post('register', [RegisterController::class, 'store']); + Route::post('login', [AuthController::class, 'login'])->middleware('throttle:10,1'); + Route::post('register', [RegisterController::class, 'store'])->middleware('throttle:5,1'); Route::prefix('auth')->group(function () { - Route::get('register/captcha', [RegisterController::class, 'captcha']); - Route::post('register', [RegisterController::class, 'store']); - Route::post('login', [AuthController::class, 'login']); + Route::get('register/captcha', [RegisterController::class, 'captcha'])->middleware('throttle:30,1'); + Route::post('register', [RegisterController::class, 'store'])->middleware('throttle:5,1'); + Route::post('login', [AuthController::class, 'login'])->middleware('throttle:10,1'); Route::post('refresh', [AuthController::class, 'refresh'])->middleware('jwt.auth'); Route::post('logout', [AuthController::class, 'logout'])->middleware('auth:api'); Route::get('me', [AuthController::class, 'me'])->middleware('auth:api'); @@ -208,7 +211,7 @@ Route::post('contact', [PageController::class, 'submitContact']); }); - Route::post('contact', [SupportContactController::class, 'send']); + Route::post('contact', [SupportContactController::class, 'send'])->middleware('throttle:5,1'); /* | Badge scan kiosk (public, throttled). Logs: authenticated staff. --- bootstrap/app.php --- school_api_orig/school_api/bootstrap/app.php 2026-05-29 03:38:30.000000000 +0000 +++ school_api_work/school_api/bootstrap/app.php 2026-06-04 17:35:04.128252803 +0000 @@ -15,6 +15,8 @@ health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { + $middleware->append(\App\Http\Middleware\SecurityHeaders::class); + $middleware->alias([ // JWT auth 'jwt.auth' => \PHPOpenSourceSaver\JWTAuth\Http\Middleware\Authenticate::class, --- app/Http/Middleware/SecurityHeaders.php --- app/Services/Files/FileServeService.php --- school_api_orig/school_api/app/Services/Files/FileServeService.php 2026-05-29 00:28:37.000000000 +0000 +++ school_api_work/school_api/app/Services/Files/FileServeService.php 2026-06-04 17:35:04.131136361 +0000 @@ -39,13 +39,10 @@ '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); } --- app/Services/Events/EventManagementService.php --- school_api_orig/school_api/app/Services/Events/EventManagementService.php 2026-05-29 00:28:37.000000000 +0000 +++ school_api_work/school_api/app/Services/Events/EventManagementService.php 2026-06-04 17:35:04.131625457 +0000 @@ -103,12 +103,28 @@ 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; --- app/Services/Expenses/ExpenseReceiptService.php --- school_api_orig/school_api/app/Services/Expenses/ExpenseReceiptService.php 2026-05-29 00:28:37.000000000 +0000 +++ school_api_work/school_api/app/Services/Expenses/ExpenseReceiptService.php 2026-06-04 17:35:04.132035612 +0000 @@ -14,8 +14,30 @@ 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 --- app/Services/BroadcastEmail/BroadcastEmailImageService.php --- school_api_orig/school_api/app/Services/BroadcastEmail/BroadcastEmailImageService.php 2026-05-29 00:28:37.000000000 +0000 +++ school_api_work/school_api/app/Services/BroadcastEmail/BroadcastEmailImageService.php 2026-06-04 17:35:04.132596226 +0000 @@ -17,6 +17,14 @@ 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 @@ } $ext = $this->allowedTypes[$mime]; - $newName = uniqid('em_', true) . '.' . $ext; + $newName = 'em_' . bin2hex(random_bytes(16)) . '.' . $ext; $file->move($targetDir, $newName); return [ --- app/Services/PrintRequests/PrintRequestsPortalService.php --- school_api_orig/school_api/app/Services/PrintRequests/PrintRequestsPortalService.php 2026-06-04 06:36:56.000000000 +0000 +++ school_api_work/school_api/app/Services/PrintRequests/PrintRequestsPortalService.php 2026-06-04 17:35:04.133321294 +0000 @@ -387,7 +387,7 @@ 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 @@ } } } - $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 @@ 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; --- app/Http/Controllers/Api/PrintRequests/PrintRequestsController.php --- school_api_orig/school_api/app/Http/Controllers/Api/PrintRequests/PrintRequestsController.php 2026-05-29 00:28:37.000000000 +0000 +++ school_api_work/school_api/app/Http/Controllers/Api/PrintRequests/PrintRequestsController.php 2026-06-04 17:35:04.133827253 +0000 @@ -47,7 +47,7 @@ } $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 @@ ]; 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); --- app/Models/User.php --- test.sql --- school_api_orig/school_api/test.sql 2026-05-29 00:27:51.000000000 +0000 +++ school_api_work/school_api/test.sql 2026-06-04 17:35:52.923658533 +0000 @@ -479,15 +479,15 @@ (29, 'total_semester2_days', '13'), (30, 'delete_email_notify', 'support@alrahmaisgl.org, melabidi@alrahmaisgl.org'), (31, 'fall_semester_start', '2025-09-21'), -(32, 'sandbox_secret_paypal', 'Ar0Br3ZhBukQwwdSCxIcSveiu70-Ekt-qqF0nSyYNr3eRAYQKcWTTlhCAKncZiYXmBoPeo4IceV6FjT'), +(32, 'sandbox_secret_paypal', 'CHANGE_ME_SANDBOX_SECRET_PAYPAL'), (33, 'sandbox_verifylink_paypal', 'https://api-m.sandbox.paypal.com/v1/notifications/verify-webhook-signature'), (34, 'sandbox_tokenurl_paypal', 'https://api-m.sandbox.paypal.com/v1/oauth2/token'), -(35, 'sandbox_clientid_paypal', 'AYsiOekZUvrjgx9C5c554ZeSQ4W6yd0ZX-OHE93_D0fWoa4YXrMmroEeLiAjjdCKkELH8EVZR_yGMLPS'), -(36, 'sandbox_webhookid_paypal', '0EU27043LC060650D'), +(35, 'sandbox_clientid_paypal', 'CHANGE_ME_SANDBOX_CLIENTID_PAYPAL'), +(36, 'sandbox_webhookid_paypal', 'CHANGE_ME_SANDBOX_WEBHOOKID_PAYPAL'), (37, 'production_verifylink_paypal', 'https://api-m.paypal.com/v1/notifications/verify-webhook-signature'), -(38, 'production_webhookid_paypal', '0H5876426L0092829'), -(39, 'production_secret_paypal', 'KKvmE-0cvwx8rEHnEcFaAvn1Rlc3cy07IGIjqGGPR8EqxABbmPodM_JrjRQq2SF2903A4q0DBjjUWai'), -(40, 'production_clientid_paypal', 'AQhCgmypaitgLgretMqNrh_7mEG5fAmwCECxCxevuLDQfsQVwECFyQOU50byzPzFrwd0cVVD8r2lujIB'), +(38, 'production_webhookid_paypal', 'CHANGE_ME_PRODUCTION_WEBHOOKID_PAYPAL'), +(39, 'production_secret_paypal', 'CHANGE_ME_PRODUCTION_SECRET_PAYPAL'), +(40, 'production_clientid_paypal', 'CHANGE_ME_PRODUCTION_CLIENTID_PAYPAL'), (41, 'production_tokenurl_paypal', 'https://api-m.paypal.com/v1/oauth2/token'), (42, 'comment_reviewer', 'administrator, principal'), (43, 'paypal_mode', 'sandbox'),