Merged in feature/MAPG-69-implement-google-registration-login (pull request #123)

Feature/MAPG-69 implement google registration login
This commit is contained in:
Bence Pőcze 2020-06-21 13:48:28 +00:00
commit a56ce849c0
28 changed files with 623 additions and 232 deletions

View File

@ -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;

View File

@ -0,0 +1,9 @@
Hi,
<br><br>
You recently signed up on MapGuesser with this Google account ({{EMAIL}}).
<br><br>
Have fun on MapGuesser!
<br><br>
Regards,<br>
MapGuesser<br>
<a href="{{BASE_URL}}" title="MapGuesser">{{BASE_URL}}</a>

View File

@ -10,4 +10,5 @@ However if you want to immediately delete it, please click on the following link
Have fun on MapGuesser! Have fun on MapGuesser!
<br><br> <br><br>
Regards,<br> Regards,<br>
MapGuesser MapGuesser<br>
<a href="{{BASE_URL}}" title="MapGuesser">{{BASE_URL}}</a>

View File

@ -9,7 +9,7 @@ if (($pos = strpos($url, '?')) !== false) {
} }
$url = rawurldecode($url); $url = rawurldecode($url);
$match = Container::$routeCollection->match($method, explode('/', $url)); $match = Container::$routeCollection->match($method, $url == '' ? [] : explode('/', $url));
if ($match !== null) { if ($match !== null) {
list($route, $params) = $match; list($route, $params) = $match;
@ -40,7 +40,7 @@ if ($match !== null) {
return; return;
} elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) { } 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; return;
} }

View File

