diff --git a/routes/routes.php b/routes/routes.php index 6998aea..8972ff5 100644 --- a/routes/routes.php +++ b/routes/routes.php @@ -18,4 +18,5 @@ $app->group('/account', function () { ->setName('register'); $this->post('/forgot-password', 'UserFrosting\Sprinkle\UFTweaks\Controller\AccountController:forgotPassword'); + $this->post('/resend-verification', 'UserFrosting\Sprinkle\UFTweaks\Controller\AccountController:resendVerification'); })->add(new NoCache()); diff --git a/src/Controller/AccountController.php b/src/Controller/AccountController.php index 796af1d..2bae61b 100644 --- a/src/Controller/AccountController.php +++ b/src/Controller/AccountController.php @@ -90,7 +90,7 @@ class AccountController extends UFAccountController $throttler = $this->ci->throttler; $throttleData = [ - 'email' => $data['email'], + 'email' => mb_strtolower($data['email']), ]; $delay = $throttler->getDelay('password_reset_request', $throttleData); @@ -101,7 +101,7 @@ class AccountController extends UFAccountController } // Load the user, by email address - $user = $classMapper->getClassMapping('user')::where('email', $data['email'])->first(); + $user = $classMapper->getClassMapping('user')::findUnique($data['email'], 'email'); if ($user) { if (!$user->flag_verified) { @@ -138,6 +138,106 @@ class AccountController extends UFAccountController }); } + + /** + * Processes a request to resend the verification email for a new user account. + * + * Processes the request from the resend verification email form, checking that: + * 1. The rate limit on this type of request is observed; + * 2. The provided email is associated with an existing user account; + * 3. The user account is not already verified; + * 4. The submitted data is valid. + * This route is "public access". + * + * AuthGuard: false + * Route: /account/resend-verification + * Route Name: {none} + * Request type: POST + * + * @param Request $request + * @param Response $response + * @param array $args + */ + public function resendVerification(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/resend-verification.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + + return $response->withJson([], 400); + } + + // Throttle requests + + /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ + $throttler = $this->ci->throttler; + + $throttleData = [ + 'email' => mb_strtolower($data['email']), + ]; + $delay = $throttler->getDelay('verification_request', $throttleData); + + if ($delay > 0) { + $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', ['delay' => $delay]); + + return $response->withJson([], 429); + } + + // All checks passed! log events/activities, create user, and send verification email (if required) + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($classMapper, $data, $throttler, $throttleData, $config) { + // Log throttleable event + $throttler->logEvent('verification_request', $throttleData); + + // Load the user, by email address + $user = $classMapper->getClassMapping('user')::findUnique($data['email'], 'email'); + + // Check that the user exists and is not already verified. + // If there is no user with that email address, or the user exists and is already verified, + // we pretend like we succeeded to prevent account enumeration + if ($user && $user->flag_verified != '1') { + // We're good to go - record user activity and send the email + $verification = $this->ci->repoVerification->create($user, $config['verification.timeout']); + + // Create and send verification email + $message = new TwigMailMessage($this->ci->view, 'mail/resend-verification.html.twig'); + + $message->from($config['address_book.admin']) + ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) + ->addParams([ + 'user' => $user, + 'token' => $verification->getToken(), + ]); + + $this->ci->mailer->send($message); + } + }); + + $ms->addMessageTranslated('success', 'ACCOUNT.VERIFICATION.NEW_LINK_SENT', ['email' => $data['email']]); + + return $response->withJson([], 200); + } + /** * Account settings page. *