Compare commits

...
Author SHA1 Message Date
bence da2c8d96a4 Merged in bugfix/MAPG-142-fix-timestamp-onupdate (pull request #175)
MAPG-142 fix timestamp onupdate issue
2020-07-05 16:24:17 +00:00
bence cd581c6fcd MAPG-142 fix timestamp onupdate issue 2020-07-05 18:23:02 +02:00
bence f7b268e10f Merged in feature/MAPG-185-create-cron-script (pull request #174)
Feature/MAPG-185 create cron script
2020-07-05 15:56:12 +00:00
bence a8b1ab3057 MAPG-185 adapt README 2020-07-05 17:52:13 +02:00
bence 8403c21ec0 MAPG-185 consolidation of email templates 2020-07-05 17:37:02 +02:00
bence 0ba1cda884 MAPG-185 fix getter of User 2020-07-05 17:36:27 +02:00
bence b2e09c394b MAPG-185 switch off builtin session gc 2020-07-05 16:48:37 +02:00
bence 0bcb0187c1 MAPG-185 rename migrate database command 2020-07-05 16:45:38 +02:00
bence f3d1608164 MAPG-185 add maintain database command 2020-07-05 16:43:36 +02:00
bence b254603fb1 MAPG-185 add getter for expired users and password resetters 2020-07-05 16:37:37 +02:00
bence 776561a41c Merged in feature/MAPG-193-replace-direct-selects-to-repository-calls (pull request #173)
Feature/MAPG-193 replace direct selects to repository calls
2020-07-05 13:20:08 +00:00
bence 27fc687043 MAPG-193 replace Select usage to repository calls where it is easily possible 2020-07-05 15:17:28 +02:00
bence 45869321ac MAPG-193 fix PersistentDataManager::selectMultipleFromDb 2020-07-05 15:15:29 +02:00
bence b58137acc9 Merged in feature/MAPG-142-resend-activation-link-functionality (pull request #172)
Feature/MAPG-142 resend activation link functionality
2020-07-05 12:32:32 +00:00
bence dd6bb5ef9a MAPG-142 limit password reset query if the existing is not expired 2020-07-05 14:24:06 +02:00
bence 37094e552b MAPG-142 modify UserPasswordResetterRepository to return only one resetter for user
(it is not possible for an user to have multiple)
2020-07-05 14:24:06 +02:00
bence 32392590bd MAPG-142 save user creation date 2020-07-05 14:24:06 +02:00
bence 38885849de MAPG-142 remove unnecessary 'now' from DateTime constructor 2020-07-05 14:24:05 +02:00
bence ef013b8b9e MAPG-142 rename app to MapGuesser in .env.example 2020-07-05 14:24:05 +02:00
bence ab23b37b97 MAPG-142 redefine tokens and increase OAuth security with nonce 2020-07-05 14:24:05 +02:00
bence 7e3315fc88 MAPG-142 implemenet confirmation mail resend 2020-07-05 13:39:56 +02:00
bence 6cafac1b65 MAPG-142 modify UserConfirmationRepository to return only one confirmation for user
(it is not possible for an user to have multiple)
2020-07-05 13:33:02 +02:00
bence 631582a912 Merged in feature/MAPG-191-get-rid-of-unnecessary-passing-by-reference (pull request #171)
Feature/MAPG-191 get rid of unnecessary passing by reference
2020-07-04 23:02:57 +00:00
bence 6db6208896 MAPG-191 don't create separate variable for data where not necessary 2020-07-05 00:58:03 +02:00
bence e17cf68007 MAPG-191 don't pass data by reference to IContent 2020-07-05 00:25:34 +02:00
bence 091afb0aab Merged in feature/MAPG-141-password-forgotten-functionality (pull request #170)
Feature/MAPG-141 password forgotten functionality
2020-07-04 22:15:50 +00:00
bence 954f111254 MAPG-141 delete password resetters when deleting account 2020-07-05 00:09:45 +02:00
bence de1d7338a4 MAPG-141 add reset password functionality 2020-07-05 00:09:11 +02:00
bence 7539f637b0 MAPG-141 add and fix some links to login/signup 2020-07-05 00:04:35 +02:00
bence 67534a22f5 MAPG-141 add password resetter table, model and repository 2020-07-05 00:03:56 +02:00
bence 43aef22c4e Merged in feature/MAPG-44-possibility-to-add-pov-data-to-locations (pull request #169)
Feature/MAPG-44 possibility to add pov data to locations
2020-07-03 23:12:56 +00:00
bence 3123643bb7 MAPG-44 modify game to use pov of place 2020-07-04 01:11:04 +02:00
bence b736ce8970 MAPG-44 modify MapAdminController to get/save place pov data 2020-07-04 01:10:03 +02:00
bence 4e48e6ae73 MAPG-44 add Pov class and adjust Place model to store pov 2020-07-04 01:09:00 +02:00
bence 028d8a22a2 MAPG-44 modify DB structure to store pov data 2020-07-04 01:06:37 +02:00
bence a73e60ae5a Merged in bugfix/MAPG-189-fix-inputwithbuttons-input-padding (pull request #168)
MAPG-189 fix div.inputWithButton>input's padding when it is focused
2020-07-03 21:51:38 +00:00
bence 5383da81b3 MAPG-189 fix div.inputWithButton>input's padding when it is focused 2020-07-03 23:50:22 +02:00
53 changed files with 988 additions and 236 deletions

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
APP_NAME=MapQuiz
APP_NAME=MapGuesser
DEV=1
DB_HOST=mariadb
DB_USER=mapguesser
+8
View File
@@ -46,6 +46,14 @@ One very important variable is `DEV`. This indicates that the application operat
If you install the application in the Docker stack for development (staging) environment, only the variables for external dependencies (API keys, map attribution, etc.) should be adapted. All other variables (for DB connection, static root, mailing, etc.) are fine with the default value.
### (Production only) Create cron job
To maintain database (delete inactive users, old sessions etc.), the command `db:maintain` should be regularly executed. It is recommened to create a cron job that runs every hour:
```
0 * * * * /path/to/your/installation/mapg db:maintain >>/var/log/cron-mapguesser.log 2>&1
```
### Finalize installation
After you set the environment variables in the `.env` file, execute the following command:
@@ -0,0 +1,8 @@
ALTER TABLE
`places`
ADD
`pov_heading` decimal(6, 3) NOT NULL DEFAULT 0.0,
ADD
`pov_pitch` decimal(5, 3) NOT NULL DEFAULT 0.0,
ADD
`pov_zoom` decimal(5, 4) NOT NULL DEFAULT 0.0;
@@ -0,0 +1,10 @@
CREATE TABLE `user_password_resetters` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`token` varchar(32) CHARACTER SET ascii NOT NULL,
`expires` timestamp NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `token` (`token`),
CONSTRAINT `user_password_resetters_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
@@ -0,0 +1,5 @@
UPDATE `user_confirmations` SET token=SUBSTRING(token, 1, 32);
ALTER TABLE `user_confirmations`
ADD `last_sent` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `token` varchar(32) CHARACTER SET ascii NOT NULL;
@@ -0,0 +1,2 @@
ALTER TABLE `sessions`
MODIFY `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP;
@@ -0,0 +1,4 @@
UPDATE `sessions` SET id=SUBSTRING(id, 1, 32);
ALTER TABLE `sessions`
MODIFY `id` varchar(32) CHARACTER SET ascii NOT NULL;
@@ -0,0 +1,2 @@
ALTER TABLE `users`
ADD `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP;
+13
View File
@@ -0,0 +1,13 @@
Hi,
<br><br>
You recently requested password reset on {{APP_NAME}} with this email address ({{EMAIL}}).
To reset the password to your account, please click on the following link:<br>
<a href="{{RESET_LINK}}" title="Reset password">{{RESET_LINK}}</a>
<br><br>
You can reset your password with this link util {{EXPIRES}}.
<br><br>
If you did not requested password reset, no further action is required, your account is not touched.
<br><br>
Regards,<br>
{{APP_NAME}}<br>
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>
+7 -3
View File
@@ -1,10 +1,14 @@
Hi,
<br><br>
You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}). To activate your account, please click on the following link:<br>
You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}).
To activate your account, please click on the following link:<br>
<a href="{{ACTIVATE_LINK}}" title="Account activation">{{ACTIVATE_LINK}}</a>
<br><br>
If you did not sign up on {{APP_NAME}} or changed your mind, no further action is required, your email address will be deleted soon.<br>
However if you want to immediately delete it, please click on the following link:<br>
You can activate your account until {{ACTIVATABLE_UNTIL}}.
If you don't activate your account, your email address will be permanently deleted after this point of time.
<br><br>
If you did not sign up on {{APP_NAME}} or changed your mind, no further action is required.
However if you want to immediately delete your email address, please click on the following link:<br>
<a href="{{CANCEL_LINK}}" title="Sign up cancellation">{{CANCEL_LINK}}</a>
<br><br>
Have fun on {{APP_NAME}}!
+1 -1
View File
@@ -16,7 +16,7 @@ class Container
{
static MapGuesser\Interfaces\Database\IConnection $dbConnection;
static MapGuesser\Routing\RouteCollection $routeCollection;
static \SessionHandlerInterface $sessionHandler;
static MapGuesser\Interfaces\Session\ISessionHandler $sessionHandler;
static MapGuesser\Interfaces\Request\IRequest $request;
}
+2 -1
View File
@@ -5,8 +5,9 @@ require 'main.php';
$app = new Symfony\Component\Console\Application('MapGuesser Console', '');
$app->add(new MapGuesser\Cli\DatabaseMigration());
$app->add(new MapGuesser\Cli\MigrateDatabaseCommand());
$app->add(new MapGuesser\Cli\AddUserCommand());
$app->add(new MapGuesser\Cli\LinkViewCommand());
$app->add(new \MapGuesser\Cli\MaintainDatabaseCommand());
$app->run();
+2 -1
View File
@@ -26,8 +26,9 @@ if ($match !== null) {
}
if ($method === 'post' && Container::$request->post('anti_csrf_token') !== Container::$request->session()->get('anti_csrf_token')) {
$content = new MapGuesser\Response\JsonContent(['error' => 'no_valid_anti_csrf_token']);
header('Content-Type: text/html; charset=UTF-8', true, 403);
echo json_encode(['error' => 'no_valid_anti_csrf_token']);
$content->render();
return;
}
+5 -1
View File
@@ -298,7 +298,11 @@ input.big:focus, select.big:focus {
div.inputWithButton>input {
width: 100%;
padding: 0 83px 0 5px;
padding: 0 83px 0 6px;
}
div.inputWithButton>input:focus {
padding: 0 82px 0 5px;
}
textarea.big:focus {
+7 -4
View File
@@ -8,6 +8,7 @@
rounds: [],
scoreSum: 0,
panoId: null,
pov: null,
panorama: null,
map: null,
guessMarker: null,
@@ -35,6 +36,7 @@
}
Game.panoId = this.response.panoId;
Game.pov = this.response.pov;
if (this.response.history) {
for (var i = 0; i < this.response.history.length; ++i) {
@@ -116,7 +118,7 @@
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
Game.loadPano(Game.panoId);
Game.loadPano(Game.panoId, Game.pov);
},
handleErrorResponse: function (error) {
@@ -129,13 +131,13 @@
});
},
loadPano: function (panoId) {
loadPano: function (panoId, pov) {
if (Game.adaptGuess) {
document.getElementById('guess').classList.add('adapt');
}
Game.panorama.setPov({ heading: 0, pitch: 0 });
Game.panorama.setZoom(0);
Game.panorama.setPov({ heading: pov.heading, pitch: pov.pitch });
Game.panorama.setZoom(pov.zoom);
Game.panorama.setPano(panoId);
},
@@ -199,6 +201,7 @@
}
Game.panoId = this.response.panoId;
Game.pov = this.response.pov;
}, data);
},
+1 -1
View File
@@ -16,7 +16,7 @@ echo "Installing MapGuesser DB..."
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/db/mapguesser.sql
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg migrate)
(cd ${ROOT_DIR} && ./mapg db:migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
+1 -1
View File
@@ -15,7 +15,7 @@ echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg migrate)
(cd ${ROOT_DIR} && ./mapg db:migrate)
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
+2
View File
@@ -1,5 +1,6 @@
<?php namespace MapGuesser\Cli;
use DateTime;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User;
use Symfony\Component\Console\Command\Command;
@@ -24,6 +25,7 @@ class AddUserCommand extends Command
$user->setEmail($input->getArgument('email'));
$user->setPlainPassword($input->getArgument('password'));
$user->setActive(true);
$user->setCreatedDate(new DateTime());
if ($input->hasArgument('type')) {
$user->setType($input->getArgument('type'));
+107
View File
@@ -0,0 +1,107 @@
<?php namespace MapGuesser\Cli;
use DateTime;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserRepository;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MaintainDatabaseCommand extends Command
{
private PersistentDataManager $persistentDataManager;
private UserRepository $userRepository;
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
public function __construct()
{
parent::__construct();
$this->persistentDataManager = new PersistentDataManager();
$this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
}
public function configure()
{
$this->setName('db:maintain')
->setDescription('Maintain database.');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
try {
$this->deleteInactiveExpiredUsers();
$this->deleteExpiredPasswordResetters();
$this->deleteExpiredSessions();
} catch (\Exception $e) {
$output->writeln('<error>Maintenance failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$output->writeln('<info>Maintenance was successful!</info>');
$output->writeln('');
return 0;
}
private function deleteInactiveExpiredUsers(): void
{
\Container::$dbConnection->startTransaction();
foreach ($this->userRepository->getAllInactiveExpired() as $user) {
//TODO: these can be in some wrapper class
$userConfirmation = $this->userConfirmationRepository->getByUser($user);
if ($userConfirmation !== null) {
$this->pdm->deleteFromDb($userConfirmation);
}
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($userPasswordResetter !== null) {
$this->pdm->deleteFromDb($userPasswordResetter);
}
$this->persistentDataManager->deleteFromDb($user);
}
\Container::$dbConnection->commit();
}
private function deleteExpiredPasswordResetters(): void
{
foreach ($this->userPasswordResetterRepository->getAllExpired() as $passwordResetter) {
$this->persistentDataManager->deleteFromDb($passwordResetter);
}
}
private function deleteExpiredSessions(): void
{
//TODO: model may be used for sessions too
$select = new Select(\Container::$dbConnection, 'sessions');
$select->columns(['id']);
$select->where('updated', '<', (new DateTime('-7 days'))->format('Y-m-d H:i:s'));
$result = $select->execute();
while ($session = $result->fetch(IResultSet::FETCH_ASSOC)) {
$modify = new Modify(\Container::$dbConnection, 'sessions');
$modify->setId($session['id']);
$modify->delete();
}
}
}
@@ -7,11 +7,11 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DatabaseMigration extends Command
class MigrateDatabaseCommand extends Command
{
public function configure()
{
$this->setName('migrate')
$this->setName('db:migrate')
->setDescription('Migration of database changes.');
}
+4 -4
View File
@@ -21,15 +21,15 @@ class GameController
public function getGame(): IContent
{
$mapId = (int) $this->request->query('mapId');
$data = $this->prepareGame($mapId);
return new HtmlContent('game', $data);
return new HtmlContent('game', $this->prepareGame($mapId));
}
public function getGameJson(): IContent
{
$mapId = (int) $this->request->query('mapId');
$data = $this->prepareGame($mapId);
return new JsonContent($data);
return new JsonContent($this->prepareGame($mapId));
}
private function prepareGame(int $mapId)
+18 -11
View File
@@ -29,8 +29,7 @@ class GameFlowController
$session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$data = ['error' => 'no_session_found'];
return new JsonContent($data);
return new JsonContent(['error' => 'no_session_found']);
}
if (count($state['rounds']) === 0) {
@@ -41,7 +40,10 @@ class GameFlowController
$session->set('state', $state);
$data = ['panoId' => $place->getPanoIdCached()];
$data = [
'panoId' => $place->getPanoIdCached(),
'pov' => $place->getPov()->toArray()
];
} else {
$rounds = count($state['rounds']);
$last = $state['rounds'][$rounds - 1];
@@ -59,7 +61,10 @@ class GameFlowController
$data = [
'history' => $history,
'panoId' => $last['panoId']
'panoId' => $last['panoId'],
'pov' => isset($last['pov']) ? // should be checked not to break with old sessions
$last['pov']->toArray() :
['heading' => 0.0, 'pitch' => 0.0, 'zoom' => 0.0],
];
}
@@ -73,8 +78,7 @@ class GameFlowController
$session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$data = ['error' => 'no_session_found'];
return new JsonContent($data);
return new JsonContent(['error' => 'no_session_found']);
}
$last = $state['rounds'][count($state['rounds']) - 1];
@@ -103,23 +107,25 @@ class GameFlowController
$this->addNewRoundToState($state, $place, $placesWithoutPano);
$panoId = $place->getPanoIdCached();
$pov = $place->getPov()->toArray();
} else {
$state['rounds'] = [];
$panoId = null;
$pov = null;
}
$session->set('state', $state);
$data = [
return new JsonContent([
'result' => [
'position' => $position->toArray(),
'distance' => $distance,
'score' => $score
],
'panoId' => $panoId
];
return new JsonContent($data);
'panoId' => $panoId,
'pov' => $pov
]);
}
private function addNewRoundToState(&$state, Place $place, array $placesWithoutPano): void
@@ -128,7 +134,8 @@ class GameFlowController
'placesWithoutPano' => $placesWithoutPano,
'placeId' => $place->getId(),
'position' => $place->getPosition(),
'panoId' => $place->getPanoIdCached()
'panoId' => $place->getPanoIdCached(),
'pov' => $place->getPov()
];
}
+1 -2
View File
@@ -24,7 +24,6 @@ class HomeController
{
// session starts with the request, this method just sends valid data to the client
$data = ['antiCsrfToken' => $this->request->session()->get('anti_csrf_token')];
return new JsonContent($data);
return new JsonContent(['antiCsrfToken' => $this->request->session()->get('anti_csrf_token')]);
}
}
+282 -67
View File
@@ -1,5 +1,7 @@
<?php namespace MapGuesser\Controller;
use DateInterval;
use DateTime;
use MapGuesser\Http\Request;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
@@ -8,8 +10,10 @@ use MapGuesser\Mailing\Mail;
use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\Model\UserPasswordResetter;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserRepository;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
@@ -26,12 +30,15 @@ class LoginController
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
}
public function getLoginForm()
@@ -40,18 +47,23 @@ class LoginController
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$data = [];
return new HtmlContent('login/login', $data);
return new HtmlContent('login/login');
}
public function getGoogleLoginRedirect(): IRedirect
{
$state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
$this->request->session()->set('oauth_state', $state);
$this->request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl($state, $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink());
$url = $oAuth->getDialogUrl(
$state,
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink(),
$nonce
);
return new Redirect($url, IRedirect::TEMPORARY);
}
@@ -75,8 +87,7 @@ class LoginController
public function getSignupSuccess(): IContent
{
$data = [];
return new HtmlContent('login/signup_success', $data);
return new HtmlContent('login/signup_success');
}
public function getSignupWithGoogleForm()
@@ -93,48 +104,96 @@ class LoginController
$user = $this->userRepository->getByEmail($userData['email']);
$data = ['found' => $user !== null, 'email' => $userData['email']];
return new HtmlContent('login/google_signup', $data);
return new HtmlContent('login/google_signup', ['found' => $user !== null, 'email' => $userData['email']]);
}
public function getRequestPasswordResetForm()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
return new HtmlContent('login/password_reset_request', ['email' => $this->request->query('email')]);
}
public function getRequestPasswordResetSuccess(): IContent
{
return new HtmlContent('login/password_reset_request_success');
}
public function getResetPasswordForm()
{
if ($this->request->user() !== null) {
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$token = $this->request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
return new HtmlContent('login/reset_password', ['success' => false]);
}
$user = $this->userRepository->getById($resetter->getUserId());
return new HtmlContent('login/reset_password', ['success' => true, 'token' => $token, 'email' => $user->getEmail()]);
}
public function login(): IContent
{
if ($this->request->user() !== null) {
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
$user = $this->userRepository->getByEmail($this->request->post('email'));
if ($user === null) {
if (strlen($this->request->post('password')) < 6) {
$data = ['error' => ['errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!']];
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
$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()]);
$this->request->session()->set('tmp_user_data', [
'email' => $this->request->post('email'),
'password_hashed' => $tmpUser->getPassword()
]);
$data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('signup')->generateLink()]];
return new JsonContent($data);
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('signup')->generateLink()
]
]);
}
if (!$user->getActive()) {
$data = ['error' => ['errorText' => 'User found with the given email address, but the account is not activated. Please check your email and click on the activation link!']];
return new JsonContent($data);
$this->resendConfirmationEmail($user);
return new JsonContent([
'error' => [
'errorText' => 'User found with the given email address, but the account is not activated. ' .
'Please check your email and click on the activation link!'
]
]);
}
if (!$user->checkPassword($this->request->post('password'))) {
$data = ['error' => ['errorText' => 'The given password is wrong.']];
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'The given password is wrong. You can <a href="/password/requestReset?email=' .
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
]
]);
}
$this->request->setUser($user);
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
public function loginWithGoogle()
@@ -144,30 +203,34 @@ class LoginController
}
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
$data = [];
return new HtmlContent('login/google_login', $data);
return new HtmlContent('login/google_login');
}
$oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken($this->request->query('code'), $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink());
$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);
return new HtmlContent('login/google_login');
}
$jwtParser = new JwtParser($tokenData['id_token']);
$userData = $jwtParser->getPayload();
$idToken = $jwtParser->getPayload();
if (!$userData['email_verified']) {
$data = [];
return new HtmlContent('login/google_login', $data);
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
return new HtmlContent('login/google_login');
}
$user = $this->userRepository->getByGoogleSub($userData['sub']);
if (!$idToken['email_verified']) {
return new HtmlContent('login/google_login');
}
$user = $this->userRepository->getByGoogleSub($idToken['sub']);
if ($user === null) {
$this->request->session()->set('google_user_data', $userData);
$this->request->session()->set('google_user_data', ['sub' => $idToken['sub'], 'email' => $idToken['email']]);
return new Redirect(\Container::$routeCollection->getRoute('signup-google')->generateLink(), IRedirect::TEMPORARY);
}
@@ -187,8 +250,7 @@ class LoginController
public function signup(): IContent
{
if ($this->request->user() !== null) {
$data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()]];
return new JsonContent($data);
return new JsonContent(['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()]]);
}
$user = $this->userRepository->getByEmail($this->request->post('email'));
@@ -196,22 +258,31 @@ class LoginController
if ($user !== null) {
if ($user->getActive()) {
if (!$user->checkPassword($this->request->post('password'))) {
$data = ['error' => ['errorText' => 'There is a user already registered with the given email address, but the given password is wrong.']];
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'There is a user already registered with the given email address, ' .
'but the given password is wrong. You can <a href="/password/requestReset?email=' .
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
]
]);
}
$this->request->setUser($user);
$data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('index')->generateLink()]];
} else {
$data = ['error' => ['errorText' => 'There is a user already registered with the given email address. Please check your email and click on the activation link!']];
$data = [
'error' => [
'errorText' => 'There is a user already registered with the given email address. ' .
'Please check your email and click on the activation link!'
]
];
}
return new JsonContent($data);
}
if (filter_var($this->request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
$data = ['error' => ['errorText' => 'The given email address is not valid.']];
return new JsonContent($data);
return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]);
}
if ($this->request->session()->has('tmp_user_data')) {
@@ -221,52 +292,53 @@ class LoginController
$tmpUser->setPassword($tmpUserData['password_hashed']);
if (!$tmpUser->checkPassword($this->request->post('password'))) {
$data = ['error' => ['errorText' => 'The given passwords do not match.']];
return new JsonContent($data);
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
} else {
if (strlen($this->request->post('password')) < 6) {
$data = ['error' => ['errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!']];
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
$data = ['error' => ['errorText' => 'The given passwords do not match.']];
return new JsonContent($data);
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
}
$user = new User();
$user->setEmail($this->request->post('email'));
$user->setPlainPassword($this->request->post('password'));
$user->setCreatedDate(new DateTime());
\Container::$dbConnection->startTransaction();
$this->pdm->saveToDb($user);
$token = hash('sha256', serialize($user) . random_bytes(10) . microtime());
$token = bin2hex(random_bytes(16));
$confirmation = new UserConfirmation();
$confirmation->setUser($user);
$confirmation->setToken($token);
$confirmation->setLastSentDate(new DateTime());
$this->pdm->saveToDb($confirmation);
\Container::$dbConnection->commit();
$this->sendConfirmationEmail($user->getEmail(), $token);
$this->sendConfirmationEmail($user->getEmail(), $token, $user->getCreatedDate());
$this->request->session()->delete('tmp_user_data');
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
public function signupWithGoogle(): IContent
{
if ($this->request->user() !== null) {
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
$userData = $this->request->session()->get('google_user_data');
@@ -278,6 +350,7 @@ class LoginController
$user = new User();
$user->setEmail($userData['email']);
$user->setCreatedDate(new DateTime());
} else {
$sendWelcomeEmail = false;
}
@@ -294,24 +367,21 @@ class LoginController
$this->request->session()->delete('google_user_data');
$this->request->setUser($user);
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
public function resetSignup(): IContent
{
$this->request->session()->delete('tmp_user_data');
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
public function resetGoogleSignup(): IContent
{
$this->request->session()->delete('google_user_data');
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
public function activate()
@@ -320,11 +390,10 @@ class LoginController
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token'));
$confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32));
if ($confirmation === null) {
$data = [];
return new HtmlContent('login/activate', $data);
return new HtmlContent('login/activate');
}
\Container::$dbConnection->startTransaction();
@@ -349,11 +418,10 @@ class LoginController
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
}
$confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token'));
$confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32));
if ($confirmation === null) {
$data = ['success' => false];
return new HtmlContent('login/cancel', $data);
return new HtmlContent('login/cancel', ['success' => false]);
}
\Container::$dbConnection->startTransaction();
@@ -366,23 +434,156 @@ class LoginController
\Container::$dbConnection->commit();
$data = ['success' => true];
return new HtmlContent('login/cancel', $data);
return new HtmlContent('login/cancel', ['success' => true]);
}
private function sendConfirmationEmail(string $email, string $token): void
public function requestPasswordReset(): IContent
{
if ($this->request->user() !== null) {
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
]
]);
}
$user = $this->userRepository->getByEmail($this->request->post('email'));
if ($user === null) {
return new JsonContent([
'error' => [
'errorText' => 'No user found with the given email address. You can <a href="/signup" title="Sign up">sign up</a>!'
]
]);
}
if (!$user->getActive()) {
$this->resendConfirmationEmail($user);
return new JsonContent([
'error' => [
'errorText' => 'User found with the given email address, but the account is not activated. ' .
'Please check your email and click on the activation link!'
]
]);
}
$existingResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($existingResetter !== null && $existingResetter->getExpiresDate() > new DateTime()) {
return new JsonContent([
'error' => [
'errorText' => 'Password reset was recently requested for this account. Please check your email, or try again later!'
]
]);
}
$token = bin2hex(random_bytes(16));
$expires = new DateTime('+1 hour');
$passwordResetter = new UserPasswordResetter();
$passwordResetter->setUser($user);
$passwordResetter->setToken($token);
$passwordResetter->setExpiresDate($expires);
\Container::$dbConnection->startTransaction();
if ($existingResetter !== null) {
$this->pdm->deleteFromDb($existingResetter);
}
$this->pdm->saveToDb($passwordResetter);
\Container::$dbConnection->commit();
$this->sendPasswordResetEmail($user->getEmail(), $token, $expires);
return new JsonContent(['success' => true]);
}
public function resetPassword(): IContent
{
if ($this->request->user() !== null) {
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
]
]);
}
$token = $this->request->query('token');
$resetter = $this->userPasswordResetterRepository->getByToken($token);
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
return new JsonContent([
'redirect' => [
'target' => '/' . \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token])
]
]);
}
if (strlen($this->request->post('password')) < 6) {
return new JsonContent([
'error' => [
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
}
\Container::$dbConnection->startTransaction();
$this->pdm->deleteFromDb($resetter);
$user = $this->userRepository->getById($resetter->getUserId());
$user->setPlainPassword($this->request->post('password'));
$this->pdm->saveToDb($user);
\Container::$dbConnection->commit();
$this->request->setUser($user);
return new JsonContent(['success' => true]);
}
private function sendConfirmationEmail(string $email, string $token, DateTime $created): void
{
$mail = new Mail();
$mail->addRecipient($email);
$mail->setSubject('Welcome to ' . $_ENV['APP_NAME'] . ' - 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]),
'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]),
'ACTIVATABLE_UNTIL' => (clone $created)->add(new DateInterval('P1D'))->format('Y-m-d H:i T')
]);
$mail->send();
}
private function resendConfirmationEmail(User $user): bool
{
$confirmation = $this->userConfirmationRepository->getByUser($user);
if ($confirmation === null || (clone $confirmation->getLastSentDate())->add(new DateInterval('PT1H')) > new DateTime()) {
return false;
}
$confirmation->setLastSentDate(new DateTime());
$this->pdm->saveToDb($confirmation);
$this->sendConfirmationEmail($user->getEmail(), $confirmation->getToken(), $user->getCreatedDate());
return true;
}
private function sendWelcomeEmail(string $email): void
{
$mail = new Mail();
@@ -393,4 +594,18 @@ class LoginController
]);
$mail->send();
}
private function sendPasswordResetEmail(string $email, string $token, DateTime $expires): void
{
$mail = new Mail();
$mail->addRecipient($email);
$mail->setSubject($_ENV['APP_NAME'] . ' - Password reset');
$mail->setBodyFromTemplate('password-reset', [
'EMAIL' => $email,
'RESET_LINK' => $this->request->getBase() . '/' .
\Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]),
'EXPIRES' => $expires->format('Y-m-d H:i T')
]);
$mail->send();
}
}
+36 -49
View File
@@ -1,10 +1,8 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Authentication\IUser;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\PersistentData\Model\Map;
@@ -15,7 +13,7 @@ use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Util\Geo\Position;
use MapGuesser\Util\Panorama\Pov;
class MapAdminController implements ISecured
{
@@ -57,8 +55,13 @@ class MapAdminController implements ISecured
$places = [];
}
$data = ['mapId' => $mapId, 'mapName' => $map->getName(), 'mapDescription' => str_replace('<br>', "\n", $map->getDescription()), 'bounds' => $map->getBounds()->toArray(), 'places' => &$places];
return new HtmlContent('admin/map_editor', $data);
return new HtmlContent('admin/map_editor', [
'mapId' => $mapId,
'mapName' => $map->getName(),
'mapDescription' => str_replace('<br>', "\n", $map->getDescription()),
'bounds' => $map->getBounds()->toArray(),
'places' => &$places
]);
}
public function getPlace(): IContent
@@ -67,8 +70,7 @@ class MapAdminController implements ISecured
$place = $this->placeRepository->getById($placeId);
$data = ['panoId' => $place->getFreshPanoId()];
return new JsonContent($data);
return new JsonContent(['panoId' => $place->getFreshPanoId()]);
}
public function saveMap(): IContent
@@ -94,6 +96,11 @@ class MapAdminController implements ISecured
$place->setMap($map);
$place->setLat((float) $placeRaw['lat']);
$place->setLng((float) $placeRaw['lng']);
$place->setPov(new Pov(
(float) $placeRaw['pov']['heading'],
(float) $placeRaw['pov']['pitch'],
(float) $placeRaw['pov']['zoom']
));
if ($placeRaw['panoId'] === -1) {
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
@@ -114,6 +121,11 @@ class MapAdminController implements ISecured
$place = $this->placeRepository->getById((int) $placeRaw['id']);
$place->setLat((float) $placeRaw['lat']);
$place->setLng((float) $placeRaw['lng']);
$place->setPov(new Pov(
(float) $placeRaw['pov']['heading'],
(float) $placeRaw['pov']['pitch'],
(float) $placeRaw['pov']['zoom']
));
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
$this->pdm->saveToDb($place);
@@ -146,11 +158,11 @@ class MapAdminController implements ISecured
\Container::$dbConnection->commit();
$data = ['mapId' => $map->getId(), 'added' => $addedIds];
return new JsonContent($data);
return new JsonContent(['mapId' => $map->getId(), 'added' => $addedIds]);
}
public function deleteMap() {
public function deleteMap()
{
$mapId = (int) $this->request->query('mapId');
$map = $this->mapRepository->getById($mapId);
@@ -163,40 +175,22 @@ class MapAdminController implements ISecured
\Container::$dbConnection->commit();
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
private function deletePlaces(Map $map): void
{
//TODO: relations?
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(Place::getFields());
$select->where('map_id', '=', $map->getId());
$result = $select->execute();
while ($placeData = $result->fetch(IResultSet::FETCH_ASSOC)) {
$place = new Place();
$this->pdm->fillWithData($placeData, $place);
foreach ($this->placeRepository->getAllForMap($map) as $place) {
$this->pdm->deleteFromDb($place);
}
}
private function calculateMapBounds(Map $map): Bounds
{
//TODO: from repository or relations
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['lat', 'lng']);
$select->where('map_id', '=', $map->getId());
$result = $select->execute();
$bounds = new Bounds();
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
$bounds->extend(new Position($place['lat'], $place['lng']));
foreach ($this->placeRepository->getAllForMap($map) as $place) {
$bounds->extend($place->getPosition());
}
return $bounds;
@@ -204,26 +198,19 @@ class MapAdminController implements ISecured
private function &getPlaces(Map $map): array
{
//TODO: from repository or relations
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp']);
$select->where('map_id', '=', $map->getId());
$result = $select->execute();
$places = [];
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
//$panoId = ???
//$pov = ???
$noPano = $place['pano_id_cached_timestamp'] && $place['pano_id_cached'] === null;
foreach ($this->placeRepository->getAllForMap($map) as $place) {
$noPano = $place->getPanoIdCachedTimestampDate() !== null && $place->getPanoIdCached() === null;
$places[$place['id']] = [
'id' => $place['id'],
'lat' => $place['lat'],
'lng' => $place['lng'],
$placeId = $place->getId();
$places[$placeId] = [
'id' => $placeId,
'lat' => $place->getLat(),
'lng' => $place->getLng(),
'panoId' => null,
'pov' => ['heading' => 0.0, 'pitch' => 0.0, 'zoom' => 0.0],
'pov' => $place->getPov()->toArray(),
'noPano' => $noPano
];
}
+5 -3
View File
@@ -19,7 +19,7 @@ class MapsController
public function getMaps(): IContent
{
//TODO: from repository
//TODO: from repository - count should be added
$select = new Select(\Container::$dbConnection, 'maps');
$select->columns([
['maps', 'id'],
@@ -46,8 +46,10 @@ class MapsController
}
$user = $this->request->user();
$data = ['maps' => $maps, 'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN)];
return new HtmlContent('maps', $data);
return new HtmlContent('maps', [
'maps' => $maps,
'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN)
]);
}
private function formatMapAreaForHuman(float $area): array
+50 -31
View File
@@ -1,18 +1,16 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use MapGuesser\Database\Query\Select;
use MapGuesser\Http\Request;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect;
@@ -26,11 +24,14 @@ class UserController implements ISecured
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
}
public function authorize(): bool
@@ -47,8 +48,7 @@ class UserController implements ISecured
*/
$user = $this->request->user();
$data = ['user' => $user->toArray()];
return new HtmlContent('account/account', $data);
return new HtmlContent('account/account', ['user' => $user->toArray()]);
}
public function getGoogleAuthenticateRedirect(): IRedirect
@@ -59,14 +59,17 @@ class UserController implements ISecured
$user = $this->request->user();
$state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
$this->request->session()->set('oauth_state', $state);
$this->request->session()->set('oauth_nonce', $nonce);
$oAuth = new GoogleOAuth(new Request());
$url = $oAuth->getDialogUrl(
$state,
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(),
$nonce,
$user->getEmail()
);
@@ -81,31 +84,40 @@ class UserController implements ISecured
$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);
return new HtmlContent('account/google_authenticate', ['success' => false]);
}
$oAuth = new GoogleOAuth(new Request());
$tokenData = $oAuth->getToken($this->request->query('code'), $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink());
$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);
return new HtmlContent('account/google_authenticate', ['success' => false]);
}
$jwtParser = new JwtParser($tokenData['id_token']);
$userData = $jwtParser->getPayload();
$idToken = $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);
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
return new HtmlContent('account/google_authenticate', ['success' => false]);
}
if ($idToken['sub'] !== $user->getGoogleSub()) {
return new HtmlContent('account/google_authenticate', [
'success' => false,
'errorText' => 'This Google account is not linked to your account.'
]);
}
$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);
return new HtmlContent('account/google_authenticate', [
'success' => true,
'authenticatedWithGoogleUntil' => $authenticatedWithGoogleUntil
]);
}
public function getDeleteAccount(): IContent
@@ -115,8 +127,7 @@ class UserController implements ISecured
*/
$user = $this->request->user();
$data = ['user' => $user->toArray()];
return new HtmlContent('account/delete', $data);
return new HtmlContent('account/delete', ['user' => $user->toArray()]);
}
public function saveAccount(): IContent
@@ -132,19 +143,24 @@ class UserController implements ISecured
$this->request->post('password'),
$error
)) {
$data = ['error' => ['errorText' => $error]];
return new JsonContent($data);
return new JsonContent(['error' => ['errorText' => $error]]);
}
if (strlen($this->request->post('password_new')) > 0) {
if (strlen($this->request->post('password_new')) < 6) {
$data = ['error' => ['errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!']];
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!'
]
]);
}
if ($this->request->post('password_new') !== $this->request->post('password_new_confirm')) {
$data = ['error' => ['errorText' => 'The given new passwords do not match.']];
return new JsonContent($data);
return new JsonContent([
'error' => [
'errorText' => 'The given new passwords do not match.'
]
]);
}
$user->setPlainPassword($this->request->post('password_new'));
@@ -154,8 +170,7 @@ class UserController implements ISecured
$this->request->session()->delete('authenticated_with_google_until');
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
public function deleteAccount(): IContent
@@ -171,24 +186,28 @@ class UserController implements ISecured
$this->request->post('password'),
$error
)) {
$data = ['error' => ['errorText' => $error]];
return new JsonContent($data);
return new JsonContent(['error' => ['errorText' => $error]]);
}
\Container::$dbConnection->startTransaction();
foreach ($this->userConfirmationRepository->getByUser($user) as $userConfirmation) {
$userConfirmation = $this->userConfirmationRepository->getByUser($user);
if ($userConfirmation !== null) {
$this->pdm->deleteFromDb($userConfirmation);
}
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($userPasswordResetter !== null) {
$this->pdm->deleteFromDb($userPasswordResetter);
}
$this->pdm->deleteFromDb($user);
\Container::$dbConnection->commit();
$this->request->session()->delete('authenticated_with_google_until');
$data = ['success' => true];
return new JsonContent($data);
return new JsonContent(['success' => true]);
}
private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, &$error): bool
+1 -1
View File
@@ -135,6 +135,6 @@ class Modify
private function generateKey(): string
{
return substr(hash('sha256', serialize($this->attributes) . random_bytes(10) . microtime()), 0, 7);
return substr(hash('sha256', serialize($this->attributes) . random_bytes(5) . microtime()), 0, 7);
}
}
+3 -1
View File
@@ -2,7 +2,9 @@
interface IContent
{
public function &getData(): array;
public function setData(array $data): void;
public function getData(): array;
public function render(): void;
@@ -0,0 +1,9 @@
<?php namespace MapGuesser\Interfaces\Session;
use SessionHandlerInterface;
use SessionIdInterface;
use SessionUpdateTimestampHandlerInterface;
interface ISessionHandler extends SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface
{
}
+5 -2
View File
@@ -15,7 +15,7 @@ class GoogleOAuth
$this->request = $request;
}
public function getDialogUrl(string $state, string $redirectUrl, ?string $loginHint = null): string
public function getDialogUrl(string $state, string $redirectUrl, ?string $nonce = null, ?string $loginHint = null): string
{
$oauthParams = [
'response_type' => 'code',
@@ -23,9 +23,12 @@ class GoogleOAuth
'scope' => 'openid email',
'redirect_uri' => $redirectUrl,
'state' => $state,
'nonce' => hash('sha256', random_bytes(10) . microtime()),
];
if ($nonce !== null) {
$oauthParams['nonce'] = $nonce;
}
if ($loginHint !== null) {
$oauthParams['login_hint'] = $loginHint;
}
+46 -2
View File
@@ -5,12 +5,13 @@ use DateTime;
use MapGuesser\Http\Request;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Util\Geo\Position;
use MapGuesser\Util\Panorama\Pov;
class Place extends Model
{
protected static string $table = 'places';
protected static array $fields = ['map_id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp'];
protected static array $fields = ['map_id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp', 'pov_heading', 'pov_pitch', 'pov_zoom'];
protected static array $relations = ['map' => Map::class];
@@ -24,9 +25,12 @@ class Place extends Model
private ?DateTime $panoIdCachedTimestamp = null;
private Pov $pov;
public function __construct()
{
$this->position = new Position(0.0, 0.0);
$this->pov = new Pov(0.0, 0.0, 0.0);
}
public function setMap(Map $map): void
@@ -54,6 +58,26 @@ class Place extends Model
$this->position->setLng($lng);
}
public function setPov(Pov $pov): void
{
$this->pov = $pov;
}
public function setPovHeading(float $heading): void
{
$this->pov->setHeading($heading);
}
public function setPovPitch(float $pitch): void
{
$this->pov->setPitch($pitch);
}
public function setPovZoom(float $zoom): void
{
$this->pov->setZoom($zoom);
}
public function setPanoIdCached(?string $panoIdCached): void
{
$this->panoIdCached = $panoIdCached;
@@ -96,6 +120,26 @@ class Place extends Model
return $this->position->getLng();
}
public function getPov(): Pov
{
return $this->pov;
}
public function getPovHeading(): float
{
return $this->pov->getHeading();
}
public function getPovPitch(): float
{
return $this->pov->getPitch();
}
public function getPovZoom(): float
{
return $this->pov->getZoom();
}
public function getFreshPanoId(bool $canBeIndoor = false): ?string
{
if (
@@ -122,7 +166,7 @@ class Place extends Model
}
$this->panoIdCached = $panoId;
$this->panoIdCachedTimestamp = new DateTime('now');
$this->panoIdCachedTimestamp = new DateTime();
(new PersistentDataManager())->saveToDb($this);
+24 -1
View File
@@ -1,12 +1,13 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
use MapGuesser\Interfaces\Authentication\IUser;
class User extends Model implements IUser
{
protected static string $table = 'users';
protected static array $fields = ['email', 'password', 'type', 'active', 'google_sub'];
protected static array $fields = ['email', 'password', 'type', 'active', 'google_sub', 'created'];
private static array $types = ['user', 'admin'];
@@ -20,6 +21,8 @@ class User extends Model implements IUser
private ?string $googleSub = null;
private DateTime $created;
public function setEmail(string $email): void
{
$this->email = $email;
@@ -52,6 +55,16 @@ class User extends Model implements IUser
$this->googleSub = $googleSub;
}
public function setCreatedDate(DateTime $created): void
{
$this->created = $created;
}
public function setCreated(string $created): void
{
$this->created = new DateTime($created);
}
public function getEmail(): string
{
return $this->email;
@@ -77,6 +90,16 @@ class User extends Model implements IUser
return $this->googleSub;
}
public function getCreatedDate(): DateTime
{
return $this->created;
}
public function getCreated(): string
{
return $this->created->format('Y-m-d H:i:s');
}
public function hasPermission(int $permission): bool
{
switch ($permission) {
+25 -1
View File
@@ -1,10 +1,12 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
class UserConfirmation extends Model
{
protected static string $table = 'user_confirmations';
protected static array $fields = ['user_id', 'token'];
protected static array $fields = ['user_id', 'token', 'last_sent'];
protected static array $relations = ['user' => User::class];
@@ -14,6 +16,8 @@ class UserConfirmation extends Model
private string $token = '';
private DateTime $lastSent;
public function setUser(User $user): void
{
$this->user = $user;
@@ -29,6 +33,16 @@ class UserConfirmation extends Model
$this->token = $token;
}
public function setLastSentDate(DateTime $lastSent): void
{
$this->lastSent = $lastSent;
}
public function setLastSent(string $lastSent): void
{
$this->lastSent = new DateTime($lastSent);
}
public function getUser(): ?User
{
return $this->user;
@@ -43,4 +57,14 @@ class UserConfirmation extends Model
{
return $this->token;
}
public function getLastSentDate(): DateTime
{
return $this->lastSent;
}
public function getLastSent(): string
{
return $this->lastSent->format('Y-m-d H:i:s');
}
}
@@ -0,0 +1,70 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
class UserPasswordResetter extends Model
{
protected static string $table = 'user_password_resetters';
protected static array $fields = ['user_id', 'token', 'expires'];
protected static array $relations = ['user' => User::class];
private ?User $user = null;
private ?int $userId = null;
private string $token = '';
private DateTime $expires;
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function setToken(string $token): void
{
$this->token = $token;
}
public function setExpiresDate(DateTime $expires): void
{
$this->expires = $expires;
}
public function setExpires(string $expires): void
{
$this->expires = new DateTime($expires);
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getToken(): string
{
return $this->token;
}
public function getExpiresDate(): DateTime
{
return $this->expires;
}
public function getExpires(): string
{
return $this->expires->format('Y-m-d H:i:s');
}
}
+2 -1
View File
@@ -27,8 +27,9 @@ class PersistentDataManager
public function selectMultipleFromDb(Select $select, string $type, bool $withRelations = false): Generator
{
$select = $this->createSelect($select, $type, $withRelations);
$result = $select->execute();
while ($data = $select->execute()->fetch(IResultSet::FETCH_ASSOC)) {
while ($data = $result->fetch(IResultSet::FETCH_ASSOC)) {
$model = new $type();
$this->fillWithData($data, $model);
+12 -1
View File
@@ -1,6 +1,8 @@
<?php namespace MapGuesser\Repository;
use Generator;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\PersistentDataManager;
@@ -18,6 +20,15 @@ class PlaceRepository
return $this->pdm->selectFromDbById($placeId, Place::class);
}
public function getAllForMap(Map $map): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('map_id', '=', $map->getId());
yield from $this->pdm->selectMultipleFromDb($select, Place::class);
}
//TODO: use Map instead of id
public function getRandomForMapWithValidPano(int $mapId, array $exclude = [], array &$placesWithoutPano): Place
{
$placesWithoutPano = [];
@@ -37,7 +48,7 @@ class PlaceRepository
private function selectRandomFromDbForMap(int $mapId, array $exclude): ?Place
{
$select = new Select(\Container::$dbConnection, 'places');
$select = new Select(\Container::$dbConnection);
$select->where('id', 'NOT IN', $exclude);
$select->where('map_id', '=', $mapId);
@@ -1,8 +1,6 @@
<?php namespace MapGuesser\Repository;
use Generator;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\PersistentDataManager;
@@ -29,11 +27,11 @@ class UserConfirmationRepository
return $this->pdm->selectFromDb($select, UserConfirmation::class);
}
public function getByUser(User $user): Generator
public function getByUser(User $user): ?UserConfirmation
{
$select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $user->getId());
yield from $this->pdm->selectMultipleFromDb($select, UserConfirmation::class);
return $this->pdm->selectFromDb($select, UserConfirmation::class);
}
}
@@ -0,0 +1,47 @@
<?php namespace MapGuesser\Repository;
use DateTime;
use Generator;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserPasswordResetter;
use MapGuesser\PersistentData\PersistentDataManager;
class UserPasswordResetterRepository
{
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $userConfirmationId): ?UserPasswordResetter
{
return $this->pdm->selectFromDbById($userConfirmationId, UserPasswordResetter::class);
}
public function getByToken(string $token): ?UserPasswordResetter
{
$select = new Select(\Container::$dbConnection);
$select->where('token', '=', $token);
return $this->pdm->selectFromDb($select, UserPasswordResetter::class);
}
public function getByUser(User $user): ?UserPasswordResetter
{
$select = new Select(\Container::$dbConnection);
$select->where('user_id', '=', $user->getId());
return $this->pdm->selectFromDb($select, UserPasswordResetter::class);
}
public function getAllExpired(): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('expires', '<', (new DateTime())->format('Y-m-d H:i:s'));
yield from $this->pdm->selectMultipleFromDb($select, UserPasswordResetter::class);
}
}
+11
View File
@@ -1,5 +1,7 @@
<?php namespace MapGuesser\Repository;
use DateTime;
use Generator;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\PersistentDataManager;
@@ -33,4 +35,13 @@ class UserRepository
return $this->pdm->selectFromDb($select, User::class);
}
public function getAllInactiveExpired(): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('active', '=', false);
$select->where('created', '<', (new DateTime('-1 day'))->format('Y-m-d H:i:s'));
yield from $this->pdm->selectMultipleFromDb($select, User::class);
}
}
+6 -1
View File
@@ -6,7 +6,12 @@ abstract class ContentBase implements IContent
{
protected array $data;
public function &getData(): array
public function setData(array $data): void
{
$this->data = $data;
}
public function getData(): array
{
return $this->data;
}
+2 -2
View File
@@ -6,10 +6,10 @@ class HtmlContent extends ContentBase
{
private string $view;
public function __construct(string $view, array &$data = [])
public function __construct(string $view, array $data = [])
{
$this->view = $view;
$this->data = &$data;
$this->data = $data;
}
public function render(): void
+2 -2
View File
@@ -2,9 +2,9 @@
class JsonContent extends ContentBase
{
public function __construct(array &$data = [])
public function __construct(array $data = [])
{
$this->data = &$data;
$this->data = $data;
}
public function render(): void
+14 -23
View File
@@ -4,11 +4,9 @@ use DateTime;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use SessionHandlerInterface;
use SessionIdInterface;
use SessionUpdateTimestampHandlerInterface;
use MapGuesser\Interfaces\Session\ISessionHandler;
class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface
class DatabaseSessionHandler implements ISessionHandler
{
private bool $exists = false;
@@ -28,7 +26,7 @@ class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterf
{
$select = new Select(\Container::$dbConnection, 'sessions');
$select->columns(['data']);
$select->whereId($id);
$select->whereId(substr($id, 0, 32));
$result = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
@@ -46,16 +44,16 @@ class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterf
$modify = new Modify(\Container::$dbConnection, 'sessions');
if ($this->exists) {
$modify->setId($id);
$modify->setId(substr($id, 0, 32));
} else {
$modify->setExternalId($id);
$modify->setExternalId(substr($id, 0, 32));
}
$modify->set('data', $data);
$modify->set('updated', (new DateTime())->format('Y-m-d H:i:s'));
$modify->save();
$written = true;
$this->written = true;
return true;
}
@@ -63,37 +61,30 @@ class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterf
public function destroy($id): bool
{
$modify = new Modify(\Container::$dbConnection, 'sessions');
$modify->setId($id);
$modify->setId(substr($id, 0, 32));
$modify->delete();
$this->exists = false;
return true;
}
public function gc($maxlifetime): int
{
$select = new Select(\Container::$dbConnection, 'sessions');
$select->columns(['id']);
$select->where('updated', '<', (new DateTime('-' . $maxlifetime . ' seconds'))->format('Y-m-d H:i:s'));
$result = $select->execute();
while ($session = $result->fetch(IResultSet::FETCH_ASSOC)) {
$modify = new Modify(\Container::$dbConnection, 'sessions');
$modify->setId($session['id']);
$modify->delete();
}
// empty on purpose
// old sessions are deleted by MaintainDatabaseCommand
return true;
}
public function create_sid(): string
{
return hash('sha256', random_bytes(10) . microtime());
return bin2hex(random_bytes(16));
}
public function validateId($id): bool
{
return preg_match('/^[a-f0-9]{64}$/', $id);
return preg_match('/^[a-f0-9]{32}$/', $id);
}
public function updateTimestamp($id, $data): bool
@@ -104,7 +95,7 @@ class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterf
$modify = new Modify(\Container::$dbConnection, 'sessions');
$modify->setId($id);
$modify->setId(substr($id, 0, 32));
$modify->set('updated', (new DateTime())->format('Y-m-d H:i:s'));
$modify->save();
+56
View File
@@ -0,0 +1,56 @@
<?php namespace MapGuesser\Util\Panorama;
class Pov
{
private float $heading;
private float $pitch;
private float $zoom;
public function __construct(float $heading, float $pitch, float $zoom)
{
$this->heading = $heading;
$this->pitch = $pitch;
$this->zoom = $zoom;
}
public function setHeading(float $heading): void
{
$this->heading = $heading;
}
public function setPitch(float $pitch): void
{
$this->pitch = $pitch;
}
public function setZoom(float $zoom): void
{
$this->zoom = $zoom;
}
public function getHeading(): float
{
return $this->heading;
}
public function getPitch(): float
{
return $this->pitch;
}
public function getZoom(): float
{
return $this->zoom;
}
public function toArray(): array
{
return [
'heading' => $this->heading,
'pitch' => $this->pitch,
'zoom' => $this->zoom
];
}
}
+4 -8
View File
@@ -13,6 +13,7 @@ final class GoogleOAuthTest extends TestCase
{
$_ENV['GOOGLE_OAUTH_CLIENT_ID'] = 'xyz';
$state = 'random_state_string';
$nonce = 'random_nonce_string';
$redirectUrl = 'http://example.com/oauth';
$requestMock = $this->getMockBuilder(IRequest::class)
@@ -20,7 +21,7 @@ final class GoogleOAuthTest extends TestCase
->getMock();
$googleOAuth = new GoogleOAuth($requestMock);
$dialogUrl = $googleOAuth->getDialogUrl($state, $redirectUrl);
$dialogUrl = $googleOAuth->getDialogUrl($state, $redirectUrl, $nonce);
$dialogUrlParsed = explode('?', $dialogUrl);
$this->assertEquals('https://accounts.google.com/o/oauth2/v2/auth', $dialogUrlParsed[0]);
@@ -33,15 +34,10 @@ final class GoogleOAuthTest extends TestCase
'scope' => 'openid email',
'redirect_uri' => $redirectUrl,
'state' => $state,
'nonce' => hash('sha256', random_bytes(10) . microtime()),
'nonce' => $nonce,
];
$this->assertEquals($expectedQueryParams['response_type'], $dialogUrlQueryParams['response_type']);
$this->assertEquals($expectedQueryParams['client_id'], $dialogUrlQueryParams['client_id']);
$this->assertEquals($expectedQueryParams['scope'], $dialogUrlQueryParams['scope']);
$this->assertEquals($expectedQueryParams['redirect_uri'], $dialogUrlQueryParams['redirect_uri']);
$this->assertEquals($expectedQueryParams['state'], $dialogUrlQueryParams['state']);
$this->assertMatchesRegularExpression('/^[a-f0-9]{64}$/', $dialogUrlQueryParams['nonce']);
$this->assertEquals($expectedQueryParams, $dialogUrlQueryParams);
}
public function testCanRequestToken(): void
+1 -1
View File
@@ -3,6 +3,6 @@
@section(main)
<h2>Account activation</h2>
<div class="box">
<p class="error justify">Activation failed. Please check the link you entered or retry <a href="/signup" title="Sign up">sign up</a>!</p>
<p class="error justify">Activation failed. Please check the link you entered, or retry <a href="/signup" title="Sign up">signing up</a>!</p>
</div>
@endsection
+2
View File
@@ -10,6 +10,8 @@
<div class="right marginTop">
<button type="submit">Login</button>
</div>
<p class="center marginTop"><a href="/password/requestReset" title="Request password reset">Forgot your password?</a></p>
<p class="center marginTop"><a href="/signup" title="Sign up">New to <?= $_ENV['APP_NAME'] ?>?</a></p>
<hr>
<div class="center">
<a class="button yellow" href="/login/google" title="Login with Google">Login with Google</a>
+14
View File
@@ -0,0 +1,14 @@
@extends(templates/layout_normal)
@section(main)
<h2>Request password reset</h2>
<div class="box">
<form id="passwordResetForm" action="/password/requestReset" method="post" data-redirect-on-success="/password/requestReset/success">
<input class="big fullWidth" type="email" name="email" placeholder="Email address" value="<?= isset($email) ? $email : '' ?>" required autofocus>
<p id="passwordResetFormError" class="formError justify marginTop"></p>
<div class="right marginTop">
<button type="submit">Continue</button>
</div>
</form>
</div>
@endsection
@@ -0,0 +1,8 @@
@extends(templates/layout_normal)
@section(main)
<h2>Request password reset</h2>
<div class="box">
<p class="justify">Password reset was successfully requested. Please check your email and click on the link to reset your password!</p>
</div>
@endsection
+20
View File
@@ -0,0 +1,20 @@
@extends(templates/layout_normal)
@section(main)
<h2>Reset password</h2>
<div class="box">
<?php if ($success) : ?>
<form id="resetPasswordForm" action="/password/reset/<?= $token ?>" method="post" data-redirect-on-success="/">
<input class="big fullWidth" type="email" name="email" placeholder="Email address" value="<?= $email ?>" disabled>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Password" required minlength="6" autofocus>
<input class="big fullWidth marginTop" type="password" name="password_confirm" placeholder="Password confirmation" required minlength="6">
<p id="resetPasswordFormError" class="formError justify marginTop"></p>
<div class="right">
<button class="marginTop" type="submit">Reset password</button>
</div>
</form>
<?php else: ?>
<p class="error justify">Confirming your identity failed. Please check the link you entered, or retry <a href="/password/requestReset" title="Request password reset">requesting password reset</a>!</p>
<?php endif; ?>
</div>
@endsection
+1
View File
@@ -22,6 +22,7 @@
--><button id="resetSignupButton" class="gray marginTop marginLeft" type="button">Reset</button>
<?php endif; ?>
</div>
<p class="center marginTop"><a href="/login" title="Login">Already have an account?</a></p>
<hr>
<div class="center">
<a class="button yellow" href="/login/google" title="Signup with Google">Signup with Google</a>
+15 -2
View File
@@ -31,6 +31,13 @@ Container::$routeCollection->group('signup', function (MapGuesser\Routing\RouteC
$routeCollection->get('signup.activate', 'activate/{token}', [MapGuesser\Controller\LoginController::class, 'activate']);
$routeCollection->get('signup.cancel', 'cancel/{token}', [MapGuesser\Controller\LoginController::class, 'cancel']);
});
Container::$routeCollection->group('password', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('password-requestReset', 'requestReset', [MapGuesser\Controller\LoginController::class, 'getRequestPasswordResetForm']);
$routeCollection->post('password-requestReset-action', 'requestReset', [MapGuesser\Controller\LoginController::class, 'requestPasswordReset']);
$routeCollection->get('password-requestReset.success', 'requestReset/success', [MapGuesser\Controller\LoginController::class, 'getRequestPasswordResetSuccess']);
$routeCollection->get('password-reset', 'reset/{token}', [MapGuesser\Controller\LoginController::class, 'getResetPasswordForm']);
$routeCollection->post('password-reset.action', 'reset/{token}', [MapGuesser\Controller\LoginController::class, 'resetPassword']);
});
Container::$routeCollection->get('logout', 'logout', [MapGuesser\Controller\LoginController::class, 'logout']);
Container::$routeCollection->group('account', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('account', '', [MapGuesser\Controller\UserController::class, 'getAccount']);
@@ -60,10 +67,16 @@ if (isset($_COOKIE['COOKIES_CONSENT'])) {
session_set_save_handler(Container::$sessionHandler, true);
session_start([
'gc_maxlifetime' => 604800,
'cookie_lifetime' => 604800,
'gc_probability' => 0, // old sessions are deleted by MaintainDatabaseCommand
'cookie_lifetime' => 604800, // TODO: cookie is not renewed so session can be lost
'cookie_httponly' => true,
'cookie_samesite' => 'Lax'
]);
// this is needed to handle old type of session IDs
if (!Container::$sessionHandler->validateId(session_id())) {
session_regenerate_id(true);
}
} else {
$_SESSION = [];
}
@@ -71,5 +84,5 @@ if (isset($_COOKIE['COOKIES_CONSENT'])) {
Container::$request = new MapGuesser\Request\Request($_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'], $_GET, $_POST, $_SESSION);
if (!Container::$request->session()->has('anti_csrf_token')) {
Container::$request->session()->set('anti_csrf_token', hash('sha256', random_bytes(10) . microtime()));
Container::$request->session()->set('anti_csrf_token', bin2hex(random_bytes(16)));
}