API Improvements #116
API Security has been reworked to check if request is secure, verifies the API token created on site setup given to every member, and then confirms the system is accepting API requests by way of the API enabled toggle in settings API usage is now only meant for backend use, so this needs to be noted in the docs
This commit is contained in:
parent
c5afbb9131
commit
4337a20fb8
8 changed files with 92 additions and 332 deletions
|
@ -62,3 +62,19 @@ function createAppKey()
|
|||
{
|
||||
return 'base64:' . base64_encode(Encrypter::generateKey(config('app.cipher')));
|
||||
}
|
||||
|
||||
function isHttps()
|
||||
{
|
||||
if (
|
||||
(isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] == 1)) ||
|
||||
(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') ||
|
||||
(isset($_SERVER['HTTP_X_FORWARDED_SCHEME']) && strtolower($_SERVER['HTTP_X_FORWARDED_SCHEME']) === 'https') ||
|
||||
(isset($_SERVER['HTTP_X_FORWARDED_SSL']) && ($_SERVER['HTTP_X_FORWARDED_SSL'] === 'on' || $_SERVER['HTTP_X_FORWARDED_SSL'] == 1)) ||
|
||||
(isset($_SERVER['REQUEST_SCHEME']) && strtolower($_SERVER['REQUEST_SCHEME']) === 'https') ||
|
||||
(isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -19,8 +19,9 @@ class PageAPIController extends Controller
|
|||
public function write(Request $request)
|
||||
{
|
||||
$body = json_decode($request->getContent());
|
||||
$result = $this->pages->update($body);
|
||||
return response()->json($result)->header('Content-Type', 'application/json');
|
||||
dd($body);
|
||||
//$result = $this->pages->update($body);
|
||||
//return response()->json($result)->header('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
|
|
|
@ -70,6 +70,6 @@ class Kernel extends HttpKernel
|
|||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
||||
'member.check' => \App\Http\Middleware\MemberCheck::class,
|
||||
'validate.token' => \App\Http\Middleware\ValidateAPIToken::class,
|
||||
'validate.key' => \App\Http\Middleware\ValidateAPIKey::class,
|
||||
];
|
||||
}
|
||||
|
|
66
app/Http/Middleware/ValidateAPIKey.php
Normal file
66
app/Http/Middleware/ValidateAPIKey.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use App\Interfaces\MemberRepositoryInterface;
|
||||
use App\Services\Data\SettingsService;
|
||||
|
||||
use function _\find;
|
||||
|
||||
class ValidateAPIKey
|
||||
{
|
||||
protected $member;
|
||||
protected $settings;
|
||||
|
||||
public function __construct(
|
||||
MemberRepositoryInterface $memberRepo,
|
||||
SettingsService $settingsService,
|
||||
) {
|
||||
$this->member = $memberRepo;
|
||||
$this->settings = $settingsService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = [];
|
||||
//checks to see if request is secure
|
||||
if (isHttps()) {
|
||||
$key = $request->header('fipamo-api-key');
|
||||
$folks = $this->member->getAll();
|
||||
//looks to see if API key exists
|
||||
if (find($folks, ['key' => $key])) {
|
||||
//final check to see if API requests are being accepted
|
||||
$global = $this->settings->getGlobal();
|
||||
if (isset($global['externalAPI']) && $global['externalAPI'] == "true") {
|
||||
return $next($request);
|
||||
} else {
|
||||
$response = [
|
||||
'message' => "API Auth Fail: Not Accepting Requests",
|
||||
'type' => 'postError',
|
||||
];
|
||||
return response()->json($response)->header('Content-Type', 'application/json');
|
||||
}
|
||||
} else {
|
||||
$response = [
|
||||
'message' => "API Auth Fail: API Key Invalid",
|
||||
'type' => 'postError',
|
||||
];
|
||||
return response()->json($response)->header('Content-Type', 'application/json');
|
||||
}
|
||||
} else {
|
||||
$response = [
|
||||
'message' => "API Auth Fail: Request must be secure (HTTPS)",
|
||||
'type' => 'postError',
|
||||
];
|
||||
return response()->json($response)->header('Content-Type', 'application/json');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ValidateAPIToken
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$token = $request->header('fipamo-access-token');
|
||||
if ($token == session('token')) {
|
||||
return $next($request);
|
||||
} else {
|
||||
$response = [
|
||||
'message' => "API Auth Fail",
|
||||
'type' => 'postError',
|
||||
];
|
||||
return response()->json($response)->header('Content-Type', 'application/json');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,7 +27,7 @@ class MemberRepository implements MemberRepositoryInterface
|
|||
|
||||
public function getAll()
|
||||
{
|
||||
return $this->$folks;
|
||||
return $this->folks;
|
||||
}
|
||||
|
||||
public function getById($id)
|
||||
|
|
|
@ -1,294 +0,0 @@
|
|||
//** REQUEST TYPES **//
|
||||
export const REQUEST_TYPE_POST = "POST";
|
||||
export const REQUEST_TYPE_GET = "GET";
|
||||
export const REQUEST_TYPE_PUT = "PUT";
|
||||
export const REQUEST_TYPE_DELETE = "DELETE";
|
||||
|
||||
//** POST CONTENT TYPES **//
|
||||
export const CONTENT_TYPE_JSON = "json";
|
||||
export const CONTENT_TYPE_FORM = "x-www-form-urlencoded";
|
||||
|
||||
//** API URLS **//
|
||||
export const API_GET_PAGES = "/api/v1/page/published";
|
||||
export const API_GET_FEATURED = "/api/v1/page/featured";
|
||||
export const API_GET_PAGE = "/api/v1/page/single";
|
||||
export const API_GET_MENU = "/api/v1/page/menu";
|
||||
export const API_GET_TAGS = "/api/v1/page/tags";
|
||||
|
||||
//** API TASKS **//
|
||||
export const TASK_GET_CONTENT = "retrieveContent";
|
||||
|
||||
/**
|
||||
* A bag of methods for getting content from an install.
|
||||
*/
|
||||
class FipamoContentAPI {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {string} baseURL - url of install, defaults to local
|
||||
* @param {string} key - user api key found in Settings
|
||||
* @author Ro
|
||||
*/
|
||||
constructor(baseURL = null, key = null) {
|
||||
this.baseURL = null;
|
||||
this.key = null;
|
||||
if (key) this.key = key;
|
||||
if (baseURL) this.baseURL = baseURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* *Promise method for retrieving page data*\
|
||||
* **GET**`/api/v1/page/:type`
|
||||
* @param {string} type - type of pages (`published | menu | featured`) being retrieved; null value defaults to `published`
|
||||
* @example
|
||||
* api.pages('published').then(pages=>{
|
||||
* console.log("Pages Object", pages);
|
||||
* })
|
||||
* @returns {object} json object that contains pages of requested type
|
||||
*
|
||||
* *pages object example*
|
||||
* ```
|
||||
{
|
||||
"pages":
|
||||
[
|
||||
{
|
||||
"id":1,
|
||||
"uuid":"uuid-for-entry",
|
||||
"title":"Entry Title",
|
||||
"feature":"/path/to/image.jpg",
|
||||
"path":"2020/09",
|
||||
"layout":"page",
|
||||
"tags":"these, are, tags",
|
||||
"author":"your-name",
|
||||
"created":"2020 Sep Tue 01",
|
||||
"updated":"2020 Sep Tue 01",
|
||||
"deleted":false,
|
||||
"menu":false,
|
||||
"featured":false,
|
||||
"published":true,
|
||||
"slug":"entry-title",
|
||||
"content":"Premium Content"
|
||||
},
|
||||
{
|
||||
"id":2,
|
||||
"uuid":"uuid-for-entry",
|
||||
"title":"Another Title",
|
||||
"feature":"/path/to/image.jpg",
|
||||
"path":"2020/09",
|
||||
"layout":"page",
|
||||
"tags":"these, are, tags",
|
||||
"author":"your-name",
|
||||
"created":"2020 Sep Tue 01",
|
||||
"updated":"2020 Sep Tue 01",
|
||||
"deleted":false,
|
||||
"menu":false,
|
||||
"featured":false,
|
||||
"published":true,
|
||||
"slug":"another-title",
|
||||
"content":"Premium Content"
|
||||
}
|
||||
],
|
||||
"totalItems":2
|
||||
}
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
pages(type = null) {
|
||||
//set url based on request type
|
||||
let requestURL = "";
|
||||
switch (type) {
|
||||
default:
|
||||
case "published":
|
||||
requestURL = API_GET_PAGES + "?key=" + this.key;
|
||||
break;
|
||||
case "featured":
|
||||
requestURL = API_GET_FEATURED + "?key=" + this.key;
|
||||
break;
|
||||
case "menu":
|
||||
requestURL = API_GET_MENU + "?key=" + this.key;
|
||||
break;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this._request(
|
||||
this.baseURL ? this.baseURL + requestURL : requestURL,
|
||||
TASK_GET_CONTENT
|
||||
)
|
||||
.then((result) => {
|
||||
resolve(result);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* *Promise method for retrieving single page*\
|
||||
* **GET** `/api/v1/page/single/:id`
|
||||
* @param {string} id - uuid of desired page
|
||||
* @example
|
||||
* api.page("a-uuid-for-a-page").then(page=>{
|
||||
console.log("Page Object", page);
|
||||
* })
|
||||
* @returns {object} json object that contains data for requested page
|
||||
*
|
||||
* *page object example*
|
||||
* ```
|
||||
{
|
||||
"id":1,
|
||||
"uuid":"uuid-for-entry",
|
||||
"title":"Entry Title",
|
||||
"feature":"/path/to/image.jpg",
|
||||
"path":"2020/09",
|
||||
"layout":"page",
|
||||
"tags":"these, are, tags",
|
||||
"author":"your-name",
|
||||
"created":"2020 Sep Tue 01",
|
||||
"updated":"2020 Sep Tue 01",
|
||||
"deleted":false,
|
||||
"menu":false,
|
||||
"featured":false,
|
||||
"published":true,
|
||||
"slug":"entry-title",
|
||||
"content":"Premium Content"
|
||||
}
|
||||
* ```
|
||||
*/
|
||||
page(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._request(
|
||||
this.baseURL
|
||||
? this.baseURL + API_GET_PAGE + "/" + id + "?key=" + this.key
|
||||
: API_GET_PAGE + "/" + id + "?key=" + this.key,
|
||||
TASK_GET_CONTENT,
|
||||
REQUEST_TYPE_GET
|
||||
)
|
||||
.then((result) => {
|
||||
resolve(result);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* *Promise method for retrieving all tags used by pages*\
|
||||
* **GET** `/api/v1/page/tags`
|
||||
* @example
|
||||
* api.tags().then(tags=>{
|
||||
console.log("Tags Object", tags);
|
||||
* })
|
||||
* @returns {object} json object that contains site tags and page stubs associated with said tag
|
||||
*
|
||||
* *tags object example*
|
||||
* ```
|
||||
[
|
||||
{
|
||||
"tag_name":"this is a tag",
|
||||
"slug":"this-is-a-tag",
|
||||
"pages":
|
||||
[
|
||||
{
|
||||
"title":"This is a title",
|
||||
"slug":"this-is-a-title",
|
||||
"path":"2021/04"
|
||||
},
|
||||
|
||||
{
|
||||
"title":"This is another title",
|
||||
"slug":"this-is-another-title",
|
||||
"path":"2020/10"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag_name":"this is another tag",
|
||||
"slug":"this-is-another-tag",
|
||||
"pages":
|
||||
[
|
||||
{
|
||||
"title":"This is a title",
|
||||
"slug":"this-is-a-title",
|
||||
"path":"2021/04"
|
||||
},
|
||||
|
||||
{
|
||||
"title":"This is another title",
|
||||
"slug":"this-is-another-title",
|
||||
"path":"2020/10"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
* ```
|
||||
*/
|
||||
tags() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._request(
|
||||
this.baseURL
|
||||
? this.baseURL + API_GET_TAGS + "?key=" + this.key
|
||||
: API_GET_TAGS + "?key=" + this.key,
|
||||
TASK_GET_CONTENT,
|
||||
REQUEST_TYPE_GET
|
||||
)
|
||||
.then((result) => {
|
||||
resolve(result);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//--------------------------
|
||||
// private
|
||||
//--------------------------
|
||||
_request(
|
||||
requestURL,
|
||||
eventType,
|
||||
requestType = REQUEST_TYPE_GET,
|
||||
contentType = CONTENT_TYPE_JSON,
|
||||
requestData = null
|
||||
) {
|
||||
var self = this;
|
||||
return new Promise(function (resolve, reject) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.upload.onprogress = self.handleLoadProgress;
|
||||
request.open(requestType, requestURL, true);
|
||||
request.onload = () => {
|
||||
if (request.status == 200) {
|
||||
let response = JSON.parse(request["response"]);
|
||||
resolve(response);
|
||||
} else {
|
||||
let error = JSON.parse(request["response"]);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
if (requestType == REQUEST_TYPE_PUT || requestType == REQUEST_TYPE_POST) {
|
||||
switch (contentType) {
|
||||
case CONTENT_TYPE_JSON:
|
||||
request.setRequestHeader(
|
||||
"Content-type",
|
||||
"application/" + contentType
|
||||
);
|
||||
request.send(JSON.stringify(requestData));
|
||||
break;
|
||||
case CONTENT_TYPE_FORM:
|
||||
request.send(requestData);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
request.send();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//--------------------------
|
||||
// event handlers
|
||||
//--------------------------
|
||||
handleLoadProgress(e) {
|
||||
this.percentComplete = Math.ceil((e.loaded / e.total) * 100);
|
||||
//pass element to display request progress
|
||||
}
|
||||
}
|
||||
|
||||
export { FipamoContentAPI as default };
|
|
@ -23,27 +23,27 @@ Route::post("/v1/init", [InitAPIController::class, 'setupFresh']);
|
|||
Route::post("/v1/restore", [InitAPIController::class, 'setupRestore']);
|
||||
|
||||
//handle page editing actions
|
||||
Route::group(['prefix' => '/v1/page', 'middleware' => 'validate.token'], function () {
|
||||
Route::group(['prefix' => '/v1/page', 'middleware' => 'validate.key'], function () {
|
||||
Route::put("/write", [PageAPIController::class, 'write']);
|
||||
Route::post("/create", [PageAPIController::class, 'create']);
|
||||
Route::delete("/delete", [PageAPIController::class, 'delete']);
|
||||
});
|
||||
|
||||
//settings
|
||||
Route::group(['prefix' => '/v1/settings', 'middleware' => 'validate.token'], function () {
|
||||
Route::group(['prefix' => '/v1/settings', 'middleware' => 'validate.key'], function () {
|
||||
Route::put("/publish", [SettingsAPIController::class, 'publish']);
|
||||
Route::put("/sync", [SettingsAPIController::class, 'sync']);
|
||||
Route::put("/nav-sync", [SettingsAPIController::class, 'navSync']);
|
||||
});
|
||||
//backups
|
||||
Route::group(['prefix' => '/v1/backup', 'middleware' => 'validate.token'], function () {
|
||||
Route::group(['prefix' => '/v1/backup', 'middleware' => 'validate.key'], function () {
|
||||
Route::put("/create", [SettingsAPIController::class, 'createBackup']);
|
||||
Route::get("/content-download", [SettingsAPIController::class, 'downloadBackup']);
|
||||
Route::get("/files-download", [SettingsAPIController::class, 'downloadBackup']);
|
||||
});
|
||||
|
||||
//other
|
||||
Route::group(['prefix' => '/v1', 'middleware' => 'validate.token'], function () {
|
||||
Route::group(['prefix' => '/v1', 'middleware' => 'validate.key'], function () {
|
||||
Route::post("/files", [FileUploadAPIController::class, 'upload']);
|
||||
Route::post("/reset", [InitAPIController::class, 'setupReset']);
|
||||
Route::post("/mailer", [MailAPIController::class, 'sendNotify']);
|
||||
|
|
Loading…
Reference in a new issue