userModel = new UserModel(); $this->integrationsModel = new IntegrationsModel(); $this->unipileClient = new UnipileClient(); } public function __invoke(ServerRequestInterface $request) { $userId = (int) ($request->getAttribute('user_id') ?? 0); $userEmail = (string) ($request->getAttribute('user_email') ?? ''); $body = json_decode((string) $request->getBody(), true) ?: []; $type = mb_strtolower(trim((string) ($body['type'] ?? 'create'))); $integrationId = (int) ($body['integration_id'] ?? 0); if ($userId <= 0) { return Payload::fail('Unauthorized: Missing authenticated user', [], 'E_VALIDATE', 401); } $validator = (new Validator(['type' => $type]))->required('type')->in('type', ['create', 'reconnect']); if ($validator->fails()) { return Payload::fail($validator->firstError(), [], 'E_VALIDATE', 400); } if ($type === 'reconnect' && $integrationId <= 0) { return Payload::fail('Missing or invalid integration_id', [], 'E_VALIDATE', 400); } if (!$this->unipileClient->isConfigured()) { return Payload::fail('Unipile is not configured', [], 'E_GENERIC', 500); } if (trim((string) ($_ENV['UNIPILE_NOTIFY_SECRET'] ?? '')) === '') { return Payload::fail('UNIPILE_NOTIFY_SECRET is not configured', [], 'E_GENERIC', 500); } try { $companyId = $this->userModel->getCompanyIdByUserId($userId); if ($companyId === null) { return Payload::fail('User not found', [], 'E_NOT_FOUND', 404); } $operatorId = $this->integrationsModel->getOperatorIdByUserEmail($companyId, $userEmail); $integration = null; if ($type === 'reconnect') { $integration = $this->integrationsModel->findById($companyId, $integrationId); if ($integration === null) { return Payload::fail('Integration not found', [], 'E_NOT_FOUND', 404); } } $hostedPayload = [ 'type' => $type, 'providers' => ['WHATSAPP'], 'api_url' => $this->unipileClient->apiUrl(), 'expiresOn' => gmdate('Y-m-d\TH:i:s.000\Z', time() + 1800), 'notify_url' => $this->publicUrl($request, '/v1/webhooks/unipile/hosted-auth') . '?secret=' . rawurlencode((string) ($_ENV['UNIPILE_NOTIFY_SECRET'] ?? '')), 'success_redirect_url' => $this->redirectUrl($request, 'UNIPILE_SUCCESS_REDIRECT_URL', '/v1/integrations/unipile/whatsapp/success'), 'failure_redirect_url' => $this->redirectUrl($request, 'UNIPILE_FAILURE_REDIRECT_URL', '/v1/integrations/unipile/whatsapp/failure'), 'name' => $this->signedName($companyId, $userId, $operatorId, $integrationId), ]; if ($integration !== null) { $hostedPayload['account_id'] = $integration['integration_account_id']; } $response = $this->unipileClient->createHostedAuthLink($hostedPayload); $url = (string) ($response['url'] ?? ''); if ($url === '') { Logger::warning('Unipile hosted auth response missing url', ['response' => $response]); return Payload::fail('Failed to create hosted auth link', [], 'E_GENERIC', 502); } return Payload::ok(['url' => $url]); } catch (\Throwable $e) { Logger::error('Failed to create Unipile hosted auth link', ['error' => $e->getMessage()]); return Payload::fail('Failed to create hosted auth link', [], 'E_GENERIC', 500); } } private function signedName(int $companyId, int $userId, int $operatorId, int $integrationId): string { $secret = (string) ($_ENV['JWT_SECRET'] ?? ''); $payload = [ 'company_id' => $companyId, 'user_id' => $userId, 'operator_id' => $operatorId, 'integration_id' => $integrationId, 'nonce' => bin2hex(random_bytes(12)), 'iat' => time(), ]; $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($json === false) { $json = '{}'; } $encoded = rtrim(strtr(base64_encode($json), '+/', '-_'), '='); $signature = hash_hmac('sha256', $encoded, $secret); return $encoded . '.' . $signature; } private function redirectUrl(ServerRequestInterface $request, string $envKey, string $fallbackPath): string { $configured = trim((string) ($_ENV[$envKey] ?? '')); if ($configured !== '') { return $configured; } return $this->publicUrl($request, $fallbackPath); } private function publicUrl(ServerRequestInterface $request, string $path): string { $configured = rtrim(trim((string) ($_ENV['APP_PUBLIC_URL'] ?? '')), '/'); if ($configured !== '') { return $configured . $path; } $host = $request->getHeaderLine('Host'); if ($host === '') { $host = 'localhost:8080'; } return 'https://' . $host . $path; } }