Add Insomnia export and dynamic hostname for API doc

Insomnia export (/api/doc/insomnia.json):
- Generates Insomnia v4 export format with all API routes
- Workspace with environment variables (base_url, env, email, password, jwt_token)
- Folders per section (Auth, Events, Categories, Billets, Scanner)
- Each request with correct method, URL with Insomnia template vars, headers, body
- Auth routes use base_url directly, others use base_url/api/{env}/...
- Download button (indigo) next to Spec JSON button

Dynamic hostname:
- Insomnia export uses request.getSchemeAndHttpHost() (not hardcoded)
- Template passes host via data-host attribute
- JS env switcher reads host from data-host or falls back to location.origin
- Base URLs update dynamically when switching sandbox/live

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-23 19:35:36 +01:00
parent e6b410e715
commit bb35e0d8ae
3 changed files with 134 additions and 24 deletions

View File

@@ -1,24 +1,26 @@
const ENVS = {
sandbox: {
prefix: '/api/sandbox',
baseUrl: 'https://ticket.e-cosplay.fr/api/sandbox',
color: 'text-orange-400',
btnBg: 'bg-orange-500',
desc: 'Environnement de test. Les donnees ne sont pas modifiees.',
},
live: {
prefix: '/api/live',
baseUrl: 'https://ticket.e-cosplay.fr/api/live',
color: 'text-green-400',
btnBg: 'bg-green-600',
desc: 'Environnement de production. Les donnees sont reelles.',
},
}
const BTN_BASE = 'env-btn px-5 py-2 font-black uppercase text-xs tracking-widest transition-all cursor-pointer '
function switchEnv(env) {
const config = ENVS[env]
function getEnvs(host) {
return {
sandbox: {
prefix: '/api/sandbox',
baseUrl: host + '/api/sandbox',
color: 'text-orange-400',
btnBg: 'bg-orange-500',
desc: 'Environnement de test. Les donnees ne sont pas modifiees.',
},
live: {
prefix: '/api/live',
baseUrl: host + '/api/live',
color: 'text-green-400',
btnBg: 'bg-green-600',
desc: 'Environnement de production. Les donnees sont reelles.',
},
}
}
function switchEnv(env, envs) {
const config = envs[env]
if (!config) return
document.querySelectorAll('.env-btn').forEach(btn => {
@@ -42,7 +44,11 @@ export function initApiEnvSwitcher() {
const switcher = document.getElementById('env-switcher')
if (!switcher) return
const hostEl = document.querySelector('[data-host]')
const host = hostEl ? hostEl.dataset.host : globalThis.location.origin
const envs = getEnvs(host)
document.querySelectorAll('.env-btn').forEach(btn => {
btn.addEventListener('click', () => switchEnv(btn.dataset.env))
btn.addEventListener('click', () => switchEnv(btn.dataset.env, envs))
})
}

View File

@@ -3,6 +3,7 @@
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
@@ -23,6 +24,105 @@ class ApiDocController extends AbstractController
return $this->json($this->getApiSpec());
}
#[Route('/api/doc/insomnia.json', name: 'app_api_doc_insomnia', methods: ['GET'])]
public function insomnia(Request $request): Response
{
$baseUrl = $request->getSchemeAndHttpHost();
$resources = [];
$workspaceId = 'wrk_eticket';
$resources[] = [
'_type' => 'workspace',
'_id' => $workspaceId,
'name' => 'E-Ticket API',
'description' => 'API E-Ticket - Organisateur & Scanner',
];
$envId = 'env_eticket';
$resources[] = [
'_type' => 'environment',
'_id' => $envId,
'parentId' => $workspaceId,
'name' => 'E-Ticket',
'data' => [
'base_url' => $baseUrl,
'env' => 'sandbox',
'email' => '',
'password' => '',
'jwt_token' => '',
],
];
$folderIndex = 0;
foreach ($this->getApiSpec() as $section) {
++$folderIndex;
$folderId = 'fld_'.$folderIndex;
$resources[] = [
'_type' => 'request_group',
'_id' => $folderId,
'parentId' => $workspaceId,
'name' => $section['name'],
];
foreach ($section['endpoints'] as $reqIndex => $endpoint) {
$isAuth = str_starts_with($endpoint['path'], '/api/auth');
$url = $isAuth
? '{{ _.base_url }}'.$endpoint['path']
: '{{ _.base_url }}/api/{{ _.env }}'.str_replace('/api', '', $endpoint['path']);
$headers = [];
$headers[] = ['name' => 'Content-Type', 'value' => 'application/json'];
if (!$isAuth) {
$headers[] = ['name' => 'ETicket-Email', 'value' => '{{ _.email }}'];
$headers[] = ['name' => 'ETicket-JWT', 'value' => '{{ _.jwt_token }}'];
}
$body = null;
if ($endpoint['request']) {
$example = [];
foreach ($endpoint['request'] as $name => $field) {
$example[$name] = $field['example'] ?? '';
}
$body = [
'mimeType' => 'application/json',
'text' => json_encode($example, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE),
];
}
$resources[] = [
'_type' => 'request',
'_id' => 'req_'.$folderIndex.'_'.$reqIndex,
'parentId' => $folderId,
'name' => $endpoint['summary'],
'method' => $endpoint['method'],
'url' => $url,
'headers' => $headers,
'body' => $body,
];
}
}
$export = [
'_type' => 'export',
'__export_format' => 4,
'__export_date' => date('Y-m-d\TH:i:s\Z'),
'__export_source' => 'eticket.api.doc',
'resources' => $resources,
];
$response = new Response(
json_encode($export, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES),
200,
[
'Content-Type' => 'application/json',
'Content-Disposition' => 'attachment; filename="eticket-api-insomnia.json"',
]
);
return $response;
}
/**
* @return list<array{name: string, slug: string, baseUrl: string, description: string, badge: string, badgeColor: string}>
*/

View File

@@ -38,9 +38,9 @@
</button>
</div>
</div>
<div class="mt-3 border-2 border-gray-700 bg-gray-800 px-4 py-3 flex items-center gap-3">
<div class="mt-3 border-2 border-gray-700 bg-gray-800 px-4 py-3 flex items-center gap-3" data-host="{{ app.request.schemeAndHttpHost }}">
<p class="text-[10px] font-black uppercase tracking-widest text-gray-500">Base URL</p>
<p class="font-mono font-bold text-sm text-[#fabf04]" id="env-base-url">https://ticket.e-cosplay.fr/api/sandbox</p>
<p class="font-mono font-bold text-sm text-[#fabf04]" id="env-base-url">{{ app.request.schemeAndHttpHost }}/api/sandbox</p>
</div>
<p class="mt-2 text-xs font-bold text-gray-400" id="env-description">Environnement de test. Les donnees ne sont pas modifiees.</p>
<p class="mt-2 text-xs font-bold text-gray-500">L'authentification (<span class="font-mono">/api/auth/login</span>) est commune aux deux environnements (vos vrais identifiants).</p>
@@ -64,10 +64,14 @@
</div>
</div>
<div class="mt-8">
<div class="mt-8 flex flex-wrap gap-3">
<a href="{{ path('app_api_doc_json') }}" target="_blank" class="inline-flex items-center gap-2 px-6 py-3 border-4 border-[#fabf04] bg-[#fabf04] text-gray-900 font-black uppercase text-xs tracking-widest shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:shadow-[8px_8px_0px_rgba(0,0,0,1)] hover:translate-y-[-2px] transition-all">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
Voir la spec JSON
Spec JSON
</a>
<a href="{{ path('app_api_doc_insomnia') }}" class="inline-flex items-center gap-2 px-6 py-3 border-4 border-indigo-600 bg-indigo-600 text-white font-black uppercase text-xs tracking-widest shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:shadow-[8px_8px_0px_rgba(0,0,0,1)] hover:translate-y-[-2px] transition-all">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
Insomnia
</a>
</div>
</div>