From b254603fb1a5a3a43f65cb2d29e54c9ce9dc4709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=91cze=20Bence?= Date: Sun, 5 Jul 2020 16:37:37 +0200 Subject: [PATCH 1/7] MAPG-185 add getter for expired users and password resetters --- src/Repository/UserPasswordResetterRepository.php | 10 ++++++++++ src/Repository/UserRepository.php | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/Repository/UserPasswordResetterRepository.php b/src/Repository/UserPasswordResetterRepository.php index 466c3c8..47c70d7 100644 --- a/src/Repository/UserPasswordResetterRepository.php +++ b/src/Repository/UserPasswordResetterRepository.php @@ -1,5 +1,7 @@ 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); + } } diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index c7ddcaf..be96c79 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -1,5 +1,7 @@ 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); + } } From f3d16081645d71422a6bd9303ee6b546999e3f97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=91cze=20Bence?= Date: Sun, 5 Jul 2020 16:43:36 +0200 Subject: [PATCH 2/7] MAPG-185 add maintain database command --- mapg | 1 + src/Cli/MaintainDatabaseCommand.php | 107 ++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) mode change 100755 => 100644 mapg create mode 100644 src/Cli/MaintainDatabaseCommand.php diff --git a/mapg b/mapg old mode 100755 new mode 100644 index 8acfda2..684c7b4 --- a/mapg +++ b/mapg @@ -8,5 +8,6 @@ $app = new Symfony\Component\Console\Application('MapGuesser Console', ''); $app->add(new MapGuesser\Cli\DatabaseMigration()); $app->add(new MapGuesser\Cli\AddUserCommand()); $app->add(new MapGuesser\Cli\LinkViewCommand()); +$app->add(new \MapGuesser\Cli\MaintainDatabaseCommand()); $app->run(); diff --git a/src/Cli/MaintainDatabaseCommand.php b/src/Cli/MaintainDatabaseCommand.php new file mode 100644 index 0000000..f2afc70 --- /dev/null +++ b/src/Cli/MaintainDatabaseCommand.php @@ -0,0 +1,107 @@ +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('Maintenance failed!'); + $output->writeln(''); + + $output->writeln((string) $e); + $output->writeln(''); + + return 1; + } + + $output->writeln('Maintenance was successful!'); + $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(); + } + } +} From 0bcb0187c1b84e7ed48b96a22378c9be6868a619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=91cze=20Bence?= Date: Sun, 5 Jul 2020 16:45:38 +0200 Subject: [PATCH 3/7] MAPG-185 rename migrate database command --- mapg | 2 +- scripts/install.sh | 2 +- scripts/update.sh | 2 +- src/Cli/{DatabaseMigration.php => MigrateDatabaseCommand.php} | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) mode change 100644 => 100755 mapg rename src/Cli/{DatabaseMigration.php => MigrateDatabaseCommand.php} (97%) diff --git a/mapg b/mapg old mode 100644 new mode 100755 index 684c7b4..65ffefd --- a/mapg +++ b/mapg @@ -5,7 +5,7 @@ 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()); diff --git a/scripts/install.sh b/scripts/install.sh index d6191c5..2ba224f 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -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..." diff --git a/scripts/update.sh b/scripts/update.sh index 8c44bc4..2bd04b6 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -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..." diff --git a/src/Cli/DatabaseMigration.php b/src/Cli/MigrateDatabaseCommand.php similarity index 97% rename from src/Cli/DatabaseMigration.php rename to src/Cli/MigrateDatabaseCommand.php index f7f600b..ac81743 100644 --- a/src/Cli/DatabaseMigration.php +++ b/src/Cli/MigrateDatabaseCommand.php @@ -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.'); } From b2e09c394b74234b707acbaeac2340c5d71176cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=91cze=20Bence?= Date: Sun, 5 Jul 2020 16:48:37 +0200 Subject: [PATCH 4/7] MAPG-185 switch off builtin session gc --- src/Session/DatabaseSessionHandler.php | 13 ++----------- web.php | 3 ++- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/Session/DatabaseSessionHandler.php b/src/Session/DatabaseSessionHandler.php index 49f7f6f..3949625 100644 --- a/src/Session/DatabaseSessionHandler.php +++ b/src/Session/DatabaseSessionHandler.php @@ -71,17 +71,8 @@ class DatabaseSessionHandler implements ISessionHandler 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; } diff --git a/web.php b/web.php index 6ebfecf..920ad90 100644 --- a/web.php +++ b/web.php @@ -67,7 +67,8 @@ 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' ]); From 0ba1cda884a4e6184067b7b44f15aa6f60a09533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=91cze=20Bence?= Date: Sun, 5 Jul 2020 17:36:27 +0200 Subject: [PATCH 5/7] MAPG-185 fix getter of User --- src/PersistentData/Model/User.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PersistentData/Model/User.php b/src/PersistentData/Model/User.php index 147362d..a4ebf60 100644 --- a/src/PersistentData/Model/User.php +++ b/src/PersistentData/Model/User.php @@ -90,7 +90,7 @@ class User extends Model implements IUser return $this->googleSub; } - public function getCreatedData(): DateTime + public function getCreatedDate(): DateTime { return $this->created; } From 8403c21ec046a0fbdefb280b481fc9d69ca3a0d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=91cze=20Bence?= Date: Sun, 5 Jul 2020 17:37:02 +0200 Subject: [PATCH 6/7] MAPG-185 consolidation of email templates --- mail/password-reset.html | 5 +++-- mail/signup.html | 10 +++++++--- src/Controller/LoginController.php | 9 +++++---- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/mail/password-reset.html b/mail/password-reset.html index bf5b60d..9248002 100644 --- a/mail/password-reset.html +++ b/mail/password-reset.html @@ -2,8 +2,9 @@ Hi,

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:
-{{RESET_LINK}}
-(This link expires at {{EXPIRES}}.) +{{RESET_LINK}} +