@ -107,6 +107,10 @@ hr {
margin-right: 10px; margin-right: 10px;
} }
.center {
text-align: center;
}
.right { .right {
text-align: right; text-align: right;
} }
@ -350,6 +354,12 @@ div.box {
padding: 0; padding: 0;
width: 100%; width: 100%;
} }
button.marginLeft, a.button.marginLeft {
margin-left: 0;
}
button.marginRight, a.button.marginRight {
margin-right: 0;
}
div.modal { div.modal {
left: 20px; left: 20px;
right: 20px; right: 20px;

View File

@ -10,10 +10,15 @@
MapGuesser.httpRequest('POST', form.action, function () { MapGuesser.httpRequest('POST', form.action, function () {
if (this.response.error) { if (this.response.error) {
if (this.response.error === 'user_not_found') {
window.location.replace('/signup');
return;
}
var errorText; var errorText;
switch (this.response.error) { switch (this.response.error) {
case 'user_not_found': case 'password_too_short':
errorText = 'No user found with the given email address. You can <a href="/signup" title="Sign up">sign up here</a>!'; errorText = 'The given password is too short. Please choose a password that is at least 6 characters long!'
break; break;
case 'user_not_active': 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!'; errorText = 'User found with the given email address, but the account is not activated. Please check your email and click on the activation link!';

View File

@ -25,7 +25,7 @@
case 'password_not_match': case 'password_not_match':
errorText = 'The given current password is wrong.' errorText = 'The given current password is wrong.'
break; 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!' errorText = 'The given new password is too short. Please choose a password that is at least 6 characters long!'
break; break;
case 'passwords_not_match': case 'passwords_not_match':

View File

@ -9,28 +9,33 @@
var formData = new FormData(form); var formData = new FormData(form);
MapGuesser.httpRequest('POST', form.action, function () { MapGuesser.httpRequest('POST', form.action, function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) { if (this.response.error) {
if (this.response.error === 'user_found') {
window.location.replace('/');
return;
}
var errorText; var errorText;
switch (this.response.error) { switch (this.response.error) {
case 'email_not_valid': case 'email_not_valid':
errorText = 'The given email address is not valid.' errorText = 'The given email address is not valid.'
break; 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!' errorText = 'The given password is too short. Please choose a password that is at least 6 characters long!'
break; break;
case 'passwords_not_match': case 'passwords_not_match':
errorText = 'The given passwords do not match.' errorText = 'The given passwords do not match.'
break; break;
case 'user_found': case 'user_found_user_not_active':
errorText = 'There is a user already registered with the given email address. Please <a href="/login" title="Login">login here</a>!';
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!'; errorText = 'There is a user already registered with the given email address. Please check your email and click on the activation link!';
break; 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'); var signupFormError = document.getElementById('signupFormError');
signupFormError.style.display = 'block'; signupFormError.style.display = 'block';
signupFormError.innerHTML = errorText; signupFormError.innerHTML = errorText;
@ -38,10 +43,18 @@
return; return;
} }
document.getElementById('signupFormError').style.display = 'none'; window.location.replace('/signup/success');
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!');
}, formData); }, 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();
});
};
}
})(); })();

View File

@ -7,6 +7,6 @@ class HomeController
{ {
public function getIndex(): IRedirect public function getIndex(): IRedirect
{ {
return new Redirect([\Container::$routeCollection->getRoute('maps'), []], IRedirect::TEMPORARY); return new Redirect(\Container::$routeCollection->getRoute('maps')->generateLink(), IRedirect::TEMPORARY);
} }
} }

View File

@ -3,31 +3,97 @@
use MapGuesser\Interfaces\Request\IRequest; use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect; 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\Repository\UserRepository;
use MapGuesser\Response\HtmlContent; use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent; use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect; use MapGuesser\Response\Redirect;
use MapGuesser\Util\JwtParser;
class LoginController class LoginController
{ {
private IRequest $request; private IRequest $request;
private PersistentDataManager $pdm;
private UserRepository $userRepository; private UserRepository $userRepository;
private UserConfirmationRepository $userConfirmationRepository;
public function __construct(IRequest $request) public function __construct(IRequest $request)
{ {
$this->request = $request; $this->request = $request;
$this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository(); $this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
} }
public function getLoginForm() public function getLoginForm()
{ {
if ($this->request->user() !== null) { 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 = []; $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 public function login(): IContent
@ -40,6 +106,16 @@ class LoginController
$user = $this->userRepository->getByEmail($this->request->post('email')); $user = $this->userRepository->getByEmail($this->request->post('email'));
if ($user === null) { 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']; $data = ['error' => 'user_not_found'];
return new JsonContent($data); return new JsonContent($data);
} }
@ -60,10 +136,256 @@ class LoginController
return new JsonContent($data); 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 public function logout(): IRedirect
{ {
$this->request->setUser(null); $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();
} }
} }

View File

@ -1,170 +0,0 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Mailing\Mail;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserRepository;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect;
class SignupController
{
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 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();
}
}

View File

@ -52,7 +52,7 @@ class UserController implements ISecured
if (strlen($this->request->post('password_new')) > 0) { if (strlen($this->request->post('password_new')) > 0) {
if (strlen($this->request->post('password_new')) < 6) { if (strlen($this->request->post('password_new')) < 6) {
$data = ['error' => 'passwords_too_short']; $data = ['error' => 'password_too_short'];
return new JsonContent($data); return new JsonContent($data);
} }

41
src/OAuth/GoogleOAuth.php Normal file
View File

@ -0,0 +1,41 @@
<?php namespace MapGuesser\OAuth;
use MapGuesser\Http\Request;
class GoogleOAuth
{
private static $dialogUrlBase = 'https://accounts.google.com/o/oauth2/v2/auth';
private static $tokenUrlBase = 'https://oauth2.googleapis.com/token';
public function getDialogUrl(string $state, string $redirectUrl): string
{
$oauthParams = [
'response_type' => '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);
}
}

View File

@ -6,24 +6,26 @@ class User extends Model implements IUser
{ {
protected static string $table = 'users'; 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 static array $types = ['user', 'admin'];
private string $email = ''; private string $email = '';
private string $password = ''; private ?string $password = null;
private string $type = 'user'; private string $type = 'user';
private bool $active = false; private bool $active = false;
private ?string $googleSub = null;
public function setEmail(string $email): void public function setEmail(string $email): void
{ {
$this->email = $email; $this->email = $email;
} }
public function setPassword(string $hashedPassword): void public function setPassword(?string $hashedPassword): void
{ {
$this->password = $hashedPassword; $this->password = $hashedPassword;
} }
@ -45,12 +47,17 @@ class User extends Model implements IUser
$this->active = (bool) $active; $this->active = (bool) $active;
} }
public function setGoogleSub(?string $googleSub): void
{
$this->googleSub = $googleSub;
}
public function getEmail(): string public function getEmail(): string
{ {
return $this->email; return $this->email;
} }
public function getPassword(): string public function getPassword(): ?string
{ {
return $this->password; return $this->password;
} }
@ -65,6 +72,11 @@ class User extends Model implements IUser
return $this->active; return $this->active;
} }
public function getGoogleSub(): ?string
{
return $this->googleSub;
}
public function hasPermission(int $permission): bool public function hasPermission(int $permission): bool
{ {
switch ($permission) { switch ($permission) {

View File

@ -6,6 +6,8 @@ class UserConfirmation extends Model
protected static array $fields = ['user_id', 'token']; protected static array $fields = ['user_id', 'token'];
protected static array $relations = ['user' => User::class];
private ?User $user = null; private ?User $user = null;
private ?int $userId = null; private ?int $userId = null;

View File

@ -25,4 +25,12 @@ class UserRepository
return $this->pdm->selectFromDb($select, User::class); 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);
}
} }

View File

@ -4,11 +4,11 @@ use MapGuesser\Interfaces\Response\IRedirect;
class Redirect implements IRedirect class Redirect implements IRedirect
{ {
private $target; private string $target;
private int $type; private int $type;
public function __construct($target, int $type = IRedirect::TEMPORARY) public function __construct(string $target, int $type = IRedirect::TEMPORARY)
{ {
$this->target = $target; $this->target = $target;
$this->type = $type; $this->type = $type;
@ -16,10 +16,10 @@ class Redirect implements IRedirect
public function getUrl(): string public function getUrl(): string
{ {
if (is_array($this->target)) { if (preg_match('/^http(s)?/', $this->target)) {
$link = $this->target[0]->generateLink($this->target[1]);
} else {
$link = $this->target; $link = $this->target;
} else {
$link = \Container::$request->getBase() . '/' . $this->target;
} }
return $link; return $link;

View File

@ -64,7 +64,7 @@ class RouteCollection
throw new \Exception('Route already exists: ' . $id); 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); $route = new Route($id, $pattern, $handler);
$groupNumber = count($pattern); $groupNumber = count($pattern);

33
src/Util/JwtParser.php Normal file
View File

@ -0,0 +1,33 @@
<?php namespace MapGuesser\Util;
class JwtParser
{
private array $token;
public function __construct(?string $token = null)
{
if ($token !== null) {
$this->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]);
}
}

View File

@ -0,0 +1,9 @@
<?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?>
<div class="main">
<h2>Login up with Google</h2>
<div class="box">
<p class="error justify">Authenticating with Google failed. Please <a href="/login/google" title="Login with Google">retry</a>!</p>
</div>
</div>
<?php require ROOT . '/views/templates/main_footer.php'; ?>

View File

@ -0,0 +1,49 @@
<?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?>
<div class="main">
<h2>Sign up</h2>
<div class="box">
<form id="googleSignupForm" action="/signup/google" method="post">
<?php if ($found): ?>
<p class="justify">Please confirm that you link your account to your Google account.</p>
<?php else: ?>
<p class="justify">Please confirm your sign up request. Your account will be linked to your Google account.</p>
<?php endif; ?>
<input class="big fullWidth marginTop" type="email" name="email" placeholder="Email address" value="<?= $email ?>" disabled>
<div class="right">
<button class="marginTop marginRight" type="submit">
<?php if ($found): ?>
Link
<?php else: ?>
Sign up
<?php endif; ?>
</button><!--
--><button id="cancelGoogleSignupButton" class="gray marginTop" type="button">Cancel</button>
</div>
</form>
</div>
</div>
<script>
(function () {
var form = document.getElementById('googleSignupForm');
form.onsubmit = function (e) {
document.getElementById('loading').style.visibility = 'visible';
e.preventDefault();
MapGuesser.httpRequest('POST', form.action, function () {
window.location.replace('/');
});
};
document.getElementById('cancelGoogleSignupButton').onclick = function () {
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/signup/google/reset', function () {
window.location.replace('/signup');
});
};
})();
</script>
<?php require ROOT . '/views/templates/main_footer.php'; ?>

View File

@ -15,6 +15,10 @@ $jsFiles = [
<div class="right marginTop"> <div class="right marginTop">
<button type="submit">Login</button> <button type="submit">Login</button>
</div> </div>
<hr>
<div class="center">
<a class="button yellow" href="/login/google" title="Login with Google">Login with Google</a>
</div>
</form> </form>
</div> </div>
</div> </div>

35
views/login/signup.php Normal file
View File

@ -0,0 +1,35 @@
<?php
$jsFiles = [
'js/signup.js',
];
?>
<?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?>
<div class="main">
<h2>Sign up</h2>
<div class="box">
<form id="signupForm" action="/signup" method="post">
<?php if (isset($email)): ?>
<p class="justify">No user found with the given email address. Sign up with one click!</p>
<input class="big fullWidth marginTop" type="email" name="email" placeholder="Email address" value="<?= $email ?>" required>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Password confirmation" required minlength="6" autofocus>
<?php else: ?>
<input class="big fullWidth" type="email" name="email" placeholder="Email address" required autofocus>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Password" required minlength="6">
<input class="big fullWidth marginTop" type="password" name="password_confirm" placeholder="Password confirmation" minlength="6">
<?php endif; ?>
<p id="signupFormError" class="formError justify marginTop"></p>
<div class="right">
<button class="marginTop" type="submit">Sign up</button><!--
--><?php if (isset($email)): ?><!--
--><button id="resetSignupButton" class="gray marginTop marginLeft" type="button">Reset</button>
<?php endif; ?>
</div>
<hr>
<div class="center">
<a class="button yellow" href="/login/google" title="Signup with Google">Signup with Google</a>
</div>
</form>
</div>
</div>
<?php require ROOT . '/views/templates/main_footer.php'; ?>

View File

@ -0,0 +1,9 @@
<?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?>
<div class="main">
<h2>Sign up</h2>
<div class="box">
<p class="justify">Sign up was successful. Please check your email and click on the activation link to activate your account!</p>
</div>
</div>
<?php require ROOT . '/views/templates/main_footer.php'; ?>

View File

@ -1,22 +0,0 @@
<?php
$jsFiles = [
'js/signup.js',
];
?>
<?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?>
<div class="main">
<h2>Sign up</h2>
<div class="box">
<form id="signupForm" action="/signup" method="post">
<input class="big fullWidth" type="email" name="email" placeholder="Email address" required autofocus>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Password" required minlength="6">
<input class="big fullWidth marginTop" type="password" name="password_confirm" placeholder="Password confirmation" required minlength="6">
<p id="signupFormError" class="formError justify marginTop"></p>
<div class="right marginTop">
<button type="submit">Sign up</button>
</div>
</form>
</div>
</div>
<?php require ROOT . '/views/templates/main_footer.php'; ?>

29
web.php
View File

@ -13,15 +13,28 @@ if (!empty($_ENV['DEV'])) {
Container::$routeCollection = new MapGuesser\Routing\RouteCollection(); Container::$routeCollection = new MapGuesser\Routing\RouteCollection();
Container::$routeCollection->get('index', '', [MapGuesser\Controller\HomeController::class, 'getIndex']); Container::$routeCollection->get('index', '', [MapGuesser\Controller\HomeController::class, 'getIndex']);
Container::$routeCollection->get('login', 'login', [MapGuesser\Controller\LoginController::class, 'getLoginForm']); Container::$routeCollection->group('login', function (MapGuesser\Routing\RouteCollection $routeCollection) {
Container::$routeCollection->post('login-action', 'login', [MapGuesser\Controller\LoginController::class, 'login']); $routeCollection->get('login', '', [MapGuesser\Controller\LoginController::class, 'getLoginForm']);
Container::$routeCollection->get('signup', 'signup', [MapGuesser\Controller\SignupController::class, 'getSignupForm']); $routeCollection->post('login-action', '', [MapGuesser\Controller\LoginController::class, 'login']);
Container::$routeCollection->post('signup-action', 'signup', [MapGuesser\Controller\SignupController::class, 'signup']); $routeCollection->get('login-google', 'google', [MapGuesser\Controller\LoginController::class, 'getGoogleLoginRedirect']);
Container::$routeCollection->get('signup.activate', 'signup/activate/{token}', [MapGuesser\Controller\SignupController::class, 'activate']); $routeCollection->get('login-google-action', 'google/code', [MapGuesser\Controller\LoginController::class, 'loginWithGoogle']);
Container::$routeCollection->get('signup.cancel', 'signup/cancel/{token}', [MapGuesser\Controller\SignupController::class, 'cancel']); });
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('logout', 'logout', [MapGuesser\Controller\LoginController::class, 'logout']);
Container::$routeCollection->get('profile', 'profile', [MapGuesser\Controller\UserController::class, 'getProfile']); Container::$routeCollection->group('profile', function (MapGuesser\Routing\RouteCollection $routeCollection) {
Container::$routeCollection->post('profile-action', 'profile', [MapGuesser\Controller\UserController::class, 'saveProfile']); $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->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']);
Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) { Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('game', '{mapId}', [MapGuesser\Controller\GameController::class, 'getGame']); $routeCollection->get('game', '{mapId}', [MapGuesser\Controller\GameController::class, 'getGame']);