diff --git a/app/config/services.php b/app/config/services.php index c9112e9..0193e1f 100644 --- a/app/config/services.php +++ b/app/config/services.php @@ -30,7 +30,8 @@ use Httpcb\Auth, Httpcb\Menu; use App\Listener\AclListener, - App\Listener\DispatchListener; + App\Listener\DispatchListener, + App\Listener\ActivityLog; /** * The FactoryDefault Dependency Injector automatically register the right services providing a full stack framework @@ -216,6 +217,17 @@ $di->setShared('db', function () use ($di, $config) { return $db; }); +$di->setShared('eventsManager', function () { + + $activityLog = new ActivityLog(); + + $eventsManager = new Phalcon\Events\Manager(); + $eventsManager->attach('user', $activityLog); + $eventsManager->attach('auth', $activityLog); + + return $eventsManager; +}); + /** * If the configuration specify the use of metadata adapter use it or use memory otherwise */ @@ -286,8 +298,11 @@ $di->set('oauth', function() use ($config) { return new OAuth($config); }); -$di->set('auth', function() use ($config) { - return new Auth($config); +$di->set('auth', function() use ($di, $config) { + $auth = new Auth($config); + $auth->setEventsManager($di->get('eventsManager')); + + return $auth; }); $di->set('acl', 'Httpcb\Acl'); diff --git a/app/controllers/UserController.php b/app/controllers/UserController.php index b87fb23..0d038d3 100644 --- a/app/controllers/UserController.php +++ b/app/controllers/UserController.php @@ -3,7 +3,8 @@ namespace App\Controller; use App\Controller\ControllerBase, - App\Form\UserSettings as UserSettingsForm; + App\Form\UserSettings as UserSettingsForm, + App\Model\Data\ActivityLog; class UserController extends ControllerBase { @@ -34,4 +35,14 @@ class UserController extends ControllerBase $this->view->form = $form; } + + public function activityAction($page = 1) + { + $user = $this->_getAuth()->getUser(); + + $paginator = ActivityLog::getPaginationList($user->getId(), $page); + + $this->view->page = $paginator->getPaginate(); + $this->view->pagination_url = '/user/activity/'; + } } diff --git a/app/library/Auth.php b/app/library/Auth.php index ac77a2c..0aa51b5 100644 --- a/app/library/Auth.php +++ b/app/library/Auth.php @@ -28,7 +28,11 @@ class Auth extends Component // Verify password $hash = $user->getPassword(); if (strlen($hash) > 1 && password_verify($password, $hash)) { + $this->setIdentity($user->getId()); + + $this->_eventsManager->fire('auth:onLogin', $this, 'password'); + return true; } } @@ -68,6 +72,9 @@ class Auth extends Component } $this->setIdentity($user->getId()); + + $this->_eventsManager->fire('auth:onLogin', $this, + "OAuth {$auth['provider']}"); } /** diff --git a/app/listeners/ActivityLog.php b/app/listeners/ActivityLog.php new file mode 100644 index 0000000..2b62892 --- /dev/null +++ b/app/listeners/ActivityLog.php @@ -0,0 +1,68 @@ +_log($auth->getUser(), "Logged in ({$type})"); + } + + /** + * @param Event $event + * @param User $auth + */ + public function onPasswordCreated(Event $event, User $user) + { + $this->_log($user, "Created password"); + } + + /** + * @param Event $event + * @param User $auth + */ + public function onPasswordChanged(Event $event, User $user) + { + $this->_log($user, "Changed password"); + } + + protected function _log(User $user, $message) + { + $ip = (new \Phalcon\Http\Request())->getClientAddress(); + + $data = [ + 'user_id' => $user->getId(), + 'ip' => $ip, + 'message' => $message + ]; + + return $this->_getLogger()->create($data); + } + + protected function _getLogger() + { + if ($this->_table === null) { + $this->_table = new ActivityLogger(); + } + return $this->_table; + } +} diff --git a/app/migrations/20180401083402_create_activity_log.php b/app/migrations/20180401083402_create_activity_log.php new file mode 100644 index 0000000..6b49d51 --- /dev/null +++ b/app/migrations/20180401083402_create_activity_log.php @@ -0,0 +1,32 @@ +table('activity_log'); + + $table->addColumn('timestamp', 'datetime', [ + 'default' => 'CURRENT_TIMESTAMP', + 'after' => 'email' + ]); + + $table->addColumn('user_id', 'integer') + ->addForeignKey('user_id', 'user', ['id']); + + $table->addColumn('ip', 'string', [ + 'null' => true, + 'length' => 50, + ]); + + $table->addColumn('message', 'string', [ + 'null' => true, + 'length' => 255, + ]); + + $table->save(); + } +} diff --git a/app/models/Data/ActivityLog.php b/app/models/Data/ActivityLog.php new file mode 100644 index 0000000..91f3158 --- /dev/null +++ b/app/models/Data/ActivityLog.php @@ -0,0 +1,131 @@ +id; + } + + /** + * @param mixed $id + * @return User + */ + public function setId($id) + { + $this->id = $id; + return $this; + } + + /** + * @return mixed + */ + public function getTimestamp() + { + return $this->timestamp; + } + + /** + * @param mixed $timestamp + * @return ActivityLog + */ + public function setTimestamp($timestamp) + { + $this->timestamp = $timestamp; + return $this; + } + + /** + * @return mixed + */ + public function getUserId() + { + return $this->user_id; + } + + /** + * @param mixed $user_id + * @return ActivityLog + */ + public function setUserId($user_id) + { + $this->user_id = $user_id; + return $this; + } + + /** + * @return mixed + */ + public function getIp() + { + return $this->ip; + } + + /** + * @param mixed $ip + * @return ActivityLog + */ + public function setIp($ip) + { + $this->ip = $ip; + return $this; + } + + /** + * @return mixed + */ + public function getMessage() + { + return $this->message; + } + + /** + * @param mixed $message + * @return ActivityLog + */ + public function setMessage($message) + { + $this->message = $message; + return $this; + } + + /** + * @param $userid + * @param int $page + * @param int $limit + * @return \Phalcon\Paginator\AdapterInterface + */ + public static function getPaginationList($userid, $page = 1, $limit = 30) + { + $builder = (new self())->getModelsManager()->createBuilder(); + + $builder->from(self::class) + ->where('user_id = :uid:', array('uid' => $userid)) + ->orderBy('timestamp DESC'); + + $paginator = new QueryBuilder(array( + 'builder' => $builder, + 'page' => $page, + 'limit' => $limit + )); + + return $paginator; + } +} diff --git a/app/models/Data/User.php b/app/models/Data/User.php index 2f3252c..1f1bc92 100644 --- a/app/models/Data/User.php +++ b/app/models/Data/User.php @@ -30,6 +30,12 @@ class User extends Model public function initialize() { $this->useDynamicUpdate(true); + + // Keep snapshots so we know if something changes. + $this->keepSnapshots(true); + + // Set default event manager + $this->setEventsManager($this->getDI()->get('eventsManager')); } /** @@ -200,4 +206,22 @@ class User extends Model "bind" => [ 'v' => $value ] ]); } + + public function beforeSave() + { + $manager = $this->getEventsManager(); + + // EventManager exist and password field has changed. + if ($manager && $this->hasChanged('password')) { + + $old_value = $this->getOldSnapshotData()['password']; + + // Empty password before + if (strlen($old_value) < 1) { + $manager->fire('user:onPasswordCreated', $this); + } else { + $manager->fire('user:onPasswordChanged', $this); + } + } + } } diff --git a/app/views/_templates/navigation.volt b/app/views/_templates/navigation.volt index 0d0d072..ef7d12a 100644 --- a/app/views/_templates/navigation.volt +++ b/app/views/_templates/navigation.volt @@ -16,6 +16,7 @@ diff --git a/app/views/user/activity.volt b/app/views/user/activity.volt new file mode 100644 index 0000000..be1aa4a --- /dev/null +++ b/app/views/user/activity.volt @@ -0,0 +1,31 @@ + +
+ +

Activity Log

+ + + + + + + + + + + + {% for item in page.items %} + + + + + + {% endfor %} + +
DateIpMessage
{{ item.getTimestamp() }}{{ item.getIp() }}{{ item.getMessage() }}
+ + +
+ +