diff --git a/app/cron.ini b/app/cron.ini index 8e411446..cbb18c5d 100644 --- a/app/cron.ini +++ b/app/cron.ini @@ -16,6 +16,9 @@ tenMinutes = */10 * * * * ; 2 times per hour (each 30min) halfHour = */30 * * * * +; 1 times per hour (12:30, 13:30, 14:30,...) +halfPastHour = 30 * * * * + ; run on EVE downtime 11:00 GMT/UTC downtime = 0 11 * * * @@ -36,7 +39,7 @@ deleteLogData = Cron\CharacterUpdate->deleteLogData, @in deleteSignatures = Cron\MapUpdate->deleteSignatures, @halfHour ; import system data (jump, kill,..) from CCP API -importSystemData = Cron\CcpSystemsUpdate->importSystemData, @hourly +importSystemData = Cron\CcpSystemsUpdate->importSystemData, @halfPastHour ; disable outdated maps deactivateMapData = Cron\MapUpdate->deactivateMapData, @hourly diff --git a/app/environment.ini b/app/environment.ini index 4f300efa..64c0e6ec 100644 --- a/app/environment.ini +++ b/app/environment.ini @@ -35,6 +35,7 @@ CCP_SSO_SECRET_KEY = CCP_ESI_URL = https://esi.tech.ccp.is CCP_ESI_DATASOURCE = singularity CCP_ESI_SCOPES = esi-location.read_location.v1,esi-location.read_ship_type.v1,esi-ui.write_waypoint.v1,esi-ui.open_window.v1 +CCP_ESI_SCOPES_ADMIN = esi-corporations.read_corporation_membership.v1 ; SMTP settings (optional) SMTP_HOST = localhost @@ -80,6 +81,7 @@ CCP_SSO_SECRET_KEY = CCP_ESI_URL = https://esi.tech.ccp.is CCP_ESI_DATASOURCE = tranquility CCP_ESI_SCOPES = esi-location.read_location.v1,esi-location.read_ship_type.v1,esi-ui.write_waypoint.v1,esi-ui.open_window.v1 +CCP_ESI_SCOPES_ADMIN = esi-corporations.read_corporation_membership.v1 ; SMTP settings (optional) SMTP_HOST = localhost diff --git a/app/main/controller/accesscontroller.php b/app/main/controller/accesscontroller.php index 9e74634c..e35cca71 100644 --- a/app/main/controller/accesscontroller.php +++ b/app/main/controller/accesscontroller.php @@ -17,13 +17,14 @@ class AccessController extends Controller { /** * event handler * @param \Base $f3 + * @param array $params */ - function beforeroute(\Base $f3) { - parent::beforeroute($f3); + function beforeroute(\Base $f3, $params) { + parent::beforeroute($f3, $params); // Any route/endpoint of a child class of this one, // requires a valid logged in user! - $loginCheck = $this->checkLogTimer($f3); + $loginCheck = $this->isLoggedIn($f3); if( !$loginCheck ){ // no user found or login timer expired @@ -34,7 +35,7 @@ class AccessController extends Controller { $f3->status(403); }else{ // redirect to landing page - $f3->reroute('@login'); + $f3->reroute(['login']); } // die() triggers unload() function @@ -42,6 +43,55 @@ class AccessController extends Controller { } } + /** + * get current character and check if it is a valid character + * @param \Base $f3 + * @return bool + */ + protected function isLoggedIn(\Base $f3){ + $loginCheck = false; + if( $character = $this->getCharacter() ){ + if($this->checkLogTimer($f3, $character)){ + if($character->isAuthorized() === 'OK'){ + $loginCheck = true; + } + } + } + + return $loginCheck; + } + + /** + * checks whether a user/character is currently logged in + * @param \Base $f3 + * @param Model\CharacterModel $character + * @return bool + */ + private function checkLogTimer(\Base $f3, Model\CharacterModel $character){ + $loginCheck = false; + + if( + !$character->dry() && + $character->lastLogin + ){ + // check logIn time + $logInTime = new \DateTime($character->lastLogin); + $now = new \DateTime(); + + $timeDiff = $now->diff($logInTime); + + $minutes = $timeDiff->days * 60 * 24 * 60; + $minutes += $timeDiff->h * 60; + $minutes += $timeDiff->i; + + if($minutes <= $f3->get('PATHFINDER.TIMER.LOGGED')){ + $loginCheck = true; + } + } + + return $loginCheck; + } + /** * broadcast map data to clients * -> send over TCP Socket diff --git a/app/main/controller/admin.php b/app/main/controller/admin.php new file mode 100644 index 00000000..a38e345c --- /dev/null +++ b/app/main/controller/admin.php @@ -0,0 +1,246 @@ + '5m', + 60 => '1h', + 1440 => '24h' + ]; + + /** + * event handler for all "views" + * some global template variables are set in here + * @param \Base $f3 + */ + function beforeroute(\Base $f3, $params) { + parent::beforeroute($f3, $params); + + $f3->set('tplPage', 'login'); + + if($character = $this->getAdminCharacter($f3)){ + $f3->set('tplLogged', true); + $f3->set('character', $character); + $this->dispatch($f3, $params, $character); + } + + $f3->set('tplAuthType', $f3->alias( 'sso', ['action' => 'requestAdminAuthorization'])); + + // page title + $f3->set('pageTitle', 'Admin'); + + // main page content + $f3->set('pageContent', $f3->get('PATHFINDER.VIEW.ADMIN')); + + // body element class + $f3->set('bodyClass', 'pf-body pf-landing'); + + // js path (build/minified or raw uncompressed files) + $f3->set('pathJs', 'public/js/' . $f3->get('PATHFINDER.VERSION') ); + } + + /** + * event handler after routing + * @param \Base $f3 + */ + public function afterroute(\Base $f3) { + // js view (file) + $f3->set('jsView', 'login'); + + // render view + echo \Template::instance()->render( $f3->get('PATHFINDER.VIEW.INDEX') ); + + // clear all SSO related temp data + if( $f3->exists(Sso::SESSION_KEY_SSO) ){ + $f3->clear('SESSION.SSO.ERROR'); + } + } + + /** + * returns valid admin $characterModel for current user + * @param \Base $f3 + * @return CharacterModel|null + */ + protected function getAdminCharacter(\Base $f3){ + $adminCharacter = null; + if( !$f3->exists(Sso::SESSION_KEY_SSO_ERROR) ){ + if( $character = $this->getCharacter() ){ + if($character->roleId == 1){ + // current character is admin + $adminCharacter = $character; + }else{ + $f3->set(Sso::SESSION_KEY_SSO_ERROR, + sprintf( + self::ERROR_SSO_CHARACTER_ROLES, + $character->name, + implode(', ', CorporationModel::ADMIN_ROLES + ))); + } + }else{ + $f3->set(Sso::SESSION_KEY_SSO_ERROR, self::ERROR_SSO_CHARACTER_EXISTS); + } + } + + return $adminCharacter; + } + + /** + * dispatch page events by URL $params + * @param \Base $f3 + * @param array $params + * @param null $character + */ + public function dispatch(\Base $f3, $params, $character = null){ + if($character instanceof CharacterModel){ + // user logged in + + $parts = array_values(array_filter(array_map('strtolower', explode('/', $params['*'])))); + $f3->set('tplPage', $parts[0]); + + switch($parts[0]){ + case 'settings': + break; + case 'members': + switch($parts[1]){ + case 'kick': + $objectId = (int)$parts[2]; + $value = (int)$parts[3]; + $this->kickCharacter($character, $objectId, $value); + + $f3->reroute('@admin(@*=/' . $parts[0] . ')'); + break; + case 'ban': + $objectId = (int)$parts[2]; + $value = (int)$parts[3]; + $this->banCharacter($character, $objectId, $value); + break; + } + $f3->set('tplPage', 'members'); + $f3->set('tplKickOptions', self::KICK_OPTIONS); + + $this->initMembers($f3, $character); + break; + case 'login': + default: + $f3->set('tplPage', 'login'); + break; + } + } + } + + /** + * kick or revoke a character + * @param CharacterModel $character + * @param int $kickCharacterId + * @param int $minutes + */ + protected function kickCharacter(CharacterModel $character, $kickCharacterId, $minutes){ + $kickOptions = self::KICK_OPTIONS; + $minKickTime = key($kickOptions) ; + end($kickOptions); + $maxKickTime = key($kickOptions); + $minutes = in_array($minutes, range($minKickTime, $maxKickTime)) ? $minutes : 0; + + $kickCharacters = $this->filterValidCharacters($character, $kickCharacterId); + foreach($kickCharacters as $kickCharacter){ + $kickCharacter->kick($minutes); + $kickCharacter->save(); + + self::getLogger()->write( + sprintf( + self::LOG_TEXT_KICK_BAN, + $minutes ? 'KICK' : 'KICK REVOKE', + $kickCharacter->name, + $kickCharacter->getCorporation()->name, + $character->name + ) + ); + } + } + + /** + * @param CharacterModel $character + * @param int $banCharacterId + * @param int $value + */ + protected function banCharacter(CharacterModel $character, $banCharacterId, $value){ + $banCharacters = $this->filterValidCharacters($character, $banCharacterId); + + foreach($banCharacters as $banCharacter){ + $banCharacter->ban($value); + $banCharacter->save(); + + self::getLogger()->write( + sprintf( + self::LOG_TEXT_KICK_BAN, + $value ? 'BAN' : 'BAN REVOKE', + $banCharacter->name, + $banCharacter->getCorporation()->name, + $character->name + ) + ); + } + } + + /** + * checks whether a $character has admin access rights for $charcterId + * -> must be in same corporation + * @param CharacterModel $character + * @param int $characterId + * @return array|CharacterModel[] + */ + protected function filterValidCharacters(CharacterModel $character, $characterId){ + $characters = []; + // check if kickCharacters belong to same Corp as admin character + // -> remove admin char from kickCharacters... + if( !empty($characterIds = array_diff( [$characterId], [$character->_id])) ){ + $characters = $character->getCorporation()->getCharacters($characterIds); + } + return $characters; + } + + /** + * get log file for "admin" logs + * @param string $type + * @return \Log + */ + static function getLogger($type = 'ADMIN'){ + return parent::getLogger('ADMIN'); + } + + /** + * init /member page data + * @param \Base $f3 + * @param CharacterModel $character + */ + protected function initMembers(\Base $f3, CharacterModel $character){ + $data = (object) [];; + + $test = BasicModel::getNew('CharacterModel'); + $test->getById( $character->_id, 0); + + $data->members = $test->getCorporation()->getCharacters(); + + $f3->set('tplMembers', $data); + } + +} \ No newline at end of file diff --git a/app/main/controller/api/access.php b/app/main/controller/api/access.php index 02e45f1a..c323a54b 100644 --- a/app/main/controller/api/access.php +++ b/app/main/controller/api/access.php @@ -15,11 +15,12 @@ class Access extends Controller\AccessController { /** * event handler * @param \Base $f3 + * @param array $params */ - function beforeroute(\Base $f3) { + function beforeroute(\Base $f3, $params) { // set header for all routes header('Content-type: application/json'); - parent::beforeroute($f3); + parent::beforeroute($f3, $params); } /** diff --git a/app/main/controller/api/connection.php b/app/main/controller/api/connection.php index 2ef66ba5..89717d28 100644 --- a/app/main/controller/api/connection.php +++ b/app/main/controller/api/connection.php @@ -14,11 +14,12 @@ class Connection extends Controller\AccessController { /** * @param \Base $f3 + * @param array $params */ - function beforeroute(\Base $f3) { + function beforeroute(\Base $f3, $params) { // set header for all routes header('Content-type: application/json'); - parent::beforeroute($f3); + parent::beforeroute($f3, $params); } /** diff --git a/app/main/controller/api/map.php b/app/main/controller/api/map.php index 7c9de808..37362da1 100644 --- a/app/main/controller/api/map.php +++ b/app/main/controller/api/map.php @@ -27,11 +27,12 @@ class Map extends Controller\AccessController { /** * event handler * @param \Base $f3 + * @param array $params */ - function beforeroute(\Base $f3) { + function beforeroute(\Base $f3, $params) { // set header for all routes header('Content-type: application/json'); - parent::beforeroute($f3); + parent::beforeroute($f3, $params); } /** diff --git a/app/main/controller/api/signature.php b/app/main/controller/api/signature.php index fe6e8545..6a3b0e55 100644 --- a/app/main/controller/api/signature.php +++ b/app/main/controller/api/signature.php @@ -16,11 +16,12 @@ class Signature extends Controller\AccessController { /** * event handler * @param \Base $f3 + * @param array $params */ - function beforeroute(\Base $f3) { + function beforeroute(\Base $f3, $params) { // set header for all routes header('Content-type: application/json'); - parent::beforeroute($f3); + parent::beforeroute($f3, $params); } /** diff --git a/app/main/controller/api/system.php b/app/main/controller/api/system.php index 725554d0..3c4f0a05 100644 --- a/app/main/controller/api/system.php +++ b/app/main/controller/api/system.php @@ -67,9 +67,10 @@ class System extends Controller\AccessController { /** * @param \Base $f3 + * @param array $params */ - function beforeroute(\Base $f3) { - parent::beforeroute($f3); + function beforeroute(\Base $f3, $params) { + parent::beforeroute($f3, $params); // set header for all routes header('Content-type: application/json'); diff --git a/app/main/controller/api/user.php b/app/main/controller/api/user.php index aaba4f51..72b999c0 100644 --- a/app/main/controller/api/user.php +++ b/app/main/controller/api/user.php @@ -80,6 +80,7 @@ class User extends Controller\Controller{ $this->f3->set(self::SESSION_KEY_CHARACTERS, $sessionCharacters); // save user login information -------------------------------------------------------- + $characterModel->roleId = $characterModel->requestRoleId(); $characterModel->touch('lastLogin'); $characterModel->save(); @@ -113,7 +114,8 @@ class User extends Controller\Controller{ if( !empty($data['cookie']) ){ if( !empty($cookieData = $this->getCookieByName($data['cookie']) )){ // cookie data is valid -> validate data against DB (security check!) - if( !empty($characters = $this->getCookieCharacters(array_slice($cookieData, 0, 1, true))) ){ + // -> add characters WITHOUT permission to log in too! + if( !empty($characters = $this->getCookieCharacters(array_slice($cookieData, 0, 1, true), false)) ){ // character is valid and allowed to login $return->character = reset($characters)->getData(); }else{ diff --git a/app/main/controller/appcontroller.php b/app/main/controller/appcontroller.php index 1ad3ae1e..c354a9a5 100644 --- a/app/main/controller/appcontroller.php +++ b/app/main/controller/appcontroller.php @@ -42,6 +42,9 @@ class AppController extends Controller { // JS main file $f3->set('jsView', 'login'); + // href for SSO Auth + $f3->set('tplAuthType', $f3->alias( 'sso', ['action' => 'requestAuthorization'] )); + // characters from cookies $f3->set('cookieCharacters', $this->getCookieByName(self::COOKIE_PREFIX_CHARACTER, true)); $f3->set('getCharacterGrid', function($characters){ diff --git a/app/main/controller/ccp/sso.php b/app/main/controller/ccp/sso.php index 9e2e7229..d8d70ff8 100644 --- a/app/main/controller/ccp/sso.php +++ b/app/main/controller/ccp/sso.php @@ -33,7 +33,7 @@ class Sso extends Api\User{ const SESSION_KEY_SSO = 'SESSION.SSO'; const SESSION_KEY_SSO_ERROR = 'SESSION.SSO.ERROR'; const SESSION_KEY_SSO_STATE = 'SESSION.SSO.STATE'; - const SESSION_KEY_SSO_FROM_MAP = 'SESSION.SSO.FROM_MAP'; + const SESSION_KEY_SSO_FROM = 'SESSION.SSO.FROM'; // error messages const ERROR_CCP_SSO_URL = 'Invalid "ENVIRONMENT.[ENVIRONMENT].CCP_SSO_URL" url. %s'; @@ -42,76 +42,98 @@ class Sso extends Api\User{ const ERROR_VERIFY_CHARACTER = 'Unable to verify character data. %s'; const ERROR_LOGIN_FAILED = 'Failed authentication due to technical problems: %s'; const ERROR_CHARACTER_VERIFICATION = 'Character verification failed by SSP SSO'; - const ERROR_CHARACTER_FORBIDDEN = 'Character "%s" is not authorized to log in'; + const ERROR_CHARACTER_FORBIDDEN = 'Character "%s" is not authorized to log in. Reason: %s'; const ERROR_SERVICE_TIMEOUT = 'CCP SSO service timeout (%ss). Try again later'; const ERROR_COOKIE_LOGIN = 'Login from Cookie failed. Please retry by CCP SSO'; + + /** + * redirect user to CCP SSO page and request authorization + * -> cf. Controller->getCookieCharacters() ( equivalent cookie based login) + * @param \Base $f3 + */ + public function requestAdminAuthorization($f3){ + $f3->set(self::SESSION_KEY_SSO_FROM, 'admin'); + + $scopes = $this->getScopesByAuthType('admin'); + $this->rerouteAuthorization($f3, $scopes, 'admin'); + } + /** * redirect user to CCP SSO page and request authorization * -> cf. Controller->getCookieCharacters() ( equivalent cookie based login) * @param \Base $f3 */ public function requestAuthorization($f3){ + $params = $f3->get('GET'); - if( !empty($ssoCcpClientId = Controller\Controller::getEnvironmentData('CCP_SSO_CLIENT_ID')) ){ - $params = $f3->get('GET'); + if( + isset($params['characterId']) && + ( $activeCharacter = $this->getCharacter(0) ) + ){ + // authentication restricted to a characterId ----------------------------------------------- + // restrict login to this characterId e.g. for character switch on map page + $characterId = (int)trim($params['characterId']); + /** + * @var Model\CharacterModel $character + */ + $character = Model\BasicModel::getNew('CharacterModel'); + $character->getById($characterId, 0); + + // check if character is valid and exists if( - isset($params['characterId']) && - ( $activeCharacter = $this->getCharacter(0) ) + !$character->dry() && + $character->hasUserCharacter() && + ($activeCharacter->getUser()->_id === $character->getUser()->_id) ){ - // authentication restricted to a characterId ----------------------------------------------- - // restrict login to this characterId e.g. for character switch on map page - $characterId = (int)trim($params['characterId']); + // requested character belongs to current user + // -> update character vom ESI (e.g. corp changed,..) + $updateStatus = $character->updateFromESI(); - /** - * @var Model\CharacterModel $character - */ - $character = Model\BasicModel::getNew('CharacterModel'); - $character->getById($characterId, 0); + if( empty($updateStatus) ){ - // check if character is valid and exists - if( - !$character->dry() && - $character->hasUserCharacter() && - ($activeCharacter->getUser()->_id === $character->getUser()->_id) - ){ - // requested character belongs to current user - // -> update character vom ESI (e.g. corp changed,..) - $updateStatus = $character->updateFromESI(); + // make sure character data is up2date! + // -> this is not the case if e.g. userCharacters was removed "ownerHash" changed... + $character->getById($character->_id); - if( empty($updateStatus) ){ + if( + $character->hasUserCharacter() && + ($character->isAuthorized() === 'OK') + ){ + $loginCheck = $this->loginByCharacter($character); - // make sure character data is up2date! - // -> this is not the case if e.g. userCharacters was removed "ownerHash" changed... - $character->getById($character->_id); + if($loginCheck){ + // set "login" cookie + $this->setLoginCookie($character, $this->generateHashFromScopes($this->getScopesByAuthType()) ); - if( - $character->hasUserCharacter() && - $character->isAuthorized() - ){ - $loginCheck = $this->loginByCharacter($character); + // -> pass current character data to target page + $f3->set(Api\User::SESSION_KEY_TEMP_CHARACTER_ID, $character->_id); - if($loginCheck){ - // set "login" cookie - $this->setLoginCookie($character, $this->getRequestedScopeHash()); - - // -> pass current character data to target page - $f3->set(Api\User::SESSION_KEY_TEMP_CHARACTER_ID, $character->_id); - - // route to "map" - $f3->reroute('@map'); - } + // route to "map" + $f3->reroute(['map']); } } } - - // redirect to map map page on successful login - $f3->set(self::SESSION_KEY_SSO_FROM_MAP, true); } - // redirect to CCP SSO ---------------------------------------------------------------------- + // redirect to map map page on successful login + $f3->set(self::SESSION_KEY_SSO_FROM, 'map'); + } + // redirect to CCP SSO ---------------------------------------------------------------------- + $scopes = $this->getScopesByAuthType(); + $this->rerouteAuthorization($f3, $scopes); + } + + /** + * redirect user to CCPs SSO page + * @param \Base $f3 + * @param array $scopes + * @param string $rootAlias + */ + private function rerouteAuthorization(\Base $f3, $scopes = [], $rootAlias = 'login'){ + if( !empty( Controller\Controller::getEnvironmentData('CCP_SSO_CLIENT_ID') ) ){ // used for "state" check between request and callback $state = bin2hex( openssl_random_pseudo_bytes(12) ); $f3->set(self::SESSION_KEY_SSO_STATE, $state); @@ -120,7 +142,7 @@ class Sso extends Api\User{ 'response_type' => 'code', 'redirect_uri' => Controller\Controller::getEnvironmentData('URL') . Controller\Controller::getEnvironmentData('BASE') . $f3->build('/sso/callbackAuthorization'), 'client_id' => Controller\Controller::getEnvironmentData('CCP_SSO_CLIENT_ID'), - 'scope' => implode(' ', Controller\Controller::getEnvironmentData('CCP_ESI_SCOPES')), + 'scope' => implode(' ', $scopes), 'state' => $state ]; @@ -128,12 +150,11 @@ class Sso extends Api\User{ $f3->status(302); $f3->reroute($ssoAuthUrl); - }else{ // SSO clientId missing $f3->set(self::SESSION_KEY_SSO_ERROR, self::ERROR_CCP_CLIENT_ID); self::getSSOLogger()->write(self::ERROR_CCP_CLIENT_ID); - $f3->reroute('@login'); + $f3->reroute([$rootAlias, ['*' => '']]); } } @@ -146,8 +167,12 @@ class Sso extends Api\User{ $getParams = (array)$f3->get('GET'); // users can log in either from @login (new user) or @map (existing user) root alias + // -> or from /admin page // -> in case login fails, users should be redirected differently - $authFromMapAlias = false; + $rootAlias = 'login'; + if( !empty($f3->get(self::SESSION_KEY_SSO_FROM)) ){ + $rootAlias = $f3->get(self::SESSION_KEY_SSO_FROM); + } if($f3->exists(self::SESSION_KEY_SSO_STATE)){ // check response and validate 'state' @@ -158,14 +183,9 @@ class Sso extends Api\User{ !empty($getParams['state']) && $f3->get(self::SESSION_KEY_SSO_STATE) === $getParams['state'] ){ - // check if user came from map (for redirect) - if( $f3->get(self::SESSION_KEY_SSO_FROM_MAP) ){ - $authFromMapAlias = true; - } - // clear 'state' for new next login request $f3->clear(self::SESSION_KEY_SSO_STATE); - $f3->clear(self::SESSION_KEY_SSO_FROM_MAP); + $f3->clear(self::SESSION_KEY_SSO_FROM); $accessData = $this->getSsoAccessData($getParams['code']); @@ -196,8 +216,7 @@ class Sso extends Api\User{ if( !is_null($characterModel) ){ // check if character is authorized to log in - if($characterModel->isAuthorized()){ - + if( ($authStatus = $characterModel->isAuthorized()) === 'OK'){ // character is authorized to log in // -> update character log (current location,...) $characterModel = $characterModel->updateLog(); @@ -236,19 +255,25 @@ class Sso extends Api\User{ if($loginCheck){ // set "login" cookie - $this->setLoginCookie($characterModel, $this->getRequestedScopeHash()); + $this->setLoginCookie($characterModel, $this->generateHashFromScopes( explode(' ', $verificationCharacterData->Scopes) )); // -> pass current character data to target page $f3->set(Api\User::SESSION_KEY_TEMP_CHARACTER_ID, $characterModel->_id); // route to "map" - $f3->reroute('@map'); + if($rootAlias == 'admin'){ + $f3->reroute([$rootAlias, ['*' => '']]); + }else{ + $f3->reroute(['map']); + } }else{ $f3->set(self::SESSION_KEY_SSO_ERROR, sprintf(self::ERROR_LOGIN_FAILED, $characterModel->name)); } }else{ // character is not authorized to log in - $f3->set(self::SESSION_KEY_SSO_ERROR, sprintf(self::ERROR_CHARACTER_FORBIDDEN, $characterModel->name)); + $f3->set(self::SESSION_KEY_SSO_ERROR, + sprintf(self::ERROR_CHARACTER_FORBIDDEN, $characterModel->name, Model\CharacterModel::AUTHORIZATION_STATUS[$authStatus]) + ); } } } @@ -266,13 +291,7 @@ class Sso extends Api\User{ } } - if($authFromMapAlias){ - // on error -> route back to map - $f3->reroute('@map'); - }else{ - // on error -> route back to login form - $f3->reroute('@login'); - } + $f3->reroute([$rootAlias, ['*' => '']]); } /** @@ -303,13 +322,13 @@ class Sso extends Api\User{ $f3->set(Api\User::SESSION_KEY_TEMP_CHARACTER_ID, $character->_id); // route to "map" - $f3->reroute('@map'); + $f3->reroute(['map']); } } // on error -> route back to login form $f3->set(self::SESSION_KEY_SSO_ERROR, self::ERROR_COOKIE_LOGIN); - $f3->reroute('@login'); + $f3->reroute(['login']); } /** diff --git a/app/main/controller/controller.php b/app/main/controller/controller.php index 52dd3783..95e02293 100644 --- a/app/main/controller/controller.php +++ b/app/main/controller/controller.php @@ -70,8 +70,9 @@ class Controller { * event handler for all "views" * some global template variables are set in here * @param \Base $f3 + * @param array $params */ - function beforeroute(\Base $f3) { + function beforeroute(\Base $f3, $params) { $this->setF3($f3); // initiate DB connection @@ -242,11 +243,12 @@ class Controller { * -> validate cookie data * -> validate characters * -> cf. Sso->requestAuthorization() ( equivalent DB based login) + * * @param array $cookieData - * @return array - * @throws \Exception + * @param bool $checkAuthorization + * @return Model\CharacterModel[] */ - protected function getCookieCharacters($cookieData = []){ + protected function getCookieCharacters($cookieData = [], $checkAuthorization = true){ $characters = []; if( @@ -268,12 +270,17 @@ class Controller { $data = explode(':', $value); if(count($data) === 2){ // cookie data is well formatted - $characterAuth->getByForeignKey('selector', $data[0], ['limit' => 1]); + $characterAuth->getByForeignKey('selector', $data[0], ['limit' => 1], 0); - // validate "scope hash", "expire data" and "validate token" + // validate "scope hash" + // -> either "normal" scopes OR "admin" scopes + // "expire data" and "validate token" if( !$characterAuth->dry() ){ if( - $characterAuth->scopeHash === $this->getRequestedScopeHash() && + ( + $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])) ){ @@ -294,10 +301,16 @@ class Controller { // 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() && - $character->isAuthorized() - ){ + if( $character->hasUserCharacter() ){ + $authStatus = $character->isAuthorized(); + + if( + $authStatus == 'OK' || + !$checkAuthorization + ){ + $character->virtual( 'authStatus', $authStatus); + } + $characters[$name] = $character; } }else{ @@ -365,35 +378,6 @@ class Controller { return $data; } - /** - * checks whether a user/character is currently logged in - * @param \Base $f3 - * @return bool - */ - protected function checkLogTimer($f3){ - $loginCheck = false; - $characterData = $this->getSessionCharacterData(); - - if( !empty($characterData) ){ - // check logIn time - $logInTime = new \DateTime(); - $logInTime->setTimestamp( (int)$characterData['TIME'] ); - $now = new \DateTime(); - - $timeDiff = $now->diff($logInTime); - - $minutes = $timeDiff->days * 60 * 24 * 60; - $minutes += $timeDiff->h * 60; - $minutes += $timeDiff->i; - - if($minutes <= $f3->get('PATHFINDER.TIMER.LOGGED')){ - $loginCheck = true; - } - } - - return $loginCheck; - } - /** * get current character * @param int $ttl @@ -452,12 +436,32 @@ class Controller { } /** - * get a hash over all requested ESI scopes - * -> this helps to invalidate "authentication data" after scope change + * 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 getRequestedScopeHash(){ - return md5(serialize( self::getEnvironmentData('CCP_ESI_SCOPES') )); + protected function generateHashFromScopes($scopes){ + $scopes = (array)$scopes; + sort($scopes); + return md5(serialize( $scopes )); } /** diff --git a/app/main/controller/setup.php b/app/main/controller/setup.php index 25b835ac..4eee0fdf 100644 --- a/app/main/controller/setup.php +++ b/app/main/controller/setup.php @@ -121,8 +121,9 @@ class Setup extends Controller { * event handler for all "views" * some global template variables are set in here * @param \Base $f3 + * @param array $params */ - function beforeroute(\Base $f3) { + function beforeroute(\Base $f3, $params) { // page title $f3->set('pageTitle', 'Setup'); @@ -405,6 +406,7 @@ class Setup extends Controller { // server type ------------------------------------------------------------------ $serverData = self::getServerData(0); + $checkRequirements = [ 'serverType' => [ 'label' => 'Server type', @@ -429,6 +431,12 @@ class Setup extends Controller { 'version' => phpversion(), 'check' => version_compare( phpversion(), $f3->get('REQUIREMENTS.PHP.VERSION'), '>=') ], + 'php_bit' => [ + 'label' => 'php_int_size', + 'required' => ($f3->get('REQUIREMENTS.PHP.PHP_INT_SIZE') * 8 ) . '-bit', + 'version' => (PHP_INT_SIZE * 8) . '-bit', + 'check' => $f3->get('REQUIREMENTS.PHP.PHP_INT_SIZE') == PHP_INT_SIZE + ], 'pcre' => [ 'label' => 'PCRE', 'required' => $f3->get('REQUIREMENTS.PHP.PCRE_VERSION'), diff --git a/app/main/lib/config.php b/app/main/lib/config.php index d7cca1b1..00b84e9e 100644 --- a/app/main/lib/config.php +++ b/app/main/lib/config.php @@ -20,7 +20,7 @@ class Config extends \Prefab { * environment config keys that should be parsed as array * -> use "," as delimiter in config files/data */ - const ARRAY_KEYS = ['CCP_ESI_SCOPES']; + const ARRAY_KEYS = ['CCP_ESI_SCOPES', 'CCP_ESI_SCOPES_ADMIN']; /** * all environment data diff --git a/app/main/model/basicmodel.php b/app/main/model/basicmodel.php index 8b547500..135c7807 100644 --- a/app/main/model/basicmodel.php +++ b/app/main/model/basicmodel.php @@ -139,28 +139,26 @@ abstract class BasicModel extends \DB\Cortex { * @throws Exception\ValidationException */ public function set($key, $val){ - if( !$this->dry() && $key != 'updated' ){ if( $this->exists($key) ){ - $currentVal = $this->get($key); + // get raw column data (no objects) + $currentVal = $this->get($key, true); - // if current value is not a relational object - // and value has changed -> update table col - if(is_object($currentVal)){ + if(is_object($val)){ if( - is_numeric($val) && - is_subclass_of($currentVal, 'Model\BasicModel') && - $currentVal->_id !== (int)$val + is_subclass_of($val, 'Model\BasicModel') && + $val->_id != $currentVal ){ + // relational object changed $this->touch('updated'); } - }elseif($currentVal != $val){ + }elseif($val != $currentVal){ + // non object value $this->touch('updated'); } - } } @@ -600,6 +598,16 @@ abstract class BasicModel extends \DB\Cortex { return true; } + /** + * format dateTime column + * @param $column + * @param string $format + * @return false|null|string + */ + public function getFormattedColumn($column, $format = 'Y-m-d H:i'){ + return $this->get($column) ? date($format, strtotime( $this->get($column) )) : null;; + } + /** * export and download table data as *.csv * this is primarily used for static tables @@ -773,8 +781,7 @@ abstract class BasicModel extends \DB\Cortex { * @param string $text * @param string $type */ - public static function log($text, $type = null){ - $type = isset($type) ? $type : 'DEBUG'; + public static function log($text, $type = 'DEBUG'){ Controller\LogController::getLogger($type)->write($text); } diff --git a/app/main/model/charactermodel.php b/app/main/model/charactermodel.php index 52546d04..e8b12083 100644 --- a/app/main/model/charactermodel.php +++ b/app/main/model/charactermodel.php @@ -21,6 +21,34 @@ class CharacterModel extends BasicModel { */ const DATA_CACHE_KEY_LOG = 'LOG'; + /** + * character authorization status + * @var array + */ + const AUTHORIZATION_STATUS = [ + 'OK' => true, // success + 'UNKNOWN' => 'error', // general authorization error + 'CORPORATION' => 'failed to match corporation whitelist', + 'ALLIANCE' => 'failed to match alliance whitelist', + 'KICKED' => 'character is kicked', + 'BANNED' => 'character is banned' + ]; + + /** + * enables change for "kicked" column + * -> see kick(); + * @var bool + */ + private $allowKickChange = false; + + /** + * enables change for "banned" column + * -> see ban(); + * @var bool + */ + private $allowBanChange = false; + + protected $fieldConf = [ 'lastLogin' => [ 'type' => Schema::DT_TIMESTAMP, @@ -75,6 +103,20 @@ class CharacterModel extends BasicModel { ] ] ], + 'roleId' => [ + 'type' => Schema::DT_TINYINT, + 'nullable' => false, + 'default' => 0, + 'index' => true + ], + 'kicked' => [ + 'type' => Schema::DT_TIMESTAMP, + 'index' => true + ], + 'banned' => [ + 'type' => Schema::DT_TIMESTAMP, + 'index' => true + ], 'shared' => [ 'type' => Schema::DT_BOOL, 'nullable' => false, @@ -125,9 +167,14 @@ class CharacterModel extends BasicModel { $characterData = (object) []; $characterData->id = $this->id; $characterData->name = $this->name; + $characterData->roleId = $this->roleId; $characterData->shared = $this->shared; $characterData->logLocation = $this->logLocation; + if( $this->authStatus ){ + $characterData->authStatus = $this->authStatus; + } + if($addCharacterLogData){ if($logModel = $this->getLog()){ $characterData->log = $logModel->getData(); @@ -187,6 +234,58 @@ class CharacterModel extends BasicModel { return $accessToken; } + /** + * setter for "kicked" until time + * @param bool|int $minutes + * @return mixed + */ + public function set_kicked($minutes){ + if($this->allowKickChange){ + // allowed to set/change -> reset "allowed" property + $this->allowKickChange = false; + $kicked = null; + + if($minutes){ + $seconds = $minutes * 60; + $timezone = new \DateTimeZone( self::getF3()->get('TZ') ); + $kickedUntil = new \DateTime('now', $timezone); + + // add cookie expire time + $kickedUntil->add(new \DateInterval('PT' . $seconds . 'S')); + $kicked = $kickedUntil->format('Y-m-d H:i:s'); + } + }else{ + // not allowed to set/change -> keep current status + $kicked = $this->kicked; + } + + return $kicked; + } + + /** + * setter for "banned" status + * @param bool|int $status + * @return mixed + */ + public function set_banned($status){ + if($this->allowBanChange){ + // allowed to set/change -> reset "allowed" property + $this->allowBanChange = false; + $banned = null; + + if($status){ + $timezone = new \DateTimeZone( self::getF3()->get('TZ') ); + $bannedSince = new \DateTime('now', $timezone); + $banned = $bannedSince->format('Y-m-d H:i:s'); + } + }else{ + // not allowed to set/change -> keep current status + $banned = $this->banned; + } + + return $banned; + } + /** * logLocation specifies whether the current system should be tracked or not * @param $logLocation @@ -205,6 +304,30 @@ class CharacterModel extends BasicModel { return $logLocation; } + /** + * kick character for $minutes + * -> do NOT use $this->kicked! + * -> this will not work (prevent abuse) + * @param bool|int $minutes + */ + public function kick($minutes = false){ + // enables "kicked" change for this model + $this->allowKickChange = true; + $this->kicked = $minutes; + } + + /** + * ban character + * -> do NOT use $this->banned! + * -> this will not work (prevent abuse) + * @param bool|int $status + */ + public function ban($status = false){ + // enables "banned" change for this model + $this->allowBanChange = true; + $this->banned = $status; + } + /** * Event "Hook" function * @param self $self @@ -317,7 +440,7 @@ class CharacterModel extends BasicModel { !empty($this->crestAccessToken) && !empty($this->crestAccessTokenUpdated) ){ - $timezone = new \DateTimeZone( $this->getF3()->get('TZ') ); + $timezone = new \DateTimeZone( self::getF3()->get('TZ') ); $tokenTime = \DateTime::createFromFormat( 'Y-m-d H:i:s', $this->crestAccessTokenUpdated, @@ -357,46 +480,112 @@ class CharacterModel extends BasicModel { return $accessToken; } + /** + * check if character is currently kicked + * @return bool + */ + public function isKicked(){ + $kicked = false; + if( !is_null($this->kicked) ){ + $kickedUntil = new \DateTime(); + $kickedUntil->setTimestamp( (int)strtotime($this->kicked) ); + $now = new \DateTime(); + $kicked = ($kickedUntil > $now); + } + + return $kicked; + } + /** * checks whether this character is authorized to log in * -> check corp/ally whitelist config (pathfinder.ini) * @return bool */ public function isAuthorized(){ - $isAuthorized = false; - $f3 = self::getF3(); + $authStatus = 'UNKNOWN'; - $whitelistCorporations = array_filter( array_map('trim', (array)$f3->get('PATHFINDER.LOGIN.CORPORATION') ) ); - $whitelistAlliance = array_filter( array_map('trim', (array)$f3->get('PATHFINDER.LOGIN.ALLIANCE') ) ); + // check whether character is banned or temp kicked + if(is_null($this->banned)){ + if( !$this->isKicked() ){ + $f3 = self::getF3(); + $whitelistCorporations = array_filter( array_map('trim', (array)$f3->get('PATHFINDER.LOGIN.CORPORATION') ) ); + $whitelistAlliance = array_filter( array_map('trim', (array)$f3->get('PATHFINDER.LOGIN.ALLIANCE') ) ); - if( - empty($whitelistCorporations) && - empty($whitelistAlliance) - ){ - // no corp/ally restrictions set -> any character is allowed to login - $isAuthorized = true; - }else{ - // check if character corporation is set in whitelist - if( - !empty($whitelistCorporations) && - $this->hasCorporation() && - in_array((int)$this->get('corporationId', true), $whitelistCorporations) - ){ - $isAuthorized = true; + if( + empty($whitelistCorporations) && + empty($whitelistAlliance) + ){ + // no corp/ally restrictions set -> any character is allowed to login + $authStatus = 'OK'; + }else{ + // check if character corporation is set in whitelist + if( + !empty($whitelistCorporations) && + $this->hasCorporation() && + in_array((int)$this->get('corporationId', true), $whitelistCorporations) + ){ + $authStatus = 'OK'; + }else{ + $authStatus = 'CORPORATION'; + } + + // check if character alliance is set in whitelist + if( + !$authStatus && + !empty($whitelistAlliance) && + $this->hasAlliance() && + in_array((int)$this->get('allianceId', true), $whitelistAlliance) + ){ + $authStatus = 'OK'; + }else{ + $authStatus = 'ALLIANCE'; + } + } + }else{ + $authStatus = 'KICKED'; } + }else{ + $authStatus = 'BANNED'; + } - // check if character alliance is set in whitelist - if( - !$isAuthorized && - !empty($whitelistAlliance) && - $this->hasAlliance() && - in_array((int)$this->get('allianceId', true), $whitelistAlliance) - ){ - $isAuthorized = true; + return $authStatus; + } + + /** + * get pathfinder roleId + * @return int + */ + public function requestRoleId(){ + $roleId = 0; + $rolesData = $this->requestRoles(); + if( !empty($rolesData) ){ + // roles that grant admin access for this character + $adminRoles = array_intersect(CorporationModel::ADMIN_ROLES, $rolesData); + if( !empty($adminRoles) ){ + $roleId = 1; } } - return $isAuthorized; + return $roleId; + } + + /** + * request all corporation roles granted to this character + * @return array + */ + 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]; + } + } + } + + return $rolesData; } /** diff --git a/app/main/model/corporationmodel.php b/app/main/model/corporationmodel.php index 240f8158..35de4b83 100644 --- a/app/main/model/corporationmodel.php +++ b/app/main/model/corporationmodel.php @@ -14,6 +14,72 @@ class CorporationModel extends BasicModel { protected $table = 'corporation'; + /** + * all available corp roles EVE has + * -> a corp member has granted roles 0 up to all roles + */ + const CCP_ROLES = [ + 'director', + 'personnel_manager', + 'accountant', + 'security_officer', + 'factory_manager', + 'station_manager', + 'auditor', + 'hangar_take_1', + 'hangar_take_2', + 'hangar_take_3', + 'hangar_take_4', + 'hangar_take_5', + 'hangar_take_6', + 'hangar_take_7', + 'hangar_query_1', + 'hangar_query_2', + 'hangar_query_3', + 'hangar_query_4', + 'hangar_query_5', + 'hangar_query_6', + 'hangar_query_7', + 'account_take_1', + 'account_take_2', + 'account_take_3', + 'account_take_4', + 'account_take_5', + 'account_take_6', + 'account_take_7', + 'diplomat', + 'config_equipment', + 'container_take_1', + 'container_take_2', + 'container_take_3', + 'container_take_4', + 'container_take_5', + 'container_take_6', + 'container_take_7', + 'rent_office', + 'rent_factory_facility', + 'rent_research_facility', + 'junior_accountant', + 'config_starbase_equipment', + 'trader', + 'communications_officer', + 'contract_manager', + 'starbase_defense_operator', + 'starbase_fuel_technician', + 'fitting_manager', + 'terrestrial_combat_officer', + 'terrestrial_logistics_officer' + ]; + + /** + * corp roles that give admin access for a corp + */ + const ADMIN_ROLES = [ + 'director', + 'personnel_manager', + 'security_officer' + ]; + protected $fieldConf = [ 'active' => [ 'type' => Schema::DT_BOOL, @@ -60,7 +126,7 @@ class CorporationModel extends BasicModel { /** * get all maps for this corporation - * @return array|mixed + * @return MapModel[] */ public function getMaps(){ $maps = []; @@ -90,12 +156,19 @@ class CorporationModel extends BasicModel { /** * get all characters in this corporation - * @return array + * @param array $characterIds + * @return CharacterModel[] */ - public function getCharacters(){ + public function getCharacters( $characterIds = []){ $characters = []; + $filter = ['active = ?', 1]; - $this->filter('corporationCharacters', ['active = ?', 1]); + if( !empty($characterIds) ){ + $filter[0] .= ' AND id IN (?)'; + $filter[] = $characterIds; + } + + $this->filter('corporationCharacters', $filter); if($this->corporationCharacters){ foreach($this->corporationCharacters as $character){ @@ -105,4 +178,22 @@ class CorporationModel extends BasicModel { return $characters; } + + /** + * get roles for each character in this corp + * -> CCP API call + * @param string $accessToken + * @return array + */ + public function getCharactersRoles($accessToken){ + $characterRolesData = []; + if( + !empty($accessToken) && + !$this->isNPC + ){ + $characterRolesData = self::getF3()->ccpClient->getCorporationRoles($this->_id, $accessToken); + } + + return $characterRolesData; + } } \ No newline at end of file diff --git a/app/pathfinder.ini b/app/pathfinder.ini index 14a9dd0d..6e80088a 100644 --- a/app/pathfinder.ini +++ b/app/pathfinder.ini @@ -39,6 +39,7 @@ ALLIANCE = INDEX = templates/view/index.html SETUP = templates/view/setup.html LOGIN = templates/view/login.html +ADMIN = templates/view/admin.html ; HTTP status pages =============================================================================== [PATHFINDER.STATUS] @@ -156,6 +157,8 @@ SESSION_SUSPECT = session_suspect DELETE_ACCOUNT = account_delete ; unauthorized request (HTTP 401) UNAUTHORIZED = unauthorized +; admin action (e.g. kick, bann) log +ADMIN = admin ; TCP socket errors SOCKET_ERROR = socket_error ; debug log for development diff --git a/app/requirements.ini b/app/requirements.ini index 10bd9834..edb84ac4 100644 --- a/app/requirements.ini +++ b/app/requirements.ini @@ -12,6 +12,9 @@ NGINX.VERSION = 1.9 ; recommended is >= 5.6 VERSION = 7.0 +; 64-bit version of PHP (4 = 32-bit, 8 = 64-bit) +PHP_INT_SIZE = 8 + ; "Perl-Compatible Regular Expressions" ; usually shipped with PHP package, ; but needs to be additionally updated on CentOS or Red Hat systems diff --git a/app/routes.ini b/app/routes.ini index e139be66..22d469d4 100644 --- a/app/routes.ini +++ b/app/routes.ini @@ -10,6 +10,8 @@ GET @login: / [sync] = Controller\AppContro GET @sso: /sso/@action [sync] = Controller\Ccp\Sso->@action ; map page GET @map: /map [sync] = Controller\MapController->init +; admin panel +GET @admin: /admin* [sync] = Controller\Admin->dispatch ; ajax wildcard APIs (throttled) GET|POST /api/@controller/@action [ajax] = Controller\Api\@controller->@action, 0, 512 diff --git a/js/app/init.js b/js/app/init.js index e9f76993..f0826162 100644 --- a/js/app/init.js +++ b/js/app/init.js @@ -53,8 +53,8 @@ define(['jquery'], function($) { gitHubReleases: 'api/github/releases' // ajax URL - get release info from GitHub }, url: { - ccpImageServer: 'https://image.eveonline.com/', // CCP image Server - zKillboard: 'https://zkillboard.com/api/' // killboard api + ccpImageServer: '//image.eveonline.com/', // CCP image Server + zKillboard: '//zkillboard.com/api/' // killboard api }, breakpoints: [ { name: 'desktop', width: Infinity }, diff --git a/js/app/login.js b/js/app/login.js index 1c588d76..f639fed4 100644 --- a/js/app/login.js +++ b/js/app/login.js @@ -65,8 +65,9 @@ define([ // notification panel notificationPanelId: 'pf-notification-panel', // id for "notification panel" (e.g. last update information) - // server panel - serverPanelId: 'pf-server-panel', // id for EVE Online server status panel + // sticky panel + stickyPanelClass: 'pf-landing-sticky-panel', // class for sticky panels + stickyPanelServerId: 'pf-landing-server-panel', // id for EVE Online server status panel // animation animateElementClass: 'pf-animate-on-visible', // class for elements that will be animated to show @@ -376,14 +377,14 @@ define([ $('.youtube').each(function() { // Based on the YouTube ID, we can easily find the thumbnail image - $(this).css('background-image', 'url(https://i.ytimg.com/vi/' + this.id + '/sddefault.jpg)'); + $(this).css('background-image', 'url(//i.ytimg.com/vi/' + this.id + '/sddefault.jpg)'); // Overlay the Play icon to make it look like a video player $(this).append($('
', {'class': 'play'})); $(document).delegate('#' + this.id, 'click', function() { // Create an iFrame with autoplay set to true - let iFrameUrl = 'https://www.youtube.com/embed/' + this.id + '?autoplay=1&autohide=1'; + let iFrameUrl = '//www.youtube.com/embed/' + this.id + '?autoplay=1&autohide=1'; if ( $(this).data('params') ){ iFrameUrl += '&'+$(this).data('params'); } @@ -467,7 +468,8 @@ define([ if(responseData.hasOwnProperty('status')){ let data = responseData.status; - data.serverPanelId = config.serverPanelId; + data.stickyPanelServerId = config.stickyPanelServerId; + data.stickyPanelClass = config.stickyPanelClass; let statusClass = ''; switch(data.serviceStatus.toLowerCase()){ @@ -483,7 +485,7 @@ define([ requirejs(['text!templates/ui/server_panel.html', 'mustache'], function(template, Mustache) { let content = Mustache.render(template, data); $('#' + config.headerId).prepend(content); - $('#' + config.serverPanelId).velocity('transition.slideLeftBigIn', { + $('#' + config.stickyPanelServerId).velocity('transition.slideLeftBigIn', { duration: 240 }); }); @@ -612,6 +614,25 @@ define([ }); }; + // -------------------------------------------------------------------- + + let getCharacterAuthLabel = (authStatus) => { + let label = ''; + switch(authStatus){ + case 'UNKNOWN': + label = 'ERROR'; + break; + case 'CORPORATION': + case 'ALLIANCE': + label = 'INVALID'; + break; + default: + label = authStatus; + break; + } + return label; + }; + // -------------------------------------------------------------------- // request character data for each character panel requirejs(['text!templates/ui/character_panel.html', 'mustache'], function(template, Mustache){ @@ -653,7 +674,9 @@ define([ let data = { link: this.characterElement.data('href'), cookieName: this.cookieName, - character: responseData.character + character: responseData.character, + authLabel: getCharacterAuthLabel(responseData.character.authStatus), + authOK: responseData.character.authStatus === 'OK' }; let content = Mustache.render(template, data); @@ -801,6 +824,38 @@ define([ }); }, false); + + require([ + 'datatables.net', + 'datatables.net-buttons', + 'datatables.net-buttons-html', + 'datatables.net-responsive', + 'datatables.net-select' + ], function (startup) { + let systemsDataTable = $('.dataTable').dataTable( { + pageLength: 100, + paging: true, + // lengthMenu: [[5, 10, 20, 50, -1], [5, 10, 20, 50, 'All']], + ordering: true, + // order: [[ 9, 'desc' ], [ 3, 'asc' ]], + autoWidth: false, + // responsive: { + // breakpoints: Init.breakpoints, + // details: false + // }, + hover: false, + //data: systemsData, + columnDefs: [], + language: { + emptyTable: 'No members', + zeroRecords: 'No members found', + lengthMenu: 'Show _MENU_ members', + info: 'Showing _START_ to _END_ of _TOTAL_ members' + } + }); + }); + + }); diff --git a/js/app/ui/header.js b/js/app/ui/header.js index 1dd7bfb0..17f715b1 100644 --- a/js/app/ui/header.js +++ b/js/app/ui/header.js @@ -9,22 +9,22 @@ define([ ], function($) { 'use strict'; - var config = { + let config = { previewElementClass: 'pf-header-preview-element' // class for "preview" elements }; - var width, height, largeHeader, canvas, ctx, points, target, animateHeader = true; + let width, height, largeHeader, canvas, ctx, points, target, animateHeader = true; - var canvasHeight = 450; - var colorRGB = '108, 174, 173'; - var connectionCount = 4; + let canvasHeight = 355; + let colorRGB = '108, 174, 173'; + let connectionCount = 4; - var initHeader = function() { + let initHeader = function() { width = window.innerWidth; height = canvasHeight; - target = {x: width * 0.8, y: 230}; + target = {x: width * 1, y: 230}; largeHeader.style.height = height+'px'; @@ -34,24 +34,24 @@ define([ // create points points = []; - for(var x = 0; x < width; x = x + width/20) { - for(var y = 0; y < height; y = y + height/15) { - var px = x + Math.random()*width/15; - var py = y + Math.random()*height/15; - var p = {x: px, originX: px, y: py, originY: py }; + for(let x = 0; x < width; x = x + width/20) { + for(let y = 0; y < height; y = y + height/15) { + let px = x + Math.random()*width/15; + let py = y + Math.random()*height/15; + let p = {x: px, originX: px, y: py, originY: py }; points.push(p); } } // for each point find the 5 closest points - for(var i = 0; i < points.length; i++) { - var closest = []; - var p1 = points[i]; - for(var j = 0; j < points.length; j++) { - var p2 = points[j]; + for(let i = 0; i < points.length; i++) { + let closest = []; + let p1 = points[i]; + for(let j = 0; j < points.length; j++) { + let p2 = points[j]; if(p1 !== p2) { - var placed = false; - for(var k = 0; k < connectionCount; k++) { + let placed = false; + for(let k = 0; k < connectionCount; k++) { if(!placed) { if(closest[k] === undefined) { closest[k] = p2; @@ -60,7 +60,7 @@ define([ } } - for(var m = 0; m < connectionCount; m++) { + for(let m = 0; m < connectionCount; m++) { if(!placed) { if(getDistance(p1, p2) < getDistance(p1, closest[m])) { closest[m] = p2; @@ -74,14 +74,14 @@ define([ } // assign a circle to each point - for(var n in points) { - var c = new Circle(points[n], 2+Math.random()*2, 'rgba(255,255,255,0.3)'); + for(let n in points) { + let c = new Circle(points[n], 2+Math.random()*2, 'rgba(255,255,255,0.3)'); points[n].circle = c; } }; // Event handling - var addListeners = function() { + let addListeners = function() { if(!('ontouchstart' in window)) { window.addEventListener('mousemove', mouseMove); } @@ -89,9 +89,9 @@ define([ window.addEventListener('resize', resize); }; - var mouseMove = function(e) { - var posx = 0; - var posy = 0; + let mouseMove = function(e) { + let posx = 0; + let posy = 0; if (e.pageX || e.pageY) { posx = e.pageX; posy = e.pageY; @@ -103,7 +103,7 @@ define([ target.y = posy; }; - var scrollCheck = function() { + let scrollCheck = function() { if(document.body.scrollTop > height){ animateHeader = false; }else{ @@ -111,7 +111,7 @@ define([ } }; - var resize = function() { + let resize = function() { width = window.innerWidth; height = canvasHeight; largeHeader.style.height = height+'px'; @@ -120,17 +120,17 @@ define([ }; // animation - var initAnimation = function() { + let initAnimation = function() { animate(); - for(var i in points) { + for(let i in points) { shiftPoint(points[i]); } }; - var animate = function animate() { + let animate = function animate() { if(animateHeader) { ctx.clearRect(0,0,width,height); - for(var i in points) { + for(let i in points) { // detect points in range if(Math.abs(getDistance(target, points[i])) < 4000) { points[i].active = 0.25; @@ -153,7 +153,7 @@ define([ requestAnimationFrame(animate); }; - var shiftPoint = function (p) { + let shiftPoint = function (p) { TweenLite.to(p, 1 + 1 * Math.random(), {x: p.originX - 50 + Math.random() * 100, y: p.originY - 50 + Math.random() * 100, ease: Circ.easeInOut, onComplete: function () { @@ -162,9 +162,9 @@ define([ }; // Canvas manipulation - var drawLines = function (p) { + let drawLines = function (p) { if(!p.active) return; - for(var i in p.closest) { + for(let i in p.closest) { ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p.closest[i].x, p.closest[i].y); @@ -173,8 +173,8 @@ define([ } }; - var Circle = function(pos,rad,color) { - var _this = this; + let Circle = function(pos,rad,color) { + let _this = this; // constructor (function() { @@ -193,7 +193,7 @@ define([ }; // Util - var getDistance = function(p1, p2) { + let getDistance = function(p1, p2) { return Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2); }; diff --git a/js/app/ui/system_killboard.js b/js/app/ui/system_killboard.js index 10384024..32913f02 100644 --- a/js/app/ui/system_killboard.js +++ b/js/app/ui/system_killboard.js @@ -6,7 +6,7 @@ define([ ], function($, Init, Util, Morris) { 'use strict'; - var config = { + let config = { // module info moduleClass: 'pf-module', // class for each module @@ -23,10 +23,9 @@ define([ systemKillboardListImgShip: 'pf-system-killboard-img-ship', // class for all ship images systemKillboardListImgAlly: 'pf-system-killboard-img-ally', // class for all alliance logos systemKillboardListImgCorp: 'pf-system-killboard-img-corp' // class for all corp logos - }; - var cache = { + let cache = { systemKillsGraphData: {} // data for system kills info graph }; @@ -35,8 +34,8 @@ define([ * @param text * @returns {*|XMLList} */ - var getLabel = function(text, options){ - var label = $('', { + let getLabel = function(text, options){ + let label = $('', { class: ['label', options.type, options.align].join(' ') }).text( text ); @@ -44,16 +43,16 @@ define([ }; - var showKillmails = function(moduleElement, killboardData){ + let showKillmails = function(moduleElement, killboardData){ // show number of killMails - var killMailCounterMax = 20; - var killMailCounter = 0; + let killMailCounterMax = 20; + let killMailCounter = 0; // change order (show right to left) killboardData.tableData.reverse(); - for(var i = 0; i < killboardData.tableData.length; i++){ + for(let i = 0; i < killboardData.tableData.length; i++){ // check if killMails exist in this hour if(killboardData.tableData[i].killmails){ @@ -64,43 +63,43 @@ define([ moduleElement.append( $('
').text(i + 'h ago')); - var killMailData = killboardData.tableData[i].killmails; + let killMailData = killboardData.tableData[i].killmails; - var listeElement = $('