API: Init with news endpoint (ro)
This commit is contained in:
parent
f826cee63c
commit
8adad075bf
|
@ -44,12 +44,13 @@
|
|||
"illuminate/database": "^10.23",
|
||||
"illuminate/support": "^10.23",
|
||||
"league/oauth2-client": "^2.7",
|
||||
"league/openapi-psr7-validator": "^0.21",
|
||||
"nikic/fast-route": "^1.3",
|
||||
"nyholm/psr7": "^1.8",
|
||||
"psr/container": "^2.0",
|
||||
"psr/http-message": "^1.1",
|
||||
"psr/http-server-middleware": "^1.0",
|
||||
"psr/log": "^3.0",
|
||||
"psr/http-message": "^1.1",
|
||||
"rcrowe/twigbridge": "^0.14.0",
|
||||
"respect/validation": "^1.1",
|
||||
"symfony/http-foundation": "^6.3",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -108,7 +108,29 @@ $route->addGroup(
|
|||
);
|
||||
|
||||
// API
|
||||
$route->get('/api[/{resource:.+}]', 'ApiController@index');
|
||||
$route->addGroup(
|
||||
'/api',
|
||||
function (RouteCollector $route): void {
|
||||
$route->get('/', 'ApiController@index');
|
||||
|
||||
$route->addGroup(
|
||||
'/v0-beta',
|
||||
function (RouteCollector $route): void {
|
||||
$route->addRoute(['OPTIONS'], '[/{resource:.+}]', 'ApiController@options');
|
||||
$route->get('/', 'ApiController@indexV0');
|
||||
|
||||
$route->get('/news', 'ApiController@news');
|
||||
|
||||
$route->addRoute(
|
||||
['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'],
|
||||
'[/{resource:.+}]',
|
||||
'ApiController@notImplemented'
|
||||
);
|
||||
}
|
||||
);
|
||||
$route->get('[/{resource:.+}]', 'ApiController@notImplemented');
|
||||
}
|
||||
);
|
||||
|
||||
// Feeds
|
||||
$route->get('/atom', 'FeedController@atom');
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Engelsystem\Migrations;
|
||||
|
||||
use Engelsystem\Database\Migration\Migration;
|
||||
|
||||
class CreateApiPermissions extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$db = $this->schema->getConnection();
|
||||
$db->table('privileges')->insert([
|
||||
['name' => 'api', 'description' => 'Use the API'],
|
||||
]);
|
||||
$db->table('groups')->insert([
|
||||
['id' => 40, 'name' => 'API'],
|
||||
]);
|
||||
|
||||
$bureaucratGroup = 80;
|
||||
$apiId = $db->table('privileges')->where('name', 'api')->first()->id;
|
||||
$db->table('group_privileges')->insert([
|
||||
['group_id' => $bureaucratGroup, 'privilege_id' => $apiId],
|
||||
['group_id' => 40, 'privilege_id' => $apiId],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$db = $this->schema->getConnection();
|
||||
$db->table('privileges')
|
||||
->where('name', 'api')
|
||||
->delete();
|
||||
$db->table('groups')
|
||||
->where('id', 40)
|
||||
->delete();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
openapi: 3.0.3
|
||||
|
||||
info:
|
||||
version: 0.0.1-beta
|
||||
title: Engelsystem
|
||||
description: |
|
||||
This API description is as stable as a **beta** version might be.
|
||||
It could burst into flames and morph into a monster at any second!
|
||||
(But we try to keep it somewhat consistent, at least during events).
|
||||
contact:
|
||||
name: GitHub Issues
|
||||
url: https://github.com/engelsystem/engelsystem/issues
|
||||
license:
|
||||
name: GPL 2.0
|
||||
# identifier: GPL-2.0
|
||||
|
||||
servers:
|
||||
- url: /api/v0-beta
|
||||
description: This server
|
||||
- url: http://localhost:5080/api/v0-beta
|
||||
description: Your local dev instance
|
||||
|
||||
tags:
|
||||
- name: news
|
||||
description: News and Meeting announcements
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: API key from settings
|
||||
|
||||
responses:
|
||||
UnauthorizedError:
|
||||
description: Access token is missing or invalid
|
||||
ForbiddenError:
|
||||
description: The client is not allowed to acces
|
||||
|
||||
schemas:
|
||||
News:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
example: 42
|
||||
title:
|
||||
type: string
|
||||
example: First helper introduction
|
||||
text:
|
||||
type: string
|
||||
example: |
|
||||
The first introduction meeting takes place at the **Foo Hall** at 13:37.
|
||||
Please bring your own seating as it might take some time.
|
||||
is_meeting:
|
||||
type: boolean
|
||||
example: true
|
||||
description: Whether or not the news announces a meeting
|
||||
is_pinned:
|
||||
type: boolean
|
||||
example: false
|
||||
description: True if the news is pinned to the top
|
||||
is_highlighted:
|
||||
type: boolean
|
||||
example: false
|
||||
description: True if the news should be highlightet and shown on the dashboard
|
||||
created_at:
|
||||
type: string
|
||||
example: 2023-05-13T13:37:42.000000Z
|
||||
updated_at:
|
||||
type: string
|
||||
nullable: true
|
||||
example: 2023-05-13T23:00:00.000000Z
|
||||
|
||||
security:
|
||||
- bearerAuth: [ ]
|
||||
|
||||
paths:
|
||||
/news:
|
||||
get:
|
||||
tags:
|
||||
- news
|
||||
summary: Get a list of all news
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/News'
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
$ref: '#/components/responses/ForbiddenError'
|
|
@ -0,0 +1,6 @@
|
|||
# Engelsystem API documentation
|
||||
Here you can find the OpenAPI files that describe the Engelsystem API.
|
||||
Please be aware that the API is still in Beta and might change every second (but we try to keep it consistent during events ;))
|
||||
|
||||
## Links
|
||||
* [OpenAPI Spec](https://swagger.io/specification/)
|
|
@ -101,6 +101,7 @@ class Application extends Container
|
|||
$this->instance('path', $appPath);
|
||||
$this->instance('path.config', $appPath . DIRECTORY_SEPARATOR . 'config');
|
||||
$this->instance('path.resources', $appPath . DIRECTORY_SEPARATOR . 'resources');
|
||||
$this->instance('path.resources.api', $this->get('path.resources') . DIRECTORY_SEPARATOR . 'api');
|
||||
$this->instance('path.public', $appPath . DIRECTORY_SEPARATOR . 'public');
|
||||
$this->instance('path.assets', $this->get('path.resources') . DIRECTORY_SEPARATOR . 'assets');
|
||||
$this->instance('path.assets.public', $this->get('path.public') . DIRECTORY_SEPARATOR . 'assets');
|
||||
|
|
|
@ -5,18 +5,66 @@ declare(strict_types=1);
|
|||
namespace Engelsystem\Controllers;
|
||||
|
||||
use Engelsystem\Http\Response;
|
||||
use Engelsystem\Models\News;
|
||||
|
||||
class ApiController extends BaseController
|
||||
{
|
||||
public array $permissions = [
|
||||
'api',
|
||||
];
|
||||
|
||||
public function __construct(protected Response $response)
|
||||
{
|
||||
$this->response = $this->response
|
||||
->withHeader('content-type', 'application/json')
|
||||
->withHeader('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
|
||||
public function index(): Response
|
||||
{
|
||||
return $this->response
|
||||
->withContent(json_encode([
|
||||
'versions' => [
|
||||
'0.0.1-beta' => '/v0-beta',
|
||||
],
|
||||
]));
|
||||
}
|
||||
|
||||
public function indexV0(): Response
|
||||
{
|
||||
return $this->response
|
||||
->withContent(json_encode([
|
||||
'version' => '0.0.1-beta',
|
||||
'description' => 'Beta API, might break any second.',
|
||||
'paths' => [
|
||||
'/news',
|
||||
],
|
||||
]));
|
||||
}
|
||||
|
||||
public function options(): Response
|
||||
{
|
||||
return $this->response
|
||||
->setStatusCode(204)
|
||||
->withHeader('Allow', 'OPTIONS, HEAD, GET')
|
||||
->withHeader('Access-Control-Allow-Headers', 'Authorization');
|
||||
}
|
||||
|
||||
public function notImplemented(): Response
|
||||
{
|
||||
return $this->response
|
||||
->setStatusCode(501)
|
||||
->withHeader('content-type', 'application/json')
|
||||
->withContent(json_encode(['error' => 'Not implemented']));
|
||||
}
|
||||
|
||||
public function news(): Response
|
||||
{
|
||||
$news = News::query()
|
||||
->orderByDesc('updated_at')
|
||||
->orderByDesc('created_at')
|
||||
->get(['id', 'title', 'text', 'is_meeting', 'is_pinned', 'is_highlighted', 'created_at', 'updated_at']);
|
||||
|
||||
return $this->response
|
||||
->withContent(json_encode($news));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Engelsystem\Middleware;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
|
@ -13,14 +14,18 @@ use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface;
|
|||
|
||||
class SessionHandler implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(protected SessionStorageInterface $session, protected array $paths = [])
|
||||
{
|
||||
public function __construct(
|
||||
protected SessionStorageInterface $session,
|
||||
protected array $paths = [],
|
||||
protected ?string $apiPrefix = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$requestPath = $request->getAttribute('route-request-path');
|
||||
$isApi = in_array($requestPath, $this->paths);
|
||||
$isApi = in_array($requestPath, $this->paths)
|
||||
|| ($this->apiPrefix && Str::startsWith($requestPath, $this->apiPrefix));
|
||||
$request = $request->withAttribute('route-api', $isApi);
|
||||
|
||||
$return = $handler->handle($request);
|
||||
|
|
|
@ -15,7 +15,6 @@ class SessionHandlerServiceProvider extends ServiceProvider
|
|||
->needs('$paths')
|
||||
->give(function () {
|
||||
return [
|
||||
'/api',
|
||||
'/atom',
|
||||
'/rss',
|
||||
'/health',
|
||||
|
@ -25,5 +24,9 @@ class SessionHandlerServiceProvider extends ServiceProvider
|
|||
'/stats',
|
||||
];
|
||||
});
|
||||
$this->app
|
||||
->when(SessionHandler::class)
|
||||
->needs('$apiPrefix')
|
||||
->give('/api');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ class ApplicationTest extends TestCase
|
|||
$this->assertTrue($app->has('path.config'));
|
||||
$this->assertTrue($app->has('path.lang'));
|
||||
$this->assertTrue($app->has('path.resources'));
|
||||
$this->assertTrue($app->has('path.resources.api'));
|
||||
$this->assertTrue($app->has('path.views'));
|
||||
$this->assertTrue($app->has('path.storage'));
|
||||
$this->assertTrue($app->has('path.cache'));
|
||||
|
|
|
@ -6,10 +6,15 @@ namespace Engelsystem\Test\Unit\Controllers;
|
|||
|
||||
use Engelsystem\Controllers\ApiController;
|
||||
use Engelsystem\Http\Response;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Engelsystem\Models\News;
|
||||
use League\OpenAPIValidation\PSR7\OperationAddress as OpenApiAddress;
|
||||
use League\OpenAPIValidation\PSR7\ResponseValidator as OpenApiResponseValidator;
|
||||
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
|
||||
|
||||
class ApiControllerTest extends TestCase
|
||||
class ApiControllerTest extends ControllerTest
|
||||
{
|
||||
protected OpenApiResponseValidator $validator;
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\ApiController::__construct
|
||||
* @covers \Engelsystem\Controllers\ApiController::index
|
||||
|
@ -20,8 +25,89 @@ class ApiControllerTest extends TestCase
|
|||
|
||||
$response = $controller->index();
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(['application/json'], $response->getHeader('content-type'));
|
||||
$this->assertEquals(['*'], $response->getHeader('Access-Control-Allow-Origin'));
|
||||
$this->assertJson($response->getContent());
|
||||
|
||||
$data = json_decode($response->getContent(), true);
|
||||
$this->assertArrayHasKey('versions', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\ApiController::indexV0
|
||||
*/
|
||||
public function testIndexV0(): void
|
||||
{
|
||||
$controller = new ApiController(new Response());
|
||||
|
||||
$response = $controller->indexV0();
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertJson($response->getContent());
|
||||
|
||||
$data = json_decode($response->getContent(), true);
|
||||
$this->assertArrayHasKey('version', $data);
|
||||
$this->assertArrayHasKey('paths', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\ApiController::options
|
||||
*/
|
||||
public function testOptions(): void
|
||||
{
|
||||
$controller = new ApiController(new Response());
|
||||
|
||||
$response = $controller->options();
|
||||
|
||||
$this->assertEquals(204, $response->getStatusCode());
|
||||
$this->assertNotEmpty($response->getHeader('Allow'));
|
||||
$this->assertNotEmpty($response->getHeader('Access-Control-Allow-Headers'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\ApiController::notImplemented
|
||||
*/
|
||||
public function testNotImplemented(): void
|
||||
{
|
||||
$controller = new ApiController(new Response());
|
||||
|
||||
$response = $controller->notImplemented();
|
||||
|
||||
$this->assertEquals(501, $response->getStatusCode());
|
||||
$this->assertEquals(['application/json'], $response->getHeader('content-type'));
|
||||
$this->assertJson($response->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\ApiController::news
|
||||
*/
|
||||
public function testNews(): void
|
||||
{
|
||||
$this->initDatabase();
|
||||
News::factory(3)->create();
|
||||
|
||||
$controller = new ApiController(new Response());
|
||||
|
||||
$response = $controller->news();
|
||||
|
||||
$operation = new OpenApiAddress('/news', 'get');
|
||||
$this->validator->validate($operation, $response);
|
||||
|
||||
$this->assertEquals(['application/json'], $response->getHeader('content-type'));
|
||||
$this->assertJson($response->getContent());
|
||||
|
||||
$data = json_decode($response->getContent(), true);
|
||||
$this->assertCount(3, $data);
|
||||
}
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$openApiDefinition = $this->app->get('path.resources.api') . '/openapi.yml';
|
||||
$this->validator = (new ValidatorBuilder())
|
||||
->fromYamlFile($openApiDefinition)
|
||||
->getResponseValidator();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue