diff --git a/mail/signup-noconfirm.html b/mail/signup-noconfirm.html new file mode 100644 index 0000000..8c00ae0 --- /dev/null +++ b/mail/signup-noconfirm.html @@ -0,0 +1,9 @@ +Hi, +

+You recently signed up on MapGuesser with this Google account ({{EMAIL}}). +

+Have fun on MapGuesser! +

+Regards,
+MapGuesser
+{{BASE_URL}} diff --git a/mail/signup.html b/mail/signup.html index 480e0c1..629d774 100644 --- a/mail/signup.html +++ b/mail/signup.html @@ -10,4 +10,5 @@ However if you want to immediately delete it, please click on the following link Have fun on MapGuesser!

Regards,
-MapGuesser +MapGuesser
+{{BASE_URL}} diff --git a/public/static/css/mapguesser.css b/public/static/css/mapguesser.css index c492c19..0663d22 100644 --- a/public/static/css/mapguesser.css +++ b/public/static/css/mapguesser.css @@ -107,6 +107,10 @@ hr { margin-right: 10px; } +.center { + text-align: center; +} + .right { text-align: right; } diff --git a/src/Controller/LoginController.php b/src/Controller/LoginController.php index f83633d..be27d43 100644 --- a/src/Controller/LoginController.php +++ b/src/Controller/LoginController.php @@ -3,31 +3,84 @@ use MapGuesser\Interfaces\Request\IRequest; use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IRedirect; +use MapGuesser\Mailing\Mail; +use MapGuesser\OAuth\GoogleOAuth; +use MapGuesser\PersistentData\Model\User; +use MapGuesser\PersistentData\Model\UserConfirmation; +use MapGuesser\PersistentData\PersistentDataManager; +use MapGuesser\Repository\UserConfirmationRepository; use MapGuesser\Repository\UserRepository; use MapGuesser\Response\HtmlContent; use MapGuesser\Response\JsonContent; use MapGuesser\Response\Redirect; +use MapGuesser\Util\JwtParser; class LoginController { private IRequest $request; + private PersistentDataManager $pdm; + private UserRepository $userRepository; + private UserConfirmationRepository $userConfirmationRepository; + public function __construct(IRequest $request) { $this->request = $request; + $this->pdm = new PersistentDataManager(); $this->userRepository = new UserRepository(); + $this->userConfirmationRepository = new UserConfirmationRepository(); } public function getLoginForm() { if ($this->request->user() !== null) { - return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY); + return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); } $data = []; - return new HtmlContent('login', $data); + return new HtmlContent('login/login', $data); + } + + public function getGoogleLoginRedirect() + { + $state = bin2hex(random_bytes(16)); + + $this->request->session()->set('oauth_state', $state); + + $oAuth = new GoogleOAuth(); + $url = $oAuth->getDialogUrl($state, $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink()); + + return new Redirect($url, IRedirect::TEMPORARY); + } + + public function getSignupForm() + { + if ($this->request->user() !== null) { + return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); + } + + $data = []; + return new HtmlContent('login/signup', $data); + } + + public function getSignupWithGoogleForm() + { + if ($this->request->user() !== null) { + return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); + } + + if (!$this->request->session()->has('google_user_data')) { + return new Redirect(\Container::$routeCollection->getRoute('login-google')->generateLink(), IRedirect::TEMPORARY); + } + + $userData = $this->request->session()->get('google_user_data'); + + $user = $this->userRepository->getByEmail($userData['email']); + + $data = ['found' => $user !== null, 'email' => $userData['email']]; + return new HtmlContent('login/google_signup', $data); } public function login(): IContent @@ -60,10 +113,219 @@ class LoginController return new JsonContent($data); } + public function loginWithGoogle() + { + if ($this->request->user() !== null) { + return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); + } + + if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) { + $data = []; + return new HtmlContent('login/google_login', $data); + } + + $oAuth = new GoogleOAuth(); + $tokenData = $oAuth->getToken($this->request->query('code'), $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink()); + + if (!isset($tokenData['id_token'])) { + $data = []; + return new HtmlContent('login/google_login', $data); + } + + $jwtParser = new JwtParser($tokenData['id_token']); + $userData = $jwtParser->getPayload(); + + if (!$userData['email_verified']) { + $data = []; + return new HtmlContent('login/google_login', $data); + } + + $user = $this->userRepository->getByGoogleSub($userData['sub']); + + if ($user === null) { + $this->request->session()->set('google_user_data', $userData); + + return new Redirect(\Container::$routeCollection->getRoute('signup-google')->generateLink(), IRedirect::TEMPORARY); + } + + $this->request->setUser($user); + + return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); + } + public function logout(): IRedirect { $this->request->setUser(null); - return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY); + return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); + } + + public function signup(): IContent + { + if ($this->request->user() !== null) { + $data = ['error' => 'logged_in']; + return new JsonContent($data); + } + + if (filter_var($this->request->post('email'), FILTER_VALIDATE_EMAIL) === false) { + $data = ['error' => 'email_not_valid']; + return new JsonContent($data); + } + + $user = $this->userRepository->getByEmail($this->request->post('email')); + + if ($user !== null) { + 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(); + $user->setEmail($this->request->post('email')); + $user->setPlainPassword($this->request->post('password')); + + \Container::$dbConnection->startTransaction(); + + $this->pdm->saveToDb($user); + + $token = hash('sha256', serialize($user) . random_bytes(10) . microtime()); + + $confirmation = new UserConfirmation(); + $confirmation->setUser($user); + $confirmation->setToken($token); + + $this->pdm->saveToDb($confirmation); + + \Container::$dbConnection->commit(); + + $this->sendConfirmationEmail($user->getEmail(), $token); + + $data = ['success' => true]; + return new JsonContent($data); + } + + public function signupWithGoogle() + { + if ($this->request->user() !== null) { + $data = ['success' => true]; + return new JsonContent($data); + } + + $userData = $this->request->session()->get('google_user_data'); + + $user = $this->userRepository->getByEmail($userData['email']); + + if ($user === null) { + $user = new User(); + $user->setEmail($userData['email']); + } + + $user->setActive(true); + $user->setGoogleSub($userData['sub']); + + $this->pdm->saveToDb($user); + + $this->sendWelcomeEmail($user->getEmail()); + + $this->request->session()->delete('google_user_data'); + $this->request->setUser($user); + + $data = ['success' => true]; + return new JsonContent($data); + } + + public function activate() + { + if ($this->request->user() !== null) { + return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); + } + + $confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token')); + + if ($confirmation === null) { + $data = []; + return new HtmlContent('login/activate', $data); + } + + \Container::$dbConnection->startTransaction(); + + $this->pdm->deleteFromDb($confirmation); + + $user = $this->userRepository->getById($confirmation->getUserId()); + $user->setActive(true); + + $this->pdm->saveToDb($user); + + \Container::$dbConnection->commit(); + + $this->request->setUser($user); + + return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); + } + + public function cancel() + { + if ($this->request->user() !== null) { + return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); + } + + $confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token')); + + if ($confirmation === null) { + $data = ['success' => false]; + return new HtmlContent('login/cancel', $data); + } + + \Container::$dbConnection->startTransaction(); + + $this->pdm->deleteFromDb($confirmation); + + $user = $this->userRepository->getById($confirmation->getUserId()); + + $this->pdm->deleteFromDb($user); + + \Container::$dbConnection->commit(); + + $data = ['success' => true]; + return new HtmlContent('login/cancel', $data); + } + + private function sendConfirmationEmail(string $email, string $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() . '/'. \Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]), + 'CANCEL_LINK' => $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]), + 'BASE_URL' => $this->request->getBase(), + ]); + $mail->send(); + } + + private function sendWelcomeEmail(string $email): void + { + $mail = new Mail(); + $mail->addRecipient($email); + $mail->setSubject('Welcome to MapGuesser'); + $mail->setBodyFromTemplate('signup-noconfirm', [ + 'EMAIL' => $email, + 'BASE_URL' => $this->request->getBase(), + ]); + $mail->send(); } } diff --git a/src/Controller/SignupController.php b/src/Controller/SignupController.php deleted file mode 100644 index 77b3197..0000000 --- a/src/Controller/SignupController.php +++ /dev/null @@ -1,170 +0,0 @@ -request = $request; - $this->pdm = new PersistentDataManager(); - $this->userRepository = new UserRepository(); - $this->userConfirmationRepository = new UserConfirmationRepository(); - } - - public function getSignupForm() - { - if ($this->request->user() !== null) { - return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY); - } - - $data = []; - return new HtmlContent('signup/signup', $data); - } - - public function signup(): IContent - { - if ($this->request->user() !== null) { - //TODO: return with some error - $data = ['success' => true]; - return new JsonContent($data); - } - - if (filter_var($this->request->post('email'), FILTER_VALIDATE_EMAIL) === false) { - $data = ['error' => 'email_not_valid']; - return new JsonContent($data); - } - - $user = $this->userRepository->getByEmail($this->request->post('email')); - - if ($user !== null) { - 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(); - $user->setEmail($this->request->post('email')); - $user->setPlainPassword($this->request->post('password')); - - \Container::$dbConnection->startTransaction(); - - $this->pdm->saveToDb($user); - - $token = hash('sha256', serialize($user) . random_bytes(10) . microtime()); - - $confirmation = new UserConfirmation(); - $confirmation->setUser($user); - $confirmation->setToken($token); - - $this->pdm->saveToDb($confirmation); - - \Container::$dbConnection->commit(); - - $this->sendConfirmationEmail($user->getEmail(), $token); - - $data = ['success' => true]; - return new JsonContent($data); - } - - public function activate() - { - if ($this->request->user() !== null) { - return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY); - } - - $confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token')); - - if ($confirmation === null) { - $data = []; - return new HtmlContent('signup/activate', $data); - } - - \Container::$dbConnection->startTransaction(); - - $this->pdm->deleteFromDb($confirmation); - - $user = $this->userRepository->getById($confirmation->getUserId()); - $user->setActive(true); - - $this->pdm->saveToDb($user); - - \Container::$dbConnection->commit(); - - $this->request->setUser($user); - - return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY); - } - - public function cancel() - { - if ($this->request->user() !== null) { - return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY); - } - - $confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token')); - - if ($confirmation === null) { - $data = ['success' => false]; - return new HtmlContent('signup/cancel', $data); - } - - \Container::$dbConnection->startTransaction(); - - $this->pdm->deleteFromDb($confirmation); - - $user = $this->userRepository->getById($confirmation->getUserId()); - - $this->pdm->deleteFromDb($user); - - \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/login/activate.php similarity index 100% rename from views/signup/activate.php rename to views/login/activate.php diff --git a/views/signup/cancel.php b/views/login/cancel.php similarity index 100% rename from views/signup/cancel.php rename to views/login/cancel.php diff --git a/views/login/google_login.php b/views/login/google_login.php new file mode 100644 index 0000000..334857b --- /dev/null +++ b/views/login/google_login.php @@ -0,0 +1,9 @@ + + +
+

Login up with Google

+
+

Authenticating with Google failed. Please retry!

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

Sign up

+
+
+ +

Please confirm that you link your account to your Google account.

+ +

Please confirm your sign up request. Your account will be linked to your Google account.

+ + +
+ +
+
+
+
+ + \ No newline at end of file diff --git a/views/login.php b/views/login/login.php similarity index 81% rename from views/login.php rename to views/login/login.php index 1dfdd94..c5bc6af 100644 --- a/views/login.php +++ b/views/login/login.php @@ -15,6 +15,10 @@ $jsFiles = [
+
+
+ Login with Google +
diff --git a/views/signup/signup.php b/views/login/signup.php similarity index 84% rename from views/signup/signup.php rename to views/login/signup.php index c2c12bc..0dc8fcb 100644 --- a/views/signup/signup.php +++ b/views/login/signup.php @@ -16,6 +16,10 @@ $jsFiles = [
+
+
+ Signup with Google +
diff --git a/web.php b/web.php index 9d1ff68..044974f 100644 --- a/web.php +++ b/web.php @@ -15,10 +15,14 @@ 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('login-google', 'login/google', [MapGuesser\Controller\LoginController::class, 'getGoogleLoginRedirect']); +Container::$routeCollection->get('login-google-action', 'login/google/code', [MapGuesser\Controller\LoginController::class, 'loginWithGoogle']); +Container::$routeCollection->get('signup', 'signup', [MapGuesser\Controller\LoginController::class, 'getSignupForm']); +Container::$routeCollection->post('signup-action', 'signup', [MapGuesser\Controller\LoginController::class, 'signup']); +Container::$routeCollection->get('signup-google', 'signup/google', [MapGuesser\Controller\LoginController::class, 'getSignupWithGoogleForm']); +Container::$routeCollection->post('signup-google-action', 'signup/google', [MapGuesser\Controller\LoginController::class, 'signupWithGoogle']); +Container::$routeCollection->get('signup.activate', 'signup/activate/{token}', [MapGuesser\Controller\LoginController::class, 'activate']); +Container::$routeCollection->get('signup.cancel', 'signup/cancel/{token}', [MapGuesser\Controller\LoginController::class, 'cancel']); Container::$routeCollection->get('logout', 'logout', [MapGuesser\Controller\LoginController::class, 'logout']); Container::$routeCollection->get('profile', 'profile', [MapGuesser\Controller\UserController::class, 'getProfile']); Container::$routeCollection->post('profile-action', 'profile', [MapGuesser\Controller\UserController::class, 'saveProfile']);