diff --git a/api/.env.example b/api/.env.example index dbd9974..10a3d48 100644 --- a/api/.env.example +++ b/api/.env.example @@ -2,4 +2,7 @@ DATABASES_HOST=psql.domain.tld DATABASES_NAME=db DATABASES_USER=user -DATABASES_PASS=1234 \ No newline at end of file +DATABASES_PASS=1234 + +## CORS Einstellungen +CORS_ALLOW_ORIGIN_REGEX=/https?:\/\/localhost:\d+/ \ No newline at end of file diff --git a/api/html/index.php b/api/html/index.php index d9a872c..cbd9aca 100644 --- a/api/html/index.php +++ b/api/html/index.php @@ -7,10 +7,12 @@ $dotenv->load(); use DI\Container; use Slim\Factory\AppFactory; use Slim\Routing\RouteCollectorProxy; +use TorstenHettstedt\TimekeepingApi\Controller\PreflightController; use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursController; use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursViewController; use TorstenHettstedt\TimekeepingApi\Middleware\ErrorHandler; use TorstenHettstedt\TimekeepingApi\Middleware\JsonBodyParserMiddleware; +use TorstenHettstedt\TimekeepingApi\Middleware\OriginAccessControlHandler; $container = new Container(); @@ -22,17 +24,25 @@ $container->set('databases', function () { AppFactory::setContainer($container); $app = AppFactory::create(); $app->add(new JsonBodyParserMiddleware()); +$app->addBodyParsingMiddleware(); +$app->add(new OriginAccessControlHandler()); +$app->addRoutingMiddleware(); $app->group('/views/working-hours', function (RouteCollectorProxy $group) { + $group->options('/weekly', PreflightController::class . ':preflight'); $group->get('/weekly', WorkingHoursViewController::class . ':browseWeekly'); + $group->options('/monthly', PreflightController::class . ':preflight'); $group->get('/monthly', WorkingHoursViewController::class . ':browseMonthly'); + $group->options('/yearly', PreflightController::class . ':preflight'); $group->get('/yearly', WorkingHoursViewController::class . ':browseYearly'); }); $app->group('/working-hours', function (RouteCollectorProxy $group) { + $group->options('', PreflightController::class . ':preflight'); $group->get('', WorkingHoursController::class . ':browse'); $group->post('', WorkingHoursController::class . ':creat'); $group->group('/{id:\d\d\d\d-\d\d-\d\d}', function (RouteCollectorProxy $group) { + $group->options('', PreflightController::class . ':preflight'); $group->get('', WorkingHoursController::class . ':read'); $group->put('', WorkingHoursController::class . ':update'); }); diff --git a/api/src/Controller/PreflightController.php b/api/src/Controller/PreflightController.php new file mode 100644 index 0000000..1ac6bf9 --- /dev/null +++ b/api/src/Controller/PreflightController.php @@ -0,0 +1,18 @@ +originAccessControl($request, $response); + } +} \ No newline at end of file diff --git a/api/src/Middleware/ErrorHandler.php b/api/src/Middleware/ErrorHandler.php index 9693206..80d5f15 100644 --- a/api/src/Middleware/ErrorHandler.php +++ b/api/src/Middleware/ErrorHandler.php @@ -84,7 +84,9 @@ class ErrorHandler json_encode($payload, JSON_UNESCAPED_UNICODE) ); - return $response; + $originAccessControlHandler = new OriginAccessControlHandler(); + + return $originAccessControlHandler->originAccessControl($request, $response); } } \ No newline at end of file diff --git a/api/src/Middleware/OriginAccessControlHandler.php b/api/src/Middleware/OriginAccessControlHandler.php new file mode 100644 index 0000000..a62de79 --- /dev/null +++ b/api/src/Middleware/OriginAccessControlHandler.php @@ -0,0 +1,58 @@ +handle($request); + return $this->originAccessControl($request, $response); + } + + public function originAccessControl(Request $request, Response $response): Response + { + + $routeContext = RouteContext::fromRequest($request); + $routingResults = $routeContext->getRoutingResults(); + $methods = $routingResults->getAllowedMethods(); + $requestHeaders = $request->getHeaderLine('Access-Control-Request-Headers'); + + if ($this->testRoute($request)) { + // Stunden Minuten Sekunden + $accessControlMaxAge = 24 * 60 * 60; + $response = $response->withHeader('Access-Control-Allow-Origin', $this->buildOrigin($request)); + $response = $response->withHeader('Access-Control-Allow-Methods', implode(',', $methods)); + $response = $response->withHeader('Access-Control-Allow-Headers', $requestHeaders); + $response = $response->withHeader('Access-Control-Max-Age', (string)$accessControlMaxAge); + } + return $response; + } + + protected function testRoute(Request $request): bool + { + $corsAllowOriginRegex = $_ENV['CORS_ALLOW_ORIGIN_REGEX'] ?? '/https?:\/\/.*:.*/'; + return preg_match($corsAllowOriginRegex, $this->buildOrigin($request)); + } + + protected function buildOrigin(Request $request): string + { + return $request->getHeaderLine('Origin') ?? ''; + } +} \ No newline at end of file diff --git a/api/tests/_support/Helper/Api.php b/api/tests/_support/Helper/Api.php index 3656476..677c203 100644 --- a/api/tests/_support/Helper/Api.php +++ b/api/tests/_support/Helper/Api.php @@ -32,4 +32,6 @@ class Api extends Module "message" => "string", "path" => "string", ]; + + const TEST_ORIGIN = 'http://localhost:1234'; } diff --git a/api/tests/api/Views/BrowseWorkingHoursMonthlyCest.php b/api/tests/api/Views/BrowseWorkingHoursMonthlyCest.php index 5614233..4208663 100644 --- a/api/tests/api/Views/BrowseWorkingHoursMonthlyCest.php +++ b/api/tests/api/Views/BrowseWorkingHoursMonthlyCest.php @@ -8,13 +8,27 @@ use Helper\Api; class BrowseWorkingHoursMonthlyCest { + public static function preflight(ApiTester $I): void + { + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); + $I->haveHttpHeader('Access-Control-Request-Method', 'GET'); + $I->sendOptions('/views/working-hours/monthly'); + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); + $I->seeHttpHeader('Access-Control-Allow-Methods'); + $I->seeHttpHeader('Access-Control-Allow-Headers'); + $I->seeHttpHeader('Access-Control-Max-Age'); + } + /** * @param ApiTester $I */ public function browseWorkingHoursView(ApiTester $I): void { + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); $I->sendGet('/views/working-hours/monthly'); $I->seeResponseCodeIs(HttpCode::OK); + $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); $I->seeResponseIsJson(); $I->seeResponseMatchesJsonType(Api::WORKING_HOURS_VIEW_JSON_FORMAT); } diff --git a/api/tests/api/Views/BrowseWorkingHoursWeeklyCest.php b/api/tests/api/Views/BrowseWorkingHoursWeeklyCest.php index 6a751c5..2ba0c72 100644 --- a/api/tests/api/Views/BrowseWorkingHoursWeeklyCest.php +++ b/api/tests/api/Views/BrowseWorkingHoursWeeklyCest.php @@ -8,13 +8,27 @@ use Helper\Api; class BrowseWorkingHoursWeeklyCest { + public static function preflight(ApiTester $I): void + { + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); + $I->haveHttpHeader('Access-Control-Request-Method', 'GET'); + $I->sendOptions('/views/working-hours/weekly'); + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); + $I->seeHttpHeader('Access-Control-Allow-Methods'); + $I->seeHttpHeader('Access-Control-Allow-Headers'); + $I->seeHttpHeader('Access-Control-Max-Age'); + } + /** * @param ApiTester $I */ public function browseWorkingHoursView(ApiTester $I): void { + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); $I->sendGet('/views/working-hours/weekly'); $I->seeResponseCodeIs(HttpCode::OK); + $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); $I->seeResponseIsJson(); $I->seeResponseMatchesJsonType(Api::WORKING_HOURS_VIEW_JSON_FORMAT); } diff --git a/api/tests/api/Views/BrowseWorkingHoursYearlyCest.php b/api/tests/api/Views/BrowseWorkingHoursYearlyCest.php index 4a4c0c1..c42bfaa 100644 --- a/api/tests/api/Views/BrowseWorkingHoursYearlyCest.php +++ b/api/tests/api/Views/BrowseWorkingHoursYearlyCest.php @@ -8,13 +8,27 @@ use Helper\Api; class BrowseWorkingHoursYearlyCest { + public static function preflight(ApiTester $I): void + { + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); + $I->haveHttpHeader('Access-Control-Request-Method', 'GET'); + $I->sendOptions('/views/working-hours/yearly'); + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); + $I->seeHttpHeader('Access-Control-Allow-Methods'); + $I->seeHttpHeader('Access-Control-Allow-Headers'); + $I->seeHttpHeader('Access-Control-Max-Age'); + } + /** * @param ApiTester $I */ public function browseWorkingHoursView(ApiTester $I): void { + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); $I->sendGet('/views/working-hours/yearly'); $I->seeResponseCodeIs(HttpCode::OK); + $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); $I->seeResponseIsJson(); $I->seeResponseMatchesJsonType(Api::WORKING_HOURS_VIEW_JSON_FORMAT); } diff --git a/api/tests/api/WorkingHours/BrowseWorkingHoursCest.php b/api/tests/api/WorkingHours/BrowseWorkingHoursCest.php index bcfff03..65a5d1b 100644 --- a/api/tests/api/WorkingHours/BrowseWorkingHoursCest.php +++ b/api/tests/api/WorkingHours/BrowseWorkingHoursCest.php @@ -8,13 +8,27 @@ use Helper\Api; class BrowseWorkingHoursCest { + public static function preflight(ApiTester $I): void + { + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); + $I->haveHttpHeader('Access-Control-Request-Method', 'GET'); + $I->sendOptions('/working-hours'); + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); + $I->seeHttpHeader('Access-Control-Allow-Methods'); + $I->seeHttpHeader('Access-Control-Allow-Headers'); + $I->seeHttpHeader('Access-Control-Max-Age'); + } + /** * @param ApiTester $I */ public function browseWorkingHours(ApiTester $I): void { + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); $I->sendGet('/working-hours'); $I->seeResponseCodeIs(HttpCode::OK); + $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); $I->seeResponseIsJson(); $I->seeResponseMatchesJsonType(Api::WORKING_HOURS_JSON_FORMAT); } diff --git a/api/tests/api/WorkingHours/CreateWorkingHoursCest.php b/api/tests/api/WorkingHours/CreateWorkingHoursCest.php index 725b895..c2707bc 100644 --- a/api/tests/api/WorkingHours/CreateWorkingHoursCest.php +++ b/api/tests/api/WorkingHours/CreateWorkingHoursCest.php @@ -8,15 +8,29 @@ use Helper\Api; class CreateWorkingHoursCest { + public static function preflight(ApiTester $I): void + { + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); + $I->haveHttpHeader('Access-Control-Request-Method', 'POST'); + $I->sendOptions('/working-hours'); + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); + $I->seeHttpHeader('Access-Control-Allow-Methods'); + $I->seeHttpHeader('Access-Control-Allow-Headers'); + $I->seeHttpHeader('Access-Control-Max-Age'); + } + public function createWorkingHoursWithNewValidRecord(ApiTester $I): void { $I->haveHttpHeader('accept', 'application/json'); $I->haveHttpHeader('content-type', 'application/json'); + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); $I->sendPost('/working-hours', [ 'workingDay' => '2020-04-01', 'workingTime' => '08:00:00', ]); $I->seeResponseCodeIs(HttpCode::CREATED); + $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); $I->seeResponseIsJson(); $I->seeResponseMatchesJsonType(Api::WORKING_HOURS_JSON_FORMAT); } @@ -26,11 +40,13 @@ class CreateWorkingHoursCest { $I->haveHttpHeader('accept', 'application/json'); $I->haveHttpHeader('content-type', 'application/json'); + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); $I->sendPost('/working-hours', [ 'workingDay' => '2020-04-01', 'workingTime' => 8.0, ]); $I->seeResponseCodeIs(HttpCode::BAD_REQUEST); + $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); $I->seeResponseIsJson(); $I->seeResponseMatchesJsonType(Api::ERROR_JSON_FORMAT); } @@ -40,11 +56,13 @@ class CreateWorkingHoursCest { $I->haveHttpHeader('accept', 'application/json'); $I->haveHttpHeader('content-type', 'application/json'); + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); $I->sendPost('/working-hours', [ 'workingDay' => '2020-01-15', 'workingTime' => '08:00:00', ]); $I->seeResponseCodeIs(HttpCode::CONFLICT); + $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); $I->seeResponseIsJson(); $I->seeResponseMatchesJsonType(Api::ERROR_JSON_FORMAT); } diff --git a/api/tests/api/WorkingHours/ReadWorkingHoursCest.php b/api/tests/api/WorkingHours/ReadWorkingHoursCest.php index af212be..2f2eafb 100644 --- a/api/tests/api/WorkingHours/ReadWorkingHoursCest.php +++ b/api/tests/api/WorkingHours/ReadWorkingHoursCest.php @@ -8,18 +8,34 @@ use Helper\Api; class ReadWorkingHoursCest { + public static function preflight(ApiTester $I): void + { + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); + $I->haveHttpHeader('Access-Control-Request-Method', 'GET'); + $I->sendOptions('/working-hours/2020-04-15'); + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); + $I->seeHttpHeader('Access-Control-Allow-Methods'); + $I->seeHttpHeader('Access-Control-Allow-Headers'); + $I->seeHttpHeader('Access-Control-Max-Age'); + } + public function readExistingWorkingHours(ApiTester $I): void { + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); $I->sendGet('/working-hours/2020-01-15'); $I->seeResponseCodeIs(HttpCode::OK); + $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); $I->seeResponseIsJson(); $I->seeResponseMatchesJsonType(Api::WORKING_HOURS_JSON_FORMAT); } public function readNotExistingWorkingHours(ApiTester $I): void { + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); $I->sendGet('/working-hours/2020-04-15'); $I->seeResponseCodeIs(HttpCode::NOT_FOUND); + $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); $I->seeResponseIsJson(); $I->seeResponseMatchesJsonType(Api::ERROR_JSON_FORMAT); } diff --git a/api/tests/api/WorkingHours/UpdateWorkingHoursCest.php b/api/tests/api/WorkingHours/UpdateWorkingHoursCest.php index a4e433b..176eb88 100644 --- a/api/tests/api/WorkingHours/UpdateWorkingHoursCest.php +++ b/api/tests/api/WorkingHours/UpdateWorkingHoursCest.php @@ -8,15 +8,29 @@ use Helper\Api; class UpdateWorkingHoursCest { + public static function preflight(ApiTester $I): void + { + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); + $I->haveHttpHeader('Access-Control-Request-Method', 'PUT'); + $I->sendOptions('/working-hours/2020-01-15'); + $I->seeResponseCodeIs(HttpCode::OK); + $I->seeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); + $I->seeHttpHeader('Access-Control-Allow-Methods'); + $I->seeHttpHeader('Access-Control-Allow-Headers'); + $I->seeHttpHeader('Access-Control-Max-Age'); + } + public function updateWorkingHoursWithExistingValidRecord(ApiTester $I): void { $I->haveHttpHeader('accept', 'application/json'); $I->haveHttpHeader('content-type', 'application/json'); + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); $I->sendPut('/working-hours/2020-01-15', [ 'workingDay' => '2020-01-15', 'workingTime' => '10:00:00', ]); $I->seeResponseCodeIs(HttpCode::OK); + $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); $I->seeResponseIsJson(); $I->seeResponseMatchesJsonType(Api::WORKING_HOURS_JSON_FORMAT); } @@ -25,11 +39,13 @@ class UpdateWorkingHoursCest { $I->haveHttpHeader('accept', 'application/json'); $I->haveHttpHeader('content-type', 'application/json'); + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); $I->sendPut('/working-hours/2020-04-15', [ 'workingDay' => '2020-04-15', 'workingTime' => '10:00:00', ]); $I->seeResponseCodeIs(HttpCode::NOT_FOUND); + $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); $I->seeResponseIsJson(); $I->seeResponseMatchesJsonType(Api::ERROR_JSON_FORMAT); } @@ -38,11 +54,13 @@ class UpdateWorkingHoursCest { $I->haveHttpHeader('accept', 'application/json'); $I->haveHttpHeader('content-type', 'application/json'); + $I->haveHttpHeader('Origin', Api::TEST_ORIGIN); $I->sendPut('/working-hours/2020-01-15', [ 'workingDay' => '2020-01-15', 'workingTime' => 8.0, ]); $I->seeResponseCodeIs(HttpCode::BAD_REQUEST); + $I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN); $I->seeResponseIsJson(); $I->seeResponseMatchesJsonType(Api::ERROR_JSON_FORMAT); }