API: Init with news endpoint (ro)
This commit is contained in:
parent
f826cee63c
commit
8adad075bf
|
@ -44,12 +44,13 @@
|
||||||
"illuminate/database": "^10.23",
|
"illuminate/database": "^10.23",
|
||||||
"illuminate/support": "^10.23",
|
"illuminate/support": "^10.23",
|
||||||
"league/oauth2-client": "^2.7",
|
"league/oauth2-client": "^2.7",
|
||||||
|
"league/openapi-psr7-validator": "^0.21",
|
||||||
"nikic/fast-route": "^1.3",
|
"nikic/fast-route": "^1.3",
|
||||||
"nyholm/psr7": "^1.8",
|
"nyholm/psr7": "^1.8",
|
||||||
"psr/container": "^2.0",
|
"psr/container": "^2.0",
|
||||||
|
"psr/http-message": "^1.1",
|
||||||
"psr/http-server-middleware": "^1.0",
|
"psr/http-server-middleware": "^1.0",
|
||||||
"psr/log": "^3.0",
|
"psr/log": "^3.0",
|
||||||
"psr/http-message": "^1.1",
|
|
||||||
"rcrowe/twigbridge": "^0.14.0",
|
"rcrowe/twigbridge": "^0.14.0",
|
||||||
"respect/validation": "^1.1",
|
"respect/validation": "^1.1",
|
||||||
"symfony/http-foundation": "^6.3",
|
"symfony/http-foundation": "^6.3",
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -108,7 +108,29 @@ $route->addGroup(
|
||||||
);
|
);
|
||||||
|
|
||||||
// API
|
// 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
|
// Feeds
|
||||||
$route->get('/atom', 'FeedController@atom');
|
$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', $appPath);
|
||||||
$this->instance('path.config', $appPath . DIRECTORY_SEPARATOR . 'config');
|
$this->instance('path.config', $appPath . DIRECTORY_SEPARATOR . 'config');
|
||||||
$this->instance('path.resources', $appPath . DIRECTORY_SEPARATOR . 'resources');
|
$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.public', $appPath . DIRECTORY_SEPARATOR . 'public');
|
||||||
$this->instance('path.assets', $this->get('path.resources') . DIRECTORY_SEPARATOR . 'assets');
|
$this->instance('path.assets', $this->get('path.resources') . DIRECTORY_SEPARATOR . 'assets');
|
||||||
$this->instance('path.assets.public', $this->get('path.public') . 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;
|
namespace Engelsystem\Controllers;
|
||||||
|
|
||||||
use Engelsystem\Http\Response;
|
use Engelsystem\Http\Response;
|
||||||
|
use Engelsystem\Models\News;
|
||||||
|
|
||||||
class ApiController extends BaseController
|
class ApiController extends BaseController
|
||||||
{
|
{
|
||||||
|
public array $permissions = [
|
||||||
|
'api',
|
||||||
|
];
|
||||||
|
|
||||||
public function __construct(protected Response $response)
|
public function __construct(protected Response $response)
|
||||||
{
|
{
|
||||||
|
$this->response = $this->response
|
||||||
|
->withHeader('content-type', 'application/json')
|
||||||
|
->withHeader('Access-Control-Allow-Origin', '*');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index(): Response
|
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
|
return $this->response
|
||||||
->setStatusCode(501)
|
->setStatusCode(501)
|
||||||
->withHeader('content-type', 'application/json')
|
|
||||||
->withContent(json_encode(['error' => 'Not implemented']));
|
->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;
|
namespace Engelsystem\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\MiddlewareInterface;
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
@ -13,14 +14,18 @@ use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface;
|
||||||
|
|
||||||
class SessionHandler implements MiddlewareInterface
|
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
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
{
|
{
|
||||||
$requestPath = $request->getAttribute('route-request-path');
|
$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);
|
$request = $request->withAttribute('route-api', $isApi);
|
||||||
|
|
||||||
$return = $handler->handle($request);
|
$return = $handler->handle($request);
|
||||||
|
|
|
@ -15,7 +15,6 @@ class SessionHandlerServiceProvider extends ServiceProvider
|
||||||
->needs('$paths')
|
->needs('$paths')
|
||||||
->give(function () {
|
->give(function () {
|
||||||
return [
|
return [
|
||||||
'/api',
|
|
||||||
'/atom',
|
'/atom',
|
||||||
'/rss',
|
'/rss',
|
||||||
'/health',
|
'/health',
|
||||||
|
@ -25,5 +24,9 @@ class SessionHandlerServiceProvider extends ServiceProvider
|
||||||
'/stats',
|
'/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.config'));
|
||||||
$this->assertTrue($app->has('path.lang'));
|
$this->assertTrue($app->has('path.lang'));
|
||||||
$this->assertTrue($app->has('path.resources'));
|
$this->assertTrue($app->has('path.resources'));
|
||||||
|
$this->assertTrue($app->has('path.resources.api'));
|
||||||
$this->assertTrue($app->has('path.views'));
|
$this->assertTrue($app->has('path.views'));
|
||||||
$this->assertTrue($app->has('path.storage'));
|
$this->assertTrue($app->has('path.storage'));
|
||||||
$this->assertTrue($app->has('path.cache'));
|
$this->assertTrue($app->has('path.cache'));
|
||||||
|
|
|
@ -6,10 +6,15 @@ namespace Engelsystem\Test\Unit\Controllers;
|
||||||
|
|
||||||
use Engelsystem\Controllers\ApiController;
|
use Engelsystem\Controllers\ApiController;
|
||||||
use Engelsystem\Http\Response;
|
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::__construct
|
||||||
* @covers \Engelsystem\Controllers\ApiController::index
|
* @covers \Engelsystem\Controllers\ApiController::index
|
||||||
|
@ -20,8 +25,89 @@ class ApiControllerTest extends TestCase
|
||||||
|
|
||||||
$response = $controller->index();
|
$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(501, $response->getStatusCode());
|
||||||
$this->assertEquals(['application/json'], $response->getHeader('content-type'));
|
$this->assertEquals(['application/json'], $response->getHeader('content-type'));
|
||||||
$this->assertJson($response->getContent());
|
$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