mirror of
https://github.com/cydrobolt/polr.git
synced 2024-09-19 15:11:40 +02:00
Use ApiException to handle API errors and use ApiMiddleware to handle API authentication
This commit is contained in:
parent
8f6d9762e9
commit
65794f83d7
42
app/Exceptions/Api/ApiException.php
Normal file
42
app/Exceptions/Api/ApiException.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Exceptions\Api;
|
||||||
|
|
||||||
|
class ApiException extends \Exception {
|
||||||
|
/**
|
||||||
|
* Catch an API exception.
|
||||||
|
*
|
||||||
|
* @param string $text_code
|
||||||
|
* @param string $message
|
||||||
|
* @param integer $status_code
|
||||||
|
* @param string $response_type
|
||||||
|
* @param \Exception $previous
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function __construct($text_code='SERVER_ERROR', $message, $status_code = 0, $response_type='plain_text', Exception $previous = null) {
|
||||||
|
// TODO special Polr error codes for JSON
|
||||||
|
|
||||||
|
$this->response_type = $response_type;
|
||||||
|
$this->text_code = $text_code;
|
||||||
|
parent::__construct($message, $status_code, $previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function encodeJsonResponse($status_code, $message, $text_code) {
|
||||||
|
$response = [
|
||||||
|
'status_code' => $status_code,
|
||||||
|
'error_code' => $text_code,
|
||||||
|
'error' => $message
|
||||||
|
];
|
||||||
|
|
||||||
|
return json_encode($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEncodedErrorMessage() {
|
||||||
|
if ($this->response_type == 'json') {
|
||||||
|
return $this->encodeJsonResponse($this->code, $this->message, $this->text_code);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return $this->code . ' ' . $this->message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|||||||
use Laravel\Lumen\Exceptions\Handler as ExceptionHandler;
|
use Laravel\Lumen\Exceptions\Handler as ExceptionHandler;
|
||||||
use Illuminate\Support\Facades\Response;
|
use Illuminate\Support\Facades\Response;
|
||||||
|
|
||||||
|
use App\Exceptions\Api\ApiException;
|
||||||
|
|
||||||
class Handler extends ExceptionHandler {
|
class Handler extends ExceptionHandler {
|
||||||
/**
|
/**
|
||||||
* A list of the exception types that should not be reported.
|
* A list of the exception types that should not be reported.
|
||||||
@ -43,6 +45,7 @@ class Handler extends ExceptionHandler {
|
|||||||
if (env('APP_DEBUG') != true) {
|
if (env('APP_DEBUG') != true) {
|
||||||
// Render nice error pages if debug is off
|
// Render nice error pages if debug is off
|
||||||
if ($e instanceof NotFoundHttpException) {
|
if ($e instanceof NotFoundHttpException) {
|
||||||
|
// Handle 404 exceptions
|
||||||
if (env('SETTING_REDIRECT_404')) {
|
if (env('SETTING_REDIRECT_404')) {
|
||||||
// Redirect 404s to SETTING_INDEX_REDIRECT
|
// Redirect 404s to SETTING_INDEX_REDIRECT
|
||||||
return redirect()->to(env('SETTING_INDEX_REDIRECT'));
|
return redirect()->to(env('SETTING_INDEX_REDIRECT'));
|
||||||
@ -51,6 +54,7 @@ class Handler extends ExceptionHandler {
|
|||||||
return view('errors.404');
|
return view('errors.404');
|
||||||
}
|
}
|
||||||
if ($e instanceof HttpException) {
|
if ($e instanceof HttpException) {
|
||||||
|
// Handle HTTP exceptions thrown by public-facing controllers
|
||||||
$status_code = $e->getStatusCode();
|
$status_code = $e->getStatusCode();
|
||||||
$status_message = $e->getMessage();
|
$status_message = $e->getMessage();
|
||||||
|
|
||||||
@ -63,6 +67,20 @@ class Handler extends ExceptionHandler {
|
|||||||
return response(view('errors.generic', ['status_code' => $status_code, 'status_message' => $status_message]), $status_code);
|
return response(view('errors.generic', ['status_code' => $status_code, 'status_message' => $status_message]), $status_code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ($e instanceof ApiException) {
|
||||||
|
// Handle HTTP exceptions thrown by API controllers
|
||||||
|
$status_code = $e->getCode();
|
||||||
|
$encoded_status_message = $e->getEncodedErrorMessage();
|
||||||
|
if ($e->response_type == 'json') {
|
||||||
|
return response($encoded_status_message, $status_code)
|
||||||
|
->header('Content-Type', 'application/json')
|
||||||
|
->header('Access-Control-Allow-Origin', '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response($encoded_status_message, $status_code)
|
||||||
|
->header('Content-Type', 'text/plain')
|
||||||
|
->header('Access-Control-Allow-Origin', '*');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent::render($request, $e);
|
return parent::render($request, $e);
|
||||||
|
@ -5,13 +5,14 @@ use Illuminate\Http\Request;
|
|||||||
use App\Helpers\LinkHelper;
|
use App\Helpers\LinkHelper;
|
||||||
use App\Helpers\UserHelper;
|
use App\Helpers\UserHelper;
|
||||||
use App\Helpers\StatsHelper;
|
use App\Helpers\StatsHelper;
|
||||||
|
use App\Exceptions\Api\ApiException;
|
||||||
|
|
||||||
class ApiAnalyticsController extends ApiController {
|
class ApiAnalyticsController extends ApiController {
|
||||||
public function lookupLinkStats (Request $request, $stats_type=false) {
|
public function lookupLinkStats (Request $request, $stats_type=false) {
|
||||||
$response_type = $request->input('response_type') ?: 'json';
|
$response_type = $request->input('response_type') ?: 'json';
|
||||||
|
|
||||||
if ($response_type != 'json') {
|
if ($response_type != 'json') {
|
||||||
abort(401, 'Only JSON-encoded data is available for this endpoint.');
|
throw new ApiException('JSON_ONLY', 'Only JSON-encoded data is available for this endpoint.', 401, $response_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = self::getApiUserInfo($request);
|
$user = self::getApiUserInfo($request);
|
||||||
@ -24,7 +25,7 @@ class ApiAnalyticsController extends ApiController {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
return abort(400, 'Invalid or missing parameters.');
|
throw new ApiException('MISSING_PARAMETERS', 'Invalid or missing parameters.', 400, $response_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
$url_ending = $request->input('url_ending');
|
$url_ending = $request->input('url_ending');
|
||||||
@ -37,13 +38,13 @@ class ApiAnalyticsController extends ApiController {
|
|||||||
$link = LinkHelper::linkExists($url_ending);
|
$link = LinkHelper::linkExists($url_ending);
|
||||||
|
|
||||||
if ($link === false) {
|
if ($link === false) {
|
||||||
abort(404, 'Link not found.');
|
throw new ApiException('NOT_FOUND', 'Link not found.', 404, $response_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (($link->creator != $user->username) &&
|
if (($link->creator != $user->username) &&
|
||||||
!(UserHelper::userIsAdmin($user->username))){
|
!(UserHelper::userIsAdmin($user->username))){
|
||||||
// If user does not own link and is not an admin
|
// If user does not own link and is not an admin
|
||||||
abort(401, 'Unauthorized.');
|
throw new ApiException('ACCESS_DENIED', 'Unauthorized.', 401, $response_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
$stats = new StatsHelper($link->id, $left_bound, $right_bound);
|
$stats = new StatsHelper($link->id, $left_bound, $right_bound);
|
||||||
@ -58,7 +59,7 @@ class ApiAnalyticsController extends ApiController {
|
|||||||
$fetched_stats = $stats->getRefererStats();
|
$fetched_stats = $stats->getRefererStats();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
abort(400, 'Invalid analytics type requested.');
|
throw new ApiException('INVALID_ANALYTICS_TYPE', 'Invalid analytics type requested.', 400, $response_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::encodeResponse([
|
return self::encodeResponse([
|
||||||
|
@ -2,48 +2,8 @@
|
|||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Helpers\ApiHelper;
|
|
||||||
|
|
||||||
class ApiController extends Controller {
|
class ApiController extends Controller {
|
||||||
protected static function getApiUserInfo(Request $request) {
|
|
||||||
$api_key = $request->input('key');
|
|
||||||
|
|
||||||
if (!$api_key) {
|
|
||||||
// no API key provided -- check whether anonymous API is on
|
|
||||||
|
|
||||||
if (env('SETTING_ANON_API')) {
|
|
||||||
$username = 'ANONIP-' . $request->ip();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
abort(401, "Authentication token required.");
|
|
||||||
}
|
|
||||||
$user = (object) [
|
|
||||||
'username' => $username
|
|
||||||
];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$user = User::where('active', 1)
|
|
||||||
->where('api_key', $api_key)
|
|
||||||
->where('api_active', 1)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (!$user) {
|
|
||||||
abort(401, "Invalid authentication token.");
|
|
||||||
}
|
|
||||||
$username = $user->username;
|
|
||||||
}
|
|
||||||
|
|
||||||
$api_limit_reached = ApiHelper::checkUserApiQuota($username);
|
|
||||||
|
|
||||||
if ($api_limit_reached) {
|
|
||||||
abort(403, "Quota exceeded.");
|
|
||||||
}
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static function encodeResponse($result, $action, $response_type='json', $plain_text_response=false) {
|
protected static function encodeResponse($result, $action, $response_type='json', $plain_text_response=false) {
|
||||||
$response = [
|
$response = [
|
||||||
"action" => $action,
|
"action" => $action,
|
||||||
@ -64,7 +24,6 @@ class ApiController extends Controller {
|
|||||||
return response($result)
|
return response($result)
|
||||||
->header('Content-Type', 'text/plain')
|
->header('Content-Type', 'text/plain')
|
||||||
->header('Access-Control-Allow-Origin', '*');
|
->header('Access-Control-Allow-Origin', '*');
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,13 @@ use Illuminate\Http\Request;
|
|||||||
|
|
||||||
use App\Factories\LinkFactory;
|
use App\Factories\LinkFactory;
|
||||||
use App\Helpers\LinkHelper;
|
use App\Helpers\LinkHelper;
|
||||||
|
use App\Exceptions\Api\ApiException;
|
||||||
|
|
||||||
class ApiLinkController extends ApiController {
|
class ApiLinkController extends ApiController {
|
||||||
public function shortenLink(Request $request) {
|
public function shortenLink(Request $request) {
|
||||||
$response_type = $request->input('response_type');
|
$response_type = $request->input('response_type');
|
||||||
$user = self::getApiUserInfo($request);
|
// $user = self::getApiUserInfo($request);
|
||||||
|
$user = $request->user;
|
||||||
|
|
||||||
// Validate parameters
|
// Validate parameters
|
||||||
// Encode spaces as %20 to avoid validator conflicts
|
// Encode spaces as %20 to avoid validator conflicts
|
||||||
@ -19,7 +21,7 @@ class ApiLinkController extends ApiController {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
return abort(400, 'Invalid or missing parameters.');
|
throw new ApiException('MISSING_PARAMETERS', 'Invalid or missing parameters.', 400, $response_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
$long_url = $request->input('url'); // * required
|
$long_url = $request->input('url'); // * required
|
||||||
@ -32,15 +34,17 @@ class ApiLinkController extends ApiController {
|
|||||||
$formatted_link = LinkFactory::createLink($long_url, $is_secret, $custom_ending, $link_ip, $user->username, false, true);
|
$formatted_link = LinkFactory::createLink($long_url, $is_secret, $custom_ending, $link_ip, $user->username, false, true);
|
||||||
}
|
}
|
||||||
catch (\Exception $e) {
|
catch (\Exception $e) {
|
||||||
abort(400, $e->getMessage());
|
throw new ApiException('CREATE_ERROR', $e->getMessage(), 400, $response_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::encodeResponse($formatted_link, 'shorten', $response_type);
|
return self::encodeResponse($formatted_link, 'shorten', $response_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function lookupLink(Request $request) {
|
public function lookupLink(Request $request) {
|
||||||
|
$user = $request->user;
|
||||||
|
|
||||||
$response_type = $request->input('response_type');
|
$response_type = $request->input('response_type');
|
||||||
$user = self::getApiUserInfo($request);
|
// $user = self::getApiUserInfo($request);
|
||||||
|
|
||||||
// Validate URL form data
|
// Validate URL form data
|
||||||
$validator = \Validator::make($request->all(), [
|
$validator = \Validator::make($request->all(), [
|
||||||
@ -48,7 +52,7 @@ class ApiLinkController extends ApiController {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
return abort(400, 'Invalid or missing parameters.');
|
throw new ApiException('MISSING_PARAMETERS', 'Invalid or missing parameters.', 400, $response_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
$url_ending = $request->input('url_ending');
|
$url_ending = $request->input('url_ending');
|
||||||
@ -60,7 +64,7 @@ class ApiLinkController extends ApiController {
|
|||||||
|
|
||||||
if ($link['secret_key']) {
|
if ($link['secret_key']) {
|
||||||
if ($url_key != $link['secret_key']) {
|
if ($url_key != $link['secret_key']) {
|
||||||
abort(401, "Invalid URL code for secret URL.");
|
throw new ApiException('ACCESS_DENIED', 'Invalid URL code for secret URL.', 401, $response_type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,8 +78,7 @@ class ApiLinkController extends ApiController {
|
|||||||
], 'lookup', $response_type, $link['long_url']);
|
], 'lookup', $response_type, $link['long_url']);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
abort(404, "Link not found.");
|
throw new ApiException('NOT_FOUND', 'Link not found.', 404, $response_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
62
app/Http/Middleware/ApiMiddleware.php
Normal file
62
app/Http/Middleware/ApiMiddleware.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Helpers\ApiHelper;
|
||||||
|
use App\Exceptions\Api\ApiException;
|
||||||
|
|
||||||
|
class ApiMiddleware {
|
||||||
|
protected static function getApiUserInfo(Request $request) {
|
||||||
|
$api_key = $request->input('key');
|
||||||
|
$response_type = $request->input('response_type');
|
||||||
|
|
||||||
|
if (!$api_key) {
|
||||||
|
// no API key provided; check whether anonymous API is enabled
|
||||||
|
|
||||||
|
if (env('SETTING_ANON_API')) {
|
||||||
|
$username = 'ANONIP-' . $request->ip();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new ApiException('AUTH_ERROR', 'Authentication token required.', 401, $response_type);
|
||||||
|
}
|
||||||
|
$user = (object) [
|
||||||
|
'username' => $username
|
||||||
|
];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$user = User::where('active', 1)
|
||||||
|
->where('api_key', $api_key)
|
||||||
|
->where('api_active', 1)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
abort(401, "Invalid authentication token.");
|
||||||
|
}
|
||||||
|
$username = $user->username;
|
||||||
|
}
|
||||||
|
|
||||||
|
$api_limit_reached = ApiHelper::checkUserApiQuota($username);
|
||||||
|
|
||||||
|
if ($api_limit_reached) {
|
||||||
|
throw new ApiException('QUOTA_EXCEEDED', 'Quota exceeded.', 429, $response_type);
|
||||||
|
}
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
|
||||||
|
public function handle($request, Closure $next) {
|
||||||
|
$request->user = $this->getApiUserInfo($request);
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,7 @@ class VerifyCsrfToken extends BaseVerifier
|
|||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
public function handle($request, \Closure $next) {
|
public function handle($request, \Closure $next) {
|
||||||
if ($request->is('api/v*/action/*')) {
|
if ($request->is('api/v*/action/*') || $request->is('api/v*/data/*')) {
|
||||||
// Exclude public API from CSRF protection
|
// Exclude public API from CSRF protection
|
||||||
// but do not exclude private API endpoints
|
// but do not exclude private API endpoints
|
||||||
return $next($request);
|
return $next($request);
|
||||||
|
@ -59,17 +59,18 @@ $app->group(['prefix' => '/api/v2', 'namespace' => 'App\Http\Controllers'], func
|
|||||||
$app->get('admin/get_admin_users', ['as' => 'api_get_admin_users', 'uses' => 'AdminPaginationController@paginateAdminUsers']);
|
$app->get('admin/get_admin_users', ['as' => 'api_get_admin_users', 'uses' => 'AdminPaginationController@paginateAdminUsers']);
|
||||||
$app->get('admin/get_admin_links', ['as' => 'api_get_admin_links', 'uses' => 'AdminPaginationController@paginateAdminLinks']);
|
$app->get('admin/get_admin_links', ['as' => 'api_get_admin_links', 'uses' => 'AdminPaginationController@paginateAdminLinks']);
|
||||||
$app->get('admin/get_user_links', ['as' => 'api_get_user_links', 'uses' => 'AdminPaginationController@paginateUserLinks']);
|
$app->get('admin/get_user_links', ['as' => 'api_get_user_links', 'uses' => 'AdminPaginationController@paginateUserLinks']);
|
||||||
|
});
|
||||||
|
|
||||||
|
$app->group(['prefix' => '/api/v2', 'namespace' => 'App\Http\Controllers\Api', 'middleware' => 'api'], function ($app) {
|
||||||
/* API shorten endpoints */
|
/* API shorten endpoints */
|
||||||
$app->post('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'Api\ApiLinkController@shortenLink']);
|
$app->post('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'ApiLinkController@shortenLink']);
|
||||||
$app->get('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'Api\ApiLinkController@shortenLink']);
|
$app->get('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'ApiLinkController@shortenLink']);
|
||||||
|
|
||||||
/* API lookup endpoints */
|
/* API lookup endpoints */
|
||||||
$app->post('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'Api\ApiLinkController@lookupLink']);
|
$app->post('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'ApiLinkController@lookupLink']);
|
||||||
$app->get('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'Api\ApiLinkController@lookupLink']);
|
$app->get('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'ApiLinkController@lookupLink']);
|
||||||
|
|
||||||
/* API data endpoints */
|
/* API data endpoints */
|
||||||
$app->get('data/link', ['as' => 'api_link_analytics', 'uses' => 'Api\ApiAnalyticsController@lookupLinkStats']);
|
$app->get('data/link', ['as' => 'api_link_analytics', 'uses' => 'ApiAnalyticsController@lookupLinkStats']);
|
||||||
$app->post('data/link', ['as' => 'api_link_analytics', 'uses' => 'Api\ApiAnalyticsController@lookupLinkStats']);
|
$app->post('data/link', ['as' => 'api_link_analytics', 'uses' => 'ApiAnalyticsController@lookupLinkStats']);
|
||||||
});
|
});
|
||||||
|
@ -61,12 +61,12 @@ $app->middleware([
|
|||||||
// Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
// Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||||
Illuminate\Session\Middleware\StartSession::class,
|
Illuminate\Session\Middleware\StartSession::class,
|
||||||
Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||||
App\Http\Middleware\VerifyCsrfToken::class
|
App\Http\Middleware\VerifyCsrfToken::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// $app->routeMiddleware([
|
$app->routeMiddleware([
|
||||||
|
'api' => App\Http\Middleware\ApiMiddleware::class,
|
||||||
// ]);
|
]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
Loading…
Reference in New Issue
Block a user