Merged in feature/MAPG-185-create-cron-script (pull request #174)
Feature/MAPG-185 create cron script
This commit is contained in:
commit
f7b268e10f
@ -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.
|
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
|
### Finalize installation
|
||||||
|
|
||||||
After you set the environment variables in the `.env` file, execute the following command:
|
After you set the environment variables in the `.env` file, execute the following command:
|
||||||
|
@ -2,8 +2,9 @@ Hi,
|
|||||||
<br><br>
|
<br><br>
|
||||||
You recently requested password reset on {{APP_NAME}} with this email address ({{EMAIL}}).
|
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>
|
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>
|
<a href="{{RESET_LINK}}" title="Reset password">{{RESET_LINK}}</a>
|
||||||
(This link expires at {{EXPIRES}}.)
|
<br><br>
|
||||||
|
You can reset your password with this link util {{EXPIRES}}.
|
||||||
<br><br>
|
<br><br>
|
||||||
If you did not requested password reset, no further action is required, your account is not touched.
|
If you did not requested password reset, no further action is required, your account is not touched.
|
||||||
<br><br>
|
<br><br>
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
Hi,
|
Hi,
|
||||||
<br><br>
|
<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>
|
<a href="{{ACTIVATE_LINK}}" title="Account activation">{{ACTIVATE_LINK}}</a>
|
||||||
<br><br>
|
<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>
|
You can activate your account until {{ACTIVATABLE_UNTIL}}.
|
||||||
However if you want to immediately delete it, please click on the following link:<br>
|
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>
|
<a href="{{CANCEL_LINK}}" title="Sign up cancellation">{{CANCEL_LINK}}</a>
|
||||||
<br><br>
|
<br><br>
|
||||||
Have fun on {{APP_NAME}}!
|
Have fun on {{APP_NAME}}!
|
||||||
|
3
mapg
3
mapg
@ -5,8 +5,9 @@ require 'main.php';
|
|||||||
|
|
||||||
$app = new Symfony\Component\Console\Application('MapGuesser Console', '');
|
$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\AddUserCommand());
|
||||||
$app->add(new MapGuesser\Cli\LinkViewCommand());
|
$app->add(new MapGuesser\Cli\LinkViewCommand());
|
||||||
|
$app->add(new \MapGuesser\Cli\MaintainDatabaseCommand());
|
||||||
|
|
||||||
$app->run();
|
$app->run();
|
||||||
|
@ -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
|
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/db/mapguesser.sql
|
||||||
|
|
||||||
echo "Migrating DB..."
|
echo "Migrating DB..."
|
||||||
(cd ${ROOT_DIR} && ./mapg migrate)
|
(cd ${ROOT_DIR} && ./mapg db:migrate)
|
||||||
|
|
||||||
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
||||||
echo "Minifying JS, CSS and SVG files..."
|
echo "Minifying JS, CSS and SVG files..."
|
||||||
|
@ -15,7 +15,7 @@ echo "Installing Yarn packages..."
|
|||||||
(cd ${ROOT_DIR}/public/static && yarn install)
|
(cd ${ROOT_DIR}/public/static && yarn install)
|
||||||
|
|
||||||
echo "Migrating DB..."
|
echo "Migrating DB..."
|
||||||
(cd ${ROOT_DIR} && ./mapg migrate)
|
(cd ${ROOT_DIR} && ./mapg db:migrate)
|
||||||
|
|
||||||
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
||||||
echo "Minifying JS, CSS and SVG files..."
|
echo "Minifying JS, CSS and SVG files..."
|
||||||
|
107
src/Cli/MaintainDatabaseCommand.php
Normal file
107
src/Cli/MaintainDatabaseCommand.php
Normal 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\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
class DatabaseMigration extends Command
|
class MigrateDatabaseCommand extends Command
|
||||||
{
|
{
|
||||||
public function configure()
|
public function configure()
|
||||||
{
|
{
|
||||||
$this->setName('migrate')
|
$this->setName('db:migrate')
|
||||||
->setDescription('Migration of database changes.');
|
->setDescription('Migration of database changes.');
|
||||||
}
|
}
|
||||||
|
|
@ -328,7 +328,7 @@ class LoginController
|
|||||||
|
|
||||||
\Container::$dbConnection->commit();
|
\Container::$dbConnection->commit();
|
||||||
|
|
||||||
$this->sendConfirmationEmail($user->getEmail(), $token);
|
$this->sendConfirmationEmail($user->getEmail(), $token, $user->getCreatedDate());
|
||||||
|
|
||||||
$this->request->session()->delete('tmp_user_data');
|
$this->request->session()->delete('tmp_user_data');
|
||||||
|
|
||||||
@ -551,7 +551,7 @@ class LoginController
|
|||||||
return new JsonContent(['success' => true]);
|
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 = new Mail();
|
||||||
$mail->addRecipient($email);
|
$mail->addRecipient($email);
|
||||||
@ -562,6 +562,7 @@ class LoginController
|
|||||||
\Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]),
|
\Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]),
|
||||||
'CANCEL_LINK' => $this->request->getBase() . '/' .
|
'CANCEL_LINK' => $this->request->getBase() . '/' .
|
||||||
\Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]),
|
\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();
|
$mail->send();
|
||||||
}
|
}
|
||||||
@ -578,7 +579,7 @@ class LoginController
|
|||||||
|
|
||||||
$this->pdm->saveToDb($confirmation);
|
$this->pdm->saveToDb($confirmation);
|
||||||
|
|
||||||
$this->sendConfirmationEmail($user->getEmail(), $confirmation->getToken());
|
$this->sendConfirmationEmail($user->getEmail(), $confirmation->getToken(), $user->getCreatedDate());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -603,7 +604,7 @@ class LoginController
|
|||||||
'EMAIL' => $email,
|
'EMAIL' => $email,
|
||||||
'RESET_LINK' => $this->request->getBase() . '/' .
|
'RESET_LINK' => $this->request->getBase() . '/' .
|
||||||
\Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]),
|
\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();
|
$mail->send();
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,7 @@ class User extends Model implements IUser
|
|||||||
return $this->googleSub;
|
return $this->googleSub;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCreatedData(): DateTime
|
public function getCreatedDate(): DateTime
|
||||||
{
|
{
|
||||||
return $this->created;
|
return $this->created;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<?php namespace MapGuesser\Repository;
|
<?php namespace MapGuesser\Repository;
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
|
use Generator;
|
||||||
use MapGuesser\Database\Query\Select;
|
use MapGuesser\Database\Query\Select;
|
||||||
use MapGuesser\PersistentData\Model\User;
|
use MapGuesser\PersistentData\Model\User;
|
||||||
use MapGuesser\PersistentData\Model\UserPasswordResetter;
|
use MapGuesser\PersistentData\Model\UserPasswordResetter;
|
||||||
@ -34,4 +36,12 @@ class UserPasswordResetterRepository
|
|||||||
|
|
||||||
return $this->pdm->selectFromDb($select, UserPasswordResetter::class);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<?php namespace MapGuesser\Repository;
|
<?php namespace MapGuesser\Repository;
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
|
use Generator;
|
||||||
use MapGuesser\Database\Query\Select;
|
use MapGuesser\Database\Query\Select;
|
||||||
use MapGuesser\PersistentData\Model\User;
|
use MapGuesser\PersistentData\Model\User;
|
||||||
use MapGuesser\PersistentData\PersistentDataManager;
|
use MapGuesser\PersistentData\PersistentDataManager;
|
||||||
@ -33,4 +35,13 @@ class UserRepository
|
|||||||
|
|
||||||
return $this->pdm->selectFromDb($select, User::class);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,17 +71,8 @@ class DatabaseSessionHandler implements ISessionHandler
|
|||||||
|
|
||||||
public function gc($maxlifetime): int
|
public function gc($maxlifetime): int
|
||||||
{
|
{
|
||||||
$select = new Select(\Container::$dbConnection, 'sessions');
|
// empty on purpose
|
||||||
$select->columns(['id']);
|
// old sessions are deleted by MaintainDatabaseCommand
|
||||||
$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();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
3
web.php
3
web.php
@ -67,7 +67,8 @@ if (isset($_COOKIE['COOKIES_CONSENT'])) {
|
|||||||
session_set_save_handler(Container::$sessionHandler, true);
|
session_set_save_handler(Container::$sessionHandler, true);
|
||||||
session_start([
|
session_start([
|
||||||
'gc_maxlifetime' => 604800,
|
'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_httponly' => true,
|
||||||
'cookie_samesite' => 'Lax'
|
'cookie_samesite' => 'Lax'
|
||||||
]);
|
]);
|
||||||
|
Loading…
Reference in New Issue
Block a user