diff --git a/public/static/css/mapguesser.css b/public/static/css/mapguesser.css index c00b5ab..64b50bc 100644 --- a/public/static/css/mapguesser.css +++ b/public/static/css/mapguesser.css @@ -153,13 +153,27 @@ button, a.button { line-height: 35px; } -button.small { +button.small, div.inputWithButton>button { font-size: 14px; padding: 0 12px; height: 32px; line-height: 32px; } +button.small { + height: 32px; + line-height: 32px; +} + +div.inputWithButton>button { + border-radius: 2px; + height: 27px; + line-height: 27px; + width: 75px; + margin-left: -79px; + vertical-align: 2px; +} + button:enabled:hover, button:enabled:focus, a.button:hover, a.button:focus { background-color: #29457f; outline: none; @@ -191,7 +205,7 @@ button.gray, a.button.gray { background-color: #808080; } -button.gray:hover, button.gray:focus, a.button.gray:hover, a.button.gray:focus { +button.gray:enabled:hover, button.gray:enabled:focus, a.button.gray:hover, a.button.gray:focus { background-color: #555555; } @@ -199,7 +213,7 @@ button.red, a.button.red { background-color: #aa5e5e; } -button.red:hover, button.red:focus, a.button.red:hover, a.button.red:focus { +button.red:enabled:hover, button.red:enabled:focus, a.button.red:hover, a.button.red:focus { background-color: #7f2929; } @@ -207,7 +221,7 @@ button.yellow, a.button.yellow { background-color: #e8a349; } -button.yellow:hover, button.yellow:focus, a.button.yellow:hover, a.button.yellow:focus { +button.yellow:enabled:hover, button.yellow:enabled:focus, a.button.yellow:hover, a.button.yellow:focus { background-color: #c37713; } @@ -215,7 +229,7 @@ button.green, a.button.green { background-color: #28a745; } -button.green:hover, button.green:focus, a.button.green:hover, a.button.green:focus { +button.green:enabled:hover, button.green:enabled:focus, a.button.green:hover, a.button.green:focus { background-color: #1b7d31; } @@ -223,22 +237,36 @@ input, select, textarea { background-color: #f9fafb; border: solid #c8d2e1 1px; border-radius: 2px; - padding: 4px; box-sizing: border-box; font-size: 15px; font-weight: 300; } +input, select { + height: 30px; + line-height: 30px; + padding: 0 5px; +} + textarea { - font-size: 13px; + padding: 5px; resize: none; } -input.big, select.big, textarea.big { - padding: 5px; +input.big, select.big, textarea.big, div.inputWithButton>input { font-size: 18px; } +input.big, select.big, div.inputWithButton>input { + height: 35px; + line-height: 35px; + padding: 0 6px; +} + +textarea.big { + padding: 6px; +} + input.fullWidth, select.fullWidth, textarea.fullWidth { display: block; width: 100%; @@ -253,14 +281,30 @@ input:disabled, select:disabled, textarea:disabled { input:focus, select:focus, textarea:focus { background-color: #ffffff; border: solid #29457f 2px; - padding: 3px; outline: none; } -input.big:focus, select.big:focus, textarea.big:focus { +input:focus, select:focus { + padding: 0 4px; +} + +textarea:focus { padding: 4px; } +input.big:focus, select.big:focus { + padding: 0 5px; +} + +div.inputWithButton>input { + width: 100%; + padding: 0 83px 0 5px; +} + +textarea.big:focus { + padding: 5px; +} + div.modal { position: fixed; background-color: #ffffff; diff --git a/public/static/js/account/account.js b/public/static/js/account/account.js new file mode 100644 index 0000000..d0bc3f6 --- /dev/null +++ b/public/static/js/account/account.js @@ -0,0 +1,69 @@ +var Account = { + original: null, + countdown: null, + + openGoogleAuthenticate: function () { + window.open('/account/googleAuthenticate', 'googleAuthenticate', 'height=600,width=600') + }, + + authenticatedWithGoogleCallback: function (authenticatedWithGoogleUntil) { + var password = document.getElementsByTagName('form')[0].elements.password; + var button = document.getElementById('authenticateWithGoogleButton'); + + Account.original = { + type: password.type, + placeholder: password.placeholder, + required: password.required, + disabled: password.disabled + }; + + password.type = 'text'; + password.placeholder = 'Authenticated!' + password.value = ''; + password.required = false; + password.disabled = true; + + button.disabled = true; + + Account.countdown = setInterval(function () { + var timeLeft = Math.ceil((authenticatedWithGoogleUntil.getTime() - new Date().getTime()) / 1000); + + if (timeLeft > 30) { + return; + } + + if (timeLeft <= 0) { + Account.resetGoogleAuthentication(); + return; + } + + password.placeholder = 'Authenticated! ' + timeLeft + ' seconds left...'; + }, 1000); + }, + + resetGoogleAuthentication: function () { + if (Account.countdown !== null) { + clearInterval(Account.countdown); + } + + var password = document.getElementsByTagName('form')[0].elements.password; + var button = document.getElementById('authenticateWithGoogleButton'); + + password.type = Account.original.type; + password.placeholder = Account.original.placeholder + password.required = Account.original.required; + password.disabled = Account.original.disabled; + + button.disabled = false; + } +}; + +(function () { + document.getElementById('authenticateWithGoogleButton').onclick = function () { + Account.openGoogleAuthenticate(); + }; + + document.getElementsByTagName('form')[0].onreset = function () { + Account.resetGoogleAuthentication(); + }; +})(); diff --git a/public/static/js/account/google_authenticate.js b/public/static/js/account/google_authenticate.js new file mode 100644 index 0000000..aff35b2 --- /dev/null +++ b/public/static/js/account/google_authenticate.js @@ -0,0 +1,10 @@ +(function () { + if (success) { + window.opener.Account.authenticatedWithGoogleCallback(authenticatedWithGoogleUntil); + window.close(); + } else { + document.getElementById('closeWindowButton').onclick = function () { + window.close(); + } + } +})(); diff --git a/src/Cli/AddUserCommand.php b/src/Cli/AddUserCommand.php index 90f3ba3..9ccebc8 100644 --- a/src/Cli/AddUserCommand.php +++ b/src/Cli/AddUserCommand.php @@ -23,6 +23,7 @@ class AddUserCommand extends Command $user = new User(); $user->setEmail($input->getArgument('email')); $user->setPlainPassword($input->getArgument('password')); + $user->setActive(true); if ($input->hasArgument('type')) { $user->setType($input->getArgument('type')); diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 06d3d35..25d14e9 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -1,16 +1,22 @@ request->user(); + + $state = bin2hex(random_bytes(16)); + + $this->request->session()->set('oauth_state', $state); + + $oAuth = new GoogleOAuth(new Request()); + + $url = $oAuth->getDialogUrl( + $state, + $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(), + $user->getEmail() + ); + + return new Redirect($url, IRedirect::TEMPORARY); + } + + public function authenticateWithGoogle(): IContent + { + /** + * @var User $user + */ + $user = $this->request->user(); + + if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) { + $data = ['success' => false]; + return new HtmlContent('account/google_authenticate', $data); + } + + $oAuth = new GoogleOAuth(new Request()); + $tokenData = $oAuth->getToken($this->request->query('code'), $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink()); + + if (!isset($tokenData['id_token'])) { + $data = ['success' => false]; + return new HtmlContent('account/google_authenticate', $data); + } + + $jwtParser = new JwtParser($tokenData['id_token']); + $userData = $jwtParser->getPayload(); + + if ($userData['sub'] !== $user->getGoogleSub()) { + $data = ['success' => false, 'errorText' => 'This Google account is not linked to your account.']; + return new HtmlContent('account/google_authenticate', $data); + } + + $authenticatedWithGoogleUntil = new DateTime('+45 seconds'); + $this->request->session()->set('authenticated_with_google_until', $authenticatedWithGoogleUntil); + + $data = ['success' => true, 'authenticatedWithGoogleUntil' => $authenticatedWithGoogleUntil]; + return new HtmlContent('account/google_authenticate', $data); + } + public function getDeleteAccount(): IContent { /** @@ -63,8 +126,13 @@ class UserController implements ISecured */ $user = $this->request->user(); - if (!$user->checkPassword($this->request->post('password'))) { - $data = ['error' => ['errorText' => 'The given current password is wrong.']]; + if (!$this->confirmUserIdentity( + $user, + $this->request->session()->get('authenticated_with_google_until'), + $this->request->post('password'), + $error + )) { + $data = ['error' => ['errorText' => $error]]; return new JsonContent($data); } @@ -84,6 +152,8 @@ class UserController implements ISecured $this->pdm->saveToDb($user); + $this->request->session()->delete('authenticated_with_google_until'); + $data = ['success' => true]; return new JsonContent($data); } @@ -95,8 +165,13 @@ class UserController implements ISecured */ $user = $this->request->user(); - if (!$user->checkPassword($this->request->post('password'))) { - $data = ['error' => ['errorText' => 'The given current password is wrong.']]; + if (!$this->confirmUserIdentity( + $user, + $this->request->session()->get('authenticated_with_google_until'), + $this->request->post('password'), + $error + )) { + $data = ['error' => ['errorText' => $error]]; return new JsonContent($data); } @@ -110,7 +185,28 @@ class UserController implements ISecured \Container::$dbConnection->commit(); + $this->request->session()->delete('authenticated_with_google_until'); + $data = ['success' => true]; return new JsonContent($data); } + + private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, &$error): bool + { + if ($authenticatedWithGoogleUntil !== null && $authenticatedWithGoogleUntil > new DateTime()) { + return true; + } + + if ($password !== null) { + if ($user->checkPassword($password)) { + return true; + } + + $error = 'The given current password is wrong.'; + return false; + } + + $error = 'Could not confirm your identity. Please try again!'; + return false; + } } diff --git a/src/OAuth/GoogleOAuth.php b/src/OAuth/GoogleOAuth.php index c339179..5f78ee2 100644 --- a/src/OAuth/GoogleOAuth.php +++ b/src/OAuth/GoogleOAuth.php @@ -15,7 +15,7 @@ class GoogleOAuth $this->request = $request; } - public function getDialogUrl(string $state, string $redirectUrl): string + public function getDialogUrl(string $state, string $redirectUrl, ?string $loginHint = null): string { $oauthParams = [ 'response_type' => 'code', @@ -26,6 +26,10 @@ class GoogleOAuth 'nonce' => hash('sha256', random_bytes(10) . microtime()), ]; + if ($loginHint !== null) { + $oauthParams['login_hint'] = $loginHint; + } + return self::$dialogUrlBase . '?' . http_build_query($oauthParams); } diff --git a/views/account/account.php b/views/account/account.php index f6186b6..f1452f6 100644 --- a/views/account/account.php +++ b/views/account/account.php @@ -1,10 +1,27 @@ +@js('js/account/account.js') + @extends('templates/layout_normal') @section('main')