Add session history

This commit is contained in:
maranqz
2019-11-14 20:55:18 +03:00
parent 8dea2d8078
commit be2b495c9e
26 changed files with 1639 additions and 4 deletions

8
src/User/Bootstrap.php Normal file → Executable file
View File

@ -16,7 +16,9 @@ use Da\User\Contracts\AuthManagerInterface;
use Da\User\Controller\SecurityController;
use Da\User\Event\FormEvent;
use Da\User\Helper\ClassMapHelper;
use Da\User\Model\SessionHistory;
use Da\User\Model\User;
use Da\User\Search\SessionHistorySearch;
use Yii;
use yii\authclient\Collection;
use yii\base\Application;
@ -128,7 +130,7 @@ class Bootstrap implements BootstrapInterface
$model = is_array($definition) ? $definition['class'] : $definition;
$name = substr($class, strrpos($class, '\\') + 1);
$modelClassMap[$class] = $model;
if (in_array($name, ['User', 'Profile', 'Token', 'SocialNetworkAccount'])) {
if (in_array($name, ['User', 'Profile', 'Token', 'SocialNetworkAccount', 'SessionHistory'])) {
$di->set(
"Da\\User\\Query\\{$name}Query",
function () use ($model) {
@ -315,10 +317,12 @@ class Bootstrap implements BootstrapInterface
'Assignment' => 'Da\User\Model\Assignment',
'Permission' => 'Da\User\Model\Permission',
'Role' => 'Da\User\Model\Role',
'SessionHistory' => SessionHistory::class,
// --- search
'UserSearch' => 'Da\User\Search\UserSearch',
'PermissionSearch' => 'Da\User\Search\PermissionSearch',
'RoleSearch' => 'Da\User\Search\RoleSearch',
'SessionHistorySearch' => SessionHistorySearch::class,
// --- forms
'RegistrationForm' => 'Da\User\Form\RegistrationForm',
'ResendForm' => 'Da\User\Form\ResendForm',
@ -338,11 +342,13 @@ class Bootstrap implements BootstrapInterface
'Assignment',
'Permission',
'Role',
'SessionHistory'
],
'Da\User\Search' => [
'UserSearch',
'PermissionSearch',
'RoleSearch',
'SessionHistorySearch',
],
'Da\User\Form' => [
'RegistrationForm',

34
src/User/Controller/AdminController.php Normal file → Executable file
View File

@ -17,9 +17,11 @@ use Da\User\Filter\AccessRuleFilter;
use Da\User\Model\Profile;
use Da\User\Model\User;
use Da\User\Query\UserQuery;
use Da\User\Search\SessionHistorySearch;
use Da\User\Search\UserSearch;
use Da\User\Service\PasswordExpireService;
use Da\User\Service\PasswordRecoveryService;
use Da\User\Service\SessionHistory\TerminateUserSessionsService;
use Da\User\Service\SwitchIdentityService;
use Da\User\Service\UserBlockService;
use Da\User\Service\UserConfirmationService;
@ -66,7 +68,7 @@ class AdminController extends Controller
*/
public function beforeAction($action)
{
if (in_array($action->id, ['index', 'update', 'update-profile', 'info', 'assignments'], true)) {
if (in_array($action->id, ['index', 'update', 'update-profile', 'info', 'assignments', 'session-history'], true)) {
Url::remember('', 'actions-redirect');
}
@ -88,6 +90,7 @@ class AdminController extends Controller
'switch-identity' => ['post'],
'password-reset' => ['post'],
'force-password-change' => ['post'],
'terminate-sessions' => ['post'],
],
],
'access' => [
@ -346,4 +349,33 @@ class AdminController extends Controller
}
$this->redirect(['index']);
}
/**
* Display list session history
*/
public function actionSessionHistory($id)
{
$searchModel = new SessionHistorySearch([
'user_id' => $id,
]);
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
$user = $this->userQuery->where(['id' => $id])->one();
return $this->render('_session-history', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
'user' => $user,
]);
}
/**
* Terminate all session user
*/
public function actionTerminateSessions($id)
{
$this->make(TerminateUserSessionsService::class, [$id])->run();
return $this->redirect(Url::previous('actions-redirect'));
}
}

35
src/User/Controller/SettingsController.php Normal file → Executable file
View File

@ -26,7 +26,9 @@ use Da\User\Module;
use Da\User\Query\ProfileQuery;
use Da\User\Query\SocialNetworkAccountQuery;
use Da\User\Query\UserQuery;
use Da\User\Search\SessionHistorySearch;
use Da\User\Service\EmailChangeService;
use Da\User\Service\SessionHistory\TerminateUserSessionsService;
use Da\User\Service\TwoFactorQrCodeUriGeneratorService;
use Da\User\Traits\ContainerAwareTrait;
use Da\User\Traits\ModuleAwareTrait;
@ -91,7 +93,8 @@ class SettingsController extends Controller
'actions' => [
'disconnect' => ['post'],
'delete' => ['post'],
'two-factor-disable' => ['post']
'two-factor-disable' => ['post'],
'terminate-sessions' => ['post'],
],
],
'access' => [
@ -111,7 +114,9 @@ class SettingsController extends Controller
'delete',
'two-factor',
'two-factor-enable',
'two-factor-disable'
'two-factor-disable',
'session-history',
'terminate-sessions',
],
'roles' => ['@'],
],
@ -463,6 +468,32 @@ class SettingsController extends Controller
$this->redirect(['account']);
}
/**
* Display list session history.
*/
public function actionSessionHistory()
{
$searchModel = new SessionHistorySearch([
'user_id' => Yii::$app->user->id,
]);
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
return $this->render('session-history', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}
/**
* Terminate all session user
*/
public function actionTerminateSessions()
{
$this->make(TerminateUserSessionsService::class, [Yii::$app->user->id])->run();
return $this->redirect(['session-history']);
}
/**
* @param $id
* @throws ForbiddenHttpException

37
src/User/Event/SessionEvent.php Executable file
View File

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the 2amigos/yii2-usuario project.
*
* (c) 2amigOS! <http://2amigos.us/>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Da\User\Event;
use Da\User\Model\User;
use yii\base\Event;
/**
* @property-read User $user
*/
class SessionEvent extends Event
{
const EVENT_BEFORE_TERMINATE_USER_SESSIONS = 'beforeTerminateUserSessions';
const EVENT_AFTER_TERMINATE_USER_SESSIONS = 'afterTerminateUserSessions';
protected $user;
public function __construct(User $user, array $config = [])
{
$this->user = $user;
parent::__construct($config);
}
public function getUser()
{
return $this->user;
}
}

View File

@ -0,0 +1,73 @@
<?php
/*
* This file is part of the 2amigos/yii2-usuario project.
*
* (c) 2amigOS! <http://2amigos.us/>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Da\User\Migration\Session;
use Da\User\Helper\MigrationHelper;
use yii\db\Migration;
class m000000_000001_create_session_history_table extends Migration
{
const SESSION_HISTORY_TABLE = '{{%session_history}}';
const USER_TABLE = '{{%user}}';
/**
* {@inheritdoc}
*/
public function safeUp()
{
$this->createTable(self::SESSION_HISTORY_TABLE, [
'user_id' => $this->integer(),
'session_id' => $this->string()->null(),
'user_agent' => $this->string()->notNull(),
'ip' => $this->string(45)->notNull(),
'created_at' => $this->integer()->notNull(),
'updated_at' => $this->integer()->notNull(),
]);
$this->createIndex(
'{{%session_history_user_id}}',
self::SESSION_HISTORY_TABLE,
['user_id']
);
$this->createIndex(
'{{%session_history_session_id}}',
self::SESSION_HISTORY_TABLE,
['session_id']
);
$this->createIndex(
'{{%session_history_updated_at}}',
self::SESSION_HISTORY_TABLE,
['updated_at']
);
$this->addForeignKey(
'{{%fk_user_session_history}}',
self::SESSION_HISTORY_TABLE,
'user_id',
self::USER_TABLE,
'id',
'CASCADE',
MigrationHelper::isMicrosoftSQLServer($this->db->driverName) ? 'NO ACTION' : 'RESTRICT'
);
}
/**
* {@inheritdoc}
*/
public function safeDown()
{
$this->dropTable(self::SESSION_HISTORY_TABLE);
}
}

