Api + unit tests
This commit is contained in:
parent
aef61a6c8f
commit
5e14218ad4
46 changed files with 1935 additions and 97 deletions
60
.env.testing
Normal file
60
.env.testing
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
APP_NAME=Dam
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=base64:2fKdUeMKBwyKzJeOcIaRXC0yujSMKkQJ98XLhC2uqf0=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_URL=https://dam.s-web.test
|
||||||
|
PROJECT_UUID=
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
DB_CONNECTION=mysql
|
||||||
|
DB_HOST=dev-mariadb
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=testing_dam
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=secret
|
||||||
|
|
||||||
|
ELASTICSEARCH_HOSTS=dev-elasticsearch:9200
|
||||||
|
|
||||||
|
BROADCAST_DRIVER=log
|
||||||
|
CACHE_DRIVER=file
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=sync
|
||||||
|
SESSION_DRIVER=file
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
|
||||||
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MAIL_MAILER=smtp
|
||||||
|
MAIL_HOST=dev-mailhog
|
||||||
|
MAIL_PORT=1025
|
||||||
|
MAIL_USERNAME=
|
||||||
|
MAIL_PASSWORD=
|
||||||
|
MAIL_ENCRYPTION=null
|
||||||
|
MAIL_FROM_ADDRESS=no-reply@dam.lesidc.com
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
MAIL_ADMIN=equipe.web@ldc.fr
|
||||||
|
|
||||||
|
|
||||||
|
LDAP_ACCOUNT_PREFIX="cn="
|
||||||
|
LDAP_ACCOUNT_SUFFIX=",ou=users,dc=example,dc=org"
|
||||||
|
LDAP_HOSTS=dev-openldap
|
||||||
|
LDAP_PORT=1389
|
||||||
|
LDAP_BASE_DN="dc=example,dc=org"
|
||||||
|
LDAP_USERNAME="cn=admin,dc=example,dc=org"
|
||||||
|
LDAP_PASSWORD=adminpassword
|
||||||
|
LDAP_USE_SSL=false
|
||||||
|
|
||||||
|
PROJECT_UUID=
|
||||||
|
|
||||||
|
## MAIL ADDRESSES
|
||||||
|
MAIL_LIST_SERVICEWEB=equipe.web@ldc.fr
|
||||||
|
|
||||||
|
LDC_STATUS_CHECKER_API_KEY=CE8S52Qq7stf5k8frF4T
|
||||||
|
LDC_STATUS_CHECKER_UUID=4d8b2c20-107a-4682-b591-dc6c685126df
|
||||||
60
.env.testing.example
Normal file
60
.env.testing.example
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
APP_NAME=Dam
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=base64:2fKdUeMKBwyKzJeOcIaRXC0yujSMKkQJ98XLhC2uqf0=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_URL=https://dam.s-web.test
|
||||||
|
PROJECT_UUID=
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
DB_CONNECTION=mysql
|
||||||
|
DB_HOST=dev-mariadb
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=testing_dam
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=
|
||||||
|
|
||||||
|
ELASTICSEARCH_HOSTS=dev-elasticsearch:9200
|
||||||
|
|
||||||
|
BROADCAST_DRIVER=log
|
||||||
|
CACHE_DRIVER=file
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=sync
|
||||||
|
SESSION_DRIVER=file
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
|
||||||
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MAIL_MAILER=smtp
|
||||||
|
MAIL_HOST=dev-mailhog
|
||||||
|
MAIL_PORT=1025
|
||||||
|
MAIL_USERNAME=
|
||||||
|
MAIL_PASSWORD=
|
||||||
|
MAIL_ENCRYPTION=null
|
||||||
|
MAIL_FROM_ADDRESS=no-reply@dam.lesidc.com
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
MAIL_ADMIN=equipe.web@ldc.fr
|
||||||
|
|
||||||
|
|
||||||
|
LDAP_ACCOUNT_PREFIX=
|
||||||
|
LDAP_ACCOUNT_SUFFIX=
|
||||||
|
LDAP_HOSTS=
|
||||||
|
LDAP_PORT=
|
||||||
|
LDAP_BASE_DN=
|
||||||
|
LDAP_USERNAME=
|
||||||
|
LDAP_PASSWORD=
|
||||||
|
LDAP_USE_SSL=
|
||||||
|
|
||||||
|
PROJECT_UUID=
|
||||||
|
|
||||||
|
## MAIL ADDRESSES
|
||||||
|
MAIL_LIST_SERVICEWEB=equipe.web@ldc.fr
|
||||||
|
|
||||||
|
LDC_STATUS_CHECKER_API_KEY=
|
||||||
|
LDC_STATUS_CHECKER_UUID=
|
||||||
16
README.md
16
README.md
|
|
@ -3,6 +3,7 @@
|
||||||
# TODO:
|
# TODO:
|
||||||
- Remplace par VUE 3 + Runtime Only
|
- Remplace par VUE 3 + Runtime Only
|
||||||
- Typescript
|
- Typescript
|
||||||
|
- Faut-il exporter les stats Api avant de supprimer un ApiToken ?
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
### - Default
|
### - Default
|
||||||
|
|
@ -12,20 +13,10 @@ composer install
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
php artisan key:generate
|
php artisan key:generate
|
||||||
```
|
```
|
||||||
### - Stack en attendant le bump Laravel
|
### - Stack Laravel
|
||||||
```
|
```
|
||||||
npm install && npm run prod
|
npm install && npm run prod
|
||||||
composer install
|
composer install
|
||||||
```
|
|
||||||
Dans vendor/laravel/framework/src/Illuminate/Foundation/PackageManifest.php, ligne 116
|
|
||||||
```
|
|
||||||
//$packages = json_decode($this->files->get($path), true);
|
|
||||||
$installed = json_decode($this->files->get($path), true);
|
|
||||||
$packages = $installed['packages'] ?? $installed;
|
|
||||||
```
|
|
||||||
Puis
|
|
||||||
```
|
|
||||||
composer install
|
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
php artisan key:generate
|
php artisan key:generate
|
||||||
```
|
```
|
||||||
|
|
@ -36,6 +27,9 @@ php artisan key:generate
|
||||||
.env rajouter PROJECT_UUID=
|
.env rajouter PROJECT_UUID=
|
||||||
.env rajouter MAIL_LIST_SERVICEWEB=equipe.web@ldc.fr
|
.env rajouter MAIL_LIST_SERVICEWEB=equipe.web@ldc.fr
|
||||||
|
|
||||||
|
# SUPERVISOR
|
||||||
|
jobs Api stats sur queue `database`
|
||||||
|
|
||||||
## Dans les LABs
|
## Dans les LABs
|
||||||
```
|
```
|
||||||
MAIL_VERIFY_PEER=false
|
MAIL_VERIFY_PEER=false
|
||||||
|
|
|
||||||
93
app/Enums/Legacy/ApiError.php
Normal file
93
app/Enums/Legacy/ApiError.php
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums\Legacy;
|
||||||
|
|
||||||
|
use Kwaadpepper\Enum\BaseEnumRoutable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This describes an API error.
|
||||||
|
*
|
||||||
|
* @method static self modelNotFound()
|
||||||
|
* The request model was not found (typically the customer)
|
||||||
|
*
|
||||||
|
* @method static self validationFailed()
|
||||||
|
* This means some data could not be validated,
|
||||||
|
* you may have to look into message to get the details
|
||||||
|
*
|
||||||
|
* @method static self apiAuthFailed()
|
||||||
|
* The authentification to the api failed. You may have to
|
||||||
|
* check into config or .env file for your service credentials
|
||||||
|
*
|
||||||
|
* @method static self authFailed()
|
||||||
|
* The authentification for this customer failed because of wrong credentials
|
||||||
|
*
|
||||||
|
* @method static self methodNotAllowed()
|
||||||
|
* This http method is not allowed
|
||||||
|
*
|
||||||
|
* @method static self wrongTimeStamp()
|
||||||
|
* The timestamp given is wrong. This would happen in case if
|
||||||
|
* a replay attack scenario. Timestamps are provided in headers
|
||||||
|
* and in the http body
|
||||||
|
*
|
||||||
|
* @method static self missingParameter()
|
||||||
|
* The request is missing some parameters
|
||||||
|
*
|
||||||
|
* @method static self throttleRequestsException()
|
||||||
|
* Too many requests have been attempted. The is a throttle security.
|
||||||
|
* Normally you should not trigger this.
|
||||||
|
* If this happens fill a ticket on https://supportweb.ldc.fr
|
||||||
|
* with some technical details
|
||||||
|
*
|
||||||
|
* @method static self authorizationException()
|
||||||
|
* You are maybe trying to access data that is out of your range.
|
||||||
|
* This should not happen normally, it is here to allow you to handle this case.
|
||||||
|
* If this happens fill a ticket on https://supportweb.ldc.fr
|
||||||
|
* with some technical details
|
||||||
|
*
|
||||||
|
* @package DAMclient
|
||||||
|
* @subpackage enums
|
||||||
|
*
|
||||||
|
* TODO: pass this as php8 enum, when support of 7 will be dropped.
|
||||||
|
*/
|
||||||
|
class ApiError extends BaseEnumRoutable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Valeurs des possibilités de cet énumération (value)
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'modelNotFound' => 101,
|
||||||
|
'validationFailed' => 102,
|
||||||
|
'authFailed' => 117,
|
||||||
|
'apiAuthFailed' => 119,
|
||||||
|
'methodNotAllowed' => 150,
|
||||||
|
'wrongTimeStamp' => 151,
|
||||||
|
'missingParameter' => 400,
|
||||||
|
'authorizationException' => 403,
|
||||||
|
'throttleRequestsException' => 429
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Libellés des possibilités de cet énumération (label)
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function labels(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'modelNotFound' => '%s introuvable.',
|
||||||
|
'validationFailed' => 'Impossible de valider les données',
|
||||||
|
'authFailed' => 'Echec de l\'authentification du compte',
|
||||||
|
'apiAuthFailed' => 'Echec de l\'authentification client à l\'Api DAM',
|
||||||
|
'methodNotAllowed' => 'This method is not allowed for this request',
|
||||||
|
'wrongTimeStamp' => 'Your request is malformed, please check timestamp header and parameter',
|
||||||
|
'missingParameter' => 'The request is missing some parameters',
|
||||||
|
'authorizationException' => 'You are not autorized to access this',
|
||||||
|
'throttleRequestsException' => 'Throttle: too many request have been attempted, please retry later'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
18
app/Enums/Legacy/ApiStatus.php
Normal file
18
app/Enums/Legacy/ApiStatus.php
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums\Legacy;
|
||||||
|
|
||||||
|
use Kwaadpepper\Enum\BaseEnumRoutable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @method static self OK()
|
||||||
|
* @method static self KO()
|
||||||
|
*
|
||||||
|
* @package DAMclient
|
||||||
|
* @subpackage enums
|
||||||
|
*
|
||||||
|
* TODO: pass this as php8 enum, when support of 7 will be dropped.
|
||||||
|
*/
|
||||||
|
class ApiStatus extends BaseEnumRoutable
|
||||||
|
{
|
||||||
|
}
|
||||||
102
app/Exceptions/Api/ApiErrorException.php
Normal file
102
app/Exceptions/Api/ApiErrorException.php
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions\Api;
|
||||||
|
|
||||||
|
use App\Enums\Legacy\ApiError;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This would be thrown if any of the case
|
||||||
|
* referenced in \App\Enums\ApiError would
|
||||||
|
* occur within the dam
|
||||||
|
*
|
||||||
|
* On the client an ApiErrorException would
|
||||||
|
* also be throw with the ApiError corresponding to
|
||||||
|
* the situation
|
||||||
|
*
|
||||||
|
* @package DAMclient
|
||||||
|
* @subpackage exceptions
|
||||||
|
*/
|
||||||
|
class ApiErrorException extends \RuntimeException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Api Error
|
||||||
|
* This value makes the error more explicit.
|
||||||
|
* Handling these errors are normal as inputs or
|
||||||
|
* request may be undesired or refused by the dam.
|
||||||
|
*
|
||||||
|
* @var \App\Enums\Legacy\ApiError
|
||||||
|
*/
|
||||||
|
protected $error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an explicit error message,
|
||||||
|
* ou may print this to the front or not depending
|
||||||
|
* on $error value
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the http code returned by the dam.
|
||||||
|
* This value could be used to handle errors.
|
||||||
|
* But please take in consideration $error first
|
||||||
|
*
|
||||||
|
* @var integer
|
||||||
|
*/
|
||||||
|
protected $httpCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new exception base on ApiError enum.
|
||||||
|
*
|
||||||
|
* @param \App\Enums\Legacy\ApiError $error
|
||||||
|
* @param integer $httpCode
|
||||||
|
* @param string $message
|
||||||
|
* @param \Throwable|null $previous
|
||||||
|
*/
|
||||||
|
public function __construct(ApiError $error, int $httpCode = 400, string $message = '', \Throwable $previous = null)
|
||||||
|
{
|
||||||
|
$this->error = $error;
|
||||||
|
$this->message = $message;
|
||||||
|
$this->httpCode = $httpCode;
|
||||||
|
parent::__construct(
|
||||||
|
strlen($this->message) ? $this->message : $error->label,
|
||||||
|
$error->value,
|
||||||
|
$previous
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Api Error
|
||||||
|
*
|
||||||
|
* @return \App\Enums\Legacy\ApiError
|
||||||
|
*/
|
||||||
|
public function getApiError(): ApiError
|
||||||
|
{
|
||||||
|
return $this->error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the http code
|
||||||
|
*
|
||||||
|
* @return integer
|
||||||
|
*/
|
||||||
|
public function getStatusCode(): int
|
||||||
|
{
|
||||||
|
return $this->httpCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is used by laravel only.
|
||||||
|
* It uses Log class to print the error.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function report()
|
||||||
|
{
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
Log::debug(sprintf('DAM API: %s', $this->getMessage()));
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,20 @@
|
||||||
|
|
||||||
namespace App\Exceptions;
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use App\Enums\Legacy\ApiError;
|
||||||
|
use App\Enums\Legacy\ApiStatus;
|
||||||
|
use App\Exceptions\Api\ApiErrorException;
|
||||||
|
use App\Http\Resources\Api\ApiResponse as ApiResponseResource;
|
||||||
|
use App\Lib\Helper;
|
||||||
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
|
use Illuminate\Auth\AuthenticationException;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
|
use Illuminate\Http\Exceptions\ThrottleRequestsException;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
use Kwaadpepper\ExceptionHandler\Exceptions\ExceptionHandler;
|
use Kwaadpepper\ExceptionHandler\Exceptions\ExceptionHandler;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class Handler extends ExceptionHandler
|
class Handler extends ExceptionHandler
|
||||||
|
|
@ -12,14 +25,18 @@ class Handler extends ExceptionHandler
|
||||||
*
|
*
|
||||||
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
|
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
|
||||||
*/
|
*/
|
||||||
protected $levels = [];
|
protected $levels = [
|
||||||
|
ApiErrorException::class => \Psr\Log\LogLevel::WARNING
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of the exception types that are not reported.
|
* A list of the exception types that are not reported.
|
||||||
*
|
*
|
||||||
* @var array<int, class-string<\Throwable>>
|
* @var array<int, class-string<\Throwable>>
|
||||||
*/
|
*/
|
||||||
protected $dontReport = [];
|
protected $dontReport = [
|
||||||
|
ApiErrorException::class,
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of the inputs that are never flashed for validation exceptions.
|
* A list of the inputs that are never flashed for validation exceptions.
|
||||||
|
|
@ -42,4 +59,126 @@ class Handler extends ExceptionHandler
|
||||||
$this->reportable(function (Throwable $e) {
|
$this->reportable(function (Throwable $e) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render an exception into an HTTP response.
|
||||||
|
*
|
||||||
|
* @param mixed $request
|
||||||
|
* @param \Throwable $e
|
||||||
|
* @return \Symfony\Component\HttpFoundation\Response
|
||||||
|
*
|
||||||
|
* @throws \Throwable If needed.
|
||||||
|
*/
|
||||||
|
public function render(mixed $request, Throwable $e)
|
||||||
|
{
|
||||||
|
if (Helper::isRequestForApi($request) and $request->isJson()) {
|
||||||
|
return $this->handleApiException($request, $e);
|
||||||
|
} //end if
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
return parent::render($request, $e);
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a json response for any api error case
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Throwable $e
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
private function handleApiException(Request $request, \Throwable $e): JsonResponse
|
||||||
|
{
|
||||||
|
switch (get_class($e)) {
|
||||||
|
case AuthenticationException::class:
|
||||||
|
/** @var AuthenticationException $e */
|
||||||
|
return $this->getReponse(
|
||||||
|
$request,
|
||||||
|
401,
|
||||||
|
ApiError::apiAuthFailed()->value,
|
||||||
|
sprintf('%s, %s', ApiError::apiAuthFailed()->label, $e->getMessage())
|
||||||
|
);
|
||||||
|
case MethodNotAllowedHttpException::class:
|
||||||
|
/** @var MethodNotAllowedHttpException $e */
|
||||||
|
return $this->getReponse(
|
||||||
|
$request,
|
||||||
|
$e->getStatusCode(),
|
||||||
|
ApiError::methodNotAllowed()->value,
|
||||||
|
sprintf('%s, %s', ApiError::methodNotAllowed()->label, $e->getMessage())
|
||||||
|
);
|
||||||
|
case ValidationException::class:
|
||||||
|
/** @var ValidationException $e */
|
||||||
|
return $this->getReponse(
|
||||||
|
$request,
|
||||||
|
$e->status,
|
||||||
|
ApiError::validationFailed()->value,
|
||||||
|
sprintf(ApiError::validationFailed()->label, $e->getMessage()),
|
||||||
|
['errors' => $e->errors()]
|
||||||
|
);
|
||||||
|
case ModelNotFoundException::class:
|
||||||
|
/** @var ModelNotFoundException $e */
|
||||||
|
return $this->getReponse(
|
||||||
|
$request,
|
||||||
|
404,
|
||||||
|
ApiError::modelNotFound()->value,
|
||||||
|
sprintf(
|
||||||
|
ApiError::modelNotFound()->label,
|
||||||
|
\array_slice(explode('\\', (string)$e->getModel()), -1)[0]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
case ApiErrorException::class:
|
||||||
|
/** @var ApiErrorException $e */
|
||||||
|
return $this->getReponse(
|
||||||
|
$request,
|
||||||
|
$e->getStatusCode(),
|
||||||
|
$e->getCode(),
|
||||||
|
$e->getMessage()
|
||||||
|
);
|
||||||
|
case ThrottleRequestsException::class:
|
||||||
|
/** @var ThrottleRequestsException $e */
|
||||||
|
return $this->getReponse(
|
||||||
|
$request,
|
||||||
|
429,
|
||||||
|
ApiError::throttleRequestsException()->value,
|
||||||
|
ApiError::throttleRequestsException()->label
|
||||||
|
);
|
||||||
|
case AuthorizationException::class:
|
||||||
|
/** @var AuthorizationException $e */
|
||||||
|
return $this->getReponse(
|
||||||
|
$request,
|
||||||
|
403,
|
||||||
|
ApiError::authorizationException()->value,
|
||||||
|
ApiError::authorizationException()->label
|
||||||
|
);
|
||||||
|
} //end switch
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
if (config('app.env') === 'testing') {
|
||||||
|
// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Found
|
||||||
|
print_r((string)$e);
|
||||||
|
}
|
||||||
|
return $this->getReponse($request, 500, intval($e->getCode()), $e->getMessage());
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an Api json response
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param integer $httpCode
|
||||||
|
* @param integer $apiCode
|
||||||
|
* @param string $message
|
||||||
|
* @param array $data
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
private static function getReponse(
|
||||||
|
Request $request,
|
||||||
|
int $httpCode,
|
||||||
|
int $apiCode,
|
||||||
|
string $message,
|
||||||
|
array $data = []
|
||||||
|
): JsonResponse {
|
||||||
|
return (new ApiResponseResource(
|
||||||
|
(object)array_merge($data, ['message' => $message, 'code' => $apiCode]),
|
||||||
|
ApiStatus::KO()
|
||||||
|
))->toResponse($request)->setStatusCode($httpCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
19
app/Http/Controllers/Api/ApiController.php
Normal file
19
app/Http/Controllers/Api/ApiController.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class ApiController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Just returns HTTP 200 response
|
||||||
|
*
|
||||||
|
* @return JsonResponse
|
||||||
|
*/
|
||||||
|
public function ping(): JsonResponse
|
||||||
|
{
|
||||||
|
return response()->json();
|
||||||
|
}
|
||||||
|
}
|
||||||
86
app/Http/Controllers/Api/MediaController.php
Normal file
86
app/Http/Controllers/Api/MediaController.php
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Enums\MediaStatut;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Resources\Api\MediaListResource;
|
||||||
|
use App\Http\Resources\Api\MediaPictureResource;
|
||||||
|
use App\Http\Resources\Api\MediaResource;
|
||||||
|
use App\Models\Article;
|
||||||
|
use App\Models\Media;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
use Illuminate\Http\Resources\Json\ResourceCollection as JsonResourceCollection;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class MediaController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show specific media
|
||||||
|
*
|
||||||
|
* @param \App\Models\Media $media
|
||||||
|
* @return \Illuminate\Http\Resources\Json\JsonResource
|
||||||
|
*/
|
||||||
|
public function show(Media $media): JsonResource
|
||||||
|
{
|
||||||
|
return new MediaResource($media);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Media list of the latest version for each vue type and variant
|
||||||
|
* of media that is linked with an article.
|
||||||
|
*
|
||||||
|
* Working example
|
||||||
|
* SELECT `mediaList`.`id`,`mediaList`.`arcadia_id`,`mediaList`.`libelle`,`mediaList`.`uid`,
|
||||||
|
* `mediaList`.`version`,`mediaList`.`type_vue_id`,`mediaList`.`variante_id`,`mediaList`.`created_at`,`mediaList`.`updated_at`
|
||||||
|
* FROM (
|
||||||
|
* SELECT `media`.*
|
||||||
|
* from `media`
|
||||||
|
* JOIN `media_article` ON `media_article`.`media_id` =`media`.`id`
|
||||||
|
* JOIN `articles` ON `articles`.`id` =`media_article`.`article_id` AND `articles`.`noArt` = 2842
|
||||||
|
* WHERE
|
||||||
|
* `media`.type_media_id IN (1,5) AND
|
||||||
|
* `media`.isLocked = 0 AND
|
||||||
|
* `media`.statut != 0
|
||||||
|
* ORDER BY `version` DESC
|
||||||
|
* LIMIT 18446744073709551615
|
||||||
|
* ) AS mediaList
|
||||||
|
* GROUP BY `type_vue_id`,`variante_id`
|
||||||
|
*
|
||||||
|
* @param \App\Models\Article $article
|
||||||
|
* @return \Illuminate\Http\Resources\Json\ResourceCollection
|
||||||
|
*/
|
||||||
|
public function getMediaListByArticle(Article $article): JsonResourceCollection
|
||||||
|
{
|
||||||
|
// ! Hack to allow group by (may have side effects)
|
||||||
|
DB::statement("SET SQL_MODE=''");
|
||||||
|
|
||||||
|
/** @var \Illuminate\Database\Eloquent\Collection<\App\Models\Media> */
|
||||||
|
$mediaList = Media::withTrashed()->fromSub(
|
||||||
|
Media::query()->whereHas('articles', function ($q) use ($article) {
|
||||||
|
$q->where('noArt', $article->noArt);
|
||||||
|
})->whereHas('typeMedia', function ($q) {
|
||||||
|
$q->whereIn('libelle', ['Photo', 'jpeg']);
|
||||||
|
})->where('isLocked', false)
|
||||||
|
->where('statut', '!=', MediaStatut::brouillon)
|
||||||
|
->orderBy('version', 'DESC')
|
||||||
|
// ! Maria Hack: use limit to preserve order.
|
||||||
|
->limit(\PHP_INT_MAX),
|
||||||
|
'mediaList'
|
||||||
|
)->groupBy(['mediaList.type_vue_id', 'mediaList.variante_id'])
|
||||||
|
->paginate();
|
||||||
|
|
||||||
|
return new MediaListResource($mediaList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Media picture resource
|
||||||
|
*
|
||||||
|
* @param \App\Models\Media $media
|
||||||
|
* @return \Illuminate\Http\Resources\Json\JsonResource
|
||||||
|
*/
|
||||||
|
public function getMediaPicture(Media $media): JsonResource
|
||||||
|
{
|
||||||
|
return new MediaPictureResource($media);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -580,7 +580,7 @@ class MediaController extends Controller
|
||||||
|
|
||||||
// * On est sur un sujet arcadia (donc pas le sujet Groupe par exemple) .
|
// * On est sur un sujet arcadia (donc pas le sujet Groupe par exemple) .
|
||||||
// ! restreindre pour tout sauf si type de vue 19 (BAT) et sujet 1 (Articles)
|
// ! restreindre pour tout sauf si type de vue 19 (BAT) et sujet 1 (Articles)
|
||||||
if (\optional($media->typevue)->id === 19 and \optional($media->sujet)->id === 1) {
|
if (\optional($media->typeVue)->id === 19 and \optional($media->sujet)->id === 1) {
|
||||||
return \response()->json(ArcadiaLinkUtilities::getRefKey($media)->count() > 0);
|
return \response()->json(ArcadiaLinkUtilities::getRefKey($media)->count() > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -626,6 +626,7 @@ class MediaController extends Controller
|
||||||
*
|
*
|
||||||
* @param \App\Models\Media $media
|
* @param \App\Models\Media $media
|
||||||
* @return \Illuminate\Http\JsonResponse
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
* @throws \Illuminate\Validation\ValidationException If the media is already linked to Arcadia.
|
||||||
*/
|
*/
|
||||||
public function createArcadiaLink(Media $media): JsonResponse
|
public function createArcadiaLink(Media $media): JsonResponse
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ class UploadController extends Controller
|
||||||
// 400 instead of 422 is for resumablejs to handle properly.
|
// 400 instead of 422 is for resumablejs to handle properly.
|
||||||
return \response()->json($e->errors(), 400);
|
return \response()->json($e->errors(), 400);
|
||||||
}
|
}
|
||||||
}
|
}//end if
|
||||||
|
|
||||||
// We are in chunk mode, lets send the current progress.
|
// We are in chunk mode, lets send the current progress.
|
||||||
/** @var AbstractHandler $handler */
|
/** @var AbstractHandler $handler */
|
||||||
|
|
@ -156,7 +156,7 @@ class UploadController extends Controller
|
||||||
// 400 instead of 422 is for resumablejs to handle properly.
|
// 400 instead of 422 is for resumablejs to handle properly.
|
||||||
return \response()->json($e->errors(), 400);
|
return \response()->json($e->errors(), 400);
|
||||||
}
|
}
|
||||||
}
|
}//end if
|
||||||
|
|
||||||
// We are in chunk mode, lets send the current progress.
|
// We are in chunk mode, lets send the current progress.
|
||||||
/** @var AbstractHandler $handler */
|
/** @var AbstractHandler $handler */
|
||||||
|
|
@ -190,6 +190,7 @@ class UploadController extends Controller
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Http\UploadedFile $file
|
* @param \Illuminate\Http\UploadedFile $file
|
||||||
* @return void
|
* @return void
|
||||||
|
* @throws \Illuminate\Validation\ValidationException If file extension could not be confirmed.
|
||||||
*/
|
*/
|
||||||
private function validateExtension(UploadedFile $file)
|
private function validateExtension(UploadedFile $file)
|
||||||
{
|
{
|
||||||
|
|
@ -203,16 +204,19 @@ class UploadController extends Controller
|
||||||
$mediaExtension = MediaExtension::from($file->getClientOriginalExtension());
|
$mediaExtension = MediaExtension::from($file->getClientOriginalExtension());
|
||||||
|
|
||||||
if ($file->guessExtension()) {
|
if ($file->guessExtension()) {
|
||||||
$mt = new MimeTypes();
|
$mt = new MimeTypes();
|
||||||
$guessedExts = $mt->getExtensions($mt->guessMimeType($file->path()));
|
$guessedExts = $mt->getExtensions($mt->guessMimeType($file->path()));
|
||||||
|
|
||||||
if (!\in_array($mediaExtension->value, $guessedExts)) {
|
if (!\in_array($mediaExtension->value, $guessedExts)) {
|
||||||
$guessedExts = \implode(',', $guessedExts);
|
$guessedExts = \implode(',', $guessedExts);
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'file' => trans('File guessed extensions :guessedExts does not match file name extension :extension', [
|
'file' => trans(
|
||||||
'guessedExt' => $guessedExts,
|
'File guessed extensions :guessedExts does not match file name extension :extension',
|
||||||
'extension' => $mediaExtension,
|
[
|
||||||
])
|
'guessedExt' => $guessedExts,
|
||||||
|
'extension' => $mediaExtension,
|
||||||
|
]
|
||||||
|
)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,10 @@ class Kernel extends HttpKernel
|
||||||
|
|
||||||
'api' => [
|
'api' => [
|
||||||
'throttle:api',
|
'throttle:api',
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
\App\Http\Middleware\JsonOnly::class,
|
||||||
|
\App\Http\Middleware\ApiResponse::class,
|
||||||
|
\App\Http\Middleware\ApiCheckTimeStamps::class,
|
||||||
|
\Illuminate\Routing\Middleware\SubstituteBindings::class
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
41
app/Http/Middleware/ApiCheckTimeStamps.php
Normal file
41
app/Http/Middleware/ApiCheckTimeStamps.php
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Enums\Legacy\ApiError;
|
||||||
|
use App\Exceptions\Api\ApiErrorException;
|
||||||
|
use App\Lib\Helper;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check Http Header Timestamp with request timestamp
|
||||||
|
* to prevent replay attacks
|
||||||
|
* https://restfulapi.net/security-essentials/
|
||||||
|
*/
|
||||||
|
class ApiCheckTimeStamps
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
* @return mixed
|
||||||
|
* @throws ApiErrorException With ApiError::wrongTimeStamp if timestamp has an incorrect format or is timed out.
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, \Closure $next)
|
||||||
|
{
|
||||||
|
if (Helper::isRequestForApi($request)) {
|
||||||
|
if (!\is_string($request->timestamp) or !Carbon::hasFormat($request->timestamp, 'Y-m-d H:i:s')) {
|
||||||
|
throw new ApiErrorException(ApiError::wrongTimeStamp());
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
Carbon::parse($request->timestamp, 'utc')
|
||||||
|
->isBefore(Carbon::now('UTC')->subSeconds(config('api.timestampTimeout', 10)))
|
||||||
|
) {
|
||||||
|
throw new ApiErrorException(ApiError::wrongTimeStamp());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
app/Http/Middleware/ApiResponse.php
Normal file
59
app/Http/Middleware/ApiResponse.php
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Enums\Legacy\ApiStatus;
|
||||||
|
use App\Http\Resources\Api\ApiResponse as ApiResponseResource;
|
||||||
|
use App\Models\Api\ApiToken;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sur the response is an ApiResource resource.
|
||||||
|
*/
|
||||||
|
class ApiResponse
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
* @return mixed
|
||||||
|
* @throws \ErrorException If the received response is not a JsonResponse.
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, \Closure $next)
|
||||||
|
{
|
||||||
|
/** @var \Illuminate\Http\JsonResponse $response */
|
||||||
|
$response = $next($request);
|
||||||
|
|
||||||
|
if (\get_class($response) !== JsonResponse::class) {
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
throw new \ErrorException('All api responses should be json.');
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set no cache header.
|
||||||
|
$response->header('Cache-Control', 'No-Store');
|
||||||
|
|
||||||
|
/** @var \App\Models\Api\ApiToken|null */
|
||||||
|
if (\optional($apiToken = app(ApiToken::class))->id) {
|
||||||
|
$apiToken->incrementStat();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var object $data */
|
||||||
|
$data = (object)$response->getData();
|
||||||
|
|
||||||
|
// If reponse is already a ApiResponse then just return it. This happens if error handler comes into action.
|
||||||
|
if (
|
||||||
|
isset($data->status) and
|
||||||
|
isset($data->version) and
|
||||||
|
in_array($data->status, [ApiStatus::OK()->value, ApiStatus::KO()->value])
|
||||||
|
) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var \App\Enums\Legacy\ApiStatus $status */
|
||||||
|
$status = intval($response->getStatusCode() / 100) == 2 ? ApiStatus::OK() : ApiStatus::KO();
|
||||||
|
return $response->setData(new ApiResponseResource((object)\optional($data)->data ?? $data, $status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ class Authenticate extends Middleware
|
||||||
*/
|
*/
|
||||||
protected function redirectTo($request)
|
protected function redirectTo($request)
|
||||||
{
|
{
|
||||||
|
// phpcs:enable
|
||||||
if (! $request->expectsJson()) {
|
if (! $request->expectsJson()) {
|
||||||
return route('login');
|
return route('login');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
app/Http/Middleware/JsonOnly.php
Normal file
31
app/Http/Middleware/JsonOnly.php
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow ony json requests
|
||||||
|
*/
|
||||||
|
class JsonOnly
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, \Closure $next)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
!$request->isJson() or
|
||||||
|
!$request->expectsJson() or
|
||||||
|
is_null(json_decode($request->getContent()))
|
||||||
|
) {
|
||||||
|
abort(400, 'Invalid JSON');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/Http/Resources/Api/ApiResponse.php
Normal file
42
app/Http/Resources/Api/ApiResponse.php
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Api;
|
||||||
|
|
||||||
|
use App\Enums\Legacy\ApiStatus;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
class ApiResponse extends JsonResource
|
||||||
|
{
|
||||||
|
/** @var stdClass */
|
||||||
|
protected $data;
|
||||||
|
|
||||||
|
/** @var \App\Enums\ApiStatus */
|
||||||
|
protected $status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param stdClass $data
|
||||||
|
* @param ApiStatus $status
|
||||||
|
*/
|
||||||
|
public function __construct(stdClass $data = null, ApiStatus $status)
|
||||||
|
{
|
||||||
|
$this->status = $status;
|
||||||
|
$this->data = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @param mixed $request
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass, Squiz.Commenting.FunctionComment.Missing
|
||||||
|
public function toArray($request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'status' => $this->status->value,
|
||||||
|
'data' => (array)$this->data,
|
||||||
|
'version' => config('api.version')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/Http/Resources/Api/MediaListResource.php
Normal file
23
app/Http/Resources/Api/MediaListResource.php
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Api;
|
||||||
|
|
||||||
|
use App\Models\Media;
|
||||||
|
use Illuminate\Http\Resources\Json\ResourceCollection as JsonResourceCollection;
|
||||||
|
|
||||||
|
class MediaListResource extends JsonResourceCollection
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass, Squiz.Commenting.FunctionComment.Missing
|
||||||
|
public function toArray($request)
|
||||||
|
{
|
||||||
|
return $this->collection->map(function (Media $media) use ($request) {
|
||||||
|
return (new MediaResource($media))->toArray($request);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
51
app/Http/Resources/Api/MediaPictureResource.php
Normal file
51
app/Http/Resources/Api/MediaPictureResource.php
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Api;
|
||||||
|
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class MediaPictureResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @var array */
|
||||||
|
private static $skeleton = [
|
||||||
|
'base64_bin' => '',
|
||||||
|
'uid' => '',
|
||||||
|
'extension' => ''
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return array
|
||||||
|
* + base64_bin string
|
||||||
|
* + uid string
|
||||||
|
* + extension string
|
||||||
|
*/
|
||||||
|
//phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass, Squiz.Commenting.FunctionComment.Missing
|
||||||
|
public function toArray($request)
|
||||||
|
{
|
||||||
|
/** @var \Illuminate\Filesystem\FilesystemAdapter */
|
||||||
|
$storage = Storage::disk('media');
|
||||||
|
|
||||||
|
return array_intersect_key(
|
||||||
|
array_merge([
|
||||||
|
'base64_bin' => \base64_encode((string)$storage->get($this->relative_path)),
|
||||||
|
'uid' => $this->uid,
|
||||||
|
'extension' => $this->extension->value
|
||||||
|
]),
|
||||||
|
$this->getSkeleton()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the resource skeletton
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getSkeleton(): array
|
||||||
|
{
|
||||||
|
return self::$skeleton;
|
||||||
|
}
|
||||||
|
}
|
||||||
61
app/Http/Resources/Api/MediaResource.php
Normal file
61
app/Http/Resources/Api/MediaResource.php
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Api;
|
||||||
|
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class MediaResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @var array */
|
||||||
|
private static $skeleton = [
|
||||||
|
'id' => 0,
|
||||||
|
'arcadia_id' => 0,
|
||||||
|
'libelle' => '',
|
||||||
|
'uid' => '',
|
||||||
|
'version' => '',
|
||||||
|
'created_at' => '',
|
||||||
|
'updated_at' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return array
|
||||||
|
* + id integer
|
||||||
|
* + arcadia_id integer
|
||||||
|
* + libelle string
|
||||||
|
* + uid string
|
||||||
|
* + version integer
|
||||||
|
* + created_at datetime
|
||||||
|
* + updated_at datetime
|
||||||
|
*/
|
||||||
|
//phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass, Squiz.Commenting.FunctionComment.Missing
|
||||||
|
public function toArray($request)
|
||||||
|
{
|
||||||
|
$data = array_intersect_key(
|
||||||
|
array_merge([
|
||||||
|
'id' => $this->id,
|
||||||
|
'arcadia_id' => $this->arcadia_id,
|
||||||
|
'libelle' => $this->libelle,
|
||||||
|
'uid' => $this->uid,
|
||||||
|
'version' => $this->version,
|
||||||
|
'created_at' => $this->created_at,
|
||||||
|
'updated_at' => $this->updated_at,
|
||||||
|
]),
|
||||||
|
$this->getSkeleton()
|
||||||
|
);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the resource skeletton
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getSkeleton(): array
|
||||||
|
{
|
||||||
|
return self::$skeleton;
|
||||||
|
}
|
||||||
|
}
|
||||||
80
app/Jobs/SetApiTokenStats.php
Normal file
80
app/Jobs/SetApiTokenStats.php
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\Api\ApiToken;
|
||||||
|
use App\Models\Api\ApiTokenStat;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @codeCoverageIgnore
|
||||||
|
*/
|
||||||
|
class SetApiTokenStats implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable;
|
||||||
|
use InteractsWithQueue;
|
||||||
|
use Queueable;
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
private const KEY = '2202020';
|
||||||
|
|
||||||
|
/** @var \App\Models\Api\ApiToken */
|
||||||
|
protected $apiToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*
|
||||||
|
* @param \App\Models\Api\ApiToken $apiToken
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(ApiToken $apiToken)
|
||||||
|
{
|
||||||
|
$this->apiToken = $apiToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$date = date('Y-m-d');
|
||||||
|
$apiTokenStat = ApiTokenStat::where('date', $date)
|
||||||
|
->whereHas('apiToken', function ($q) {
|
||||||
|
$q->where('id', $this->apiToken->id);
|
||||||
|
})->firstOrNew();
|
||||||
|
$apiTokenStat->date = $date;
|
||||||
|
$apiTokenStat->nb_calls += 1;
|
||||||
|
$apiTokenStat->apiToken()->associate($this->apiToken);
|
||||||
|
$apiTokenStat->saveOrFail();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the time at which the job should timeout.
|
||||||
|
*
|
||||||
|
* @return \DateTime
|
||||||
|
*/
|
||||||
|
public function retryUntil()
|
||||||
|
{
|
||||||
|
return now()->addMinutes(rand(0, 360));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the middleware the job should pass through.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function middleware()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
(new WithoutOverlapping(self::KEY))
|
||||||
|
->releaseAfter(rand(90, 180))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -131,7 +131,7 @@ class Helper
|
||||||
'method' => $method
|
'method' => $method
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$parameters = collect(Arr::flatten($parameters));
|
$parameters = collect(Arr::dot($parameters));
|
||||||
|
|
||||||
// * Remove signature from parameters.
|
// * Remove signature from parameters.
|
||||||
$parameters->pull('signature');
|
$parameters->pull('signature');
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ class HistorizeMediaEvent
|
||||||
* @return void
|
* @return void
|
||||||
* @throws \RuntimeException If saving historic to database failed.
|
* @throws \RuntimeException If saving historic to database failed.
|
||||||
*/
|
*/
|
||||||
public function handle(MediaCreated | MediaLinkWithArcadia $event)
|
public function handle(MediaCreated | MediaLinkWithArcadia $event)
|
||||||
{
|
{
|
||||||
$historique = new Historique();
|
$historique = new Historique();
|
||||||
$type = EventHistoryType::created;
|
$type = EventHistoryType::created;
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@
|
||||||
|
|
||||||
namespace App\Models\Api;
|
namespace App\Models\Api;
|
||||||
|
|
||||||
|
use App\Jobs\SetApiTokenStats;
|
||||||
use App\Lib\Helper;
|
use App\Lib\Helper;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Crypt;
|
use Illuminate\Support\Facades\Crypt;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
@ -26,6 +28,24 @@ class ApiToken extends Model
|
||||||
'token_date' => 'datetime'
|
'token_date' => 'datetime'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timeout cache for this model
|
||||||
|
*
|
||||||
|
* @var integer
|
||||||
|
*/
|
||||||
|
protected $cacheFor = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service
|
||||||
|
*
|
||||||
|
* @param array $attributes
|
||||||
|
*/
|
||||||
|
public function __construct(array $attributes = [])
|
||||||
|
{
|
||||||
|
$this->cacheFor = config('api.servicesDBCache') * 60;
|
||||||
|
parent::__construct($attributes);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform any actions required after the model boots.
|
* Perform any actions required after the model boots.
|
||||||
*
|
*
|
||||||
|
|
@ -57,6 +77,19 @@ class ApiToken extends Model
|
||||||
|
|
||||||
// * METHODS
|
// * METHODS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment API CRM Stat
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function incrementStat(): void
|
||||||
|
{
|
||||||
|
SetApiTokenStats::dispatch($this)
|
||||||
|
->afterResponse()
|
||||||
|
->onConnection('database')
|
||||||
|
->onQueue('stats');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates new public token
|
* Generates new public token
|
||||||
*
|
*
|
||||||
|
|
@ -84,6 +117,26 @@ class ApiToken extends Model
|
||||||
return $pwd;
|
return $pwd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Service from bearerToken with its brand
|
||||||
|
*
|
||||||
|
* @param string $bearerToken
|
||||||
|
* @return \App\Models\Api\ApiToken
|
||||||
|
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException If the token matches no known service.
|
||||||
|
*/
|
||||||
|
public static function getFromBearerToken(string $bearerToken): self
|
||||||
|
{
|
||||||
|
$apiTokens = static::all();
|
||||||
|
/** @var \App\Models\Api\ApiToken $apiToken */
|
||||||
|
foreach ($apiTokens as $apiToken) {
|
||||||
|
// * Keep compatibility with V3.
|
||||||
|
if ($bearerToken === $apiToken->public_token) {
|
||||||
|
return $apiToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new ModelNotFoundException('Could not find apiToken from bearer token');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticate the request for this service
|
* Authenticate the request for this service
|
||||||
*
|
*
|
||||||
|
|
|
||||||
48
app/Models/Api/ApiTokenStat.php
Normal file
48
app/Models/Api/ApiTokenStat.php
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Api;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ApiTokenStat extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
protected $fillable = ['date'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $casts = [
|
||||||
|
'date' => 'date'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The model's attributes.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $attributes = [
|
||||||
|
'nb_calls' => 0
|
||||||
|
];
|
||||||
|
|
||||||
|
// * RELATIONSHIPS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BelongsTo apiToken relation
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
*/
|
||||||
|
public function apiToken()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ApiToken::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,6 @@ class Article extends Model
|
||||||
*/
|
*/
|
||||||
public function mediaList()
|
public function mediaList()
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Media::class);
|
return $this->belongsToMany(Media::class, 'media_article', 'media_id', 'article_id');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,12 @@
|
||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Lib\Helper;
|
||||||
|
use App\Models\Api\ApiToken;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
class AuthServiceProvider extends ServiceProvider
|
class AuthServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
|
@ -21,5 +26,46 @@ class AuthServiceProvider extends ServiceProvider
|
||||||
public function boot()
|
public function boot()
|
||||||
{
|
{
|
||||||
$this->registerPolicies();
|
$this->registerPolicies();
|
||||||
|
|
||||||
|
Auth::viaRequest('api-hmac', function (Request $request) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Un token devrais être fourni pour chaque requete
|
||||||
|
*/
|
||||||
|
if (is_null($request->bearer_token)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// * Si la route actuelle n'as pas de nom alors on refuse de la traiter.
|
||||||
|
if (!(Helper::getRoute($request))) {
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
return null;
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiToken = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$apiToken = ApiToken::getFromBearerToken($request->bearer_token ?? '');
|
||||||
|
} catch (ModelNotFoundException $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
** All Api routes must pass ApiToken model to controller.
|
||||||
|
** This hack is a custom binder instead of passing
|
||||||
|
** this to route as a parameter.
|
||||||
|
*/
|
||||||
|
app()->bind(ApiToken::class, function () use ($apiToken) {
|
||||||
|
return $apiToken;
|
||||||
|
});
|
||||||
|
|
||||||
|
// * On authenticate la request en vérifiant sa signature.
|
||||||
|
if ($apiToken->authenticateApi($request)) {
|
||||||
|
return $apiToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Enums\MediaStatut;
|
||||||
|
use App\Models\Article;
|
||||||
use App\Models\Media;
|
use App\Models\Media;
|
||||||
use App\Models\Panier;
|
use App\Models\Panier;
|
||||||
use Illuminate\Cache\RateLimiting\Limit;
|
use Illuminate\Cache\RateLimiting\Limit;
|
||||||
|
|
@ -44,7 +46,29 @@ class RouteServiceProvider extends ServiceProvider
|
||||||
});
|
});
|
||||||
Route::bind('basket_uuid', function ($value) {
|
Route::bind('basket_uuid', function ($value) {
|
||||||
return Panier::where('uid', $value)
|
return Panier::where('uid', $value)
|
||||||
->with('owner')->firstOrFail();
|
->with('owner')->firstOrFail();
|
||||||
|
});
|
||||||
|
|
||||||
|
// * API
|
||||||
|
Route::bind('apiNoArticle', function ($value) {
|
||||||
|
return Article::where('noArt', $value)->firstOrFail();
|
||||||
|
});
|
||||||
|
Route::bind('apiMedia', function ($value) {
|
||||||
|
return Media::where(function ($q) use ($value) {
|
||||||
|
$q->where('id', $value);
|
||||||
|
})->whereIn('statut', [
|
||||||
|
MediaStatut::public,
|
||||||
|
MediaStatut::public_etendu
|
||||||
|
])->firstOrFail();
|
||||||
|
});
|
||||||
|
Route::bind('apiMediaArcadiaId', function ($value) {
|
||||||
|
return Media::where(function ($q) use ($value) {
|
||||||
|
$q->where('arcadia_id', $value)
|
||||||
|
->whereNotNull('arcadia_id');
|
||||||
|
})->whereIn('statut', [
|
||||||
|
MediaStatut::public,
|
||||||
|
MediaStatut::public_etendu
|
||||||
|
])->firstOrFail();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,8 +79,10 @@ class RouteServiceProvider extends ServiceProvider
|
||||||
*/
|
*/
|
||||||
protected function configureRateLimiting()
|
protected function configureRateLimiting()
|
||||||
{
|
{
|
||||||
|
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
|
||||||
RateLimiter::for('api', function (Request $request) {
|
RateLimiter::for('api', function (Request $request) {
|
||||||
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
|
return Limit::perMinute(config('api.apiRequestRatePerMinute', 100))
|
||||||
|
->by($request->user()?->id ?: app('clientIp'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "laravel/laravel",
|
"name": "ldc/dam",
|
||||||
|
"version": "1.0.0",
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"description": "The Laravel Framework.",
|
"description": "Big freaking media manager (because arcadia sucks)",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"framework",
|
"application",
|
||||||
"laravel"
|
"media"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -16,6 +17,7 @@
|
||||||
"elasticquent/elasticquent": "dev-master",
|
"elasticquent/elasticquent": "dev-master",
|
||||||
"guzzlehttp/guzzle": "^7.2",
|
"guzzlehttp/guzzle": "^7.2",
|
||||||
"james-heinrich/getid3": "^1.9",
|
"james-heinrich/getid3": "^1.9",
|
||||||
|
"kwaadpepper/enum": "^2.0",
|
||||||
"kwaadpepper/exception-handler": "^0.0.8",
|
"kwaadpepper/exception-handler": "^0.0.8",
|
||||||
"laravel/framework": "^9.2",
|
"laravel/framework": "^9.2",
|
||||||
"laravel/sanctum": "^2.14.1",
|
"laravel/sanctum": "^2.14.1",
|
||||||
|
|
@ -50,6 +52,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"test": [
|
||||||
|
"@configclear",
|
||||||
|
"@runtestTokenSeeder",
|
||||||
|
"@runtest"
|
||||||
|
],
|
||||||
|
"testCoverage": [
|
||||||
|
"@configclear",
|
||||||
|
"@runtestTokenSeeder",
|
||||||
|
"@runtestWithCoverage"
|
||||||
|
],
|
||||||
|
"configclear": "@php artisan config:clear",
|
||||||
|
"runtestTokenSeeder": "@php artisan db:seed --class=ApiTokenSeeder",
|
||||||
|
"runtest": "@php artisan test --no-coverage --testsuite API",
|
||||||
|
"runtestWithCoverage": "@php artisan test --testsuite API",
|
||||||
"post-autoload-dump": [
|
"post-autoload-dump": [
|
||||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
"@php artisan package:discover --ansi"
|
"@php artisan package:discover --ansi"
|
||||||
|
|
|
||||||
58
composer.lock
generated
58
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "77f343483e87b0d4403a8b8209b33bd9",
|
"content-hash": "d3caf1a99a48f4de736a4a4f8aa14124",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "adldap2/adldap2",
|
"name": "adldap2/adldap2",
|
||||||
|
|
@ -1758,6 +1758,62 @@
|
||||||
},
|
},
|
||||||
"time": "2021-09-22T16:34:51+00:00"
|
"time": "2021-09-22T16:34:51+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "kwaadpepper/enum",
|
||||||
|
"version": "2.0.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Kwaadpepper/enum.git",
|
||||||
|
"reference": "a9f6e0722a68dd63eb9e989d7e65fc9b37e1f8e5"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Kwaadpepper/enum/zipball/a9f6e0722a68dd63eb9e989d7e65fc9b37e1f8e5",
|
||||||
|
"reference": "a9f6e0722a68dd63eb9e989d7e65fc9b37e1f8e5",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"orchestra/testbench": "^6.13",
|
||||||
|
"phpunit/phpunit": "~9.0",
|
||||||
|
"slevomat/coding-standard": "^7.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.5"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Kwaadpepper\\Enum\\EnumServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Kwaadpepper\\Enum\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jérémy Munsch",
|
||||||
|
"email": "github@jeremydev.ovh",
|
||||||
|
"homepage": "https://jeremydev.ovh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Enums to use for any project",
|
||||||
|
"homepage": "https://github.com/kwaadpepper/enum",
|
||||||
|
"keywords": [
|
||||||
|
"enum",
|
||||||
|
"laravel"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Kwaadpepper/enum/issues",
|
||||||
|
"source": "https://github.com/Kwaadpepper/enum/tree/2.0.1"
|
||||||
|
},
|
||||||
|
"time": "2021-09-12T00:23:39+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "kwaadpepper/exception-handler",
|
"name": "kwaadpepper/exception-handler",
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
|
|
|
||||||
30
config/api.php
Normal file
30
config/api.php
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Lib\Helper;
|
||||||
|
|
||||||
|
return [
|
||||||
|
/**
|
||||||
|
* Api versions
|
||||||
|
** this value is sent in response
|
||||||
|
*/
|
||||||
|
'version' => Helper::getPackageInfo()['version'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp timeout in seconds
|
||||||
|
* Request with timestamp older than 30sec
|
||||||
|
* will be rejected
|
||||||
|
*/
|
||||||
|
'timestampTimeout' => 30,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of request
|
||||||
|
* per user or ip per minutes
|
||||||
|
*/
|
||||||
|
'apiRequestRatePerMinute' => 256,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service requests cache
|
||||||
|
* time in minutes
|
||||||
|
*/
|
||||||
|
'servicesDBCache' => 10
|
||||||
|
];
|
||||||
|
|
@ -40,6 +40,10 @@ return [
|
||||||
'driver' => 'session',
|
'driver' => 'session',
|
||||||
'provider' => 'users',
|
'provider' => 'users',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'api' => [
|
||||||
|
'driver' => 'api-hmac'
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,37 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
/*
|
|
||||||
* The storage config
|
// * The storage config
|
||||||
*/
|
|
||||||
'storage' => [
|
'storage' => [
|
||||||
/*
|
// * Returns the folder name of the chunks. The location is in storage/app/{folder_name}
|
||||||
* Returns the folder name of the chunks. The location is in storage/app/{folder_name}
|
|
||||||
*/
|
|
||||||
'chunks' => 'chunks',
|
'chunks' => 'chunks',
|
||||||
'disk' => 'chunk_upload',
|
'disk' => 'chunk_upload',
|
||||||
],
|
],
|
||||||
'clear' => [
|
'clear' => [
|
||||||
/*
|
// * How old chunks we should delete
|
||||||
* How old chunks we should delete
|
|
||||||
*/
|
|
||||||
'timestamp' => '-3 HOURS',
|
'timestamp' => '-3 HOURS',
|
||||||
'schedule' => [
|
'schedule' => [
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'cron' => '25 * * * *', // run every hour on the 25th minute
|
// * run every hour on the 25th minute
|
||||||
|
'cron' => '25 * * * *',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'chunk' => [
|
'chunk' => [
|
||||||
// setup for the chunk naming setup to ensure same name upload at same time
|
// * setup for the chunk naming setup to ensure same name upload at same time
|
||||||
'name' => [
|
'name' => [
|
||||||
'use' => [
|
'use' => [
|
||||||
'session' => true, // should the chunk name use the session id? The uploader must send cookie!,
|
// * should the chunk name use the session id? The uploader must send cookie!,
|
||||||
'browser' => false, // instead of session we can use the ip and browser?
|
'session' => true,
|
||||||
|
// * instead of session we can use the ip and browser?
|
||||||
|
'browser' => false,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'handlers' => [
|
'handlers' => [
|
||||||
// A list of handlers/providers that will be appended to existing list of handlers
|
// * A list of handlers/providers that will be appended to existing list of handlers
|
||||||
'custom' => [],
|
'custom' => [],
|
||||||
// Overrides the list of handlers - use only what you really want
|
// * Overrides the list of handlers - use only what you really want
|
||||||
'override' => [
|
'override' => [],
|
||||||
// \Pion\Laravel\ChunkUpload\Handler\DropZoneUploadHandler::class
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,14 @@ return new class extends Migration
|
||||||
File::link($newPath, $newLink);
|
File::link($newPath, $newLink);
|
||||||
(new ConsoleOutput())->writeln("Lien symbolique créé de $oldPath vers $newPath");
|
(new ConsoleOutput())->writeln("Lien symbolique créé de $oldPath vers $newPath");
|
||||||
}
|
}
|
||||||
Media::withTrashed()->update(['folder' => DB::raw("REPLACE(folder, 'medias/', '')")]);
|
Media::withTrashed()->update([
|
||||||
Media::withTrashed()->update(['folder' => DB::raw("REPLACE(folder, '/', '')")]);
|
'folder' => DB::raw("REPLACE(folder, 'medias/', '')"),
|
||||||
|
'updated_at' => DB::raw('updated_at'),
|
||||||
|
]);
|
||||||
|
Media::withTrashed()->update([
|
||||||
|
'folder' => DB::raw("REPLACE(folder, '/', '')"),
|
||||||
|
'updated_at' => DB::raw('updated_at'),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ return new class extends Migration
|
||||||
throw new \RuntimeException('Failed to convert size');
|
throw new \RuntimeException('Failed to convert size');
|
||||||
}
|
}
|
||||||
} //end switch
|
} //end switch
|
||||||
|
$media->timestamps = false;
|
||||||
$media->saveQuietly();
|
$media->saveQuietly();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@ return new class extends Migration
|
||||||
]));
|
]));
|
||||||
return $historique;
|
return $historique;
|
||||||
})->filter()->map(function (Historique $historique) {
|
})->filter()->map(function (Historique $historique) {
|
||||||
$historique->saveOrFail();
|
$historique->timestamps = false;
|
||||||
|
$historique->saveQuietly();
|
||||||
});
|
});
|
||||||
Schema::table('historiques', function (Blueprint $table) {
|
Schema::table('historiques', function (Blueprint $table) {
|
||||||
$table->unsignedInteger('type')->default(EventHistoryType::created->value)
|
$table->unsignedInteger('type')->default(EventHistoryType::created->value)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Api\ApiToken;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('api_token_stats', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->date('date');
|
||||||
|
$table->unsignedBigInteger('nb_calls')->default(0);
|
||||||
|
$table->foreignIdFor(ApiToken::class)->constrained()->cascadeOnDelete();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('api_token_stats');
|
||||||
|
}
|
||||||
|
};
|
||||||
36
database/migrations/2022_07_28_144413_create_jobs_table.php
Normal file
36
database/migrations/2022_07_28_144413_create_jobs_table.php
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('jobs', function (Blueprint $table) {
|
||||||
|
$table->bigIncrements('id');
|
||||||
|
$table->string('queue')->index();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->unsignedTinyInteger('attempts');
|
||||||
|
$table->unsignedInteger('reserved_at')->nullable();
|
||||||
|
$table->unsignedInteger('available_at');
|
||||||
|
$table->unsignedInteger('created_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('jobs');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -15,7 +15,7 @@ class ApiTokenSeeder extends Seeder
|
||||||
*/
|
*/
|
||||||
public function run()
|
public function run()
|
||||||
{
|
{
|
||||||
DB::table('api_tokens')->truncate();
|
DB::table((new ApiToken())->getTable())->delete();
|
||||||
ApiToken::factory(10)->create();
|
ApiToken::factory(10)->create();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,8 @@
|
||||||
colors="true">
|
colors="true">
|
||||||
|
|
||||||
<testsuites>
|
<testsuites>
|
||||||
<testsuite name="Unit">
|
<testsuite name="API">
|
||||||
<directory suffix="Test.php">./tests/Unit</directory>
|
<directory suffix="Test.php">./tests/Api</directory>
|
||||||
</testsuite>
|
|
||||||
<testsuite name="Feature">
|
|
||||||
<directory suffix="Test.php">./tests/Feature</directory>
|
|
||||||
</testsuite>
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
<coverage processUncoveredFiles="true">
|
<coverage processUncoveredFiles="true">
|
||||||
|
|
|
||||||
|
|
@ -1 +1,24 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\ApiController;
|
||||||
|
use App\Http\Controllers\Api\MediaController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::name('api.')->group(function () {
|
||||||
|
|
||||||
|
// * PUBLIC
|
||||||
|
Route::get('/ping', [ApiController::class, 'ping'])->name('ping');
|
||||||
|
|
||||||
|
// * PRIVATE
|
||||||
|
Route::middleware('auth:api')->group(function () {
|
||||||
|
|
||||||
|
// * OPTINS
|
||||||
|
Route::get('/media/{apiMedia}', [MediaController::class, 'show'])->name('media.show');
|
||||||
|
Route::get('/media/{apiMediaArcadiaId}/byArcadiaId', [MediaController::class, 'show'])
|
||||||
|
->name('media.showByArcadiaId');
|
||||||
|
Route::get('/article/{apiNoArticle}/media', [MediaController::class, 'getMediaListByArticle'])
|
||||||
|
->name('articles.media.index');
|
||||||
|
Route::get('/media/{apiMedia}/picture', [MediaController::class, 'getMediaPicture'])
|
||||||
|
->name('media.picture');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
89
tests/Api/ApiBase.php
Normal file
89
tests/Api/ApiBase.php
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Api;
|
||||||
|
|
||||||
|
use App\Enums\Legacy\ApiStatus;
|
||||||
|
use App\Lib\Helper;
|
||||||
|
use App\Models\Api\ApiToken;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||||
|
use Illuminate\Testing\AssertableJsonString;
|
||||||
|
use Illuminate\Testing\TestResponse;
|
||||||
|
use Tests\CreatesApplication;
|
||||||
|
|
||||||
|
class ApiBase extends BaseTestCase
|
||||||
|
{
|
||||||
|
use CreatesApplication;
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* apiToken used to make calls
|
||||||
|
*
|
||||||
|
* @var \App\Models\Api\ApiToken|null
|
||||||
|
*/
|
||||||
|
protected $apiToken = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make assertion that json response has status.
|
||||||
|
*
|
||||||
|
* @param AssertableJsonString $json
|
||||||
|
* @param ApiStatus $status
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function assertApiResponseStatusIs(AssertableJsonString $json, ApiStatus $status)
|
||||||
|
{
|
||||||
|
$json->assertFragment(['status' => $status->value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw Function to call api
|
||||||
|
*
|
||||||
|
* @param string $method
|
||||||
|
* @param string $uri
|
||||||
|
* @param array $data
|
||||||
|
* @param array $headers
|
||||||
|
* @return \Illuminate\Testing\TestResponse
|
||||||
|
*/
|
||||||
|
protected function callApi(string $method, string $uri, array $data = [], array $headers = []): TestResponse
|
||||||
|
{
|
||||||
|
if (!isset($data['timestamp'])) {
|
||||||
|
$data['timestamp'] = \time();
|
||||||
|
}
|
||||||
|
return $this->json($method, $uri, $data, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw Function to call api with Token
|
||||||
|
*
|
||||||
|
* @param string $method
|
||||||
|
* @param string $uri
|
||||||
|
* @param array $data
|
||||||
|
* @param array $headers
|
||||||
|
* @return \Illuminate\Testing\TestResponse
|
||||||
|
*/
|
||||||
|
protected function callApiWithToken(
|
||||||
|
string $method,
|
||||||
|
string $uri,
|
||||||
|
array $data = [],
|
||||||
|
array $headers = []
|
||||||
|
): TestResponse {
|
||||||
|
if (!$this->apiToken) {
|
||||||
|
$this->apiToken = ApiToken::query()->inRandomOrder()->firstOrFail();
|
||||||
|
$publicToken = $this->apiToken->generatePublicToken();
|
||||||
|
$this->apiToken->saveOrFail();
|
||||||
|
$this->apiToken->public_token = $publicToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['bearer_token'] = $this->apiToken->public_token;
|
||||||
|
$data['timestamp'] = \gmdate('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$data['signature'] = Helper::signRequest(
|
||||||
|
$this->apiToken->private_token,
|
||||||
|
$uri,
|
||||||
|
$method,
|
||||||
|
$data
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->callApi($method, $uri, $data, $headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
186
tests/Api/ApiBaseTest.php
Normal file
186
tests/Api/ApiBaseTest.php
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Api;
|
||||||
|
|
||||||
|
use App\Enums\Legacy\ApiError;
|
||||||
|
use App\Enums\Legacy\ApiStatus;
|
||||||
|
use App\Models\Api\ApiToken;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Tests\CreatesApplication;
|
||||||
|
|
||||||
|
class ApiBaseTest extends ApiBase
|
||||||
|
{
|
||||||
|
use CreatesApplication;
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Api only handle json calls
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testOnlyJsonCallsAreHandled()
|
||||||
|
{
|
||||||
|
$response = $this->call('get', route('api.ping'), [
|
||||||
|
'timestamp' => \gmdate('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
$response->assertStatus(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test timestamp middleware works as expected.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testTimestampMiddleware()
|
||||||
|
{
|
||||||
|
$response = $this->json('get', route('api.ping'), [
|
||||||
|
'timestamp' => \gmdate('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
$json = $response->decodeResponseJson();
|
||||||
|
$this->assertApiResponseStatusIs($json, ApiStatus::OK());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp middleware should fail if timestamp parameter is missing.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testTimestampMiddlewareCheckFailMissing()
|
||||||
|
{
|
||||||
|
$response = $this->json('get', route('api.ping'), []);
|
||||||
|
$json = $response->decodeResponseJson();
|
||||||
|
$this->assertApiResponseStatusIs($json, ApiStatus::KO());
|
||||||
|
$json->assertFragment(['data' => [
|
||||||
|
'code' => ApiError::wrongTimeStamp()->value,
|
||||||
|
'message' => ApiError::wrongTimeStamp()->label
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp middleware should fail if timestamp parameter has wrong format.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testTimestampMiddlewareCheckFailWrongFormat()
|
||||||
|
{
|
||||||
|
$response = $this->json('get', route('api.ping'), ['timestamp' => \gmdate('Y/m/d')]);
|
||||||
|
$json = $response->decodeResponseJson();
|
||||||
|
$this->assertApiResponseStatusIs($json, ApiStatus::KO());
|
||||||
|
$json->assertFragment(['data' => [
|
||||||
|
'code' => ApiError::wrongTimeStamp()->value,
|
||||||
|
'message' => ApiError::wrongTimeStamp()->label
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp middleware should fail if timestamp is older than "config('api.timestampTimeout', 10)".
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testTimestampMiddlewareCheckFailIfTooOld()
|
||||||
|
{
|
||||||
|
$response = $this->json('get', route('api.ping'), [
|
||||||
|
'timestamp' => \gmdate('Y-m-d H:i:s', \time() - config('api.timestampTimeout', 10))
|
||||||
|
]);
|
||||||
|
$json = $response->decodeResponseJson();
|
||||||
|
$this->assertApiResponseStatusIs($json, ApiStatus::KO());
|
||||||
|
$json->assertFragment(['data' => [
|
||||||
|
'code' => ApiError::wrongTimeStamp()->value,
|
||||||
|
'message' => ApiError::wrongTimeStamp()->label
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test api route throttle.
|
||||||
|
*
|
||||||
|
* ! This has to be the last test of this class.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testApiThrottle()
|
||||||
|
{
|
||||||
|
for ($i = config('api.apiRequestRatePerMinute', 100); $i >= 0; $i--) {
|
||||||
|
$this->json('get', route('api.ping'), [
|
||||||
|
'timestamp' => \gmdate('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->json('get', route('api.ping'), [
|
||||||
|
'timestamp' => \gmdate('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
$json = $response->decodeResponseJson();
|
||||||
|
$this->assertApiResponseStatusIs($json, ApiStatus::KO());
|
||||||
|
$response->assertStatus(429);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test wrong http method call
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWrongMethodCall()
|
||||||
|
{
|
||||||
|
$response = $this->json('post', route('api.ping'), [
|
||||||
|
'timestamp' => \gmdate('Y-m-d H:i:s', \time())
|
||||||
|
]);
|
||||||
|
$json = $response->decodeResponseJson();
|
||||||
|
$this->assertApiResponseStatusIs($json, ApiStatus::KO());
|
||||||
|
$this->assertEquals(ApiError::methodNotAllowed()->value, $json->json('data.code'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Auth fail when no token
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testAuthFailNoToken()
|
||||||
|
{
|
||||||
|
$response = $this->json('get', route('api.media.show', [
|
||||||
|
'apiMedia' => Str::uuid()
|
||||||
|
]), [
|
||||||
|
'timestamp' => \gmdate('Y-m-d H:i:s', \time())
|
||||||
|
]);
|
||||||
|
$json = $response->decodeResponseJson();
|
||||||
|
$this->assertApiResponseStatusIs($json, ApiStatus::KO());
|
||||||
|
$this->assertEquals(ApiError::apiAuthFailed()->value, $json->json('data.code'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Auth fail when unknown token
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testAuthFailUnknownToken()
|
||||||
|
{
|
||||||
|
$response = $this->json('get', route('api.media.show', [
|
||||||
|
'apiMedia' => Str::uuid()
|
||||||
|
]), [
|
||||||
|
'bearer_token' => 'qsdqsdqsd',
|
||||||
|
'timestamp' => \gmdate('Y-m-d H:i:s', \time())
|
||||||
|
]);
|
||||||
|
$json = $response->decodeResponseJson();
|
||||||
|
$this->assertApiResponseStatusIs($json, ApiStatus::KO());
|
||||||
|
$this->assertEquals(ApiError::apiAuthFailed()->value, $json->json('data.code'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Auth fail when wrong signature
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testAuthFailWrongSignature()
|
||||||
|
{
|
||||||
|
$apiToken = ApiToken::query()->firstOrFail();
|
||||||
|
$response = $this->json('get', route('api.media.show', [
|
||||||
|
'apiMedia' => Str::uuid()
|
||||||
|
]), [
|
||||||
|
'bearer_token' => $apiToken->public_token,
|
||||||
|
'timestamp' => \gmdate('Y-m-d H:i:s', \time()),
|
||||||
|
'signature' => 'sdqdqsdqd'
|
||||||
|
]);
|
||||||
|
$json = $response->decodeResponseJson();
|
||||||
|
$this->assertApiResponseStatusIs($json, ApiStatus::KO());
|
||||||
|
$this->assertEquals(ApiError::apiAuthFailed()->value, $json->json('data.code'));
|
||||||
|
}
|
||||||
|
}
|
||||||
259
tests/Api/ApiMediaTest.php
Normal file
259
tests/Api/ApiMediaTest.php
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Api;
|
||||||
|
|
||||||
|
use App\Enums\Legacy\ApiError;
|
||||||
|
use App\Enums\Legacy\ApiStatus;
|
||||||
|
use App\Enums\MediaExtension;
|
||||||
|
use App\Enums\MediaStatut;
|
||||||
|
use App\Models\Article;
|
||||||
|
use App\Models\Media;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Testing\TestResponse;
|
||||||
|
use Symfony\Component\Mime\MimeTypes;
|
||||||
|
use Tests\CreatesApplication;
|
||||||
|
|
||||||
|
class ApiMediaTest extends ApiBase
|
||||||
|
{
|
||||||
|
use CreatesApplication;
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test get media by Id
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testMediaShowById()
|
||||||
|
{
|
||||||
|
\mt_srand();
|
||||||
|
$rand = \mt_rand(15, 60);
|
||||||
|
for ($i = 0; $i < $rand; $i++) {
|
||||||
|
\mt_srand();
|
||||||
|
$media = Media::query()->inRandomOrder(\mt_rand())
|
||||||
|
->where('statut', '!=', MediaStatut::brouillon)
|
||||||
|
->where('isDefault', false)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$response = $this->callApiWithToken('get', route('api.media.show', [
|
||||||
|
'apiMedia' => $media->id
|
||||||
|
]));
|
||||||
|
|
||||||
|
|
||||||
|
$this->assertResponseIsMediaResource($response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Could throw api error exception not found
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testMediaShowByArcadiaId()
|
||||||
|
{
|
||||||
|
\mt_srand();
|
||||||
|
$rand = \mt_rand(15, 60);
|
||||||
|
for ($i = 0; $i < $rand; $i++) {
|
||||||
|
\mt_srand();
|
||||||
|
$media = Media::query()->whereNotNull('arcadia_id')
|
||||||
|
->where('statut', '!=', MediaStatut::brouillon)
|
||||||
|
->where('isDefault', false)
|
||||||
|
->inRandomOrder(\mt_rand())->first();
|
||||||
|
|
||||||
|
$response = $this->callApiWithToken('get', route('api.media.showByArcadiaId', [
|
||||||
|
'apiMediaArcadiaId' => $media->arcadia_id
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertResponseIsMediaResource($response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test get media by Id not found
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testMediaShowNotFound()
|
||||||
|
{
|
||||||
|
$response = $this->callApiWithToken('get', route('api.media.show', [
|
||||||
|
'apiMedia' => '94984919191616161'
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertResponseIsNotFound($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test get media picture
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testMediaPicture()
|
||||||
|
{
|
||||||
|
\mt_srand();
|
||||||
|
$rand = \mt_rand(15, 60);
|
||||||
|
for ($i = 0; $i < $rand; $i++) {
|
||||||
|
\mt_srand();
|
||||||
|
$media = Media::query()->whereNotNull('arcadia_id')
|
||||||
|
->where('statut', '!=', MediaStatut::brouillon)
|
||||||
|
->where('isDefault', false)
|
||||||
|
->inRandomOrder(\mt_rand())->first();
|
||||||
|
|
||||||
|
$response = $this->callApiWithToken('get', route('api.media.picture', [
|
||||||
|
'apiMedia' => $media->id
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertResponseIsMediaPictureResource($response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test get media picture
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testMediaPictureNotFound()
|
||||||
|
{
|
||||||
|
\mt_srand();
|
||||||
|
$response = $this->callApiWithToken('get', route('api.media.picture', [
|
||||||
|
'apiMedia' => \mt_rand(9999999, 99999999999)
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertResponseIsNotFound($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test get media picture
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetMediaListByArticle()
|
||||||
|
{
|
||||||
|
\mt_srand();
|
||||||
|
$rand = \mt_rand(15, 60);
|
||||||
|
for ($i = 0; $i < $rand; $i++) {
|
||||||
|
\mt_srand();
|
||||||
|
$article = Article::query()
|
||||||
|
->inRandomOrder(\mt_rand())->first();
|
||||||
|
|
||||||
|
$response = $this->callApiWithToken('get', route('api.articles.media.index', [
|
||||||
|
'apiNoArticle' => $article->noArt
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertResponseIsMediaListResource($response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// * PRIVATE METHODS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert the response contains a not found message.
|
||||||
|
*
|
||||||
|
* @param TestResponse $response
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function assertResponseIsNotFound(TestResponse $response)
|
||||||
|
{
|
||||||
|
$json = $response->decodeResponseJson();
|
||||||
|
$this->assertApiResponseStatusIs($json, ApiStatus::KO());
|
||||||
|
$json->assertStructure(['data' => [
|
||||||
|
'code', 'message'
|
||||||
|
]], $json->json());
|
||||||
|
|
||||||
|
$this->assertEquals(ApiError::modelNotFound()->value, $json->json('data.code'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert the response contains a media list resource.
|
||||||
|
*
|
||||||
|
* @param TestResponse $response
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function assertResponseIsMediaListResource(TestResponse $response)
|
||||||
|
{
|
||||||
|
$json = $response->decodeResponseJson();
|
||||||
|
$this->assertApiResponseStatusIs($json, ApiStatus::OK());
|
||||||
|
$json->assertStructure([
|
||||||
|
'data' => [
|
||||||
|
'*' => [
|
||||||
|
'id', 'arcadia_id', 'libelle', 'uid',
|
||||||
|
'version', 'created_at', 'updated_at'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], $json->json());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert the response contains a media resource.
|
||||||
|
*
|
||||||
|
* @param TestResponse $response
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function assertResponseIsMediaResource(TestResponse $response)
|
||||||
|
{
|
||||||
|
$json = $response->decodeResponseJson();
|
||||||
|
$this->assertApiResponseStatusIs($json, ApiStatus::OK());
|
||||||
|
$json->assertStructure(['data' => [
|
||||||
|
'id', 'arcadia_id', 'libelle', 'uid',
|
||||||
|
'version', 'created_at', 'updated_at'
|
||||||
|
]], $json->json());
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Carbon::class, Carbon::parse($json->json('data.created_at')));
|
||||||
|
$this->assertInstanceOf(Carbon::class, Carbon::parse($json->json('data.updated_at')));
|
||||||
|
|
||||||
|
$this->assertDatabaseHas((new Media())->getTable(), [
|
||||||
|
'id' => $json->json('data.id'),
|
||||||
|
'arcadia_id' => $json->json('data.arcadia_id'),
|
||||||
|
'libelle' => $json->json('data.libelle'),
|
||||||
|
'uid' => $json->json('data.uid'),
|
||||||
|
'version' => $json->json('data.version'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert the response contains a media picture resource.
|
||||||
|
*
|
||||||
|
* @param TestResponse $response
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function assertResponseIsMediaPictureResource(TestResponse $response)
|
||||||
|
{
|
||||||
|
$json = $response->decodeResponseJson();
|
||||||
|
$this->assertApiResponseStatusIs($json, ApiStatus::OK());
|
||||||
|
$json->assertStructure(['data' => [
|
||||||
|
'base64_bin', 'uid', 'extension'
|
||||||
|
]], $json->json());
|
||||||
|
|
||||||
|
$this->assertTrue(
|
||||||
|
base64_encode(base64_decode($json->json('data.base64_bin'), true)) === $json->json('data.base64_bin'),
|
||||||
|
'Should be base64 encoded'
|
||||||
|
);
|
||||||
|
$this->assertTrue(
|
||||||
|
\in_array($json->json('data.extension'), collect(MediaExtension::cases())->pluck('value')->all()),
|
||||||
|
'Extension should exist and be a MediaExtension enum'
|
||||||
|
);
|
||||||
|
$this->assertDatabaseHas((new Media())->getTable(), [
|
||||||
|
'uid' => $json->json('data.uid'),
|
||||||
|
'extension' => $json->json('data.extension'),
|
||||||
|
]);
|
||||||
|
$tmpFilePath = 'tmp/' . $json->json('data.uid') . '.' . $json->json('data.extension');
|
||||||
|
|
||||||
|
Storage::put(
|
||||||
|
$tmpFilePath,
|
||||||
|
\base64_decode($json->json('data.base64_bin'))
|
||||||
|
);
|
||||||
|
$ext = $json->json('data.extension');
|
||||||
|
|
||||||
|
$mt = new MimeTypes();
|
||||||
|
$guessedExts = $mt->getExtensions($mt->guessMimeType(Storage::path($tmpFilePath)));
|
||||||
|
|
||||||
|
$strGuessedExts = \implode(',', $guessedExts);
|
||||||
|
|
||||||
|
Storage::delete($tmpFilePath);
|
||||||
|
|
||||||
|
$this->assertTrue(
|
||||||
|
\in_array($ext, $guessedExts),
|
||||||
|
"File guessed extensions '{$strGuessedExts}' must contain name extension '{$ext}'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature;
|
|
||||||
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class ExampleTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* A basic test example.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testExample()
|
|
||||||
{
|
|
||||||
$response = $this->get('/');
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit;
|
|
||||||
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class ExampleTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* A basic test example.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function testExample()
|
|
||||||
{
|
|
||||||
$this->assertTrue(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Reference in a new issue