diff --git a/database/migrations/structure/20200620_2113_google_login.sql b/database/migrations/structure/20200620_2113_google_login.sql new file mode 100644 index 0000000..152f328 --- /dev/null +++ b/database/migrations/structure/20200620_2113_google_login.sql @@ -0,0 +1,8 @@ +ALTER TABLE + `users` +ADD + `google_sub` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NULL DEFAULT NULL, +ADD + UNIQUE `google_sub` (`google_sub`), +MODIFY + `password` varchar(60) NULL DEFAULT NULL; 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/index.php b/public/index.php index ad425a0..24595aa 100644 --- a/public/index.php +++ b/public/index.php @@ -9,7 +9,7 @@ if (($pos = strpos($url, '?')) !== false) { } $url = rawurldecode($url); -$match = Container::$routeCollection->match($method, explode('/', $url)); +$match = Container::$routeCollection->match($method, $url == '' ? [] : explode('/', $url)); if ($match !== null) { list($route, $params) = $match; @@ -40,7 +40,7 @@ if ($match !== null) { return; } elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) { - header('Location: ' . Container::$request->getBase() . '/' . $response->getUrl(), true, $response->getHttpCode()); + header('Location: ' . $response->getUrl(), true, $response->getHttpCode()); return; } diff --git a/public/static/css/mapguesser.css b/public/static/css/mapguesser.css index c492c19..0d2cf8c 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; } @@ -350,6 +354,12 @@ div.box { padding: 0; width: 100%; } + button.marginLeft, a.button.marginLeft { + margin-left: 0; + } + button.marginRight, a.button.marginRight { + margin-right: 0; + } div.modal { left: 20px; right: 20px; diff --git a/public/static/js/login.js b/public/static/js/login.js index 0044488..cb97b3c 100644 --- a/public/static/js/login.js +++ b/public/static/js/login.js @@ -10,10 +10,15 @@ MapGuesser.httpRequest('POST', form.action, function () { if (this.response.error) { + if (this.response.error === 'user_not_found') { + window.location.replace('/signup'); + return; + } + var errorText; switch (this.response.error) { - case 'user_not_found': - errorText = 'No user found with the given email address. You can sign up here!'; + case 'password_too_short': + errorText = 'The given password is too short. Please choose a password that is at least 6 characters long!' break; case 'user_not_active': errorText = 'User found with the given email address, but the account is not activated. Please check your email and click on the activation link!'; diff --git a/public/static/js/profile.js b/public/static/js/profile.js index aff3137..0c2a2d3 100644 --- a/public/static/js/profile.js +++ b/public/static/js/profile.js @@ -25,7 +25,7 @@ case 'password_not_match': errorText = 'The given current password is wrong.' break; - case 'passwords_too_short': + case 'password_too_short': errorText = 'The given new password is too short. Please choose a password that is at least 6 characters long!' break; case 'passwords_not_match': diff --git a/public/static/js/signup.js b/public/static/js/signup.js index d4674f2..89e4209 100644 --- a/public/static/js/signup.js +++ b/public/static/js/signup.js @@ -9,28 +9,33 @@ var formData = new FormData(form); MapGuesser.httpRequest('POST', form.action, function () { - document.getElementById('loading').style.visibility = 'hidden'; - if (this.response.error) { + if (this.response.error === 'user_found') { + window.location.replace('/'); + return; + } + var errorText; switch (this.response.error) { case 'email_not_valid': errorText = 'The given email address is not valid.' break; - case 'passwords_too_short': + case 'password_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': + case 'user_found_user_not_active': errorText = 'There is a user already registered with the given email address. Please check your email and click on the activation link!'; break; + case 'user_found_password_not_match': + errorText = 'There is a user already registered with the given email address, but the given password is wrong.' + break; } + document.getElementById('loading').style.visibility = 'hidden'; + var signupFormError = document.getElementById('signupFormError'); signupFormError.style.display = 'block'; signupFormError.innerHTML = errorText; @@ -38,10 +43,18 @@ return; } - document.getElementById('signupFormError').style.display = 'none'; - form.reset(); - - MapGuesser.showModalWithContent('Sign up successful', 'Sign up was successful. Please check your email and click on the activation link to activate your account!'); + window.location.replace('/signup/success'); }, formData); }; + + var resetSignupButton = document.getElementById('resetSignupButton'); + if (resetSignupButton) { + resetSignupButton.onclick = function () { + document.getElementById('loading').style.visibility = 'visible'; + + MapGuesser.httpRequest('POST', '/signup/reset', function () { + window.location.reload(); + }); + }; + } })(); diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index ae2fa61..b5a180e 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -7,6 +7,6 @@ class HomeController { public function getIndex(): IRedirect { - return new Redirect([\Container::$routeCollection->getRoute('maps'), []], IRedirect::TEMPORARY); + return new Redirect(\Container::$routeCollection->getRoute('maps')->generateLink(), IRedirect::TEMPORARY); } } diff --git a/src/Controller/LoginController.php b/src/Controller/LoginController.php index f83633d..400f95d 100644 --- a/src/Controller/LoginController.php +++ b/src/Controller/LoginController.php @@ -3,31 +3,97 @@ 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(): IRedirect + { + $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); + } + + if ($this->request->session()->has('tmp_user_data')) { + $tmpUserData = $this->request->session()->get('tmp_user_data'); + + $data = ['email' => $tmpUserData['email']]; + } else { + $data = []; + } + + return new HtmlContent('login/signup', $data); + } + + public function getSignupSuccess(): IContent + { + $data = []; + return new HtmlContent('login/signup_success', $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 @@ -40,6 +106,16 @@ class LoginController $user = $this->userRepository->getByEmail($this->request->post('email')); if ($user === null) { + if (strlen($this->request->post('password')) < 6) { + $data = ['error' => 'password_too_short']; + return new JsonContent($data); + } + + $tmpUser = new User(); + $tmpUser->setPlainPassword($this->request->post('password')); + + $this->request->session()->set('tmp_user_data', ['email' => $this->request->post('email'), 'password_hashed' => $tmpUser->getPassword()]); + $data = ['error' => 'user_not_found']; return new JsonContent($data); } @@ -60,10 +136,256 @@ 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); + } + + $user = $this->userRepository->getByEmail($this->request->post('email')); + + if ($user !== null) { + if ($user->getActive()) { + if (!$user->checkPassword($this->request->post('password'))) { + $data = ['error' => 'user_found_password_not_match']; + return new JsonContent($data); + } + + $this->request->setUser($user); + + $data = ['error' => 'user_found']; + } else { + $data = ['error' => 'user_found_user_not_active']; + } + return new JsonContent($data); + } + + if (filter_var($this->request->post('email'), FILTER_VALIDATE_EMAIL) === false) { + $data = ['error' => 'email_not_valid']; + return new JsonContent($data); + } + + if ($this->request->session()->has('tmp_user_data')) { + $tmpUserData = $this->request->session()->get('tmp_user_data'); + + $tmpUser = new User(); + $tmpUser->setPassword($tmpUserData['password_hashed']); + + if (!$tmpUser->checkPassword($this->request->post('password'))) { + $data = ['error' => 'passwords_not_match']; + return new JsonContent($data); + } + } else { + if (strlen($this->request->post('password')) < 6) { + $data = ['error' => 'password_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); + + $this->request->session()->delete('tmp_user_data'); + + $data = ['success' => true]; + return new JsonContent($data); + } + + public function signupWithGoogle(): IContent + { + 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 resetSignup(): IContent + { + $this->request->session()->delete('tmp_user_data'); + + $data = ['success' => true]; + return new JsonContent($data); + } + + public function resetGoogleSignup(): IContent + { + $this->request->session()->delete('google_user_data'); + + $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/src/Controller/UserController.php b/src/Controller/UserController.php index 778217a..14ed237 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -52,7 +52,7 @@ class UserController implements ISecured if (strlen($this->request->post('password_new')) > 0) { if (strlen($this->request->post('password_new')) < 6) { - $data = ['error' => 'passwords_too_short']; + $data = ['error' => 'password_too_short']; return new JsonContent($data); } diff --git a/src/OAuth/GoogleOAuth.php b/src/OAuth/GoogleOAuth.php new file mode 100644 index 0000000..07c2107 --- /dev/null +++ b/src/OAuth/GoogleOAuth.php @@ -0,0 +1,41 @@ + 'code', + 'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'], + 'scope' => 'openid email', + 'redirect_uri' => $redirectUrl, + 'state' => $state, + 'nonce' => hash('sha256', random_bytes(10) . microtime()), + ]; + + return self::$dialogUrlBase . '?' . http_build_query($oauthParams); + } + + public function getToken(string $code, string $redirectUrl) + { + $tokenParams = [ + 'code' => $code, + 'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'], + 'client_secret' => $_ENV['GOOGLE_OAUTH_CLIENT_SECRET'], + 'redirect_uri' => $redirectUrl, + 'grant_type' => 'authorization_code', + ]; + + $request = new Request(self::$tokenUrlBase, Request::HTTP_POST); + $request->setQuery($tokenParams); + $response = $request->send(); + + return json_decode($response->getBody(), true); + } +} diff --git a/src/PersistentData/Model/User.php b/src/PersistentData/Model/User.php index fc3b185..e13adfd 100644 --- a/src/PersistentData/Model/User.php +++ b/src/PersistentData/Model/User.php @@ -6,24 +6,26 @@ class User extends Model implements IUser { protected static string $table = 'users'; - protected static array $fields = ['email', 'password', 'type', 'active']; + protected static array $fields = ['email', 'password', 'type', 'active', 'google_sub']; private static array $types = ['user', 'admin']; private string $email = ''; - private string $password = ''; + private ?string $password = null; private string $type = 'user'; private bool $active = false; + private ?string $googleSub = null; + public function setEmail(string $email): void { $this->email = $email; } - public function setPassword(string $hashedPassword): void + public function setPassword(?string $hashedPassword): void { $this->password = $hashedPassword; } @@ -45,12 +47,17 @@ class User extends Model implements IUser $this->active = (bool) $active; } + public function setGoogleSub(?string $googleSub): void + { + $this->googleSub = $googleSub; + } + public function getEmail(): string { return $this->email; } - public function getPassword(): string + public function getPassword(): ?string { return $this->password; } @@ -65,6 +72,11 @@ class User extends Model implements IUser return $this->active; } + public function getGoogleSub(): ?string + { + return $this->googleSub; + } + public function hasPermission(int $permission): bool { switch ($permission) { diff --git a/src/PersistentData/Model/UserConfirmation.php b/src/PersistentData/Model/UserConfirmation.php index 70a76d6..fac2b2d 100644 --- a/src/PersistentData/Model/UserConfirmation.php +++ b/src/PersistentData/Model/UserConfirmation.php @@ -6,6 +6,8 @@ class UserConfirmation extends Model protected static array $fields = ['user_id', 'token']; + protected static array $relations = ['user' => User::class]; + private ?User $user = null; private ?int $userId = null; diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index b67771d..c7ddcaf 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -25,4 +25,12 @@ class UserRepository return $this->pdm->selectFromDb($select, User::class); } + + public function getByGoogleSub(string $sub): ?User + { + $select = new Select(\Container::$dbConnection); + $select->where('google_sub', '=', $sub); + + return $this->pdm->selectFromDb($select, User::class); + } } diff --git a/src/Response/Redirect.php b/src/Response/Redirect.php index de5ab39..ed78183 100644 --- a/src/Response/Redirect.php +++ b/src/Response/Redirect.php @@ -4,11 +4,11 @@ use MapGuesser\Interfaces\Response\IRedirect; class Redirect implements IRedirect { - private $target; + private string $target; private int $type; - public function __construct($target, int $type = IRedirect::TEMPORARY) + public function __construct(string $target, int $type = IRedirect::TEMPORARY) { $this->target = $target; $this->type = $type; @@ -16,10 +16,10 @@ class Redirect implements IRedirect public function getUrl(): string { - if (is_array($this->target)) { - $link = $this->target[0]->generateLink($this->target[1]); - } else { + if (preg_match('/^http(s)?/', $this->target)) { $link = $this->target; + } else { + $link = \Container::$request->getBase() . '/' . $this->target; } return $link; diff --git a/src/Routing/RouteCollection.php b/src/Routing/RouteCollection.php index c729c3e..d93007a 100644 --- a/src/Routing/RouteCollection.php +++ b/src/Routing/RouteCollection.php @@ -64,7 +64,7 @@ class RouteCollection throw new \Exception('Route already exists: ' . $id); } - $pattern = array_merge($this->groupStack, explode('/', $pattern)); + $pattern = array_merge($this->groupStack, $pattern === '' ? [] : explode('/', $pattern)); $route = new Route($id, $pattern, $handler); $groupNumber = count($pattern); diff --git a/src/Util/JwtParser.php b/src/Util/JwtParser.php new file mode 100644 index 0000000..b6e57a1 --- /dev/null +++ b/src/Util/JwtParser.php @@ -0,0 +1,33 @@ +setToken($token); + } + } + + public function setToken(string $token) + { + $this->token = explode('.', str_replace(['_', '-'], ['/', '+'], $token)); + } + + public function getHeader(): array + { + return json_decode(base64_decode($this->token[0]), true); + } + + public function getPayload(): array + { + return json_decode(base64_decode($this->token[1]), true); + } + + public function getSignature(): string + { + return base64_decode($this->token[2]); + } +} 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..510cb08 --- /dev/null +++ b/views/login/google_signup.php @@ -0,0 +1,49 @@ + + +
+

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/login/signup.php b/views/login/signup.php new file mode 100644 index 0000000..22ef35f --- /dev/null +++ b/views/login/signup.php @@ -0,0 +1,35 @@ + + + +
+

Sign up

+
+
+ +

No user found with the given email address. Sign up with one click!

+ + + + + + + +

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

Sign up

+
+

Sign up was successful. Please check your email and click on the activation link to activate your account!

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

Sign up

-
-
- - - -

-
- -
-
-
-
- \ No newline at end of file diff --git a/web.php b/web.php index 9d1ff68..ac3041e 100644 --- a/web.php +++ b/web.php @@ -13,15 +13,28 @@ if (!empty($_ENV['DEV'])) { 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->group('login', function (MapGuesser\Routing\RouteCollection $routeCollection) { + $routeCollection->get('login', '', [MapGuesser\Controller\LoginController::class, 'getLoginForm']); + $routeCollection->post('login-action', '', [MapGuesser\Controller\LoginController::class, 'login']); + $routeCollection->get('login-google', 'google', [MapGuesser\Controller\LoginController::class, 'getGoogleLoginRedirect']); + $routeCollection->get('login-google-action', 'google/code', [MapGuesser\Controller\LoginController::class, 'loginWithGoogle']); +}); +Container::$routeCollection->group('signup', function (MapGuesser\Routing\RouteCollection $routeCollection) { + $routeCollection->get('signup', '', [MapGuesser\Controller\LoginController::class, 'getSignupForm']); + $routeCollection->post('signup-action', '', [MapGuesser\Controller\LoginController::class, 'signup']); + $routeCollection->get('signup-google', 'google', [MapGuesser\Controller\LoginController::class, 'getSignupWithGoogleForm']); + $routeCollection->post('signup-google-action', 'google', [MapGuesser\Controller\LoginController::class, 'signupWithGoogle']); + $routeCollection->post('signup.reset', 'reset', [MapGuesser\Controller\LoginController::class, 'resetSignup']); + $routeCollection->post('signup-google.reset', 'google/reset', [MapGuesser\Controller\LoginController::class, 'resetGoogleSignup']); + $routeCollection->get('signup.success', 'success', [MapGuesser\Controller\LoginController::class, 'getSignupSuccess']); + $routeCollection->get('signup.activate', 'activate/{token}', [MapGuesser\Controller\LoginController::class, 'activate']); + $routeCollection->get('signup.cancel', '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']); +Container::$routeCollection->group('profile', function (MapGuesser\Routing\RouteCollection $routeCollection) { + $routeCollection->get('profile', '', [MapGuesser\Controller\UserController::class, 'getProfile']); + $routeCollection->post('profile-action', '', [MapGuesser\Controller\UserController::class, 'saveProfile']); +}); Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']); Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) { $routeCollection->get('game', '{mapId}', [MapGuesser\Controller\GameController::class, 'getGame']);