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:
|
||||
- Remplace par VUE 3 + Runtime Only
|
||||
- Typescript
|
||||
- Faut-il exporter les stats Api avant de supprimer un ApiToken ?
|
||||
|
||||
## Installation
|
||||
### - Default
|
||||
|
|
@ -12,20 +13,10 @@ composer install
|
|||
cp .env.example .env
|
||||
php artisan key:generate
|
||||
```
|
||||
### - Stack en attendant le bump Laravel
|
||||
### - Stack Laravel
|
||||
```
|
||||
npm install && npm run prod
|
||||
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
|
||||
php artisan key:generate
|
||||
```
|
||||
|
|
@ -36,6 +27,9 @@ php artisan key:generate
|
|||
.env rajouter PROJECT_UUID=
|
||||
.env rajouter MAIL_LIST_SERVICEWEB=equipe.web@ldc.fr
|
||||
|
||||
# SUPERVISOR
|
||||
jobs Api stats sur queue `database`
|
||||
|
||||
## Dans les LABs
|
||||
```
|
||||
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;
|
||||
|
||||
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 Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
|
|
@ -12,14 +25,18 @@ class Handler extends ExceptionHandler
|
|||
*
|
||||
* @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.
|
||||
*
|
||||
* @var array<int, class-string<\Throwable>>
|
||||
*/
|
||||
protected $dontReport = [];
|
||||
protected $dontReport = [
|
||||
ApiErrorException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) .
|
||||
// ! 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);
|
||||
}
|
||||
|
||||
|
|
@ -626,6 +626,7 @@ class MediaController extends Controller
|
|||
*
|
||||
* @param \App\Models\Media $media
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @throws \Illuminate\Validation\ValidationException If the media is already linked to Arcadia.
|
||||
*/
|
||||
public function createArcadiaLink(Media $media): JsonResponse
|
||||
{
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ class UploadController extends Controller
|
|||
// 400 instead of 422 is for resumablejs to handle properly.
|
||||
return \response()->json($e->errors(), 400);
|
||||
}
|
||||
}
|
||||
}//end if
|
||||
|
||||
// We are in chunk mode, lets send the current progress.
|
||||
/** @var AbstractHandler $handler */
|
||||
|
|
@ -156,7 +156,7 @@ class UploadController extends Controller
|
|||
// 400 instead of 422 is for resumablejs to handle properly.
|
||||
return \response()->json($e->errors(), 400);
|
||||
}
|
||||
}
|
||||
}//end if
|
||||
|
||||
// We are in chunk mode, lets send the current progress.
|
||||
/** @var AbstractHandler $handler */
|
||||
|
|
@ -190,6 +190,7 @@ class UploadController extends Controller
|
|||
*
|
||||
* @param \Illuminate\Http\UploadedFile $file
|
||||
* @return void
|
||||
* @throws \Illuminate\Validation\ValidationException If file extension could not be confirmed.
|
||||
*/
|
||||
private function validateExtension(UploadedFile $file)
|
||||
{
|
||||
|
|
@ -203,16 +204,19 @@ class UploadController extends Controller
|
|||
$mediaExtension = MediaExtension::from($file->getClientOriginalExtension());
|
||||
|
||||
if ($file->guessExtension()) {
|
||||
$mt = new MimeTypes();
|
||||
$mt = new MimeTypes();
|
||||
$guessedExts = $mt->getExtensions($mt->guessMimeType($file->path()));
|
||||
|
||||
if (!\in_array($mediaExtension->value, $guessedExts)) {
|
||||
$guessedExts = \implode(',', $guessedExts);
|
||||
throw ValidationException::withMessages([
|
||||
'file' => trans('File guessed extensions :guessedExts does not match file name extension :extension', [
|
||||
'guessedExt' => $guessedExts,
|
||||
'extension' => $mediaExtension,
|
||||
])
|
||||
'file' => trans(
|
||||
'File guessed extensions :guessedExts does not match file name extension :extension',
|
||||
[
|
||||
'guessedExt' => $guessedExts,
|
||||
'extension' => $mediaExtension,
|
||||
]
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,10 @@ class Kernel extends HttpKernel
|
|||
|
||||
'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)
|
||||
{
|
||||
// phpcs:enable
|
||||
if (! $request->expectsJson()) {
|
||||
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
|
||||
]);
|
||||
|
||||
$parameters = collect(Arr::flatten($parameters));
|
||||
$parameters = collect(Arr::dot($parameters));
|
||||
|
||||
// * Remove signature from parameters.
|
||||
$parameters->pull('signature');
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class HistorizeMediaEvent
|
|||
* @return void
|
||||
* @throws \RuntimeException If saving historic to database failed.
|
||||
*/
|
||||
public function handle(MediaCreated | MediaLinkWithArcadia $event)
|
||||
public function handle(MediaCreated | MediaLinkWithArcadia $event)
|
||||
{
|
||||
$historique = new Historique();
|
||||
$type = EventHistoryType::created;
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
namespace App\Models\Api;
|
||||
|
||||
use App\Jobs\SetApiTokenStats;
|
||||
use App\Lib\Helper;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Str;
|
||||
|
|
@ -26,6 +28,24 @@ class ApiToken extends Model
|
|||
'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.
|
||||
*
|
||||
|
|
@ -57,6 +77,19 @@ class ApiToken extends Model
|
|||
|
||||
// * METHODS
|
||||
|
||||
/**
|
||||
* Increment API CRM Stat
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function incrementStat(): void
|
||||
{
|
||||
SetApiTokenStats::dispatch($this)
|
||||
->afterResponse()
|
||||
->onConnection('database')
|
||||
->onQueue('stats');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates new public token
|
||||
*
|
||||
|
|
@ -84,6 +117,26 @@ class ApiToken extends Model
|
|||
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
|
||||
*
|
||||
|
|
|
|||
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()
|
||||
{
|
||||
return $this->belongsToMany(Media::class);
|
||||
return $this->belongsToMany(Media::class, 'media_article', 'media_id', 'article_id');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@
|
|||
|
||||
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\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
|
@ -21,5 +26,46 @@ class AuthServiceProvider extends ServiceProvider
|
|||
public function boot()
|
||||
{
|
||||
$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;
|
||||
|
||||
use App\Enums\MediaStatut;
|
||||
use App\Models\Article;
|
||||
use App\Models\Media;
|
||||
use App\Models\Panier;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
|
|
@ -44,7 +46,29 @@ class RouteServiceProvider extends ServiceProvider
|
|||
});
|
||||
Route::bind('basket_uuid', function ($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()
|
||||
{
|
||||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
|
||||
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",
|
||||
"description": "The Laravel Framework.",
|
||||
"description": "Big freaking media manager (because arcadia sucks)",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"laravel"
|
||||
"application",
|
||||
"media"
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
|
|
@ -16,6 +17,7 @@
|
|||
"elasticquent/elasticquent": "dev-master",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"james-heinrich/getid3": "^1.9",
|
||||
"kwaadpepper/enum": "^2.0",
|
||||
"kwaadpepper/exception-handler": "^0.0.8",
|
||||
"laravel/framework": "^9.2",
|
||||
"laravel/sanctum": "^2.14.1",
|
||||
|
|
@ -50,6 +52,20 @@
|
|||
}
|
||||
},
|
||||
"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": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@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",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "77f343483e87b0d4403a8b8209b33bd9",
|
||||
"content-hash": "d3caf1a99a48f4de736a4a4f8aa14124",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adldap2/adldap2",
|
||||
|
|
@ -1758,6 +1758,62 @@
|
|||
},
|
||||
"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",
|
||||
"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',
|
||||
'provider' => 'users',
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'driver' => 'api-hmac'
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -1,41 +1,37 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
* The storage config
|
||||
*/
|
||||
|
||||
// * The storage config
|
||||
'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',
|
||||
'disk' => 'chunk_upload',
|
||||
],
|
||||
'clear' => [
|
||||
/*
|
||||
* How old chunks we should delete
|
||||
*/
|
||||
// * How old chunks we should delete
|
||||
'timestamp' => '-3 HOURS',
|
||||
'schedule' => [
|
||||
'enabled' => true,
|
||||
'cron' => '25 * * * *', // run every hour on the 25th minute
|
||||
// * run every hour on the 25th minute
|
||||
'cron' => '25 * * * *',
|
||||
],
|
||||
],
|
||||
'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' => [
|
||||
'use' => [
|
||||
'session' => true, // 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?
|
||||
// * should the chunk name use the session id? The uploader must send cookie!,
|
||||
'session' => true,
|
||||
// * instead of session we can use the ip and browser?
|
||||
'browser' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
'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' => [],
|
||||
// Overrides the list of handlers - use only what you really want
|
||||
'override' => [
|
||||
// \Pion\Laravel\ChunkUpload\Handler\DropZoneUploadHandler::class
|
||||
],
|
||||
// * Overrides the list of handlers - use only what you really want
|
||||
'override' => [],
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -23,8 +23,14 @@ return new class extends Migration
|
|||
File::link($newPath, $newLink);
|
||||
(new ConsoleOutput())->writeln("Lien symbolique créé de $oldPath vers $newPath");
|
||||
}
|
||||
Media::withTrashed()->update(['folder' => DB::raw("REPLACE(folder, 'medias/', '')")]);
|
||||
Media::withTrashed()->update(['folder' => DB::raw("REPLACE(folder, '/', '')")]);
|
||||
Media::withTrashed()->update([
|
||||
'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');
|
||||
}
|
||||
} //end switch
|
||||
$media->timestamps = false;
|
||||
$media->saveQuietly();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ return new class extends Migration
|
|||
]));
|
||||
return $historique;
|
||||
})->filter()->map(function (Historique $historique) {
|
||||
$historique->saveOrFail();
|
||||
$historique->timestamps = false;
|
||||
$historique->saveQuietly();
|
||||
});
|
||||
Schema::table('historiques', function (Blueprint $table) {
|
||||
$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()
|
||||
{
|
||||
DB::table('api_tokens')->truncate();
|
||||
DB::table((new ApiToken())->getTable())->delete();
|
||||
ApiToken::factory(10)->create();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,8 @@
|
|||
colors="true">
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory suffix="Test.php">./tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory suffix="Test.php">./tests/Feature</directory>
|
||||
<testsuite name="API">
|
||||
<directory suffix="Test.php">./tests/Api</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<coverage processUncoveredFiles="true">
|
||||
|
|
|
|||
|
|
@ -1 +1,24 @@
|
|||
<?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