Fix SonarQube issues, store sessions in Redis, use direct analytics URLs

- ApiSandboxController: reduce scan() returns from 4 to 3 via ternary
- ApiDocController: add MIME_JSON constant, extract buildInsomniaRequest()
  and buildInsomniaBody() to reduce cognitive complexity
- Store sessions in Redis to fix SSO disconnect with 2 PHP replicas
  (round-robin load balancing caused session loss on filesystem storage)
- Configure session cookie: 24h lifetime, secure auto, samesite lax
- Replace Caddy analytics proxies (/stats/*, /assets/perf.js, /sperf)
  with direct URLs to tools-security.esy-web.dev and cloudflareinsights.com
- Update JS tests for new direct analytics URLs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-24 09:21:19 +01:00
parent 867eaadddf
commit d44e75e3fd
9 changed files with 94 additions and 86 deletions

4
.env
View File

@@ -32,6 +32,10 @@ DATABASE_URL="postgresql://app:secret@database:5432/ecosplay?serverVersion=16&ch
MESSENGER_TRANSPORT_DSN=redis://redis:6379/messages
###< symfony/messenger ###
###> session ###
SESSION_HANDLER_DSN=redis://redis:6379/sessions
###< session ###
###> symfony/mailer ###
MAILER_DSN=smtp://mailpit:1025
###< symfony/mailer ###

View File

@@ -12,5 +12,6 @@ STRIPE_WEBHOOK_SECRET=whsec_test
STRIPE_WEBHOOK_SECRET_CONNECT=whsec_test_connect
OUTSIDE_URL=https://test.example.com
MESSENGER_TRANSPORT_DSN=redis://:e_ticket@redis:6379/messages
SESSION_HANDLER_DSN=redis://:e_ticket@redis:6379/sessions
SMIME_PASSPHRASE=test
ADMIN_EMAIL=contact@test.com

View File

@@ -9,27 +9,6 @@ ticket.e-cosplay.fr {
file_server
}
handle_path /stats/* {
rewrite * {uri}
reverse_proxy https://tools-security.esy-web.dev {
header_up Host tools-security.esy-web.dev
}
}
handle /assets/perf.js {
rewrite * /beacon.min.js
reverse_proxy https://static.cloudflareinsights.com {
header_up Host static.cloudflareinsights.com
}
}
handle_path /sperf {
rewrite * /cdn-cgi/rum
reverse_proxy https://cloudflareinsights.com {
header_up Host cloudflareinsights.com
}
}
@maintenance file /var/www/e-ticket/public/.update
handle @maintenance {
root * /var/www/e-ticket/public

View File

@@ -24,9 +24,9 @@ function loadAnalytics() {
const script = document.createElement('script')
script.defer = true
script.src = '/stats/script.js'
script.src = 'https://tools-security.esy-web.dev/script.js'
script.dataset.websiteId = 'a1f85dd5-741f-4df7-840a-7ef0931ed0cc'
script.dataset.hostUrl = '/stats'
script.dataset.hostUrl = 'https://tools-security.esy-web.dev'
script.dataset.analytics = '1'
document.head.appendChild(script)
@@ -40,7 +40,7 @@ function loadCloudflareTunnel() {
const script = document.createElement('script')
script.defer = true
script.src = '/assets/perf.js'
script.src = 'https://static.cloudflareinsights.com/beacon.min.js'
script.dataset.cfBeacon = '{"token":"5f2f3b8e1f824be6984a348fe31d2f04","spa":true}'
document.head.appendChild(script)
}

View File

@@ -3,7 +3,11 @@ framework:
secret: '%env(APP_SECRET)%'
# Note that the session will be started ONLY if you read or write from it.
session: true
session:
handler_id: '%env(SESSION_HANDLER_DSN)%'
cookie_lifetime: 86400
cookie_secure: auto
cookie_samesite: lax
#esi: true
#fragments: true

View File

@@ -1271,9 +1271,9 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* lifetime?: int|Param, // Default: 31536000
* path?: scalar|Param|null, // Default: "/"
* domain?: scalar|Param|null, // Default: null
* secure?: true|false|"auto"|Param, // Default: false
* secure?: true|false|"auto"|Param, // Default: null
* httponly?: bool|Param, // Default: true
* samesite?: null|"lax"|"strict"|"none"|Param, // Default: null
* samesite?: null|"lax"|"strict"|"none"|Param, // Default: "lax"
* always_remember_me?: bool|Param, // Default: false
* remember_me_parameter?: scalar|Param|null, // Default: "_remember_me"
* },

View File

@@ -119,13 +119,9 @@ class ApiSandboxController extends AbstractController
$fixtures = $this->loadFixtures();
$result = $fixtures['scan'][$reference] ?? null;
if (!$result) {
return $this->error('Billet introuvable.', 404);
}
unset($result['_comment']);
return $this->success($result);
return $result
? $this->success(array_diff_key($result, ['_comment' => true]))
: $this->error('Billet introuvable.', 404);
}
/**

View File

@@ -9,6 +9,7 @@ use Symfony\Component\Routing\Attribute\Route;
class ApiDocController extends AbstractController
{
private const MIME_JSON = 'application/json';
private const STATUS_401 = 'Non authentifie';
private const STATUS_403_EVENT = 'Evenement non accessible';
private const STATUS_403_BILLET = 'Billet non accessible';
@@ -74,38 +75,48 @@ class ApiDocController extends AbstractController
];
foreach ($section['endpoints'] as $reqIndex => $endpoint) {
$resources[] = $this->buildInsomniaRequest($endpoint, $folderIndex, $reqIndex, $folderId);
}
}
$export = [
'_type' => 'export',
'__export_format' => 4,
'__export_date' => date('Y-m-d\TH:i:s\Z'),
'__export_source' => 'eticket.api.doc',
'resources' => $resources,
];
return new Response(
json_encode($export, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES),
200,
[
'Content-Type' => self::MIME_JSON,
'Content-Disposition' => 'attachment; filename="eticket-api-insomnia.json"',
]
);
}
/**
* @param array<string, mixed> $endpoint
*
* @return array<string, mixed>
*/
private function buildInsomniaRequest(array $endpoint, int $folderIndex, int $reqIndex, string $folderId): array
{
$isAuthRoute = str_starts_with($endpoint['path'], '/api/auth');
$isLogin = '/api/auth/login' === $endpoint['path'];
$url = $isAuthRoute
? '{{ _.base_url }}'.$endpoint['path']
: '{{ _.base_url }}/api/{{ _.env }}'.str_replace('/api', '', $endpoint['path']);
$headers = [];
$headers[] = ['name' => 'Content-Type', 'value' => 'application/json'];
$headers = [['name' => 'Content-Type', 'value' => self::MIME_JSON]];
if (!$isLogin) {
$headers[] = ['name' => 'ETicket-Email', 'value' => '{{ _.email }}'];
$headers[] = ['name' => 'ETicket-JWT', 'value' => '{{ _.jwt_token }}'];
}
$body = null;
if ($isLogin) {
$body = [
'mimeType' => 'application/json',
'text' => json_encode([
'email' => '{{ _.email }}',
'password' => '{{ _.password }}',
], \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE),
];
} elseif ($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),
];
}
$body = $this->buildInsomniaBody($endpoint, $isLogin);
$req = [
'_type' => 'request',
@@ -122,26 +133,39 @@ class ApiDocController extends AbstractController
$req['afterResponseScript'] = "const res = insomnia.response.json();\nif (res && res.success && res.data && res.data.token) {\n insomnia.environment.set('jwt_token', res.data.token);\n}";
}
$resources[] = $req;
}
return $req;
}
$export = [
'_type' => 'export',
'__export_format' => 4,
'__export_date' => date('Y-m-d\TH:i:s\Z'),
'__export_source' => 'eticket.api.doc',
'resources' => $resources,
/**
* @param array<string, mixed> $endpoint
*
* @return array{mimeType: string, text: string}|null
*/
private function buildInsomniaBody(array $endpoint, bool $isLogin): ?array
{
if ($isLogin) {
return [
'mimeType' => self::MIME_JSON,
'text' => json_encode([
'email' => '{{ _.email }}',
'password' => '{{ _.password }}',
], \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE),
];
}
return 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"',
]
);
if ($endpoint['request']) {
$example = [];
foreach ($endpoint['request'] as $name => $field) {
$example[$name] = $field['example'] ?? '';
}
return [
'mimeType' => self::MIME_JSON,
'text' => json_encode($example, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE),
];
}
return null;
}
/**

View File

@@ -66,7 +66,7 @@ describe('initCookieConsent', () => {
document.getElementById('cookie-accept').click()
const script = document.querySelector('script[data-analytics]')
expect(script).not.toBeNull()
expect(script.src).toContain('/stats/script.js')
expect(script.src).toContain('tools-security.esy-web.dev/script.js')
expect(script.dataset.websiteId).toBe('a1f85dd5-741f-4df7-840a-7ef0931ed0cc')
})
@@ -105,7 +105,7 @@ describe('initCookieConsent', () => {
document.getElementById('cookie-accept').click()
const script = document.querySelector('script[data-cf-beacon]')
expect(script).not.toBeNull()
expect(script.src).toContain('/assets/perf.js')
expect(script.src).toContain('static.cloudflareinsights.com/beacon.min.js')
})
it('does not duplicate cloudflare script', () => {