Merge branch 'master' into release

This commit is contained in:
2024-07-18 08:19:59 -04:00
10 changed files with 365 additions and 0 deletions

26
composer.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "provm/api",
"version": "1.0.0",
"type": "library",
"require": {
"psr/http-message": "*",
"slim/slim": "^4.0"
},
"require-dev": {
"phpunit/phpunit": "*"
},
"authors": [
{
"name": "Aldarien",
"email": "aldarien85@gmail.com"
}
],
"autoload": {
"psr-4": {
"ProVM\\": "src/"
}
},
"config": {
"sort-packages": true
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace ProVM\Concept\API;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
interface Controller
{
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface;
public function get(ServerRequestInterface $request, ResponseInterface $response, int $id): ResponseInterface;
public function getMany(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface;
public function add(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface;
public function edit(ServerRequestInterface $request, ResponseInterface $response, int $id): ResponseInterface;
public function editMany(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface;
public function delete(ServerRequestInterface $request, ResponseInterface $response, int $id): ResponseInterface;
public function deleteMany(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface;
}

View File

@ -0,0 +1,13 @@
<?php
namespace ProVM\Exception\Authorization;
use Throwable;
use Exception;
class MissingBearer extends Exception
{
public function __construct(Throwable $previous = null)
{
parent::__construct('Missing Bearer token', 401, $previous);
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace ProVM\Exception\Authorization;
use Throwable;
use Exception;
class MissingHeader extends Exception
{
public function __construct(Throwable $previous = null)
{
parent::__construct('No Authorization header found', 401, $previous);
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace ProVM\Exception\Authorization;
use Throwable;
use Exception;
class MissingToken extends Exception
{
public function __construct(string $placement, Throwable $previous)
{
parent::__construct("No token found in {$placement}", 401, $previous);
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace ProVM\Ideal\API;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use ProVM\Concept;
use ProVM\Reinforce\Controller\Json;
abstract class Controller implements Concept\API\Controller
{
use Json;
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$output = [
$this->getPlural() => $this->getService()->getAll()
];
return $this->withJson($response, $output);
}
public function get(ServerRequestInterface $request, ResponseInterface $response, int $id): ResponseInterface
{
$output = [
'id' => $id,
$this->getSingular() => $this->getService()->getById($id)
];
return $this->withJson($response, $output);
}
public function getMany(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$input = $request->getParsedBody();
$output = [
'input' => $input,
$this->getPlural() => []
];
if (isset($input[$this->getPlural()])) {
if (!is_array($input[$this->getPlural()])) {
$input[$this->getPlural()] = json_decode($input[$this->getPlural()], true);
}
foreach ($input[$this->getPlural()] as $id) {
$output[$this->getPlural()][$id] = $this->getService()->getById($id);
}
}
return $this->withJson($response, $output);
}
public function add(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$input = $request->getParsedBody();
$output = [
'input' => $input,
$this->getPlural() => []
];
if (isset($input[$this->getPlural()])) {
if (!is_array($input[$this->getPlural()])) {
$input[$this->getPlural()] = json_decode($input[$this->getPlural()], true);
}
foreach ($input[$this->getPlural()] as $data) {
$output[$this->getPlural()] []= $this->getService()->add($data);
}
}
return $this->withJson($response, $output);
}
public function edit(ServerRequestInterface $request, ResponseInterface $response, int $id): ResponseInterface
{
$input = $request->getParsedBody();
$obj = $this->getService()->getById($id);
$output = [
'id' => $id,
'input' => $input,
'old' => $obj,
$this->getSingular() => $this->getService()->edit($obj, $input)
];
return $this->withJson($response, $output);
}
public function editMany(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$input = $request->getParsedBody();
$output = [
'input' => $input,
'old' => [],
$this->getPlural() => []
];
if (isset($input[$this->getPlural()])) {
if (!is_array($input[$this->getPlural()])) {
$input[$this->getPlural()] = json_decode($input[$this->getPlural()], true);
}
foreach ($input[$this->getPlural()] as $id => $data) {
$obj = $this->getService()->getById($id);
$output['old'][$id]= $obj;
$output[$this->getPlural()][$id]= $this->getService()->edit($obj, $data);
}
}
return $this->withJson($response, $output);
}
public function delete(ServerRequestInterface $request, ResponseInterface $response, int $id): ResponseInterface
{
$obj = $this->getService()->getById($id);
$output = [
'id' => $id,
$this->getSingular() => $obj,
'deleted' => $this->getService()->delete($obj)
];
return $this->withJson($response, $output);
}
public function deleteMany(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$input = $request->getParsedBody();
$output = [
'input' => $input,
'old' => [],
$this->getPlural() => []
];
if (isset($input[$this->getPlural()])) {
if (!is_array($input[$this->getPlural()])) {
$input[$this->getPlural()] = json_decode($input[$this->getPlural()], true);
}
foreach ($input[$this->getPlural()] as $id) {
$obj = $this->getService()->getById($id);
$output['old'][$id] = $obj;
$output[$this->getPlural()][$id] = $this->getService()->delete($obj);
}
}
return $this->withJson($response, $output);
}
abstract protected function getService(): Concept\Model\Service;
abstract protected function getSingular(): string;
abstract protected function getPlural(): string;
}

View File

@ -0,0 +1,30 @@
<?php
namespace ProVM\Middleware;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use ProVM\Exception\Authorization\MissingToken;
use ProVM\Service;
class Authorization
{
public function __construct(protected ResponseFactoryInterface $responseFactory,
protected Service\Authorization $authorizationService,
protected LoggerInterface $logger) {}
public function __invoke(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
if (!$this->authorizationService->isAuthorized($request)) {
return $this->responseFactory->createResponse(403); // Forbidden
}
} catch (MissingToken $exception) {
$this->logger->alert($exception, ['request' => $request]);
return $this->responseFactory->createResponse(401); // Unathorized
}
return $handler->handle($request);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace ProVM\Middleware;
use Psr\Http\Message\{ResponseInterface, ServerRequestInterface, ResponseFactoryInterface};
use Psr\Http\Server\{RequestHandlerInterface};
use Psr\Log\LoggerInterface;
use Slim\Exception\HttpNotFoundException;
class NotFound
{
public function __construct(protected ResponseFactoryInterface $responseFactory, protected LoggerInterface $logger) {}
public function __invoke(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
return $handler->handle($request);
} catch (HttpNotFoundException $exception) {
$this->logger->error($exception);
$response = $this->responseFactory->createResponse(404)
->withHeader('Content-Type', 'application/json');
$response->getBody()->write(json_encode(['code' => 404, 'error' => 'Not Found']));
return $response;
}
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace ProVM\Reinforce\Controller;
use Psr\Http\Message\ResponseInterface;
trait Json
{
public function withJson(ResponseInterface $response, mixed $data = null, int $statusCode = 200): ResponseInterface
{
$response->getBody()->write(json_encode($data));
return $response
->withStatus($statusCode)
->withHeader('Content-Type', 'application/json');
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace ProVM\Service;
use Exception;
use ProVM\Exception\Authorization\MissingBearer;
use ProVM\Exception\Authorization\MissingHeader;
use ProVM\Exception\Authorization\MissingToken;
use Psr\Http\Message\ServerRequestInterface;
class Authorization
{
public function __construct(protected string $token) {}
/**
* @throws MissingToken
*/
public function isAuthorized(ServerRequestInterface $request): bool
{
$suppliedToken = $this->getToken($request);
return ($suppliedToken === $this->token);
}
/**
* @throws MissingToken
*/
private function getToken(ServerRequestInterface $request): string
{
try {
return $this->getHeaderToken($request);
} catch (Exception $exception) {
try {
return $this->getBodyToken($request, $exception);
} catch (Exception $exception) {
try {
return $this->getQueryToken($request, $exception);
} catch (Exception $exception) {
throw new MissingToken('request', $exception);
}
}
}
}
/**
* @throws MissingHeader
* @throws MissingBearer
*/
private function getHeaderToken(ServerRequestInterface $request): string
{
$headers = $request->getHeader('Authorization');
if (empty($headers)) {
throw new MissingHeader();
}
foreach ($headers as $header) {
if (!str_starts_with($header, 'Bearer ')) {
continue;
}
return substr($header, strlen('Bearer '));
}
throw new MissingBearer();
}
/**
* @throws MissingToken
*/
private function getBodyToken(ServerRequestInterface $request, Exception $previous): string
{
$body = $request->getParsedBody();
if (empty($body['token'])) {
throw new MissingToken('body', $previous);
}
return $body['token'];
}
/**
* @throws MissingToken
*/
private function getQueryToken(ServerRequestInterface $request, Exception $previous): string
{
$query = $request->getQueryParams();
if (empty($query['token'])) {
throw new MissingToken('query', $previous);
}
return $query['token'];
}
}