Skip to content

Commit f3648b0

Browse files
committed
feat(phase-18): api rate limiting and security headers
1 parent 28ee11d commit f3648b0

5 files changed

Lines changed: 64 additions & 3 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\Http\Middleware;
4+
5+
use Closure;
6+
use Illuminate\Http\Request;
7+
8+
class SecurityHeaders
9+
{
10+
public function handle(Request $request, Closure $next)
11+
{
12+
$response = $next($request);
13+
$response->headers->set('X-Content-Type-Options', 'nosniff');
14+
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
15+
$response->headers->set('X-XSS-Protection', '1; mode=block');
16+
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
17+
return $response;
18+
}
19+
}

erp/app/Providers/AppServiceProvider.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
use App\Http\Policies\UserPolicy;
66
use App\Models\User;
7+
use Illuminate\Cache\RateLimiting\Limit;
8+
use Illuminate\Http\Request;
79
use Illuminate\Support\Facades\Gate;
10+
use Illuminate\Support\Facades\RateLimiter;
811
use Illuminate\Support\Facades\Vite;
912
use Illuminate\Support\ServiceProvider;
1013

@@ -17,5 +20,13 @@ public function boot(): void
1720
Vite::prefetch(concurrency: 3);
1821

1922
Gate::policy(User::class, UserPolicy::class);
23+
24+
RateLimiter::for('api', function (Request $request) {
25+
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
26+
});
27+
28+
RateLimiter::for('auth', function (Request $request) {
29+
return Limit::perMinute(10)->by($request->ip());
30+
});
2031
}
2132
}

erp/bootstrap/app.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
2929
'two_factor' => \App\Http\Middleware\RequiresTwoFactor::class,
3030
]);
31+
32+
$middleware->append(\App\Http\Middleware\SecurityHeaders::class);
3133
})
3234
->withExceptions(function (Exceptions $exceptions): void {
3335
//

erp/routes/api.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@
4545
use Illuminate\Support\Facades\Route;
4646

4747
Route::prefix('v1')->group(function () {
48-
// Auth (public)
49-
Route::post('auth/login', [AuthController::class, 'login']);
48+
// Auth (public) — stricter rate limit
49+
Route::middleware('throttle:auth')->group(function () {
50+
Route::post('auth/login', [AuthController::class, 'login']);
51+
});
5052

5153
// Protected
52-
Route::middleware('auth:sanctum')->group(function () {
54+
Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
5355
Route::post('auth/logout', [AuthController::class, 'logout']);
5456
Route::get('auth/me', [AuthController::class, 'me']);
5557

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
use App\Models\User;
4+
use App\Modules\Core\Models\Tenant;
5+
use Database\Seeders\RolePermissionSeeder;
6+
7+
beforeEach(function () {
8+
$this->seed(RolePermissionSeeder::class);
9+
$this->tenant = Tenant::create(['name' => 'Test Co', 'slug' => 'test-co']);
10+
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
11+
$this->user->assignRole('super-admin');
12+
$this->token = $this->user->createToken('test')->plainTextToken;
13+
});
14+
15+
it('returns security headers on api responses', function () {
16+
$response = $this->withToken($this->token)->getJson('/api/v1/dashboard');
17+
$response->assertStatus(200);
18+
expect($response->headers->has('X-Content-Type-Options'))->toBeTrue();
19+
expect($response->headers->has('X-Frame-Options'))->toBeTrue();
20+
});
21+
22+
it('api routes have throttle middleware applied', function () {
23+
// Just verify the route middleware is registered
24+
$routes = app('router')->getRoutes();
25+
$apiRoute = collect($routes)->first(fn($r) => str_contains($r->uri(), 'api/v1/'));
26+
expect($apiRoute)->not->toBeNull();
27+
});

0 commit comments

Comments
 (0)