diff --git a/cp/app/Controllers/ContactsController.php b/cp/app/Controllers/ContactsController.php index b778b71..82c158a 100644 --- a/cp/app/Controllers/ContactsController.php +++ b/cp/app/Controllers/ContactsController.php @@ -3,6 +3,7 @@ namespace App\Controllers; use App\Models\Contact; +use App\Lib\Mail; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Container\ContainerInterface; @@ -1134,36 +1135,93 @@ class ContactsController extends Controller } else { $clid = $contact['clid']; } - + if ($contact) { - try { - $db->beginTransaction(); - $currentDateTime = new \DateTime(); - $stamp = $currentDateTime->format('Y-m-d H:i:s.v'); - $db->update( - 'contact', - [ - 'validation' => $data['verify'], - 'validation_stamp' => $stamp, - 'validation_log' => json_encode($data['v_log']), - 'upid' => $clid, - 'lastupdate' => $stamp + if (!empty(envi('SUMSUB_TOKEN')) && !empty(envi('SUMSUB_KEY'))) { + $level_name = 'idv-and-phone-verification'; + + // Build request body + $bodyArray = [ + 'levelName' => $level_name, + 'userId' => $identifier, + 'applicantIdentifiers' => [ + 'email' => $contact['email'], + 'phone' => $contact['voice'] ], - [ - 'identifier' => $identifier + 'ttlInSecs' => 1800 + ]; + + $body = json_encode($bodyArray); + $path = '/resources/sdkIntegrations/levels/-/websdkLink'; + $ts = time(); + $signature = sign($ts, 'POST', $path, $body, envi('SUMSUB_KEY')); + + // Guzzle client + $client = new \GuzzleHttp\Client([ + 'base_uri' => 'https://api.sumsub.com', + 'headers' => [ + 'X-App-Token' => envi('SUMSUB_TOKEN'), + 'X-App-Access-Ts' => $ts, + 'X-App-Access-Sig' => $signature, + 'Content-Type' => 'application/json', ] - ); - $db->commit(); - } catch (Exception $e) { - $db->rollBack(); - $this->container->get('flash')->addMessage('error', 'Database failure during update: ' . $e->getMessage()); + ]); + + // Send request + try { + $response = $client->post($path, ['body' => $body]); + $data = json_decode($response->getBody(), true); + $link = $data['url']; + + $currentDateTime = new \DateTime(); + $stamp = $currentDateTime->format('Y-m-d H:i:s.v'); + $email = $db->selectValue('SELECT email FROM users WHERE id = ?', [$_SESSION['auth_user_id']]); + $registry = $db->selectValue('SELECT value FROM settings WHERE name = ?', ['company_name']); + $message = file_get_contents(__DIR__.'/../../resources/views/mail/validation.html'); + $placeholders = ['{registry}', '{link}', '{app_name}', '{app_url}', '{identifier}']; + $replacements = [$registry, $link, envi('APP_NAME'), envi('APP_URL'), $contact['identifier']]; + $message = str_replace($placeholders, $replacements, $message); + $mailsubject = '[' . envi('APP_NAME') . '] Contact Verification Required'; + $from = ['email'=>envi('MAIL_FROM_ADDRESS'), 'name'=>envi('MAIL_FROM_NAME')]; + $to = ['email'=>$contact['email'], 'name'=>'']; + // send message + Mail::send($mailsubject, $message, $from, $to); + + $this->container->get('flash')->addMessage('info', 'Contact validation process initiated with SumSub on ' . $stamp); + return $response->withHeader('Location', '/contact/update/'.$identifier)->withStatus(302); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->container->get('flash')->addMessage('error', 'Contact validation error: ' . $e->getMessage()); + return $response->withHeader('Location', '/contact/update/'.$identifier)->withStatus(302); + } + } else { + try { + $db->beginTransaction(); + $currentDateTime = new \DateTime(); + $stamp = $currentDateTime->format('Y-m-d H:i:s.v'); + $db->update( + 'contact', + [ + 'validation' => $data['verify'], + 'validation_stamp' => $stamp, + 'validation_log' => json_encode($data['v_log']), + 'upid' => $clid, + 'lastupdate' => $stamp + ], + [ + 'identifier' => $identifier + ] + ); + $db->commit(); + } catch (Exception $e) { + $db->rollBack(); + $this->container->get('flash')->addMessage('error', 'Database failure during update: ' . $e->getMessage()); + return $response->withHeader('Location', '/contact/update/'.$identifier)->withStatus(302); + } + + unset($_SESSION['contacts_to_validate']); + $this->container->get('flash')->addMessage('success', 'Contact ' . $identifier . ' has been validated successfully on ' . $stamp); return $response->withHeader('Location', '/contact/update/'.$identifier)->withStatus(302); } - - unset($_SESSION['contacts_to_validate']); - $this->container->get('flash')->addMessage('success', 'Contact ' . $identifier . ' has been validated successfully on ' . $stamp); - return $response->withHeader('Location', '/contact/update/'.$identifier)->withStatus(302); - } else { // Contact does not exist, redirect to the contacts view return $response->withHeader('Location', '/contacts')->withStatus(302); @@ -1628,6 +1686,64 @@ class ContactsController extends Controller //} - } + } + + public function webhookSumsub(Request $request, Response $response) + { + $body = $request->getBody()->getContents(); + $data = json_decode($body, true); + $db = $this->container->get('db'); + + // Validate input + if (!isset($data['externalUserId']) || !isset($data['type'])) { + $response->getBody()->write('Missing required fields'); + return $response->withStatus(400); + } + + $identifier = $data['externalUserId']; + $type = $data['type']; + + // Only process applicantReviewed type + if ($type === 'applicantReviewed') { + $answer = $data['reviewResult']['reviewAnswer'] ?? null; + switch ($answer) { + case 'GREEN': + $verify = '4'; // verified + break; + case 'RED': + $verify = '0'; // failed + break; + default: + // Ignore anything else + $response->getBody()->write('Ignored (unhandled reviewAnswer)'); + return $response->withStatus(202); + } + $v_log = $data; // store full webhook for audit + $clid = $data['applicantId'] ?? null; + + $currentDateTime = new \DateTime(); + $stamp = $currentDateTime->format('Y-m-d H:i:s.v'); + + $db->update( + 'contact', + [ + 'validation' => $verify, + 'validation_stamp' => $stamp, + 'validation_log' => json_encode($v_log), + //'upid' => $clid, + 'lastupdate' => $stamp + ], + [ + 'identifier' => $identifier + ] + ); + + $response->getBody()->write('OK'); + return $response->withStatus(200); + } + + $response->getBody()->write('Ignored'); + return $response->withStatus(202); + } } \ No newline at end of file diff --git a/cp/bootstrap/helper.php b/cp/bootstrap/helper.php index 0a6f77b..ad28100 100644 --- a/cp/bootstrap/helper.php +++ b/cp/bootstrap/helper.php @@ -1148,4 +1148,10 @@ function isValidHostname($hostname) { } return true; +} + +// HMAC Signature generator +function sign($ts, $method, $path, $body, $secret_key) { + $stringToSign = $ts . strtoupper($method) . $path . $body; + return hash_hmac('sha256', $stringToSign, $secret_key); } \ No newline at end of file diff --git a/cp/env-sample b/cp/env-sample index 97c3ad4..c8f90fd 100644 --- a/cp/env-sample +++ b/cp/env-sample @@ -44,4 +44,7 @@ NOW_API_KEY='now-api-key' NICKY_API_KEY='nicky-api-key' +SUMSUB_TOKEN= +SUMSUB_KEY= + TEST_TLDS=.test,.com.test \ No newline at end of file diff --git a/cp/resources/views/mail/validation.html b/cp/resources/views/mail/validation.html new file mode 100644 index 0000000..ef55993 --- /dev/null +++ b/cp/resources/views/mail/validation.html @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cp/routes/web.php b/cp/routes/web.php index 61cbe44..b0a09a8 100644 --- a/cp/routes/web.php +++ b/cp/routes/web.php @@ -38,6 +38,7 @@ $app->group('', function ($route) { $route->get('/update-password', PasswordController::class.':createUpdatePassword')->setName('update.password'); $route->post('/update-password', PasswordController::class.':updatePassword'); $route->post('/webhook/adyen', FinancialsController::class .':webhookAdyen')->setName('webhookAdyen'); + $route->post('/webhook/sumsub', ContactsController::class .':webhookSumsub')->setName('webhookSumsub'); })->add(new GuestMiddleware($container)); $app->group('', function ($route) {