Make analytics endpoint dynamic: /t/{token} derived from APP_SECRET
The endpoint path is now /t/<8-char hash of APP_SECRET> instead of static /t. Token is injected via data-e attribute on body, read by JS. Server validates token on every hit, returns 404 if invalid. Changes with each APP_SECRET = impossible to hardcode in a blocker. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
const ENDPOINT = '/t'
|
let ENDPOINT = '/t'
|
||||||
const SK_UID = '_u'
|
const SK_UID = '_u'
|
||||||
const SK_HASH = '_h'
|
const SK_HASH = '_h'
|
||||||
|
|
||||||
@@ -86,7 +86,9 @@ async function trackPageView(visitor) {
|
|||||||
|
|
||||||
export async function initAnalytics() {
|
export async function initAnalytics() {
|
||||||
const keyB64 = document.body.dataset.k
|
const keyB64 = document.body.dataset.k
|
||||||
if (!keyB64) return
|
const ep = document.body.dataset.e
|
||||||
|
if (!keyB64 || !ep) return
|
||||||
|
ENDPOINT = ep
|
||||||
|
|
||||||
try {
|
try {
|
||||||
encKey = await importKey(keyB64)
|
encKey = await importKey(keyB64)
|
||||||
|
|||||||
@@ -11,17 +11,25 @@ use Symfony\Component\HttpFoundation\JsonResponse;
|
|||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
class AnalyticsController extends AbstractController
|
class AnalyticsController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route('/t', name: 'app_analytics_track', methods: ['POST'])]
|
#[Route('/t/{token}', name: 'app_analytics_track', methods: ['POST'])]
|
||||||
public function track(
|
public function track(
|
||||||
|
string $token,
|
||||||
|
#[Autowire('%kernel.secret%')] string $appSecret,
|
||||||
Request $request,
|
Request $request,
|
||||||
AnalyticsCryptoService $crypto,
|
AnalyticsCryptoService $crypto,
|
||||||
EntityManagerInterface $em,
|
EntityManagerInterface $em,
|
||||||
MessageBusInterface $bus,
|
MessageBusInterface $bus,
|
||||||
): Response {
|
): Response {
|
||||||
|
$expectedToken = substr(hash('sha256', $appSecret.'_endpoint'), 0, 8);
|
||||||
|
if (!hash_equals($expectedToken, $token)) {
|
||||||
|
return new Response('', 404);
|
||||||
|
}
|
||||||
|
|
||||||
$body = $request->getContent();
|
$body = $request->getContent();
|
||||||
$envelope = json_decode($body, true);
|
$envelope = json_decode($body, true);
|
||||||
|
|
||||||
|
|||||||
@@ -3,20 +3,26 @@
|
|||||||
namespace App\Twig;
|
namespace App\Twig;
|
||||||
|
|
||||||
use App\Service\AnalyticsCryptoService;
|
use App\Service\AnalyticsCryptoService;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Twig\Extension\AbstractExtension;
|
use Twig\Extension\AbstractExtension;
|
||||||
use Twig\Extension\GlobalsInterface;
|
use Twig\Extension\GlobalsInterface;
|
||||||
|
|
||||||
class AnalyticsExtension extends AbstractExtension implements GlobalsInterface
|
class AnalyticsExtension extends AbstractExtension implements GlobalsInterface
|
||||||
{
|
{
|
||||||
|
private string $endpointToken;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private AnalyticsCryptoService $crypto,
|
private AnalyticsCryptoService $crypto,
|
||||||
|
#[Autowire('%kernel.secret%')] string $appSecret,
|
||||||
) {
|
) {
|
||||||
|
$this->endpointToken = substr(hash('sha256', $appSecret.'_endpoint'), 0, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getGlobals(): array
|
public function getGlobals(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'analytics_key' => $this->crypto->getKeyForJs(),
|
'analytics_key' => $this->crypto->getKeyForJs(),
|
||||||
|
'analytics_endpoint' => '/t/'.$this->endpointToken,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen flex flex-col bg-[#fbfbfb] text-[#111827]" data-env="{{ app.environment }}" data-k="{{ analytics_key }}"{% if app.user and app.user.id is defined and app.request.cookies.get('e_ticket_consent') == 'accepted' %} data-uid="{{ app.user.id }}"{% endif %}>
|
<body class="min-h-screen flex flex-col bg-[#fbfbfb] text-[#111827]" data-env="{{ app.environment }}" data-k="{{ analytics_key }}" data-e="{{ analytics_endpoint }}"{% if app.user and app.user.id is defined and app.request.cookies.get('e_ticket_consent') == 'accepted' %} data-uid="{{ app.user.id }}"{% endif %}>
|
||||||
<header class="sticky top-0 z-50 bg-white border-b-4 border-gray-900">
|
<header class="sticky top-0 z-50 bg-white border-b-4 border-gray-900">
|
||||||
<nav class="mx-auto px-4 lg:px-8" role="navigation" aria-label="Navigation principale" itemscope itemtype="https://schema.org/SiteNavigationElement">
|
<nav class="mx-auto px-4 lg:px-8" role="navigation" aria-label="Navigation principale" itemscope itemtype="https://schema.org/SiteNavigationElement">
|
||||||
<div class="flex justify-between items-center h-20">
|
<div class="flex justify-between items-center h-20">
|
||||||
|
|||||||
Reference in New Issue
Block a user