110
src/User/Model/SessionHistory.php Executable file
View File

@ -0,0 +1,110 @@
<?php
/*
* This file is part of the 2amigos/yii2-usuario project.
*
* (c) 2amigOS! <http://2amigos.us/>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Da\User\Model;
use Da\User\Module;
use Da\User\Query\SessionHistoryQuery;
use Da\User\Traits\ModuleAwareTrait;
use Yii;
use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;
use yii\db\ActiveQuery;
/**
* @property int $user_id
* @property string $session_id
* @property string $user_agent
* @property string $ip
* @property int $created_at
* @property int $updated_at
*
* @property User $user
* @property bool $isActive
*
* Dependencies:
* @property-read Module $module
*/
class SessionHistory extends ActiveRecord
{
use ModuleAwareTrait;
/**
* {@inheritdoc}
*/
public static function tableName()
{
return '{{%session_history}}';
}
/** @inheritdoc */
public function behaviors()
{
return [
[
'class' => TimestampBehavior::class,
'updatedAtAttribute' => false,
]
];
}
/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'user_id' => Yii::t('usuario', 'User ID'),
'session_id' => Yii::t('usuario', 'Session ID'),
'user_agent' => Yii::t('usuario', 'User agent'),
'ip' => Yii::t('usuario', 'IP'),
'created_at' => Yii::t('usuario', 'Created at'),
'updated_at' => Yii::t('usuario', 'Last activity'),
];
}
/**
* @return bool Whether the session is an active or not.
*/
public function getIsActive()
{
return isset($this->session_id) && $this->updated_at + $this->getModule()->rememberLoginLifespan > time();
}
/**
* @return ActiveQuery
*/
public function getUser()
{
return $this->hasOne($this->module->classMap['User'], ['id' => 'user_id']);
}
/** @inheritdoc */
public function beforeSave($insert)
{
if ($insert && empty($this->session_id)) {
$this->setAttribute('session_id', Yii::$app->session->getId());
}
return parent::beforeSave($insert);
}
/** @inheritdoc */
public static function primaryKey()
{
return ['user_id', 'session_id'];
}
public static function find()
{
return new SessionHistoryQuery(static::class);
}
}

