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:
4
.env
4
.env
@@ -32,6 +32,10 @@ DATABASE_URL="postgresql://app:secret@database:5432/ecosplay?serverVersion=16&ch
|
|||||||
MESSENGER_TRANSPORT_DSN=redis://redis:6379/messages
|
MESSENGER_TRANSPORT_DSN=redis://redis:6379/messages
|
||||||
###< symfony/messenger ###
|
###< symfony/messenger ###
|
||||||
|
|
||||||
|
###> session ###
|
||||||
|
SESSION_HANDLER_DSN=redis://redis:6379/sessions
|
||||||
|
###< session ###
|
||||||
|
|
||||||
###> symfony/mailer ###
|
###> symfony/mailer ###
|
||||||
MAILER_DSN=smtp://mailpit:1025
|
MAILER_DSN=smtp://mailpit:1025
|
||||||
###< symfony/mailer ###
|
###< symfony/mailer ###
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ STRIPE_WEBHOOK_SECRET=whsec_test
|
|||||||
STRIPE_WEBHOOK_SECRET_CONNECT=whsec_test_connect
|
STRIPE_WEBHOOK_SECRET_CONNECT=whsec_test_connect
|
||||||
OUTSIDE_URL=https://test.example.com
|
OUTSIDE_URL=https://test.example.com
|
||||||
MESSENGER_TRANSPORT_DSN=redis://:e_ticket@redis:6379/messages
|
MESSENGER_TRANSPORT_DSN=redis://:e_ticket@redis:6379/messages
|
||||||
|
SESSION_HANDLER_DSN=redis://:e_ticket@redis:6379/sessions
|
||||||
SMIME_PASSPHRASE=test
|
SMIME_PASSPHRASE=test
|
||||||
ADMIN_EMAIL=contact@test.com
|
ADMIN_EMAIL=contact@test.com
|
||||||
|
|||||||
@@ -9,27 +9,6 @@ ticket.e-cosplay.fr {
|
|||||||
file_server
|
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
|
@maintenance file /var/www/e-ticket/public/.update
|
||||||
handle @maintenance {
|
handle @maintenance {
|
||||||
root * /var/www/e-ticket/public
|
root * /var/www/e-ticket/public
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ function loadAnalytics() {
|
|||||||
|
|
||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
script.defer = true
|
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.websiteId = 'a1f85dd5-741f-4df7-840a-7ef0931ed0cc'
|
||||||
script.dataset.hostUrl = '/stats'
|
script.dataset.hostUrl = 'https://tools-security.esy-web.dev'
|
||||||
script.dataset.analytics = '1'
|
script.dataset.analytics = '1'
|
||||||
document.head.appendChild(script)
|
document.head.appendChild(script)
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ function loadCloudflareTunnel() {
|
|||||||
|
|
||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
script.defer = true
|
script.defer = true
|
||||||
script.src = '/assets/perf.js'
|
script.src = 'https://static.cloudflareinsights.com/beacon.min.js'
|
||||||
script.dataset.cfBeacon = '{"token":"5f2f3b8e1f824be6984a348fe31d2f04","spa":true}'
|
script.dataset.cfBeacon = '{"token":"5f2f3b8e1f824be6984a348fe31d2f04","spa":true}'
|
||||||
document.head.appendChild(script)
|
document.head.appendChild(script)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ framework:
|
|||||||
secret: '%env(APP_SECRET)%'
|
secret: '%env(APP_SECRET)%'
|
||||||
|
|
||||||
# Note that the session will be started ONLY if you read or write from it.
|
# 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
|
#esi: true
|
||||||
#fragments: true
|
#fragments: true
|
||||||
|
|||||||
@@ -1271,9 +1271,9 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
|||||||
* lifetime?: int|Param, // Default: 31536000
|
* lifetime?: int|Param, // Default: 31536000
|
||||||
* path?: scalar|Param|null, // Default: "/"
|
* path?: scalar|Param|null, // Default: "/"
|
||||||
* domain?: scalar|Param|null, // Default: null
|
* 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
|
* 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
|
* always_remember_me?: bool|Param, // Default: false
|
||||||
* remember_me_parameter?: scalar|Param|null, // Default: "_remember_me"
|
* remember_me_parameter?: scalar|Param|null, // Default: "_remember_me"
|
||||||
* },
|
* },
|
||||||
|
|||||||
@@ -119,13 +119,9 @@ class ApiSandboxController extends AbstractController
|
|||||||
$fixtures = $this->loadFixtures();
|
$fixtures = $this->loadFixtures();
|
||||||
$result = $fixtures['scan'][$reference] ?? null;
|
$result = $fixtures['scan'][$reference] ?? null;
|
||||||
|
|
||||||
if (!$result) {
|
return $result
|
||||||
return $this->error('Billet introuvable.', 404);
|
? $this->success(array_diff_key($result, ['_comment' => true]))
|
||||||
}
|
: $this->error('Billet introuvable.', 404);
|
||||||
|
|
||||||
unset($result['_comment']);
|
|
||||||
|
|
||||||
return $this->success($result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||||||
|
|
||||||
class ApiDocController extends AbstractController
|
class ApiDocController extends AbstractController
|
||||||
{
|
{
|
||||||
|
private const MIME_JSON = 'application/json';
|
||||||
private const STATUS_401 = 'Non authentifie';
|
private const STATUS_401 = 'Non authentifie';
|
||||||
private const STATUS_403_EVENT = 'Evenement non accessible';
|
private const STATUS_403_EVENT = 'Evenement non accessible';
|
||||||
private const STATUS_403_BILLET = 'Billet non accessible';
|
private const STATUS_403_BILLET = 'Billet non accessible';
|
||||||
@@ -74,55 +75,7 @@ class ApiDocController extends AbstractController
|
|||||||
];
|
];
|
||||||
|
|
||||||
foreach ($section['endpoints'] as $reqIndex => $endpoint) {
|
foreach ($section['endpoints'] as $reqIndex => $endpoint) {
|
||||||
$isAuthRoute = str_starts_with($endpoint['path'], '/api/auth');
|
$resources[] = $this->buildInsomniaRequest($endpoint, $folderIndex, $reqIndex, $folderId);
|
||||||
$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'];
|
|
||||||
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),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$req = [
|
|
||||||
'_type' => 'request',
|
|
||||||
'_id' => 'req_'.$folderIndex.'_'.$reqIndex,
|
|
||||||
'parentId' => $folderId,
|
|
||||||
'name' => $endpoint['summary'],
|
|
||||||
'method' => $endpoint['method'],
|
|
||||||
'url' => $url,
|
|
||||||
'headers' => $headers,
|
|
||||||
'body' => $body,
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($isAuthRoute) {
|
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,12 +91,83 @@ class ApiDocController extends AbstractController
|
|||||||
json_encode($export, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES),
|
json_encode($export, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES),
|
||||||
200,
|
200,
|
||||||
[
|
[
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => self::MIME_JSON,
|
||||||
'Content-Disposition' => 'attachment; filename="eticket-api-insomnia.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 = [['name' => 'Content-Type', 'value' => self::MIME_JSON]];
|
||||||
|
if (!$isLogin) {
|
||||||
|
$headers[] = ['name' => 'ETicket-Email', 'value' => '{{ _.email }}'];
|
||||||
|
$headers[] = ['name' => 'ETicket-JWT', 'value' => '{{ _.jwt_token }}'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = $this->buildInsomniaBody($endpoint, $isLogin);
|
||||||
|
|
||||||
|
$req = [
|
||||||
|
'_type' => 'request',
|
||||||
|
'_id' => 'req_'.$folderIndex.'_'.$reqIndex,
|
||||||
|
'parentId' => $folderId,
|
||||||
|
'name' => $endpoint['summary'],
|
||||||
|
'method' => $endpoint['method'],
|
||||||
|
'url' => $url,
|
||||||
|
'headers' => $headers,
|
||||||
|
'body' => $body,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($isAuthRoute) {
|
||||||
|
$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}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $req;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list<array{name: string, slug: string, baseUrl: string, description: string, badge: string, badgeColor: string}>
|
* @return list<array{name: string, slug: string, baseUrl: string, description: string, badge: string, badgeColor: string}>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ describe('initCookieConsent', () => {
|
|||||||
document.getElementById('cookie-accept').click()
|
document.getElementById('cookie-accept').click()
|
||||||
const script = document.querySelector('script[data-analytics]')
|
const script = document.querySelector('script[data-analytics]')
|
||||||
expect(script).not.toBeNull()
|
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')
|
expect(script.dataset.websiteId).toBe('a1f85dd5-741f-4df7-840a-7ef0931ed0cc')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ describe('initCookieConsent', () => {
|
|||||||
document.getElementById('cookie-accept').click()
|
document.getElementById('cookie-accept').click()
|
||||||
const script = document.querySelector('script[data-cf-beacon]')
|
const script = document.querySelector('script[data-cf-beacon]')
|
||||||
expect(script).not.toBeNull()
|
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', () => {
|
it('does not duplicate cloudflare script', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user