UnipileHostedAuthWebhookController.php 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. <?php
  2. namespace Controllers;
  3. use Libs\Logger;
  4. use Libs\Payload;
  5. use Models\IntegrationsModel;
  6. use Psr\Http\Message\ServerRequestInterface;
  7. class UnipileHostedAuthWebhookController
  8. {
  9. private IntegrationsModel $integrationsModel;
  10. public function __construct()
  11. {
  12. $this->integrationsModel = new IntegrationsModel();
  13. }
  14. public function __invoke(ServerRequestInterface $request)
  15. {
  16. if (!$this->isAuthorized($request)) {
  17. return Payload::fail('Unauthorized', [], 'E_VALIDATE', 401);
  18. }
  19. $body = json_decode((string) $request->getBody(), true) ?: [];
  20. $status = (string) ($body['status'] ?? '');
  21. $accountId = (string) ($body['account_id'] ?? '');
  22. $name = (string) ($body['name'] ?? '');
  23. if ($status === '' || $accountId === '' || $name === '') {
  24. return Payload::fail('Invalid callback payload', [], 'E_VALIDATE', 400);
  25. }
  26. $identity = $this->decodeName($name);
  27. if ($identity === null) {
  28. return Payload::fail('Invalid callback identity', [], 'E_VALIDATE', 400);
  29. }
  30. try {
  31. $integration = $this->integrationsModel->upsertWhatsappIntegration(
  32. (int) $identity['company_id'],
  33. (int) $identity['user_id'],
  34. (int) $identity['operator_id'],
  35. $accountId,
  36. $status,
  37. $body
  38. );
  39. return Payload::ok(['integration' => $this->integrationsModel->formatIntegration($integration)]);
  40. } catch (\Throwable $e) {
  41. Logger::error('Failed to process Unipile hosted auth callback', ['error' => $e->getMessage()]);
  42. return Payload::fail('Failed to process callback', [], 'E_GENERIC', 500);
  43. }
  44. }
  45. private function isAuthorized(ServerRequestInterface $request): bool
  46. {
  47. $expected = (string) ($_ENV['UNIPILE_NOTIFY_SECRET'] ?? '');
  48. if ($expected === '') {
  49. return false;
  50. }
  51. $provided = (string) ($request->getQueryParams()['secret'] ?? '');
  52. if ($provided === '') {
  53. $provided = $request->getHeaderLine('Unipile-Notify-Secret');
  54. }
  55. return hash_equals($expected, $provided);
  56. }
  57. private function decodeName(string $name): ?array
  58. {
  59. $parts = explode('.', $name, 2);
  60. if (count($parts) !== 2) {
  61. return null;
  62. }
  63. [$encoded, $signature] = $parts;
  64. $secret = (string) ($_ENV['JWT_SECRET'] ?? '');
  65. $expected = hash_hmac('sha256', $encoded, $secret);
  66. if (!hash_equals($expected, $signature)) {
  67. return null;
  68. }
  69. $padded = str_pad(strtr($encoded, '-_', '+/'), strlen($encoded) % 4 === 0 ? strlen($encoded) : strlen($encoded) + 4 - strlen($encoded) % 4, '=', STR_PAD_RIGHT);
  70. $json = base64_decode($padded, true);
  71. if ($json === false) {
  72. return null;
  73. }
  74. $payload = json_decode($json, true);
  75. if (!is_array($payload)) {
  76. return null;
  77. }
  78. foreach (['company_id', 'user_id', 'operator_id'] as $key) {
  79. if (!isset($payload[$key]) || (int) $payload[$key] < 0) {
  80. return null;
  81. }
  82. }
  83. return $payload;
  84. }
  85. }