<?php
namespace App\Controller;
use App\Service\DoceboLearnerSync;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
/**
* Webhooks Docebo
*/
class WebhookController extends AbstractController
{
private $syncUser;
private $syncPassword;
public function __construct(string $syncUser, string $syncPassword)
{
$this->syncUser = $syncUser;
$this->syncPassword = $syncPassword;
}
/**
* Webhook Docebo "sync-users-in-dash-app" : branch.user.added / branch.user.removed / user.updated / user.created / branch.created
*
* @Route("/sync-learners", name="sync_learners", methods={"POST"})
*/
public function syncLearners(Request $request, DoceboLearnerSync $sync, LoggerInterface $logger)
{
if (!$this->isAuthorized($request)) {
return new JsonResponse(['success' => false, 'message' => 'Unauthorized'], 401, ['WWW-Authenticate' => 'Basic realm="sync-learners"']);
}
$data = json_decode($request->getContent(), true);
if (!is_array($data) || empty($data['event'])) {
return new JsonResponse(['success' => false, 'message' => 'Invalid payload'], 400);
}
$event = $data['event'];
$required = [
'branch.user.added' => ['user_id', 'branch_id'],
'branch.user.removed' => ['user_id', 'branch_id'],
'user.updated' => ['user_id'],
'user.created' => ['user_id'],
'branch.created' => ['branch_id'],
];
$items = $data['payloads'] ?? [$data['payload'] ?? null];
$results = [];
try {
foreach ($items as $item) {
if (!is_array($item) || !$this->hasRequired($item, $required[$event] ?? [])
|| ($event === 'branch.created' && !isset($item['parent_id']))) {
$results[] = ['status' => 'invalid_item'];
continue;
}
switch ($event) {
case 'branch.user.added':
$results[] = $sync->addToBranch((int) $item['user_id'], (int) $item['branch_id'], $item['username'] ?? null);
break;
case 'branch.user.removed':
$results[] = $sync->removeFromBranch((int) $item['user_id'], (int) $item['branch_id']);
break;
case 'user.updated':
$results[] = $sync->updateUser((int) $item['user_id'], $item);
break;
case 'user.created':
$results[] = $sync->createUser((int) $item['user_id'], $item);
break;
case 'branch.created':
$results[] = $sync->branchCreated((int) $item['branch_id'], (int) $item['parent_id'], $item['code'] ?? null, $item['name'] ?? null);
break;
default:
$results[] = ['status' => 'ignored'];
break;
}
}
} catch (\Throwable $e) {
$logger->error('[sync-learners] ' . $e->getMessage(), [
'event' => $event,
'message_id' => $data['message_id'] ?? null,
'exception' => $e,
]);
return new JsonResponse(['success' => false, 'message' => 'Internal error'], 500);
}
return new JsonResponse([
'success' => true,
'event' => $event,
'message_id' => $data['message_id'] ?? null,
'results' => $results,
]);
}
private function hasRequired(array $item, array $fields): bool
{
foreach ($fields as $field) {
if (empty($item[$field])) {
return false;
}
}
return true;
}
private function isAuthorized(Request $request): bool
{
if ($this->syncUser === '' || $this->syncPassword === '') {
return false;
}
$user = $request->getUser();
$password = $request->getPassword();
// Fallback si PHP_AUTH_USER n'est pas renseigné (CGI / proxy)
if ($user === null) {
$header = $request->headers->get('Authorization', '');
if (stripos($header, 'Basic ') === 0) {
$decoded = base64_decode(substr($header, 6), true);
if ($decoded !== false && strpos($decoded, ':') !== false) {
list($user, $password) = explode(':', $decoded, 2);
}
}
}
return hash_equals($this->syncUser, (string) $user) && hash_equals($this->syncPassword, (string) $password);
}
}