31
src/User/Module.php Normal file → Executable file
View File

@ -22,6 +22,21 @@ use yii\helpers\Html;
*/
class Module extends BaseModule
{
/**
* @var bool Enable the 'session history' function
* Using with {@see SessionHistoryDecorator}
*/
public $enableSessionHistory = false;
/**
* @var int|bool The number of 'session history' records will be stored for user
* if equals false records will not be deleted
*/
public $numberSessionHistory = false;
/**
* @var int|bool The time after which the expired 'session history' will be deleted
* if equals false records will not be deleted
*/
public $timeoutSessionHistory = false;
/**
* @var bool whether to enable european G.D.P.R. compliance.
* This will add a few elements to comply with european general data protection regulation.
@ -226,4 +241,20 @@ class Module extends BaseModule
return $this->gdprConsentMessage ?: $defaultConsentMessage;
}
/**
* @return bool
*/
public function hasNumberSessionHistory()
{
return $this->numberSessionHistory !== false && $this->numberSessionHistory > 0;
}
/**
* @return bool
*/
public function hasTimeoutSessionHistory()
{
return $this->timeoutSessionHistory !== false && $this->timeoutSessionHistory > 0;
}
}

View File

@ -0,0 +1,158 @@
<?php
/*
* This file is part of the 2amigos/yii2-usuario project.
*
* (c) 2amigOS! <http://2amigos.us/>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Da\User\Query;
use Da\User\Traits\ModuleAwareTrait;
use yii\web\Session;
use Yii;
class SessionHistoryCondition
{
use ModuleAwareTrait;
private $session;
public function __construct(Session $session)
{
$this->session = $session;
}
public function unbindSession()
{
return ['session_id' => null];
}
public function bySession($sessionId)
{
return ['session_id' => $sessionId];
}
public function byUser($userId)
{
return [
'user_id' => $userId,
];
}
public function byUserSession($userId, $sessionId)
{
return [
'user_id' => $userId,
'session_id' => $sessionId,
];
}
public function inactive($userId = null)
{
$where = [
'AND',
['session_id' => null]
];
if (isset($userId)) {
$where[] = $this->byUser($userId);
}
return $where;
}
public function expired($userId = null)
{
$where = [
'AND',
['<', 'updated_at', $this->getExpiredTime()]
];
if (isset($userId)) {
$where[] = $this->byUser($userId);
}
return $where;
}
public function expiredInactive($userId = null)
{
return [
'OR',
$this->expired($userId),
$this->inactive($userId),
];
}
public function shouldDeleteBefore($updatedAt, $userId)
{
$condition = ['<', 'updated_at', $updatedAt];
if ($updatedAt > $this->getExpiredTime()) {
$condition = [
'OR',
[
'AND',
$this->inactive(),
$condition,
],
$this->expired()
];
}
return [
'AND',
$this->byUser($userId),
$condition,
];
}
/**
* @return int
*/
public function getExpiredTime()
{
$module = $this->getModule();
$time = time() - max($module->rememberLoginLifespan, $this->session->getTimeout());
if (false === $module->hasTimeoutSessionHistory()) {
return $time;
}
return $time - $module->timeoutSessionHistory;
}
public function inactiveData()
{
return [
'session_id' => null,
];
}
/**
* @return array
*/
public function currentUserData()
{
return [
'user_id' => Yii::$app->user->id,
'session_id' => Yii::$app->session->getId(),
'user_agent' => Yii::$app->request->userAgent,
'ip' => Yii::$app->request->userIP,
];
}
/**
* @return array
*/
public function currentUserCondition()
{
return [
'user_id' => Yii::$app->user->id,
'session_id' => Yii::$app->session->getId(),
'user_agent' => Yii::$app->request->userAgent,
];
}
}

