- fixed "online" status for characters, closed #507

This commit is contained in:
Exodus4D
2017-07-16 14:16:15 +02:00
parent f0cd1cdaf0
commit fc2e0ffe58
11 changed files with 182 additions and 109 deletions

View File

@@ -17,6 +17,7 @@ use lib\Config;
class Admin extends Controller{
const ERROR_SSO_CHARACTER_EXISTS = 'No character found. Please login first.';
const ERROR_SSO_CHARACTER_SCOPES = 'Additional ESI scopes are required for "%s". Use the SSO button below.';
const ERROR_SSO_CHARACTER_ROLES = 'Insufficient in-game roles. "%s" requires at least one of these corp roles: %s.';
const LOG_TEXT_KICK_BAN = '%s "%s" from corporation "%s", by "%s"';
@@ -86,13 +87,19 @@ class Admin extends Controller{
if($character->role != 'MEMBER'){
// current character is admin
$adminCharacter = $character;
}elseif( !$character->hasAdminScopes() ){
$f3->set(Sso::SESSION_KEY_SSO_ERROR,
sprintf(
self::ERROR_SSO_CHARACTER_SCOPES,
$character->name
));
}else{
$f3->set(Sso::SESSION_KEY_SSO_ERROR,
sprintf(
self::ERROR_SSO_CHARACTER_ROLES,
$character->name,
implode(', ', CorporationModel::ADMIN_ROLES
)));
implode(', ', CorporationModel::ADMIN_ROLES)
));
}
}else{
$f3->set(Sso::SESSION_KEY_SSO_ERROR, self::ERROR_SSO_CHARACTER_EXISTS);
@@ -107,9 +114,9 @@ class Admin extends Controller{
* @param CharacterModel $character
*/
protected function setCharacterRole(CharacterModel $character){
$character->virtual('role', function($character){
$character->virtual('role', function ($character){
// default role based on roleId (auto-detected)
if( ($role = array_search ($character->roleId, CharacterModel::ROLES)) === false ){
if(($role = array_search($character->roleId, CharacterModel::ROLES)) === false){
$role = 'MEMBER';
}
@@ -119,9 +126,9 @@ class Admin extends Controller{
*/
if($this->getF3()->exists('PATHFINDER.ADMIN.CHARACTER', $globalAdminData)){
foreach((array)$globalAdminData as $adminData){
if($adminData['ID'] === $character->_id){
if( CharacterModel::ROLES[$adminData['ROLE']] ){
$role = $adminData['ROLE'];
if($adminData[ 'ID' ] === $character->_id){
if(CharacterModel::ROLES[ $adminData[ 'ROLE' ] ]){
$role = $adminData[ 'ROLE' ];
}
break;
}

View File

@@ -55,7 +55,7 @@ class Sso extends Api\User{
public function requestAdminAuthorization($f3){
$f3->set(self::SESSION_KEY_SSO_FROM, 'admin');
$scopes = $this->getScopesByAuthType('admin');
$scopes = self::getScopesByAuthType('admin');
$this->rerouteAuthorization($f3, $scopes, 'admin');
}
@@ -105,7 +105,7 @@ class Sso extends Api\User{
if($loginCheck){
// set "login" cookie
$this->setLoginCookie($character, $this->generateHashFromScopes($this->getScopesByAuthType()) );
$this->setLoginCookie($character);
// -> pass current character data to target page
$f3->set(Api\User::SESSION_KEY_TEMP_CHARACTER_ID, $character->_id);
@@ -122,7 +122,7 @@ class Sso extends Api\User{
}
// redirect to CCP SSO ----------------------------------------------------------------------
$scopes = $this->getScopesByAuthType();
$scopes = self::getScopesByAuthType();
$this->rerouteAuthorization($f3, $scopes);
}
@@ -207,9 +207,10 @@ class Sso extends Api\User{
if( isset($characterData->character) ){
// add "ownerHash" and SSO tokens
$characterData->character['ownerHash'] = $verificationCharacterData->CharacterOwnerHash;
$characterData->character['crestAccessToken'] = $accessData->accessToken;
$characterData->character['crestRefreshToken'] = $accessData->refreshToken;
$characterData->character['ownerHash'] = $verificationCharacterData->CharacterOwnerHash;
$characterData->character['crestAccessToken'] = $accessData->accessToken;
$characterData->character['crestRefreshToken'] = $accessData->refreshToken;
$characterData->character['esiScopes'] = Lib\Util::convertScopesString($verificationCharacterData->Scopes);
// add/update static character data
$characterModel = $this->updateCharacter($characterData);
@@ -255,7 +256,7 @@ class Sso extends Api\User{
if($loginCheck){
// set "login" cookie
$this->setLoginCookie($characterModel, $this->generateHashFromScopes( explode(' ', $verificationCharacterData->Scopes) ));
$this->setLoginCookie($characterModel);
// -> pass current character data to target page
$f3->set(Api\User::SESSION_KEY_TEMP_CHARACTER_ID, $characterModel->_id);
@@ -569,7 +570,9 @@ class Sso extends Api\User{
*/
$characterModel = Model\BasicModel::getNew('CharacterModel');
$characterModel->getById((int)$characterData->character['id'], 0);
$characterModel->copyfrom($characterData->character, ['id', 'name', 'ownerHash', 'crestAccessToken', 'crestRefreshToken', 'securityStatus']);
$characterModel->copyfrom($characterData->character, [
'id', 'name', 'ownerHash', 'crestAccessToken', 'crestRefreshToken', 'esiScopes', 'securityStatus'
]);
$characterModel->corporationId = $characterData->corporation;
$characterModel->allianceId = $characterData->alliance;
$characterModel = $characterModel->save();

View File

@@ -10,6 +10,7 @@ namespace Controller;
use Controller\Api as Api;
use lib\Config;
use lib\Socket;
use Lib\Util;
use Model;
use DB;
@@ -187,9 +188,8 @@ class Controller {
* set/update logged in cookie by character model
* -> store validation data in DB
* @param Model\CharacterModel $character
* @param string $scopeHash
*/
protected function setLoginCookie(Model\CharacterModel $character, $scopeHash = ''){
protected function setLoginCookie(Model\CharacterModel $character){
if( $this->getCookieState() ){
$expireSeconds = (int) $this->getF3()->get('PATHFINDER.LOGIN.COOKIE_EXPIRE');
@@ -221,8 +221,7 @@ class Controller {
'characterId' => $character,
'selector' => $selector,
'token' => $token,
'expires' => $expireTime->format('Y-m-d H:i:s'),
'scopeHash' => $scopeHash
'expires' => $expireTime->format('Y-m-d H:i:s')
];
$authenticationModel = $character->rel('characterAuthentications');
@@ -276,10 +275,6 @@ class Controller {
// "expire data" and "validate token"
if( !$characterAuth->dry() ){
if(
(
$characterAuth->scopeHash === $this->generateHashFromScopes($this->getScopesByAuthType()) ||
$characterAuth->scopeHash === $this->generateHashFromScopes($this->getScopesByAuthType('admin'))
) &&
strtotime($characterAuth->expires) >= $currentTime->getTimestamp() &&
hash_equals($characterAuth->token, hash('sha256', $data[1]))
){
@@ -297,20 +292,32 @@ class Controller {
$character = $characterAuth->rel('characterId');
$character->getById( $characterAuth->get('characterId', true) );
// check if character still has user (is not the case of "ownerHash" changed
// check if character is still authorized to log in (e.g. corp/ally or config has changed
// -> do NOT remove cookie on failure. This can be a temporary problem (e.g. ESI is down,..)
if( $character->hasUserCharacter() ){
$authStatus = $character->isAuthorized();
// check ESI scopes
$scopeHash = Util::getHashFromScopes($character->esiScopes);
if(
$authStatus == 'OK' ||
!$checkAuthorization
){
$character->virtual( 'authStatus', $authStatus);
if(
$scopeHash === Util::getHashFromScopes(self::getScopesByAuthType()) ||
$scopeHash === Util::getHashFromScopes(self::getScopesByAuthType('admin'))
){
// check if character still has user (is not the case of "ownerHash" changed
// check if character is still authorized to log in (e.g. corp/ally or config has changed
// -> do NOT remove cookie on failure. This can be a temporary problem (e.g. ESI is down,..)
if( $character->hasUserCharacter() ){
$authStatus = $character->isAuthorized();
if(
$authStatus == 'OK' ||
!$checkAuthorization
){
$character->virtual( 'authStatus', $authStatus);
}
$characters[$name] = $character;
}
$characters[$name] = $character;
}else{
// outdated/invalid ESI scopes
$characterAuth->erase();
$invalidCookie = true;
}
}else{
$invalidCookie = true;
@@ -434,35 +441,6 @@ class Controller {
return $user;
}
/**
* get scope array by a "role"
* @param string $authType
* @return array
*/
protected function getScopesByAuthType($authType = ''){
$scopes = (array)self::getEnvironmentData('CCP_ESI_SCOPES');
switch($authType){
case 'admin':
$scopesAdmin = (array)self::getEnvironmentData('CCP_ESI_SCOPES_ADMIN');
$scopes = array_merge($scopes, $scopesAdmin);
break;
}
sort($scopes, SORT_NUMERIC);
return $scopes;
}
/**
* get hash from an array of ESI scopes
* @param array $scopes
* @return string
*/
protected function generateHashFromScopes($scopes){
$scopes = (array)$scopes;
sort($scopes);
return md5(serialize( $scopes ));
}
/**
* log out current character
* @param \Base $f3
@@ -715,6 +693,25 @@ class Controller {
return $controller;
}
/**
* get scope array by a "role"
* @param string $authType
* @return array
*/
static function getScopesByAuthType($authType = ''){
$scopes = (array)self::getEnvironmentData('CCP_ESI_SCOPES');
switch($authType){
case 'admin':
$scopesAdmin = (array)self::getEnvironmentData('CCP_ESI_SCOPES_ADMIN');
$scopes = array_merge($scopes, $scopesAdmin);
break;
}
sort($scopes);
return $scopes;
}
/**
* Helper function to return all headers because
* getallheaders() is not available under nginx

View File

@@ -8,7 +8,6 @@
namespace Lib;
class Util {
/**
@@ -38,4 +37,36 @@ class Util {
}, array_keys($arr)), $arr
);
}
/**
* convert a string with multiple scopes into an array
* @param string $scopes
* @return array|null
*/
static function convertScopesString($scopes){
$scopes = array_filter(
array_map('strtolower',
(array)explode(' ', $scopes)
)
);
if($scopes){
sort($scopes);
}else{
$scopes = null;
}
return $scopes;
}
/**
* get hash from an array of ESI scopes
* @param array $scopes
* @return string
*/
static function getHashFromScopes($scopes){
$scopes = (array)$scopes;
sort($scopes);
return md5(serialize($scopes));
}
}

View File

@@ -11,6 +11,7 @@ namespace Model;
use Controller\Ccp\Sso as Sso;
use Controller\Api\User as User;
use DB\SQL\Schema;
use Lib\Util;
class CharacterModel extends BasicModel {
@@ -90,6 +91,9 @@ class CharacterModel extends BasicModel {
'crestRefreshToken' => [
'type' => Schema::DT_VARCHAR256
],
'esiScopes' => [
'type' => self::DT_JSON
],
'corporationId' => [
'type' => Schema::DT_INT,
'index' => true,
@@ -611,12 +615,16 @@ class CharacterModel extends BasicModel {
*/
protected function requestRoles(){
$rolesData = [];
if( $accessToken = $this->getAccessToken() ){
// check if corporation exists (should never fail)
if( $corporation = $this->getCorporation() ){
$characterRolesData = $corporation->getCharactersRoles($accessToken);
if( !empty($characterRolesData[$this->_id]) ){
$rolesData = $characterRolesData[$this->_id];
// check if character has accepted all admin scopes (one of them is required for "role" request)
if( $this->hasAdminScopes() ){
if( $accessToken = $this->getAccessToken() ){
// check if corporation exists (should never fail)
if( $corporation = $this->getCorporation() ){
$characterRolesData = $corporation->getCharactersRoles($accessToken);
if( !empty($characterRolesData[$this->_id]) ){
$rolesData = $characterRolesData[$this->_id];
}
}
}
}
@@ -624,11 +632,19 @@ class CharacterModel extends BasicModel {
return $rolesData;
}
/**
* check whether this char has accepted all admin api scopes
* @return bool
*/
public function hasAdminScopes(){
return empty( array_diff(Sso::getScopesByAuthType('admin'), $this->esiScopes) );
}
/**
* update character log (active system, ...)
* -> API request for character log data
* @param array $additionalOptions (optional) request options for cURL request
* @return $this
* @return CharacterModel
*/
public function updateLog($additionalOptions = []){
$deleteLog = false;
@@ -806,8 +822,9 @@ class CharacterModel extends BasicModel {
$characterData = $ssoController->getCharacterData($this->_id);
if( !empty($characterData->character) ){
$characterData->character['ownerHash'] = $verificationCharacterData->CharacterOwnerHash;
$characterData->character['esiScopes'] = Util::convertScopesString($verificationCharacterData->Scopes);
$this->copyfrom($characterData->character, ['ownerHash', 'securityStatus']);
$this->copyfrom($characterData->character, ['ownerHash', 'esiScopes', 'securityStatus']);
$this->corporationId = $characterData->corporation;
$this->allianceId = $characterData->alliance;
$this->save();

View File

@@ -12,7 +12,7 @@ define([
'use strict';
var config = {
let config = {
title: '',
text: '',
type: '', // 'info', 'success', error, 'warning'
@@ -38,13 +38,13 @@ define([
};
// initial page title (cached)
var initialPageTitle = document.title;
let initialPageTitle = document.title;
// global blink timeout cache
var blinkTimer;
let blinkTimer;
// stack container for all notifications
var stack = {
let stack = {
bottomRight: {
stack: {
dir1: 'up',
@@ -76,7 +76,7 @@ define([
* @param customConfig
* @param settings
*/
var showNotify = function(customConfig, settings){
let showNotify = function(customConfig, settings){
customConfig = $.extend(true, {}, config, customConfig );
@@ -140,13 +140,13 @@ define([
* change document.title and make the browsers tab blink
* @param blinkTitle
*/
var startTabBlink = function(blinkTitle){
var initBlink = (function(blinkTitle){
let startTabBlink = function(blinkTitle){
let initBlink = (function(blinkTitle){
// count blinks if tab is currently active
var activeTabBlinkCount = 0;
let activeTabBlinkCount = 0;
var blink = function(){
let blink = function(){
// number of "blinks" should be limited if tab is currently active
if(window.isVisible){
activeTabBlinkCount++;
@@ -173,7 +173,7 @@ define([
/**
* stop blinking document.title
*/
var stopTabBlink = function(){
let stopTabBlink = function(){
if(blinkTimer){
clearInterval(blinkTimer);
document.title = initialPageTitle;

View File

@@ -1005,7 +1005,7 @@ define([
let initTabChangeObserver = function(){
// increase the timer if a user is inactive
let increaseTimer = 10000;
let increaseTimer = 5000;
// timer keys
let mapUpdateKey = 'UPDATE_SERVER_MAP';

View File

@@ -581,8 +581,9 @@ define([
e.preventDefault();
e.stopPropagation();
let easeEffect = $(this).attr('data-easein');
let popoverData = $(this).data('bs.popover');
let button = $(this);
let easeEffect = button.attr('data-easein');
let popoverData = button.data('bs.popover');
let popoverElement = null;
let velocityOptions = {
@@ -591,8 +592,16 @@ define([
if(popoverData === undefined){
button.on('shown.bs.popover', function (e) {
let tmpPopupElement = $(this).data('bs.popover').tip();
tmpPopupElement.find('.btn').on('click', function(e){
// close popover
$('body').click();
});
});
// init popover and add specific class to it (for styling)
$(this).popover({
button.popover({
html: true,
title: 'select character',
trigger: 'manual',
@@ -602,17 +611,17 @@ define([
animation: false
}).data('bs.popover').tip().addClass('pf-popover');
$(this).popover('show');
button.popover('show');
popoverElement = $(this).data('bs.popover').tip();
popoverElement = button.data('bs.popover').tip();
popoverElement.velocity('transition.' + easeEffect, velocityOptions);
popoverElement.initTooltips();
}else{
popoverElement = $(this).data('bs.popover').tip();
popoverElement = button.data('bs.popover').tip();
if(popoverElement.is(':visible')){
popoverElement.velocity('reverse');
}else{
$(this).popover('show');
button.popover('show');
popoverElement.initTooltips();
popoverElement.velocity('transition.' + easeEffect, velocityOptions);
}

View File

@@ -12,7 +12,7 @@ define([
'use strict';
var config = {
let config = {
title: '',
text: '',
type: '', // 'info', 'success', error, 'warning'
@@ -38,13 +38,13 @@ define([
};
// initial page title (cached)
var initialPageTitle = document.title;
let initialPageTitle = document.title;
// global blink timeout cache
var blinkTimer;
let blinkTimer;
// stack container for all notifications
var stack = {
let stack = {
bottomRight: {
stack: {
dir1: 'up',
@@ -76,7 +76,7 @@ define([
* @param customConfig
* @param settings
*/
var showNotify = function(customConfig, settings){
let showNotify = function(customConfig, settings){
customConfig = $.extend(true, {}, config, customConfig );
@@ -140,13 +140,13 @@ define([
* change document.title and make the browsers tab blink
* @param blinkTitle
*/
var startTabBlink = function(blinkTitle){
var initBlink = (function(blinkTitle){
let startTabBlink = function(blinkTitle){
let initBlink = (function(blinkTitle){
// count blinks if tab is currently active
var activeTabBlinkCount = 0;
let activeTabBlinkCount = 0;
var blink = function(){
let blink = function(){
// number of "blinks" should be limited if tab is currently active
if(window.isVisible){
activeTabBlinkCount++;
@@ -173,7 +173,7 @@ define([
/**
* stop blinking document.title
*/
var stopTabBlink = function(){
let stopTabBlink = function(){
if(blinkTimer){
clearInterval(blinkTimer);
document.title = initialPageTitle;

View File

@@ -1005,7 +1005,7 @@ define([
let initTabChangeObserver = function(){
// increase the timer if a user is inactive
let increaseTimer = 10000;
let increaseTimer = 5000;
// timer keys
let mapUpdateKey = 'UPDATE_SERVER_MAP';

View File

@@ -581,8 +581,9 @@ define([
e.preventDefault();
e.stopPropagation();
let easeEffect = $(this).attr('data-easein');
let popoverData = $(this).data('bs.popover');
let button = $(this);
let easeEffect = button.attr('data-easein');
let popoverData = button.data('bs.popover');
let popoverElement = null;
let velocityOptions = {
@@ -591,8 +592,16 @@ define([
if(popoverData === undefined){
button.on('shown.bs.popover', function (e) {
let tmpPopupElement = $(this).data('bs.popover').tip();
tmpPopupElement.find('.btn').on('click', function(e){
// close popover
$('body').click();
});
});
// init popover and add specific class to it (for styling)
$(this).popover({
button.popover({
html: true,
title: 'select character',
trigger: 'manual',
@@ -602,17 +611,17 @@ define([
animation: false
}).data('bs.popover').tip().addClass('pf-popover');
$(this).popover('show');
button.popover('show');
popoverElement = $(this).data('bs.popover').tip();
popoverElement = button.data('bs.popover').tip();
popoverElement.velocity('transition.' + easeEffect, velocityOptions);
popoverElement.initTooltips();
}else{
popoverElement = $(this).data('bs.popover').tip();
popoverElement = button.data('bs.popover').tip();
if(popoverElement.is(':visible')){
popoverElement.velocity('reverse');
}else{
$(this).popover('show');
button.popover('show');
popoverElement.initTooltips();
popoverElement.velocity('transition.' + easeEffect, velocityOptions);
}