mirror of https://github.com/4xmen/xshop.git
added swagger for project api
parent
9bc2cde901
commit
c9966474c7
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\OpenApi;
|
||||
|
||||
/**
|
||||
* @OA\Info(title="xShop API", version="1.0.0")
|
||||
*/
|
||||
class Swagger
|
||||
{
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,318 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'default' => 'default',
|
||||
'documentations' => [
|
||||
'default' => [
|
||||
'api' => [
|
||||
'title' => 'xShop Swagger UI',
|
||||
],
|
||||
|
||||
'routes' => [
|
||||
/*
|
||||
* Route for accessing api documentation interface
|
||||
*/
|
||||
'api' => 'api/documentation',
|
||||
],
|
||||
'paths' => [
|
||||
/*
|
||||
* Edit to include full URL in ui for assets
|
||||
*/
|
||||
'use_absolute_path' => env('L5_SWAGGER_USE_ABSOLUTE_PATH', true),
|
||||
|
||||
/*
|
||||
* Edit to set path where swagger ui assets should be stored
|
||||
*/
|
||||
'swagger_ui_assets_path' => env('L5_SWAGGER_UI_ASSETS_PATH', 'vendor/swagger-api/swagger-ui/dist/'),
|
||||
|
||||
/*
|
||||
* File name of the generated json documentation file
|
||||
*/
|
||||
'docs_json' => 'api-docs.json',
|
||||
|
||||
/*
|
||||
* File name of the generated YAML documentation file
|
||||
*/
|
||||
'docs_yaml' => 'api-docs.yaml',
|
||||
|
||||
/*
|
||||
* Set this to `json` or `yaml` to determine which documentation file to use in UI
|
||||
*/
|
||||
'format_to_use_for_docs' => env('L5_FORMAT_TO_USE_FOR_DOCS', 'json'),
|
||||
|
||||
/*
|
||||
* Absolute paths to directory containing the swagger annotations are stored.
|
||||
*/
|
||||
'annotations' => [
|
||||
base_path('app'),
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'defaults' => [
|
||||
'routes' => [
|
||||
/*
|
||||
* Route for accessing parsed swagger annotations.
|
||||
*/
|
||||
'docs' => 'docs',
|
||||
|
||||
/*
|
||||
* Route for Oauth2 authentication callback.
|
||||
*/
|
||||
'oauth2_callback' => 'api/oauth2-callback',
|
||||
|
||||
/*
|
||||
* Middleware allows to prevent unexpected access to API documentation
|
||||
*/
|
||||
'middleware' => [
|
||||
'api' => [],
|
||||
'asset' => [],
|
||||
'docs' => [],
|
||||
'oauth2_callback' => [],
|
||||
],
|
||||
|
||||
/*
|
||||
* Route Group options
|
||||
*/
|
||||
'group_options' => [],
|
||||
],
|
||||
|
||||
'paths' => [
|
||||
/*
|
||||
* Absolute path to location where parsed annotations will be stored
|
||||
*/
|
||||
'docs' => storage_path('api-docs'),
|
||||
|
||||
/*
|
||||
* Absolute path to directory where to export views
|
||||
*/
|
||||
'views' => base_path('resources/views/vendor/l5-swagger'),
|
||||
|
||||
/*
|
||||
* Edit to set the api's base path
|
||||
*/
|
||||
'base' => env('L5_SWAGGER_BASE_PATH', null),
|
||||
|
||||
/*
|
||||
* Absolute path to directories that should be excluded from scanning
|
||||
* @deprecated Please use `scanOptions.exclude`
|
||||
* `scanOptions.exclude` overwrites this
|
||||
*/
|
||||
'excludes' => [],
|
||||
],
|
||||
|
||||
'scanOptions' => [
|
||||
/**
|
||||
* Configuration for default processors. Allows to pass processors configuration to swagger-php.
|
||||
*
|
||||
* @link https://zircote.github.io/swagger-php/reference/processors.html
|
||||
*/
|
||||
'default_processors_configuration' => [
|
||||
/** Example */
|
||||
/**
|
||||
* 'operationId.hash' => true,
|
||||
* 'pathFilter' => [
|
||||
* 'tags' => [
|
||||
* '/pets/',
|
||||
* '/store/',
|
||||
* ],
|
||||
* ],.
|
||||
*/
|
||||
],
|
||||
|
||||
/**
|
||||
* analyser: defaults to \OpenApi\StaticAnalyser .
|
||||
*
|
||||
* @see \OpenApi\scan
|
||||
*/
|
||||
'analyser' => null,
|
||||
|
||||
/**
|
||||
* analysis: defaults to a new \OpenApi\Analysis .
|
||||
*
|
||||
* @see \OpenApi\scan
|
||||
*/
|
||||
'analysis' => null,
|
||||
|
||||
/**
|
||||
* Custom query path processors classes.
|
||||
*
|
||||
* @link https://github.com/zircote/swagger-php/tree/master/Examples/processors/schema-query-parameter
|
||||
* @see \OpenApi\scan
|
||||
*/
|
||||
'processors' => [
|
||||
// new \App\SwaggerProcessors\SchemaQueryParameter(),
|
||||
],
|
||||
|
||||
/**
|
||||
* pattern: string $pattern File pattern(s) to scan (default: *.php) .
|
||||
*
|
||||
* @see \OpenApi\scan
|
||||
*/
|
||||
'pattern' => null,
|
||||
|
||||
/*
|
||||
* Absolute path to directories that should be excluded from scanning
|
||||
* @note This option overwrites `paths.excludes`
|
||||
* @see \OpenApi\scan
|
||||
*/
|
||||
'exclude' => [],
|
||||
|
||||
/*
|
||||
* Allows to generate specs either for OpenAPI 3.0.0 or OpenAPI 3.1.0.
|
||||
* By default the spec will be in version 3.0.0
|
||||
*/
|
||||
'open_api_spec_version' => env('L5_SWAGGER_OPEN_API_SPEC_VERSION', \L5Swagger\Generator::OPEN_API_DEFAULT_SPEC_VERSION),
|
||||
],
|
||||
|
||||
/*
|
||||
* API security definitions. Will be generated into documentation file.
|
||||
*/
|
||||
'securityDefinitions' => [
|
||||
'securitySchemes' => [
|
||||
/*
|
||||
* Examples of Security schemes
|
||||
*/
|
||||
/*
|
||||
'api_key_security_example' => [ // Unique name of security
|
||||
'type' => 'apiKey', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2".
|
||||
'description' => 'A short description for security scheme',
|
||||
'name' => 'api_key', // The name of the header or query parameter to be used.
|
||||
'in' => 'header', // The location of the API key. Valid values are "query" or "header".
|
||||
],
|
||||
'oauth2_security_example' => [ // Unique name of security
|
||||
'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2".
|
||||
'description' => 'A short description for oauth2 security scheme.',
|
||||
'flow' => 'implicit', // The flow used by the OAuth2 security scheme. Valid values are "implicit", "password", "application" or "accessCode".
|
||||
'authorizationUrl' => 'http://example.com/auth', // The authorization URL to be used for (implicit/accessCode)
|
||||
//'tokenUrl' => 'http://example.com/auth' // The authorization URL to be used for (password/application/accessCode)
|
||||
'scopes' => [
|
||||
'read:projects' => 'read your projects',
|
||||
'write:projects' => 'modify projects in your account',
|
||||
]
|
||||
],
|
||||
*/
|
||||
|
||||
/* Open API 3.0 support
|
||||
'passport' => [ // Unique name of security
|
||||
'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2".
|
||||
'description' => 'Laravel passport oauth2 security.',
|
||||
'in' => 'header',
|
||||
'scheme' => 'https',
|
||||
'flows' => [
|
||||
"password" => [
|
||||
"authorizationUrl" => config('app.url') . '/oauth/authorize',
|
||||
"tokenUrl" => config('app.url') . '/oauth/token',
|
||||
"refreshUrl" => config('app.url') . '/token/refresh',
|
||||
"scopes" => []
|
||||
],
|
||||
],
|
||||
],
|
||||
'sanctum' => [ // Unique name of security
|
||||
'type' => 'apiKey', // Valid values are "basic", "apiKey" or "oauth2".
|
||||
'description' => 'Enter token in format (Bearer <token>)',
|
||||
'name' => 'Authorization', // The name of the header or query parameter to be used.
|
||||
'in' => 'header', // The location of the API key. Valid values are "query" or "header".
|
||||
],
|
||||
*/
|
||||
],
|
||||
'security' => [
|
||||
/*
|
||||
* Examples of Securities
|
||||
*/
|
||||
[
|
||||
/*
|
||||
'oauth2_security_example' => [
|
||||
'read',
|
||||
'write'
|
||||
],
|
||||
|
||||
'passport' => []
|
||||
*/
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
* Set this to `true` in development mode so that docs would be regenerated on each request
|
||||
* Set this to `false` to disable swagger generation on production
|
||||
*/
|
||||
'generate_always' => env('L5_SWAGGER_GENERATE_ALWAYS', false),
|
||||
|
||||
/*
|
||||
* Set this to `true` to generate a copy of documentation in yaml format
|
||||
*/
|
||||
'generate_yaml_copy' => env('L5_SWAGGER_GENERATE_YAML_COPY', false),
|
||||
|
||||
/*
|
||||
* Edit to trust the proxy's ip address - needed for AWS Load Balancer
|
||||
* string[]
|
||||
*/
|
||||
'proxy' => false,
|
||||
|
||||
/*
|
||||
* Configs plugin allows to fetch external configs instead of passing them to SwaggerUIBundle.
|
||||
* See more at: https://github.com/swagger-api/swagger-ui#configs-plugin
|
||||
*/
|
||||
'additional_config_url' => null,
|
||||
|
||||
/*
|
||||
* Apply a sort to the operation list of each API. It can be 'alpha' (sort by paths alphanumerically),
|
||||
* 'method' (sort by HTTP method).
|
||||
* Default is the order returned by the server unchanged.
|
||||
*/
|
||||
'operations_sort' => env('L5_SWAGGER_OPERATIONS_SORT', null),
|
||||
|
||||
/*
|
||||
* Pass the validatorUrl parameter to SwaggerUi init on the JS side.
|
||||
* A null value here disables validation.
|
||||
*/
|
||||
'validator_url' => null,
|
||||
|
||||
/*
|
||||
* Swagger UI configuration parameters
|
||||
*/
|
||||
'ui' => [
|
||||
'display' => [
|
||||
'dark_mode' => env('L5_SWAGGER_UI_DARK_MODE', false),
|
||||
/*
|
||||
* Controls the default expansion setting for the operations and tags. It can be :
|
||||
* 'list' (expands only the tags),
|
||||
* 'full' (expands the tags and operations),
|
||||
* 'none' (expands nothing).
|
||||
*/
|
||||
'doc_expansion' => env('L5_SWAGGER_UI_DOC_EXPANSION', 'none'),
|
||||
|
||||
/**
|
||||
* If set, enables filtering. The top bar will show an edit box that
|
||||
* you can use to filter the tagged operations that are shown. Can be
|
||||
* Boolean to enable or disable, or a string, in which case filtering
|
||||
* will be enabled using that string as the filter expression. Filtering
|
||||
* is case-sensitive matching the filter expression anywhere inside
|
||||
* the tag.
|
||||
*/
|
||||
'filter' => env('L5_SWAGGER_UI_FILTERS', true), // true | false
|
||||
],
|
||||
|
||||
'authorization' => [
|
||||
/*
|
||||
* If set to true, it persists authorization data, and it would not be lost on browser close/refresh
|
||||
*/
|
||||
'persist_authorization' => env('L5_SWAGGER_UI_PERSIST_AUTHORIZATION', false),
|
||||
|
||||
'oauth2' => [
|
||||
/*
|
||||
* If set to true, adds PKCE to AuthorizationCodeGrant flow
|
||||
*/
|
||||
'use_pkce_with_authorization_code_grant' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
/*
|
||||
* Constants which can be used in annotations
|
||||
*/
|
||||
'constants' => [
|
||||
'L5_SWAGGER_CONST_HOST' => env('L5_SWAGGER_CONST_HOST', 'http://my-default-host.com'),
|
||||
],
|
||||
],
|
||||
];
|
@ -0,0 +1,164 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{config('l5-swagger.documentations.'.$documentation.'.api.title')}}</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ l5_swagger_asset($documentation, 'swagger-ui.css') }}">
|
||||
<link rel="icon" type="image/png" href="{{ l5_swagger_asset($documentation, 'favicon-32x32.png') }}" sizes="32x32"/>
|
||||
<link rel="icon" type="image/png" href="{{ l5_swagger_asset($documentation, 'favicon-16x16.png') }}" sizes="16x16"/>
|
||||
<style>
|
||||
html
|
||||
{
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
*,
|
||||
*:before,
|
||||
*:after
|
||||
{
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
margin:0;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
@if(config('l5-swagger.defaults.ui.display.dark_mode'))
|
||||
<style>
|
||||
body#dark-mode,
|
||||
#dark-mode .scheme-container {
|
||||
background: #1b1b1b;
|
||||
}
|
||||
#dark-mode .scheme-container,
|
||||
#dark-mode .opblock .opblock-section-header{
|
||||
box-shadow: 0 1px 2px 0 rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
#dark-mode .operation-filter-input,
|
||||
#dark-mode .dialog-ux .modal-ux,
|
||||
#dark-mode input[type=email],
|
||||
#dark-mode input[type=file],
|
||||
#dark-mode input[type=password],
|
||||
#dark-mode input[type=search],
|
||||
#dark-mode input[type=text],
|
||||
#dark-mode textarea{
|
||||
background: #343434;
|
||||
color: #e7e7e7;
|
||||
}
|
||||
#dark-mode .title,
|
||||
#dark-mode li,
|
||||
#dark-mode p,
|
||||
#dark-mode table,
|
||||
#dark-mode label,
|
||||
#dark-mode .opblock-tag,
|
||||
#dark-mode .opblock .opblock-summary-operation-id,
|
||||
#dark-mode .opblock .opblock-summary-path,
|
||||
#dark-mode .opblock .opblock-summary-path__deprecated,
|
||||
#dark-mode h1,
|
||||
#dark-mode h2,
|
||||
#dark-mode h3,
|
||||
#dark-mode h4,
|
||||
#dark-mode h5,
|
||||
#dark-mode .btn,
|
||||
#dark-mode .tab li,
|
||||
#dark-mode .parameter__name,
|
||||
#dark-mode .parameter__type,
|
||||
#dark-mode .prop-format,
|
||||
#dark-mode .loading-container .loading:after{
|
||||
color: #e7e7e7;
|
||||
}
|
||||
#dark-mode .opblock-description-wrapper p,
|
||||
#dark-mode .opblock-external-docs-wrapper p,
|
||||
#dark-mode .opblock-title_normal p,
|
||||
#dark-mode .response-col_status,
|
||||
#dark-mode table thead tr td,
|
||||
#dark-mode table thead tr th,
|
||||
#dark-mode .response-col_links,
|
||||
#dark-mode .swagger-ui{
|
||||
color: wheat;
|
||||
}
|
||||
#dark-mode .parameter__extension,
|
||||
#dark-mode .parameter__in,
|
||||
#dark-mode .model-title{
|
||||
color: #949494;
|
||||
}
|
||||
#dark-mode table thead tr td,
|
||||
#dark-mode table thead tr th{
|
||||
border-color: rgba(120,120,120,.2);
|
||||
}
|
||||
#dark-mode .opblock .opblock-section-header{
|
||||
background: transparent;
|
||||
}
|
||||
#dark-mode .opblock.opblock-post{
|
||||
background: rgba(73,204,144,.25);
|
||||
}
|
||||
#dark-mode .opblock.opblock-get{
|
||||
background: rgba(97,175,254,.25);
|
||||
}
|
||||
#dark-mode .opblock.opblock-put{
|
||||
background: rgba(252,161,48,.25);
|
||||
}
|
||||
#dark-mode .opblock.opblock-delete{
|
||||
background: rgba(249,62,62,.25);
|
||||
}
|
||||
#dark-mode .loading-container .loading:before{
|
||||
border-color: rgba(255,255,255,10%);
|
||||
border-top-color: rgba(255,255,255,.6);
|
||||
}
|
||||
#dark-mode svg:not(:root){
|
||||
fill: #e7e7e7;
|
||||
}
|
||||
</style>
|
||||
@endif
|
||||
</head>
|
||||
|
||||
<body @if(config('l5-swagger.defaults.ui.display.dark_mode')) id="dark-mode" @endif>
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<script src="{{ l5_swagger_asset($documentation, 'swagger-ui-bundle.js') }}"></script>
|
||||
<script src="{{ l5_swagger_asset($documentation, 'swagger-ui-standalone-preset.js') }}"></script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
// Build a system
|
||||
const ui = SwaggerUIBundle({
|
||||
dom_id: '#swagger-ui',
|
||||
url: "{!! $urlToDocs !!}",
|
||||
operationsSorter: {!! isset($operationsSorter) ? '"' . $operationsSorter . '"' : 'null' !!},
|
||||
configUrl: {!! isset($configUrl) ? '"' . $configUrl . '"' : 'null' !!},
|
||||
validatorUrl: {!! isset($validatorUrl) ? '"' . $validatorUrl . '"' : 'null' !!},
|
||||
oauth2RedirectUrl: "{{ route('l5-swagger.'.$documentation.'.oauth2_callback', [], $useAbsolutePath) }}",
|
||||
|
||||
requestInterceptor: function(request) {
|
||||
request.headers['X-CSRF-TOKEN'] = '{{ csrf_token() }}';
|
||||
return request;
|
||||
},
|
||||
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
|
||||
layout: "StandaloneLayout",
|
||||
docExpansion : "{!! config('l5-swagger.defaults.ui.display.doc_expansion', 'none') !!}",
|
||||
deepLinking: true,
|
||||
filter: {!! config('l5-swagger.defaults.ui.display.filter') ? 'true' : 'false' !!},
|
||||
persistAuthorization: "{!! config('l5-swagger.defaults.ui.authorization.persist_authorization') ? 'true' : 'false' !!}",
|
||||
|
||||
})
|
||||
|
||||
window.ui = ui
|
||||
|
||||
@if(in_array('oauth2', array_column(config('l5-swagger.defaults.securityDefinitions.securitySchemes'), 'type')))
|
||||
ui.initOAuth({
|
||||
usePkceWithAuthorizationCodeGrant: "{!! (bool)config('l5-swagger.defaults.ui.authorization.oauth2.use_pkce_with_authorization_code_grant') !!}"
|
||||
})
|
||||
@endif
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,163 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "xShop API",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"paths": {
|
||||
"/api/v1": {},
|
||||
"/api/v1/categories": {
|
||||
"get": {
|
||||
"summary": "Get list of categories",
|
||||
"operationId": "52c3c8d702572c2b4347b0ecf7573e54",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A list of categories"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/category/{category}": {
|
||||
"get": {
|
||||
"summary": "Get category",
|
||||
"operationId": "76dbdac414f00d4e4fe1e4be92166a85",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "category",
|
||||
"in": "path",
|
||||
"description": "Slug of one category",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "sub products per page",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A category with datas"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/groups": {
|
||||
"get": {
|
||||
"summary": "Get list of groups",
|
||||
"operationId": "2714891154ddf29a81395feb023c1c88",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A list of categories"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/group/{group}": {
|
||||
"get": {
|
||||
"summary": "Get category",
|
||||
"operationId": "e80ca23d059084d3b1a92295162b9103",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "group",
|
||||
"in": "path",
|
||||
"description": "Slug of one group",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"description": "sub posts per page",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A group with datas"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/products": {
|
||||
"get": {
|
||||
"summary": "Get list of products",
|
||||
"operationId": "072fd15d3be55577bb08c19d19f1e75c",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sort",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"new",
|
||||
"old",
|
||||
"most_view",
|
||||
"less_view",
|
||||
"most_buy",
|
||||
"less_buy"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "category",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "min_price",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "max_price",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A list of products"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue