Implement & test API-authentication, simplify Api-routes
This commit is contained in:
@@ -56,7 +56,8 @@ class Handler extends ExceptionHandler
|
||||
{
|
||||
$response = parent::render($request, $exception);
|
||||
|
||||
if (in_array($response->status(), [403, 404])) {
|
||||
// Only return an Inertia-response when there are special Vue-templates (403 and 404) and when it isn't an API request.
|
||||
if (in_array($response->status(), [403, 404]) && ! $request->routeIs('api.*')) {
|
||||
return app(HandleInertiaRequests::class)
|
||||
->handle($request, fn () => inertia()->render('Errors/' . $response->status(), ['status' => $response->status()])
|
||||
->toResponse($request));
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
if (!function_exists('setting')) {
|
||||
/**
|
||||
* @param null $key
|
||||
* @param null $default
|
||||
* @return array|ArrayAccess|bool|\Illuminate\Contracts\Foundation\Application|mixed
|
||||
* @return array|ArrayAccess|bool|Application|mixed
|
||||
*/
|
||||
function setting($key = null, $default = null)
|
||||
{
|
||||
if (is_array($key)) {
|
||||
\App\Models\Setting::updateOrCreate([
|
||||
Setting::updateOrCreate([
|
||||
'key' => key($key)
|
||||
], [
|
||||
'value' => Arr::first($key)
|
||||
@@ -20,6 +22,7 @@ if (!function_exists('setting')) {
|
||||
try {
|
||||
cache()->forget('core.settings');
|
||||
} catch (Exception $e) {
|
||||
//
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -8,15 +8,12 @@ use App\Services\Ploi\Exceptions\Http\Unauthenticated;
|
||||
|
||||
class GlobalApiAuthenticated
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
abort_unless($this->hasApiEnabled(), 404);
|
||||
|
||||
abort_unless($this->isAuthenticated($request), 403);
|
||||
|
||||
if (! $this->isAuthenticated($request)) {
|
||||
throw new Unauthenticated('Unauthenticated for global access.');
|
||||
}
|
||||
@@ -24,12 +21,14 @@ class GlobalApiAuthenticated
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
protected function hasApiEnabled(): bool
|
||||
{
|
||||
return setting('enable_api') && (bool) setting('api_token');
|
||||
}
|
||||
|
||||
protected function isAuthenticated(Request $request)
|
||||
{
|
||||
return
|
||||
setting('enable_api') &&
|
||||
setting('api_token') &&
|
||||
$request->bearerToken() &&
|
||||
$request->bearerToken() === decrypt(setting('api_token'));
|
||||
return $request->bearerToken()
|
||||
&& $request->bearerToken() === decrypt(setting('api_token'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,7 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class UserResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($request)
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Exception;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
@@ -14,7 +15,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
return $app['cache']->remember('core.settings', now()->addDay(), function () {
|
||||
try {
|
||||
return Setting::pluck('value', 'key')->toArray();
|
||||
} catch (\Exception $exception) {
|
||||
} catch (Exception $exception) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -36,12 +36,13 @@ class RouteServiceProvider extends ServiceProvider
|
||||
$this->configureRateLimiting();
|
||||
|
||||
$this->routes(function () {
|
||||
if (setting('enable_api')) {
|
||||
// The settings('enable_api') is now handled by the GlobalApiAuthenticated middleware,
|
||||
// because the conditional inside this service makes testing very hard.
|
||||
Route::prefix('api')
|
||||
->middleware('api')
|
||||
->middleware(['api', 'global.api.authenticated'])
|
||||
->namespace($this->namespace . '\Api')
|
||||
->as('api.')
|
||||
->group(base_path('routes/api.php'));
|
||||
}
|
||||
|
||||
Route::middleware('web')
|
||||
->namespace($this->namespace)
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\UserController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::group(['middleware' => 'global.api.authenticated'], function () {
|
||||
Route::group(['prefix' => 'users'], function () {
|
||||
Route::get('/', 'UserController@index');
|
||||
Route::post('/', 'UserController@store');
|
||||
Route::get('{user}', 'UserController@show');
|
||||
});
|
||||
});
|
||||
Route::resource('users', UserController::class)
|
||||
->names('user')
|
||||
->only('index', 'store', 'show');
|
||||
|
||||
30
tests/Unit/Http/Middleware/GlobalApiAuthenticatedTest.php
Normal file
30
tests/Unit/Http/Middleware/GlobalApiAuthenticatedTest.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
use function Pest\Laravel\get;
|
||||
|
||||
it('cannot use the API when a user doesn\'t have the API enabled', function () {
|
||||
expect(setting('enable_api'))->toBeNull();
|
||||
expect(setting('api_token'))->toBeNull();
|
||||
|
||||
get(route('api.user.index'))
|
||||
->assertNotFound();
|
||||
|
||||
setting(['enable_api' => true,]);
|
||||
setting(['api_token' => encrypt('secret')]);
|
||||
|
||||
App::forgetInstance('settings');
|
||||
|
||||
expect(setting('enable_api'))->toBeTrue();
|
||||
expect(setting('api_token'))->not->toBeNull();
|
||||
|
||||
get(route('api.user.index'))
|
||||
->assertForbidden();
|
||||
|
||||
get(route('api.user.index'), ['Authorization' => 'Bearer wrong-secret'])
|
||||
->assertForbidden();
|
||||
|
||||
get(route('api.user.index'), ['Authorization' => 'Bearer secret'])
|
||||
->assertOk();
|
||||
});
|
||||
Reference in New Issue
Block a user