Added SubSub support for contact verification

This commit is contained in:
Pinga 2025-03-21 13:13:09 +02:00
parent f3d1fa22e6
commit 8def259c5f
5 changed files with 678 additions and 26 deletions

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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

View file

@ -0,0 +1,526 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="color-scheme: light dark; supported-color-schemes: light dark;">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
<title></title>
<style type="text/css" rel="stylesheet" media="all">
/* Base ------------------------------ */
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&amp;display=swap");
body {
width: 100% !important;
height: 100%;
margin: 0;
-webkit-text-size-adjust: none;
}
a {
color: #3869D4;
}
a img {
border: none;
}
td {
word-break: break-word;
}
.preheader {
display: none !important;
visibility: hidden;
mso-hide: all;
font-size: 1px;
line-height: 1px;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
}
/* Type ------------------------------ */
body,
td,
th {
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
}
h1 {
margin-top: 0;
color: #333333;
font-size: 22px;
font-weight: bold;
text-align: left;
}
h2 {
margin-top: 0;
color: #333333;
font-size: 16px;
font-weight: bold;
text-align: left;
}
h3 {
margin-top: 0;
color: #333333;
font-size: 14px;
font-weight: bold;
text-align: left;
}
td,
th {
font-size: 16px;
}
p,
ul,
ol,
blockquote {
margin: .4em 0 1.1875em;
font-size: 16px;
line-height: 1.625;
}
p.sub {
font-size: 13px;
}
/* Utilities ------------------------------ */
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.u-margin-bottom-none {
margin-bottom: 0;
}
/* Buttons ------------------------------ */
.button {
background-color: #3869D4;
border-top: 10px solid #3869D4;
border-right: 18px solid #3869D4;
border-bottom: 10px solid #3869D4;
border-left: 18px solid #3869D4;
display: inline-block;
color: #FFF;
text-decoration: none;
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
-webkit-text-size-adjust: none;
box-sizing: border-box;
}
.button--green {
background-color: #22BC66;
border-top: 10px solid #22BC66;
border-right: 18px solid #22BC66;
border-bottom: 10px solid #22BC66;
border-left: 18px solid #22BC66;
}
.button--red {
background-color: #FF6136;
border-top: 10px solid #FF6136;
border-right: 18px solid #FF6136;
border-bottom: 10px solid #FF6136;
border-left: 18px solid #FF6136;
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
text-align: center !important;
}
}
/* Attribute list ------------------------------ */
.attributes {
margin: 0 0 21px;
}
.attributes_content {
background-color: #F4F4F7;
padding: 16px;
}
.attributes_item {
padding: 0;
}
/* Related Items ------------------------------ */
.related {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.related_item {
padding: 10px 0;
color: #CBCCCF;
font-size: 15px;
line-height: 18px;
}
.related_item-title {
display: block;
margin: .5em 0 0;
}
.related_item-thumb {
display: block;
padding-bottom: 10px;
}
.related_heading {
border-top: 1px solid #CBCCCF;
text-align: center;
padding: 25px 0 10px;
}
/* Discount Code ------------------------------ */
.discount {
width: 100%;
margin: 0;
padding: 24px;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #F4F4F7;
border: 2px dashed #CBCCCF;
}
.discount_heading {
text-align: center;
}
.discount_body {
text-align: center;
font-size: 15px;
}
/* Social Icons ------------------------------ */
.social {
width: auto;
}
.social td {
padding: 0;
width: auto;
}
.social_icon {
height: 20px;
margin: 0 8px 10px 8px;
padding: 0;
}
/* Data table ------------------------------ */
.purchase {
width: 100%;
margin: 0;
padding: 35px 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_content {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_item {
padding: 10px 0;
color: #51545E;
font-size: 15px;
line-height: 18px;
}
.purchase_heading {
padding-bottom: 8px;
border-bottom: 1px solid #EAEAEC;
}
.purchase_heading p {
margin: 0;
color: #85878E;
font-size: 12px;
}
.purchase_footer {
padding-top: 15px;
border-top: 1px solid #EAEAEC;
}
.purchase_total {
margin: 0;
text-align: right;
font-weight: bold;
color: #333333;
}
.purchase_total--label {
padding: 0 15px 0 0;
}
body {
background-color: #F2F4F6;
color: #51545E;
}
p {
color: #51545E;
}
.email-wrapper {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #F2F4F6;
}
.email-content {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
/* Masthead ----------------------- */
.email-masthead {
padding: 25px 0;
text-align: center;
}
.email-masthead_logo {
width: 94px;
}
.email-masthead_name {
font-size: 16px;
font-weight: bold;
color: #A8AAAF;
text-decoration: none;
text-shadow: 0 1px 0 white;
}
/* Body ------------------------------ */
.email-body {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.email-body_inner {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #FFFFFF;
}
.email-footer {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.email-footer p {
color: #A8AAAF;
}
.body-action {
width: 100%;
margin: 30px auto;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.body-sub {
margin-top: 25px;
padding-top: 25px;
border-top: 1px solid #EAEAEC;
}
.content-cell {
padding: 45px;
}
/*Media Queries ------------------------------ */
@media only screen and (max-width: 600px) {
.email-body_inner,
.email-footer {
width: 100% !important;
}
}
@media (prefers-color-scheme: dark) {
body,
.email-body,
.email-body_inner,
.email-content,
.email-wrapper,
.email-masthead,
.email-footer {
background-color: #333333 !important;
color: #FFF !important;
}
p,
ul,
ol,
blockquote,
h1,
h2,
h3,
span,
.purchase_item {
color: #FFF !important;
}
.attributes_content,
.discount {
background-color: #222 !important;
}
.email-masthead_name {
text-shadow: none !important;
}
}
:root {
color-scheme: light dark;
supported-color-schemes: light dark;
}
</style>
<!--[if mso]>
<style type="text/css">
.f-fallback {
font-family: Arial, sans-serif;
}
</style>
<![endif]-->
<style type="text/css" rel="stylesheet" media="all">
body {
width: 100% !important;
height: 100%;
margin: 0;
-webkit-text-size-adjust: none;
}
body {
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
}
body {
background-color: #F2F4F6;
color: #51545E;
}
</style>
</head>
<body style="width: 100% !important; height: 100%; -webkit-text-size-adjust: none; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; background-color: #F2F4F6; color: #51545E; margin: 0;" bgcolor="#F2F4F6">
<span class="preheader" style="display: none !important; visibility: hidden; mso-hide: all; font-size: 1px; line-height: 1px; max-height: 0; max-width: 0; opacity: 0; overflow: hidden;">We would like to inform you that the registry {registry} has initiated a contact verification process for your domain registration data.</span>
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; background-color: #F2F4F6; margin: 0; padding: 0;" bgcolor="#F2F4F6">
<tr>
<td align="center" style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px;">
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0; padding: 0;">
<tr>
<td class="email-masthead" style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px; text-align: center; padding: 25px 0;" align="center">
<a href="{app_url}" class="f-fallback email-masthead_name" style="color: #A8AAAF; font-size: 16px; font-weight: bold; text-decoration: none; text-shadow: 0 1px 0 white;">
{registry}
</a>
</td>
</tr>
<!-- Email Body -->
<tr>
<td class="email-body" width="570" cellpadding="0" cellspacing="0" style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px; width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0; padding: 0;">
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation" style="width: 570px; -premailer-width: 570px; -premailer-cellpadding: 0; -premailer-cellspacing: 0; background-color: #FFFFFF; margin: 0 auto; padding: 0;" bgcolor="#FFFFFF">
<!-- Body content -->
<tr>
<td class="content-cell" style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px; padding: 45px;">
<div class="f-fallback">
<h1 style="margin-top: 0; color: #333333; font-size: 22px; font-weight: bold; text-align: left;" align="left">Contact Verification Required</h1>
<p style="font-size: 16px; line-height: 1.625; color: #51545E; margin: .4em 0 1.1875em;">Dear Contact {identifier},</p>
<p style="font-size: 16px; line-height: 1.625; color: #51545E; margin: .4em 0 1.1875em;">We would like to inform you that the registry {registry} has initiated a <strong>contact verification process</strong> for your domain registration data.</p>
<p style="font-size: 16px; line-height: 1.625; color: #51545E; margin: .4em 0 1.1875em;">To <strong>ensure compliance</strong> with our domain policies, please complete the verification by following the link below:</p>
<!-- Action -->
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; text-align: center; margin: 30px auto; padding: 0;">
<tr>
<td align="center" style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px;">
<!-- Border based button
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center" style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px;">
<a href="{link}" class="f-fallback button" target="_blank" style="color: #FFF; background-color: #3869D4; display: inline-block; text-decoration: none; border-radius: 3px; box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16); -webkit-text-size-adjust: none; box-sizing: border-box; border-color: #3869D4; border-style: solid; border-width: 10px 18px;">Start Validation</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p style="font-size: 16px; line-height: 1.625; color: #51545E; margin: .4em 0 1.1875em;">This verification is <strong>required</strong> to confirm the accuracy of your contact details associated with one or more domain names.</p>
<p style="font-size: 16px; line-height: 1.625; color: #51545E; margin: .4em 0 1.1875em;">If the verification is not completed within <strong>15 days</strong>, it may result in temporary suspension or limitations on your domain services. We kindly urge you to complete the verification as soon as possible to avoid any disruptions.</p>
<p style="font-size: 16px; line-height: 1.625; color: #51545E; margin: .4em 0 1.1875em;">Thank you for choosing {registry} for your needs. We are here to help!</p>
<p style="font-size: 16px; line-height: 1.625; color: #51545E; margin: .4em 0 1.1875em;">Kind regards,
<br /><strong>{registry} Support Team</strong></p>
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px;">
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation" style="width: 570px; -premailer-width: 570px; -premailer-cellpadding: 0; -premailer-cellspacing: 0; text-align: center; margin: 0 auto; padding: 0;">
<tr>
<td class="content-cell" align="center" style="word-break: break-word; font-family: &quot;Nunito Sans&quot;, Helvetica, Arial, sans-serif; font-size: 16px; padding: 45px;">
<p class="f-fallback sub align-center" style="font-size: 13px; line-height: 1.625; text-align: center; color: #A8AAAF; margin: .4em 0 1.1875em;" align="center">
Powered by <a href="{app_url}" target="_blank">{app_name}</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View file

@ -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) {