UnipileHostedLinkController.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. <?php
  2. namespace Controllers;
  3. use Libs\Logger;
  4. use Libs\Payload;
  5. use Libs\Validator;
  6. use Models\IntegrationsModel;
  7. use Models\UserModel;
  8. use Psr\Http\Message\ServerRequestInterface;
  9. use Services\UnipileClient;
  10. class UnipileHostedLinkController
  11. {
  12. private UserModel $userModel;
  13. private IntegrationsModel $integrationsModel;
  14. private UnipileClient $unipileClient;
  15. public function __construct()
  16. {
  17. $this->userModel = new UserModel();
  18. $this->integrationsModel = new IntegrationsModel();
  19. $this->unipileClient = new UnipileClient();
  20. }
  21. public function __invoke(ServerRequestInterface $request)
  22. {
  23. $userId = (int) ($request->getAttribute('user_id') ?? 0);
  24. $userEmail = (string) ($request->getAttribute('user_email') ?? '');
  25. $body = json_decode((string) $request->getBody(), true) ?: [];
  26. $type = mb_strtolower(trim((string) ($body['type'] ?? 'create')));
  27. $integrationId = (int) ($body['integration_id'] ?? 0);
  28. if ($userId <= 0) {
  29. return Payload::fail('Unauthorized: Missing authenticated user', [], 'E_VALIDATE', 401);
  30. }
  31. $validator = (new Validator(['type' => $type]))->required('type')->in('type', ['create', 'reconnect']);
  32. if ($validator->fails()) {
  33. return Payload::fail($validator->firstError(), [], 'E_VALIDATE', 400);
  34. }
  35. if ($type === 'reconnect' && $integrationId <= 0) {
  36. return Payload::fail('Missing or invalid integration_id', [], 'E_VALIDATE', 400);
  37. }
  38. if (!$this->unipileClient->isConfigured()) {
  39. return Payload::fail('Unipile is not configured', [], 'E_GENERIC', 500);
  40. }
  41. if (trim((string) ($_ENV['UNIPILE_NOTIFY_SECRET'] ?? '')) === '') {
  42. return Payload::fail('UNIPILE_NOTIFY_SECRET is not configured', [], 'E_GENERIC', 500);
  43. }
  44. try {
  45. $companyId = $this->userModel->getCompanyIdByUserId($userId);
  46. if ($companyId === null) {
  47. return Payload::fail('User not found', [], 'E_NOT_FOUND', 404);
  48. }
  49. $operatorId = $this->integrationsModel->getOperatorIdByUserEmail($companyId, $userEmail);
  50. $integration = null;
  51. if ($type === 'reconnect') {
  52. $integration = $this->integrationsModel->findById($companyId, $integrationId);
  53. if ($integration === null) {
  54. return Payload::fail('Integration not found', [], 'E_NOT_FOUND', 404);
  55. }
  56. }
  57. $hostedPayload = [
  58. 'type' => $type,
  59. 'providers' => ['WHATSAPP'],
  60. 'api_url' => $this->unipileClient->apiUrl(),
  61. 'expiresOn' => gmdate('Y-m-d\TH:i:s.000\Z', time() + 1800),
  62. 'notify_url' => $this->publicUrl($request, '/v1/webhooks/unipile/hosted-auth') . '?secret=' . rawurlencode((string) ($_ENV['UNIPILE_NOTIFY_SECRET'] ?? '')),
  63. 'success_redirect_url' => $this->redirectUrl($request, 'UNIPILE_SUCCESS_REDIRECT_URL', '/v1/integrations/unipile/whatsapp/success'),
  64. 'failure_redirect_url' => $this->redirectUrl($request, 'UNIPILE_FAILURE_REDIRECT_URL', '/v1/integrations/unipile/whatsapp/failure'),
  65. 'name' => $this->signedName($companyId, $userId, $operatorId, $integrationId),
  66. ];
  67. if ($integration !== null) {
  68. $hostedPayload['account_id'] = $integration['integration_account_id'];
  69. }
  70. $response = $this->unipileClient->createHostedAuthLink($hostedPayload);
  71. $url = (string) ($response['url'] ?? '');
  72. if ($url === '') {
  73. Logger::warning('Unipile hosted auth response missing url', ['response' => $response]);
  74. return Payload::fail('Failed to create hosted auth link', [], 'E_GENERIC', 502);
  75. }
  76. return Payload::ok(['url' => $url]);
  77. } catch (\Throwable $e) {
  78. Logger::error('Failed to create Unipile hosted auth link', ['error' => $e->getMessage()]);
  79. return Payload::fail('Failed to create hosted auth link', [], 'E_GENERIC', 500);
  80. }
  81. }
  82. private function signedName(int $companyId, int $userId, int $operatorId, int $integrationId): string
  83. {
  84. $secret = (string) ($_ENV['JWT_SECRET'] ?? '');
  85. $payload = [
  86. 'company_id' => $companyId,
  87. 'user_id' => $userId,
  88. 'operator_id' => $operatorId,
  89. 'integration_id' => $integrationId,
  90. 'nonce' => bin2hex(random_bytes(12)),
  91. 'iat' => time(),
  92. ];
  93. $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
  94. if ($json === false) {
  95. $json = '{}';
  96. }
  97. $encoded = rtrim(strtr(base64_encode($json), '+/', '-_'), '=');
  98. $signature = hash_hmac('sha256', $encoded, $secret);
  99. return $encoded . '.' . $signature;
  100. }
  101. private function redirectUrl(ServerRequestInterface $request, string $envKey, string $fallbackPath): string
  102. {
  103. $configured = trim((string) ($_ENV[$envKey] ?? ''));
  104. if ($configured !== '') {
  105. return $configured;
  106. }
  107. return $this->publicUrl($request, $fallbackPath);
  108. }
  109. private function publicUrl(ServerRequestInterface $request, string $path): string
  110. {
  111. $configured = rtrim(trim((string) ($_ENV['APP_PUBLIC_URL'] ?? '')), '/');
  112. if ($configured !== '') {
  113. return $configured . $path;
  114. }
  115. $host = $request->getHeaderLine('Host');
  116. if ($host === '') {
  117. $host = 'localhost:8080';
  118. }
  119. return 'https://' . $host . $path;
  120. }
  121. }