From d80831d36e0a3987bfff0cf505597812e213c560 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Mon, 3 Mar 2025 14:34:07 +0200 Subject: [PATCH] Resend Confirmation Code --- app/plugins/CoreEnroller/config/routes.php | 53 +++++ .../resources/locales/en_US/core_enroller.po | 22 +- .../Controller/EmailVerifiersController.php | 50 +++++ .../src/Model/Table/EmailVerifiersTable.php | 55 +++-- .../templates/EmailVerifiers/dispatch.inc | 130 ++---------- .../templates/element/emailVerifiers/list.php | 127 ++++++++++++ .../element/emailVerifiers/resendLinkSpa.php | 188 ++++++++++++++++++ .../element/emailVerifiers/verify.php | 91 +++++++++ app/resources/locales/en_US/error.po | 6 + app/resources/locales/en_US/operation.po | 3 + app/src/Command/TransmogrifyCommand.php | 2 +- app/templates/Standard/dispatch.php | 8 +- app/webroot/css/co-base.css | 8 + 13 files changed, 605 insertions(+), 138 deletions(-) create mode 100644 app/plugins/CoreEnroller/config/routes.php create mode 100644 app/plugins/CoreEnroller/templates/element/emailVerifiers/list.php create mode 100644 app/plugins/CoreEnroller/templates/element/emailVerifiers/resendLinkSpa.php create mode 100644 app/plugins/CoreEnroller/templates/element/emailVerifiers/verify.php diff --git a/app/plugins/CoreEnroller/config/routes.php b/app/plugins/CoreEnroller/config/routes.php new file mode 100644 index 000000000..1cfe2b477 --- /dev/null +++ b/app/plugins/CoreEnroller/config/routes.php @@ -0,0 +1,53 @@ +plugin( + 'CoreEnroller', + ['path' => '/core-enroller/'], + function ($routes) { + $routes->setRouteClass(DashedRoute::class); + + $routes->get( + 'email-verifiers/resend', + [ + 'plugin' => 'CoreEnroller', + 'controller' => 'EmailVerifiers', + 'action' => 'resend', + ]) + ->setPass(['id']) + ->setPatterns(['id' => '[0-9]+']); + } +); \ No newline at end of file diff --git a/app/plugins/CoreEnroller/resources/locales/en_US/core_enroller.po b/app/plugins/CoreEnroller/resources/locales/en_US/core_enroller.po index 5a41e73cb..0a1f6f085 100644 --- a/app/plugins/CoreEnroller/resources/locales/en_US/core_enroller.po +++ b/app/plugins/CoreEnroller/resources/locales/en_US/core_enroller.po @@ -98,7 +98,22 @@ msgid "information.EmailVerifiers.A" msgstr "The following email addresses have been found in this Petition. You must verify all of them in order to proceed to the next Enrollment Step." msgid "information.EmailVerifiers.code_sent" -msgstr "A code has been sent to {0}. Please enter it below. You may also request a new code if you haven't received it after a few minutes, or cancel verification and return to the list of available Email Addresses." +msgstr "A code has been sent to {0}. Please enter it below. You may also request a new code if you haven't received it after a few minutes, or cancel verification and return to the list of available Email Addresses." + +msgid "information.EmailVerifiers.resend-pre-text" +msgstr "Didn't receive the code?" + +msgid "information.EmailVerifiers.resend" +msgstr "Resend" + +msgid "information.EmailVerifiers.sending" +msgstr "Sending" + +msgid "information.EmailVerifiers.success" +msgstr "New Code Submitted!" + +msgid "information.EmailVerifiers.abort" +msgstr "Abort" msgid "field.AttributeCollectors.valid_through.default.after.desc" msgstr "Days After Finalization" @@ -257,4 +272,7 @@ msgid "result.InvitationAccepters.declined" msgstr "Invitation Declined at {0}" msgid "result.InvitationAccepters.none" -msgstr "No response to invitation yet" \ No newline at end of file +msgstr "No response to invitation yet" + +msgid "op.EmailVerifiers.verify" +msgstr "Verify Email" \ No newline at end of file diff --git a/app/plugins/CoreEnroller/src/Controller/EmailVerifiersController.php b/app/plugins/CoreEnroller/src/Controller/EmailVerifiersController.php index 23dd271df..ac86386d9 100644 --- a/app/plugins/CoreEnroller/src/Controller/EmailVerifiersController.php +++ b/app/plugins/CoreEnroller/src/Controller/EmailVerifiersController.php @@ -29,6 +29,7 @@ namespace CoreEnroller\Controller; +use Cake\Http\Exception\BadRequestException; use Cake\ORM\TableRegistry; use App\Controller\StandardEnrollerController; use App\Lib\Enum\PetitionStatusEnum; @@ -60,9 +61,58 @@ public function beforeRender(\Cake\Event\EventInterface $event) { $this->set('vv_bc_parent_primarykey', $this->EmailVerifiers->EnrollmentFlowSteps->getPrimaryKey()); } + if ($this->getRequest()->getQuery("op") == "verify" || $this->getRequest()->getQuery("op") == "index") { + // This will suppress the default behavior. By default, we print the submit button in the + // unorderedList.php element. But for the verify view we want to override and customize + $this->set('suppress_submit', true); + } + return parent::beforeRender($event); } + /** + * Resend the email verification request. + * + * @param string $id Email Verifier ID + * @throws BadRequestException If the request is not AJAX + * @throws \InvalidArgumentException If required query parameters are missing + * @return void + * @since COmanage Registry v5.1.0 + */ + public function resend($id) + { + + $this->viewBuilder()->setClassName('Json'); + + if (!$this->getRequest()->is('ajax')) { + throw new BadRequestException(__('Bad Request')); + } + + if (!$this->getRequest()->getQuery('petition_id') || !$this->getRequest()->getQuery('m')) { + throw new \InvalidArgumentException(__('error', 'invalid.request')); + } + + // Generate a Verification request and send it + $Petitions = TableRegistry::getTableLocator()->get('Petitions'); + $petition = $Petitions->get($this->getRequest()->getQuery('petition_id')); + $cfg = $this->EmailVerifiers->get($id); + $mail = StringUtilities::urlbase64decode($this->requestParam('m')); + $status = $this->EmailVerifiers->sendVerificationRequest($cfg, $petition, $mail, true); + + if ($status) { + return $this->response + ->withType('application/json') + ->withStatus(200) + ->withStringBody(json_encode(['status' => 'ok'])); + } + + + return $this->response + ->withType('application/json') + ->withStatus(501) + ->withStringBody(json_encode(['status' => 'failed'])); + } + /** * Dispatch an Enrollment Flow Step. * diff --git a/app/plugins/CoreEnroller/src/Model/Table/EmailVerifiersTable.php b/app/plugins/CoreEnroller/src/Model/Table/EmailVerifiersTable.php index c515f1f0b..4f64ed4ef 100644 --- a/app/plugins/CoreEnroller/src/Model/Table/EmailVerifiersTable.php +++ b/app/plugins/CoreEnroller/src/Model/Table/EmailVerifiersTable.php @@ -85,7 +85,7 @@ public function initialize(array $config): void { $this->setPrimaryLink('enrollment_flow_step_id'); $this->setRequiresCO(true); - $this->setAllowLookupPrimaryLink(['dispatch', 'display']); + $this->setAllowLookupPrimaryLink(['dispatch', 'display', 'resend']); // All the tabs share the same configuration in the ModelTable file $this->setTabsConfig( @@ -112,6 +112,14 @@ public function initialize(array $config): void { 'type' => 'select', 'model' => 'MessageTemplates', 'where' => ['context' => \App\Lib\Enum\MessageTemplateContextEnum::Verification] + ], + 'cosettings' => [ + 'type' => 'auxiliary', + 'model' => 'CoSettings' + ], + 'types' => [ + 'type' => 'auxiliary', + 'model' => 'Types' ] ]); @@ -122,6 +130,7 @@ public function initialize(array $config): void { 'dispatch' => true, 'display' => true, 'edit' => ['platformAdmin', 'coAdmin'], + 'resend' => true, 'view' => ['platformAdmin', 'coAdmin'] ], // Actions that operate over a table (ie: do not require an $id) @@ -412,8 +421,10 @@ public function prepare( public function sendVerificationRequest( EmailVerifier $emailVerifier, Petition $petition, - string $mail - ) { + string $mail, + bool $resend = false, + ): bool + { // First check if there is already an existing Petition Verification. // If so, use that to get the existing Verification. @@ -427,21 +438,7 @@ public function sendVerificationRequest( ]) ->first(); - if(!empty($pVerification)) { - // Request a new code - - $this->llog('debug', "Sending replacement verification code to $mail for Petition " . $petition->id); - - $verificationId = $Verifications->requestCodeForPetition( - $petition->id, - $mail, - $emailVerifier->message_template_id, - $emailVerifier->request_validity, - $pVerification->verification_id - ); - - // There's nothing to update in the Petition Verification - } else { + if (empty($pVerification)) { // Request Verification and create an associated Petition Verification $this->llog('debug', "Sending verification code to $mail for Petition " . $petition->id); @@ -458,8 +455,28 @@ public function sendVerificationRequest( 'petition_id' => $petition->id, 'mail' => $mail, 'verification_id' => $verificationId - ])); + ])); + return true; } + + if ($resend) { + // Request a new code + + $this->llog('debug', "Sending replacement verification code to $mail for Petition " . $petition->id); + + $verificationId = $Verifications->requestCodeForPetition( + $petition->id, + $mail, + $emailVerifier->message_template_id, + $emailVerifier->request_validity, + $pVerification->verification_id + ); + // There's nothing to update in the Petition Verification + + return true; + } + + return false; } /** diff --git a/app/plugins/CoreEnroller/templates/EmailVerifiers/dispatch.inc b/app/plugins/CoreEnroller/templates/EmailVerifiers/dispatch.inc index 62b3b9c4e..c76132611 100644 --- a/app/plugins/CoreEnroller/templates/EmailVerifiers/dispatch.inc +++ b/app/plugins/CoreEnroller/templates/EmailVerifiers/dispatch.inc @@ -27,121 +27,21 @@ declare(strict_types = 1); -use CoreEnroller\Lib\Enum\VerificationModeEnum; -use App\Lib\Util\StringUtilities; - print $this->element('flash', []); // This view is intended to work with dispatch -if($vv_action == 'dispatch') { - if($vv_op == 'index') { - // Render the list of known email addresses and their verification statuses. - // The configuration drives how many email addresses are required to complete this step. - - print '

'; - - if($vv_all_done) { - print __d('core_enroller', 'information.EmailVerifiers.done'); - } else { - switch($vv_config->mode) { - case VerificationModeEnum::All: - print __d('core_enroller', 'information.EmailVerifiers.A'); - break; - case VerificationModeEnum::None: - print __d('core_enroller', 'information.EmailVerifiers.0'); - break; - case VerificationModeEnum::One: - if($vv_minimum_met) { - print __d('core_enroller', 'information.EmailVerifiers.1.met'); - } else { - print __d('core_enroller', 'information.EmailVerifiers.1.none'); - } - break; - } - } - - print '

'; - - print ' - - - - - - - - - '; - - foreach(array_keys($vv_email_addresses) as $addr) { - $verified = isset($vv_verified_addresses[$addr]) && $vv_verified_addresses[$addr]; - - $button = ""; - - if(!$verified) { - // We're already in a form here, so we need to use a GET URL to not mess things up. - // This also means we need to manually insert the token and petition ID, which is - // a bit duplicative with templates/Standard/dispatch.php - - $url = [ - 'plugin' => 'CoreEnroller', - 'controller' => 'email_verifiers', - 'action' => 'dispatch', - $vv_config->id, - '?' => [ - 'op' => 'verify', - 'petition_id' => $vv_petition->id, - // We base64 encode the address partly to not have bare email addresses in URLs - // and partly to avoid special characters (like dots) messing up the URL - 'm' => StringUtilities::urlbase64encode($addr) - ] - ]; - - if(isset($vv_token_ok) && $vv_token_ok && !empty($vv_petition->token)) { - $url['?']['token'] = $vv_petition->token; - } - - $button = $this->Html->link(__d('operation', 'verify'), $url); - } - - print ' - - - - - '; - } - - print ' - -
' . __d('controller', 'EmailAddresses', [1]) . '' . __d('field', 'status') . '
' . $addr . '' . __d('result', ($verified ? 'verified' : 'verified.not')) . $button . '
- '; - - if($vv_minimum_met) { - $this->Field->enableFormEditMode(); - - print $this->Form->hidden('op', ['default' => 'finish']); - } - } elseif($vv_op == 'verify') { - if(!empty($vv_verify_address)) { - // Render a form prompting for the code that was sent to the Enrollee - - print __d('core_enroller', 'information.EmailVerifiers.code_sent', [$vv_verify_address]); - - $this->Field->enableFormEditMode(); - - print $this->Form->hidden('op', ['default' => 'verify']); - print $this->Form->hidden('co_id', ['default' => $vv_cur_co->id]); - print $this->Form->hidden('m', ['default' => StringUtilities::urlbase64encode($vv_verify_address)]); - - print $this->element('form/listItem', [ - 'arguments' => [ - 'fieldName' => 'code', - 'fieldLabel' => "Code", //__d('field', 'mail') - 'fieldOptions' => [ - 'required' => true - ] - ]]); - } - } -} \ No newline at end of file +if ($vv_action !== 'dispatch') { + return; +} + +if ($vv_op == 'index') { + $this->set('vv_include_cancel', false); + $this->set('vv_submit_button_label', __d('operation', 'finish')); + print $this->element('CoreEnroller.emailVerifiers/list'); +} elseif ($vv_op == 'verify') { + $this->set('vv_submit_button_label', __d('core_enroller', 'op.EmailVerifiers.verify')); + $this->set('vv_include_cancel', true); + print $this->element('CoreEnroller.emailVerifiers/verify'); +} else { + print __d('error', 'something.went.wrong'); +} diff --git a/app/plugins/CoreEnroller/templates/element/emailVerifiers/list.php b/app/plugins/CoreEnroller/templates/element/emailVerifiers/list.php new file mode 100644 index 000000000..fdab1d278 --- /dev/null +++ b/app/plugins/CoreEnroller/templates/element/emailVerifiers/list.php @@ -0,0 +1,127 @@ +mode) { + VerificationModeEnum::All => __d('core_enroller', 'information.EmailVerifiers.A'), + VerificationModeEnum::None => __d('core_enroller', 'information.EmailVerifiers.0'), + VerificationModeEnum::One => $vv_minimum_met + ? __d('core_enroller', 'information.EmailVerifiers.1.met') + : __d('core_enroller', 'information.EmailVerifiers.1.none'), + default => 'Unknown Verification Mode' // Optional fallback for unexpected cases + }; + +} + +?> +

+ + + + + + + + + + + 'CoreEnroller', + 'controller' => 'email_verifiers', + 'action' => 'dispatch', + $vv_config->id, + '?' => [ + 'op' => 'verify', + 'petition_id' => $vv_petition->id, + // We base64 encode the address partly to not have bare email addresses in URLs + // and partly to avoid special characters (like dots) messing up the URL + 'm' => StringUtilities::urlbase64encode($addr) + ] + ]; + + if(isset($vv_token_ok) && $vv_token_ok && !empty($vv_petition->token)) { + $url['?']['token'] = $vv_petition->token; + } + + $materialIcon = ''; + $button = $this->Html->link( + $materialIcon . ' ' . __d('operation', 'verify'), + $url, + [ + 'class' => 'float-end', + 'escape' => false, + ] + ); + } + ?> + + + + + + +
+ + + + + + +
+ +Field->enableFormEditMode(); + + print $this->Form->hidden('op', ['default' => 'finish']); + + print $this->element('form/submit', ['label' => $vv_submit_button_label]); +} +?> + diff --git a/app/plugins/CoreEnroller/templates/element/emailVerifiers/resendLinkSpa.php b/app/plugins/CoreEnroller/templates/element/emailVerifiers/resendLinkSpa.php new file mode 100644 index 000000000..85644fd74 --- /dev/null +++ b/app/plugins/CoreEnroller/templates/element/emailVerifiers/resendLinkSpa.php @@ -0,0 +1,188 @@ +loadHelper('Vue'); + +$relativeUrl = "core-enroller/email-verifiers/resend/$vv_config->id" + . '?' + . "petition_id=$petitionId" + . "&m=$emailAddress" + +?> + + + +
diff --git a/app/plugins/CoreEnroller/templates/element/emailVerifiers/verify.php b/app/plugins/CoreEnroller/templates/element/emailVerifiers/verify.php new file mode 100644 index 000000000..8dcc7a4c2 --- /dev/null +++ b/app/plugins/CoreEnroller/templates/element/emailVerifiers/verify.php @@ -0,0 +1,91 @@ +Field->enableFormEditMode(); + +$m = StringUtilities::urlbase64encode($vv_verify_address); + +print $this->Form->hidden('op', ['default' => 'verify']); +print $this->Form->hidden('co_id', ['default' => $vv_cur_co->id]); +print $this->Form->hidden('m', ['default' => $m]); + +print $this->element('form/listItem', [ + 'arguments' => [ + 'fieldName' => 'code', + 'fieldLabel' => "Code", //__d('field', 'mail') + 'fieldOptions' => [ + 'required' => true + ] + ]]); + +$resendLink = $this->Html->link( + __d('core_enroller', 'Resend'), + ['controller' => 'email_verifiers', 'action' => 'resend', $vv_verify_address], + ['class' => 'text-primary'] +); + +?> +Field->isEditable()): ?> +
  • +
    +
    + * +
    +
    + Form->submit($vv_submit_button_label) ?> + + + +
    +
    +
  • + + + +element('CoreEnroller.emailVerifiers/resendLinkSpa', [ + 'htmlId' => 'resend-link', + 'petitionId' => $vv_petition->id, + 'containerClasses' => 'border-top border-1 pt-2 text-center text-muted', + 'emailAddress' => $m, + 'vv_config' => $vv_config, +]) ?> + diff --git a/app/resources/locales/en_US/error.po b/app/resources/locales/en_US/error.po index dc8c648e3..7f0408c5b 100644 --- a/app/resources/locales/en_US/error.po +++ b/app/resources/locales/en_US/error.po @@ -214,6 +214,9 @@ msgstr "{0} must be provided" msgid "invalid" msgstr "Invalid value \"{0}\"" +msgid "invalid.request" +msgstr "Invalid Request" + msgid "javascript.copy" msgstr "Could not copy." @@ -364,6 +367,9 @@ msgstr "No type defined for table \"{0}\" column \"{1}\"" msgid "schema.parse" msgstr "Failed to parse file {0}" +msgid "something.went.wrong" +msgstr "Ooops... Something went wrong." + msgid "setup.co.comanage" msgstr "Failed to setup COmanage CO" diff --git a/app/resources/locales/en_US/operation.po b/app/resources/locales/en_US/operation.po index 7a5c2ff2b..908a83da1 100644 --- a/app/resources/locales/en_US/operation.po +++ b/app/resources/locales/en_US/operation.po @@ -180,6 +180,9 @@ msgstr "Sync Record to CO" msgid "filter" msgstr "Filter" +msgid "finish" +msgstr "Finish" + msgid "first" msgstr "First" diff --git a/app/src/Command/TransmogrifyCommand.php b/app/src/Command/TransmogrifyCommand.php index 37871a6bb..93a72c00a 100644 --- a/app/src/Command/TransmogrifyCommand.php +++ b/app/src/Command/TransmogrifyCommand.php @@ -620,7 +620,7 @@ public function execute(Arguments $args, ConsoleIo $io) { $err = 0; // Loop over each row from the inbound table. - while($row = $stmt->fetch()) { + while($row = $stmt->fetchAssociative()) { if(!empty($row[ $this->tables[$t]['displayField'] ])) { $io->verbose("$t " . $row[ $this->tables[$t]['displayField'] ]); } diff --git a/app/templates/Standard/dispatch.php b/app/templates/Standard/dispatch.php index be78db43f..c65fc1283 100644 --- a/app/templates/Standard/dispatch.php +++ b/app/templates/Standard/dispatch.php @@ -31,6 +31,10 @@ $modelsName = $this->name; // $tablename = models $tableName = \Cake\Utility\Inflector::tableize(\Cake\Utility\Inflector::singularize($this->name)); +// Populate the AutoViewVars. These are the same we do for the EnrollmentAttributes configuration view +$this->Petition->populateAutoViewVars(); +// We just populated the AutoViewVars. Add them to the current context +extract($this->viewVars); // $vv_template_path will be set for plugins $templatePath = $vv_template_path ?? ROOT . DS . 'templates' . DS . $modelsName; @@ -76,7 +80,9 @@ // Set the Include file name // Will be used by the unorderedList element below $this->set('vv_fields_inc', 'dispatch.inc'); -$this->set('vv_submit_button_label', __d('operation', 'continue')); +if (empty($vv_submit_button_label)) { + $this->set('vv_submit_button_label', __d('operation', 'continue')); +} // By default, the form will POST to the current controller // Note we need to open the form for view so Cake will autopopulate values diff --git a/app/webroot/css/co-base.css b/app/webroot/css/co-base.css index e124762f8..bef7e4f1c 100644 --- a/app/webroot/css/co-base.css +++ b/app/webroot/css/co-base.css @@ -2684,6 +2684,14 @@ html.dark-mode .btn-default:active { color: var(--cmg-color-btn-bg-003); border: 1px solid var(--cmg-color-btn-bg-003); } +.accessible-underline { + text-decoration: none; + border-bottom: 2px solid #000; +} +.accessible-underline:hover { + color: #0056b3; +} + /* Other Bootstrapisms */ .accordion-item { background-color: var(--cmg-color-body-bg);