+You can reset your password with this link util {{EXPIRES}}.

If you did not requested password reset, no further action is required, your account is not touched.

diff --git a/mail/signup.html b/mail/signup.html index dcb3849..ebdec4f 100644 --- a/mail/signup.html +++ b/mail/signup.html @@ -1,10 +1,14 @@ Hi,

-You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}). To activate your account, please click on the following link:
+You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}). +To activate your account, please click on the following link:
{{ACTIVATE_LINK}}

-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.
-However if you want to immediately delete it, please click on the following link:
+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. +

+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:
{{CANCEL_LINK}}

Have fun on {{APP_NAME}}! diff --git a/src/Controller/LoginController.php b/src/Controller/LoginController.php index 92d7a46..e54d44a 100644 --- a/src/Controller/LoginController.php +++ b/src/Controller/LoginController.php @@ -328,7 +328,7 @@ class LoginController \Container::$dbConnection->commit(); - $this->sendConfirmationEmail($user->getEmail(), $token); + $this->sendConfirmationEmail($user->getEmail(), $token, $user->getCreatedDate()); $this->request->session()->delete('tmp_user_data'); @@ -551,7 +551,7 @@ class LoginController return new JsonContent(['success' => true]); } - private function sendConfirmationEmail(string $email, string $token): void + private function sendConfirmationEmail(string $email, string $token, DateTime $created): void { $mail = new Mail(); $mail->addRecipient($email); @@ -562,6 +562,7 @@ class LoginController \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(); } @@ -578,7 +579,7 @@ class LoginController $this->pdm->saveToDb($confirmation); - $this->sendConfirmationEmail($user->getEmail(), $confirmation->getToken()); + $this->sendConfirmationEmail($user->getEmail(), $confirmation->getToken(), $user->getCreatedDate()); return true; } @@ -603,7 +604,7 @@ class LoginController 'EMAIL' => $email, 'RESET_LINK' => $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]), - 'EXPIRES' => $expires->format('Y-m-d H:i:s T') + 'EXPIRES' => $expires->format('Y-m-d H:i T') ]); $mail->send(); } From a8b1ab30575feb76be7c35338a0875b8303b5206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=91cze=20Bence?= Date: Sun, 5 Jul 2020 17:52:13 +0200 Subject: [PATCH 7/7] MAPG-185 adapt README --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index db50a03..f7371e5 100644 --- a/README.md +++ b/README.md @@ -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: