From 20102f0577087c525b6700086e70b5d33afacb16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=91cze=20Bence?= Date: Sun, 14 Jun 2020 17:20:20 +0200 Subject: [PATCH] MAPG-69 implement sign up functionality --- mail/signup.tpl | 13 ++ public/static/js/signup.js | 47 +++++++ src/Controller/SignupController.php | 192 ++++++++++++++++++++++++++++ views/signup/activate.php | 9 ++ views/signup/cancel.php | 13 ++ views/signup/signup.php | 22 ++++ web.php | 4 + 7 files changed, 300 insertions(+) create mode 100644 mail/signup.tpl create mode 100644 public/static/js/signup.js create mode 100644 src/Controller/SignupController.php create mode 100644 views/signup/activate.php create mode 100644 views/signup/cancel.php create mode 100644 views/signup/signup.php diff --git a/mail/signup.tpl b/mail/signup.tpl new file mode 100644 index 0000000..c93b404 --- /dev/null +++ b/mail/signup.tpl @@ -0,0 +1,13 @@ +Hi, + +You recently signed up on MapGuesser with this email address ({{EMAIL}}). To activate your account, please click on the following link: +{{ACTIVATE_LINK}} + +If you did not sign up on MapGuesser or changed your mind, no further action is required, your email address will be deleted soon. +However if you want to immediately delete it, please click on the following link: +{{CANCEL_LINK}} + +Have fun on MapGuesser! + +Regards, +MapGuesser diff --git a/public/static/js/signup.js b/public/static/js/signup.js new file mode 100644 index 0000000..4031b32 --- /dev/null +++ b/public/static/js/signup.js @@ -0,0 +1,47 @@ +(function () { + var form = document.getElementById('signupForm'); + + form.onsubmit = function (e) { + document.getElementById('loading').style.visibility = 'visible'; + + e.preventDefault(); + + var formData = new FormData(form); + + MapGuesser.httpRequest('POST', form.action, function () { + document.getElementById('loading').style.visibility = 'hidden'; + + if (this.response.error) { + var errorText; + switch (this.response.error) { + case 'passwords_too_short': + errorText = 'The given password is too short. Please choose a password that is at least 6 characters long!' + break; + case 'passwords_not_match': + errorText = 'The given passwords do not match.' + break; + case 'user_found': + errorText = 'There is a user already registered with the given email address. Please login here!'; + break; + case 'not_active_user_found': + errorText = 'There is a user already registered with the given email address. Please check your email and click on the activation link!'; + break; + } + + var signupFormError = document.getElementById('signupFormError'); + signupFormError.style.display = 'block'; + signupFormError.innerHTML = errorText; + + form.elements.email.select(); + + return; + } + + document.getElementById('signupFormError').style.display = 'none'; + form.reset(); + form.elements.email.focus(); + + MapGuesser.showModalWithContent('Sign up successful', 'Sign up was successful. Please check your email and click on the activation link to activate your account!'); + }, formData); + }; +})(); diff --git a/src/Controller/SignupController.php b/src/Controller/SignupController.php new file mode 100644 index 0000000..ea049bc --- /dev/null +++ b/src/Controller/SignupController.php @@ -0,0 +1,192 @@ +request = $request; + } + + public function getSignupForm() + { + $session = $this->request->session(); + + if ($session->get('user')) { + return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY); + } + + $data = []; + return new HtmlContent('signup/signup', $data); + } + + public function signup(): IContent + { + $session = $this->request->session(); + + if ($session->get('user')) { + //TODO: return with some error + $data = ['success' => true]; + return new JsonContent($data); + } + + $select = new Select(\Container::$dbConnection, 'users'); + $select->columns(User::getFields()); + $select->where('email', '=', $this->request->post('email')); + + $userData = $select->execute()->fetch(IResultSet::FETCH_ASSOC); + + if ($userData !== null) { + $user = new User($userData); + + if ($user->getActive()) { + $data = ['error' => 'user_found']; + } else { + $data = ['error' => 'not_active_user_found']; + } + return new JsonContent($data); + } + + if (strlen($this->request->post('password')) < 6) { + $data = ['error' => 'passwords_too_short']; + return new JsonContent($data); + } + + if ($this->request->post('password') !== $this->request->post('password_confirm')) { + $data = ['error' => 'passwords_not_match']; + return new JsonContent($data); + } + + $user = new User([ + 'email' => $this->request->post('email'), + ]); + + $user->setPlainPassword($this->request->post('password')); + + \Container::$dbConnection->startTransaction(); + + $modify = new Modify(\Container::$dbConnection, 'users'); + $modify->fill($user->toArray()); + $modify->save(); + $userId = $modify->getId(); + + $token = hash('sha256', serialize($user) . random_bytes(10) . microtime()); + + $modify = new Modify(\Container::$dbConnection, 'user_confirmations'); + $modify->set('user_id', $userId); + $modify->set('token', $token); + $modify->save(); + + \Container::$dbConnection->commit(); + + $this->sendConfirmationEmail($user->getEmail(), $token); + + $data = ['success' => true]; + return new JsonContent($data); + } + + public function activate() + { + $session = $this->request->session(); + + if ($session->get('user')) { + return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY); + } + + $select = new Select(\Container::$dbConnection, 'user_confirmations'); + $select->columns(['id', 'user_id']); + $select->where('token', '=', $this->request->query('token')); + + $confirmation = $select->execute()->fetch(IResultSet::FETCH_ASSOC); + + if ($confirmation === null) { + $data = []; + return new HtmlContent('signup/activate', $data); + } + + \Container::$dbConnection->startTransaction(); + + $modify = new Modify(\Container::$dbConnection, 'user_confirmations'); + $modify->setId($confirmation['id']); + $modify->delete(); + + $modify = new Modify(\Container::$dbConnection, 'users'); + $modify->setId($confirmation['user_id']); + $modify->set('active', true); + $modify->save(); + + \Container::$dbConnection->commit(); + + $select = new Select(\Container::$dbConnection, 'users'); + $select->columns(User::getFields()); + $select->whereId($confirmation['user_id']); + + $userData = $select->execute()->fetch(IResultSet::FETCH_ASSOC); + $user = new User($userData); + + $session->set('user', $user); + + return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY); + } + + public function cancel() + { + $session = $this->request->session(); + + if ($session->get('user')) { + return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY); + } + + $select = new Select(\Container::$dbConnection, 'user_confirmations'); + $select->columns(['id', 'user_id']); + $select->where('token', '=', $this->request->query('token')); + + $confirmation = $select->execute()->fetch(IResultSet::FETCH_ASSOC); + + if ($confirmation === null) { + $data = ['success' => false]; + return new HtmlContent('signup/cancel', $data); + } + + \Container::$dbConnection->startTransaction(); + + $modify = new Modify(\Container::$dbConnection, 'user_confirmations'); + $modify->setId($confirmation['id']); + $modify->delete(); + + $modify = new Modify(\Container::$dbConnection, 'users'); + $modify->setId($confirmation['user_id']); + $modify->delete(); + + \Container::$dbConnection->commit(); + + $data = ['success' => true]; + return new HtmlContent('signup/cancel', $data); + } + + private function sendConfirmationEmail($email, $token): void + { + $mail = new Mail(); + $mail->addRecipient($email); + $mail->setSubject('Welcome to MapGuesser - Activate your account'); + $mail->setBodyFromTemplate('signup', [ + 'EMAIL' => $email, + 'ACTIVATE_LINK' => $this->request->getBase() . '/signup/activate/' . $token, + 'CANCEL_LINK' => $this->request->getBase() . '/signup/cancel/' . $token, + ]); + $mail->send(); + } +} diff --git a/views/signup/activate.php b/views/signup/activate.php new file mode 100644 index 0000000..6ad9b78 --- /dev/null +++ b/views/signup/activate.php @@ -0,0 +1,9 @@ + + +
+

Account activation

+
+

Activation failed. Please check the link you entered or retry sign up!

+
+
+ \ No newline at end of file diff --git a/views/signup/cancel.php b/views/signup/cancel.php new file mode 100644 index 0000000..c22293e --- /dev/null +++ b/views/signup/cancel.php @@ -0,0 +1,13 @@ + + +
+

Account cancellation

+
+ +

Cancellation was successfull. You can sign up any time if you want!

+ +

Cancellation failed. Please check the link you entered! Maybe the account was already deleted, in this case no further action is required.

+ +
+
+ \ No newline at end of file diff --git a/views/signup/signup.php b/views/signup/signup.php new file mode 100644 index 0000000..e6be4e4 --- /dev/null +++ b/views/signup/signup.php @@ -0,0 +1,22 @@ + + + +
+

Sign up

+
+
+ + + +

+
+ +
+
+
+
+ \ No newline at end of file diff --git a/web.php b/web.php index b9ef8a1..e8e159e 100644 --- a/web.php +++ b/web.php @@ -15,6 +15,10 @@ Container::$routeCollection = new MapGuesser\Routing\RouteCollection(); Container::$routeCollection->get('index', '', [MapGuesser\Controller\HomeController::class, 'getIndex']); Container::$routeCollection->get('login', 'login', [MapGuesser\Controller\LoginController::class, 'getLoginForm']); Container::$routeCollection->post('login-action', 'login', [MapGuesser\Controller\LoginController::class, 'login']); +Container::$routeCollection->get('signup', 'signup', [MapGuesser\Controller\SignupController::class, 'getSignupForm']); +Container::$routeCollection->post('signup-action', 'signup', [MapGuesser\Controller\SignupController::class, 'signup']); +Container::$routeCollection->get('signup.activate', 'signup/activate/{token}', [MapGuesser\Controller\SignupController::class, 'activate']); +Container::$routeCollection->get('signup.cancel', 'signup/cancel/{token}', [MapGuesser\Controller\SignupController::class, 'cancel']); Container::$routeCollection->get('logout', 'logout', [MapGuesser\Controller\LoginController::class, 'logout']); Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']); Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) {