View File

@ -0,0 +1,82 @@
<?php
/*
* This file is part of the 2amigos/yii2-usuario project.
*
* (c) 2amigOS! <http://2amigos.us/>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Da\User\Query;
use Da\User\Traits\ModuleAwareTrait;
use yii\db\ActiveQuery;
use Yii;
class SessionHistoryQuery extends ActiveQuery
{
use ModuleAwareTrait;
public function whereUserId($userId)
{
return $this->andWhere($this->getCondition()->byUser($userId));
}
public function whereActive()
{
return $this->andWhere(['IS NOT', 'session_id', null]);
}
public function whereInActive($userId)
{
return $this->andWhere($this->getCondition()->inactive($userId));
}
public function whereExpired($userId)
{
return $this->andWhere($this->getCondition()->expired($userId));
}
public function whereExpiredInActive($userId)
{
return $this->andWhere($this->getCondition()->expiredInactive($userId));
}
public function selectSessionId()
{
return $this->select(['session_id']);
}
public function whereUserSession($userId, $sessionId)
{
return $this->andWhere($this->getCondition()->byUserSession(
$userId,
$sessionId
));
}
public function whereCurrentUser()
{
return $this->andWhere($this->getCondition()->currentUserCondition());
}
public function oldestUpdatedTimeActiveSession($userId)
{
return $this->whereExpiredInActive($userId)
->select(['updated_at'])
->limit(1)
->offset($this->getModule()->numberSessionHistory)
->orderBy(['updated_at' => SORT_DESC])->scalar();
}
/**
* @return SessionHistoryCondition
*/
protected function getCondition()
{
return Yii::$container->get(SessionHistoryCondition::class);
}
}

View File

@ -0,0 +1,74 @@
<?php
/*
* This file is part of the 2amigos/yii2-usuario project.
*
* (c) 2amigOS! <http://2amigos.us/>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Da\User\Search;
use Da\User\Model\SessionHistory;
use Da\User\Traits\ContainerAwareTrait;
use yii\base\InvalidConfigException;
use yii\base\InvalidParamException;
use yii\data\ActiveDataProvider;
class SessionHistorySearch extends SessionHistory
{
use ContainerAwareTrait;
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['user_agent', 'ip'], 'safe'],
];
}
/**
* @param array $params
*
* @throws InvalidConfigException
* @throws InvalidParamException
*
* @return ActiveDataProvider
*/
public function search($params)
{
$query = SessionHistory::find()->andWhere([
'user_id' => $this->user_id,
]);
/** @var ActiveDataProvider $dataProvider */
$dataProvider = $this->make(
ActiveDataProvider::class,
[],
[
'query' => $query,
'sort' => [
'defaultOrder' => [
'updated_at' => SORT_DESC
],
]
]
);
$this->load($params);
if (!$this->validate()) {
return $dataProvider;
}
$query->andFilterWhere(['like', 'user_agent', $this->user_agent])
->andFilterWhere(['like', 'ip', $this->ip]);
return $dataProvider;
}
}

View File

