<?php
namespace App\EventSubscriber;
use App\Service\UserService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* JWT Authentication Middleware
*
* Validates Bearer tokens on protected routes and injects
* the authenticated AppUser into the request attributes.
*
* Public routes (no auth required):
* - /auth/login
* - /auth/setup
* - Pimcore admin routes (/admin/*)
*
* Backward-compatible routes (accept both JWT and legacy userid):
* - /dashboard
* - /picking_*
* - /stockAll
* - All other existing endpoints that use ?userid=
*/
class AuthMiddleware implements EventSubscriberInterface
{
private UserService $userService;
/**
* Routes that never require authentication
*/
private const PUBLIC_ROUTES = [
'/auth/login',
'/auth/setup',
];
/**
* Route prefixes that are always public
*/
private const PUBLIC_PREFIXES = [
'/admin',
'/_profiler',
'/_wdt',
'/public/',
];
/**
* Routes that accept JWT but don't require it (backward compat)
* These still work with ?userid= parameter
*/
private const OPTIONAL_AUTH_PREFIXES = [
'/dashboard',
'/picking_',
'/picklist',
'/stockAll',
'/stockdata',
'/stockdirectories',
'/orderData',
'/orders',
'/fullorders',
'/fullorder',
'/plc/',
'/paketa/',
'/printlabel',
'/document',
'/invoices',
'/invoice_categories',
'/import',
'/resetpicklist',
'/importpicklist',
'/upsertstock',
'/updatePickStatus',
'/deletePicklistOrder',
'/crosschannel',
'/api/',
];
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['onKernelRequest', 10],
];
}
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
$path = $request->getPathInfo();
// Always allow public routes
if ($this->isPublicRoute($path)) {
return;
}
// Try to extract and validate JWT token
$authHeader = $request->headers->get('Authorization', '');
$token = null;
if (strpos($authHeader, 'Bearer ') === 0) {
$token = substr($authHeader, 7);
}
if ($token) {
$user = $this->userService->validateToken($token);
if ($user) {
// Inject authenticated user into request for controllers
$request->attributes->set('_auth_user', $user);
// Also set the userid for backward compat with existing code
$request->query->set('userid', $user->getUsername());
return;
}
// Token was provided but invalid — only reject on protected routes
if (!$this->isOptionalAuthRoute($path)) {
$event->setResponse(new JsonResponse(
['error' => 'Invalid or expired token'],
Response::HTTP_UNAUTHORIZED
));
return;
}
}
// No token: if it's an optional-auth route, let it through (backward compat)
if ($this->isOptionalAuthRoute($path)) {
return;
}
// Protected route with no valid token
if ($this->isProtectedRoute($path)) {
$event->setResponse(new JsonResponse(
['error' => 'Authentication required'],
Response::HTTP_UNAUTHORIZED
));
}
}
private function isPublicRoute(string $path): bool
{
foreach (self::PUBLIC_ROUTES as $route) {
if ($path === $route) {
return true;
}
}
foreach (self::PUBLIC_PREFIXES as $prefix) {
if (strpos($path, $prefix) === 0) {
return true;
}
}
return false;
}
private function isOptionalAuthRoute(string $path): bool
{
foreach (self::OPTIONAL_AUTH_PREFIXES as $prefix) {
if (strpos($path, $prefix) === 0) {
return true;
}
}
return false;
}
private function isProtectedRoute(string $path): bool
{
// Routes that strictly require auth
$protectedPrefixes = [
'/auth/me',
'/auth/users',
'/rounds',
];
foreach ($protectedPrefixes as $prefix) {
if (strpos($path, $prefix) === 0) {
return true;
}
}
return false;
}
}