363 lines
16 KiB
Diff
363 lines
16 KiB
Diff
--- .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'),
|