@ -0,0 +1,43 @@
<?php
/*
* This file is part of the 2amigos/yii2-usuario project.
*
* (c) 2amigOS! <http://2amigos.us/>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Da\User\Service\SessionHistory;
use yii\web\DbSession;
class DBTerminateSessionsService implements TerminateSessionsServiceInterface
{
protected $sessionIds;
protected $dbSession;
protected $fieldName;
public function __construct(array $sessionIds, DbSession $dbSession, $fieldName = 'id')
{
$this->sessionIds = $sessionIds;
$this->dbSession = $dbSession;
$this->fieldName = $fieldName;
}
public function run()
{
if (in_array(session_id(), $this->sessionIds)) {
session_write_close();
}
$this->dbSession->db->createCommand()->delete(
$this->dbSession->sessionTable,
[$this->fieldName => $this->sessionIds]
)->execute();
return true;
}
}

View File

@ -0,0 +1,461 @@
<?php
/*
* This file is part of the 2amigos/yii2-usuario project.
*
* (c) 2amigOS! <http://2amigos.us/>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Da\User\Service\SessionHistory;
use Da\User\Model\SessionHistory;
use Da\User\Query\SessionHistoryCondition;
use Da\User\Query\SessionHistoryQuery;
use Da\User\Traits\ModuleAwareTrait;
use Yii;
use yii\db\Exception;
use yii\web\Session;
use yii\base\InvalidArgumentException as BaseInvalidArgumentException;
/**
* Decorator for the {@see Session} class for storing the 'session history'
*
* Not decorated methods:
* {@see Session::open()}
* {@see Session::close()}
* {@see Session::destroy()}
* {@see Session::get()}
* {@see Session::set()}
*/
class SessionHistoryDecorator extends Session
{
use ModuleAwareTrait;
public $sessionHistoryTable = '{{%session_history}}';
/**
* @var Session
*/
public $session;
public $condition;
public function __construct(
Session $session,
SessionHistoryCondition $historyCondition,
$config = []
) {
$this->session = $session;
$this->condition = $historyCondition;
parent::__construct($config);
}
/** @inheritdoc */
public function getUseCustomStorage()
{
return $this->session->getUseCustomStorage();
}
/** @inheritdoc */
public function getIsActive()
{
return $this->session->getIsActive();
}
/** @inheritdoc */
public function getHasSessionId()
{
return $this->session->getHasSessionId();
}
/** @inheritdoc */
public function setHasSessionId($value)
{
return $this->session->setHasSessionId($value);
}
/** @inheritdoc */
public function getId()
{
return $this->session->getId();
}
/** @inheritdoc */
public function setId($value)
{
return $this->session->setId($value);
}
/** @inheritdoc */
public function regenerateID($deleteOldSession = false)
{
return $this->getDb()->transaction(function () use ($deleteOldSession) {
$oldSid = session_id();
if (false === $this->session->regenerateID($deleteOldSession)) {
return false;
}
if (false === $this->getModule()->enableSessionHistory) {
return true;
}
$user = Yii::$app->user;
if ($user->getIsGuest()) {
$this->unbindSessionHistory($oldSid);
} else {
$this->getDB()->createCommand()
->delete(
$this->sessionHistoryTable,
$this->condition->byUserSession($user->getId(), $oldSid)
)->execute();
}
return true;
});
}
/** @inheritdoc */
public function getName()
{
return $this->session->getName();
}
/** @inheritdoc */
public function setName($value)
{
return $this->session->setName($value);
}
/** @inheritdoc */
public function getSavePath()
{
return $this->session->getSavePath();
}
/** @inheritdoc */
public function setSavePath($value)
{
return $this->session->setSavePath($value);
}
/** @inheritdoc */
public function getCookieParams()
{
return $this->session->getCookieParams();
}
/** @inheritdoc */
public function setCookieParams(array $value)
{
return $this->session->setCookieParams($value);
}
/** @inheritdoc */
public function getUseCookies()
{
return $this->session->getUseCookies();
}
/** @inheritdoc */
public function setUseCookies($value)
{
return $this->session->setUseCookies($value);
}
/** @inheritdoc */
public function getGCProbability()
{
return $this->session->getGCProbability();
}
/** @inheritdoc */
public function setGCProbability($value)
{
return $this->session->setGCProbability($value);
}
/** @inheritdoc */
public function getUseTransparentSessionID()
{
return $this->session->getUseTransparentSessionID();
}
/** @inheritdoc */
public function setUseTransparentSessionID($value)
{
return $this->session->setUseTransparentSessionID($value);
}
/** @inheritdoc */
public function getTimeout()
{
return $this->session->getTimeout();
}
/** @inheritdoc */
public function setTimeout($value)
{
return $this->session->setTimeout($value);
}
/** @inheritdoc */
public function openSession($savePath, $sessionName)
{
return $this->session->openSession($savePath, $sessionName);
}
/** @inheritdoc */
public function closeSession()
{
return $this->session->closeSession();
}
/** @inheritdoc */
public function readSession($id)
{
return $this->session->readSession($id);
}
/** @inheritdoc */
public function writeSession($id, $data)
{
return $this->session->writeSession($id, $data) &&
(
false === $this->getModule()->enableSessionHistory ||
$this->getDb()->transaction(function () use ($id, $data) {
if (Yii::$app->user->getIsGuest()) {
return true;
}
$updatedAt = ['updated_at' => time()];
$model = $this->getHistoryQuery()
->whereCurrentUser()
->one();
if (isset($model)) {
$model->updateAttributes($updatedAt);
$result = true;
} else {
$model = Yii::createObject([
'class' => SessionHistory::class,
] + $this->condition->currentUserData() + $updatedAt);
if (!$result = $model->save()) {
throw new BaseInvalidArgumentException(
print_r($model->errors, 1)
);
}
$this->displacementHistory($model->user_id);
}
return $result;
})
);
}
/** @inheritdoc */
public function destroySession($id)
{
return $this->session->destroySession($id) &&
(
false === $this->getModule()->enableSessionHistory ||
$this->getDb()->transaction(function () use ($id) {
$this->unbindSessionHistory($id);
return true;
})
);
}
/** @inheritdoc */
public function gcSession($maxLifetime)
{
return $this->session->gcSession($maxLifetime) &&
(
false === $this->getModule()->enableSessionHistory ||
$this->getDb()->transaction(function () use ($maxLifetime) {
$this->getDb()->createCommand()->update(
$this->sessionHistoryTable,
$this->condition->inactiveData(),
$this->condition->expired()
)->execute();
return true;
})
);
}
/** @inheritdoc */
public function getIterator()
{
return $this->session->getIterator();
}
/** @inheritdoc */
public function getCount()
{
return $this->session->getCount();
}
/** @inheritdoc */
public function count()
{
return $this->session->count();
}
/** @inheritdoc */
public function remove($key)
{
return $this->session->remove($key);
}
/** @inheritdoc */
public function removeAll()
{
return $this->session->removeAll();
}
/** @inheritdoc */
public function has($key)
{
return $this->session->has($key);
}
/** @inheritdoc */
public function getFlash($key, $defaultValue = null, $delete = false)
{
return $this->session->getFlash($key, $defaultValue, $delete);
}
/** @inheritdoc */
public function getAllFlashes($delete = false)
{
return $this->session->getAllFlashes($delete);
}
/** @inheritdoc */
public function setFlash($key, $value = true, $removeAfterAccess = true)
{
return $this->session->setFlash($key, $value, $removeAfterAccess);
}
/** @inheritdoc */
public function addFlash($key, $value = true, $removeAfterAccess = true)
{
return $this->session->addFlash($key, $value, $removeAfterAccess);
}
/** @inheritdoc */
public function removeFlash($key)
{
return $this->session->removeFlash($key);
}
/** @inheritdoc */
public function removeAllFlashes()
{
return $this->session->removeAllFlashes();
}
/** @inheritdoc */
public function hasFlash($key)
{
return $this->session->hasFlash($key);
}
/** @inheritdoc */
public function offsetExists($offset)
{
return $this->session->offsetExists($offset);
}
/** @inheritdoc */
public function offsetGet($offset)
{
return $this->session->offsetGet($offset);
}
/** @inheritdoc */
public function offsetSet($offset, $item)
{
return $this->session->offsetSet($offset, $item);
}
/** @inheritdoc */
public function offsetUnset($offset)
{
return $this->session->offsetUnset($offset);
}
/** @inheritdoc */
public function setCacheLimiter($cacheLimiter)
{
return $this->session->setCacheLimiter($cacheLimiter);
}
/** @inheritdoc */
public function getCacheLimiter()
{
return $this->session->getCacheLimiter();
}
/**
* @param string $id
* @return bool
* @throws Exception
*/
protected function unbindSessionHistory($id)
{
return (bool)$this->getDb()->createCommand()->update(
$this->sessionHistoryTable,
$this->condition->unbindSession(),
$this->condition->bySession($id)
)->execute();
}
/**
*
* @param int $userId
* @return bool
* @throws Exception
*/
protected function displacementHistory($userId)
{
$module = $this->getModule();
if (false === $module->hasNumberSessionHistory()) {
return true;
}
$updatedAt = $this->getHistoryQuery()
->oldestUpdatedTimeActiveSession($userId);
if (!$updatedAt) {
return true;
}
$this->getDB()->createCommand()->delete(
$this->sessionHistoryTable,
$this->condition->shouldDeleteBefore(intval($updatedAt), $userId)
)->execute();
return true;
}
/**
* @return SessionHistoryQuery
*/
protected function getHistoryQuery()
{
return Yii::$container->get(SessionHistoryQuery::class);
}
protected function getDb()
{
return Yii::$app->getDb();
}
}

