Skip to content
Code-Schnipsel Gruppen Projekte
Commit 69f53440 erstellt von Patrick Müller's avatar Patrick Müller
Dateien durchsuchen

feat: address verification

Übergeordneter 3e22ce9e
Keine zugehörigen Branchen gefunden
Keine zugehörigen Tags gefunden
2 Merge Requests!17feat: improve verification design,!13Update 'next-4.x' with latest changes from 'main'
Pipeline-Nr. 13990 fehlgeschlagen
werden angezeigt mit 462 Ergänzungen und 17 Löschungen
......@@ -18,8 +18,16 @@
<en><![CDATA[QUIQQER - Verifier]]></en>
</locale>
<locale name="site.types.verifier.description">
<de><![CDATA[Seite, zu der der Nutzer hingeleitet wird, um einen Prozess zu verifizieren]]></de>
<en><![CDATA[Site to which the user is directed to verify a process]]></en>
<de><![CDATA[Seite, zu der der Nutzer hingeleitet wird, um einen Prozess zu verifizieren.]]></de>
<en><![CDATA[Site to which the user is directed to verify a process.]]></en>
</locale>
<locale name="site.types.verifyAddress">
<de><![CDATA[Adress-Verifizierung]]></de>
<en><![CDATA[Address verification]]></en>
</locale>
<locale name="site.types.verifyAddress.description">
<de><![CDATA[Verifizierungs-Code Eingabe für Adress-Verifizierungen.]]></de>
<en><![CDATA[Verification code input for address verifications.]]></en>
</locale>
<!-- Settings -->
......@@ -105,6 +113,51 @@
</groups>
<groups name="quiqqer/verification" datatype="php">
<locale name="controls.AddressVerification.code_info">
<de><![CDATA[Bitte bestätigen Sie den folgenden Verifizierungs-Code, der Ihnen per Post zugestellt wurde.]]></de>
<en><![CDATA[Pleae confirm the following verification code that has been sent to you by mail.]]></en>
</locale>
<locale name="message.types.verifyAddress.error.general">
<de><![CDATA[Dieser Vorgang ist z.Z. nicht möglich. Wenn Sie dies für einen Fehler halten, wenden Sie sich bitte an den Support.]]></de>
<en><![CDATA[This process is currently not possible. If you think this is an error, please contact support.]]></en>
</locale>
<locale name="controls.AddressVerification.address_info">
<de><![CDATA[Folgende Adresse soll verifiziert werden:]]></de>
<en><![CDATA[The following address shall be verified:]]></en>
</locale>
<locale name="controls.AddressVerification.btn.confirm_code">
<de><![CDATA[Code bestätigen]]></de>
<en><![CDATA[Confirm code]]></en>
</locale>
<locale name="exception.controls.AddressVerification.error.already_verified">
<de><![CDATA[Diese Adresse wurde bereits verifiziert.]]></de>
<en><![CDATA[This address has already been verified.]]></en>
</locale>
<locale name="exception.controls.AddressVerification.error.expired">
<de><![CDATA[Der Verifizierungs-Code für diese Adresse ist abgelaufen. Bitte starten Sie die Verifizierung erneut.]]></de>
<en><![CDATA[The verification code for this address has expired. Please restart the verification process.]]></en>
</locale>
<locale name="exception.controls.AddressVerification.error.invalid_code">
<de><![CDATA[Ungültiger Verifizierungs-Code.]]></de>
<en><![CDATA[Invlice verification code.]]></en>
</locale>
<locale name="exception.controls.AddressVerification.error.general">
<de><![CDATA[Bei der Prüfung des Verifizierungs-Codes ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut oder kontaktieren Sie den Support.]]></de>
<en><![CDATA[An error occurred while checking the verification code. Please try again or contact support.]]></en>
</locale>
<locale name="controls.AddressVerification.msg.success">
<de><![CDATA[Ihre Adresse wurde erfolgreich verifiziert!]]></de>
<en><![CDATA[Your address has been successfully verified!]]></en>
</locale>
<locale name="controls.AddressVerification.btn.continue">
<de><![CDATA[Weiter]]></de>
<en><![CDATA[Continue]]></en>
</locale>
<locale name="controls.AddressVerification.title">
<de><![CDATA[Adress-Verifizierung]]></de>
<en><![CDATA[Address verification]]></en>
</locale>
<!-- Class: Verifier -->
<locale name="exception.verifier.verification.already.exists">
......
......@@ -9,6 +9,12 @@
<locale group="quiqqer/verification" var="site.types.verifier.description" />
</desc>
</type>
<type type="types/verifyAddress" icon="fa fa-address-book">
<locale group="quiqqer/verification" var="site.types.verifyAddress" />
<desc>
<locale group="quiqqer/verification" var="site.types.verifyAddress.description" />
</desc>
</type>
</types>
......
<h1>
{locale group="quiqqer/verification" var="controls.AddressVerification.title"}
</h1>
{if $errorMsg}
<div class="content-message-error">
{$errorMsg}
</div>
{elseif $success}
<div class="content-message-success">
{locale group="quiqqer/verification" var="controls.AddressVerification.msg.success"}
</div>
{if $onSuccessRedirectUrl}
<a href="{$onSuccessRedirectUrl}">
<button type="button">
{locale group="quiqqer/verification" var="controls.AddressVerification.btn.continue"}
</button>
</a>
{/if}
{elseif $showAddressAndCodeInput}
<div class="quiqqer-verification-AddressVerification-address">
<p>{locale group="quiqqer/verification" var="controls.AddressVerification.address_info"}</p>
{$address->getDisplay()}
</div>
<div class="quiqqer-verification-AddressVerification-info">
{locale group="quiqqer/verification" var="controls.AddressVerification.code_info"}
</div>
<form method="POST">
<label>
<span>Code</span>
<input type="text" name="verificationCode" value="{$verificationCode}"/>
</label>
<button type="submit">
{locale group="quiqqer/verification" var="controls.AddressVerification.btn.confirm_code"}
</button>
</form>
{/if}
\ No newline at end of file
<?php
namespace QUI\Verification\Controls;
use DateMalformedStringException;
use Libellio\B2C\Core\User\Attribute\Verification\AddressVerificationHandler;
use QUI;
use QUI\Exception;
use QUI\Utils\Security\Orthos;
use QUI\Verification\Entity\AddressVerification as AddressVerificationEntity;
use QUI\Verification\Enum\VerificationStatus;
use QUI\Verification\Exception as VerificationException;
use QUI\Verification\Exception\CannotBuildVerificationException;
use QUI\Verification\Exception\InvalidVerificationCodeException;
use QUI\Verification\Exception\VerificationAlreadyVerifiedException;
use QUI\Verification\Exception\VerificationExpiredException;
use QUI\Verification\Interface\VerificationRepositoryInterface;
use QUI\Verification\Interface\VerifierInterface;
use QUI\Verification\VerificationRepository;
use QUI\Verification\Verifier;
use function is_null;
class AddressVerification extends QUI\Control
{
private AddressVerificationEntity $addressVerification;
/**
* constructor.
*
* @param string $verificationUuid
* @param VerificationRepositoryInterface|null $verificationRepository
* @param VerifierInterface|null $verifier
* @param array $attributes
*
* @throws Exception
* @throws VerificationException
* @throws CannotBuildVerificationException
* @throws DateMalformedStringException
* @throws \Doctrine\DBAL\Exception
*/
public function __construct(
string $verificationUuid,
private ?VerificationRepositoryInterface $verificationRepository = null,
private ?VerifierInterface $verifier = null,
array $attributes = []
) {
if (is_null($this->verificationRepository)) {
$this->verificationRepository = new VerificationRepository();
}
if (is_null($this->verifier)) {
$this->verifier = new Verifier();
}
$verification = $this->verificationRepository->findByUuid($verificationUuid);
if (!($verification instanceof AddressVerificationEntity)) {
throw new VerificationException("Verification not found: {$verificationUuid}");
}
$this->addressVerification = $verification;
parent::__construct($attributes);
$this->setAttributes($attributes);
$this->addCSSFile(dirname(__FILE__) . '/AddressVerification.css');
$this->addCSSClass('quiqqer-verification-AddressVerification');
}
/**
* Return the control body
*
* @return string
*/
public function getBody(): string
{
$Engine = QUI::getTemplateManager()->getEngine();
$L = QUI::getLocale();
$errorMsg = null;
$showAddressAndCodeInput = true;
$success = false;
$onSuccessRedirectUrl = null;
if ($this->addressVerification->status === VerificationStatus::VERIFIED) {
$errorMsg = $L->get(
'quiqqer/verification',
'exception.controls.AddressVerification.error.already_verified'
);
$showAddressAndCodeInput = false;
} elseif ($this->addressVerification->status === VerificationStatus::EXPIRED) {
$errorMsg = $L->get(
'quiqqer/verification',
'exception.controls.AddressVerification.error.expired'
);
$showAddressAndCodeInput = false;
} elseif (!empty($_POST['verificationCode'])) {
$verificationCode = Orthos::clear($_POST['verificationCode']);
try {
// Just verify the code to catch exceptions
$this->verifier->verifyAddressVerification($this->addressVerification, $verificationCode);
/** @var AddressVerificationHandler $verificationHandler */
$verificationHandler = $this->verificationRepository->getVerificationHandler(
$this->addressVerification
);
$onSuccessRedirectUrl = $verificationHandler->getOnSuccessRedirectUrl($this->addressVerification);
$success = true;
} catch (VerificationExpiredException $exception) {
QUI\System\Log::writeDebugException($exception);
$errorMsg = $L->get(
'quiqqer/verification',
'exception.controls.AddressVerification.error.expired'
);
$showAddressAndCodeInput = false;
} catch (VerificationAlreadyVerifiedException $exception) {
QUI\System\Log::writeDebugException($exception);
$errorMsg = $L->get(
'quiqqer/verification',
'exception.controls.AddressVerification.error.already_verified'
);
$showAddressAndCodeInput = false;
} catch (InvalidVerificationCodeException $exception) {
QUI\System\Log::writeDebugException($exception);
$errorMsg = $L->get(
'quiqqer/verification',
'exception.controls.AddressVerification.error.invalid_code'
);
} catch (\Exception|\Throwable $exception) {
if ($exception instanceof Exception) {
QUI\System\Log::writeException($exception);
}
$errorMsg = $L->get(
'quiqqer/verification',
'exception.controls.AddressVerification.error.general'
);
}
}
$Engine->assign([
'success' => $success,
'onSuccessRedirectUrl' => $onSuccessRedirectUrl,
'showAddressAndCodeInput' => $showAddressAndCodeInput,
'errorMsg' => $errorMsg,
'address' => $this->addressVerification->address,
'verificationCode' => $this->addressVerification->verificationCode
]);
return $Engine->fetch(dirname(__FILE__) . '/AddressVerification.html');
}
}
<address class="vcard">
<div class="adr">
{if $company}
<div class="company">
{$company}
</div>
{/if}
<div class="name">{if $salutation}{$salutation} {/if}{$name}</div>
<div class="street-address">{$street} {$houseNumber}</div>
<div class="locality">
{if $countryCode}
<span class="postal-country">{$countryCode}-</span>
{/if}
<span class="postal-code">{$zipCode}</span>
<span class="postal-city">{$city}</span>
</div>
</div>
</address>
\ No newline at end of file
......@@ -2,7 +2,11 @@
namespace QUI\Verification\Entity;
final class Address
use QUI;
use function dirname;
readonly class Address
{
public function __construct(
public string $name,
......@@ -10,7 +14,9 @@ public function __construct(
public string $houseNumber,
public string $zipCode,
public string $city,
public string $countryCode
public string $countryCode,
public ?string $salutation = null,
public ?string $company = null
) {
}
......@@ -26,9 +32,24 @@ public function toArray(): array
'zipCode' => $this->zipCode,
'city' => $this->city,
'countryCode' => $this->countryCode,
'salutation' => $this->salutation,
'company' => $this->company
];
}
/**
* Return the address as HTML display
*
* @return string - HTML <address>
*/
public function getDisplay(): string
{
$Engine = QUI::getTemplateManager()->getEngine(true);
$Engine->assign($this->toArray());
return $Engine->fetch(dirname(__FILE__) . '/Address.html');
}
/**
* @param array<string,string> $data
* @return Address
......@@ -41,7 +62,9 @@ public static function fromArray(array $data): Address
$data['houseNumber'],
$data['zipCode'],
$data['city'],
$data['countryCode']
$data['countryCode'],
$data['salutation'] ?? null,
$data['company'] ?? null
);
}
}
......@@ -27,11 +27,13 @@ public function __construct(
DateTimeImmutable $updateDate,
int $tries,
public readonly Address $address,
public readonly string $verificationUrl,
VerificationStatus $status = VerificationStatus::PENDING,
array $customData = [],
?DateTimeImmutable $validUntilDate = null
) {
$customData['address'] = $address->toArray();
$customData['verificationUrl'] = $verificationUrl;
parent::__construct(
$uuid,
......@@ -63,6 +65,7 @@ public static function fromArray(array $data): static
new DateTimeImmutable($data['updateDate']),
(int)$data['tries'],
Address::fromArray($data['customData']['address']),
$data['customData']['verificationUrl'],
VerificationStatus::from($data['status']),
$data['customData'],
!empty($data['validUntilDate']) ? new DateTimeImmutable($data['validUntilDate']) : null
......
......@@ -23,4 +23,12 @@ public function onSuccess(AddressVerification $verification): void;
* @return void
*/
public function onError(AddressVerification $verification, VerificationErrorReason $reason): void;
/**
* Automatically redirect the user to this URL on successful verification
*
* @param AddressVerification $verification
* @return string|null - on NULL, no redirection takes place
*/
public function getOnSuccessRedirectUrl(AddressVerification $verification): ?string;
}
......@@ -4,6 +4,7 @@
use QUI\Verification\Entity\AbstractVerification;
use QUI\Verification\Entity\PhoneNumberVerification;
use QUI\Verification\Entity\AddressVerification;
interface VerifierInterface
{
......@@ -27,4 +28,11 @@ public function verifyPhoneNumberVerification(
* @return void
*/
public function verifyVerificationCode(AbstractVerification $verification, string $verificationCode): void;
/**
* @param AddressVerification $verification
* @param string $verificationCode
* @return void
*/
public function verifyAddressVerification(AddressVerification $verification, string $verificationCode): void;
}
......@@ -4,7 +4,6 @@
use Composer\Semver\Semver;
use QUI;
use QUI\Verification\Interface\VerificationHandlerInterface;
use function is_null;
......@@ -13,7 +12,8 @@ class Utils
/**
* Verifier site type
*/
const SITE_TYPE = 'quiqqer/verification:types/verifier';
const SITE_TYPE_VERIFIER = 'quiqqer/verification:types/verifier';
const SITE_TYPE_VERIFY_ADDRESS = 'quiqqer/verification:types/verifyAddress';
/**
* Get formatted timestamp for a given UNIX timestamp
......@@ -47,7 +47,38 @@ public static function getVerifierSite(): QUI\Interfaces\Projects\Site
$siteIds = $Project->getSitesIds([
'where' => [
'type' => self::SITE_TYPE
'type' => self::SITE_TYPE_VERIFIER
]
]);
if (empty($siteIds)) {
throw new QUI\Verification\Exception([
'quiqqer/verification',
'exception.verifier.site.does.not.exist'
]);
}
return $Project->get($siteIds[0]['id']);
}
/**
* Get verifier Site for address verification.
*
* @return QUI\Interfaces\Projects\Site
* @throws QUI\Verification\Exception
* @throws QUI\Database\Exception|QUI\Exception
*/
public static function getAddressVerifierSite(): QUI\Interfaces\Projects\Site
{
$Project = QUI::getRewrite()->getProject();
if (is_null($Project)) {
throw new Exception("Cannot load QUIQQER project to fetch verifier site.");
}
$siteIds = $Project->getSitesIds([
'where' => [
'type' => self::SITE_TYPE_VERIFY_ADDRESS
]
]);
......
......@@ -226,6 +226,11 @@ public function createAddressVerification(
$verificationCode = $this->verificationCodeFactory->createRandomDigitsCode();
}
$verifierSite = Utils::getAddressVerifierSite();
$url = $verifierSite->getUrlRewrittenWithHost([], [
'id' => $uuid,
]);
$addressVerification = new AddressVerification(
$uuid,
$identifier,
......@@ -234,6 +239,7 @@ public function createAddressVerification(
new DateTimeImmutable(),
0,
$address,
$url,
VerificationStatus::PENDING,
$customData
);
......
......@@ -14,6 +14,7 @@
use QUI\Verification\Exception\CannotBuildVerificationException;
use QUI\Verification\Interface\VerificationHandlerInterface;
use QUI\Verification\Interface\VerificationRepositoryInterface;
use QUI\Verification\Entity\AddressVerification;
use function date;
use function is_null;
......@@ -286,6 +287,16 @@ private function parseDbRowToVerification(array $row): AbstractVerification
);
}
case AddressVerification::class:
try {
return AddressVerification::fromArray($row);
} catch (\Exception $exception) {
QUI\System\Log::writeException($exception);
throw new CannotBuildVerificationException(
"Cannot build " . AddressVerification::class . " from database row #{$row['id']}"
);
}
default:
throw new Exception("Invalid verification class: {$row['verificationClass']}");
}
......
......@@ -3,17 +3,22 @@
namespace QUI\Verification;
use QUI;
use QUI\Exception;
use QUI\Verification\Entity\AbstractVerification;
use QUI\Verification\Entity\AddressVerification;
use QUI\Verification\Entity\PhoneNumberVerification;
use QUI\Verification\Enum\VerificationErrorReason;
use QUI\Verification\Enum\VerificationStatus;
use QUI\Verification\Exception\InvalidVerificationCodeException;
use QUI\Verification\Exception\VerificationAlreadyVerifiedException;
use QUI\Verification\Exception\VerificationExpiredException;
use QUI\Verification\Interface\AddressVerificationHandlerInterface;
use QUI\Verification\Interface\PhoneNumberVerificationHandlerInterface;
use QUI\Verification\Interface\VerificationRepositoryInterface;
use QUI\Verification\Interface\VerifierInterface;
use function is_null;
class Verifier implements VerifierInterface
{
/**
......@@ -91,33 +96,66 @@ public function verifyVerificationCode(AbstractVerification $verification, strin
* @param string $verificationCode
* @return void
*
* @throws Exception
* @throws \Doctrine\DBAL\Exception
* @throws \Exception
* @throws InvalidVerificationCodeException
* @throws VerificationAlreadyVerifiedException
* @throws VerificationExpiredException
*/
public function verifyPhoneNumberVerification(PhoneNumberVerification $verification, string $verificationCode): void
{
$errorReason = null;
/** @var PhoneNumberVerificationHandlerInterface $verificationHandler */
$verificationHandler = $this->verificationRepository->getVerificationHandler($verification);
try {
$this->verifyVerificationCode($verification, $verificationCode);
$verificationHandler->onSuccess($verification);
} catch (VerificationAlreadyVerifiedException $exception) {
QUI\System\Log::writeDebugException($exception);
$errorReason = VerificationErrorReason::ALREADY_VERIFIED;
$verificationHandler->onError($verification, VerificationErrorReason::ALREADY_VERIFIED);
throw $exception;
} catch (VerificationExpiredException $exception) {
QUI\System\Log::writeDebugException($exception);
$errorReason = VerificationErrorReason::EXPIRED;
$verificationHandler->onError($verification, VerificationErrorReason::EXPIRED);
throw $exception;
} catch (InvalidVerificationCodeException $exception) {
QUI\System\Log::writeDebugException($exception);
$errorReason = VerificationErrorReason::INVALID_CODE;
$verificationHandler->onError($verification, VerificationErrorReason::INVALID_CODE);
throw $exception;
}
}
/** @var PhoneNumberVerificationHandlerInterface $verificationHandler */
/**
* @param AddressVerification $verification
* @param string $verificationCode
* @return void
*
* @throws Exception
* @throws \Doctrine\DBAL\Exception
* @throws InvalidVerificationCodeException
* @throws VerificationAlreadyVerifiedException
* @throws VerificationExpiredException
*/
public function verifyAddressVerification(AddressVerification $verification, string $verificationCode): void
{
/** @var AddressVerificationHandlerInterface $verificationHandler */
$verificationHandler = $this->verificationRepository->getVerificationHandler($verification);
if (is_null($errorReason)) {
try {
$this->verifyVerificationCode($verification, $verificationCode);
$verificationHandler->onSuccess($verification);
} else {
$verificationHandler->onError($verification, $errorReason);
} catch (VerificationAlreadyVerifiedException $exception) {
QUI\System\Log::writeDebugException($exception);
$verificationHandler->onError($verification, VerificationErrorReason::ALREADY_VERIFIED);
throw $exception;
} catch (VerificationExpiredException $exception) {
QUI\System\Log::writeDebugException($exception);
$verificationHandler->onError($verification, VerificationErrorReason::EXPIRED);
throw $exception;
} catch (InvalidVerificationCodeException $exception) {
QUI\System\Log::writeDebugException($exception);
$verificationHandler->onError($verification, VerificationErrorReason::INVALID_CODE);
throw $exception;
}
}
}
{if $errorMsg}
<div class="content-message-error">
{$errorMsg}
</div>
{else}
{$addressVerificationControl->create()}
{/if}
\ No newline at end of file
<?php
use QUI\Permissions\Exception as QUIPermissionException;
use QUI\Verification\Controls\AddressVerification;
/** @var QUI\Projects\Project $Project */
/** @var QUI\Projects\Site $Site */
/** @var QUI\Interfaces\Template\EngineInterface $Engine */
function redirect(string $target): never
{
header('Location: ' . $target);
exit;
}
$User = QUI::getUserBySession();
// User must be authenticated; if not -> throw permission exception so QUIQQER shows the login form
if (!QUI::getUsers()->isAuth($User)) {
throw new QUIPermissionException("Please authenticate.", 401);
}
if (empty($_GET['id'])) {
redirect('/');
}
$errorMsg = null;
try {
$addressVerification = new AddressVerification($_GET['id']);
} catch (\Exception $exception) {
QUI\System\Log::writeException($exception);
$errorMsg = QUI::getLocale()->get(
'quiqqer/verification', 'message.types.verifyAddress.error.general'
);
}
$Engine->assign([
'errorMsg' => $errorMsg,
'addressVerificationControl' => $addressVerification ?? null
]);
0% Lade oder .
You are about to add 0 people to the discussion. Proceed with caution.
Bearbeitung dieser Nachricht zuerst beenden!
Bitte registrieren oder zum Kommentieren