View File

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the 2amigos/yii2-usuario project.
*
* (c) 2amigOS! <http://2amigos.us/>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Da\User\Service\SessionHistory;
class TerminateSessionsService implements TerminateSessionsServiceInterface
{
protected $sessionIds;
public function __construct(array $sessionIds)
{
$this->sessionIds = $sessionIds;
}
public function run()
{
$currentSessionId = session_id();
if (session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
foreach ($this->sessionIds as $sessionId) {
if ($sessionId === $currentSessionId) {
$currentSessionId = null;
}
session_id($sessionId);
session_start();
session_destroy();
}
if ($currentSessionId) {
session_id($currentSessionId);
}
session_start();
return true;
}
}

View File

@ -0,0 +1,19 @@
<?php
/*
* This file is part of the 2amigos/yii2-usuario project.
*
* (c) 2amigOS! <http://2amigos.us/>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Da\User\Service\SessionHistory;
use Da\User\Contracts\ServiceInterface;
interface TerminateSessionsServiceInterface extends ServiceInterface
{
}

View File

@ -0,0 +1,105 @@
<?php
/*
* This file is part of the 2amigos/yii2-usuario project.
*
* (c) 2amigOS! <http://2amigos.us/>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Da\User\Service\SessionHistory;
use Da\User\Contracts\ServiceInterface;
use Da\User\Event\SessionEvent;
use Da\User\Model\SessionHistory;
use Da\User\Model\User;
use Da\User\Traits\ContainerAwareTrait;
use Da\User\Traits\ModuleAwareTrait;
use yii\web\Session;
use Yii;
class TerminateUserSessionsService implements ServiceInterface
{
use ContainerAwareTrait;
use ModuleAwareTrait;
protected $userId;
protected $session;
protected $excludeCurrentSession;
public function __construct($userId, Session $session, $excludeCurrentSession = true)
{
$this->userId = intval($userId);
$this->session = $session;
$this->excludeCurrentSession = $excludeCurrentSession;
}
public function run()
{
$user = $this->getUser($this->userId);
$sessionIds = $this->getSessionIds($user->id);
Yii::$app->db->transaction(function () use ($sessionIds, $user) {
/** @var SessionEvent $event */
$event = $this->make(SessionEvent::class, [$user]);
$user->trigger(SessionEvent::EVENT_BEFORE_TERMINATE_USER_SESSIONS, $event);
$this->make(TerminateSessionsServiceInterface::class, [$sessionIds])->run();
$user->updateAttributes([
'auth_key' => Yii::$app->security->generateRandomString(),
]);
if ($this->excludeCurrentUser()) {
Yii::$app->user->switchIdentity(
$user,
$this->getModule()->rememberLoginLifespan
);
}
$user->trigger(SessionEvent::EVENT_AFTER_TERMINATE_USER_SESSIONS, $event);
});
return true;
}
/**
* @param int $userId
* @return User
*/
protected function getUser($userId)
{
return ($this->make(User::class))::findOne($userId);
}
/**
* @param $userId
* @return int[]
*/
protected function getSessionIds($userId)
{
/** @var SessionHistory $sessionHistory */
$sessionHistory = $this->make(SessionHistory::class);
$sessionIds = $sessionHistory::find()->whereUserId($userId)->whereActive()->selectSessionId()->column();
if ($this->excludeCurrentUser()) {
foreach ($sessionIds as $key => $sessionId) {
if ($sessionId === $this->session->id) {
unset($sessionIds[$key]);
break;
}
}
}
return $sessionIds;
}
protected function excludeCurrentUser()
{
return $this->excludeCurrentSession && $this->userId === Yii::$app->user->id;
}
}

View File

@ -0,0 +1,85 @@
<?php
/*
* This file is part of the 2amigos/yii2-usuario project.
*
* (c) 2amigOS! <http://2amigos.us/>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Da\User\Widget;
use Da\User\Model\SessionHistory;
use Da\User\Traits\ContainerAwareTrait;
use Yii;
use yii\base\InvalidConfigException;
use yii\base\InvalidParamException;
use yii\base\Widget;
use yii\helpers\ArrayHelper;
class SessionStatusWidget extends Widget
{
use ContainerAwareTrait;
/**
* @var SessionHistory
*/
public $model;
/**
* {@inheritdoc}
*
* @throws InvalidConfigException
*/
public function init()
{
parent::init();
if (!$this->model instanceof SessionHistory) {
throw new InvalidConfigException(
__CLASS__ . '::$userId should be instanceof ' . SessionHistory::class
);
}
}
/**
* {@inheritdoc}
*
* @throws InvalidParamException
*/
public function run()
{
if ($this->model->getIsActive()) {
if ($this->model->session_id === Yii::$app->session->id) {
$value = Yii::t('usuario', 'Current');
} else {
$value = Yii::t('usuario', 'Active');
}
} else {
$value = Yii::t('usuario', 'Inactive');
}
return $value;
}
/**
* Returns available auth items to be attached to the user.
*
* @param int|null type of auth items or null to return all
*
* @return array
*/
protected function getAvailableItems($type = null)
{
return ArrayHelper::map(
$this->getAuthManager()->getItems($type),
'name',
function ($item) {
return empty($item->description)
? $item->name
: $item->name . ' (' . $item->description . ')';
}
);
}
}

8
src/User/resources/i18n/ru/usuario.php Normal file → Executable file
View File

@ -302,4 +302,12 @@ return [
'Unable to disable two-factor authorization.' => '@@Не удалось отключить двухфакторную авторизацию.@@',
'We couldn\'t re-send the mail to confirm your address. ' => '@@Мы не можем повторно отправить письмо для подтверждения вашего адреса электронной почты.@@',
'We have sent confirmation links to both old and new email addresses. ' => '@@Мы отправили письма на ваш старый и новый почтовые ящики. Вы должны перейти по обеим, чтобы завершить процесс смены адреса.@@',
'User agent' => '',
'Status' => 'Статус',
'Last activity' => 'Последняя активность',
'Session history' => 'История сессий',
'Active' => 'Активно',
'Inactive' => 'Не автивно',
'Current' => 'Текущий',
'Terminate all sessions' => 'Прекратить другие сеансы',
];

View File

@ -0,0 +1,68 @@
<?php
/*
* This file is part of the 2amigos/yii2-usuario project.
*
* (c) 2amigOS! <http://2amigos.us/>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
use Da\User\Widget\SessionStatusWidget;
use yii\helpers\Html;
use yii\grid\GridView;
use yii\widgets\Pjax;
use Da\User\Model\SessionHistory;
use Da\User\Search\SessionHistorySearch;
use yii\web\View;
use yii\data\ActiveDataProvider;
/**
* @var $this View
* @var $searchModel SessionHistorySearch
* @var $dataProvider ActiveDataProvider
*/
?>
<?php $this->beginContent('@Da/User/resources/views/admin/update.php', ['user' => $user]) ?>
<div class="row">
<div class="col-xs-12">
<?= Html::a(
Yii::t('usuario', 'Terminate all sessions'),
['/user/admin/terminate-sessions', 'id' => $user->id],
[
'class' => 'btn btn-danger btn-xs pull-right',
'data-method' => 'post'
]
) ?>
</div>
</div>
<hr>
<?php Pjax::begin(); ?>
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
'user_agent',
'ip',
[
'contentOptions' => [
'class' => 'text-nowrap',
],
'label' => Yii::t('usuario', 'Status'),
'value' => function (SessionHistory $model) {
return SessionStatusWidget::widget(['model' => $model]);
},
],
[
'attribute' => 'updated_at',
'format' => 'datetime'
],
],
]); ?>
<?php Pjax::end(); ?>
<?php $this->endContent() ?>

4
src/User/resources/views/admin/update.php Normal file → Executable file
View File

@ -67,6 +67,10 @@ $this->params['breadcrumbs'][] = $this->title;
'label' => Yii::t('usuario', 'Assignments'),
'url' => ['/user/admin/assignments', 'id' => $user->id],
],
[
'label' => Yii::t('usuario', 'Session history'),
'url' => ['/user/admin/session-history', 'id' => $user->id],
],
'<hr>',
[
'label' => Yii::t('usuario', 'Confirm'),

4
src/User/resources/views/settings/_menu.php Normal file → Executable file
View File

@ -41,6 +41,10 @@ $networksVisible = count(Yii::$app->authClientCollection->clients) > 0;
'items' => [
['label' => Yii::t('usuario', 'Profile'), 'url' => ['/user/settings/profile']],
['label' => Yii::t('usuario', 'Account'), 'url' => ['/user/settings/account']],
[
'label' => Yii::t('usuario', 'Session history'),
'url' => ['/user/settings/session-history']
],
['label' => Yii::t('usuario', 'Privacy'),
'url' => ['/user/settings/privacy'],
'visible' => $module->enableGdprCompliance

View File

@ -0,0 +1,79 @@
<?php
/*
* This file is part of the 2amigos/yii2-usuario project.
*
* (c) 2amigOS! <http://2amigos.us/>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
use yii\helpers\Html;
use yii\grid\GridView;
use yii\widgets\Pjax;
use Da\User\Model\SessionHistory;
use Da\User\Search\SessionHistorySearch;
use yii\web\View;
use yii\data\ActiveDataProvider;
use Da\User\Widget\SessionStatusWidget;
/**
* @var $this View
* @var $searchModel SessionHistorySearch
* @var $dataProvider ActiveDataProvider
*/
$this->title = Yii::t('usuario', 'Session history');
$this->params['breadcrumbs'][] = $this->title;
?>
<?= $this->render('/shared/_alert', ['module' => Yii::$app->getModule('user')]) ?>
<div class="row">
<div class="col-md-3">
<?= $this->render('/settings/_menu') ?>
</div>
<div class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading">
<?= Html::encode($this->title) ?>
<?= Html::a(
Yii::t('usuario', 'Terminate all sessions'),
['/user/settings/terminate-sessions'],
[
'class' => 'btn btn-danger btn-xs pull-right',
'data-method' => 'post'
]
) ?>
</div>
<div class="panel-body">
<?php Pjax::begin(); ?>
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
'user_agent',
'ip',
[
'contentOptions' => [
'class' => 'text-nowrap',
],
'label' => Yii::t('usuario', 'Status'),
'value' => function (SessionHistory $model) {
return SessionStatusWidget::widget(['model' => $model]);
},
],
[
'attribute' => 'updated_at',
'format' => 'datetime'
],
],
]); ?>
<?php Pjax::end(); ?>
</div>
</div>
</div>
</div>